prime-ui-kit 0.20.1 → 0.21.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/README.md +1 -1
- package/dist/chunks/{chunk-4KONMO7Z.js → chunk-6L7VWEY6.js} +1 -1
- package/dist/chunks/{chunk-3PY52JP3.js → chunk-S27VR4ON.js} +5828 -5596
- package/dist/chunks/{chunk-4374EBWV.js → chunk-S4H2ZKWJ.js} +1049 -644
- package/dist/color-picker.js +2 -2
- package/dist/components/badge/Badge.d.ts +19 -2
- package/dist/components/card/Card.d.ts +10 -3
- package/dist/components/composer/Composer.d.ts +49 -1
- package/dist/components/crossfade/Crossfade.d.ts +6 -4
- package/dist/components/data-table/DataTable.d.ts +10 -2
- package/dist/components/data-table/Row.d.ts +12 -1
- package/dist/components/data-table/SelectionMenu.d.ts +20 -0
- package/dist/components/data-table/useRowSelection.d.ts +2 -0
- package/dist/components/empty-page/EmptyPage.d.ts +5 -2
- package/dist/components/hint/Hint.d.ts +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/input/Input.d.ts +15 -3
- package/dist/components/message/Message.d.ts +6 -3
- package/dist/components/segmented-control/SegmentedControl.d.ts +5 -1
- package/dist/components/smart-filter/SmartFilter.d.ts +4 -0
- package/dist/components/spinner/Spinner.d.ts +3 -2
- package/dist/components/tabs/Tabs.d.ts +10 -1
- package/dist/hooks/useModalLayer.d.ts +15 -8
- package/dist/index.css +1 -1
- package/dist/index.js +2 -2
- package/dist/internal/RegionSwap.d.ts +18 -10
- package/dist/internal/RollingNumber.d.ts +7 -5
- package/dist/internal/ToolRow.d.ts +6 -0
- package/dist/internal/mediaSlot.d.ts +29 -0
- package/dist/internal/overlay/DialogParts.d.ts +30 -6
- package/dist/internal/overlay/focus.d.ts +18 -3
- package/dist/internal/partPresence.d.ts +19 -2
- package/dist/layout/page/Page.d.ts +1 -1
- package/dist/layout/sidebar/Sidebar.d.ts +3 -2
- package/dist/skill/anti-slop.md +35 -37
- package/dist/skill/api-contract.md +50 -2
- package/dist/skill/cheatsheet.md +15 -10
- package/dist/skill/checklist.md +20 -6
- package/dist/skill/choosing.md +82 -8
- package/dist/skill/components.md +1 -1
- package/dist/skill/composition.md +268 -51
- package/dist/skill/foundations.md +17 -6
- package/dist/skill/layouts.md +65 -14
- package/dist/skill/motion.md +9 -3
- package/dist/skill/reference/components/avatar/COMPONENT.md +1 -0
- package/dist/skill/reference/components/badge/COMPONENT.md +29 -5
- package/dist/skill/reference/components/badge/examples/examples.module.css +14 -0
- package/dist/skill/reference/components/badge/examples/overflow-count.tsx +32 -0
- package/dist/skill/reference/components/banner/COMPONENT.md +1 -1
- package/dist/skill/reference/components/card/COMPONENT.md +8 -4
- package/dist/skill/reference/components/card/examples/person-header.tsx +38 -0
- package/dist/skill/reference/components/composer/COMPONENT.md +35 -5
- package/dist/skill/reference/components/composer/examples/bot-keyboard.tsx +43 -0
- package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -1
- package/dist/skill/reference/components/crossfade/COMPONENT.md +18 -10
- package/dist/skill/reference/components/data-table/COMPONENT.md +17 -11
- package/dist/skill/reference/components/data-table/examples/screen.tsx +6 -9
- package/dist/skill/reference/components/data-table/examples/selection.tsx +60 -18
- package/dist/skill/reference/components/drawer/COMPONENT.md +41 -19
- package/dist/skill/reference/components/drawer/examples/footer-actions.tsx +54 -0
- package/dist/skill/reference/components/drawer/examples/header-actions.tsx +46 -25
- package/dist/skill/reference/components/drawer/examples/link-loading.tsx +57 -0
- package/dist/skill/reference/components/drawer/examples/screen.tsx +3 -5
- package/dist/skill/reference/components/empty-page/COMPONENT.md +2 -2
- package/dist/skill/reference/components/field-list/COMPONENT.md +4 -4
- package/dist/skill/reference/components/field-list/examples/row-menu.tsx +1 -1
- package/dist/skill/reference/components/hint/COMPONENT.md +1 -1
- package/dist/skill/reference/components/input/COMPONENT.md +16 -3
- package/dist/skill/reference/components/input/examples/password-policy.tsx +65 -0
- package/dist/skill/reference/components/list/COMPONENT.md +1 -1
- package/dist/skill/reference/components/login-form/COMPONENT.md +13 -11
- package/dist/skill/reference/components/login-form/examples/overview.tsx +5 -5
- package/dist/skill/reference/components/login-form/examples/states.tsx +7 -6
- package/dist/skill/reference/components/message/COMPONENT.md +5 -5
- package/dist/skill/reference/components/message/examples/bot-actions.tsx +47 -28
- package/dist/skill/reference/components/modal/COMPONENT.md +31 -12
- package/dist/skill/reference/components/modal/examples/structure.tsx +2 -4
- package/dist/skill/reference/components/segmented-control/COMPONENT.md +11 -6
- package/dist/skill/reference/components/segmented-control/examples/router.tsx +22 -0
- package/dist/skill/reference/components/select/COMPONENT.md +4 -1
- package/dist/skill/reference/components/sheet/COMPONENT.md +31 -12
- package/dist/skill/reference/components/sheet/examples/header-actions.tsx +5 -3
- package/dist/skill/reference/components/skeleton/COMPONENT.md +3 -2
- package/dist/skill/reference/components/smart-filter/COMPONENT.md +2 -1
- package/dist/skill/reference/components/smart-filter/examples/ranges.tsx +8 -2
- package/dist/skill/reference/components/spinner/COMPONENT.md +1 -1
- package/dist/skill/reference/components/tabs/COMPONENT.md +7 -4
- package/dist/skill/reference/components/tabs/examples/in-drawer.tsx +18 -2
- package/dist/skill/reference/components/thumbnail/COMPONENT.md +1 -0
- package/dist/skill/reference/components/tooltip/COMPONENT.md +3 -3
- package/dist/skill/reference/foundation.md +15 -9
- package/dist/skill/reference/layout/page/COMPONENT.md +10 -6
- package/dist/skill/reference/layout/page/examples/router.tsx +124 -0
- package/dist/skill/reference/layout/page/examples/tools.tsx +12 -22
- package/dist/skill/reference/layout/sidebar/COMPONENT.md +8 -3
- package/dist/skill/reference/layout/sidebar/examples/account-profile.tsx +66 -0
- package/dist/skill/responsive.md +28 -6
- package/dist/skill/ux.md +97 -2
- package/dist/tokens/semantic.d.ts +2 -1
- package/dist/tokens/themes/dark.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/avatar/COMPONENT.md +1 -0
- package/src/components/badge/COMPONENT.md +29 -5
- package/src/components/badge/examples/examples.module.css +14 -0
- package/src/components/badge/examples/overflow-count.tsx +32 -0
- package/src/components/banner/COMPONENT.md +1 -1
- package/src/components/card/COMPONENT.md +8 -4
- package/src/components/card/examples/person-header.tsx +38 -0
- package/src/components/composer/COMPONENT.md +35 -5
- package/src/components/composer/examples/bot-keyboard.tsx +43 -0
- package/src/components/confirm-dialog/COMPONENT.md +1 -1
- package/src/components/crossfade/COMPONENT.md +18 -10
- package/src/components/data-table/COMPONENT.md +17 -11
- package/src/components/data-table/examples/screen.tsx +6 -9
- package/src/components/data-table/examples/selection.tsx +60 -18
- package/src/components/drawer/COMPONENT.md +41 -19
- package/src/components/drawer/examples/footer-actions.tsx +54 -0
- package/src/components/drawer/examples/header-actions.tsx +46 -25
- package/src/components/drawer/examples/link-loading.tsx +57 -0
- package/src/components/drawer/examples/screen.tsx +3 -5
- package/src/components/empty-page/COMPONENT.md +2 -2
- package/src/components/field-list/COMPONENT.md +4 -4
- package/src/components/field-list/examples/row-menu.tsx +1 -1
- package/src/components/hint/COMPONENT.md +1 -1
- package/src/components/input/COMPONENT.md +16 -3
- package/src/components/input/examples/password-policy.tsx +65 -0
- package/src/components/list/COMPONENT.md +1 -1
- package/src/components/login-form/COMPONENT.md +13 -11
- package/src/components/login-form/examples/overview.tsx +5 -5
- package/src/components/login-form/examples/states.tsx +7 -6
- package/src/components/message/COMPONENT.md +5 -5
- package/src/components/message/examples/bot-actions.tsx +47 -28
- package/src/components/modal/COMPONENT.md +31 -12
- package/src/components/modal/examples/structure.tsx +2 -4
- package/src/components/segmented-control/COMPONENT.md +11 -6
- package/src/components/segmented-control/examples/router.tsx +22 -0
- package/src/components/select/COMPONENT.md +4 -1
- package/src/components/sheet/COMPONENT.md +31 -12
- package/src/components/sheet/examples/header-actions.tsx +5 -3
- package/src/components/skeleton/COMPONENT.md +3 -2
- package/src/components/smart-filter/COMPONENT.md +2 -1
- package/src/components/smart-filter/examples/ranges.tsx +8 -2
- package/src/components/spinner/COMPONENT.md +1 -1
- package/src/components/tabs/COMPONENT.md +7 -4
- package/src/components/tabs/examples/in-drawer.tsx +18 -2
- package/src/components/thumbnail/COMPONENT.md +1 -0
- package/src/components/tooltip/COMPONENT.md +3 -3
- package/src/layout/page/COMPONENT.md +10 -6
- package/src/layout/page/examples/router.tsx +124 -0
- package/src/layout/page/examples/tools.tsx +12 -22
- package/src/layout/sidebar/COMPONENT.md +8 -3
- package/src/layout/sidebar/examples/account-profile.tsx +66 -0
- package/src/styles/theme-dark.css +1 -0
- package/src/styles/theme-light.css +1 -0
|
@@ -13,10 +13,10 @@ playground page. What each screen teaches is in [Screen rules](#screen-rules) at
|
|
|
13
13
|
|
|
14
14
|
| Screen | Main component | File | Shows |
|
|
15
15
|
|---|---|---|---|
|
|
16
|
-
| [List page](#list-page) | data-table | [screen.tsx](reference/components/data-table/examples/screen.tsx) | the title, a line of figures,
|
|
16
|
+
| [List page](#list-page) | data-table | [screen.tsx](reference/components/data-table/examples/screen.tsx) | the title, a line of figures, export and one primary action in the page's top bar; the table's SmartFilter search and filters in its own `toolbar`, the chips under it, the column chooser over the row menu; statuses, pages, filtered empty state |
|
|
17
17
|
| [Detail page](#detail-page) | description-list | [screen.tsx](reference/components/description-list/examples/screen.tsx) | a nested record: the path ending in the title, status and due date and the record's actions by weight in the top bar; main column + side column of Cards, Timeline |
|
|
18
18
|
| [Settings page](#settings-page) | page | [screen.tsx](reference/layout/page/examples/screen.tsx) | the path in a collapsing top bar, the large title in the flow, a card with its own header per section, form with its own save, Switches that apply at once, danger zone with a confirm |
|
|
19
|
-
| [Form in a Drawer](#form-in-a-drawer) | drawer | [screen.tsx](reference/components/drawer/examples/screen.tsx) | the title, counts
|
|
19
|
+
| [Form in a Drawer](#form-in-a-drawer) | drawer | [screen.tsx](reference/components/drawer/examples/screen.tsx) | the title, counts and the Drawer trigger in the top bar, the search in the table's toolbar; Drawer form: groups, errors on submit, focus on the first invalid field, loading, toast |
|
|
20
20
|
| [Record editor](#record-editor) | field-list | [screen.tsx](reference/components/field-list/examples/screen.tsx) | a document page (a long form): the title and a line of description in `Page.Heading`, «Сохранить тариф» in `Page.Actions`, both in a `Page.Header` in the flow; a plan card edited in card sections: name and description, specs in a `FieldList` (icon-only `Select` and `ColorPresets` in compact columns, pinned rows, reorder), the price and its changes by month in one list with the base row pinned, a setup fee in three currencies in one row under one heading, save with «Сохранено» on the button |
|
|
21
21
|
| [Dashboard](#dashboard) | card | [screen.tsx](reference/components/card/examples/screen.tsx) | the large title + a line of data in the flow, period switch and report in a collapsing `Page.TopBar`; KPI row that cross-fades to the new period, a Sparkline revenue trend, invoiced vs paid in a ChartFrame, panels with progress and a split, short table + link to the list |
|
|
22
22
|
| [Screen states](#screen-states) | crossfade | [screen.tsx](reference/components/crossfade/examples/screen.tsx) | title + the statement date in `Page.TopBar`, page error in a Banner with retry, region loading with a Skeleton, Crossfade between states, table error in place, first-run EmptyPage |
|
|
@@ -66,7 +66,7 @@ AppShell.Root once per app (layouts.md#app-frame-once-per-a
|
|
|
66
66
|
│ │ ├─ Breadcrumb.Root only on a nested record: the levels above this page
|
|
67
67
|
│ │ ├─ Page.Title the one h1, the path's last level
|
|
68
68
|
│ │ └─ Page.Description a line of data
|
|
69
|
-
│ ├─ Page.Tools what acts on the whole page — sections, a period, view; a
|
|
69
|
+
│ ├─ Page.Tools what acts on the whole page — sections, a period, view; a filter over several views
|
|
70
70
|
│ ├─ Page.Actions every button of the page, each with an icon; the primary last
|
|
71
71
|
│ └─ Page.Chips SmartFilter.Chips: a line under everything, gone while empty
|
|
72
72
|
└─ Page.Body blocks 24 apart, the first 24 under the bar's controls
|
|
@@ -99,10 +99,11 @@ order is Heading, Tools, Actions, Chips whatever the JSX order.
|
|
|
99
99
|
`Page.Description` under the whole line. The title is never a row of its own under the path, and
|
|
100
100
|
the path never repeats the title as its last item.
|
|
101
101
|
- **`Page.Tools`** — what acts on the whole page, as kit controls in the order a person reads them:
|
|
102
|
-
sections (a `SegmentedControl` with counts
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
sections (a `SegmentedControl` with counts; of route links for
|
|
103
|
+
[a section with sub-pages](#a-section-with-sub-pages)), the view, a period that drives several blocks; a
|
|
104
|
+
search and filter (`SmartFilter.Toolbar`) only when it governs more than one table — a board and
|
|
105
|
+
its list, a media grid ([where the filters go](#where-the-filters-go)). Named with `aria-label`;
|
|
106
|
+
no buttons and no wrappers inside.
|
|
106
107
|
- **`Page.Actions`** — the one place for the page's buttons: secondary ones (`soft` + `neutral`)
|
|
107
108
|
first, the primary (`solid`) last. Every Button carries a `Button.Icon` beside its label: the bar
|
|
108
109
|
shows it as the icon alone when the line runs short.
|
|
@@ -180,6 +181,62 @@ export function DealsPage({ fields, filter, onFilterChange, view, onViewChange,
|
|
|
180
181
|
}
|
|
181
182
|
```
|
|
182
183
|
|
|
184
|
+
### A section with sub-pages
|
|
185
|
+
|
|
186
|
+
One place of the app, several facets of it: «Мессенджеры» with «Каналы» and «Статистика», a client
|
|
187
|
+
with «Сделки» and «Документы». Each facet is a route of its own (`/messengers/channels`,
|
|
188
|
+
`/messengers/stats`), and the section stays one page:
|
|
189
|
+
|
|
190
|
+
- **The title is the section.** `Page.Title` reads «Мессенджеры» on every sub-route; the facet is
|
|
191
|
+
the current link of the switch, never a second title or a title that names the facet.
|
|
192
|
+
- **The switch is links in `Page.Tools`**: a `SegmentedControl` whose items are router links
|
|
193
|
+
(`SegmentedControl.Item asChild`), `value` = the current path. It is a navigation (Tab moves
|
|
194
|
+
through the links, the current one is `aria-current="page"`), on a phone it takes a line of its
|
|
195
|
+
own under the title by itself.
|
|
196
|
+
- **The body is the sub-page**, swapped in a `Crossfade` keyed by the path (`Routes` given the
|
|
197
|
+
`location`, so the leaving sub-page stays on its route while it fades).
|
|
198
|
+
- **The Sidebar stays on the section.** Its item is current on every sub-route: a `NavLink` to
|
|
199
|
+
`/messengers` without `end`, or `current={pathname.startsWith("/messengers")}`.
|
|
200
|
+
- Never a second Sidebar level for facets, never a `Tabs` strip inside `Page.Body` for sub-pages.
|
|
201
|
+
`Sidebar.Sub` is for views of one set of objects people open straight from the rail («Сделки» →
|
|
202
|
+
«Новые», «В работе»); `Tabs` switch panels that are not routes.
|
|
203
|
+
|
|
204
|
+
Live: [router.tsx](reference/layout/page/examples/router.tsx).
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
import { Page, SegmentedControl } from "prime-ui-kit";
|
|
208
|
+
import type { ReactNode } from "react";
|
|
209
|
+
import { Link, useLocation } from "react-router-dom";
|
|
210
|
+
|
|
211
|
+
const MESSENGER_PAGES = [
|
|
212
|
+
{ to: "/messengers/channels", label: "Каналы" },
|
|
213
|
+
{ to: "/messengers/stats", label: "Статистика" },
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
export function MessengersPage({ children }: { children: ReactNode }) {
|
|
217
|
+
const { pathname } = useLocation();
|
|
218
|
+
return (
|
|
219
|
+
<Page.Root>
|
|
220
|
+
<Page.TopBar>
|
|
221
|
+
<Page.Heading>
|
|
222
|
+
<Page.Title>Мессенджеры</Page.Title>
|
|
223
|
+
</Page.Heading>
|
|
224
|
+
<Page.Tools aria-label="Мессенджеры">
|
|
225
|
+
<SegmentedControl.Root value={pathname} aria-label="Подразделы">
|
|
226
|
+
{MESSENGER_PAGES.map((page) => (
|
|
227
|
+
<SegmentedControl.Item key={page.to} value={page.to} asChild>
|
|
228
|
+
<Link to={page.to}>{page.label}</Link>
|
|
229
|
+
</SegmentedControl.Item>
|
|
230
|
+
))}
|
|
231
|
+
</SegmentedControl.Root>
|
|
232
|
+
</Page.Tools>
|
|
233
|
+
</Page.TopBar>
|
|
234
|
+
<Page.Body>{children}</Page.Body>
|
|
235
|
+
</Page.Root>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
183
240
|
## 2. Rhythm — the 4px grid
|
|
184
241
|
|
|
185
242
|
Every gap is a `--prime-space-N` token (N × 4px) set as `gap` on the parent. Closer means related.
|
|
@@ -287,20 +344,63 @@ Hierarchy comes from space and a handful of type roles, not from size jumps, col
|
|
|
287
344
|
|
|
288
345
|
- **One primary per area** (the page, card, dialog, form): `solid`, last in its row. Secondary:
|
|
289
346
|
`soft` + `tone="neutral"`. Dismiss / revert: `ghost` + `neutral` in pages and cards, `outline` +
|
|
290
|
-
`neutral` in Modal / Drawer footers.
|
|
291
|
-
`soft` + `neutral` in a header row (it has a shape beside the other buttons), `ghost` size `s` in a
|
|
292
|
-
table cell.
|
|
347
|
+
`neutral` in Modal / Drawer footers.
|
|
293
348
|
- Placement: every button of the page in one `Page.Actions` — of `Page.TopBar` on a work screen, of the flow `Page.Header` on a document page — the primary last; card actions in `Card.Footer`; dialog actions in
|
|
294
|
-
`Modal.Footer` / `Drawer.Footer`; row actions in the last column;
|
|
349
|
+
`Modal.Footer` / `Drawer.Footer`; row actions in the last column; actions on the selected rows of a
|
|
350
|
+
table in its selection menu ([controls of a table](#controls-of-a-table)).
|
|
295
351
|
- A dialog footer is the dialog's answer: Cancel and the primary. Deleting the record the dialog
|
|
296
|
-
edits is not a third footer button: it
|
|
297
|
-
|
|
352
|
+
edits is not a third footer button: it stands in `Modal.Actions` / `Drawer.Actions` of the header
|
|
353
|
+
by the menu threshold below, confirmed by a `ConfirmDialog`.
|
|
298
354
|
- An empty state's action replaces the header's primary, it does not duplicate it.
|
|
299
355
|
- Destructive: `tone="danger"`; a standalone trigger is `outline`, in a menu it is the last item, the
|
|
300
356
|
confirm is a `ConfirmDialog` (its `onConfirm` returns the request's Promise).
|
|
301
357
|
- Going somewhere is a link (`LinkButton`, `Button.Root asChild` + `<a>`), doing something is a Button.
|
|
302
358
|
One action — one label on the whole screen.
|
|
303
359
|
|
|
360
|
+
### A menu from two actions
|
|
361
|
+
|
|
362
|
+
An action that is alone stays in sight; a «⋯» only gathers. Count the secondary actions of one
|
|
363
|
+
place — a page or dialog header, a table row, a card, a list row:
|
|
364
|
+
|
|
365
|
+
| Actions | Shape |
|
|
366
|
+
|---|---|
|
|
367
|
+
| one | an icon-only `Button.Root` (`aria-label`) in a `Tooltip.Trigger` (`ghost` in a dialog header), or a labelled button where its row has room |
|
|
368
|
+
| two or more | one `Dropdown` behind an `action.more` icon button — `soft` + `neutral` in a page header (a shape beside its neighbours), `ghost` in a dialog header, `ghost` size `s` in a table cell; destructive last, `tone="danger"` |
|
|
369
|
+
|
|
370
|
+
A ⋯ with one item hides the only action behind a second press and a guess. Nothing else decides:
|
|
371
|
+
not the density of the row, not the number of its columns, not the room left for later actions. A
|
|
372
|
+
row of a `FieldList` follows the same count ([forms](#7-forms)).
|
|
373
|
+
|
|
374
|
+
```tsx partial
|
|
375
|
+
// bad — a menu for the one thing this header can do
|
|
376
|
+
<Drawer.Actions>
|
|
377
|
+
<Dropdown.Root>
|
|
378
|
+
<Dropdown.Trigger>
|
|
379
|
+
<Button.Root variant="ghost" tone="neutral" aria-label="Другие действия">…</Button.Root>
|
|
380
|
+
</Dropdown.Trigger>
|
|
381
|
+
<Dropdown.Content>
|
|
382
|
+
<Dropdown.Item tone="danger">Удалить приглашение</Dropdown.Item>
|
|
383
|
+
</Dropdown.Content>
|
|
384
|
+
</Dropdown.Root>
|
|
385
|
+
</Drawer.Actions>
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
```tsx good
|
|
389
|
+
// good — one action in sight, named by its tooltip
|
|
390
|
+
<Drawer.Actions>
|
|
391
|
+
<Tooltip.Root>
|
|
392
|
+
<Tooltip.Trigger>
|
|
393
|
+
<Button.Root variant="ghost" tone="danger" aria-label="Удалить приглашение">
|
|
394
|
+
<Button.Icon>
|
|
395
|
+
<Icon name="action.delete" />
|
|
396
|
+
</Button.Icon>
|
|
397
|
+
</Button.Root>
|
|
398
|
+
</Tooltip.Trigger>
|
|
399
|
+
<Tooltip.Content>Удалить приглашение</Tooltip.Content>
|
|
400
|
+
</Tooltip.Root>
|
|
401
|
+
</Drawer.Actions>
|
|
402
|
+
```
|
|
403
|
+
|
|
304
404
|
## 7. Forms
|
|
305
405
|
|
|
306
406
|
- Labels above every field (`label`), placeholder is an example value, help in `hint`, problems in
|
|
@@ -310,10 +410,10 @@ Hierarchy comes from space and a handful of type roles, not from size jumps, col
|
|
|
310
410
|
columns. Never a label in every row, never numbered labels, never one record split over lines.
|
|
311
411
|
FieldList draws every row's actions itself — the remove «×» and the handle after it, a pinned
|
|
312
412
|
row's remove off with its reason (`labels.lockedReason`) — so never add your own.
|
|
313
|
-
- The row's actions (`rowActions`)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
413
|
+
- The row's actions (`rowActions`) follow [the menu threshold](#a-menu-from-two-actions): a row
|
|
414
|
+
that only removes shows its «×» (the default `"buttons"`, and the grip after it with
|
|
415
|
+
`reorderable`), however many columns it has; `"menu"` — one «⋯» with «Выше», «Ниже», «Удалить» —
|
|
416
|
+
only for a `reorderable` list on a touch-first screen, where moving by the menu replaces a drag.
|
|
317
417
|
- The look (`variant`): `"plain"` (default) when the list is the content of its card or section;
|
|
318
418
|
`"surface"` when the list stands among other fields of a form or card, so its rows read as one
|
|
319
419
|
block apart from the fields around it.
|
|
@@ -334,40 +434,46 @@ Hierarchy comes from space and a handful of type roles, not from size jumps, col
|
|
|
334
434
|
section, the whole `Card.Root` inside `<form>`, buttons in `Card.Footer`.
|
|
335
435
|
- Submit: `noValidate`, validate all, set `error`s, focus the first invalid field, `loading` on the
|
|
336
436
|
submit button and the fields disabled (one `Fieldset disabled` per group of fields), then the
|
|
337
|
-
result: the
|
|
338
|
-
navigates) → a `Notification`; a failure → a `Notification` or the field's `error`.
|
|
437
|
+
result: the save button stays on screen → it says «Сохранено»; the button goes away (the dialog
|
|
438
|
+
closes, the page navigates, the button shows only while dirty) → a `Notification`; a failure → a `Notification` or the field's `error`.
|
|
339
439
|
- A footer button outside the form submits it with `form={formId}`.
|
|
340
|
-
- Choice controls
|
|
341
|
-
|
|
342
|
-
([choosing.md](choosing.md#2-inside-a-category)).
|
|
440
|
+
- Choice controls are picked by what the value means, not by when it saves:
|
|
441
|
+
[choosing.md](choosing.md#a-flag-a-level-a-set).
|
|
343
442
|
|
|
344
443
|
## 8. Tables and lists
|
|
345
444
|
|
|
346
445
|
### Where the filters go
|
|
347
446
|
|
|
348
|
-
A
|
|
349
|
-
|
|
350
|
-
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
447
|
+
A control stands with what it changes. Ask of each one: what does it narrow or switch?
|
|
448
|
+
|
|
449
|
+
- **One table** — its search and filter fields, their chips, its column chooser and the actions on
|
|
450
|
+
its selected rows belong to the `DataTable`, on every page: the page that is only this table, a
|
|
451
|
+
page with KPI cards above it, a page whose sections switch between tables. `SmartFilter.Root`
|
|
452
|
+
wraps the `DataTable`; `SmartFilter.Toolbar` goes in its `toolbar` (with a `DataTableColumnsMenu`
|
|
453
|
+
when the table has no row-actions column to carry `columnsMenu`), `SmartFilter.Chips` in its
|
|
454
|
+
`toolbarChips`, the selection menu comes from `renderSelectionActions`
|
|
455
|
+
([server.tsx](reference/components/data-table/examples/server.tsx), [the list page](#list-page)).
|
|
456
|
+
The tools then stay put when the page grows a block or another section, and every table of the
|
|
457
|
+
app reads the same.
|
|
458
|
+
- **The whole page** — `Page.TopBar`: the heading with its line of data, the page's sections (a
|
|
459
|
+
`SegmentedControl`; of links, `SegmentedControl.Item asChild`, when each section is a route of its
|
|
460
|
+
own — «Сотрудники» / «Роли»), a view switch, a period that drives several blocks, the page's
|
|
461
|
+
buttons.
|
|
462
|
+
- **Several views of one set** — a search and filter that narrows a board and its list alike, a
|
|
463
|
+
media grid, a schedule without a tools row of its own: `SmartFilter.Root` wraps `Page.Root`,
|
|
464
|
+
`SmartFilter.Toolbar` in `Page.Tools`, `SmartFilter.Chips` in `Page.Chips`
|
|
465
|
+
([the board](#sales-pipeline-board)).
|
|
466
|
+
- **A table section filters by its categories.** Every column with a closed set of values (a role, a
|
|
467
|
+
status, a type, an owner, a city) is an `options` field of the table's `SmartFilter`, a date
|
|
468
|
+
column a `date-range` field (its `calendar` with the presets of the domain), free text the
|
|
469
|
+
search. A search alone (`fields={[]}`) only for records with no such column. The value lives in
|
|
470
|
+
the URL ([nothing to guess](ux.md#6-nothing-to-guess)).
|
|
365
471
|
- Both rows are one tool row of the kit: written in reading order, a search takes the free room,
|
|
366
472
|
switchers and buttons keep their width at the end, and a short row wraps by itself — pass the
|
|
367
473
|
controls as they are, never inside a flex wrapper of your own.
|
|
368
474
|
|
|
369
475
|
```tsx partial
|
|
370
|
-
// bad — the table's search, filter, chips and chooser in the bar,
|
|
476
|
+
// bad — the table's search, filter, chips and chooser in the bar, away from the rows they filter
|
|
371
477
|
<SmartFilter.Root fields={FIELDS}>
|
|
372
478
|
<Page.Root>
|
|
373
479
|
<Page.TopBar>
|
|
@@ -463,12 +569,53 @@ A filter stands next to what it filters.
|
|
|
463
569
|
chooser is `<DataTableColumnsMenu>` beside the table's search ([where the filters go](#where-the-filters-go)),
|
|
464
570
|
bound to the same `hiddenColumns` / `columnOrder` state the table reads. Give the key column and
|
|
465
571
|
the actions column `hideable: false`.
|
|
466
|
-
- The DataTable `toolbar` holds
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
572
|
+
- The DataTable `toolbar` holds the table's own controls — its search and filter, the chooser, a
|
|
573
|
+
table / cards view — as they are, in reading order; its chips go to `toolbarChips`. A search
|
|
574
|
+
heads the row; a button never does: a lone «Экспорт» or «Создать» is the page's
|
|
575
|
+
(`Page.Actions`), a lone chooser goes over the row actions, a count or a total of the list is data
|
|
576
|
+
in `Page.Description`.
|
|
577
|
+
- **Actions on selected rows are one menu, added to the row.** `renderSelectionActions` returns the
|
|
578
|
+
`Dropdown.Item`s; while rows are selected the table puts a «Действия · N» menu button last in its
|
|
579
|
+
tools row — the search, filter and chooser stay where they were, and the button leaves when the
|
|
580
|
+
selection empties. Never a row of «Выбрано: N» and buttons that replaces the tools, never one
|
|
581
|
+
button per bulk action in the row. A destructive item asks with a `ConfirmDialog` that names the
|
|
582
|
+
count ([friction](ux.md#5-friction-matches-the-cost-of-a-mistake)).
|
|
583
|
+
|
|
584
|
+
```tsx partial
|
|
585
|
+
// bad — the selection swaps the tools row for a bar of buttons; search and filter vanish
|
|
586
|
+
<DataTable
|
|
587
|
+
selectable
|
|
588
|
+
selected={selected}
|
|
589
|
+
onSelectedChange={setSelected}
|
|
590
|
+
toolbar={selected.length > 0 ? <SelectionBar count={selected.length} /> : <SmartFilter.Toolbar />}
|
|
591
|
+
/>
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
```tsx good
|
|
595
|
+
// good — the tools stay, the actions on the selection are one menu that enters last
|
|
596
|
+
// declare: const FIELDS: Parameters<typeof SmartFilter.Root>[0]["fields"]
|
|
597
|
+
// declare: const members: { id: string; name: string }[]
|
|
598
|
+
// declare: const selected: string[]
|
|
599
|
+
// declare: const setSelected: (keys: string[]) => void
|
|
600
|
+
<SmartFilter.Root fields={FIELDS}>
|
|
601
|
+
<DataTable
|
|
602
|
+
columns={[{ id: "name", header: "Сотрудник", accessor: "name" }]}
|
|
603
|
+
rows={members}
|
|
604
|
+
getRowKey={(row) => row.id}
|
|
605
|
+
selectable
|
|
606
|
+
selected={selected}
|
|
607
|
+
onSelectedChange={(keys) => setSelected(keys.map(String))}
|
|
608
|
+
toolbar={<SmartFilter.Toolbar />}
|
|
609
|
+
toolbarChips={<SmartFilter.Chips />}
|
|
610
|
+
renderSelectionActions={() => (
|
|
611
|
+
<>
|
|
612
|
+
<Dropdown.Item>Сменить роль</Dropdown.Item>
|
|
613
|
+
<Dropdown.Item tone="danger">Отключить доступ</Dropdown.Item>
|
|
614
|
+
</>
|
|
615
|
+
)}
|
|
616
|
+
/>
|
|
617
|
+
</SmartFilter.Root>
|
|
618
|
+
```
|
|
472
619
|
|
|
473
620
|
### Tables
|
|
474
621
|
|
|
@@ -500,11 +647,11 @@ A filter stands next to what it filters.
|
|
|
500
647
|
|
|
501
648
|
| Situation | Component |
|
|
502
649
|
|---|---|
|
|
503
|
-
| the result of an action (sent, created, failed to save; saved when the form goes away) | `Notification` via `notify()` |
|
|
504
|
-
| a form saved
|
|
650
|
+
| the result of an action (sent, created, failed to save; saved when the form or its save button goes away) | `Notification` via `notify()` |
|
|
651
|
+
| a form saved whose save button stays on screen | the submit button's text: «Сохранено» |
|
|
505
652
|
| a state of the page or a section that persists (sync failed, trial ends, read-only) | `Banner` as the first block of `Page.Body` (or inside the Card it concerns) |
|
|
506
653
|
| a problem with one field | the field's `error` |
|
|
507
|
-
| a refused sign-in | `invalid` on the
|
|
654
|
+
| a refused sign-in | `invalid` on the e-mail and the reason in words as the password's `error` — no Banner ([field outcomes](api-contract.md#field-outcomes-a-mark-for-success-words-for-a-problem)) |
|
|
508
655
|
| a region failed to load | the region shows it in place: DataTable `loadError`, or `EmptyPage tone="danger"` with `EmptyPage.RetryButton` |
|
|
509
656
|
| a region is loading | DataTable `loading`; otherwise a `Skeleton` in the geometry of the content + `aria-busy` on the region; `Spinner` only when there is no shape to hold |
|
|
510
657
|
| a region changes state (loading → data → empty → error, another record) | its content in `Crossfade state={status}` — always, so the change flows instead of flipping |
|
|
@@ -541,6 +688,76 @@ are `Drawer.Section` / `Modal.Section` with their own `Drawer.Actions` / `Modal.
|
|
|
541
688
|
hand-made heading row; a footer's `Drawer.Meta` / `Modal.Meta` goes above the buttons by itself when
|
|
542
689
|
the footer runs short.
|
|
543
690
|
|
|
691
|
+
### A record in a drawer
|
|
692
|
+
|
|
693
|
+
A record opened beside the page — a person, a role, a deal — is one drawer per entity, the same
|
|
694
|
+
wherever it opens:
|
|
695
|
+
|
|
696
|
+
- **One entity, one drawer.** One component opens the record from every place — the person's own
|
|
697
|
+
account in the Sidebar, a row of the team table, a mention — by its id. Who looks decides what it
|
|
698
|
+
shows: a field the viewer may not change is `readOnly` (`disabled` when the reason is said beside
|
|
699
|
+
it), a tab the viewer may not see is not rendered, an action the viewer may not take is hidden. A
|
|
700
|
+
second drawer for the same entity («Мой аккаунт» beside «Сотрудник») drifts apart from the first
|
|
701
|
+
with every change.
|
|
702
|
+
- **One record header** — the same in a Drawer, a Modal and a Sheet. `Drawer.Icon` leads: a
|
|
703
|
+
glyph gets a 40px tone tile, the record's `Avatar.Root` or `Thumbnail.Root` (even inside a wrapper
|
|
704
|
+
of your own) stands at 40 with no tile. Line 1 is `Drawer.Title`, the name (`loading` while it
|
|
705
|
+
comes). Line 2 is `Drawer.Description`: the status `Badge.Root`s first — the header sizes them, no
|
|
706
|
+
`size` — then the line of data. `Drawer.Actions` hold only buttons that act on this record: ghost
|
|
707
|
+
icon Buttons, each in a `Tooltip.Trigger`, counted by [the menu threshold](#a-menu-from-two-actions);
|
|
708
|
+
they are centred on line 1, before the ×. A Badge in `Drawer.Actions` is a status in the wrong place; a way to
|
|
709
|
+
another place (a section, a list) is no header action — a link in the body beside the data it leads
|
|
710
|
+
to, or nothing.
|
|
711
|
+
- **One form, one save.** Every editable field of every tab is one form, and its submit is the one
|
|
712
|
+
«Сохранить» of `Drawer.Footer`, shown while the form is dirty, with `Drawer.Guard` (`dirty`,
|
|
713
|
+
`busy`). No save per section or tab, no «Есть несохранённые изменения» in `Drawer.Meta`: the save
|
|
714
|
+
button and the guard already say it. Actions that work at once — turn sign-in on, send a link,
|
|
715
|
+
end the other sessions — are buttons beside what they change and never wait for the save. After
|
|
716
|
+
the save the button leaves, so the success is a `Notification`.
|
|
717
|
+
- **Tabs are topics.** Each tab holds one topic, and a record's history (its log) is always a tab of
|
|
718
|
+
its own. Tabs that outgrow the width never merge: the strip scrolls ([switchers](responsive.md#switchers)).
|
|
719
|
+
- **Data at the drawer's level.** The drawer loads the record and the data of its tabs; a tab never
|
|
720
|
+
fetches on mount, so coming back to it shows what it showed instead of a skeleton that collapses
|
|
721
|
+
it. A tab that keeps a draft or its scroll keeps its panel with `Tabs.Panel keepMounted`. The
|
|
722
|
+
opening itself — the header and the tabs at once, the body as a Skeleton in the form's geometry —
|
|
723
|
+
is in [Drawer](reference/components/drawer/COMPONENT.md).
|
|
724
|
+
|
|
725
|
+
```tsx partial
|
|
726
|
+
// bad — a way out in the header, a save per section, a status line in the footer
|
|
727
|
+
<Drawer.Actions>
|
|
728
|
+
<Button.Root variant="ghost" tone="neutral">Команда и роли</Button.Root>
|
|
729
|
+
</Drawer.Actions>
|
|
730
|
+
<Drawer.Section label="Профиль">… <Button.Root>Сохранить</Button.Root></Drawer.Section>
|
|
731
|
+
<Drawer.Section label="Доступ">… <Button.Root>Сохранить</Button.Root></Drawer.Section>
|
|
732
|
+
<Drawer.Footer>
|
|
733
|
+
<Drawer.Meta>Есть несохранённые изменения</Drawer.Meta>
|
|
734
|
+
</Drawer.Footer>
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
```tsx good
|
|
738
|
+
// good — one form over the tabs, one save while it is dirty, the guard holds the rest
|
|
739
|
+
// declare: const dirty: boolean
|
|
740
|
+
// declare: const saving: boolean
|
|
741
|
+
<Drawer.Content>
|
|
742
|
+
<Drawer.Guard dirty={dirty} busy={saving} />
|
|
743
|
+
<Drawer.Header>
|
|
744
|
+
<Drawer.Title>Анна Соколова</Drawer.Title>
|
|
745
|
+
</Drawer.Header>
|
|
746
|
+
<Drawer.Body>
|
|
747
|
+
<form id="member-form" noValidate>
|
|
748
|
+
…
|
|
749
|
+
</form>
|
|
750
|
+
</Drawer.Body>
|
|
751
|
+
<Drawer.Footer>
|
|
752
|
+
{dirty ? (
|
|
753
|
+
<Button.Root type="submit" form="member-form" loading={saving}>
|
|
754
|
+
Сохранить
|
|
755
|
+
</Button.Root>
|
|
756
|
+
) : null}
|
|
757
|
+
</Drawer.Footer>
|
|
758
|
+
</Drawer.Content>
|
|
759
|
+
```
|
|
760
|
+
|
|
544
761
|
## 11. Narrow screens
|
|
545
762
|
|
|
546
763
|
- Layouts are [intrinsic](responsive.md#intrinsic-grids) — no breakpoints needed, and they work in
|
|
@@ -630,7 +847,7 @@ What each screen in [the index](#screens) demonstrates — keep these when you s
|
|
|
630
847
|
[data-table/examples/screen.tsx](reference/components/data-table/examples/screen.tsx)
|
|
631
848
|
|
|
632
849
|
- A work screen titles itself in its `Page.TopBar`: `Page.Title` is the only heading, its `Page.Description` is one line of figures («10 счетов · просрочено 2 на …»), not a sentence about the page; there is no second header in the flow.
|
|
633
|
-
- The
|
|
850
|
+
- The search and filters narrow the table, so they are the table's: `SmartFilter.Toolbar` in its `toolbar`, `SmartFilter.Chips` in its `toolbarChips`, the column chooser over the row menu (`columnsMenu`); the bar keeps the heading, export (`soft` + `neutral`, an icon beside its label) and the one primary action (`solid`, last) in `Page.Actions`. A KPI row added above changes nothing in the table's tools.
|
|
634
851
|
- A status is a `Badge` with one `tone` per meaning across the product (`STATUS` is one map); the text carries the meaning, the tone repeats it.
|
|
635
852
|
- Numbers and dates are `numeric` columns with `headerAlign="end"` (tabular figures, values and heading on the end edge). On a phone the table scrolls inside itself and `stickyFirstColumn` keeps the number in place — the page never scrolls sideways.
|
|
636
853
|
- Row actions are a `Dropdown` behind an `action.more` icon of size `s` in the last column (`hideHeader`), pinned to the end edge with `stickyLastColumn` so they stay in reach while the columns scroll: controls in cells are one tier below the table. Delete is `tone="danger"` at the end of the menu.
|
|
@@ -664,7 +881,7 @@ What each screen in [the index](#screens) demonstrates — keep these when you s
|
|
|
664
881
|
|
|
665
882
|
[drawer/examples/screen.tsx](reference/components/drawer/examples/screen.tsx)
|
|
666
883
|
|
|
667
|
-
- A work screen titles itself in its `Page.TopBar`: `Page.Title` with the counts in `Page.Description
|
|
884
|
+
- A work screen titles itself in its `Page.TopBar`: `Page.Title` with the counts in `Page.Description`; the search (`SmartFilter` with `fields={[]}`) narrows the table, so it is the table's `toolbar`; there is no second header in the flow.
|
|
668
885
|
- The opening button is the page's primary action in `Page.Actions`; `Drawer.Trigger` wraps it as a slot.
|
|
669
886
|
- The form is tied to the footer button through `form={formId}`: `Drawer.Footer` is outside the `<form>`, and submitting still works, by Enter too.
|
|
670
887
|
- Groups are a `Fieldset` without a frame with a `title-s` heading 16 px above the fields; 32 px of air between groups, no dividers. `Fieldset disabled` turns all the fields off while the request runs.
|
|
@@ -187,9 +187,20 @@ of your own elements — when to move at all, the tokens, the rules and recipes
|
|
|
187
187
|
|
|
188
188
|
## 11. State changes are continuous
|
|
189
189
|
|
|
190
|
-
Nothing on screen flips from one state to another
|
|
191
|
-
Every region you build that changes what it shows — loading →
|
|
192
|
-
another — renders its content inside `<Crossfade state={status}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
190
|
+
Nothing on screen flips from one state to another, and nothing that loads unfolds. Kit controls
|
|
191
|
+
already move into their new state. Every region you build that changes what it shows — loading →
|
|
192
|
+
data → empty → error, one record → another — renders its content inside `<Crossfade state={status}>`.
|
|
193
|
+
|
|
194
|
+
- **Loading is a skeleton from the first paint.** While data loads the region shows the state
|
|
195
|
+
`"loading"` (that exact key): a `<Skeleton>` in the data's geometry (same rows, gaps and line
|
|
196
|
+
heights) rendered on the first render — never `null`, never an empty box that grows when the data
|
|
197
|
+
comes. A `Spinner` only where there is no shape to hold.
|
|
198
|
+
- **The data replaces the skeleton in place.** Leaving `"loading"` (or entering it), Crossfade swaps
|
|
199
|
+
with a short fade and the height settles in one frame — no glide, no reveal, no slide-in. A
|
|
200
|
+
skeleton in the wrong geometry therefore shows as a jump: match the data.
|
|
201
|
+
- **Only settled states glide.** data ↔ empty ↔ error, another record, a period or a section: the old
|
|
202
|
+
state fades out, the new one fades in, the height glides.
|
|
203
|
+
- **A region that mounts in its state is still.** Content already loaded (a cached tab, a page
|
|
204
|
+
opened again) renders as it is, with no fade.
|
|
205
|
+
|
|
206
|
+
Data already on screen stays during a refresh (`aria-busy`), it does not go back to a skeleton.
|
package/dist/skill/layouts.md
CHANGED
|
@@ -142,6 +142,7 @@ when a count gets a colour — are in
|
|
|
142
142
|
| Everyday places | `Sidebar.Group` without `label` at the top, never folded |
|
|
143
143
|
| Category of work | `Sidebar.Group label collapsible` |
|
|
144
144
|
| Views of one section | `Sidebar.Sub`, one level only |
|
|
145
|
+
| Facets of one section (its list, its statistics) | none: route links in the page's `Page.Tools` ([a section with sub-pages](composition.md#a-section-with-sub-pages)) |
|
|
145
146
|
| Meta | `Sidebar.Footer`: help, settings, then `Sidebar.Account` with its menu last |
|
|
146
147
|
|
|
147
148
|
### Sidebar icons
|
|
@@ -187,39 +188,89 @@ form rules: `Modal.Body` holds the `<form id>`, `Modal.Footer` holds Cancel + su
|
|
|
187
188
|
|
|
188
189
|
## Auth (sign in, code)
|
|
189
190
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
reset and
|
|
191
|
+
**One auth screen, in modes.** Sign-in, sign-up, an invite (set a password by a link), a password
|
|
192
|
+
reset and a one-time code are one public route — `/login`, the mode in its query (`?invite=…`,
|
|
193
|
+
`?reset=…`, `?step=code`) — and one `LoginForm` card whose header and form change with the mode.
|
|
194
|
+
A new link-driven step is a new mode of that screen, never a page of its own: it would get its own
|
|
195
|
+
canvas, its own copy of the card and drift from the others. No AppShell; `DigitInput` inside
|
|
196
|
+
`LoginForm.Form` for a code; the scenarios are in `src/components/login-form/examples/`.
|
|
197
|
+
|
|
198
|
+
**The head names the action.** `LoginForm.Root` keeps its default `align="start"`: `LoginForm.Logo`
|
|
199
|
+
holds the icon of the action (`action.login` for «Войти в аккаунт»; a key for «Задайте пароль»),
|
|
200
|
+
`LoginForm.Title` names the action, `LoginForm.Description` is one line of what or whom it concerns
|
|
201
|
+
(«Приглашение в «Северный ветер» для a.sokolova@…»). The product mark centred over the title
|
|
202
|
+
(`align="center"`) belongs to a public brand page, not to the product's own door. Another state of
|
|
203
|
+
the screen (a link that expired, the check of a token) swaps inside the canvas around the card
|
|
204
|
+
through a `Crossfade` without `fill`, its loading branch a `Skeleton` of the card's geometry.
|
|
205
|
+
|
|
206
|
+
```tsx partial
|
|
207
|
+
// bad — a page of its own for the invite, a centred brand mark over a generic title
|
|
208
|
+
// /invite/:token
|
|
209
|
+
<LoginForm.Root align="center">
|
|
210
|
+
<LoginForm.Header>
|
|
211
|
+
<LoginForm.Logo aria-label="CRM">…</LoginForm.Logo>
|
|
212
|
+
<LoginForm.Title>Добро пожаловать</LoginForm.Title>
|
|
213
|
+
</LoginForm.Header>
|
|
214
|
+
…
|
|
215
|
+
</LoginForm.Root>
|
|
216
|
+
```
|
|
193
217
|
|
|
194
218
|
```tsx
|
|
195
219
|
import { Button, Icon, Input, LinkButton, LoginForm } from "prime-ui-kit";
|
|
196
220
|
import styles from "./SignIn.module.css";
|
|
197
221
|
|
|
198
|
-
|
|
222
|
+
/** The mode comes from the query of `/login`: `?invite=…` sets a password, nothing signs in. */
|
|
223
|
+
type AuthMode = "login" | "invite";
|
|
224
|
+
|
|
225
|
+
const HEAD = {
|
|
226
|
+
login: { icon: "action.login", title: "Войти в аккаунт", description: "Рабочая почта и пароль" },
|
|
227
|
+
invite: { icon: "object.key", title: "Задайте пароль", description: "Приглашение в «Северный ветер»" },
|
|
228
|
+
} as const;
|
|
229
|
+
|
|
230
|
+
export function SignIn({ mode }: { mode: AuthMode }) {
|
|
231
|
+
const head = HEAD[mode];
|
|
199
232
|
return (
|
|
200
233
|
<main className={styles.page}>
|
|
201
|
-
<LoginForm.Root
|
|
234
|
+
<LoginForm.Root>
|
|
202
235
|
<LoginForm.Header>
|
|
203
236
|
<LoginForm.Logo>
|
|
204
|
-
<Icon name=
|
|
237
|
+
<Icon name={head.icon} />
|
|
205
238
|
</LoginForm.Logo>
|
|
206
|
-
<LoginForm.Title
|
|
207
|
-
<LoginForm.Description
|
|
239
|
+
<LoginForm.Title>{head.title}</LoginForm.Title>
|
|
240
|
+
<LoginForm.Description>{head.description}</LoginForm.Description>
|
|
208
241
|
</LoginForm.Header>
|
|
209
242
|
<LoginForm.Body>
|
|
210
243
|
<LoginForm.Form onSubmit={(event) => event.preventDefault()}>
|
|
211
|
-
|
|
244
|
+
{mode === "login" ? (
|
|
245
|
+
<Input.Root label="Рабочая почта" required>
|
|
246
|
+
<Input.Wrapper>
|
|
247
|
+
<Input.Field type="email" autoComplete="email" placeholder="name@company.ru" />
|
|
248
|
+
</Input.Wrapper>
|
|
249
|
+
</Input.Root>
|
|
250
|
+
) : null}
|
|
251
|
+
<Input.Root
|
|
252
|
+
label={mode === "login" ? "Пароль" : "Новый пароль"}
|
|
253
|
+
required
|
|
254
|
+
strength={mode === "invite"}
|
|
255
|
+
minStrength={mode === "invite" ? 3 : undefined}
|
|
256
|
+
>
|
|
212
257
|
<Input.Wrapper>
|
|
213
|
-
<Input.Field
|
|
258
|
+
<Input.Field
|
|
259
|
+
type="password"
|
|
260
|
+
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
|
261
|
+
/>
|
|
262
|
+
<Input.RevealButton />
|
|
214
263
|
</Input.Wrapper>
|
|
215
264
|
</Input.Root>
|
|
216
265
|
<Button.Root type="submit" fullWidth>
|
|
217
|
-
|
|
266
|
+
{mode === "login" ? "Войти" : "Сохранить пароль"}
|
|
218
267
|
</Button.Root>
|
|
219
268
|
</LoginForm.Form>
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
269
|
+
{mode === "login" ? (
|
|
270
|
+
<LoginForm.Footer>
|
|
271
|
+
Нет аккаунта? <LinkButton href="/login?mode=signup">Зарегистрироваться</LinkButton>
|
|
272
|
+
</LoginForm.Footer>
|
|
273
|
+
) : null}
|
|
223
274
|
</LoginForm.Body>
|
|
224
275
|
</LoginForm.Root>
|
|
225
276
|
</main>
|
package/dist/skill/motion.md
CHANGED
|
@@ -35,7 +35,8 @@ Use the component and the motion comes with it. Never re-animate these with your
|
|
|
35
35
|
| Overlays | grow from the trigger (menus, popovers, tooltip from its arrow), modals stay centred, drawers, sheets and toasts come from their edge; a dialog, drawer or sheet leaves faster than it came, a small floating panel opens and closes on the same quick `fast` clock; the last content stays on screen while the exit plays | `Dropdown`, `Popover`, `Tooltip`, `Modal`, `ConfirmDialog`, `Drawer`, `Sheet`, `MediaViewer`, notifications — the `Root` kept mounted |
|
|
36
36
|
| Tooltips along a toolbar | the first waits, the next ones show at once | `Tooltip` (shared warm window) |
|
|
37
37
|
| Toast stack | depth stack, expands on hover | `NotificationProvider` + `useNotifications` |
|
|
38
|
-
| Loading → data
|
|
38
|
+
| Loading → data | the skeleton is there from the first paint and the data takes its place with a short fade; the height never glides | `Crossfade` with the state `"loading"`, `Skeleton` in the data's geometry |
|
|
39
|
+
| Data → empty → error, another record | the region cross-fades and glides its height | `Crossfade` around the region |
|
|
39
40
|
| Drag | the carried piece lifts, its neighbours glide apart, the drop glides into place; while its save runs the piece stays still and its grip turns busy, and a refused save glides it back — across columns too | `Dnd`, `Kanban` cards and columns, DataTable and `List` `onReorder`, `FileUpload` tiles |
|
|
40
41
|
| A setting saved on the server | the thumb moves at once and a spinner turns inside it (a slider thumb dims); a refused save glides it back | `Switch` (`onCheckedChange` returns a Promise, or `loading`), `Slider` (`onCommit`) |
|
|
41
42
|
| A chip or a file row removed | it waits dimmed with a spinner on its ×; once gone it fades and closes up, the next ones glide into its place; refused, it stays | `Badge` `onRemove`, `FileUpload` `onValueChange`, TagSelect chips |
|
|
@@ -142,8 +143,10 @@ export function DeleteDraft({ onDelete }: { onDelete: () => void }) {
|
|
|
142
143
|
}
|
|
143
144
|
```
|
|
144
145
|
|
|
145
|
-
**A region that loads.** Wrap it in `Crossfade` keyed by the state
|
|
146
|
-
|
|
146
|
+
**A region that loads.** Wrap it in `Crossfade` keyed by the state, the loading one keyed
|
|
147
|
+
`"loading"`, and show a `Skeleton` in the shape of the data from the first render — never a spinner
|
|
148
|
+
in the middle of an empty card, never a flip, never a region that unfolds as the data comes
|
|
149
|
+
([the rule](foundations.md#11-state-changes-are-continuous)).
|
|
147
150
|
|
|
148
151
|
**A metric that updates.** Put a number in a `Badge` or a `Sparkline` and change it: the digits roll.
|
|
149
152
|
A number that changes by itself every frame (a running percentage) stays plain tabular text — rolling
|
|
@@ -188,6 +191,9 @@ export function CloseQuarter({ onClose }: { onClose: () => void }) {
|
|
|
188
191
|
- `transition: all 0.3s ease` → list the properties, use tokens.
|
|
189
192
|
- A spinner where the content's shape is known → `Skeleton` in that shape inside `Crossfade`.
|
|
190
193
|
- Content that appears or disappears with no transition (`{loading ? <A/> : <B/>}`) → `Crossfade`.
|
|
194
|
+
- A loading state that renders nothing, or a list that slides or unfolds in when its data arrives →
|
|
195
|
+
a `Skeleton` in the data's geometry from the first render, under the state `"loading"`, so the data
|
|
196
|
+
replaces it in place.
|
|
191
197
|
- Bounce, elastic or a spring that overshoots → `emphasized` lands without passing the target.
|
|
192
198
|
- Animating the highlight of a list moved by arrow keys, or a command palette → keep it still.
|
|
193
199
|
- Hover lift on table rows or list items → fill change only.
|
|
@@ -121,6 +121,7 @@ The soft fill is opaque: the soft hue is composited over the ground the avatar s
|
|
|
121
121
|
- Avatar → name: space step 3 (12px) in lists and cards, step 2 (8px) in table cells.
|
|
122
122
|
- Group: members overlap by an eighth of the diameter (initials are never cut) with a 2px ring in the ground they stand on: the current layer of the surface around them (no surface sets anything), or `--avatar-ring` from a host whose ground is not a layer (a reaction chip sets its own fill); the fills of initials and «+N» are composited over the same ground.
|
|
123
123
|
- An icon fallback is half the diameter: a kit `Icon` reads `--prime-icon-size`, a raw svg too.
|
|
124
|
+
- In a leading media slot (`Drawer.Icon`, `Modal.Icon`, `Sheet.Icon`, `Card.Icon`) the avatar takes the slot's size (`l`, 40px) whatever `size` it was given, also through a wrapper component of yours; the slot then draws no tile.
|
|
124
125
|
|
|
125
126
|
## Accessibility
|
|
126
127
|
|