@sableui/react 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -3
- package/CHANGELOG.md +235 -0
- package/README.md +11 -4
- package/bin/sable.mjs +266 -30
- package/dist/{_contract-CDa9-NdB.d.ts → _contract-DlklV0Im.d.ts} +2 -2
- package/dist/activity-DT8VK9fx.d.ts +25 -0
- package/dist/adapters/github.d.ts +41 -0
- package/dist/adapters/github.js +24 -0
- package/dist/blocks/index.d.ts +1 -1
- package/dist/components/activity-heatmap/activity-heatmap.js +420 -0
- package/dist/components/activity-heatmap/activity-heatmap.meta.js +142 -0
- package/dist/components/activity-heatmap/index.js +9 -0
- package/dist/components/avatar/avatar.js +16 -7
- package/dist/components/avatar/avatar.meta.js +43 -10
- package/dist/components/avatar/avatar.variants.js +26 -2
- package/dist/components/code-block/code-block.js +5 -3
- package/dist/components/code-block/code-block.meta.js +6 -0
- package/dist/components/collapsible/collapsible.js +6 -5
- package/dist/components/collapsible/collapsible.meta.js +14 -5
- package/dist/components/data-table/data-table.js +132 -45
- package/dist/components/data-table/data-table.meta.js +63 -4
- package/dist/components/date-picker/date-picker.js +13 -6
- package/dist/components/date-picker/date-picker.meta.js +22 -0
- package/dist/components/dialog/dialog.js +25 -5
- package/dist/components/dialog/dialog.meta.js +26 -5
- package/dist/components/media-image/media-image.js +3 -1
- package/dist/components/media-image/media-image.meta.js +15 -1
- package/dist/components/media-tile/media-tile.js +13 -1
- package/dist/components/media-tile/media-tile.meta.js +10 -0
- package/dist/components/menu/menu.js +2 -0
- package/dist/components/menu/menu.meta.js +2 -2
- package/dist/components/page-header/page-header.js +9 -4
- package/dist/components/page-header/page-header.meta.js +26 -1
- package/dist/components/panel-group/panel-group.js +17 -1
- package/dist/components/panel-group/panel-group.meta.js +43 -3
- package/dist/components/pill/pill.meta.js +15 -6
- package/dist/components/pill/pill.variants.js +1 -0
- package/dist/components/search-input/search-input.js +30 -5
- package/dist/components/search-input/search-input.meta.js +29 -4
- package/dist/components/section-header/section-header.js +27 -17
- package/dist/components/section-header/section-header.meta.js +27 -5
- package/dist/components/sheet/sheet.js +15 -3
- package/dist/components/sheet/sheet.meta.js +2 -2
- package/dist/components/side-nav/side-nav.variants.js +8 -3
- package/dist/components/sidebar/index.js +4 -0
- package/dist/components/sidebar/sidebar.js +90 -11
- package/dist/components/sidebar/sidebar.meta.js +38 -4
- package/dist/components/table/table.js +7 -3
- package/dist/components/tabs/tabs.js +10 -2
- package/dist/components/tabs/tabs.meta.js +4 -0
- package/dist/components/workspace/workspace.js +13 -3
- package/dist/components/workspace/workspace.meta.js +109 -1
- package/dist/file-hashes.json +94 -80
- package/dist/foundations/typography.js +13 -0
- package/dist/icon/Icon.js +21 -2
- package/dist/icon/index.d.ts +36 -1
- package/dist/icon/index.js +5 -0
- package/dist/icon/names.js +240 -0
- package/dist/icon-sets/reicon.js +41 -1
- package/dist/index.d.ts +540 -75
- package/dist/index.js +18 -1
- package/dist/layout/AspectBox.js +2 -5
- package/dist/layout/Bleed.js +10 -6
- package/dist/layout/Center.js +11 -3
- package/dist/layout/Columns.js +6 -3
- package/dist/layout/Grid.js +14 -6
- package/dist/layout/GridOverlay.js +7 -7
- package/dist/layout/Region.js +18 -3
- package/dist/layout/Row.js +27 -20
- package/dist/layout/Spacer.js +13 -2
- package/dist/layout/SplitPane.js +1 -1
- package/dist/layout/Stack.js +25 -18
- package/dist/layout/index.d.ts +57 -6
- package/dist/lib/chart/activity.js +32 -0
- package/dist/lib/chart/calendar.js +67 -0
- package/dist/lib/chart/chart-tooltip.js +2 -2
- package/dist/lib/chart/heat.js +55 -0
- package/dist/lib/cn.js +1 -0
- package/dist/lib/focus-return.js +53 -0
- package/dist/lib/taxonomy.js +1 -0
- package/dist/lib/use-media-query.js +41 -0
- package/dist/{meta-DX5r5wQL.d.ts → meta-Dfm9AExe.d.ts} +6 -0
- package/dist/patterns/dashboard-page.js +23 -17
- package/dist/patterns/index.d.ts +7 -2
- package/dist/patterns/index.js +5 -1
- package/dist/patterns/tracker-page.js +392 -0
- package/dist/patterns/tracker-page.meta.js +40 -0
- package/dist/presets/index.d.ts +1 -1
- package/dist/styles.css +301 -188
- package/dist/{theme-provider-BydfM5Z0.d.ts → theme-provider-BUBealk_.d.ts} +1 -1
- package/dist/themes/_contract.js +9 -1
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/ember/index.d.ts +1 -1
- package/dist/themes/index.d.ts +2 -2
- package/dist/themes/meridian/index.d.ts +1 -1
- package/dist/themes/poppy/index.d.ts +1 -1
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/sable/index.d.ts +1 -1
- package/dist/themes/verdant/index.d.ts +1 -1
- package/llms/components/activity-heatmap.md +133 -0
- package/llms/components/avatar.md +32 -9
- package/llms/components/code-block.md +1 -0
- package/llms/components/collapsible.md +12 -4
- package/llms/components/data-table.md +40 -3
- package/llms/components/date-picker.md +15 -1
- package/llms/components/dialog.md +34 -3
- package/llms/components/media-image.md +12 -0
- package/llms/components/media-tile.md +21 -0
- package/llms/components/menu.md +1 -1
- package/llms/components/page-header.md +16 -1
- package/llms/components/panel-group.md +34 -2
- package/llms/components/pill.md +24 -5
- package/llms/components/search-input.md +37 -4
- package/llms/components/section-header.md +28 -4
- package/llms/components/sheet.md +2 -2
- package/llms/components/sidebar.md +17 -4
- package/llms/components/tabs.md +1 -0
- package/llms/components/workspace.md +81 -1
- package/llms/foundations/typography.md +4 -1
- package/llms/layout/aspect-box.md +1 -1
- package/llms/layout/bleed.md +1 -1
- package/llms/layout/center.md +1 -1
- package/llms/layout/columns.md +1 -1
- package/llms/layout/grid.md +1 -1
- package/llms/layout/region.md +1 -1
- package/llms/layout/row.md +9 -1
- package/llms/layout/spacer.md +1 -1
- package/llms/layout/split-pane.md +1 -1
- package/llms/layout/stack.md +1 -1
- package/llms/patterns/dashboard-page.md +18 -15
- package/llms/patterns/tracker-page.md +450 -0
- package/llms/reference/token-intent.md +3 -1
- package/llms/reference/utilities.md +4 -1
- package/llms/tasks/auditing-an-app.md +35 -1
- package/llms/tasks/building-a-page.md +3 -2
- package/llms/tasks/building-a-tool.md +101 -5
- package/llms/tasks/building-with-sable.md +11 -3
- package/llms/tasks/creating-a-preset.md +9 -0
- package/llms/tasks/getting-started.md +10 -3
- package/llms/tasks/using-presets.md +1 -2
- package/llms-full.txt +1215 -89
- package/llms.txt +11 -9
- package/package.json +7 -1
- package/registry.json +1021 -70
- package/scripts/adoption.mjs +252 -0
- package/scripts/agent-setup.mjs +110 -18
- package/scripts/app-css.mjs +129 -0
- package/scripts/grade.mjs +178 -2
- package/scripts/lint-rules.mjs +126 -0
- package/scripts/wiring.mjs +2 -1
- package/skills/entry/SKILL.md +1 -1
- package/skills/sable/SKILL.md +64 -6
- package/skills/sable/agents/openai.yaml +1 -1
- package/targets/ios.json +9 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
# TrackerPage
|
|
2
|
+
|
|
3
|
+
Records workspace in the tool register: a Sidebar rail of grouped views ending in an account menu, a WorkspaceBar of Breadcrumbs over pill Tabs, a filter row, one grouped DataTable, and a docked inspector of PanelSection property rows with a pinned comment form. Below lg the rail and the inspector become sheets and a BottomNav takes the last row.
|
|
4
|
+
|
|
5
|
+
**Intent:** Reach for this when a team works through records in a dense tool — an issue tracker, a support queue, a pipeline: a grouped list, a docked inspector for the selected record, and a rail of views. For records a person reads and edits one at a time on a page, ListDetailPage is the page-register shape; for monitoring with headline numbers, DashboardPage.
|
|
6
|
+
|
|
7
|
+
## Notes
|
|
8
|
+
|
|
9
|
+
- Workspace sets the compact density for everything inside: 32px rail rows, 28px tables, 24px property fields. Nothing in the pattern sizes a control or a row by hand. The Overview tab is a reading view, so it sits in `<Density value="comfortable">` and its prose gets page type back.
|
|
10
|
+
- The rail is Sable's own parts on one grid: SidebarBrand, SidebarGroup, SidebarItem and SidebarAccount share the icon and label columns. The account row is the trigger of the account Menu, which holds Settings, an Appearance submenu wired to useSableTheme (light, dark, system) and Sign out. No Settings button or badge sits in the footer.
|
|
11
|
+
- There is no Workspace `toolbar`: the bars belong to the list column, so the inspector runs the full height beside them. The first WorkspaceBar carries Breadcrumbs; the second carries pill Tabs, a view switch in chrome, which sits beside the bar's other controls rather than drawing a rule.
|
|
12
|
+
- One DataTable groups the rows (`groupBy`, `groups`): one header for every group, a StatusDot, a count and an add action per group header, and Done starting closed. The segmented ToggleGroup switches the grouping between status and assignee. Filtering to nothing swaps the table for an EmptyState with a Clear filters action.
|
|
13
|
+
- Workflow states map onto StatusDot tones once (`STATUSES`), so group headers and the status Select read from one place. A product with a richer status glyph keeps that one mapping and swaps the icon.
|
|
14
|
+
- The inspector is a Panel with `narrow="sheet"`: docked at 320px from lg, a right sheet below it, its content rendered once. Below lg it opens only when a row is picked (`inspecting`), so a phone never loads with a sheet over the list. Properties are PanelSection + PropertyRow with xs Select triggers and an xs DatePicker; Activity uses RelativeTime; the comment Form is pinned under the scrolling body.
|
|
15
|
+
- The rail is a Panel with `narrow="sheet" side="left"`, opened from a `lg:hidden` menu button in the first bar; in the sheet the Sidebar fills its width. Workspace `bottomNav` reserves the BottomNav's row below lg, so the status bar ends above it.
|
|
16
|
+
|
|
17
|
+
## Accessibility
|
|
18
|
+
|
|
19
|
+
- Workspace renders a skip link to the canvas (`Skip to issues`) and WorkspaceCanvas is the main landmark; the rail and the inspector are labelled complementary regions (`Workspace navigation`, `Issue details`), and as sheets they keep those labels as their dialog titles.
|
|
20
|
+
- The page has one level-1 heading (visually hidden; the breadcrumb shows the place) and the inspector's title is a level-2 heading.
|
|
21
|
+
- Group header rows are real buttons with aria-expanded; rows open the inspector from the keyboard (DataTable's row activation). Every icon-only action has an accessible name and a tooltip.
|
|
22
|
+
- Each SidebarGroup is a named group; the account row names the person, and its menu is a standard Menu with a radio group for the colour mode.
|
|
23
|
+
|
|
24
|
+
## Source
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { useState } from "react"
|
|
28
|
+
import { Avatar, Badge, BottomNav, BottomNavItem, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, Breadcrumbs, BreadcrumbSeparator, Button, DataTable, DatePicker, Density, EmptyState, Form, Icon, Kbd, Menu, MenuContent, MenuItem, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuSubmenu, MenuSubmenuTrigger, MenuTrigger, Panel, PanelGroup, PanelSection, PropertyRow, RelativeTime, SearchInput, Select, SelectContent, SelectItem, SelectTrigger, Sidebar, SidebarAccount, SidebarBrand, SidebarFooter, SidebarGroup, SidebarHeader, SidebarItem, SidebarNav, SimpleTooltip, StatusBar, StatusBarGroup, StatusDot, Tabs, TabsList, TabsPanel, TabsTrigger, Textarea, Toggle, ToggleGroup, useBelowBreakpoint, useSableTheme, Workspace, WorkspaceBar, WorkspaceCanvas, type DataTableColumn } from "@sableui/react"
|
|
29
|
+
|
|
30
|
+
// Workflow states → StatusDot tones, mapped once here so every group header,
|
|
31
|
+
// row and select follows. Sable has no status vocabulary of its own.
|
|
32
|
+
const STATUSES = [
|
|
33
|
+
{ key: "progress", label: "In progress", tone: "warning" },
|
|
34
|
+
{ key: "review", label: "In review", tone: "2" },
|
|
35
|
+
{ key: "todo", label: "Todo", tone: "neutral" },
|
|
36
|
+
{ key: "done", label: "Done", tone: "success" },
|
|
37
|
+
] as const
|
|
38
|
+
type Status = (typeof STATUSES)[number]["key"]
|
|
39
|
+
|
|
40
|
+
const PRIORITIES = ["Urgent", "High", "Medium", "Low"] as const
|
|
41
|
+
type Priority = (typeof PRIORITIES)[number]
|
|
42
|
+
|
|
43
|
+
const people: Record<string, string> = {
|
|
44
|
+
AK: "Ama Kusi",
|
|
45
|
+
JR: "Jonas Reiter",
|
|
46
|
+
LM: "Lena Moreau",
|
|
47
|
+
TO: "Tomás Ortega",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const MIN = 60 * 1000
|
|
51
|
+
const HOUR = 60 * MIN
|
|
52
|
+
const DAY = 24 * HOUR
|
|
53
|
+
const isoDay = (offset: number) => {
|
|
54
|
+
const d = new Date(Date.now() + offset)
|
|
55
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type Note = { id: string; who: string; text: string; at: number }
|
|
59
|
+
type Issue = {
|
|
60
|
+
id: string
|
|
61
|
+
title: string
|
|
62
|
+
detail: string
|
|
63
|
+
status: Status
|
|
64
|
+
priority: Priority
|
|
65
|
+
assignee: string
|
|
66
|
+
label: string
|
|
67
|
+
due: Date
|
|
68
|
+
notes: Note[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const initialIssues: Issue[] = [
|
|
72
|
+
{ id: "FW-104", title: "Sync offline edits when the app reconnects", detail: "Edits made offline are queued but replayed out of order after a reconnect. Replay them by timestamp and surface conflicts.", status: "progress", priority: "Urgent", assignee: "AK", label: "Sync", due: isoDay(2 * DAY), notes: [{ id: "n1", who: "JR", text: "Repro steps are in the test plan. It only happens after a cold start.", at: -3 * HOUR }] },
|
|
73
|
+
{ id: "FW-105", title: "Onboarding checklist on first launch", detail: "Three steps: connect a calendar, invite a teammate, create a first project.", status: "progress", priority: "Medium", assignee: "LM", label: "Onboarding", due: isoDay(5 * DAY), notes: [] },
|
|
74
|
+
{ id: "FW-108", title: "Push notification settings per project", detail: "Let people mute a project without muting the workspace.", status: "review", priority: "High", assignee: "TO", label: "Notifications", due: isoDay(1 * DAY), notes: [] },
|
|
75
|
+
{ id: "FW-110", title: "Crash when attaching a large photo", detail: "Photos over 20 MB crash the uploader on older devices.", status: "review", priority: "Urgent", assignee: "JR", label: "Bug", due: isoDay(-1 * DAY), notes: [] },
|
|
76
|
+
{ id: "FW-112", title: "Search across archived projects", detail: "Archived projects are left out of search today.", status: "todo", priority: "Low", assignee: "AK", label: "Search", due: isoDay(9 * DAY), notes: [] },
|
|
77
|
+
{ id: "FW-113", title: "Keyboard shortcuts for the board view", detail: "J and K move between cards; C creates an issue in the focused column.", status: "todo", priority: "Medium", assignee: "LM", label: "Board", due: isoDay(12 * DAY), notes: [] },
|
|
78
|
+
{ id: "FW-114", title: "Export a project as CSV", detail: "One row per issue, with status, assignee and dates.", status: "todo", priority: "Low", assignee: "TO", label: "Export", due: isoDay(14 * DAY), notes: [] },
|
|
79
|
+
{ id: "FW-101", title: "Dark mode for the widget", detail: "The home-screen widget ignores the system appearance.", status: "done", priority: "Medium", assignee: "JR", label: "Widget", due: isoDay(-4 * DAY), notes: [] },
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
const MODE_LABEL = { light: "Light", dark: "Dark", system: "System" } as const
|
|
83
|
+
|
|
84
|
+
/** One icon-only action with a visible name on hover and focus. */
|
|
85
|
+
export function TrackerPage() {
|
|
86
|
+
const [issues, setIssues] = useState(initialIssues)
|
|
87
|
+
const [selectedId, setSelectedId] = useState<string | null>("FW-104")
|
|
88
|
+
// Below lg the inspector is a sheet; it opens only when someone picks a row,
|
|
89
|
+
// so a phone does not load with a sheet over the list.
|
|
90
|
+
const [inspecting, setInspecting] = useState(false)
|
|
91
|
+
const [navOpen, setNavOpen] = useState(false)
|
|
92
|
+
const [query, setQuery] = useState("")
|
|
93
|
+
const [priority, setPriority] = useState<"all" | Priority>("all")
|
|
94
|
+
const [groupBy, setGroupBy] = useState<"status" | "assignee">("status")
|
|
95
|
+
const [tab, setTab] = useState("issues")
|
|
96
|
+
const [draft, setDraft] = useState("")
|
|
97
|
+
const narrow = useBelowBreakpoint("lg")
|
|
98
|
+
const { preference, setPreference } = useSableTheme()
|
|
99
|
+
|
|
100
|
+
const needle = query.trim().toLowerCase()
|
|
101
|
+
const visible = issues.filter(
|
|
102
|
+
(i) =>
|
|
103
|
+
(priority === "all" || i.priority === priority) &&
|
|
104
|
+
`${i.id} ${i.title} ${i.label}`.toLowerCase().includes(needle),
|
|
105
|
+
)
|
|
106
|
+
const selected = issues.find((i) => i.id === selectedId) ?? null
|
|
107
|
+
const done = issues.filter((i) => i.status === "done").length
|
|
108
|
+
|
|
109
|
+
function update(id: string, patch: Partial<Issue>) {
|
|
110
|
+
setIssues((all) => all.map((i) => (i.id === id ? { ...i, ...patch } : i)))
|
|
111
|
+
}
|
|
112
|
+
function open(id: string) {
|
|
113
|
+
setSelectedId(id)
|
|
114
|
+
setInspecting(true)
|
|
115
|
+
}
|
|
116
|
+
function close() {
|
|
117
|
+
setInspecting(false)
|
|
118
|
+
if (!narrow) setSelectedId(null)
|
|
119
|
+
}
|
|
120
|
+
function addIssue(status: Status = "todo") {
|
|
121
|
+
const id = `FW-${115 + issues.length}`
|
|
122
|
+
setIssues((all) => [
|
|
123
|
+
...all,
|
|
124
|
+
{ id, title: "New issue", detail: "", status, priority: "Medium", assignee: "AK", label: "Triage", due: isoDay(7 * DAY), notes: [] },
|
|
125
|
+
])
|
|
126
|
+
open(id)
|
|
127
|
+
}
|
|
128
|
+
function addNote() {
|
|
129
|
+
if (!selected || !draft.trim()) return
|
|
130
|
+
update(selected.id, { notes: [...selected.notes, { id: `n${Date.now()}`, who: "AK", text: draft.trim(), at: 0 }] })
|
|
131
|
+
setDraft("")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const columns: DataTableColumn<Issue>[] = [
|
|
135
|
+
{ key: "id", header: "ID", className: "w-20 whitespace-nowrap", cell: (i) => <span className="text-ui-num text-muted-foreground">{i.id}</span> },
|
|
136
|
+
{ key: "title", header: "Issue", className: "w-full max-w-0", cell: (i) => <span className="block truncate">{i.title}</span> },
|
|
137
|
+
{ key: "label", header: "Label", className: "w-28 max-sm:hidden", cell: (i) => <Badge appearance="outline" size="sm">{i.label}</Badge> },
|
|
138
|
+
{ key: "due", header: "Due", className: "w-20 whitespace-nowrap max-sm:hidden", sortValue: (i) => i.due.getTime(), cell: (i) => <span className="text-muted-foreground">{i.due.toLocaleDateString("en-US", { month: "short", day: "numeric" })}</span> },
|
|
139
|
+
{ key: "assignee", header: <span className="sr-only">Assignee</span>, className: "w-10", cell: (i) => <Avatar size="2xs" fallback={i.assignee} alt={people[i.assignee]} /> },
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
const groups =
|
|
143
|
+
groupBy === "status"
|
|
144
|
+
? STATUSES.map((s) => ({
|
|
145
|
+
key: s.key,
|
|
146
|
+
label: s.label,
|
|
147
|
+
icon: <StatusDot tone={s.tone} />,
|
|
148
|
+
action: <SimpleTooltip content={`Add to ${s.label}`}><Button size="icon-xs" variant="ghost" aria-label={`Add to ${s.label}`} onClick={() => addIssue(s.key)}><Icon name="plus" /></Button></SimpleTooltip>,
|
|
149
|
+
defaultExpanded: s.key !== "done",
|
|
150
|
+
}))
|
|
151
|
+
: Object.entries(people).map(([key, name]) => ({
|
|
152
|
+
key,
|
|
153
|
+
label: name,
|
|
154
|
+
icon: <Avatar size="2xs" fallback={key} alt="" />,
|
|
155
|
+
}))
|
|
156
|
+
|
|
157
|
+
const sidebar = (
|
|
158
|
+
<Sidebar variant="inline" className="h-full w-full">
|
|
159
|
+
<SidebarHeader>
|
|
160
|
+
<SidebarBrand mark={<Avatar shape="square" size="2xs" tone="teal" fallback="F" alt="" />} label="Fieldwork" />
|
|
161
|
+
<Button variant="outline" size="sm" className="basis-full justify-between">
|
|
162
|
+
<span className="flex items-center gap-2">
|
|
163
|
+
<Icon name="search" />
|
|
164
|
+
Search
|
|
165
|
+
</span>
|
|
166
|
+
<Kbd>Ctrl K</Kbd>
|
|
167
|
+
</Button>
|
|
168
|
+
</SidebarHeader>
|
|
169
|
+
<SidebarNav aria-label="Workspace">
|
|
170
|
+
<SidebarItem icon={<Icon name="inbox" />} label="Inbox" badge={<span className="text-ui-num text-muted-foreground">3</span>} />
|
|
171
|
+
<SidebarItem icon={<Icon name="circle-check" />} label="My issues" />
|
|
172
|
+
<SidebarGroup label="Mobile app">
|
|
173
|
+
<SidebarItem icon={<Icon name="layers" />} label="Issues" active />
|
|
174
|
+
<SidebarItem icon={<Icon name="refresh" />} label="Cycles" />
|
|
175
|
+
<SidebarItem icon={<Icon name="archive" />} label="Backlog" />
|
|
176
|
+
</SidebarGroup>
|
|
177
|
+
<SidebarGroup label="Workspace">
|
|
178
|
+
<SidebarItem icon={<Icon name="folder" />} label="Projects" />
|
|
179
|
+
<SidebarItem icon={<Icon name="users" />} label="Members" />
|
|
180
|
+
</SidebarGroup>
|
|
181
|
+
</SidebarNav>
|
|
182
|
+
<SidebarFooter>
|
|
183
|
+
<Menu>
|
|
184
|
+
<MenuTrigger
|
|
185
|
+
render={<SidebarAccount avatar={<Avatar size="2xs" fallback="AK" alt="" />} label="Ama Kusi" detail="Fieldwork · Admin" />}
|
|
186
|
+
/>
|
|
187
|
+
<MenuContent side="top" align="start">
|
|
188
|
+
<MenuItem icon={<Icon name="gear" />}>Settings</MenuItem>
|
|
189
|
+
<MenuSubmenu>
|
|
190
|
+
<MenuSubmenuTrigger
|
|
191
|
+
icon={<Icon name={preference === "dark" ? "moon" : preference === "light" ? "sun" : "desktop"} />}
|
|
192
|
+
value={MODE_LABEL[preference]}
|
|
193
|
+
>
|
|
194
|
+
Appearance
|
|
195
|
+
</MenuSubmenuTrigger>
|
|
196
|
+
<MenuContent side="inline-end">
|
|
197
|
+
<MenuRadioGroup value={preference} onValueChange={(v) => setPreference(v as typeof preference)}>
|
|
198
|
+
<MenuRadioItem value="light">Light</MenuRadioItem>
|
|
199
|
+
<MenuRadioItem value="dark">Dark</MenuRadioItem>
|
|
200
|
+
<MenuRadioItem value="system">System</MenuRadioItem>
|
|
201
|
+
</MenuRadioGroup>
|
|
202
|
+
</MenuContent>
|
|
203
|
+
</MenuSubmenu>
|
|
204
|
+
<MenuSeparator />
|
|
205
|
+
<MenuItem icon={<Icon name="logout" />}>Sign out</MenuItem>
|
|
206
|
+
</MenuContent>
|
|
207
|
+
</Menu>
|
|
208
|
+
</SidebarFooter>
|
|
209
|
+
</Sidebar>
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
const list =
|
|
213
|
+
visible.length === 0 ? (
|
|
214
|
+
<EmptyState
|
|
215
|
+
icon={<Icon name="search" />}
|
|
216
|
+
title="No issues match"
|
|
217
|
+
description="Try another search, or clear the priority filter."
|
|
218
|
+
action={
|
|
219
|
+
<Button
|
|
220
|
+
variant="secondary"
|
|
221
|
+
size="sm"
|
|
222
|
+
onClick={() => {
|
|
223
|
+
setQuery("")
|
|
224
|
+
setPriority("all")
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
Clear filters
|
|
228
|
+
</Button>
|
|
229
|
+
}
|
|
230
|
+
/>
|
|
231
|
+
) : (
|
|
232
|
+
<DataTable
|
|
233
|
+
columns={columns}
|
|
234
|
+
data={visible}
|
|
235
|
+
rowKey={(i) => i.id}
|
|
236
|
+
groupBy={(i) => (groupBy === "status" ? i.status : i.assignee)}
|
|
237
|
+
groups={groups}
|
|
238
|
+
onRowClick={(i) => open(i.id)}
|
|
239
|
+
// A phone shows the selection only while its sheet is open.
|
|
240
|
+
isRowSelected={(i) => i.id === selectedId && (!narrow || inspecting)}
|
|
241
|
+
/>
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
const inspector = selected ? (
|
|
245
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
246
|
+
<WorkspaceBar className="justify-between">
|
|
247
|
+
<span className="text-ui-num text-muted-foreground">{selected.id}</span>
|
|
248
|
+
<SimpleTooltip content="Close issue"><Button size="icon-xs" variant="ghost" aria-label="Close issue" onClick={close}><Icon name="xmark" /></Button></SimpleTooltip>
|
|
249
|
+
</WorkspaceBar>
|
|
250
|
+
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-quiet">
|
|
251
|
+
<div className="flex flex-col gap-2 px-2 pt-4 pb-3">
|
|
252
|
+
<h2 className="text-title-lg">{selected.title}</h2>
|
|
253
|
+
<p className="text-ui-body text-muted-foreground">{selected.detail || "Add the details the team needs."}</p>
|
|
254
|
+
</div>
|
|
255
|
+
<PanelSection title="Properties">
|
|
256
|
+
<PropertyRow label="Status">
|
|
257
|
+
<Select value={selected.status} items={STATUSES.map((s) => ({ value: s.key, label: s.label }))} onValueChange={(v) => v && update(selected.id, { status: v as Status })}>
|
|
258
|
+
<SelectTrigger size="xs" aria-label="Status" />
|
|
259
|
+
<SelectContent>
|
|
260
|
+
{STATUSES.map((s) => (
|
|
261
|
+
<SelectItem key={s.key} value={s.key}>{s.label}</SelectItem>
|
|
262
|
+
))}
|
|
263
|
+
</SelectContent>
|
|
264
|
+
</Select>
|
|
265
|
+
</PropertyRow>
|
|
266
|
+
<PropertyRow label="Priority">
|
|
267
|
+
<Select value={selected.priority} onValueChange={(v) => v && update(selected.id, { priority: v as Priority })}>
|
|
268
|
+
<SelectTrigger size="xs" aria-label="Priority" />
|
|
269
|
+
<SelectContent>
|
|
270
|
+
{PRIORITIES.map((p) => (
|
|
271
|
+
<SelectItem key={p} value={p}>{p}</SelectItem>
|
|
272
|
+
))}
|
|
273
|
+
</SelectContent>
|
|
274
|
+
</Select>
|
|
275
|
+
</PropertyRow>
|
|
276
|
+
<PropertyRow label="Assignee">
|
|
277
|
+
<Select value={selected.assignee} items={people} onValueChange={(v) => v && update(selected.id, { assignee: v })}>
|
|
278
|
+
<SelectTrigger size="xs" aria-label="Assignee" />
|
|
279
|
+
<SelectContent>
|
|
280
|
+
{Object.entries(people).map(([key, name]) => (
|
|
281
|
+
<SelectItem key={key} value={key}>{name}</SelectItem>
|
|
282
|
+
))}
|
|
283
|
+
</SelectContent>
|
|
284
|
+
</Select>
|
|
285
|
+
</PropertyRow>
|
|
286
|
+
<PropertyRow label="Due date">
|
|
287
|
+
<DatePicker
|
|
288
|
+
size="xs"
|
|
289
|
+
aria-label="Due date"
|
|
290
|
+
value={selected.due}
|
|
291
|
+
formatOptions={{ month: "short", day: "numeric", year: "numeric" }}
|
|
292
|
+
onValueChange={(d) => d && update(selected.id, { due: d })}
|
|
293
|
+
/>
|
|
294
|
+
</PropertyRow>
|
|
295
|
+
</PanelSection>
|
|
296
|
+
<PanelSection title="Activity">
|
|
297
|
+
<div className="flex flex-col gap-3 pb-4">
|
|
298
|
+
<div className="flex items-center gap-2 text-ui-sm text-muted-foreground">
|
|
299
|
+
<Icon name="plus" />
|
|
300
|
+
<span>Created in Mobile app</span>
|
|
301
|
+
</div>
|
|
302
|
+
{selected.notes.map((n) => (
|
|
303
|
+
<div key={n.id} className="flex items-start gap-2">
|
|
304
|
+
<Avatar size="2xs" fallback={n.who} alt="" />
|
|
305
|
+
<div className="min-w-0">
|
|
306
|
+
<div className="flex items-center gap-2">
|
|
307
|
+
<span className="text-ui-md">{people[n.who]}</span>
|
|
308
|
+
<RelativeTime date={new Date(Date.now() + n.at)} className="text-ui-sm text-muted-foreground" />
|
|
309
|
+
</div>
|
|
310
|
+
<p className="mt-1 text-ui-body">{n.text}</p>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
))}
|
|
314
|
+
</div>
|
|
315
|
+
</PanelSection>
|
|
316
|
+
</div>
|
|
317
|
+
<Form
|
|
318
|
+
className="flex shrink-0 flex-col gap-2 border-t border-hairline p-2"
|
|
319
|
+
onSubmit={(e) => {
|
|
320
|
+
e.preventDefault()
|
|
321
|
+
addNote()
|
|
322
|
+
}}
|
|
323
|
+
>
|
|
324
|
+
<Textarea aria-label="Add a comment" placeholder="Leave a comment…" rows={2} value={draft} onChange={(e) => setDraft(e.target.value)} />
|
|
325
|
+
<Button size="xs" variant="secondary" type="submit" className="self-end" disabled={!draft.trim()} iconStart={<Icon name="arrow-up" />}>
|
|
326
|
+
Comment
|
|
327
|
+
</Button>
|
|
328
|
+
</Form>
|
|
329
|
+
</div>
|
|
330
|
+
) : null
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<Workspace
|
|
334
|
+
skipLinkLabel="Skip to issues"
|
|
335
|
+
statusBar={
|
|
336
|
+
<StatusBar>
|
|
337
|
+
<span className="flex items-center gap-2">
|
|
338
|
+
<StatusDot tone="success" size="sm" />
|
|
339
|
+
Saved
|
|
340
|
+
</span>
|
|
341
|
+
<span className="max-sm:hidden">
|
|
342
|
+
{issues.length} issues · {done} done
|
|
343
|
+
</span>
|
|
344
|
+
<StatusBarGroup align="end">
|
|
345
|
+
<span className="max-sm:hidden">Ctrl K to search</span>
|
|
346
|
+
</StatusBarGroup>
|
|
347
|
+
</StatusBar>
|
|
348
|
+
}
|
|
349
|
+
bottomNav={
|
|
350
|
+
<BottomNav aria-label="Primary">
|
|
351
|
+
<BottomNavItem icon={<Icon name="inbox" />} label="Inbox" showLabel />
|
|
352
|
+
<BottomNavItem icon={<Icon name="layers" />} label="Issues" showLabel active />
|
|
353
|
+
<BottomNavItem icon={<Icon name="folder" />} label="Projects" showLabel />
|
|
354
|
+
<BottomNavItem icon={<Icon name="users" />} label="Members" showLabel />
|
|
355
|
+
</BottomNav>
|
|
356
|
+
}
|
|
357
|
+
>
|
|
358
|
+
<PanelGroup>
|
|
359
|
+
{/* Docked from lg; below it the same rail is a left sheet. */}
|
|
360
|
+
<Panel label="Workspace navigation" width={232} narrow="sheet" side="left" open={narrow ? navOpen : true} onOpenChange={setNavOpen} scroll={false}>
|
|
361
|
+
{sidebar}
|
|
362
|
+
</Panel>
|
|
363
|
+
<WorkspaceCanvas className="flex flex-col">
|
|
364
|
+
<WorkspaceBar className="gap-2 px-3">
|
|
365
|
+
<span className="lg:hidden">
|
|
366
|
+
<SimpleTooltip content="Open navigation"><Button size="icon-xs" variant="ghost" aria-label="Open navigation" onClick={() => setNavOpen(true)}><Icon name="bars" /></Button></SimpleTooltip>
|
|
367
|
+
</span>
|
|
368
|
+
<h1 className="sr-only">Mobile app issues</h1>
|
|
369
|
+
<Breadcrumbs className="min-w-0">
|
|
370
|
+
<BreadcrumbItem className="max-sm:hidden">
|
|
371
|
+
<BreadcrumbLink href="#">Fieldwork</BreadcrumbLink>
|
|
372
|
+
</BreadcrumbItem>
|
|
373
|
+
<BreadcrumbSeparator className="max-sm:hidden" />
|
|
374
|
+
<BreadcrumbItem>
|
|
375
|
+
<BreadcrumbPage>Mobile app</BreadcrumbPage>
|
|
376
|
+
</BreadcrumbItem>
|
|
377
|
+
</Breadcrumbs>
|
|
378
|
+
</WorkspaceBar>
|
|
379
|
+
<Tabs value={tab} onValueChange={(v) => setTab(String(v))} className="flex min-h-0 flex-1 flex-col">
|
|
380
|
+
{/* A view switch in tool chrome: pill, beside the bar's other controls. */}
|
|
381
|
+
<WorkspaceBar className="px-3">
|
|
382
|
+
<TabsList variant="pill">
|
|
383
|
+
<TabsTrigger value="issues">
|
|
384
|
+
Issues <span className="text-ui-num text-muted-foreground">{issues.length}</span>
|
|
385
|
+
</TabsTrigger>
|
|
386
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
387
|
+
</TabsList>
|
|
388
|
+
</WorkspaceBar>
|
|
389
|
+
<TabsPanel value="issues" className="flex min-h-0 flex-1 flex-col">
|
|
390
|
+
<div className="flex flex-wrap items-center gap-2 border-b border-hairline px-3 py-2">
|
|
391
|
+
<Menu>
|
|
392
|
+
<MenuTrigger render={<Button variant="ghost" size="xs" iconStart={<Icon name="filter" />}>{priority === "all" ? "Filter" : priority}</Button>} />
|
|
393
|
+
<MenuContent>
|
|
394
|
+
<MenuRadioGroup value={priority} onValueChange={(v) => setPriority(v as typeof priority)}>
|
|
395
|
+
<MenuRadioItem value="all">All priorities</MenuRadioItem>
|
|
396
|
+
{PRIORITIES.map((p) => (
|
|
397
|
+
<MenuRadioItem key={p} value={p}>{p}</MenuRadioItem>
|
|
398
|
+
))}
|
|
399
|
+
</MenuRadioGroup>
|
|
400
|
+
</MenuContent>
|
|
401
|
+
</Menu>
|
|
402
|
+
<SearchInput size="xs" value={query} onValueChange={setQuery} placeholder="Find issue…" aria-label="Find issue" className="w-40" />
|
|
403
|
+
<ToggleGroup appearance="segmented" value={[groupBy]} onValueChange={(v) => v[0] && setGroupBy(v[0] as typeof groupBy)} aria-label="Group by" className="ms-auto">
|
|
404
|
+
<Toggle value="status">Status</Toggle>
|
|
405
|
+
<Toggle value="assignee">Assignee</Toggle>
|
|
406
|
+
</ToggleGroup>
|
|
407
|
+
<Button size="xs" iconStart={<Icon name="plus" />} onClick={() => addIssue()}>
|
|
408
|
+
New issue
|
|
409
|
+
</Button>
|
|
410
|
+
</div>
|
|
411
|
+
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-quiet">{list}</div>
|
|
412
|
+
</TabsPanel>
|
|
413
|
+
<TabsPanel value="overview" className="flex min-h-0 flex-1 flex-col">
|
|
414
|
+
{/* A reading view: page type comes back inside the compact workspace. */}
|
|
415
|
+
<Density value="comfortable" className="min-h-0 flex-1 overflow-y-auto scrollbar-quiet">
|
|
416
|
+
<article className="flex max-w-prose flex-col gap-4 p-8 max-sm:p-5">
|
|
417
|
+
<h2 className="text-heading-3">Mobile app</h2>
|
|
418
|
+
<p className="text-body-md text-muted-foreground">
|
|
419
|
+
The phone app for field teams: capture work on site, sync it when the signal comes back, and hand it to the office without retyping anything.
|
|
420
|
+
</p>
|
|
421
|
+
<dl className="sable-list-dl">
|
|
422
|
+
<dt>Lead</dt>
|
|
423
|
+
<dd>{people.AK}</dd>
|
|
424
|
+
<dt>Open issues</dt>
|
|
425
|
+
<dd>{issues.length - done}</dd>
|
|
426
|
+
<dt>Target</dt>
|
|
427
|
+
<dd>{isoDay(30 * DAY).toLocaleDateString("en-US", { month: "long", day: "numeric" })}</dd>
|
|
428
|
+
</dl>
|
|
429
|
+
</article>
|
|
430
|
+
</Density>
|
|
431
|
+
</TabsPanel>
|
|
432
|
+
</Tabs>
|
|
433
|
+
</WorkspaceCanvas>
|
|
434
|
+
{/* Docked beside the list from lg; a sheet below it. Its content renders once. */}
|
|
435
|
+
<Panel
|
|
436
|
+
label="Issue details"
|
|
437
|
+
width={320}
|
|
438
|
+
narrow="sheet"
|
|
439
|
+
open={narrow ? inspecting && selected !== null : selected !== null}
|
|
440
|
+
onOpenChange={(next) => !next && close()}
|
|
441
|
+
scroll={false}
|
|
442
|
+
className="border-s border-hairline"
|
|
443
|
+
>
|
|
444
|
+
{inspector}
|
|
445
|
+
</Panel>
|
|
446
|
+
</PanelGroup>
|
|
447
|
+
</Workspace>
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
```
|
|
@@ -191,8 +191,10 @@ Which token to reach for, and what it is most often confused with. Generated fro
|
|
|
191
191
|
- **`region-ultrawide (legacy — prefer region-full)`** — Deprecated. Kept so existing usage keeps resolving. *Use for:* Nothing new. Use `region-full`.
|
|
192
192
|
- **Not** `region-full`, which is what this became.
|
|
193
193
|
|
|
194
|
-
### typographyOverrides (
|
|
194
|
+
### typographyOverrides (61)
|
|
195
195
|
|
|
196
|
+
- **`type-base`** — The base size the page type scale is built on, as a unitless px number — 16 by default. Every page tier keeps its ratio to it, so `14` rebuilds the whole scale at 14, and the baseline step and spacing follow the body down. *Use for:* When the product asks for smaller or larger type everywhere: a dense technical tool at 13-14, a reading product at 17. Set it on :root, a theme scope or a preset (`tokens.shared["type-base"]: "14"`), not per component.
|
|
197
|
+
- **Not** Changing the root font-size, which also moves every rem the app wrote and the control ladder; or overriding tier sizes one by one, which breaks the ratios between them. Unitless: `14`, never `14px`.
|
|
196
198
|
- **`type-display-4xl-size`** — The largest type: display-lg through display-4xl. Marketing-scale, one per view at most. *Use for:* A landing page hero, a section opener on a brand surface.
|
|
197
199
|
- **Not** `heading-1` for page titles. Display is a *statement*; headings are *structure*. A settings page titled in display-3xl looks like an advert.
|
|
198
200
|
- **`type-title-lg-size`** — The title of a *component*, not of a document. *Use for:* A card's heading, a dialog title, a section label inside a panel.
|
|
@@ -91,7 +91,7 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
|
|
|
91
91
|
- **Instead of** `scrollbar-quiet` on a vertical list. A vertical region with no bar and no fade tells a reader nothing about how far through they are.
|
|
92
92
|
- `<div className="flex gap-3 overflow-x-auto scrollbar-hidden">…</div>`
|
|
93
93
|
|
|
94
|
-
### text (
|
|
94
|
+
### text (13)
|
|
95
95
|
|
|
96
96
|
- **`sable-list`** — A bulleted list as typography: body type, a hanging muted marker, and item rhythm. Put it on a native `<ul>`. *Use for:* Running content with bullets: release notes, a feature list, steps that are not ordered.
|
|
97
97
|
- **Instead of** A stack of `<div>`s with a hand-drawn dot, or an unstyled `<ul>` that inherits the reset (no markers, no indent).
|
|
@@ -108,6 +108,9 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
|
|
|
108
108
|
- **`text-overline`** — The eyebrow / overline / kicker tier: small, letter-spaced, uppercase by default (a theme may set sentence case via `--type-overline-transform`). *Use for:* A section eyebrow above a heading, a stat's label, a badge-style category line. Sits ABOVE and categorises.
|
|
109
109
|
- **Instead of** `text-label-xs uppercase tracking-wider` assembled by hand, or `text-caption`, which sits BELOW and explains. In tool chrome, panel headers use `text-ui-section` instead.
|
|
110
110
|
- `<p className="text-overline text-muted-foreground">Latest news</p>`
|
|
111
|
+
- **`text-ui-title`** — The title of what a panel or inspector shows — the open issue, the selected object: 14px at 600 on a 20px line, one step above `text-ui-md`. *Use for:* The one title at the top of an inspector, a docked panel or a peek view inside the workspace register. Compact `text-title-lg` resolves to it on its own.
|
|
112
|
+
- **Instead of** `text-ui-md` made heavier (a title the size of the description under it reads as a label), or `text-heading-*`, which is page structure rather than chrome.
|
|
113
|
+
- `<h2 className="text-ui-title text-panel-foreground">Refine product detail page</h2>`
|
|
111
114
|
- **`text-ui-md`** — Workspace-register chrome text, the default tier: ~12px at weight ~500 on 16px leading. *Use for:* Labels and values in tool chrome — panels, inspectors, toolbars, trees — inside the workspace register.
|
|
112
115
|
- **Instead of** `text-label-sm` or `text-body-sm` in a dense panel (the page register's weights read as shouting at this size), or a size class plus a weight class composed by hand.
|
|
113
116
|
- `<span className="text-ui-md text-panel-foreground">Opacity</span>`
|
|
@@ -49,7 +49,10 @@ keyboard/focus behavior, semantic token roles and motion policy. Explain which
|
|
|
49
49
|
behavior a replacement gains and what must remain. An unused page pattern is not
|
|
50
50
|
drift; repeated overrides can indicate a missing system hook rather than app misuse.
|
|
51
51
|
Run relevant `sable doctor`, `sable lint <paths> --json` and `sable grade <paths>
|
|
52
|
-
--json` checks.
|
|
52
|
+
--json` checks. Grade's `rebuiltParts` lists elements whose class names stand in
|
|
53
|
+
for a part (`.crumb` for Breadcrumbs, `.property` for PropertyRow) and app CSS
|
|
54
|
+
that re-lays a component from outside; `modes` lists colours set for light with no
|
|
55
|
+
dark value, and whether anything lets a user switch modes. Report each. Lint is lexical; source grades and pattern similarity do not judge
|
|
53
56
|
taste. Deliberately static motion slots remain valid with appropriate feedback.
|
|
54
57
|
Lint's `label-contrast` measures an app's overrides of the brand fills (`--primary`,
|
|
55
58
|
`--sidebar-primary`, `--highlight`) and status fills (`--destructive`, `--success`,
|
|
@@ -58,6 +61,37 @@ names the nearest pair that reads. Report it with that pair as a recommendation.
|
|
|
58
61
|
A reasoned `design-lint-disable-next-line label-contrast` is the app's recorded
|
|
59
62
|
choice: list it as intentional, not as a defect.
|
|
60
63
|
|
|
64
|
+
## Adoption: an app wired for Sable that barely uses it
|
|
65
|
+
|
|
66
|
+
The commonest drift is not a wrong prop. It is an app where an agent never
|
|
67
|
+
loaded this skill: the stylesheet and tokens are wired, and every control is the
|
|
68
|
+
app's own (a `Modal`, a `PrimaryButton`, a `Tabs` written from scratch). Wiring
|
|
69
|
+
checks pass, and lint only sees the raw elements. `sable check` (and `sable
|
|
70
|
+
grade --json` → `adoption`) measures it: how many UI files compose Sable, which
|
|
71
|
+
app-defined components are named for something Sable ships and do the behaviour
|
|
72
|
+
by hand, and what the app's foundation is. `sable doctor` warns when adoption is
|
|
73
|
+
`none` or `low`. It is never a failure: whether working screens move is the
|
|
74
|
+
user's call.
|
|
75
|
+
|
|
76
|
+
Put the choice to the user before changing anything. Ask, don't migrate silently
|
|
77
|
+
and don't leave it unmentioned. Show the swaps, behaviour first (overlays,
|
|
78
|
+
controls, tabs, menus, anything with focus or keyboard handling), with what each
|
|
79
|
+
one gains ("your Modal has no focus trap or Esc; Sable's Dialog has both").
|
|
80
|
+
Layout and look stay the app's. How strongly to recommend it depends on the
|
|
81
|
+
`foundation` the report names:
|
|
82
|
+
|
|
83
|
+
| Foundation | What to say |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `ad-hoc`: no system besides Sable | Recommend the move plainly. There is nothing to preserve but one-off components, and every one is a behaviour bug waiting. Offer to do it a screen at a time. |
|
|
86
|
+
| `own`: a folder of the app's own primitives | Offer the swaps and say what each gains. Keep the app's own where the user prefers them; suggest building them on Sable's behaviour (a `Button` wrapper around Sable's `Button`) instead. |
|
|
87
|
+
| `library`: another component library (MUI, Radix, shadcn-style `components/ui`) | Ask whether Sable should replace it or only cover new work. Don't mix two control libraries on one screen. |
|
|
88
|
+
| `sable`: Sable already carries the UI | Mention the few swaps listed when those screens are next touched. |
|
|
89
|
+
|
|
90
|
+
Use the question tool the host offers when there is one. Record the answer in
|
|
91
|
+
the project's instructions, so the next task doesn't ask again. On a yes, move
|
|
92
|
+
one screen or component at a time with `fix`/`polish`, running `sable check` and
|
|
93
|
+
looking at the rendered states after each.
|
|
94
|
+
|
|
61
95
|
## Deliver evidence-backed findings
|
|
62
96
|
|
|
63
97
|
Save the audit under `design/<surface>/audit/` or the existing review location.
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Marketing, docs, settings, a feed, a dashboard — anything whose primary
|
|
4
4
|
interaction is reading and navigating. This is the default register; if you are
|
|
5
|
-
building an editor
|
|
6
|
-
|
|
5
|
+
building an editor, a canvas app, or a records workspace where people work
|
|
6
|
+
through items with the selected one open beside the list (an issue tracker, a
|
|
7
|
+
support queue), see [building-a-tool.md](./building-a-tool.md).
|
|
7
8
|
|
|
8
9
|
## Start from the brief, then choose composition
|
|
9
10
|
|