New in 0.7
Composed components
Drawn with silverpoint's own pieces
@silverpoint/grounds/ui.css draws its hand-inked frames, hatched tones and heightening on any element that follows its markup contract, so these components are drawn with exactly the same pieces as the first 17. silverpoint 0.8.0 ships them in React, Vue and Angular, on @silverpoint/<framework>/ui/<name> beside the others (38 UI components in all); SpSurface and SpGlyph (sp_surface, sp_glyph) are the building blocks, for components of your own.
trigger becomes an sp_trigger, a native button that keeps the ARIA the overlay clones onto it.Every demo on this page follows these controls:
sp_surface
The root contract on any element: the ground's class, a hand-drawn frame (control, pill, box, card or round) and a hatched tone (1–4, or a state such as selected or primary). SpSurface reference →
import { SpSurface } from '@silverpoint/react/ui/surface';
<SpSurface frame="card">A card frame</SpSurface>
<SpSurface frame="pill" tone={2}>A pill, toned 2</SpSurface>
<SpSurface frame="control" tone="selected">Selected</SpSurface>
<SpSurface as="span" frame="round" tone={4}>4</SpSurface> {/* as: the HTML tag, div by default */}<script setup lang="ts">
import { SpSurface } from '@silverpoint/vue/ui/surface';
</script>
<template>
<SpSurface frame="card">A card frame</SpSurface>
<SpSurface frame="pill" :tone="2">A pill, toned 2</SpSurface>
<SpSurface frame="control" tone="selected">Selected</SpSurface>
<SpSurface as="span" frame="round" :tone="4">4</SpSurface>
</template>import { Component } from '@angular/core';
import { SpSurface } from '@silverpoint/angular/ui/surface';
// [spSurface] goes on your own element: it is the root, as `as` names it in React and Vue.
@Component({
selector: 'app-surfaces',
imports: [SpSurface],
template: `
<div spSurface frame="card">A card frame</div>
<div spSurface frame="pill" [tone]="2">A pill, toned 2</div>
<div spSurface frame="control" tone="selected">Selected</div>
<span spSurface frame="round" [tone]="4">4</span>
`,
})
export class Surfaces {}sp_surface(rx.el.span("A card frame"), frame="card")
sp_surface(rx.el.span("A pill, toned 2"), frame="pill", tone=2)
sp_surface(rx.el.span("Selected"), frame="control", tone="selected")
sp_surface("4", frame="round", tone=4, element="span") # element: the HTML tag (div by default)sp_glyph
One exact 16×16 mark of UI_GLYPHS — tick, cross, chevrons, arrows, sort marks… — stroked with the text colour, never inked. SpGlyph reference →
import { SpGlyph } from '@silverpoint/react/ui/glyph';
<SpGlyph glyph="tick" />
<SpGlyph glyph="chevronDown" />
<SpGlyph glyph="sort" /> {/* any key of UI_GLYPHS, from '@silverpoint/core/ui' */}<script setup lang="ts">
import { SpGlyph } from '@silverpoint/vue/ui/glyph';
</script>
<template>
<SpGlyph glyph="tick" />
<SpGlyph glyph="chevronDown" />
<SpGlyph glyph="sort" />
</template>import { Component } from '@angular/core';
import { SpGlyph } from '@silverpoint/angular/ui/glyph';
@Component({
selector: 'app-marks',
imports: [SpGlyph],
template: `
<sp-glyph glyph="tick" />
<sp-glyph glyph="chevronDown" />
<sp-glyph glyph="sort" />
`,
})
export class Marks {}sp_glyph(glyph="tick")
sp_glyph(glyph="chevronDown")
sp_glyph(glyph="sort") # tick, dash, cross, warning, dot, lozenge, info, plus, chevron…, arrow…, sort…, ellipsissp_table · sort_rows
Card-framed table: small-caps header, tabular figures, hatched striping, sortable headers with aria-sort. SpTable reference →
import { useState } from 'react';
import { uiSortRows } from '@silverpoint/core/ui';
import { SpTable } from '@silverpoint/react/ui/table';
const columns = [
{ key: 'team', title: 'Team', sortable: true },
{ key: 'region', title: 'Region' },
{ key: 'load', title: 'Load %', align: 'end', sortable: true },
{ key: 'incidents', title: 'Incidents', align: 'end', sortable: true },
] as const;
export function Teams({ teams }: { teams: Record<string, unknown>[] }) {
const [sortKey, setSortKey] = useState('load');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const sort = (key: string) => {
setSortOrder(key === sortKey && sortOrder === 'asc' ? 'desc' : 'asc');
setSortKey(key);
};
return (
<SpTable
caption="On-call teams"
columns={columns}
data={uiSortRows(teams, sortKey, sortOrder)} // the table draws rows as given
rowKey="team"
sortKey={sortKey}
sortOrder={sortOrder}
onSort={sort}
striped
/>
);
}<script setup lang="ts">
import { computed, ref } from 'vue';
import { uiSortRows } from '@silverpoint/core/ui';
import { SpTable } from '@silverpoint/vue/ui/table';
const props = defineProps<{ teams: Record<string, unknown>[] }>();
const columns = [
{ key: 'team', title: 'Team', sortable: true },
{ key: 'region', title: 'Region' },
{ key: 'load', title: 'Load %', align: 'end', sortable: true },
{ key: 'incidents', title: 'Incidents', align: 'end', sortable: true },
] as const;
const sortKey = ref('load');
const sortOrder = ref<'asc' | 'desc'>('desc');
const rows = computed(() => uiSortRows(props.teams, sortKey.value, sortOrder.value));
function sort(key: string) {
sortOrder.value = key === sortKey.value && sortOrder.value === 'asc' ? 'desc' : 'asc';
sortKey.value = key;
}
</script>
<template>
<SpTable caption="On-call teams" :columns="columns" :data="rows" row-key="team"
:sort-key="sortKey" :sort-order="sortOrder" striped @sort="sort" />
</template>import { Component, computed, input, signal } from '@angular/core';
import { uiSortRows } from '@silverpoint/core/ui';
import { SpTable } from '@silverpoint/angular/ui/table';
@Component({
selector: 'app-teams',
imports: [SpTable],
template: `
<sp-table caption="On-call teams" [columns]="columns" [data]="rows()" rowKey="team"
[sortKey]="sortKey()" [sortOrder]="sortOrder()" [striped]="true" (sort)="sort($event)" />
`,
})
export class Teams {
readonly teams = input.required<Record<string, unknown>[]>();
protected readonly columns = [
{ key: 'team', title: 'Team', sortable: true },
{ key: 'region', title: 'Region' },
{ key: 'load', title: 'Load %', align: 'end', sortable: true },
{ key: 'incidents', title: 'Incidents', align: 'end', sortable: true },
] as const;
protected readonly sortKey = signal('load');
protected readonly sortOrder = signal<'asc' | 'desc'>('desc');
protected readonly rows = computed(() => uiSortRows(this.teams(), this.sortKey(), this.sortOrder()));
protected sort(key: string) {
this.sortOrder.set(key === this.sortKey() && this.sortOrder() === 'asc' ? 'desc' : 'asc');
this.sortKey.set(key);
}
}class State(rx.State):
rows: list[dict] = TEAMS
sort_key: str = "load"
sort_order: str = "desc"
@rx.event
def sort(self, key: str):
same = key == self.sort_key and self.sort_order == "asc"
self.sort_key, self.sort_order = key, "desc" if same else "asc"
self.rows = sort_rows(TEAMS, self.sort_key, self.sort_order)
sp_table(
[
{"key": "team", "title": "Team", "sortable": True},
{"key": "region", "title": "Region"},
{"key": "load", "title": "Load %", "align": "end", "sortable": True},
{"key": "incidents", "title": "Incidents", "align": "end", "sortable": True},
],
State.rows,
caption="On-call teams",
sort_key=State.sort_key,
sort_order=State.sort_order,
on_sort=State.sort, # receives the column key
striped=True,
)sp_pagination
Previous and next, first and last, a window around the current page (heightened); on_change(page). SpPagination reference →
import { useState } from 'react';
import { SpPagination } from '@silverpoint/react/ui/pagination';
export function Pages() {
const [page, setPage] = useState(3);
// total: items in all pages; pageSize defaults to 10. onChange receives the page number.
return <SpPagination total={240} current={page} onChange={setPage} showTotal />;
}<script setup lang="ts">
import { ref } from 'vue';
import { SpPagination } from '@silverpoint/vue/ui/pagination';
const page = ref(3);
</script>
<template>
<SpPagination v-model:current="page" :total="240" show-total />
</template>import { Component, signal } from '@angular/core';
import { SpPagination } from '@silverpoint/angular/ui/pagination';
@Component({
selector: 'app-pages',
imports: [SpPagination],
template: `<sp-pagination [(current)]="page" [total]="240" [showTotal]="true" />`,
})
export class Pages {
protected readonly page = signal(3);
}sp_pagination(
total=240, # items in all pages; page_size=10 by default
current=State.page,
on_change=State.set_page, # receives the page number
show_total=True, # "21–30 of 240"
)sp_breadcrumb
A <nav> of links separated by hairline slashes; the last item is the current page. SpBreadcrumb reference →
import { SpBreadcrumb } from '@silverpoint/react/ui/breadcrumb';
const trail = [
{ label: 'Docs', href: '/docs' },
{ label: 'Components', href: '/components' },
{ label: 'Composed components', href: '/docs/composed' }, // the last item is the current page
];
<SpBreadcrumb items={trail} /><script setup lang="ts">
import { SpBreadcrumb } from '@silverpoint/vue/ui/breadcrumb';
const trail = [
{ label: 'Docs', href: '/docs' },
{ label: 'Components', href: '/components' },
{ label: 'Composed components', href: '/docs/composed' },
];
</script>
<template>
<SpBreadcrumb :items="trail" />
</template>import { Component } from '@angular/core';
import { SpBreadcrumb } from '@silverpoint/angular/ui/breadcrumb';
@Component({
selector: 'app-trail',
imports: [SpBreadcrumb],
template: `<sp-breadcrumb [items]="trail" />`,
})
export class Trail {
protected readonly trail = [
{ label: 'Docs', href: '/docs' },
{ label: 'Components', href: '/components' },
{ label: 'Composed components', href: '/docs/composed' },
];
}sp_breadcrumb(
[
{"label": "Docs", "href": "/docs"},
{"label": "Components", "href": "/components"},
{"label": "Composed components", "href": "/docs/composed"},
]
)sp_avatar · sp_avatar_group
A portrait or the person's initials in a drawn round (or square) frame; groups overlap. SpAvatar reference →
import { SpAvatar, SpAvatarGroup } from '@silverpoint/react/ui/avatar';
<SpAvatar name="Ernesto Crespo" size="lg" /> {/* initials when there is no src */}
<SpAvatar name="Leonardo da Vinci" shape="square" />
<SpAvatar src="/portrait.png" name="Anna Atkins" />
<SpAvatarGroup label="Draughtsmen">
<SpAvatar name="Albrecht Dürer" size="sm" />
<SpAvatar name="Raphael Sanzio" size="sm" />
</SpAvatarGroup><script setup lang="ts">
import { SpAvatar, SpAvatarGroup } from '@silverpoint/vue/ui/avatar';
</script>
<template>
<SpAvatar name="Ernesto Crespo" size="lg" />
<SpAvatar name="Leonardo da Vinci" shape="square" />
<SpAvatarGroup label="Draughtsmen">
<SpAvatar name="Albrecht Dürer" size="sm" />
<SpAvatar name="Raphael Sanzio" size="sm" />
</SpAvatarGroup>
</template>import { Component } from '@angular/core';
import { SpAvatar, SpAvatarGroup } from '@silverpoint/angular/ui/avatar';
@Component({
selector: 'app-people',
imports: [SpAvatar, SpAvatarGroup],
template: `
<sp-avatar name="Ernesto Crespo" size="lg" />
<sp-avatar name="Leonardo da Vinci" shape="square" />
<sp-avatar-group label="Draughtsmen">
<sp-avatar name="Albrecht Dürer" size="sm" />
<sp-avatar name="Raphael Sanzio" size="sm" />
</sp-avatar-group>
`,
})
export class People {}sp_avatar(name="Ernesto Crespo", size="lg") # initials when there is no src
sp_avatar(name="Leonardo da Vinci", shape="square")
sp_avatar(src="/portrait.png", name="Anna Atkins")
sp_avatar_group(
sp_avatar(name="Albrecht Dürer", size="sm"),
sp_avatar(name="Raphael Sanzio", size="sm"),
label="Draughtsmen",
)sp_statistic
A small-caps title, a large tabular figure, prefix and suffix, and a trend arrow. SpStatistic reference →
import { SpStatistic } from '@silverpoint/react/ui/statistic';
<SpStatistic title="Revenue" value="48.2" prefix="$" suffix="k" trend="up" delta="+8.2 %" />
<SpStatistic title="Churn" value="2.1" suffix="%" trend="down" delta="−0.4 %" /><script setup lang="ts">
import { SpStatistic } from '@silverpoint/vue/ui/statistic';
</script>
<template>
<SpStatistic title="Revenue" value="48.2" prefix="$" suffix="k" trend="up" delta="+8.2 %" />
<SpStatistic title="Churn" value="2.1" suffix="%" trend="down" delta="−0.4 %" />
</template>import { Component } from '@angular/core';
import { SpStatistic } from '@silverpoint/angular/ui/statistic';
@Component({
selector: 'app-figures',
imports: [SpStatistic],
template: `
<sp-statistic title="Revenue" value="48.2" prefix="$" suffix="k" trend="up" delta="+8.2 %" />
<sp-statistic title="Churn" value="2.1" suffix="%" trend="down" delta="−0.4 %" />
`,
})
export class Figures {}sp_statistic("Revenue", "48.2", prefix="$", suffix="k", trend="up", delta="+8.2 %")
sp_statistic("Churn", "2.1", suffix="%", trend="down", delta="−0.4 %")sp_descriptions
A label–value grid (a <dl>), optionally card-framed; one column on narrow containers. SpDescriptions reference →
import { SpDescriptions } from '@silverpoint/react/ui/descriptions';
const release = [
{ label: 'Ground', value: 'silverpoint' },
{ label: 'Substrate', value: 'cream' },
{ label: 'Charts', value: '60' },
];
<SpDescriptions title="The release" items={release} columns={3} /><script setup lang="ts">
import { SpDescriptions } from '@silverpoint/vue/ui/descriptions';
const release = [
{ label: 'Ground', value: 'silverpoint' },
{ label: 'Substrate', value: 'cream' },
{ label: 'Charts', value: '60' },
];
</script>
<template>
<SpDescriptions title="The release" :items="release" :columns="3" />
</template>import { Component } from '@angular/core';
import { SpDescriptions } from '@silverpoint/angular/ui/descriptions';
@Component({
selector: 'app-release',
imports: [SpDescriptions],
template: `<sp-descriptions title="The release" [items]="release" [columns]="3" />`,
})
export class Release {
protected readonly release = [
{ label: 'Ground', value: 'silverpoint' },
{ label: 'Substrate', value: 'cream' },
{ label: 'Charts', value: '60' },
];
}sp_descriptions(
[
{"label": "Ground", "value": "silverpoint"},
{"label": "Substrate", "value": "cream"},
{"label": "Charts", "value": "60"},
],
title="The release",
columns=3,
)sp_timeline_list
A vertical list of events: done (hatched), current (heightened), pending and error. SpTimelineList reference →
import { SpTimelineList } from '@silverpoint/react/ui/timeline-list';
const steps = [
{ title: 'Drawn', time: '09:12', description: 'The ground is prepared.', status: 'done' },
{ title: 'Reviewed', time: '11:05', description: 'Precision mode checked.', status: 'current' },
{ title: 'Published', time: '—', description: 'Waiting for the release.', status: 'pending' },
] as const;
<SpTimelineList items={steps} label="Release steps" /><script setup lang="ts">
import { SpTimelineList } from '@silverpoint/vue/ui/timeline-list';
const steps = [
{ title: 'Drawn', time: '09:12', description: 'The ground is prepared.', status: 'done' },
{ title: 'Reviewed', time: '11:05', description: 'Precision mode checked.', status: 'current' },
{ title: 'Published', time: '—', description: 'Waiting for the release.', status: 'pending' },
] as const;
</script>
<template>
<SpTimelineList :items="steps" label="Release steps" />
</template>import { Component } from '@angular/core';
import { SpTimelineList } from '@silverpoint/angular/ui/timeline-list';
@Component({
selector: 'app-steps',
imports: [SpTimelineList],
template: `<sp-timeline-list [items]="steps" label="Release steps" />`,
})
export class Steps {
protected readonly steps = [
{ title: 'Drawn', time: '09:12', description: 'The ground is prepared.', status: 'done' },
{ title: 'Reviewed', time: '11:05', description: 'Precision mode checked.', status: 'current' },
{ title: 'Published', time: '—', description: 'Waiting for the release.', status: 'pending' },
] as const;
}sp_timeline_list(
[
{"title": "Drawn", "time": "09:12", "description": "The ground is prepared.", "status": "done"},
{"title": "Reviewed", "time": "11:05", "description": "Precision mode checked.", "status": "current"},
{"title": "Published", "time": "—", "description": "Waiting for the release.", "status": "pending"},
],
label="Release steps",
)sp_collapse · sp_collapse_item
Panels that open and close, on native <details>; accordion=True keeps one open. SpCollapse reference →
import { SpCollapse, SpCollapseItem } from '@silverpoint/react/ui/collapse';
<SpCollapse accordion> {/* native exclusive <details name> */}
<SpCollapseItem title="What is silverpoint?" defaultOpen>
<p>A fine silver line on a prepared ground.</p>
</SpCollapseItem>
<SpCollapseItem title="And cyanotype?">
<p>A white line on Prussian blue; tone is its weight.</p>
</SpCollapseItem>
</SpCollapse><script setup lang="ts">
import { SpCollapse, SpCollapseItem } from '@silverpoint/vue/ui/collapse';
</script>
<template>
<SpCollapse accordion>
<SpCollapseItem title="What is silverpoint?" default-open>
<p>A fine silver line on a prepared ground.</p>
</SpCollapseItem>
<SpCollapseItem title="And cyanotype?">
<p>A white line on Prussian blue; tone is its weight.</p>
</SpCollapseItem>
</SpCollapse>
</template>import { Component } from '@angular/core';
import { SpCollapse, SpCollapseItem } from '@silverpoint/angular/ui/collapse';
@Component({
selector: 'app-faq',
imports: [SpCollapse, SpCollapseItem],
template: `
<sp-collapse [accordion]="true">
<sp-collapse-item title="What is silverpoint?" [defaultOpen]="true">
<p>A fine silver line on a prepared ground.</p>
</sp-collapse-item>
<sp-collapse-item title="And cyanotype?">
<p>A white line on Prussian blue; tone is its weight.</p>
</sp-collapse-item>
</sp-collapse>
`,
})
export class Faq {}sp_collapse(
sp_collapse_item("What is silverpoint?", rx.el.p("A fine silver line…"), open=True),
sp_collapse_item("And cyanotype?", rx.el.p("A white line on Prussian blue…")),
accordion=True, # native exclusive <details name>
)sp_empty
The empty state: a lightly hatched, drawn tray, a line of italic text and actions. SpEmpty reference →
import { SpButton } from '@silverpoint/react/ui/button';
import { SpEmpty } from '@silverpoint/react/ui/empty';
<SpEmpty description="No charts yet" extra={<SpButton size="sm">Add a chart</SpButton>} /><script setup lang="ts">
import { SpButton } from '@silverpoint/vue/ui/button';
import { SpEmpty } from '@silverpoint/vue/ui/empty';
</script>
<template>
<SpEmpty description="No charts yet">
<template #extra><SpButton size="sm">Add a chart</SpButton></template>
</SpEmpty>
</template>import { Component } from '@angular/core';
import { SpCardExtra } from '@silverpoint/angular/ui';
import { SpButton } from '@silverpoint/angular/ui/button';
import { SpEmpty } from '@silverpoint/angular/ui/empty';
@Component({
selector: 'app-no-charts',
imports: [SpButton, SpCardExtra, SpEmpty],
template: `
<sp-empty description="No charts yet">
<ng-template spExtra><button spButton size="sm">Add a chart</button></ng-template>
</sp-empty>
`,
})
export class NoCharts {}sp_empty(sp_button("Add a chart", size="sm"), description="No charts yet")sp_result
The outcome of an operation: a drawn round mark (tick, info, warning, cross), title and subtitle. SpResult reference →
import { SpButton } from '@silverpoint/react/ui/button';
import { SpResult } from '@silverpoint/react/ui/result';
<SpResult
status="success" // success, info, warning or error
title="Published"
subtitle="Every package reached npm."
extra={<SpButton size="sm" href="/gallery">Back to the gallery</SpButton>}
/><script setup lang="ts">
import { SpButton } from '@silverpoint/vue/ui/button';
import { SpResult } from '@silverpoint/vue/ui/result';
</script>
<template>
<SpResult status="success" title="Published" subtitle="Every package reached npm.">
<template #extra><SpButton size="sm" href="/gallery">Back to the gallery</SpButton></template>
</SpResult>
</template>import { Component } from '@angular/core';
import { SpCardExtra } from '@silverpoint/angular/ui';
import { SpButton } from '@silverpoint/angular/ui/button';
import { SpResult } from '@silverpoint/angular/ui/result';
@Component({
selector: 'app-published',
imports: [SpButton, SpCardExtra, SpResult],
template: `
<sp-result status="success" title="Published" subtitle="Every package reached npm.">
<ng-template spExtra><a spButton size="sm" href="/gallery">Back to the gallery</a></ng-template>
</sp-result>
`,
})
export class Published {}sp_result(
sp_button("Back to the gallery", href="/gallery", size="sm"),
status="success", # success, info, warning or error
title="Published",
subtitle="Every package reached PyPI.",
)sp_textarea
Multi-line text on the SpInput contract: drawn frame, italic placeholder, counter and message. SpTextarea reference →
import { useState } from 'react';
import { SpTextarea } from '@silverpoint/react/ui/textarea';
export function Note() {
const [note, setNote] = useState('Drawn in silverpoint.');
// onChange receives the text, not the DOM event
return <SpTextarea label="Note" value={note} onChange={setNote} rows={3} maxLength={120} showCount />;
}<script setup lang="ts">
import { ref } from 'vue';
import { SpTextarea } from '@silverpoint/vue/ui/textarea';
const note = ref('Drawn in silverpoint.');
</script>
<template>
<SpTextarea v-model="note" label="Note" :rows="3" :max-length="120" show-count />
</template>import { Component, signal } from '@angular/core';
import { SpTextarea } from '@silverpoint/angular/ui/textarea';
@Component({
selector: 'app-note',
imports: [SpTextarea],
// or ngModel / formControlName: SpTextarea is a ControlValueAccessor
template: `<sp-textarea [(value)]="note" label="Note" [rows]="3" [maxLength]="120" [showCount]="true" />`,
})
export class Note {
protected readonly note = signal('Drawn in silverpoint.');
}sp_textarea(
label="Note",
value=State.note,
on_change=State.set_note, # receives the text, not a DOM event
rows=3,
max_length=120,
show_count=True,
)sp_input_number
A number field with exact − and + steppers, clamped to min and max; on_change receives the number. SpInputNumber reference →
import { useState } from 'react';
import { SpInputNumber } from '@silverpoint/react/ui/input-number';
export function Copies() {
const [copies, setCopies] = useState<number | null>(3); // null is an empty field
return <SpInputNumber label="Copies" value={copies} onChange={setCopies} min={1} max={10} />;
}<script setup lang="ts">
import { ref } from 'vue';
import { SpInputNumber } from '@silverpoint/vue/ui/input-number';
const copies = ref<number | null>(3);
</script>
<template>
<SpInputNumber v-model="copies" label="Copies" :min="1" :max="10" />
</template>import { Component, signal } from '@angular/core';
import { SpInputNumber } from '@silverpoint/angular/ui/input-number';
@Component({
selector: 'app-copies',
imports: [SpInputNumber],
template: `<sp-input-number [(value)]="copies" label="Copies" [min]="1" [max]="10" />`,
})
export class Copies {
protected readonly copies = signal<number | null>(3);
}sp_input_number(label="Copies", value=State.copies, on_change=State.set_copies, min=1, max=10)sp_modal
A dialog on a card frame over a hatched backdrop: focus trapped, Escape closes it. SpModal reference →
import { useState } from 'react';
import { SpButton } from '@silverpoint/react/ui/button';
import { SpModal } from '@silverpoint/react/ui/modal';
export function ApplyChanges() {
const [open, setOpen] = useState(false);
return (
<SpModal
title="Apply changes"
description="The geometry of the data does not move."
trigger={<button type="button">Open the modal</button>} // any element you write
footer={<SpButton variant="primary" onClick={() => setOpen(false)}>Apply</SpButton>}
open={open}
onOpenChange={setOpen}
>
<p>Apply the new ground to every chart on the page?</p>
</SpModal>
);
}<script setup lang="ts">
import { ref } from 'vue';
import { SpButton } from '@silverpoint/vue/ui/button';
import { SpModal } from '@silverpoint/vue/ui/modal';
const open = ref(false);
</script>
<template>
<SpModal v-model:open="open" title="Apply changes" description="The geometry of the data does not move.">
<template #trigger><button type="button">Open the modal</button></template>
<p>Apply the new ground to every chart on the page?</p>
<template #footer><SpButton variant="primary" @click="open = false">Apply</SpButton></template>
</SpModal>
</template>import { Component, signal } from '@angular/core';
import { SpCardFooter, SpTrigger } from '@silverpoint/angular/ui';
import { SpButton } from '@silverpoint/angular/ui/button';
import { SpModal } from '@silverpoint/angular/ui/modal';
@Component({
selector: 'app-apply-changes',
imports: [SpButton, SpCardFooter, SpModal, SpTrigger],
template: `
<sp-modal [(open)]="open" title="Apply changes" description="The geometry of the data does not move.">
<button type="button" spTrigger>Open the modal</button>
<p>Apply the new ground to every chart on the page?</p>
<ng-template spFooter>
<button spButton variant="primary" (click)="open.set(false)">Apply</button>
</ng-template>
</sp-modal>
`,
})
export class ApplyChanges {
protected readonly open = signal(false);
}sp_modal(
rx.el.p("Apply the new ground to every chart on the page?"),
trigger="Open the modal", # a string becomes sp_trigger(...)
title="Apply changes",
description="The geometry of the data does not move.",
footer=sp_trigger("Apply", variant="primary", on_click=State.confirm),
open=State.modal_open,
on_open_change=State.set_modal_open,
)sp_drawer
The same dialog, sliding in from any edge (side='end', the default, 'start', 'top' or 'bottom'). SpDrawer reference →
import { SpDrawer } from '@silverpoint/react/ui/drawer';
// side: start, end (the default), top or bottom; uncontrolled here
<SpDrawer title="Settings" side="end" trigger={<button type="button">Open the drawer</button>}>
<p>A panel that slides in from any edge, over a hatched backdrop.</p>
</SpDrawer><script setup lang="ts">
import { SpDrawer } from '@silverpoint/vue/ui/drawer';
</script>
<template>
<SpDrawer title="Settings" side="end">
<template #trigger><button type="button">Open the drawer</button></template>
<p>A panel that slides in from any edge, over a hatched backdrop.</p>
</SpDrawer>
</template>import { Component } from '@angular/core';
import { SpTrigger } from '@silverpoint/angular/ui';
import { SpDrawer } from '@silverpoint/angular/ui/drawer';
@Component({
selector: 'app-settings',
imports: [SpDrawer, SpTrigger],
template: `
<sp-drawer title="Settings" side="end">
<button type="button" spTrigger>Open the drawer</button>
<p>A panel that slides in from any edge, over a hatched backdrop.</p>
</sp-drawer>
`,
})
export class Settings {}sp_drawer(rx.el.p("A panel that slides in…"), trigger="Open the drawer", title="Settings", side="end")sp_popover
Anchored content on a card frame, placed by the core on the Popover API; Escape or a click outside closes it. SpPopover reference →
import { SpPopover } from '@silverpoint/react/ui/popover';
<SpPopover title="About this chart" closable width="260px" trigger={<button type="button">Details</button>}>
<p>Anchored content, on a card frame. Escape or a click outside closes it.</p>
</SpPopover><script setup lang="ts">
import { SpPopover } from '@silverpoint/vue/ui/popover';
</script>
<template>
<SpPopover title="About this chart" closable width="260px">
<template #trigger><button type="button">Details</button></template>
<p>Anchored content, on a card frame. Escape or a click outside closes it.</p>
</SpPopover>
</template>import { Component } from '@angular/core';
import { SpTrigger } from '@silverpoint/angular/ui';
import { SpPopover } from '@silverpoint/angular/ui/popover';
@Component({
selector: 'app-about-chart',
imports: [SpPopover, SpTrigger],
template: `
<sp-popover title="About this chart" [closable]="true" width="260px">
<button type="button" spTrigger>Details</button>
<p>Anchored content, on a card frame. Escape or a click outside closes it.</p>
</sp-popover>
`,
})
export class AboutChart {}sp_popover(
rx.el.p("Anchored content, on a card frame."),
trigger=sp_trigger("Details", chevron=True),
title="About this chart",
closable=True,
width=260,
)sp_tooltip
A short label shown on hover or keyboard focus, after a delay. SpTooltip reference →
import { SpTooltip } from '@silverpoint/react/ui/tooltip';
<SpTooltip
text="Drawn on the ground's own frame" // the trigger's accessible description
trigger={<button type="button">Hover or focus me</button>}
side="top"
delay={300}
/><script setup lang="ts">
import { SpTooltip } from '@silverpoint/vue/ui/tooltip';
</script>
<template>
<SpTooltip text="Drawn on the ground's own frame" side="top" :delay="300">
<template #trigger><button type="button">Hover or focus me</button></template>
</SpTooltip>
</template>import { Component } from '@angular/core';
import { SpTrigger } from '@silverpoint/angular/ui';
import { SpTooltip } from '@silverpoint/angular/ui/tooltip';
@Component({
selector: 'app-hint',
imports: [SpTooltip, SpTrigger],
template: `
<sp-tooltip text="Drawn on the ground's own frame" side="top" [delay]="300">
<button type="button" spTrigger>Hover or focus me</button>
</sp-tooltip>
`,
})
export class Hint {}sp_tooltip("Hover or focus me", "Drawn on the ground's own frame", side="top", delay=300)sp_select
A single choice with typeahead: the chosen option ticked, the highlighted one heightened. SpSelect reference →
import { useState } from 'react';
import { SpSelect } from '@silverpoint/react/ui/select';
export function Region() {
const [region, setRegion] = useState<string | null>('North');
// items: strings, or { key, label, disabled }; onChange receives the key
return <SpSelect items={['North', 'East', 'South', 'West']} label="Region" value={region} onChange={setRegion} />;
}<script setup lang="ts">
import { ref } from 'vue';
import { SpSelect } from '@silverpoint/vue/ui/select';
const region = ref<string | null>('North');
</script>
<template>
<SpSelect v-model="region" :items="['North', 'East', 'South', 'West']" label="Region" />
</template>import { Component, signal } from '@angular/core';
import { SpSelect } from '@silverpoint/angular/ui/select';
@Component({
selector: 'app-region',
imports: [SpSelect],
template: `<sp-select [(value)]="region" [items]="regions" label="Region" />`,
})
export class Region {
protected readonly regions = ['North', 'East', 'South', 'West'];
protected readonly region = signal<string | null>('North');
}sp_select(
["North", "East", "South", "West"], # or [{"key", "label", "disabled"}]
value=State.region,
on_change=State.set_region, # receives the value
label="Region",
)sp_dropdown_menu
Commands with shortcuts, separators, labels and a danger item; on_select(key). SpDropdownMenu reference →
import { SpDropdownMenu } from '@silverpoint/react/ui/dropdown-menu';
const actions = [
{ type: 'label', key: 'drawing', label: 'Drawing' },
{ key: 'duplicate', label: 'Duplicate', shortcut: '⌘D' },
{ key: 'export', label: 'Export SVG', shortcut: '⌘E' },
{ type: 'separator', key: 'sep' },
{ key: 'delete', label: 'Delete', danger: true },
] as const;
// label names the menu and is the built-in button's text; trigger={…} replaces that button
<SpDropdownMenu label="Actions" items={actions} onSelect={(key) => console.log(key)} /><script setup lang="ts">
import { SpDropdownMenu } from '@silverpoint/vue/ui/dropdown-menu';
const actions = [
{ type: 'label', key: 'drawing', label: 'Drawing' },
{ key: 'duplicate', label: 'Duplicate', shortcut: '⌘D' },
{ key: 'export', label: 'Export SVG', shortcut: '⌘E' },
{ type: 'separator', key: 'sep' },
{ key: 'delete', label: 'Delete', danger: true },
] as const;
const run = (key: string) => console.log(key);
</script>
<template>
<SpDropdownMenu label="Actions" :items="actions" @select="run" />
</template>import { Component } from '@angular/core';
import { SpDropdownMenu } from '@silverpoint/angular/ui/dropdown-menu';
@Component({
selector: 'app-actions',
imports: [SpDropdownMenu],
template: `<sp-dropdown-menu label="Actions" [items]="actions" (select)="run($event)" />`,
})
export class Actions {
protected readonly actions = [
{ type: 'label', key: 'drawing', label: 'Drawing' },
{ key: 'duplicate', label: 'Duplicate', shortcut: '⌘D' },
{ key: 'export', label: 'Export SVG', shortcut: '⌘E' },
{ type: 'separator', key: 'sep' },
{ key: 'delete', label: 'Delete', danger: true },
] as const;
protected run(key: string) {
console.log(key);
}
}sp_dropdown_menu(
"Actions",
[
{"type": "label", "label": "Drawing"},
{"key": "duplicate", "label": "Duplicate", "shortcut": "⌘D"},
{"key": "export", "label": "Export SVG", "shortcut": "⌘E"},
{"type": "separator"},
{"key": "delete", "label": "Delete", "danger": True},
],
on_select=State.run, # receives the item's key
)sp_toast · sp_toaster
Notices that stack in a corner and dismiss themselves; error and warning are announced assertively. SpToast reference →
import { useState } from 'react';
import { SpButton } from '@silverpoint/react/ui/button';
import { SpToast, SpToaster } from '@silverpoint/react/ui/toast';
export function Save() {
const [open, setOpen] = useState(false);
return (
<>
<SpButton onClick={() => setOpen(true)}>Save</SpButton>
<SpToaster position="bottom-end">
{/* error and warning are announced assertively; onOpenChange(false) when it closes */}
<SpToast title="Saved" description="The drawing was saved." kind="success" open={open} onOpenChange={setOpen} />
</SpToaster>
</>
);
}<script setup lang="ts">
import { ref } from 'vue';
import { SpButton } from '@silverpoint/vue/ui/button';
import { SpToast, SpToaster } from '@silverpoint/vue/ui/toast';
const open = ref(false);
</script>
<template>
<SpButton @click="open = true">Save</SpButton>
<SpToaster position="bottom-end">
<SpToast v-model:open="open" title="Saved" description="The drawing was saved." kind="success" />
</SpToaster>
</template>import { Component, signal } from '@angular/core';
import { SpButton } from '@silverpoint/angular/ui/button';
import { SpToast, SpToaster } from '@silverpoint/angular/ui/toast';
@Component({
selector: 'app-save',
imports: [SpButton, SpToast, SpToaster],
template: `
<button spButton (click)="open.set(true)">Save</button>
<sp-toaster position="bottom-end">
<sp-toast [(open)]="open" title="Saved" description="The drawing was saved." kind="success" />
</sp-toaster>
`,
})
export class Save {
protected readonly open = signal(false);
}sp_button("Save", on_click=State.save) # sets toast_open = True
sp_toaster(
sp_toast(
"Saved",
"The drawing was saved.",
kind="success",
open=State.toast_open,
on_open_change=State.set_toast_open, # False when it closes
),
position="bottom-end", # the default; logical, so it mirrors in right-to-left text
)sp_trigger
The drawn button of every overlay: SpButton's drawing on a native button that keeps the ARIA and the toggle an overlay clones onto it (sp_button does not).
// React has no trigger component: an overlay's trigger is any element you write,
// cloned with aria-haspopup, aria-expanded and aria-controls.
import { SpButton } from '@silverpoint/react/ui/button';
import { SpPopover } from '@silverpoint/react/ui/popover';
<SpPopover title="Details" trigger={<SpButton>Details</SpButton>}>
<p>…</p>
</SpPopover><!-- Vue has no trigger component: the trigger slot's first element opens the overlay. -->
<script setup lang="ts">
import { SpButton } from '@silverpoint/vue/ui/button';
import { SpPopover } from '@silverpoint/vue/ui/popover';
</script>
<template>
<SpPopover title="Details">
<template #trigger><SpButton>Details</SpButton></template>
<p>…</p>
</SpPopover>
</template>// Angular marks the projected element that opens an overlay with the spTrigger directive.
import { Component } from '@angular/core';
import { SpTrigger } from '@silverpoint/angular/ui';
import { SpButton } from '@silverpoint/angular/ui/button';
import { SpPopover } from '@silverpoint/angular/ui/popover';
@Component({
selector: 'app-details',
imports: [SpButton, SpPopover, SpTrigger],
template: `
<sp-popover title="Details">
<button spButton spTrigger>Details</button>
<p>…</p>
</sp-popover>
`,
})
export class Details {}sp_trigger("Default")
sp_trigger("Primary", variant="primary") # light hatching
sp_trigger("Danger", variant="danger")
sp_trigger("Menu", chevron=True) # a down chevron after the text
sp_popover(rx.el.p("…"), trigger=sp_trigger("Details", chevron=True))Surface props
In React, Vue and Angular every component takes ground, substrate, mode, size, palette and id, and reads them from the provider. A value binds as each framework does it (value + onChange, v-model, [(value)] or a forms directive), and an overlay's open state with open + onOpenChange, v-model:open or [(open)] (or defaultOpen).
In Reflex every builder takes ground, substrate, mode, size (sm, md, lg), seed, palette, id, class_name and style. Values and lists (rows, items, options) can be static or state vars. Value components (sp_textarea, sp_input_number, sp_select, sp_pagination) send the new value itself to on_change, never a DOM event. Overlays are controlled with open + on_open_change, or uncontrolled with default_open.