prime-ui-kit 0.9.2 → 0.9.3

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.
Files changed (96) hide show
  1. package/README.md +9 -5
  2. package/dist/chunks/{chunk-ZEFCDEEQ.js → chunk-6JTZOA5I.js} +878 -331
  3. package/dist/chunks/{chunk-LXGRKN34.js → chunk-OAE4IQX2.js} +769 -479
  4. package/dist/chunks/{chunk-IQELQNMR.js → chunk-U5SR2LZR.js} +17 -4
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/button/Button.d.ts +19 -2
  7. package/dist/components/card/Card.d.ts +1 -1
  8. package/dist/components/code-block/CodeBlock.d.ts +1 -1
  9. package/dist/components/color-picker/ColorPicker.d.ts +1 -1
  10. package/dist/components/data-table/Row.d.ts +3 -1
  11. package/dist/components/data-table/rows.d.ts +4 -0
  12. package/dist/components/digit-input/DigitInput.d.ts +6 -1
  13. package/dist/components/index.d.ts +4 -1
  14. package/dist/components/input/Input.d.ts +16 -1
  15. package/dist/components/input/passwordStrength.d.ts +9 -0
  16. package/dist/components/progress-bar/ProgressBar.d.ts +16 -1
  17. package/dist/components/sparkline/Sparkline.d.ts +26 -0
  18. package/dist/hooks/useEdgeOverflow.d.ts +4 -0
  19. package/dist/icons/registry.d.ts +2 -0
  20. package/dist/index.css +830 -267
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +9 -3
  23. package/dist/internal/CheckMark.d.ts +12 -0
  24. package/dist/internal/MorphText.d.ts +12 -0
  25. package/dist/internal/RollingNumber.d.ts +14 -0
  26. package/dist/internal/surfaceDepth.d.ts +25 -0
  27. package/dist/internal/swatch.d.ts +4 -1
  28. package/dist/layout/app-shell/AppShell.d.ts +2 -2
  29. package/dist/motion/celebrate.d.ts +18 -0
  30. package/dist/tokens/layers.d.ts +37 -0
  31. package/dist/tokens/naming.d.ts +1 -1
  32. package/dist/tokens/semantic.d.ts +66 -56
  33. package/dist/tokens/themes/dark.d.ts +6 -14
  34. package/package.json +1 -1
  35. package/src/components/accordion/COMPONENT.md +4 -4
  36. package/src/components/avatar/COMPONENT.md +1 -1
  37. package/src/components/button/COMPONENT.md +26 -5
  38. package/src/components/button/examples/download.tsx +45 -0
  39. package/src/components/button/examples/hold-to-confirm.tsx +23 -0
  40. package/src/components/button/examples/label-morph.tsx +33 -0
  41. package/src/components/card/COMPONENT.md +4 -2
  42. package/src/components/card/examples/narrow.tsx +2 -2
  43. package/src/components/card/examples/panel-chart.tsx +1 -1
  44. package/src/components/card/examples/variants.tsx +1 -1
  45. package/src/components/checkbox/examples/examples.module.css +1 -1
  46. package/src/components/code-block/COMPONENT.md +6 -6
  47. package/src/components/code-block/examples/overview.tsx +1 -1
  48. package/src/components/code-block/examples/variants.tsx +1 -1
  49. package/src/components/color-picker/COMPONENT.md +1 -1
  50. package/src/components/color-swatches/COMPONENT.md +5 -5
  51. package/src/components/command-menu/COMPONENT.md +1 -1
  52. package/src/components/crossfade/examples/overview.tsx +3 -3
  53. package/src/components/data-table/COMPONENT.md +5 -4
  54. package/src/components/digit-input/COMPONENT.md +5 -3
  55. package/src/components/digit-input/examples/on-complete.tsx +7 -6
  56. package/src/components/dnd/COMPONENT.md +1 -1
  57. package/src/components/dnd/examples/examples.module.css +4 -4
  58. package/src/components/dropdown/COMPONENT.md +1 -1
  59. package/src/components/dropdown/examples/examples.module.css +1 -1
  60. package/src/components/empty-page/examples/examples.module.css +1 -1
  61. package/src/components/example-frame/COMPONENT.md +1 -1
  62. package/src/components/input/COMPONENT.md +10 -1
  63. package/src/components/input/examples/password-strength.tsx +12 -0
  64. package/src/components/kanban/COMPONENT.md +4 -4
  65. package/src/components/login-form/COMPONENT.md +4 -4
  66. package/src/components/login-form/examples/register.tsx +2 -2
  67. package/src/components/login-form/examples/reset-password.tsx +2 -2
  68. package/src/components/modal/COMPONENT.md +2 -2
  69. package/src/components/modal/examples/examples.module.css +2 -2
  70. package/src/components/page-content/examples/examples.module.css +1 -1
  71. package/src/components/popover/COMPONENT.md +1 -1
  72. package/src/components/progress-bar/COMPONENT.md +5 -1
  73. package/src/components/progress-bar/examples/examples.module.css +7 -0
  74. package/src/components/progress-bar/examples/milestone.tsx +43 -0
  75. package/src/components/progress-bar/examples/steps.tsx +35 -0
  76. package/src/components/skeleton/examples/with-crossfade.tsx +3 -3
  77. package/src/components/sparkline/COMPONENT.md +98 -0
  78. package/src/components/sparkline/examples/examples.module.css +13 -0
  79. package/src/components/sparkline/examples/overview.tsx +27 -0
  80. package/src/components/sparkline/examples/period.tsx +46 -0
  81. package/src/components/tabs/COMPONENT.md +15 -16
  82. package/src/components/tabs/examples/examples.module.css +14 -0
  83. package/src/components/tabs/examples/orientation.tsx +87 -33
  84. package/src/components/tooltip/COMPONENT.md +8 -6
  85. package/src/components/tooltip/examples/examples.module.css +2 -2
  86. package/src/layout/app-header/COMPONENT.md +1 -1
  87. package/src/layout/app-header/examples/examples.module.css +4 -4
  88. package/src/layout/app-shell/COMPONENT.md +8 -8
  89. package/src/layout/app-shell/examples/examples.module.css +3 -3
  90. package/src/layout/bottom-nav/COMPONENT.md +1 -1
  91. package/src/layout/bottom-nav/examples/examples.module.css +1 -1
  92. package/src/layout/sidebar/COMPONENT.md +4 -4
  93. package/src/layout/sidebar/examples/examples.module.css +5 -5
  94. package/src/styles/globals.css +125 -2
  95. package/src/styles/theme-dark.css +36 -13
  96. package/src/styles/theme-light.css +60 -16
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./components";
2
2
  export * from "./icons";
