@ux-builder/ui 0.0.0-stage → 0.6.1
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 +275 -0
- package/README.md +293 -2
- package/THIRD-PARTY-NOTICES.md +195 -0
- package/catalogue.json +19424 -0
- package/dist/icons.js +2 -0
- package/dist/index.js +537 -0
- package/dist/index.js.map +1 -0
- package/dist/patterns.css +900 -0
- package/dist/patterns.js +4235 -0
- package/dist/patterns.js.map +1 -0
- package/dist/shared-Bom_GJww.js +18858 -0
- package/dist/shared-Bom_GJww.js.map +1 -0
- package/dist/shared-CjrUPFqv.js +1501 -0
- package/dist/shared-CjrUPFqv.js.map +1 -0
- package/dist/ui.css +997 -0
- package/licenses/carbon.LICENSE +201 -0
- package/licenses/fluent.LICENSE +24 -0
- package/licenses/lucide.LICENSE +43 -0
- package/licenses/material.LICENSE +202 -0
- package/licenses/vendored.json +214 -0
- package/lint/core.mjs +158 -0
- package/lint/eslint.mjs +128 -0
- package/lint/stylelint.mjs +35 -0
- package/package.json +104 -4
- package/skill/ux-builder-ui/SKILL.md +280 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Building UI with @ux-builder/ui (for AI coding agents)
|
|
2
|
+
|
|
3
|
+
This app's look comes from the AI-UI SDK: widgets from `@ux-builder/ui`, colours,
|
|
4
|
+
type and shapes from the theme in `@ux-builder/tokens`. The theme can change
|
|
5
|
+
(another client, another design system, light or dark) without any change
|
|
6
|
+
to the app, as long as the app follows the rules below.
|
|
7
|
+
|
|
8
|
+
## Rules
|
|
9
|
+
|
|
10
|
+
<!-- rules:start -->
|
|
11
|
+
1. Use the SDK's widgets (@ux-builder/ui) for buttons, fields, choices, tables, menus, tabs, breadcrumbs, sheets, notices, status pills, toasts, progress bars, spinners, skeletons, icons and the logo; do not build your own. Icons beyond the shared names come from @ux-builder/ui/icons, never hand-drawn.
|
|
12
|
+
2. Style your own markup with --piq-* role tokens (var(--piq-color-text), var(--piq-space-4), var(--piq-radius-control)); pick tokens by role, not by how they look today.
|
|
13
|
+
3. Never hard-code colours, font families or radii: no hex, rgb(), named colours, font names or pixel radii in CSS or style props.
|
|
14
|
+
4. Run the lint rule ux-builder/no-hard-coded-values (@ux-builder/ui/stylelint, @ux-builder/ui/eslint) with ux-builder/no-foreign-ui and ux-builder/no-inline-svg, then the theme audit on the running app (npx ux-builder-audit <url>, @ux-builder/audit), and fix what they report.
|
|
15
|
+
5. Select themes with ThemeProvider (theme, appearance) or data-theme and data-appearance on <html>; never restyle per theme in app code.
|
|
16
|
+
6. Compose screens from @ux-builder/ui/patterns before writing any layout CSS: AppShell for the frame; CollectionList, ListDetail, RecordDetail, FormPage, QuickEdit, Dashboard, ReportViewer, SettingsPage and ErrorScreen for screens; the regions and the layout primitives (Stack, Cluster, Grid, Page, SplitView) for anything else. Supply content, data and actions through their slots; never fork or restyle a pattern.
|
|
17
|
+
<!-- rules:end -->
|
|
18
|
+
|
|
19
|
+
## Setting up
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
// once, at the entry point, before the app's own CSS
|
|
23
|
+
import "@ux-builder/tokens/themes/<theme>.css";
|
|
24
|
+
import "@ux-builder/tokens/fonts/<theme>.css";
|
|
25
|
+
import "@ux-builder/ui/ui.css";
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
import { ThemeProvider } from "@ux-builder/ui";
|
|
30
|
+
|
|
31
|
+
<ThemeProvider theme="<theme>" appearance="system">
|
|
32
|
+
<App />
|
|
33
|
+
</ThemeProvider>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Doing common things
|
|
37
|
+
|
|
38
|
+
- **A filter or view switch** with two to five options: `<ChoicePicker>`, not
|
|
39
|
+
a row of buttons.
|
|
40
|
+
- **A primary action**: `<Button>`; the action beside it:
|
|
41
|
+
`<Button variant="secondary">`; a quiet one: `<Button variant="text">`. A
|
|
42
|
+
count on it: `badge`.
|
|
43
|
+
- **A form**: `<TextField>`, `<TextArea>`, `<Select>`, `<Checkbox>`,
|
|
44
|
+
`<Switch>`, `<RadioGroup>`, each with its `label`; errors go in `error`,
|
|
45
|
+
help in `hint`.
|
|
46
|
+
- **A list of records**: `<DataTable>` with `columns` and `rows`; statuses
|
|
47
|
+
in a cell with `<StatusPill>`.
|
|
48
|
+
- **A message in the page**: `<Notice tone="warning">`; after an action,
|
|
49
|
+
`useToast()`.
|
|
50
|
+
- **An icon**: `<Icon name="search" />`; give it a `label` when it is the
|
|
51
|
+
only thing saying what a control does.
|
|
52
|
+
- **Your own layout**: plain elements styled with role tokens:
|
|
53
|
+
|
|
54
|
+
```css
|
|
55
|
+
.matter-summary {
|
|
56
|
+
padding: var(--piq-space-4);
|
|
57
|
+
background: var(--piq-color-surface);
|
|
58
|
+
color: var(--piq-color-text);
|
|
59
|
+
border: var(--piq-border-default);
|
|
60
|
+
border-radius: var(--piq-radius-card);
|
|
61
|
+
font-family: var(--piq-font-body);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- **Light and dark**: never write a dark-mode stylesheet. `useTheme()`
|
|
66
|
+
gives `setAppearance`; the tokens change by themselves.
|
|
67
|
+
|
|
68
|
+
## Do not
|
|
69
|
+
|
|
70
|
+
- Copy a widget's markup or CSS into the app.
|
|
71
|
+
- Write a colour, font family or radius as a literal, even one that matches
|
|
72
|
+
the theme today; it will be wrong under the next theme.
|
|
73
|
+
- Style a widget by targeting its `.piq-*` classes or setting its widget
|
|
74
|
+
tokens; use its props.
|
|
75
|
+
- Check for a theme name in app code to change styles.
|
|
76
|
+
|
|
77
|
+
<!-- catalogue:start -->
|
|
78
|
+
## Reference (generated from the SDK; do not edit by hand)
|
|
79
|
+
|
|
80
|
+
@ux-builder/ui 0.6.1, @ux-builder/tokens 1.5.0. The full catalogue, with every token and prop, is `@ux-builder/ui/catalogue.json`.
|
|
81
|
+
|
|
82
|
+
### Components
|
|
83
|
+
|
|
84
|
+
| Component | Props | Use it for |
|
|
85
|
+
|-----------|-------|------------|
|
|
86
|
+
| `<AppearanceSwitch>` | `value = "system", onChange, label = "Appearance", labels = { light: "Light", system: "Auto", dark: "Dark" }, available, className` | Light, the system's setting, or dark, as three icons in a pill: a radio group (arrow keys move and choose), each option named in text for assistive technology and shown on hover, the chosen one raised on a disc so the choice is never colour alone. value is "light", "system" or "dark", as ThemeProvider's appearance takes; available lists the appearances the theme has, and the others are disabled. |
|
|
87
|
+
| `<Breadcrumbs>` | `items = [], label = "Breadcrumb", onNavigate, className` | Where the page sits in a hierarchy, as an ordered list of links in a navigation landmark; the last item is the current page. Where it has little width (the compact size class) it shows only a link back to the parent. |
|
|
88
|
+
| `<Button>` | `variant = "primary", size, badge, busy = false, type = "button", className = "", onClick, children` | A button in one of three emphases: primary for the one action a view wants, secondary beside it, text for actions that should not compete. While busy it shows a spinner over its label, keeps its width and name, and ignores clicks. |
|
|
89
|
+
| `<Card>` | `title, actions, children, as = "section"` | A bounded group of content with an optional title and actions. |
|
|
90
|
+
| `<Checkbox>` | `label, className = ""` | One on/off choice with its label. |
|
|
91
|
+
| `<ChoicePicker>` | `label, options, value, onChange, disabled` | One choice from a few, every option in view: a radio group drawn as segments. It is a single tab stop; the arrow keys, Home and End move the selection, as a native radio group does. |
|
|
92
|
+
| `<Combobox>` | `label, options = [], value, onChange, placeholder, hint, error, emptyLabel = "No matches", id, className` | A choice from a long list by typing: the WAI-ARIA Combobox pattern with a listbox popup. Typing filters the options; Down and Up move the active option (focus stays in the field, aria-activedescendant follows); Enter chooses; Escape closes without changing the choice; with no match it says so. |
|
|
93
|
+
| `<DataTable>` | `columns, rows, rowKey = "id", caption, pageSize = 10, initialSort` | A table that sorts and pages. Sortable headers are buttons with aria-sort on their cell, so the order is announced; numbers align right and sort as numbers. |
|
|
94
|
+
| `<DatePicker>` | `label, value, onChange, min, max, hint, error, id, className` | A date, on the browser's own date input, so the keyboard, the calendar and screen readers work everywhere. value and onChange use YYYY-MM-DD. |
|
|
95
|
+
| `<DateRangePicker>` | `label, from, to, onChange, min, max, fromLabel = "From", toLabel = "To", className` | A date range: From and To dates in a group named by label. The To date cannot be before the From date. |
|
|
96
|
+
| `<Dialog>` | `open = false, onClose, title, description, actions, closeLabel = "Close", initialFocus, role, className, children` | A modal dialog in the middle of the screen: a short task or a decision. The page behind is inert, focus moves in (to initialFocus, a ref, if given) and returns to what opened it; Escape or the close button calls onClose. |
|
|
97
|
+
| `<Favicon>` | `—` | Keeps the page's icon on the theme's favicon, for a page that sets data-theme and data-appearance on <html> without a ThemeProvider; renders nothing. |
|
|
98
|
+
| `<Icon>` | `name, icon, set, size = "1em", label, className = ""` | An icon by shared name (ICON_NAMES) or registered name, or an icon from @ux-builder/ui/icons passed as icon, drawn in the theme's icon set, the text colour and 1em; decorative unless label names it. |
|
|
99
|
+
| `<Logo>` | `variant = "logo", size = "md", label, className = ""` | The active theme's logo, or its mark (variant="mark"), drawn from the brand's artwork; a brand without artwork gets its title as a wordmark in the logo colour. |
|
|
100
|
+
| `<MenuButton>` | `label, items = [], icon, variant = "secondary", size, align = "start", buttonLabel, className` | A button that opens a menu of actions: account, workspace, a page's or a row's "More". Keyboard as the WAI-ARIA Menu Button pattern: Enter, Space or Down opens on the first item, Up on the last; arrows, Home and End move; a letter jumps to the next item starting with it; Escape closes and returns focus to the button. |
|
|
101
|
+
| `<Notice>` | `tone = "info", title, children` | A message in the flow of the page. The tone sets its colour and its mark; the title says the point, so the colour is never the only signal. |
|
|
102
|
+
| `<Pagination>` | `page = 1, pageSize = 20, total = 0, onPageChange, pageSizes = [10, 20, 50, 100], onPageSizeChange, label = "Pagination", ofLabel = "of", previousLabel = "Previous page", nextLabel = "Next page", pageSizeLabel = "Rows per page", className` | Moving through a long list a page at a time: where the user is ("21–40 of 312"), previous and next, and how many rows a page shows. Pages count from 1. |
|
|
103
|
+
| `<ProgressBar>` | `label, value, max = 100, valueText, showValue = false, hideLabel = false, className = ""` | A bar showing how much of a task is done, named by its visible label; leave out value while the amount is unknown and it moves instead. |
|
|
104
|
+
| `<RadioGroup>` | `label, options, name, value, defaultValue, onChange, disabled` | One choice from a short list, each option with its label, in a fieldset with a legend. |
|
|
105
|
+
| `<Select>` | `label, hint, error, id, className = "", options, placeholder` | A dropdown: one choice from a longer list, with its label, and an optional placeholder, hint or error. |
|
|
106
|
+
| `<Sheet>` | `open = false, onClose, title, label, side = "start", closeLabel = "Close", initialFocus, className, children` | A modal panel that slides in from an edge: the navigation drawer, filters on a phone, a short form. It is a modal dialog: the page behind is inert, focus moves into it and returns to what opened it, and Escape, the close button or a click outside calls onClose. |
|
|
107
|
+
| `<Skeleton>` | `variant = "text", lines = 1, width, height, className = "", style` | A placeholder in the shape of content that is loading: text lines, a block or a circle. It is hidden from assistive technology, so mark the region that holds it aria-busy="true". |
|
|
108
|
+
| `<Spinner>` | `size = "md", label = "Loading", showLabel = false, className = ""` | A turning ring for a short wait, announced as a status with its label (spoken only, unless showLabel puts it beside the ring). |
|
|
109
|
+
| `<StatusPill>` | `state, children` | An availability status. It is a label, never a target, and it always says its state in words, so colour is never the only signal. |
|
|
110
|
+
| `<Switch>` | `label, className = ""` | A setting that takes effect at once: a checkbox announced as on or off. |
|
|
111
|
+
| `<Tabs>` | `tabs = [], value, defaultValue, onChange, label, className` | Panels that share one place, one shown at a time, chosen by a row of tabs. Keyboard as the WAI-ARIA Tabs pattern: Left and Right (mirrored in a right-to-left page) move between tabs and show each, Home and End go to the ends; Tab moves into the panel. |
|
|
112
|
+
| `<TextArea>` | `label, hint, error, id, className = "", rows = 4` | Several lines of typed input with its label, and an optional hint or error. |
|
|
113
|
+
| `<TextField>` | `label, hint, error, id, className = ""` | A single line of typed input with its label, and an optional hint or error. |
|
|
114
|
+
| `<ThemeProvider>` | `theme, appearance = "system", tokens, favicon = true, onChange, as = "div", style, children` | Sets the theme and appearance (light, dark or system) for its subtree, and the page's favicon from the theme; pass tokens instead of theme for an embedded view. |
|
|
115
|
+
| `<Toast>` | `message, actionLabel, onAction, onMouseEnter, onMouseLeave, onFocus, onBlur` | A short confirmation after an action, with Undo. The region is a polite live region, so a screen reader announces each toast without moving focus. |
|
|
116
|
+
| `<ToastProvider>` | `children` | Holds the toasts for its subtree and shows them in a polite live region; wrap the app once. |
|
|
117
|
+
| `<Tooltip>` | `content, children, placement = "top", className` | A short description of a control, shown while the pointer is over it or it has keyboard focus: the WAI-ARIA Tooltip pattern. 13). |
|
|
118
|
+
|
|
119
|
+
### Layout
|
|
120
|
+
|
|
121
|
+
Lay out screens with the layout primitives from `@ux-builder/ui/patterns` (load `@ux-builder/ui/patterns.css` after `ui.css`), never with your own flex or grid CSS. Each adapts to the width it is given, not the window, so it works in a narrow embed as on a phone. Size classes: compact from 0px, medium from 600px, expanded from 840px, large from 1200px, extra-large from 1600px.
|
|
122
|
+
|
|
123
|
+
| Primitive | Props | Use it for |
|
|
124
|
+
|-----------|-------|------------|
|
|
125
|
+
| `<Cluster>` | `gap = 3, justify = "start", align = "center", wrap = true, as = "div", className, children` | A row of children that wraps onto further rows when it runs out of width: buttons, tags, filters. justify is start, center, end or between. |
|
|
126
|
+
| `<Grid>` | `columns = { compact: 1, medium: 2, expanded: 3, large: 4 }, gap = 4, as = "div", className, children` | Equal columns whose count follows the width the grid is given, per size class: columns is a number for every class, or { compact, medium, expanded, large, extraLarge } where a class left out keeps the previous class's count. Counts run from 1 to 6. |
|
|
127
|
+
| `<Page>` | `width = "page", as = "div", className, children` | The frame of a screen's content: the design system's page gutter for the size class, and a maximum width (page, form or reading) centred in the space. |
|
|
128
|
+
| `<SplitView>` | `primary, secondary, show = "primary", ratio = "1:2", primaryLabel, secondaryLabel, className` | Two panes: side by side from the expanded size class up, and below it one pane at a time, the one named by show (primary or secondary). The app decides which pane shows, for example the detail once a record is chosen. |
|
|
129
|
+
| `<Stack>` | `gap = 4, align = "stretch", as = "div", className, children` | A vertical run of children with one of the theme's space steps between them. gap is a step from 0 to 8. |
|
|
130
|
+
|
|
131
|
+
| Pattern | Props | Use it for |
|
|
132
|
+
|---------|-------|------------|
|
|
133
|
+
| `<ApiKeys>` | `title = "API keys", keys = [], created, onCreate, onCreatedClose, onRevoke, className` | API keys: a new key is shown in full once, with a copy button; after that only masked, and the full key is not in the page; revoking is confirmed. keys: [{ id, name, last4, created }]; created: { name, secret } while the new key is being shown, then the app clears it on onCreatedClose(); onCreate(); onRevoke(key). |
|
|
134
|
+
| `<AppShell>` | `nav = [], current, navLabel = "Main", brand, actions, breadcrumbs, onNavigate, moreLabel = "More", skipLabel = "Skip to content", adapt = "window", children, className` | The frame of an app: its navigation, a top bar for the brand, workspace and account, and the content. Navigation follows the window: from the expanded size class a persistent side navigation in the design system's style; at medium a rail; below it a bottom bar with up to five destinations (four and More when there are more), the rest in a drawer. |
|
|
135
|
+
| `<Attachments>` | `label = "Attachments", files = [], onUpload, onRemove, accept, uploadLabel = "Upload a file", className` | 7); each file with its name (a link to it), type, size and version; removing one is confirmed. files: [{ id, name, href, type, size, version }]; onUpload(file); onRemove(file). |
|
|
136
|
+
| `<AuditLog>` | `title = "Audit log", entries = [], people = [], actions = [], filters = [], onFilter, onRemoveFilter, onClearFilters, locale, className` | Who changed what and when, read only: each entry with the person, the change, the time and the value before and after, filtered by person, action and date (applied filters as removable chips). The app filters: onFilter({ person, action, from, to }); filters: the applied ones as [{ id, label }]; onRemoveFilter(filter). |
|
|
137
|
+
| `<Board>` | `label, columns = [], cards = [], onMove, moveLabel = "Move to", className` | Records grouped by status in columns, each headed with its count. 7). |
|
|
138
|
+
| `<BulkActionBar>` | `count = 0, children, onClearSelection, clearLabel = "Clear selection", label = "Bulk actions", selectedText = (n) => `${n} selected`, className` | The actions for the selected rows, shown while any are selected, with how many are selected; the count is announced as it changes. A toolbar: arrow keys move between its controls. |
|
|
139
|
+
| `<Calendar>` | `label = "Calendar", events = [], date = new Date(), view, onViewChange, locale, className` | Events in time: month, week and day views from a view switch. The month is a grid of days that the arrow keys move through (WAI-ARIA Grid); every event is named in text with its time. |
|
|
140
|
+
| `<ChartFrame>` | `title, description, legend = [], table, status = "ready", onRetry, showTableLabel = "Show data table", hideTableLabel = "Hide data table", emptyTitle = "No data for this period", actions, children, className` | The frame around a chart the app draws: a title, a legend in text, the chart, and a data table with the same values behind a button, for keyboard and screen-reader users and anyone who wants the numbers. status is ready, loading, empty or error, shown in the chart's own space; error offers retry. |
|
|
141
|
+
| `<CollectionList>` | `title, breadcrumbs, statusPill, actions, secondaryActions, overflow, columns = [], rows = [], rowKey = "id", rowLabel, caption, status = "ready", onRetry, search, onSearchChange, searchLabel = "Search", filters = [], onRemoveFilter, onClearFilters, filterPanel, filtersLabel = "Filters", selectable = false, selected = [], onSelectedChange, bulkActions, rowActions, rowActionsLabel = (name) => `Actions for ${name}`, selectRowLabel = (name) => `Select ${name}`, selectAllLabel = "Select all on this page", sort, onSortChange, page = 1, pageSize = 20, total, onPageChange, pageSizes, onPageSizeChange, empty, showResultsLabel = "Show results", className` | A screen for finding and acting on records: the page header, search and filters, the records as a table (from the medium size class; only the priority columns until expanded) or as stacked items (compact), selection with bulk actions, row action menus, sorting, pagination, and a state panel for empty, no results, no permission, a failed or partial load, with skeletons while loading. The app supplies the rows of the current page and owns the query (useListQuery keeps it in the URL); the list fetches nothing. |
|
|
142
|
+
| `<CommandPalette>` | `recent = [], results = [], onSearch, onSelect, buttonLabel = "Search", placeholder = "Search records and actions", recentLabel = "Recent", emptyLabel = "No results", className` | Search everything and go: Ctrl+K or Cmd+K (or its button) opens a modal dialog with focus in a combobox. Before typing it lists recent items; typing asks the app (onSearch(query)) and lists the results it returns (results: [{ label, items: [{ id, label }] }]) in labelled groups. |
|
|
143
|
+
| `<ConfirmDialog>` | `open = false, onConfirm, onCancel, title, objectName, consequence, confirmLabel = "Delete", cancelLabel = "Cancel", requireTyping = false, typeLabel = (name) => `Type ${name} to confirm`, className` | The question before a destructive action: it names the object and what will be lost, starts focus on Cancel, and with requireTyping makes the user type the object's name before the action becomes available. |
|
|
144
|
+
| `<ContextMenu>` | `items = [], label = "Actions", children` | Right-click shortcuts for an element: a menu at the pointer on right-click, or beside the element on Shift+F10 or the ContextMenu key; the WAI-ARIA Menu pattern (arrows, Enter, Escape returns focus). Its items must also be in a visible menu (a MenuButton) so nothing depends on right-clicking: pass the same items to both. |
|
|
145
|
+
| `<Dashboard>` | `title, breadcrumbs, actions, filters, dateRange, refreshedAt, refreshedLabel = "Last refreshed", locale, tiles = [], charts = [], children, className` | A dashboard: page-wide filters, the date range and when the data was last refreshed, above a grid of number tiles (two across at compact size, four from expanded) and a grid of charts (one, then two across). filters and dateRange are slots; tiles and charts are lists of elements. |
|
|
146
|
+
| `<DescriptionList>` | `items = [], className` | A record's fields as label and value pairs, read only: two columns where it has room (from the medium size class), one where it has not. An item with wide spans both columns. |
|
|
147
|
+
| `<DrillDown>` | `trail = [], onSelect, label = "Drill-down", children, className` | A drill-down's trail: the summary, then each applied level, the current one marked; choosing an earlier one goes back to it. trail: [{ label }] from the summary to the current level; onSelect(index). |
|
|
148
|
+
| `<EditableGrid>` | `label, columns = [], rows = [], rowKey = "id", onChange, errors = {}, newRow = () => ({}), addLabel = "Add row", removeLabel = (n) => `Remove row ${n}`, editLabel = (n) => `Edit row ${n}`, totalLabel = "Total", saveLabel = "Save", className` | Rows edited in place, like a spreadsheet: the WAI-ARIA Grid pattern. The grid is one stop in the Tab order; arrow keys, Home and End move between cells; Enter or F2 edits a cell, Enter commits and Escape discards; rows are added and removed; an error the app reports is tied to its cell; a totals row sums the numeric columns. |
|
|
149
|
+
| `<ErrorScreen>` | `kind = "not-found", title, children, actions, className` | A whole screen that says something went wrong and how to go on: not found, no permission, offline, maintenance or an expired session (kind). One level-1 heading states what happened. |
|
|
150
|
+
| `<ErrorSummary>` | `errors = {}, idOf = (n) => n, title = "There is a problem", attempt = 0, className` | The problems with a submitted form, at its top: a heading and a link to each invalid field. It takes focus when it appears, so keyboard and screen reader users learn what is wrong at once, and each link moves focus to its field. |
|
|
151
|
+
| `<ExportDialog>` | `open = false, onClose, title = "Export", formats = [], fields = [], onExport, progress = null, ready, exportLabel = "Export", className` | Taking records out: a modal dialog to choose the format and the fields, then the export runs; a long one shows its progress outside the dialog, so the user can close it and keep working, and announces when the file is ready with a link to it. formats and fields: [{ value, label }]; onExport({ format, fields }); progress (0-100 while running, else null); ready: { href, label }. |
|
|
152
|
+
| `<FilterBar>` | `search, onSearchChange, searchLabel = "Search", filters = [], onRemoveFilter, onClearAll, clearLabel = "Clear all", onOpenFilters, filtersOpen, filtersLabel = "Filters", appliedLabel = "Applied filters", removeLabel = (f) => `Remove filter: ${f.label}`, children, className` | Search and filtering above a list: a search field, the applied filters as removable chips with Clear all, and a Filters button that opens the filter panel and shows how many are applied. Where it has little width (compact) only the search and the Filters button show. |
|
|
153
|
+
| `<FilterPanel>` | `open = false, onClose, mode = "inline", title = "Filters", closeLabel = "Close filters", footer, children, className` | The filters themselves, in collapsible sections (FilterSection): a side panel beside the list (mode inline), or a full-screen sheet where there is little width (mode sheet). CollectionList chooses the mode by its width. |
|
|
154
|
+
| `<FilterSection>` | `title, count, defaultOpen = true, children, className` | One group of filters that opens and closes, with an optional count of matching records. |
|
|
155
|
+
| `<FormPage>` | `title, breadcrumbs, description, form, onCancel, submitLabel = "Save", cancelLabel = "Cancel", discardTitle = "Discard your changes?", discardText = "What you have typed will be lost.", keepLabel = "Keep editing", discardLabel = "Discard", children, className` | A full-page form for creating or editing a record: one column of fields with labels above, no wider than the design system's form width; an error summary that takes focus when a submit finds problems; Save and Cancel in a bar that stays in view (and above the on-screen keyboard) without ever covering the focused field; a warning before leaving with unsaved changes, and a question before Cancel discards them. form comes from useFormState; fields go in children. |
|
|
156
|
+
| `<FormSection>` | `title, description, children, className` | A group of related fields with a heading. |
|
|
157
|
+
| `<HomeScreen>` | `title = "Home", tiles = [], assigned, recent, children, className` | The first screen after signing in: a summary for this person, from slots: number tiles (StatTile elements), their assigned work and their recent items, each list with its own empty state. assigned and recent: { title, items: [{ id, label, href, meta? |
|
|
158
|
+
| `<ImportFlow>` | `title = "Import records", onFile, columns = [], fields = [], onMap, errors = [], failedRowsHref, preview, onCommit, result, accept = ".csv,.xlsx", className` | 7), map each file column to a field, validation errors by row with a file of the failed rows, a preview of the rows to save, then the import and the app's result report. The app reads, checks and saves: onFile(file), onMap(mapping), onCommit(); it supplies columns, fields, errors ([{ row, message }]), failedRowsHref, preview ({ columns, rows }) and result (a node). |
|
|
159
|
+
| `<Integrations>` | `title = "Integrations", items = [], onConnect, onDisconnect, className` | Connections to other services: each with its status in text, Connect when it is not connected and Disconnect when it is. items: [{ id, name, description, connected }]; onConnect(item); onDisconnect(item). |
|
|
160
|
+
| `<ListDetail>` | `items = [], selected, onSelect, renderItem = (it) => it.label ?? it.id, listLabel = "Records", detail, backLabel = "Back", ratio = "1:2", className` | A list of records with the chosen record's detail: side by side from the expanded size class, where selection follows the arrow keys; below it the list first, and choosing a record (Enter, Space or a click) opens its detail as its own view with Back, which returns to the list on that record. When two panes become one, the pane holding focus is the one kept, so focus is never lost. |
|
|
161
|
+
| `<NotificationCentre>` | `notifications = [], onMarkRead, onMarkAllRead, preferences, onPreferenceChange, label = "Notifications", className` | Notifications: a button whose name says how many are unread ("Notifications, 3 unread"), opening a panel where each says Read or Unread in text and links to its record; mark one or all as read; channel preferences per kind of notification. notifications: [{ id, title, href, read }]; onMarkRead(notification); onMarkAllRead(); preferences: { kinds, channels: [{ value, label }], value: { kind: { channel: on } } }; onPreferenceChange(kind, channel, on). |
|
|
162
|
+
| `<PageHeader>` | `title, breadcrumbs, status, description, actions, secondaryActions, overflow, overflowLabel = "More actions", className` | The top of a screen: one level-1 heading, optional breadcrumbs and status, the primary action, secondary actions beside it, and any further actions in a More menu (overflow: MenuButton items). Its size follows the design system's page-header variant. |
|
|
163
|
+
| `<PeriodComparison>` | `label, current, previous, comparison = "on the previous period", format = (n) => String(n), currentLabel = "This period", previousLabel = "Previous", className` | 1). format(n) writes a value. |
|
|
164
|
+
| `<PermissionsMatrix>` | `title = "Permissions", roles = [], permissions = [], has = () => false, yes = "Yes", no = "No", className` | Which role may do what: a table with a column per role and a row per permission, each cell saying Yes or No in text. has(role, permission). |
|
|
165
|
+
| `<PivotTable>` | `label, groups = [], columns = [], value = () => 0, format = (n) => String(n), totalLabel = "Total", className` | Rows by columns with subtotals and a grand total, the row groups opening and closing: the WAI-ARIA Treegrid pattern. Right opens a group (or moves into it), Left closes it (or moves to its group), Up and Down move between visible rows. |
|
|
166
|
+
| `<PrimaryNav>` | `items = [], current, label = "Main", onNavigate, className` | An app's main navigation: destinations as links in a labelled navigation landmark, the current one marked (aria-current="page") and shown by the design system's active state. A destination with items is a group that opens and closes by a disclosure button, open while it holds the current page. |
|
|
167
|
+
| `<PrintLayout>` | `title, logo, runDate, locale, sections = [], children, className` | A report laid out for paper: a header with the title, the logo and the run date on the first page; each section from a new page; table headers repeat; navigation and actions are left out. While printing, the theme is switched to its light appearance and back afterwards. |
|
|
168
|
+
| `<QuickEdit>` | `open = false, onClose, title, form, submitLabel = "Save", cancelLabel = "Cancel", children, className` | A short form for a quick create or edit, without leaving the page: a modal dialog from the medium size class, a full-screen sheet below it. Focus stays inside while it is open and returns to the control that opened it. |
|
|
169
|
+
| `<RecordDetail>` | `title, statusPill, actions, secondaryActions, overflow, breadcrumbs, back, fields, tabs, tabsParam = "tab", tabsLabel, activity, activityLabel = "Activity", status = "ready", onRetry, emptyTitle = "Choose a record", emptyText = "Select a record in the list to see it here.", notFoundTitle = "This record no longer exists", children, className` | One record: its page header (title, status and actions), its fields, its tabs and its activity, which sits beside the rest from the expanded size class. status is ready, loading, empty (no record chosen), error, forbidden or not-found; each but ready and loading shows a state panel. |
|
|
170
|
+
| `<ReportBuilder>` | `measures = [], groupings = [], preview = () => [], onSave, saveLabel = "Save report", className` | Making a report: choose measures and groupings, see a preview table as you choose (groupings as rows, measures as columns), and save it with a name. measures and groupings: [{ value, label }]; preview(choice) returns the preview's rows; onSave({ name, measures, groupings }). |
|
|
171
|
+
| `<ReportViewer>` | `title, breadcrumbs, parameters, onRun, runLabel = "Run report", parametersLabel = "Parameters", applied = [], appliedLabel = "Run with", results, status = "ready", onRetry, onExport, exportLabel = "Export", onPrint, printLabel = "Print", className` | A report: its parameters, a Run action, and the results with a summary of the parameters they were run with above them at every size. Parameters sit in a side panel from the expanded size class, in a panel that opens and closes at medium, and in a sheet at compact. |
|
|
172
|
+
| `<ReviewConfirm>` | `title = "Check your answers", sections = [], consequence, submitLabel = "Submit", onSubmit, changeLabel = "Change", result, className` | 4). Once the app reports the result, a confirmation panel shows it with its reference. |
|
|
173
|
+
| `<SettingsPage>` | `title = "Settings", breadcrumbs, sections = [], current, onChange, navLabel = "Settings sections", backLabel = "All settings", className` | A settings screen: section navigation beside the current section from the expanded size class; below it the list of sections first, and a chosen section on its own with a link back to the list. Sections swap in place, so the current one is marked aria-current="true"; choosing one moves focus to its heading. |
|
|
174
|
+
| `<sizeAtLeast>` | `size, min` | Whether a size class is at least another: sizeAtLeast(size, "expanded"). |
|
|
175
|
+
| `<sizeClassOf>` | `width` | The size class a width falls in: compact, medium, expanded, large or extra-large. |
|
|
176
|
+
| `<StatePanel>` | `kind = "empty", title, children, action, filters = [], onClearFilters, clearLabel = "Clear filters", onRetry, retryLabel = "Try again", activeLabel = "Active filters:", className` | What a list or region shows when it has no content to show: first use (empty), nothing matching (no-results, with the active filters and a way to clear them), no permission (forbidden), a failed load (error, with retry) or a partial load (partial). Each says what happened and offers the next action, and is announced to screen readers. |
|
|
177
|
+
| `<StatTile>` | `label, value, change, words = DIRECTION, className` | One figure that matters: its label, its value and, optionally, its change (change: { direction: up, down or flat, amount, comparison }), written out in words ("up 8% on August") so direction never depends on colour or an icon. |
|
|
178
|
+
| `<Subscriptions>` | `items = [], onAdd, onPause, onDelete, title = "Subscriptions", className` | A report's scheduled deliveries and alerts: each with its frequency or threshold and its recipients; add one, pause one ("Paused" in text), or delete one after confirmation. , recipients: [], paused? |
|
|
179
|
+
| `<TabsSection>` | `tabs = [], param = "tab", label, defaultValue, onChange, className` | Tabs whose selection is kept in the page's URL (param), so a reload or a shared link opens the same tab. Tabs and their counts as for Tabs: [{ id, label, panel, count? |
|
|
180
|
+
| `<TaskList>` | `title, sections = [], statusLabels = STATUS, completedText = (n, t) => `${n} of ${t} sections completed`, onSelect, className` | A long form in independent sections, done in any order: each section a link with its status in text, and how many are completed. sections: [{ id, label, href, status: not-started, in-progress or completed }]. |
|
|
181
|
+
| `<Timeline>` | `events = [], label = "Activity", order = "newest", kindLabels = {}, locale, className` | What has happened to a record, in time order (newest first unless order is oldest): who, what and when, each kind of event (comment, change or system) named in text and marked with its own icon, so colour is never the only signal. events: [{ id, kind, actor, action, time, body? |
|
|
182
|
+
| `<useDrillPath>` | `param = "drill" } = {` | The path of a drill-down kept in the page's URL (param), one history entry per level, so a link opens the same level and Back returns to the one before. Returns { path, drill(id), goTo(index) }; path is the list of level ids. |
|
|
183
|
+
| `<useFormState>` | `initial = {}, validate = () => ({}), onSubmit } = {` | A form's values, errors and submission, as the form patterns expect. validate(values) returns { field: message } for what is wrong. |
|
|
184
|
+
| `<useListQuery>` | `key = "list", defaults = {} } = {` | A list's query (page, page size, search, filters, sort) kept in the page's URL under a key, so it survives opening a record and coming back, can be shared, and follows Back and Forward. Returns [query, update]; update merges, and a change to search or filters returns to page 1. |
|
|
185
|
+
| `<UsersAndRoles>` | `title = "Members", members = [], roles = [], onInvite, onRoleChange, className` | The members of a workspace with their roles: invite one by email with a role (an address that is not an email is an error at its field), and change a member's role. The list shows what the app says, so a change shows once the app confirms it. |
|
|
186
|
+
| `<useSizeClass>` | `ref` | The size class of an element's own width, kept up to date as it resizes. For behaviour that must change with width (a panel inline or in a sheet); layout itself changes in CSS. |
|
|
187
|
+
| `<useStickyClearance>` | `ref, active = true` | 11), even when the bar wraps onto two lines. |
|
|
188
|
+
| `<useUnsavedChangesWarning>` | `dirty` | Warns before the page is left with unsaved changes (the browser's own prompt on reload, close or a link out of the app). An app's router should also block its own navigation while dirty is true. |
|
|
189
|
+
| `<Wizard>` | `title, steps = [], review, onSaveDraft, onFinish, saveDraftLabel = "Save draft", backLabel = "Back", nextLabel = "Continue", finishLabel = "Submit", reviewTitle = "Check your answers", className` | A task in steps: "Step 2 of 5: Title" with a step list from the medium size class and a progress bar below it; each step is checked before moving on; going back keeps every answer (all steps stay in one form); Save draft gives the app what has been entered; the last step is the review. A step is { id, title, content, validate? |
|
|
190
|
+
| `<WorkQueue>` | `items = [], index = 0, onIndexChange, onApprove, onReject, onAssign, people = [], title = "Review queue", emptyTitle = "Nothing to review", emptyText = "New items will appear here.", className` | Work through items one at a time: the item, its position ("3 of 12"), previous and next, approve, reject (a comment is required) and assign. 4); a help dialog lists them. |
|
|
191
|
+
| `<WorkspaceSwitcher>` | `workspaces = [], current, onSwitch, label = "Workspace", searchLabel = "Search workspaces", searchAt = 8, emptyLabel = "No workspace matches", className` | The workspace or tenant switcher in an app's top bar. Up to seven workspaces it is a menu; from eight (searchAt) its popup has a search that narrows the list, as a combobox: type to filter, Up and Down to move, Enter to switch, Escape to close and return to the button. |
|
|
192
|
+
|
|
193
|
+
A list of records is `<CollectionList>`: give it the current page's rows and keep its query in the URL with `useListQuery()`; it brings the header, search, filters, selection and bulk actions, pagination, and the empty, no-results, no-permission, error and loading states. Never hand-build a list screen.
|
|
194
|
+
|
|
195
|
+
### Choosing a pattern
|
|
196
|
+
|
|
197
|
+
- `<AppShell>`: The frame of every app: navigation, top bar and content.
|
|
198
|
+
- `<AuditLog>`: A read-only history of who changed what, filterable. Choose `<Timeline>` instead when it is the activity of one record.
|
|
199
|
+
- `<CollectionList>`: Finding and acting on many records: search, filters, selection, bulk and row actions, pages. Never choose between a table and stacked items yourself: it shows a table from medium and stacked items at compact. Choose `<ListDetail>` instead when users look at one record at a time while keeping the list in view.
|
|
200
|
+
- `<Dashboard>`: An overview of figures and charts for a period. Choose `<ReportViewer>` instead when users set parameters, run a report and export or print it.
|
|
201
|
+
- `<ErrorScreen>`: A whole screen that cannot show: not found, no permission, offline, maintenance, session expired. Choose `<StatePanel>` instead when only one region of the screen has nothing to show.
|
|
202
|
+
- `<FormPage>`: Creating or editing a record with more than about six fields, or with sections. Choose `<QuickEdit>` instead when the form is short (about six fields or fewer) and users should stay on the page.
|
|
203
|
+
- `<HomeScreen>`: The first screen after signing in: this person's figures, work and recent items. Choose `<Dashboard>` instead when it is an overview of the business for a period.
|
|
204
|
+
- `<ImportFlow>`: Bringing records in from a file: upload, map, validate, preview, import.
|
|
205
|
+
- `<ListDetail>`: Working through records one at a time while the list stays in view, such as triage or review. Choose `<CollectionList>` instead when users mostly search, filter and act on many records at once.
|
|
206
|
+
- `<QuickEdit>`: A short create or edit (about six fields or fewer) without leaving the page. Choose `<FormPage>` instead when the form is long or has sections.
|
|
207
|
+
- `<RecordDetail>`: One record: its header, fields, tabs and activity. Choose `<FormPage>` instead when the record is being created or edited.
|
|
208
|
+
- `<ReportViewer>`: Parameters, Run, results with the applied parameters, Export and Print. Choose `<Dashboard>` instead when it is an at-a-glance overview.
|
|
209
|
+
- `<SettingsPage>`: An app's or a workspace's settings in sections.
|
|
210
|
+
- `<UsersAndRoles>`: A workspace's members: invite by email with a role, change a role. Choose `<CollectionList>` instead when it is a large directory to search and filter.
|
|
211
|
+
- `<Wizard>`: A task whose steps depend on each other, checked step by step, ending in a review. Choose `<TaskList>` instead when the sections are independent and can be done in any order; `<FormPage>` instead when it fits on one page.
|
|
212
|
+
- `<WorkQueue>`: Working through items one at a time: approvals, triage. Choose `<ListDetail>` instead when users pick items in any order from a list.
|
|
213
|
+
|
|
214
|
+
Every pattern's slots, states, size-class behaviour and variant points are in `catalogue.json` (`patterns`) and from the MCP tools `list_patterns` and `get_pattern`.
|
|
215
|
+
|
|
216
|
+
Frame an app with `<AppShell>`: it places the navigation for the screen size (bottom bar and drawer, rail, or side navigation) and keeps the bars from covering focus. Navigation between pages is links (`PrimaryNav`), never a menu; `<MenuButton>` is for actions.
|
|
217
|
+
|
|
218
|
+
Spacing props (`gap`) are the theme's space steps, 0 to 8. Each design system chooses its own look at some points (`navigation`, `page-header`, `table`, `tabs`); never restyle a pattern per theme.
|
|
219
|
+
|
|
220
|
+
### Hooks
|
|
221
|
+
|
|
222
|
+
- `useTheme()`: The nearest provider's theme: { theme, appearance, preference, themes, setTheme, setAppearance }. appearance is always "light" or "dark".
|
|
223
|
+
- `useToast()`: show({ message, actionLabel, onAction, duration }) a toast, or dismiss(id) one.
|
|
224
|
+
|
|
225
|
+
### Role tokens
|
|
226
|
+
|
|
227
|
+
Style your own markup with these. Each names a role; its value comes from the theme.
|
|
228
|
+
|
|
229
|
+
- **border**: `--piq-border-default`
|
|
230
|
+
- **color**: `--piq-color-accent`, `--piq-color-border`, `--piq-color-ground`, `--piq-color-ground-recessed`, `--piq-color-on-accent`, `--piq-color-structure`, `--piq-color-surface`, `--piq-color-surface-sunken`, `--piq-color-text`, `--piq-color-text-muted`, `--piq-color-text-strong`, `--piq-color-text-subtle`
|
|
231
|
+
- **focus**: `--piq-focus-offset`, `--piq-focus-ring`
|
|
232
|
+
- **font-size**: `--piq-font-size-body`, `--piq-font-size-display`, `--piq-font-size-heading`, `--piq-font-size-label`, `--piq-font-size-lead`, `--piq-font-size-small`, `--piq-font-size-title`
|
|
233
|
+
- **font-weight**: `--piq-font-weight-regular`, `--piq-font-weight-strong`
|
|
234
|
+
- **font**: `--piq-font-body`, `--piq-font-display`, `--piq-font-heading`, `--piq-font-mono`
|
|
235
|
+
- **letter-spacing**: `--piq-letter-spacing-eyebrow`, `--piq-letter-spacing-label`
|
|
236
|
+
- **line-height**: `--piq-line-height-body`, `--piq-line-height-tight`
|
|
237
|
+
- **radius**: `--piq-radius-card`, `--piq-radius-control`
|
|
238
|
+
- **shadow**: `--piq-shadow-card`, `--piq-shadow-overlay`
|
|
239
|
+
- **size**: `--piq-size-breakpoint-expanded`, `--piq-size-breakpoint-extra-large`, `--piq-size-breakpoint-large`, `--piq-size-breakpoint-medium`, `--piq-size-content-form`, `--piq-size-content-page`, `--piq-size-measure`, `--piq-size-target-minimum`, `--piq-size-target-touch`
|
|
240
|
+
- **space**: `--piq-space-1`, `--piq-space-2`, `--piq-space-3`, `--piq-space-4`, `--piq-space-5`, `--piq-space-6`, `--piq-space-7`, `--piq-space-8`, `--piq-space-gutter-compact`, `--piq-space-gutter-expanded`, `--piq-space-gutter-extra-large`, `--piq-space-gutter-large`, `--piq-space-gutter-medium`
|
|
241
|
+
- **state-layer**: `--piq-state-layer-hover`, `--piq-state-layer-press`
|
|
242
|
+
- **state**: `--piq-state-closed`, `--piq-state-evaluation`, `--piq-state-limited`, `--piq-state-live`, `--piq-state-purchasing`
|
|
243
|
+
|
|
244
|
+
Widget tokens (`badge`, `button`, `card`, `check`, `choice-picker`, `field`, `icon`, `menu`, `nav`, `notice`, `page-header`, `pill`, `progress`, `skeleton`, `spinner`, `switch`, `table`, `tabs`, `toast`) belong to the widgets; style widgets through their props, not their tokens.
|
|
245
|
+
|
|
246
|
+
### Icons
|
|
247
|
+
|
|
248
|
+
`<Icon name="…" />` with one of: `add`, `arrow-left`, `arrow-right`, `calendar`, `check`, `check-circle`, `chevron-down`, `chevron-left`, `chevron-right`, `chevron-up`, `close`, `copy`, `delete`, `download`, `edit`, `error`, `external-link`, `filter`, `help`, `home`, `info`, `lock`, `menu`, `more`, `refresh`, `search`, `settings`, `sort-ascending`, `sort-descending`, `upload`, `user`, `view`, `warning`. The glyph follows the theme's design system.
|
|
249
|
+
|
|
250
|
+
Any other icon: `@ux-builder/ui/icons` has all 2130 Lucide icons by their Lucide name, each with Material, Fluent or Carbon's own glyph where one matches (422, 408 and 347; otherwise Lucide's). Import the ones you use, so the bundle carries only those, and pass one or register them: `import { iconPin, iconPlay } from "@ux-builder/ui/icons"`, then `<Icon icon={iconPin} />`, or `registerIcons(iconPin, iconPlay)` once and `<Icon name="pin" />`. The export is `icon` + the name in PascalCase. Find names in `@ux-builder/ui/catalogue.json` (`iconLibrary.icons`) or with the MCP tool `list_icons` and a query. Never draw an icon the library has; for one it lacks, `registerIcons({ name, glyphs: { lucide: { viewBox, body } } })` so it still draws through `Icon`.
|
|
251
|
+
|
|
252
|
+
### Loading and progress
|
|
253
|
+
|
|
254
|
+
Show waiting with the SDK, never a hand-drawn loader or another library's: `<ProgressBar label="Uploading" value={40} />` when the amount is known (leave out `value` while it is not), `<Spinner label="Saving" />` for a short wait (`showLabel` to show the words, `size="sm"` inline), `<Skeleton lines={3} />` (or `variant="rect"` or `"circle"`) in place of content that is loading, inside a region marked `aria-busy="true"`, and `<Button busy>` while a button's action runs. Each follows the theme's design system and the user's reduced-motion setting.
|
|
255
|
+
|
|
256
|
+
### Logo and favicon
|
|
257
|
+
|
|
258
|
+
`<Logo />` in the app's header (`variant="mark"` where space is tight, `size="sm"` for the compact height). It draws the theme's brand artwork, or the brand's title as a wordmark when the brand has none, and changes with the theme and appearance. Never import or hard-code a logo file. An embedded view draws no logo.
|
|
259
|
+
|
|
260
|
+
The page's favicon follows the theme too: the top-level `ThemeProvider` sets it (`favicon={false}` turns that off). An app that sets `data-theme` on `<html>` without a provider renders `<Favicon />` once. Keep the app's own `<link rel="icon">` in index.html: it shows for a theme without a favicon. Never hard-code the brand's favicon.
|
|
261
|
+
|
|
262
|
+
### Themes
|
|
263
|
+
|
|
264
|
+
| Theme | Design system | Appearances |
|
|
265
|
+
|-------|---------------|-------------|
|
|
266
|
+
| `databricks` | databricks | dark, light (default light) |
|
|
267
|
+
| `plain` | own anatomy | light (default light) |
|
|
268
|
+
| `praxis` | own anatomy | dark, light (default dark) |
|
|
269
|
+
| `praxis-carbon` | carbon | dark, light (default dark) |
|
|
270
|
+
| `praxis-fluentui` | fluent | dark, light (default dark) |
|
|
271
|
+
| `praxis-material` | material | dark, light (default dark) |
|
|
272
|
+
| `praxis-v2-databricks` | databricks | dark, light (default light) |
|
|
273
|
+
| `synaptiq` | own anatomy | dark, light (default light) |
|
|
274
|
+
| `ux-builder-databricks` | databricks | dark, light (default light) |
|
|
275
|
+
<!-- catalogue:end -->
|
package/README.md
CHANGED
|
@@ -1,3 +1,294 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ux-builder/ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The widgets the harness and embedded products share: prefixed CSS (`.piq-*`)
|
|
4
|
+
plus a React 19 component for each, owning keyboard, focus, sorting, paging and
|
|
5
|
+
accessibility. Every rule reads a token from `@ux-builder/tokens`, so the theme on
|
|
6
|
+
the page decides how each widget looks.
|
|
7
|
+
|
|
8
|
+
| Component | What it is |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `Button` | `variant` primary, secondary or text; `size="sm"`; `badge` for a count; `busy` shows a spinner over the label, sets `aria-busy` and ignores clicks |
|
|
11
|
+
| `TextField`, `TextArea` | Labelled input, single or multi-line, with `hint` and `error` (sets `aria-invalid`, wired with `aria-describedby`) |
|
|
12
|
+
| `Select` | Dropdown on a native select; in Chrome 135+ the open list is themed too (`appearance: base-select`) |
|
|
13
|
+
| `Checkbox`, `Switch` | Native checkboxes, restyled; the switch has `role="switch"` |
|
|
14
|
+
| `RadioGroup` | Native radios in a fieldset whose legend names the group |
|
|
15
|
+
| `ChoicePicker` | One choice from a few: a radio group drawn as segments, arrow keys move the selection |
|
|
16
|
+
| `Card` | Title, content and actions |
|
|
17
|
+
| `Notice` | In-page message, `tone` info, accent, success, warning or error |
|
|
18
|
+
| `StatusPill` | Availability status: live, limited, purchasing, evaluation, closed |
|
|
19
|
+
| `DataTable` | Sorts (`aria-sort`) and pages; numbers align right |
|
|
20
|
+
| `Toast`, `ToastProvider`, `useToast` | Confirmation with Undo in a polite live region; waits while hovered or focused |
|
|
21
|
+
| `ProgressBar` | `label` and `value` (of `max`, default 100) for known progress; without `value`, indeterminate. `role="progressbar"` |
|
|
22
|
+
| `Spinner` | `size` sm, md or lg (16, 24, 48px); `label` is announced (`role="status"`), shown with `showLabel` |
|
|
23
|
+
| `Skeleton` | Placeholder while content loads: `variant` text (`lines`), rect or circle; hidden from assistive technology |
|
|
24
|
+
| `Logo` | The active theme's logo or mark, from the brand's artwork; a brand without artwork shows its title as a wordmark |
|
|
25
|
+
| `Favicon` | Keeps the page's icon on the theme's favicon, for a page without a `ThemeProvider` |
|
|
26
|
+
|
|
27
|
+
## Using it
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm install @ux-builder/tokens @ux-builder/ui
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import "@ux-builder/tokens/themes.css"; // or one theme's file
|
|
35
|
+
import "@ux-builder/tokens/fonts.css"; // or one theme's: fonts/<theme>.css
|
|
36
|
+
import "@ux-builder/ui/ui.css"; // after the theme CSS
|
|
37
|
+
import { Button, DataTable, ToastProvider, useToast } from "@ux-builder/ui";
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
React 19 is a peer dependency. An app that is not React can use the CSS on its
|
|
41
|
+
own markup: the class names and structure are what each component renders.
|
|
42
|
+
|
|
43
|
+
### Theme and appearance
|
|
44
|
+
|
|
45
|
+
```jsx
|
|
46
|
+
import { ThemeProvider, useTheme } from "@ux-builder/ui";
|
|
47
|
+
|
|
48
|
+
<ThemeProvider theme="databricks" appearance="system"> {/* light | dark | system */}
|
|
49
|
+
<App />
|
|
50
|
+
</ThemeProvider>
|
|
51
|
+
|
|
52
|
+
function Settings() {
|
|
53
|
+
const { theme, appearance, preference, themes, setTheme, setAppearance } = useTheme();
|
|
54
|
+
// appearance is always "light" or "dark"; preference may be "system"
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The provider's element carries `data-theme` and `data-appearance`
|
|
59
|
+
(CONTRACT-003), so the theme CSS applies inside it; the nearest provider wins.
|
|
60
|
+
`appearance="system"` is resolved before first paint and follows the user's
|
|
61
|
+
preference. An appearance the theme lacks falls back to the theme's own, with
|
|
62
|
+
a development warning. For an embedded view (ADR-011), pass
|
|
63
|
+
`tokens={resolved.tokens}` instead of `theme`: the values are set as custom
|
|
64
|
+
properties and no stylesheet is needed.
|
|
65
|
+
|
|
66
|
+
The provider does not remember the user's choice. `onChange({ theme,
|
|
67
|
+
appearance })` is called on every switch (`appearance` is the preference, so
|
|
68
|
+
it may be `system`); store it and pass it back as props. `as` sets the
|
|
69
|
+
element (default `div`).
|
|
70
|
+
|
|
71
|
+
The top-level provider also answers a window event, so a tool (such as
|
|
72
|
+
`@ux-builder/audit`) or another part of the page can switch the theme as the
|
|
73
|
+
user would:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
window.dispatchEvent(new CustomEvent("piq:set-theme", { detail: { theme: "praxis-carbon", appearance: "dark" } }));
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Nested and embedded providers ignore it.
|
|
80
|
+
|
|
81
|
+
Component tests run on the built package: `npm run build && npm test`.
|
|
82
|
+
|
|
83
|
+
### For AI coding agents
|
|
84
|
+
|
|
85
|
+
The package ships guidance an agent reads before writing UI, and a
|
|
86
|
+
machine-readable catalogue, both generated from this package's source so
|
|
87
|
+
they never name anything that does not exist (FEAT-004 SDK-12, SDK-13):
|
|
88
|
+
|
|
89
|
+
- `AGENTS.md`: the five rules, how to set up, how to do common things, and
|
|
90
|
+
a reference of every component and prop, role token, icon and theme.
|
|
91
|
+
- `skill/ux-builder-ui/SKILL.md`: the same, as a Claude Code skill. Install it
|
|
92
|
+
in an app with
|
|
93
|
+
`mkdir -p .claude/skills && cp -R node_modules/@ux-builder/ui/skill/ux-builder-ui .claude/skills/`.
|
|
94
|
+
- `catalogue.json` (`@ux-builder/ui/catalogue.json`): every token (path, tier,
|
|
95
|
+
purpose, 0.x alias), every export (props and defaults, purpose), themes,
|
|
96
|
+
design systems, icons and the lint rule.
|
|
97
|
+
|
|
98
|
+
Agents can also query the SDK over MCP with `@ux-builder/mcp`
|
|
99
|
+
(`claude mcp add ux-builder -- npx @ux-builder/mcp`): themes with every role
|
|
100
|
+
token's value, brands, design systems, widgets with props, tokens with
|
|
101
|
+
purposes, icons and this guidance.
|
|
102
|
+
|
|
103
|
+
For other agents, point the app's own `AGENTS.md` or `CLAUDE.md` at
|
|
104
|
+
`node_modules/@ux-builder/ui/AGENTS.md`. In this repository, `npm run catalogue`
|
|
105
|
+
regenerates them, and `npm run check` fails if they are stale.
|
|
106
|
+
|
|
107
|
+
### Lint
|
|
108
|
+
|
|
109
|
+
`ux-builder/no-hard-coded-values` reports literal colours, font families and
|
|
110
|
+
border radii in app CSS and JSX `style` props, and names the role token to
|
|
111
|
+
use when your theme has one with that value (FEAT-004 SDK-10, SDK-11).
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
// .stylelintrc.json
|
|
115
|
+
{ "plugins": ["@ux-builder/ui/stylelint"],
|
|
116
|
+
"rules": { "ux-builder/no-hard-coded-values": [true, {
|
|
117
|
+
"theme": "databricks",
|
|
118
|
+
"allow": [{ "value": "#4E79A7", "file": "src/charts/**", "reason": "chart series colours" }]
|
|
119
|
+
}] } }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
// eslint.config.js
|
|
124
|
+
import uxBuilder from "@ux-builder/ui/eslint";
|
|
125
|
+
export default [{ files: ["src/**/*.jsx"], plugins: { "ux-builder": uxBuilder },
|
|
126
|
+
rules: { "ux-builder/no-hard-coded-values": ["error", { theme: "databricks" }] } }];
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`#1B3139` under `databricks` reports *use var(--piq-color-text)*. Tokens,
|
|
130
|
+
`var()` fallbacks, `transparent`, `currentColor`, CSS-wide keywords, `0`,
|
|
131
|
+
generic font families and fully round radii (50%, 999px) are fine. Every
|
|
132
|
+
allow-list entry needs a `reason`; one without is reported. stylelint and
|
|
133
|
+
ESLint are optional peer dependencies.
|
|
134
|
+
|
|
135
|
+
Two more ESLint rules catch what the SDK already provides:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
rules: {
|
|
139
|
+
"ux-builder/no-foreign-ui": ["error", { allow: [] }], // package names you have reviewed
|
|
140
|
+
"ux-builder/no-inline-svg": ["error", { allow: ["chart"] }], // <svg> class names or ids
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`ux-builder/no-foreign-ui` reports imports of other UI kits (Material UI, Ant
|
|
145
|
+
Design, Chakra, Carbon React, Fluent UI, Mantine, Bootstrap, Blueprint,
|
|
146
|
+
PrimeReact, Radix Themes), icon sets (lucide-react, react-icons, Heroicons,
|
|
147
|
+
Phosphor, Tabler, Font Awesome) and loader libraries, naming the SDK's
|
|
148
|
+
counterpart. `ux-builder/no-inline-svg` reports `<svg>` written in JSX: use
|
|
149
|
+
`<Icon>`, `@ux-builder/ui/icons` or `registerIcons`. Lint reads source; for
|
|
150
|
+
what only the running app shows, run `@ux-builder/audit`.
|
|
151
|
+
|
|
152
|
+
### Icons
|
|
153
|
+
|
|
154
|
+
```jsx
|
|
155
|
+
import { Icon, ICON_NAMES } from "@ux-builder/ui";
|
|
156
|
+
|
|
157
|
+
<Icon name="warning" /> {/* decorative: aria-hidden */}
|
|
158
|
+
<Icon name="search" label="Search" size="1.25rem" /> {/* role="img", named */}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
One component, shared names (`ICON_NAMES`: add, close, search, warning, …,
|
|
162
|
+
33 in all). The glyph follows the theme's design system: Material Symbols on
|
|
163
|
+
Material, Fluent System Icons on Fluent, Carbon icons on Carbon, and Lucide
|
|
164
|
+
on every theme without a set of its own (praxis, plain, synaptiq,
|
|
165
|
+
databricks). The set comes from the nearest `ThemeProvider`, else the closest
|
|
166
|
+
`data-theme` in the page; `set="material"` forces one. A name a set lacks is
|
|
167
|
+
drawn from Lucide. Icons take the text colour and are 1em unless `size` says
|
|
168
|
+
otherwise.
|
|
169
|
+
|
|
170
|
+
### Every other icon
|
|
171
|
+
|
|
172
|
+
`@ux-builder/ui/icons` has every Lucide icon (2,130) by its Lucide name, each
|
|
173
|
+
with Material, Fluent or Carbon's own glyph where one matches (about 420,
|
|
174
|
+
410 and 350 of them; Lucide's otherwise, as on any theme without a set).
|
|
175
|
+
One export per icon, so an app's bundle carries only those it imports:
|
|
176
|
+
|
|
177
|
+
```jsx
|
|
178
|
+
import { Icon, registerIcons } from "@ux-builder/ui";
|
|
179
|
+
import { iconPin, iconPlay, iconPaperclip } from "@ux-builder/ui/icons";
|
|
180
|
+
|
|
181
|
+
<Icon icon={iconPin} label="Pin" />
|
|
182
|
+
registerIcons(iconPlay, iconPaperclip); // once, then:
|
|
183
|
+
<Icon name="play" />
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The export is `icon` + the name in PascalCase; `ICON_LIBRARY_NAMES` lists
|
|
187
|
+
them, and so do `catalogue.json` (`iconLibrary`) and the MCP tool
|
|
188
|
+
`list_icons` with a query. Matches come from the name itself, word
|
|
189
|
+
synonyms per set and hand-checked overrides (Material's own `pin` is a
|
|
190
|
+
PIN keypad, so `pin` there is `keep`), so a set never draws the wrong
|
|
191
|
+
idea. For an icon the library lacks, register the app's own,
|
|
192
|
+
`registerIcons({ name, glyphs: { lucide: { viewBox, body } } })`, and it
|
|
193
|
+
draws through `Icon` like the rest; markup with scripts, handlers or
|
|
194
|
+
links is refused.
|
|
195
|
+
|
|
196
|
+
The glyphs are extracted at build time (`npm run icons`) from pinned icon
|
|
197
|
+
packages, so an app installs none of them; each set's licence ships in
|
|
198
|
+
`licenses/` (Apache-2.0, MIT, Apache-2.0, ISC). A test fails if the extracted
|
|
199
|
+
glyphs drift from the packages.
|
|
200
|
+
|
|
201
|
+
### Logo
|
|
202
|
+
|
|
203
|
+
```jsx
|
|
204
|
+
import { Logo } from "@ux-builder/ui";
|
|
205
|
+
|
|
206
|
+
<Logo /> {/* the brand's logo at the brand's height */}
|
|
207
|
+
<Logo variant="mark" size="sm" /> {/* the mark, compact */}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The artwork comes from the theme's brand tokens (`--piq-brand-logo-image`,
|
|
211
|
+
or a mask painted in the logo colour), so the logo changes with the theme and
|
|
212
|
+
the appearance and the app imports no file. A brand without artwork shows its
|
|
213
|
+
title as a wordmark in its logo colour (the mark: its initial). It is one
|
|
214
|
+
image named by the brand's title, or by `label`. Brand tokens belong to the
|
|
215
|
+
top-level app (CONTRACT-001): in an embedded view (`ThemeProvider` with
|
|
216
|
+
`tokens`) `Logo` draws nothing.
|
|
217
|
+
|
|
218
|
+
### Favicon
|
|
219
|
+
|
|
220
|
+
The top-level `ThemeProvider` keeps the page's icon on the theme's favicon
|
|
221
|
+
(`--piq-brand-favicon`) as the theme and appearance change: it adds one
|
|
222
|
+
`<link rel="icon" data-piq-favicon>` last in the head, so browsers prefer it
|
|
223
|
+
to the app's own. A theme without a favicon removes it, and the app's own
|
|
224
|
+
`<link rel="icon">` shows again, so keep one in `index.html`.
|
|
225
|
+
`favicon={false}` turns this off; an embedded view or a nested provider never
|
|
226
|
+
touches the icon. A page that sets `data-theme` on `<html>` without a provider
|
|
227
|
+
renders `<Favicon />` once; `applyFavicon(element)` does the same by hand.
|
|
228
|
+
|
|
229
|
+
## How a theme shapes a widget
|
|
230
|
+
|
|
231
|
+
Every rule reads CONTRACT-001 role names (`--piq-color-text`,
|
|
232
|
+
`--piq-button-radius`), never a 0.x alias. The base tokens every theme has
|
|
233
|
+
(colour and state roles, type, `--piq-button-*`, `--piq-pill-*`,
|
|
234
|
+
`--piq-badge-*`, radius, shadows, `--piq-focus-*`, `--piq-state-layer-*`)
|
|
235
|
+
style every widget. On top of them, a design system may set widget tokens:
|
|
236
|
+
`--piq-field-*`, `--piq-check-*`, `--piq-switch-*`, `--piq-menu-*`,
|
|
237
|
+
`--piq-choice-picker-*`, `--piq-card-*`, `--piq-notice-*`, `--piq-toast-*`,
|
|
238
|
+
`--piq-table-*`, `--piq-progress-*`, `--piq-spinner-*`, `--piq-skeleton-*`,
|
|
239
|
+
and the `--piq-button-secondary-*` and `--piq-button-ghost-fg` variants. Each has a fallback in
|
|
240
|
+
`ui.css` that reproduces the treatment the platform's `ui.css` gave that widget,
|
|
241
|
+
so `praxis` and `plain` look as they did, and `praxis-material`,
|
|
242
|
+
`praxis-fluentui` and `praxis-carbon` set them to their system's anatomy:
|
|
243
|
+
|
|
244
|
+
| | Material 3 | Fluent 2 | Carbon |
|
|
245
|
+
|---|---|---|---|
|
|
246
|
+
| Button | Full pill, 40dp; outlined secondary | 4px, 32px; neutral secondary | Square, 48px; tertiary outline |
|
|
247
|
+
| Field, dropdown | Filled, top corners, 2dp underline on focus | Outlined 4px, brand bar on focus | Bottom rule on a layer, 2px inset focus |
|
|
248
|
+
| Open list | 48dp items, tonal selection, level 2 | 32px options, brand bar on the selected, shadow16 | Square, 40px items, overlay shadow |
|
|
249
|
+
| Checkbox, radio | 18dp, 2dp outline, primary | 16px, 1px stroke, brand | 16px, ink fill |
|
|
250
|
+
| Switch | 52×32dp, thumb grows when on | 40×20px | 48×24px, green when on |
|
|
251
|
+
| Choice picker | Joined pill segments, tonal selection with a tick | Separate toggle buttons | Square content switcher, inverse selection |
|
|
252
|
+
| Notice | Tonal container, 12dp | Tinted bar with a tinted edge | 3px stripe, square |
|
|
253
|
+
| Toast | Inverse snackbar | Neutral with shadow8 | Striped, with overlay shadow |
|
|
254
|
+
| Table | 52dp rows | 44px rows, semibold header | 48px rows, header on its own layer |
|
|
255
|
+
|
|
256
|
+
Hover and press on the M3, Fluent and Carbon controls are a state layer: the
|
|
257
|
+
label's own colour mixed over the container at the system's percentage.
|
|
258
|
+
|
|
259
|
+
The selected segment's label is `--piq-choice-picker-option-selected-fg` on
|
|
260
|
+
`--piq-choice-picker-option-selected-bg`, which the contrast gate checks, so
|
|
261
|
+
the platform's literal `#fff` on `--navy` (1.25:1 in
|
|
262
|
+
Praxis dark) is gone; the fallback is the primary button's pairing.
|
|
263
|
+
|
|
264
|
+
## Building
|
|
265
|
+
|
|
266
|
+
`npm run build` at the repo root builds `@ux-builder/tokens`, then this package:
|
|
267
|
+
`dist/index.js` (ES module, React external) and `dist/ui.css`. The gallery
|
|
268
|
+
renders these built components, so it shows exactly what an app gets.
|
|
269
|
+
|
|
270
|
+
## Storybook
|
|
271
|
+
|
|
272
|
+
```sh
|
|
273
|
+
npm run storybook # from the repo root: builds tokens, serves on :6006
|
|
274
|
+
npm run build-storybook # static build in packages/ui/storybook-static
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Every story renders inside a theme picked from the toolbar (Praxis, Praxis
|
|
278
|
+
Material, Fluent and Carbon, Synaptiq and Databricks, each dark or light;
|
|
279
|
+
Plain) and is delivered one of two ways: the theme CSS with
|
|
280
|
+
`data-theme` and `data-appearance`, as the apps do, or the theme's resolved
|
|
281
|
+
JSON as inline custom properties, as the harness will hand a theme to an
|
|
282
|
+
embedded view. A widget that looks the same both ways depends on nothing but
|
|
283
|
+
the token contract. Themes are linkable:
|
|
284
|
+
`?globals=theme:plain-light;delivery:resolved`.
|
|
285
|
+
|
|
286
|
+
The Accessibility panel (axe) checks what the browser renders, so it catches
|
|
287
|
+
literal colours in widget rules that the token-level contrast gate cannot. It
|
|
288
|
+
reports rather than fails until the contrast baseline is empty (step 4).
|
|
289
|
+
|
|
290
|
+
Stories cover the buttons, fields, choice picker, table, feedback (notice,
|
|
291
|
+
status pill, toast), loading widgets, icons and the theme provider;
|
|
292
|
+
`Foundations/Text roles` draws every pairing the contrast gate checks, from
|
|
293
|
+
the gate's own list (`tools/contrast.mjs`). CI builds Storybook on every pull request and uploads
|
|
294
|
+
it as the run's `storybook` artifact.
|