3
3
  export * from "./internal/states";
4
+ export * from "./motion/celebrate";
4
5
  export * from "./theme/applyTheme";
package/dist/index.js CHANGED
@@ -34,7 +34,6 @@ import {
34
34
  PageToolbar,
35
35
  Pagination,
36
36
  Popover,
37
- ProgressBar,
38
37
  ProgressCircle,
39
38
  Radio,
40
39
  SegmentedControl,
@@ -42,6 +41,7 @@ import {
42
41
  Skeleton,
43
42
  Slider,
44
43
  SmartFilter,
44
+ Sparkline,
45
45
  Stepper,
46
46
  Switch,
47
47
  THEME_SWITCHING_ATTRIBUTE,
@@ -53,6 +53,7 @@ import {
53
53
  Typography,
54
54
  YEARLESS_YEAR,
55
55
  applyTheme,
56
+ celebrate,
56
57
  datepickerPresets,
57
58
  formatDatepickerValue,
58
59
  matchesSmartFilter,
@@ -61,7 +62,7 @@ import {
61
62
  suspendTransitions,
62
63
  useNotifications,
63
64
  useSidebar
64
- } from "./chunks/chunk-LXGRKN34.js";
65
+ } from "./chunks/chunk-OAE4IQX2.js";
65
66
  import {
66
67
  Button,
67
68
  COLOR_PRESETS,
@@ -74,6 +75,7 @@ import {
74
75
  Icon,
75
76
  Input,
76
77
  Label,
78
+ ProgressBar,
77
79
  ScrollContainer,
78
80
  Select,
79
81
  Spinner,
@@ -81,13 +83,14 @@ import {
81
83
  controlSizes,
82
84
  createIcon,
83
85
  dataStates,
86
+ getPasswordStrength,
84
87
  paletteColors,
85
88
  stepDown,
86
89
  textTones,
87
90
  tones,
88
91
  useOptionalControlSize,
89
92
  variants
90
- } from "./chunks/chunk-ZEFCDEEQ.js";
93
+ } from "./chunks/chunk-6JTZOA5I.js";
91
94
  export {
92
95
  Accordion,
93
96
  AppHeader,
@@ -145,6 +148,7 @@ export {
145
148
  Skeleton,
146
149
  Slider,
147
150
  SmartFilter,
151
+ Sparkline,
148
152
  Spinner,
149
153
  Stepper,
150
154
  Switch,
@@ -158,11 +162,13 @@ export {
158
162
  Typography,
159
163
  YEARLESS_YEAR,
160
164
  applyTheme,
165
+ celebrate,
161
166
  controlSizes,
162
167
  createIcon,
163
168
  dataStates,
164
169
  datepickerPresets,
165
170
  formatDatepickerValue,
171
+ getPasswordStrength,
166
172
  matchesSmartFilter,
167
173
  moveBefore,
168
174
  paletteColors,
@@ -0,0 +1,12 @@
1
+ export type CheckMarkState = "checked" | "unchecked" | "indeterminate";
2
+ /**
3
+ * The kit's check and indeterminate bar, drawn in with a stroke reveal (foundation §7 rule 8: a
4
+ * check draws in, it does not pop). Both paths stay in the DOM so the mark animates both ways;
5
+ * `state` picks the one that shows. Color is `currentColor`; size comes from the host box.
6
+ * Shared by Checkbox and the color swatches. Own SVG instead of `Icon`: the motion needs
7
+ * `pathLength` on the paths.
8
+ */
9
+ export declare function CheckMark({ state, className }: {
10
+ state: CheckMarkState;
11
+ className?: string;
12
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,12 @@
1
+ export type MorphTextProps = {
2
+ children: string;
3
+ className?: string;
4
+ };
5
+ /**
6
+ * A label that changes in place (foundation §7 rule 8): the old letters leave upward, the new ones
7
+ * come from below one short step apart, and the box glides between the two widths. At rest it is
8
+ * one plain text span; the letter layers exist only while a change plays. Still on the first
9
+ * render, under reduced motion, and when only digits change (a counter or a percentage in a label
10
+ * updates in place).
11
+ */
12
+ export declare function MorphText({ children: text, className }: MorphTextProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,14 @@
1
+ export type RollingNumberProps = {
2
+ /** The formatted value («4 812 350 ₽», «12», «−3,4%»); digits roll, other characters stay put. */
3
+ children: string | number;
4
+ className?: string;
5
+ };
6
+ /**
7
+ * A number whose digits roll to their new values column by column, like an odometer (foundation §7
8
+ * rule 8: counters ease, they do not flip). The DOM holds the real digits (text, copy, assistive
9
+ * tech and a page without CSS read the value); each digit column draws the 0–9 strip in a
10
+ * pseudo-element. Columns are keyed from the right, so an unchanged digit stays still and a new
11
+ * leading digit appears without rolling. A value that changes by itself every frame (a running
12
+ * percentage) stays plain tabular text: rolling there is noise.
13
+ */
14
+ export declare function RollingNumber({ children, className }: RollingNumberProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Depth of a surface on the ladder (foundation §4): `0` the page … `4` the deepest nested layer,
4
+ * `floating` for menus, popovers, modals and drawers, `floating-1`/`floating-2` for layers nested in
5
+ * one, `tinted` for a host off the ladder (a tinted banner, an accent wash). A surface renders
6
+ * `data-depth` with it; `globals.css` maps each depth to the context variables
7
+ * (`--prime-color-layer-current`, `--prime-color-field-bg`…).
8
+ */
9
+ export type SurfaceDepth = 0 | 1 | 2 | 3 | 4 | "floating" | "floating-1" | "floating-2" | "tinted";
10
+ /**
11
+ * The depth of a surface nested in `depth`; the deepest layer and `floating-2` repeat, a card on a
12
+ * tinted host is a card on the page.
13
+ */
14
+ export declare function nextSurfaceDepth(depth: SurfaceDepth): SurfaceDepth;
15
+ /** Depth of the surface the caller renders: one above the nearest surface around it. */
16
+ export declare function useNestedSurfaceDepth(): SurfaceDepth;
17
+ /** Surfaces inside `children` count their depth from `value`. */
18
+ export declare const SurfaceDepthProvider: React.Provider<SurfaceDepth>;
19
+ /**
20
+ * The layer an ExampleFrame stage stands for (the docs' "preview background"). The stage element
21
+ * itself takes it as `data-depth`: no wrapper may sit between the stage and the example, because
22
+ * the stage lays out its children with child selectors (`> *`).
23
+ */
24
+ export declare const StageDepthProvider: React.Provider<SurfaceDepth>;
25
+ export declare function useStageDepth(): SurfaceDepth;
@@ -31,7 +31,10 @@ export declare const swatchTierClass: string;
31
31
  export declare function SwatchFill({ value }: {
32
32
  value: string | null;
33
33
  }): import("react/jsx-runtime").JSX.Element;
34
- /** Inside a selectable swatch: the color layer and, when selected, the contrasting check. */
34
+ /**
35
+ * Inside a selectable swatch: the color layer and the contrasting check, which draws in when the
36
+ * swatch is selected and retracts when it is not (`CheckMark`, the same mark as Checkbox).
37
+ */
35
38
  export declare function SwatchContent({ value, selected }: {
36
39
  value: string | null;
37
40
  selected: boolean;
@@ -7,14 +7,14 @@ export type AppShellRootProps = React.HTMLAttributes<HTMLDivElement> & {
7
7
  export type AppShellNavProps = React.HTMLAttributes<HTMLDivElement> & {
8
8
  ref?: React.Ref<HTMLDivElement>;
9
9
  };
10
- /** Navigation column (usually `Sidebar.Root`); sits on the canvas. */
10
+ /** Navigation column (usually `Sidebar.Root`, a surface one layer above the page). */
11
11
  declare function AppShellNav({ className, ...rest }: AppShellNavProps): import("react/jsx-runtime").JSX.Element;
12
12
  declare namespace AppShellNav {
13
13
  var displayName: string;
14
14
  }
15
15
  /**
16
16
  * Grid: navigation column | content panel. Everything that is not `AppShell.Nav` goes into the
17
- * content panel (`bg-surface`, inset from the window on wide screens).
17
+ * content panel, which is the page itself (the layer around the shell): cards on it are the next layer.
18
18
  */
19
19
  declare function AppShellRoot({ fillViewport, className, children, ...rest }: AppShellRootProps): import("react/jsx-runtime").JSX.Element;
20
20
  declare namespace AppShellRoot {
@@ -0,0 +1,18 @@
1
+ export type CelebrateOptions = {
2
+ /**
3
+ * Where the burst starts: an element (from its center) or a point in the viewport. Default: the
4
+ * middle of the screen, a third from the top.
5
+ */
6
+ origin?: Element | {
7
+ x: number;
8
+ y: number;
9
+ };
10
+ };
11
+ /**
12
+ * A short confetti burst for a rare milestone: a quarter closed, the first invoice paid, an
13
+ * onboarding finished (foundation §7 rule 1 — only rare moments carry delight; never for routine
14
+ * success, that is a Notification). Draws on a canvas above every layer, ignores the pointer and
15
+ * assistive tech, and removes itself after 1.4 s. Under reduced motion it draws nothing. Say the
16
+ * achievement in words too: the burst is decoration. Resolves when the canvas is gone.
17
+ */
18
+ export declare function celebrate(options?: CelebrateOptions): Promise<void>;
@@ -0,0 +1,37 @@
1
+ /** ΔL in OKLab between neighbouring layers. */
2
+ export declare const LAYER_STEP = 0.03;
3
+ /** Layers 0…4: the page and four nested surfaces. */
4
+ export declare const LAYER_DEPTH = 4;
5
+ export type LayerTheme = {
6
+ /** The page color (layer 0). */
7
+ page: string;
8
+ /** Lightness window: no color goes outside it. */
9
+ lo: number;
10
+ hi: number;
11
+ /** Direction of control fills from their host: −1 darker, +1 lighter. */
12
+ control: 1 | -1;
13
+ };
14
+ export declare const LIGHT_LAYERS: LayerTheme;
15
+ export declare const DARK_LAYERS: LayerTheme;
16
+ export declare const lightnessOf: (hex: string) => number;
17
+ /**
18
+ * `steps` steps from L towards `dir`. Reflects when less than half a step of room is left (unless
19
+ * `reflect` is off), snaps to the edge when less than half a step would remain after the move.
20
+ */
21
+ export declare function move(theme: LayerTheme, L: number, dir: 1 | -1, steps?: number, reflect?: boolean): number;
22
+ export type LayerColors = {
23
+ /** The layer itself. */
24
+ bg: string;
25
+ /** Fields, chips, segmented tracks, neutral buttons on this layer. */
26
+ fill: string;
27
+ /** Hover of that fill. */
28
+ fillHover: string;
29
+ /** The selected segment (thumb) on a track of this layer. */
30
+ selected: string;
31
+ };
32
+ /** Layers 0…LAYER_DEPTH of a theme, each one step from its parent. */
33
+ export declare function layerLadder(theme: LayerTheme): LayerColors[];
34
+ /** Floating layers (popovers, menus, modals, drawers) sit one step above a card and never reflect. */
35
+ export declare function floatingLayer(theme: LayerTheme): LayerColors;
36
+ /** Token tree for `color.layer`: `0`…`4` plus `floating` and the two layers nested in it. */
37
+ export declare function layerTokens(theme: LayerTheme): Record<string, LayerColors>;
@@ -2,5 +2,5 @@
2
2
  export declare const REF_PREFIX = "--prime-ref";
3
3
  /** Semantic variables: `--prime-*`. */
4
4
  export declare const SYS_PREFIX = "--prime";
5
- /** Token path → CSS variable: `color.bg.canvas` → `--prime-color-bg-canvas`, camelCase → kebab-case. */
5
+ /** Token path → CSS variable: `color.text.primary` → `--prime-color-text-primary`, camelCase → kebab-case. */
6
6
  export declare function toVarName(path: string, prefix?: string): string;
@@ -1,25 +1,13 @@
1
- /**
2
- * Semantic layer (`--prime-*`). Components read only these variables.
3
- * Path → variable: `color.bg.canvas` → `--prime-color-bg-canvas`, camelCase → kebab-case.
4
- * Values below are the light theme; `themes/dark.ts` overrides colors and shadows.
5
- *
6
- * Design rules (see docs/foundation.md):
7
- * - Depth comes from fills, not borders. `border.subtle` is a hairline, `border.control` is transparent
8
- * (it becomes visible only under `prefers-contrast: more`).
9
- * - Every size is on the 4px grid. Controls share one size axis: xs 28 · s 32 · m 36 · l 40 · xl 48.
10
- * - A nested radius equals the outer radius minus the padding between them.
11
- */
12
1
  export declare const semanticTokens: {
13
2
  readonly color: {
3
+ /**
4
+ * Surface ladder, computed by `layers.ts`: `0` the page … `4` the deepest nested layer, `floating`
5
+ * for menus, popovers, modals and drawers (`floating1`/`floating2` nest inside it). Each row:
6
+ * `bg` the layer, `fill` controls on it, `fillHover`, `selected` the thumb on its track.
7
+ * Components read them through the depth context variables (globals.css), not directly.
8
+ */
9
+ readonly layer: Record<string, import("./layers").LayerColors>;
14
10
  readonly bg: {
15
- /** App background. */
16
- readonly canvas: "{color.gray.50}";
17
- /** Cards, panels, table bodies. */
18
- readonly surface: "{color.gray.0}";
19
- /** Floating layers: menus, popovers, modals, drawers. */
20
- readonly raised: "{color.gray.0}";
21
- /** Inset areas inside a surface: modal footer, table head, code. */
22
- readonly sunken: "{color.gray.75}";
23
11
  readonly inverse: "{color.gray.925}";
24
12
  readonly scrim: "rgba(17, 19, 24, 0.44)";
25
13
  /** Soft shade over an edge that hides scrolled content (DataTable columns). */
@@ -32,22 +20,18 @@ export declare const semanticTokens: {
32
20
  /** The specular top edge of a glass surface: the line that makes it read as glass. */
33
21
  readonly glassEdge: "rgba(255, 255, 255, 0.85)";
34
22
  };
23
+ /**
24
+ * Translucent state washes: they sit on any background, including ones off the ladder (an
25
+ * image, a tinted banner). Resting fills of controls come from the ladder instead
26
+ * (`--prime-color-fill-muted`, `--prime-color-field-bg`).
27
+ */
35
28
  readonly fill: {
36
- /** Transparent wash: ghost hover, row hover. Works on any background. */
29
+ /** Ghost hover, row hover. */
37
30
  readonly subtle: "rgba(17, 19, 24, 0.04)";
38
31
  readonly subtleActive: "rgba(17, 19, 24, 0.07)";
39
- /**
40
- * The faintest wash: hover of a tab in the sunken strip (Tabs). Stays below the
41
- * sunken → surface step in both themes, so a hovered tab never reads as the selected one.
42
- */
32
+ /** The faintest wash: hover of a tab in the tab strip, below the selected tab's step. */
43
33
  readonly faint: "rgba(17, 19, 24, 0.04)";
44
- /**
45
- * Neutral buttons, chips, segmented track. Translucent ink wash: always one step off whatever
46
- * it sits on (canvas, card, sunken tile, a custom container), in both themes.
47
- */
48
- readonly muted: "color-mix(in srgb, var(--prime-ref-color-gray-925) 7%, transparent)";
49
- readonly mutedHover: "color-mix(in srgb, var(--prime-ref-color-gray-925) 10%, transparent)";
50
- /** Unchecked checkbox, switch track, slider track. Same wash, stronger. */
34
+ /** Unchecked checkbox, switch track, slider track. */
51
35
  readonly strong: "color-mix(in srgb, var(--prime-ref-color-gray-925) 15%, transparent)";
52
36
  readonly strongHover: "color-mix(in srgb, var(--prime-ref-color-gray-925) 22%, transparent)";
53
37
  };
@@ -105,39 +89,29 @@ export declare const semanticTokens: {
105
89
  readonly soft: "{color.sky.100}";
106
90
  readonly text: "{color.sky.800}";
107
91
  };
108
- /**
109
- * Field fill depends on what it sits on. `bg` is the canvas value; every surface component
110
- * (Card, Modal, Drawer, Popover, Sidebar…) sets `--prime-color-field-bg: var(--prime-color-field-bg-surface)`.
111
- */
92
+ /** The field fill at rest, hover and focus comes from the ladder (`--prime-color-field-bg*`). */
112
93
  readonly field: {
113
- /** Translucent wash, visible on any background; `bgSurface` kept equal for surface contexts. */
114
- readonly bg: "color-mix(in srgb, var(--prime-ref-color-gray-925) 6%, transparent)";
115
- readonly bgSurface: "color-mix(in srgb, var(--prime-ref-color-gray-925) 6%, transparent)";
116
- /** Hover wash of a field: the `bg` wash, twice as dense (≈ `bg` mixed 94% with the text color). */
117
- readonly bgHover: "color-mix(in srgb, var(--prime-ref-color-gray-925) 12%, transparent)";
118
- readonly bgFocus: "{color.gray.0}";
94
+ /** A disabled field: a faint wash on whatever it sits on. */
119
95
  readonly bgDisabled: "color-mix(in srgb, var(--prime-ref-color-gray-925) 4%, transparent)";
120
96
  };
121
- /**
122
- * Card fill depends on what it sits on (same idea as `field`). On the canvas a card is white and raised;
123
- * every surface plane (AppShell content, Card, Modal, Drawer, Popover) sets
124
- * `--prime-color-card-bg: var(--prime-color-bg-sunken)`, and a card on a sunken fill drops its shadow.
125
- */
126
- readonly card: {
127
- readonly bg: "{color.bg.surface}";
128
- };
129
97
  readonly focus: {
130
98
  readonly ring: "{color.cobalt.700}";
131
99
  };
132
100
  readonly control: {
133
101
  /** Switch thumb, slider thumb. */
134
102
  readonly thumb: "{color.white}";
135
- /** Selected segment in a segmented track: one step lighter than the track. */
136
- readonly selected: "{color.gray.0}";
137
103
  };
104
+ /**
105
+ * The tooltip is an inverse chip: everything inside it (a Kbd, a secondary line) reads its
106
+ * colors from here, through the context the chip sets (`--prime-color-text-*`, fills).
107
+ */
138
108
  readonly tooltip: {
139
109
  readonly bg: "{color.gray.925}";
140
110
  readonly text: "{color.gray.0}";
111
+ /** Secondary text on the chip (a shortcut, a second line). */
112
+ readonly muted: "{color.gray.300}";
113
+ /** A key cap or chip on the chip: a light wash over the dark fill. */
114
+ readonly fill: "rgba(255, 255, 255, 0.14)";
141
115
  };
142
116
  readonly palette: Record<string, {
143
117
  soft: string;
@@ -326,6 +300,8 @@ export declare const semanticTokens: {
326
300
  };
327
301
  /** Step between items of a staggered first render. */
328
302
  readonly stagger: "{duration.80}";
303
+ /** Blur that hides a change of shape while two states swap in place (icon → check, letters). */
304
+ readonly blur: "2px";
329
305
  /** `:active` scale of pressable controls; `compact` for icon-only and small targets. */
330
306
  readonly press: {
331
307
  readonly scale: "0.98";
@@ -561,8 +537,6 @@ export declare const semanticTokens: {
561
537
  readonly minWidth: "12rem";
562
538
  };
563
539
  readonly card: {
564
- /** Context: surfaces set it to `none` together with `--prime-color-card-bg` (see color.card). */
565
- readonly shadow: "{shadow.raised}";
566
540
  readonly radius: "{radius.12}";
567
541
  readonly paddingS: "{space.4}";
568
542
  readonly paddingM: "{space.5}";
@@ -625,10 +599,46 @@ export declare const semanticTokens: {
625
599
  readonly floatingInset: "{space.3}";
626
600
  readonly blur: "0.5rem";
627
601
  };
602
+ /**
603
+ * Tooltip tiers: a chip of tier T is as tall as a menu item of tier T (24 · 28 · 32 · 36 · 40),
604
+ * and text, padding and radius grow together, one step per tier.
605
+ */
628
606
  readonly tooltip: {
629
- readonly radius: "{radius.6}";
630
- readonly paddingX: "{space.2}";
631
- readonly paddingY: "{space.1}";
607
+ readonly xs: {
608
+ readonly textSize: "{font.size.12}";
609
+ readonly lineHeight: "{font.lineHeight.16}";
610
+ readonly paddingX: "0.375rem";
611
+ readonly paddingY: "{space.1}";
612
+ readonly radius: "{radius.4}";
613
+ };
614
+ readonly s: {
615
+ readonly textSize: "{font.size.12}";
616
+ readonly lineHeight: "{font.lineHeight.16}";
617
+ readonly paddingX: "{space.2}";
618
+ readonly paddingY: "0.375rem";
619
+ readonly radius: "{radius.6}";
620
+ };
621
+ readonly m: {
622
+ readonly textSize: "{font.size.13}";
623
+ readonly lineHeight: "{font.lineHeight.20}";
624
+ readonly paddingX: "0.625rem";
625
+ readonly paddingY: "0.375rem";
626
+ readonly radius: "{radius.6}";
627
+ };
628
+ readonly l: {
629
+ readonly textSize: "{font.size.14}";
630
+ readonly lineHeight: "{font.lineHeight.20}";
631
+ readonly paddingX: "{space.3}";
632
+ readonly paddingY: "{space.2}";
633
+ readonly radius: "{radius.8}";
634
+ };
635
+ readonly xl: {
636
+ readonly textSize: "{font.size.16}";
637
+ readonly lineHeight: "{font.lineHeight.24}";
638
+ readonly paddingX: "{space.4}";
639
+ readonly paddingY: "{space.2}";
640
+ readonly radius: "{radius.8}";
641
+ };
632
642
  readonly maxWidth: "17.5rem";
633
643
  /** Arrow base and depth; the arrow points at the trigger's centre. */
634
644
  readonly arrowWidth: "0.625rem";
@@ -1,11 +1,7 @@
1
1
  export declare const darkThemeOverrides: {
2
2
  readonly color: {
3
+ readonly layer: Record<string, import("../layers").LayerColors>;
3
4
  readonly bg: {
4
- readonly canvas: "{color.gray.950}";
5
- readonly surface: "{color.gray.900}";
6
- readonly raised: "{color.gray.875}";
7
- /** = canvas: nested tiles on the 900 surface need a clear step (925 read as 1.03:1). */
8
- readonly sunken: "{color.gray.950}";
9
5
  readonly inverse: "{color.gray.100}";
10
6
  readonly scrim: "rgba(0, 0, 0, 0.6)";
11
7
  /** Darker than the scrim's share: a shade on a 900 surface needs more ink to read. */
@@ -17,8 +13,6 @@ export declare const darkThemeOverrides: {
17
13
  readonly subtle: "rgba(233, 235, 240, 0.05)";
18
14
  readonly subtleActive: "rgba(233, 235, 240, 0.09)";
19
15
  readonly faint: "rgba(233, 235, 240, 0.02)";
20
- readonly muted: "color-mix(in srgb, var(--prime-ref-color-gray-100) 7%, transparent)";
21
- readonly mutedHover: "color-mix(in srgb, var(--prime-ref-color-gray-100) 10%, transparent)";
22
16
  readonly strong: "color-mix(in srgb, var(--prime-ref-color-gray-100) 16%, transparent)";
23
17
  readonly strongHover: "color-mix(in srgb, var(--prime-ref-color-gray-100) 22%, transparent)";
24
18
  };
@@ -72,10 +66,6 @@ export declare const darkThemeOverrides: {
72
66
  readonly text: "{color.sky.300}";
73
67
  };
74
68
  readonly field: {
75
- readonly bg: "color-mix(in srgb, var(--prime-ref-color-gray-100) 6%, transparent)";
76
- readonly bgSurface: "color-mix(in srgb, var(--prime-ref-color-gray-100) 6%, transparent)";
77
- readonly bgHover: "color-mix(in srgb, var(--prime-ref-color-gray-100) 12%, transparent)";
78
- readonly bgFocus: "{color.gray.925}";
79
69
  readonly bgDisabled: "color-mix(in srgb, var(--prime-ref-color-gray-100) 4%, transparent)";
80
70
  };
81
71
  readonly focus: {
@@ -83,11 +73,13 @@ export declare const darkThemeOverrides: {
83
73
  };
84
74
  readonly control: {
85
75
  readonly thumb: "{color.gray.100}";
86
- readonly selected: "{color.gray.750}";
87
76
  };
77
+ /** A light chip over the dark UI: inverse like in light, so it reads over any layer. */
88
78
  readonly tooltip: {
89
- readonly bg: "{color.gray.750}";
90
- readonly text: "{color.gray.100}";
79
+ readonly bg: "{color.gray.100}";
80
+ readonly text: "{color.gray.925}";
81
+ readonly muted: "{color.gray.600}";
82
+ readonly fill: "rgba(17, 19, 24, 0.08)";
91
83
  };
92
84
  readonly palette: Record<string, {
93
85
  soft: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prime-ui-kit",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "packageManager": "bun@1.3.13",
5
5
  "description": "React 19 UI kit for product interfaces: 61 composable components on one design contract — forms, selects, tables, overlays, navigation, app shell. CSS Modules, --prime-* tokens, light and dark themes, TypeScript, a11y. Ships COMPONENT.md docs and copyable examples.",
6
6
  "main": "./dist/index.js",
@@ -96,8 +96,8 @@ Accordion.Root frame (grouped) or column of cards (separate)
96
96
  ### layout
97
97
  | Value | Looks like | Use when | Default |
98
98
  |---|---|---|---|
99
- | `grouped` | One `card-bg` surface with card radius, `border-subtle` hairlines between items, no shadow of its own | FAQ, short settings lists | yes |
100
- | `separate` | Each item is a separate card (`card-bg`, card radius, `card-shadow`) with `--prime-space-2` between them | Heavier sections, checkout steps | |
99
+ | `grouped` | One surface — a layer of the ladder (`layer-current`) — with card radius, `border-subtle` hairlines between items, no shadow of its own | FAQ, short settings lists | yes |
100
+ | `separate` | Each item is a separate card (`layer-current`, card radius, the raised whisper on the page) with `--prime-space-2` between them | Heavier sections, checkout steps | |
101
101
 
102
102
  ### size
103
103
  | Value | Looks like | Use when | Default |
@@ -122,7 +122,7 @@ Accordion.Root frame (grouped) or column of cards (separate)
122
122
  |---|---|---|
123
123
  | open / closed | `value` / `defaultValue` + `onValueChange` | `data-state="open" \| "closed"` on Item, Trigger and Content; `aria-expanded` on Trigger; `inert` on a closed Content |
124
124
  | disabled | `disabled` on Item | `data-disabled` on Item and Trigger, native `disabled`, `text-disabled`, `cursor: not-allowed` |
125
- | hover / active | pointer | trigger `fill-subtle` / `fill-subtle-active` |
125
+ | hover / active | pointer | trigger `fill-muted-hover` (two ladder steps off the accordion's layer, so it never lands on the host's color at the edges) / `fill-strong` |
126
126
  | focus-visible | keyboard | inset focus ring inside the trigger |
127
127
 
128
128
  Root: `data-size`, `data-layout`. The panel height transitions through `grid-template-rows: 0fr → 1fr` (no JS measuring, follows content that resizes while open) and the body fades in while settling by `--prime-space-1`; open uses `base` + `enter`, close `fast` + `exit`. The chevron rotates over `base`. Instant under `prefers-reduced-motion`.
@@ -130,7 +130,7 @@ Root: `data-size`, `data-layout`. The panel height transitions through `grid-tem
130
130
  ## Layout & spacing
131
131
  - Takes the full width of its parent (`width: 100%`); the parent sets the measure.
132
132
  - Panel content is a flex column with `--prime-space-3` gap; it reserves `--prime-focus-space` at the top so focus rings of fields inside are not clipped.
133
- - Fields inside panels get the surface field fill automatically.
133
+ - The root is a layer of the surface ladder (`data-depth`): fields inside panels take that layer's fill automatically.
134
134
  - Stack several accordions or an accordion and other blocks with the page gap (`--prime-space-8` between groups).
135
135
 
136
136
  ## Accessibility
@@ -156,7 +156,7 @@ No keyboard interaction.
156
156
  - `alt="Анна Климова"` while the name is printed next to it → `alt=""`.
157
157
  - A random `color` on every render → derive it from the user id.
158
158
  - A custom presence dot → use `Avatar.Status`.
159
- - A group on a custom filled block with a canvas-colored ring → set `--avatar-ring` to that fill.
159
+ - A group on a custom filled block with a ring in the wrong color → set `--avatar-ring` to that fill (by default the ring is the current layer, `--prime-color-layer-current`).
160
160
  - `size` on every avatar in a group → set it once on `Avatar.Group`.
161
161
 
162
162
  ## Related
@@ -7,7 +7,9 @@
7
7
 
8
8
  ## When to use
9
9
  - Primary, secondary and destructive actions in forms, toolbars, dialogs, cards and empty states.
10
- - Async actions: `loading` shows a spinner, blocks clicks and keeps the width.
10
+ - Async actions: `loading` shows a spinner, blocks clicks and keeps the width; when it ends, a new label («Сохранено») flows in.
11
+ - Long actions with known progress (downloads, exports): `progress` fills the button itself.
12
+ - Destructive actions without a dialog: `holdToConfirm` needs a held press.
11
13
  - Compact toolbars: icon-only buttons (only `Button.Icon` inside) are square.
12
14
  - Action-styled navigation: `asChild` with an `<a>` or a router link.
13
15
 
@@ -26,8 +28,11 @@ import { Button } from "prime-ui-kit";
26
28
  ## Anatomy
27
29
  ```
28
30
  Button.Root <button> (or the single child with asChild); variant, tone, size; size tier for nested icons
31
+ ├─ (fill) progress / hold wash under the label, rendered by Root once used, aria-hidden
29
32
  ├─ (Spinner) rendered by Root while loading, aria-hidden
33
+ ├─ (label) text children; a changed label flows in letter by letter
30
34
  └─ Button.Icon decorative icon wrapper (aria-hidden), sized to the tier
35
+ (hold hint) visually hidden description beside the button with holdToConfirm
31
36
  ```
32
37
 
33
38
  ## API
@@ -44,10 +49,14 @@ Button.Root <button> (or the single child with asChild); variant, tone, s
44
49
  | `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | `host tier, else "m"` | Control tier: height 28 · 32 · 36 · 40 · 48, padding, text, icon, radius. Without it the button takes the tier of its host (LoginForm, Popover, a field, a panel with a size), else `m`. |
45
50
  | `fullWidth` | `boolean` | — | Stretches to the container width. |
46
51
  | `loading` | `boolean` | `false` | Shows a `Spinner` in place of the leading icon or over the label, sets `aria-busy`, blocks clicks; width does not change. With `asChild` no spinner is added — the child owns its content. |
52
+ | `progress` | `number` | — | Progress of a long action started by the button, `0…1`: a wash of the text color grows inside from the start edge, `aria-busy` is set and the button stays pressable (to cancel). Put the number in the label («Скачивание 42%»); remove the prop when done and the fill fades out. |
53
+ | `holdToConfirm` | `boolean` | `false` | The action needs a held press: the fill runs for 1.2 s (a gesture clock, kept under reduced motion) and `onConfirm` fires at its end. Releasing, leaving or losing focus earlier rolls it back. Space and Enter hold too; the gesture is described to assistive tech by `labels.holdHint`. |
54
+ | `onConfirm` | `() => void` | — | Fires when a `holdToConfirm` press completes; the action goes here, not in `onClick`. |
55
+ | `labels` | `Partial<ButtonLabels>` | — | Built-in strings, see Labels. |
47
56
  | `asChild` | `boolean` | `false` | Merges Button props and styles onto the single child element instead of rendering `<button>`. `disabled`/`loading` become `aria-disabled`. |
48
57
  | `type` | `"button" \| "submit" \| "reset"` | `"button"` | Native button type; not forwarded with `asChild`. |
49
58
  | `disabled` | `boolean` | — | Disabled state; `loading` also disables. |
50
- | `children` | `ReactNode` | — | Label and `Button.Icon`. Only `Button.Icon` children → square icon-only button; give it `aria-label`. |
59
+ | `children` | `ReactNode` | — | Label and `Button.Icon`. Only `Button.Icon` children → square icon-only button; give it `aria-label`. A changed text label flows into the new one letter by letter while the width glides; a change of digits only («58 с» → «57 с», «42%» → «43%») stays in place. |
51
60
  | `…rest` | `Omit<ButtonHTMLAttributes<HTMLButtonElement>, "size">` | — | `onClick`, `className`, `aria-*`, `data-*` and the other button attributes. |
52
61
 
53
62
  ### Button.Icon
@@ -109,6 +118,9 @@ A Button lines up exactly with Input, Select, Datepicker trigger, SegmentedContr
109
118
  | focus-visible | keyboard | outer focus ring with `--prime-focus-offset`; `tone="inherit"` draws it in `currentColor` |
110
119
  | disabled | `disabled` | native `disabled`, `data-disabled="true"`, `fill-muted` + `text-disabled`, `cursor: not-allowed`; ghost stays transparent |
111
120
  | loading | `loading` (native `<button>`) | `data-loading="true"`, native `disabled`, `aria-busy="true"`, colors kept (busy, not unavailable), no hover or press; a `Spinner` (`aria-hidden`) replaces the leading (or only) icon, otherwise it is centered over the hidden label (`data-loading-overlay="true"`) |
121
+ | progress | `progress` (0…1) | `data-progress="true"`, `aria-busy="true"`, still pressable; a plate of the text color with the button's radius slides in from the start edge (`fast` · standard), so corners match at any value; removing the prop fades it where it is |
122
+ | holding | `holdToConfirm` + a held pointer, `Space` or `Enter` | `data-hold="holding"`: the fill runs linearly for 1.2 s (kept under reduced motion — it is a gesture clock), then `onConfirm` and `data-hold="done"` (on release the fill fades in place, `data-hold="idle"`); releasing earlier → `data-hold="cancel"`, the fill rolls back (`fast` · exit) |
123
+ | label change | new text children | the old letters leave upward, the new ones rise from below one short step apart (`base` · emphasized, blur `--prime-motion-blur`) while the width glides; digits-only changes and reduced motion change in place |
112
124
  | asChild disabled / loading | `asChild` + `disabled`/`loading` | `aria-disabled="true"`, `pointer-events: none`, click `preventDefault`; no native `disabled`, no automatic spinner |
113
125
 
114
126
  Other data attributes: `data-variant`, `data-tone`, `data-size`, `data-full-width`, `data-icon-only`, `data-leading-icon`, `data-trailing-icon`.
@@ -123,19 +135,23 @@ Other data attributes: `data-variant`, `data-tone`, `data-size`, `data-full-widt
123
135
  ### Keyboard
124
136
  | Key | Action |
125
137
  |---|---|
126
- | `Enter` · `Space` | Presses the button (native `<button>`). |
138
+ | `Enter` · `Space` | Presses the button (native `<button>`); with `holdToConfirm` the key is held until the fill completes. |
127
139
  | `Tab` | Moves focus; a disabled button is skipped, with `asChild` it stays focusable with `aria-disabled`. |
128
140
 
129
141
  ### ARIA
130
142
  - Native `<button>`, `type="button"` by default, so it never submits a form by accident.
131
143
  - Icon-only buttons need `aria-label`; `Button.Icon` is `aria-hidden`.
132
- - `loading` sets `aria-busy="true"` and blocks the press.
144
+ - `loading` sets `aria-busy="true"` and blocks the press; `progress` sets `aria-busy` and keeps the button pressable.
145
+ - `holdToConfirm` describes the gesture through `aria-describedby` (`labels.holdHint`); `Space` / `Enter` hold like a pointer.
146
+ - A changed label is announced once: the animated letters are hidden, the name is the new text.
133
147
  - With `asChild` the disabled state is `aria-disabled="true"` without a native `disabled`.
134
148
 
135
149
  ### Labels
136
150
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
137
151
 
138
- No `labels`.
152
+ | Key | Default | Used for |
153
+ |---|---|---|
154
+ | `holdHint` | `"Удерживайте, чтобы подтвердить"` | Description of a `holdToConfirm` button for assistive tech (`aria-describedby`). |
139
155
 
140
156
  ## Examples
141
157
  | Example | Shows |
@@ -145,6 +161,9 @@ No `labels`.
145
161
  | [sizes.tsx](examples/sizes.tsx) | Every size tier, 28 to 48 px high — `size`. |
146
162
  | [states.tsx](examples/states.tsx) | Disabled and loading next to the default; the spinner keeps the width — `disabled`, `loading`. |
147
163
  | [with-icon.tsx](examples/with-icon.tsx) | An icon before or after the label, and a square icon-only button — `Button.Icon`, `aria-label`. |
164
+ | [download.tsx](examples/download.tsx) | A long download inside the button: the fill shows how far it got, the label counts and then offers the file — `progress`. |
165
+ | [hold-to-confirm.tsx](examples/hold-to-confirm.tsx) | A destructive action that needs a held press; letting go early rolls the fill back and does nothing — `holdToConfirm`, `onConfirm`. |
166
+ | [label-morph.tsx](examples/label-morph.tsx) | A new label flows into the button letter by letter while the width glides: save, then saved; a step flow — `children`, `loading`. |
148
167
  | [on-colored-host.tsx](examples/on-colored-host.tsx) | Actions on a colored strip take its text color: a ghost icon close and a soft action — `tone="inherit"`. |
149
168
  | [as-child.tsx](examples/as-child.tsx) | The button look on a real link; a disabled link blocks navigation — `asChild`, `disabled`. |
150
169
  | [in-form.tsx](examples/in-form.tsx) | A full-width submit button that shows the request in progress — `type`, `loading`, `fullWidth`. |
@@ -156,6 +175,8 @@ No `labels`.
156
175
  - Putting a `<Spinner />` inside a loading button → just set `loading`; the button places it.
157
176
  - `<a>` inside `<Button.Root>` → use `asChild` so there is one interactive element.
158
177
  - Submit button without `type="submit"` → the default is `"button"` and will not submit.
178
+ - A destructive `holdToConfirm` action in `onClick` → it fires on any click; put it in `onConfirm`.
179
+ - Rebuilding a progress label with a new word every tick («Загрузка…» / «Скачивание…») → keep the words, change only the number; digits update in place, words morph.
159
180
 
160
181
  ## Related
161
182
  - **Built from:** [Spinner](../spinner/COMPONENT.md)