@devalok/shilp-sutra 0.59.0 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +72 -0
- package/dist/composed/avatar-group.js +3 -3
- package/dist/composed/avatar-group.js.map +1 -1
- package/dist/composed/bulk-action-bar.d.ts +9 -35
- package/dist/composed/bulk-action-bar.d.ts.map +1 -1
- package/dist/composed/bulk-action-bar.js +2 -139
- package/dist/composed/index.js +6 -5
- package/dist/composed/master-detail.d.ts.map +1 -1
- package/dist/composed/master-detail.js +1 -1
- package/dist/composed/master-detail.js.map +1 -1
- package/dist/composed/multi-select-popover.d.ts.map +1 -1
- package/dist/composed/multi-select-popover.js +1 -1
- package/dist/composed/multi-select-popover.js.map +1 -1
- package/dist/tokens/semantic.css +85 -3
- package/dist/ui/avatar.d.ts.map +1 -1
- package/dist/ui/avatar.js +3 -3
- package/dist/ui/avatar.js.map +1 -1
- package/dist/ui/bulk-action-bar.d.ts +59 -0
- package/dist/ui/bulk-action-bar.d.ts.map +1 -0
- package/dist/ui/bulk-action-bar.js +145 -0
- package/dist/ui/bulk-action-bar.js.map +1 -0
- package/dist/ui/checkbox.d.ts.map +1 -1
- package/dist/ui/checkbox.js +1 -1
- package/dist/ui/checkbox.js.map +1 -1
- package/dist/ui/combobox.d.ts.map +1 -1
- package/dist/ui/combobox.js +10 -10
- package/dist/ui/combobox.js.map +1 -1
- package/dist/ui/data-table-body.d.ts.map +1 -1
- package/dist/ui/data-table-body.js +2 -1
- package/dist/ui/data-table-body.js.map +1 -1
- package/dist/ui/data-table-bulk-actions.d.ts +26 -1
- package/dist/ui/data-table-bulk-actions.d.ts.map +1 -1
- package/dist/ui/data-table-bulk-actions.js +21 -46
- package/dist/ui/data-table-bulk-actions.js.map +1 -1
- package/dist/ui/data-table-context.d.ts +36 -2
- package/dist/ui/data-table-context.d.ts.map +1 -1
- package/dist/ui/data-table-context.js +25 -21
- package/dist/ui/data-table-context.js.map +1 -1
- package/dist/ui/data-table-header.d.ts.map +1 -1
- package/dist/ui/data-table-header.js +2 -1
- package/dist/ui/data-table-header.js.map +1 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +58 -57
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/input.d.ts.map +1 -1
- package/dist/ui/input.js +6 -6
- package/dist/ui/input.js.map +1 -1
- package/dist/ui/radio.d.ts.map +1 -1
- package/dist/ui/radio.js +4 -4
- package/dist/ui/radio.js.map +1 -1
- package/dist/ui/select.d.ts.map +1 -1
- package/dist/ui/select.js +6 -6
- package/dist/ui/select.js.map +1 -1
- package/dist/ui/switch.d.ts.map +1 -1
- package/dist/ui/switch.js +2 -2
- package/dist/ui/switch.js.map +1 -1
- package/dist/ui/table.d.ts.map +1 -1
- package/dist/ui/table.js +1 -1
- package/dist/ui/table.js.map +1 -1
- package/dist/ui/textarea.d.ts.map +1 -1
- package/dist/ui/textarea.js +6 -6
- package/dist/ui/textarea.js.map +1 -1
- package/dist/ui/toast.d.ts.map +1 -1
- package/dist/ui/toast.js +1 -1
- package/dist/ui/toast.js.map +1 -1
- package/docs/components/composed/bulk-action-bar.md +14 -7
- package/docs/components/ui/bulk-action-bar.md +72 -0
- package/llms.txt +2 -2
- package/make-kit/foundations/color.md +3 -1
- package/mcp-manifest.json +178 -150
- package/package.json +6 -1
- package/skill/SKILL.md +1 -1
- package/skill/references/components.md +2 -2
- package/dist/composed/bulk-action-bar.js.map +0 -1
package/dist/tokens/semantic.css
CHANGED
|
@@ -211,6 +211,21 @@
|
|
|
211
211
|
--color-segment-track: color-mix(in oklch, var(--neutral-12) 6%, transparent);
|
|
212
212
|
--color-segment-thumb: var(--neutral-1);
|
|
213
213
|
|
|
214
|
+
/* Form-control ground (Input, Select, Textarea, Combobox). A ROLE token, not
|
|
215
|
+
a surface tier: the control is defined by its BORDER, so its fill sits
|
|
216
|
+
essentially flush with whatever it is dropped onto and the edge does the
|
|
217
|
+
work. Design 2026-08-24 moved light from neutral-2 to neutral-1.
|
|
218
|
+
|
|
219
|
+
The spec was authored light-only. Taking it literally in dark would be a
|
|
220
|
+
regression — neutral-1 in dark IS surface-base, so the field would vanish
|
|
221
|
+
into the page at 1.000:1 (the Table-striped / linear-Progress failure).
|
|
222
|
+
Dark therefore takes the step that preserves the INTENT, not the number.
|
|
223
|
+
Measured: light field #fcfcfc = 1.026:1 vs the page, hover lifts 1.063:1;
|
|
224
|
+
dark field #181818 = 1.000:1 vs a panel / 1.115:1 vs the page, hover lifts
|
|
225
|
+
1.173:1. Same relationship in both themes. */
|
|
226
|
+
--color-field: var(--neutral-1);
|
|
227
|
+
--color-field-hover: var(--neutral-2);
|
|
228
|
+
|
|
214
229
|
/* Overlay + disabled — promoted from internal :root into @theme so
|
|
215
230
|
`bg-overlay`, `text-overlay`, `bg-disabled`, `text-disabled` emit. */
|
|
216
231
|
--color-overlay: oklch(0 0 0 / 0.50);
|
|
@@ -228,6 +243,11 @@
|
|
|
228
243
|
--color-error-5: var(--red-5);
|
|
229
244
|
--color-error-6: var(--red-6);
|
|
230
245
|
--color-error-7: var(--red-7);
|
|
246
|
+
/* Step 8 exists so validation EDGES can clear WCAG 1.4.11. Step 7 measures
|
|
247
|
+
2.831:1 on a light field; step 8 is 3.929:1 (dark: 2.397:1 → 3.440:1).
|
|
248
|
+
Bridged 2026-08-24 — the primitive was always there, the semantic alias
|
|
249
|
+
was not, so `border-error-8` silently emitted nothing. */
|
|
250
|
+
--color-error-8: var(--red-8);
|
|
231
251
|
--color-error-9: var(--red-9);
|
|
232
252
|
--color-error-10: var(--red-10);
|
|
233
253
|
--color-error-11: var(--red-11);
|
|
@@ -240,6 +260,8 @@
|
|
|
240
260
|
--color-success-5: var(--green-5);
|
|
241
261
|
--color-success-6: var(--green-6);
|
|
242
262
|
--color-success-7: var(--green-7);
|
|
263
|
+
/* See --color-error-8. 2.557:1 → 3.441:1 light, 2.660:1 → 3.971:1 dark. */
|
|
264
|
+
--color-success-8: var(--green-8);
|
|
243
265
|
--color-success-9: var(--green-9);
|
|
244
266
|
--color-success-10: var(--green-10);
|
|
245
267
|
--color-success-11: var(--green-11);
|
|
@@ -526,7 +548,12 @@
|
|
|
526
548
|
/* Action state opacities (reflected by preset `opacity-action-*` util) */
|
|
527
549
|
--action-hover-opacity: 0.04;
|
|
528
550
|
--action-selected-opacity: 0.08;
|
|
529
|
-
|
|
551
|
+
/* 0.38 → 0.45 (design, 2026-08-24). Disabled text is exempt from WCAG 1.4.3,
|
|
552
|
+
so this is a legibility call, not a pass/fail one — and it moves the right
|
|
553
|
+
way on every ground. Measured over surface-panel, worst case first:
|
|
554
|
+
surface-fg-subtle 1.673:1 → 1.859:1, surface-fg-muted 1.879:1 → 2.169:1,
|
|
555
|
+
surface-fg 2.144:1 → 2.523:1 in light; surface-fg 3.046:1 → 3.745:1 in dark. */
|
|
556
|
+
--action-disabled-opacity: 0.45;
|
|
530
557
|
--action-focus-opacity: 0.12;
|
|
531
558
|
--action-active-opacity: 0.12;
|
|
532
559
|
|
|
@@ -540,7 +567,17 @@
|
|
|
540
567
|
the public `--shadow-raised`/`--shadow-floating` @theme values above.
|
|
541
568
|
They depend on `--shadow-color` (a runtime var) so they cannot live
|
|
542
569
|
in @theme (TW4 may statically-inline theme values). */
|
|
543
|
-
|
|
570
|
+
/* Light ink matches the Figma library's `#1c2533` exactly, so a designer
|
|
571
|
+
measuring a shadow in Figma and a developer measuring it in the browser
|
|
572
|
+
get the same answer. The two had drifted; Figma won on cost, not merit —
|
|
573
|
+
one value here against 26 in the library, for a difference measured at
|
|
574
|
+
**1/255** at the strongest light layer. Below what anyone can see.
|
|
575
|
+
DARK overrides this (see the .dark block) and must keep a darker ink:
|
|
576
|
+
dark multiplies every alpha by --shadow-strength 2.5, and at that weight
|
|
577
|
+
this lighter ink composites LIGHTER than the panel it sits on — a glow,
|
|
578
|
+
not a shadow. Measured before adopting; the light-mode reasoning does not
|
|
579
|
+
survive the multiplier. */
|
|
580
|
+
--shadow-color: 0.263 0.029 259; /* #1c2533 — cool blue tint, light */
|
|
544
581
|
--shadow-strength: 1;
|
|
545
582
|
--shadow-transition: box-shadow var(--duration-fast-02) var(--ease-productive-standard);
|
|
546
583
|
|
|
@@ -671,6 +708,12 @@
|
|
|
671
708
|
--color-surface-raised-hover: var(--color-surface-panel-hover);
|
|
672
709
|
--color-surface-raised-active: var(--color-surface-panel-active);
|
|
673
710
|
|
|
711
|
+
/* Form-control ground — one step up from the light values, NOT the same
|
|
712
|
+
neutral step. neutral-1 in dark is surface-base, so reusing the light
|
|
713
|
+
number would sink the field into the page. See the :root note. */
|
|
714
|
+
--color-field: var(--neutral-2);
|
|
715
|
+
--color-field-hover: var(--neutral-3);
|
|
716
|
+
|
|
674
717
|
/* Object edges invert to translucent white. */
|
|
675
718
|
--color-surface-border-subtle: oklch(1 0 0 / 0.06);
|
|
676
719
|
--color-surface-border: oklch(1 0 0 / 0.10);
|
|
@@ -697,7 +740,8 @@
|
|
|
697
740
|
/* Action state opacities (higher in dark mode) */
|
|
698
741
|
--action-hover-opacity: 0.08;
|
|
699
742
|
--action-selected-opacity: 0.12;
|
|
700
|
-
|
|
743
|
+
/* Tracks the light value — see the note on the :root declaration. */
|
|
744
|
+
--action-disabled-opacity: 0.45;
|
|
701
745
|
--action-focus-opacity: 0.12;
|
|
702
746
|
--action-active-opacity: 0.16;
|
|
703
747
|
|
|
@@ -708,6 +752,28 @@
|
|
|
708
752
|
/* Shadows (heavier in dark mode) — brand-reactive via color-mix on
|
|
709
753
|
--color-accent-9 so they follow consumer brand swaps. */
|
|
710
754
|
--shadow-strength: 2.5;
|
|
755
|
+
/* Dark needs its OWN, DARKER ink, and the reason is worth reading before
|
|
756
|
+
touching this line.
|
|
757
|
+
|
|
758
|
+
Light moved to #1c2533 to match the Figma library. Dark cannot follow:
|
|
759
|
+
--shadow-strength is 2.5 here, and at that weight #1c2533 composites to
|
|
760
|
+
#1b1d21 over a #1a1a1a panel — LIGHTER than the surface it is meant to
|
|
761
|
+
darken. A glow, not a shadow. Worst case 9/255 in the wrong direction.
|
|
762
|
+
|
|
763
|
+
The previous dark ink (oklch(0.15 …)) had a milder version of the same
|
|
764
|
+
fault: it reads as a shadow on `surface-panel` but GLOWS on
|
|
765
|
+
`surface-base` (#0a0a0a), where it is itself the lighter colour. Measured
|
|
766
|
+
separation there was 1.001:1 — the drop layers contributed literally
|
|
767
|
+
nothing. 0.05 is below both grounds, so the sign is now correct everywhere.
|
|
768
|
+
|
|
769
|
+
What this does NOT do is make drop shadows carry dark elevation, and no
|
|
770
|
+
ink can. There is no headroom below a near-black ground: the best any ink
|
|
771
|
+
achieves at the strongest layer is 1.013:1 on base and 1.040:1 on panel.
|
|
772
|
+
**The white edge ring is what actually separates an elevated surface in
|
|
773
|
+
dark, and that is structural rather than a workaround.** If dark elevation
|
|
774
|
+
ever needs to read more strongly, the answer is the ring or a lighter
|
|
775
|
+
surface step — not a darker shadow, which has nowhere left to go. */
|
|
776
|
+
--shadow-color: 0.05 0.012 260;
|
|
711
777
|
/* Light ring on dark — a dark-tinted ring would vanish against dark surfaces. */
|
|
712
778
|
--shadow-edge-ring: 0 0 0 1px oklch(1 0 0 / 0.12);
|
|
713
779
|
/* Same correction for the raised tier. Without this, `shadow-raised` /
|
|
@@ -715,6 +781,18 @@
|
|
|
715
781
|
ground, so anything using them as its only edge has none. 0.07 keeps a
|
|
716
782
|
raised card quieter than a floating overlay while still reading. */
|
|
717
783
|
--shadow-edge-ring-subtle: 0 0 0 1px oklch(1 0 0 / 0.07);
|
|
784
|
+
/* The third ring, and the one that was missed. `--shadow-pressed` is also a
|
|
785
|
+
1px ring built from `--shadow-color`, so in dark it composited to
|
|
786
|
+
**1.012:1** against `surface-panel` — a pressed control had no pressed
|
|
787
|
+
state at all. Its two siblings above were corrected; this was not, which is
|
|
788
|
+
the failure mode the comment on --shadow-edge-ring describes. White at 0.10
|
|
789
|
+
measures 2.640:1, between subtle (2.148:1) and the full edge ring
|
|
790
|
+
(2.968:1) — pressed should read more firmly than a resting card and less
|
|
791
|
+
than a floating overlay. Found while binding the Figma effect styles, where
|
|
792
|
+
it had been masked by a binding to the wrong variable.
|
|
793
|
+
All three rings stop deliberately short of 1.4.11's 3:1 —
|
|
794
|
+
deviation: SHADOW-RING-BELOW-NONTEXT */
|
|
795
|
+
--shadow-pressed: 0 0 0 1px oklch(1 0 0 / 0.10);
|
|
718
796
|
--shadow-kbd: inset 0 1px 0 oklch(1 0 0 / 0.10);
|
|
719
797
|
/* The segmented thumb is documented as deliberately ring-less, the shadow
|
|
720
798
|
being its whole edge. That works in light and cannot work in dark, where
|
|
@@ -859,6 +937,8 @@
|
|
|
859
937
|
--color-surface-disabled: Canvas;
|
|
860
938
|
--color-surface-inverted: CanvasText;
|
|
861
939
|
--color-surface-inverted-fg: Canvas;
|
|
940
|
+
--color-field: Canvas;
|
|
941
|
+
--color-field-hover: Canvas;
|
|
862
942
|
/* Forced colours flatten the object/control split — every edge must be a
|
|
863
943
|
system colour or it disappears entirely. */
|
|
864
944
|
--color-surface-border: CanvasText;
|
|
@@ -898,6 +978,7 @@
|
|
|
898
978
|
--color-error-4: Canvas;
|
|
899
979
|
--color-error-5: Canvas;
|
|
900
980
|
--color-error-7: CanvasText;
|
|
981
|
+
--color-error-8: CanvasText;
|
|
901
982
|
--color-error-9: Mark;
|
|
902
983
|
--color-error-10: Mark;
|
|
903
984
|
--color-error-11: CanvasText;
|
|
@@ -907,6 +988,7 @@
|
|
|
907
988
|
--color-success-4: Canvas;
|
|
908
989
|
--color-success-5: Canvas;
|
|
909
990
|
--color-success-7: CanvasText;
|
|
991
|
+
--color-success-8: CanvasText;
|
|
910
992
|
--color-success-9: Highlight;
|
|
911
993
|
--color-success-10: Highlight;
|
|
912
994
|
--color-success-11: CanvasText;
|
package/dist/ui/avatar.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../src/ui/avatar.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,eAAe,MAAM,0BAA0B,CAAA;AAC3D,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEjE,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAc9B,eAAO,MAAM,cAAc;;;8EAmB1B,CAAA;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAA;AAEjE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../src/ui/avatar.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,eAAe,MAAM,0BAA0B,CAAA;AAC3D,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEjE,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAc9B,eAAO,MAAM,cAAc;;;8EAmB1B,CAAA;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAA;AAEjE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;AAgF7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,MAAM,WAAW,WACf,SAAQ,IAAI,CAAC,KAAK,CAAC,wBAAwB,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,EACnF,YAAY,CAAC,OAAO,cAAc,CAAC;IACrC,8EAA8E;IAC9E,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,gCAAgC;IAChC,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC,SAAS,CAAA;IACxC,+CAA+C;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B;AAED,QAAA,MAAM,MAAM,qFA8EV,CAAA;AAGF,QAAA,MAAM,WAAW,gKAkBf,CAAA;AAGF,MAAM,WAAW,mBACf,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,eAAe,CAAC,QAAQ,CAAC;IACvE;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,QAAA,MAAM,cAAc,6FAmClB,CAAA;AAGF,OAAO,EAAE,MAAM,EAAE,cAAc,EAAC,WAAW,EAAE,CAAA"}
|
package/dist/ui/avatar.js
CHANGED
|
@@ -46,9 +46,9 @@ var p = c.createContext("circle"), m = c.createContext("md"), h = f("relative fl
|
|
|
46
46
|
lg: "lg",
|
|
47
47
|
xl: "lg"
|
|
48
48
|
}, y = {
|
|
49
|
-
lead: "ring-accent-
|
|
50
|
-
admin: "ring-warning-
|
|
51
|
-
client: "ring-info-
|
|
49
|
+
lead: "ring-accent-6",
|
|
50
|
+
admin: "ring-warning-6",
|
|
51
|
+
client: "ring-info-6"
|
|
52
52
|
}, b = {
|
|
53
53
|
circle: "rounded-pill",
|
|
54
54
|
square: "rounded-none",
|
package/dist/ui/avatar.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"avatar.js","names":[],"sources":["../../src/ui/avatar.tsx"],"sourcesContent":["'use client'\n\nimport * as AvatarPrimitive from \"@primitives/react-avatar\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { motion } from \"framer-motion\"\nimport * as React from \"react\"\n\nimport { MotionPreference } from '../motion/motion-preference'\nimport { Dot, type DotColor, type DotSize } from \"./dot\"\nimport { springs } from \"./lib/motion\"\nimport { cn } from \"./lib/utils\"\nimport { Skeleton } from \"./skeleton\"\n\n// ── Contexts so AvatarFallback can inherit shape & size from Avatar ──\ntype AvatarShape = 'circle' | 'square' | 'rounded'\ntype AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\nconst AvatarShapeContext = React.createContext<AvatarShape>('circle')\nconst AvatarSizeContext = React.createContext<AvatarSize>('md')\n\nexport const avatarVariants = cva(\n 'relative flex shrink-0 overflow-hidden',\n {\n variants: {\n size: {\n xs: 'h-ds-xs w-ds-xs',\n sm: 'h-ds-sm w-ds-sm',\n md: 'h-ds-md w-ds-md',\n lg: 'h-ds-lg w-ds-lg',\n xl: 'h-ds-xl w-ds-xl',\n },\n shape: {\n circle: 'rounded-pill',\n square: 'rounded-none',\n rounded: 'rounded-control',\n },\n },\n defaultVariants: { size: 'md', shape: 'circle' },\n }\n)\n\nexport type AvatarStatus = 'online' | 'offline' | 'busy' | 'away'\n\nexport type AvatarRing = 'none' | 'lead' | 'admin' | 'client'\n\n// Presence status → shared Dot intent colour.\nconst statusDotColor: Record<AvatarStatus, DotColor> = {\n online: 'success',\n offline: 'neutral',\n busy: 'error',\n away: 'warning',\n}\n\nconst statusLabelMap: Record<AvatarStatus, string> = {\n online: 'Online',\n offline: 'Offline',\n busy: 'Busy',\n away: 'Away',\n}\n\n/** Avatar size → Dot size (the status dot scales with the avatar). */\nconst statusDotSize: Record<AvatarSize, DotSize> = {\n xs: 'xs',\n sm: 'sm',\n md: 'md',\n lg: 'lg',\n xl: 'lg',\n}\n\n// ── Role ring ───────────────────────────────────────────────────────────────\n\nconst ringColorMap: Record<Exclude<AvatarRing, 'none'>, string> = {\n lead: 'ring-accent-7',\n admin: 'ring-warning-7',\n client: 'ring-info-7',\n}\n\nconst ringShapeMap: Record<string, string> = {\n circle: 'rounded-pill',\n square: 'rounded-none',\n rounded: 'rounded-control',\n}\n\n// ── Fallback font sizes that scale with avatar size ─────────────────────────\nconst fallbackTextSizeMap: Record<AvatarSize, string> = {\n xs: 'text-body-xs',\n sm: 'text-body-xs',\n md: 'text-body-sm',\n lg: 'text-body-md',\n xl: 'text-heading-xs',\n}\n\n// ── Deterministic fallback colors ───────────────────────────────────────────\n\nconst FALLBACK_COLORS = [\n { bg: 'bg-accent-3', text: 'text-accent-11' },\n { bg: 'bg-success-3', text: 'text-success-11' },\n { bg: 'bg-warning-3', text: 'text-warning-11' },\n { bg: 'bg-error-3', text: 'text-error-11' },\n { bg: 'bg-info-3', text: 'text-info-11' },\n { bg: 'bg-category-teal-3', text: 'text-category-teal-11' },\n { bg: 'bg-category-indigo-3', text: 'text-category-indigo-11' },\n { bg: 'bg-category-cyan-3', text: 'text-category-cyan-11' },\n] as const\n\n/**\n * Simple string hash that produces a deterministic index into FALLBACK_COLORS.\n * Uses djb2 for a reasonable distribution with short strings.\n */\nfunction getFallbackColor(seed: string): (typeof FALLBACK_COLORS)[number] {\n let hash = 5381\n for (let i = 0; i < seed.length; i++) {\n hash = ((hash << 5) + hash + seed.charCodeAt(i)) | 0\n }\n const index = Math.abs(hash) % FALLBACK_COLORS.length\n return FALLBACK_COLORS[index]\n}\n\n/**\n * Props for Avatar — a user/entity image container with size, shape, and presence-status variants.\n *\n * **Sizes:** `xs` | `sm` | `md` (default) | `lg` | `xl` — the status dot scales with the avatar size.\n *\n * **Shapes:** `circle` (default, round) | `square` (sharp corners) | `rounded` (rounded corners, for bots/apps)\n *\n * **Status dot:** `status=\"online\"` (green) | `\"offline\"` (muted) | `\"busy\"` (red) | `\"away\"` (amber).\n * The dot renders with `role=\"img\"` and an accessible `aria-label` — it is not purely decorative.\n *\n * **Role ring:** `ring=\"lead\"` (accent) | `\"admin\"` (warning) | `\"client\"` (info) — a colored ring\n * around the avatar indicating the user's role.\n *\n * **Badge:** `badge={5}` (number) | `badge=\"dot\"` (notification dot) | `badge={<Icon />}` (custom).\n * Numbers > 99 display as \"99+\". Badge is hidden when `0` or `undefined`.\n *\n * **Loading:** `loading={true}` shows a pulse skeleton placeholder.\n *\n * **Children:** Use `<AvatarImage>` for the photo and `<AvatarFallback>` for initials when the image fails.\n *\n * @example\n * // User avatar with photo and fallback initials:\n * <Avatar size=\"md\">\n * <AvatarImage src={user.avatarUrl} alt={user.name} />\n * <AvatarFallback>JD</AvatarFallback>\n * </Avatar>\n *\n * @example\n * // Online presence indicator in a team roster:\n * <Avatar size=\"lg\" status=\"online\">\n * <AvatarImage src={user.photoUrl} alt={user.name} />\n * <AvatarFallback>{user.initials}</AvatarFallback>\n * </Avatar>\n *\n * @example\n * // Square shape for a bot/integration logo:\n * <Avatar shape=\"square\" size=\"xl\">\n * <AvatarImage src=\"/logos/github.png\" alt=\"GitHub\" />\n * </Avatar>\n * // These are just a few ways — feel free to combine props creatively!\n */\nexport interface AvatarProps\n extends Omit<React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>, 'children'>,\n VariantProps<typeof avatarVariants> {\n /** Optional status indicator displayed as a dot at the bottom-right corner */\n status?: AvatarStatus\n /** Role ring color indicator */\n ring?: AvatarRing\n /** Badge overlay: number, 'dot', or custom ReactNode */\n badge?: number | 'dot' | React.ReactNode\n /** Show loading skeleton instead of content */\n loading?: boolean\n children?: React.ReactNode\n}\n\nconst Avatar = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Root>,\n AvatarProps\n>(({ className, size, shape, status, ring, badge, loading, children, ...props }, ref) => {\n const resolvedShape = shape ?? 'circle'\n\n // Build ring classes for the outer wrapper\n const ringClasses = ring && ring !== 'none'\n ? cn('ring-2 ring-offset-2 ring-offset-surface-panel', ringColorMap[ring], ringShapeMap[resolvedShape])\n : undefined\n\n // Loading skeleton — early return\n if (loading) {\n return (\n <span ref={ref} className={cn('relative inline-flex shrink-0', ringClasses)}>\n <Skeleton\n className={cn(avatarVariants({ size, shape }))}\n data-slot=\"avatar-skeleton\"\n />\n </span>\n )\n }\n\n // Determine whether to render badge\n const showBadge = badge !== undefined && badge !== 0\n\n return (\n <MotionPreference>\n <AvatarShapeContext.Provider value={resolvedShape}>\n <AvatarSizeContext.Provider value={size ?? 'md'}>\n <span className={cn('relative inline-flex shrink-0', ringClasses)}>\n <AvatarPrimitive.Root\n ref={ref}\n className={cn(avatarVariants({ size, shape }), className)}\n {...props}\n >\n {children}\n </AvatarPrimitive.Root>\n {status && (\n <span\n className=\"absolute bottom-0 right-0\"\n role=\"img\"\n aria-label={statusLabelMap[status]}\n >\n {/* Shared Dot primitive: colour + contrast ring; wrapper owns position + a11y. */}\n <Dot color={statusDotColor[status]} size={statusDotSize[size ?? 'md']} withBorder aria-hidden />\n </span>\n )}\n {showBadge && (\n badge === 'dot' ? (\n <span\n className=\"absolute -right-0.5 -top-0.5 h-ds-03 w-ds-03 rounded-pill bg-error-9 ring-2 ring-surface-panel\"\n data-slot=\"avatar-badge-dot\"\n aria-hidden=\"true\"\n />\n ) : typeof badge === 'number' ? (\n <motion.span\n initial={{ scale: 0 }}\n animate={{ scale: 1 }}\n transition={springs.smooth}\n className=\"absolute -right-1 -top-1 flex min-w-ds-05 items-center justify-center rounded-pill bg-error-9 px-1 text-[10px] font-bold leading-[16px] text-error-fg ring-2 ring-surface-panel\"\n data-slot=\"avatar-badge\"\n role=\"status\"\n aria-label={`${badge > 99 ? '99+' : badge} notifications`}\n >\n {badge > 99 ? '99+' : badge}\n </motion.span>\n ) : (\n <span className=\"absolute -right-1 -top-1\" data-slot=\"avatar-badge-custom\">\n {badge}\n </span>\n )\n )}\n </span>\n </AvatarSizeContext.Provider>\n </AvatarShapeContext.Provider>\n </MotionPreference>\n )\n})\nAvatar.displayName = AvatarPrimitive.Root.displayName\n\nconst AvatarImage = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Image>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n <MotionPreference>\n <motion.span\n initial={{ scale: 0.96 }}\n animate={{ opacity: 1, scale: 1 }}\n transition={springs.smooth}\n className=\"absolute inset-0 h-full w-full\"\n >\n <AvatarPrimitive.Image\n ref={ref}\n className={cn(\"aspect-square h-full w-full\", className)}\n {...props}\n />\n </motion.span>\n </MotionPreference>\n))\nAvatarImage.displayName = AvatarPrimitive.Image.displayName\n\nexport interface AvatarFallbackProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> {\n /**\n * Seed string for deterministic color selection.\n * Falls back to the text content of `children` if not provided.\n */\n colorSeed?: string\n}\n\nconst AvatarFallback = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Fallback>,\n AvatarFallbackProps\n>(({ className, colorSeed, children, ...props }, ref) => {\n // Derive seed: explicit colorSeed > children text content > empty string\n const childrenText = typeof children === 'string' ? children : ''\n const seed = colorSeed ?? childrenText\n const color = getFallbackColor(seed)\n\n // Inherit shape and size from parent Avatar via context\n const shape = React.useContext(AvatarShapeContext)\n const size = React.useContext(AvatarSizeContext)\n const shapeClass = shape === 'square' ? 'rounded-none' : shape === 'rounded' ? 'rounded-control' : 'rounded-pill'\n\n // Letter-spacing based on character count\n const tracking = childrenText.length === 1 ? 'tracking-wide' : 'tracking-normal'\n\n return (\n <AvatarPrimitive.Fallback\n ref={ref}\n data-slot=\"avatar-fallback\"\n className={cn(\n 'flex h-full w-full items-center justify-center font-semibold',\n shapeClass,\n fallbackTextSizeMap[size],\n color.bg,\n color.text,\n tracking,\n className,\n )}\n {...props}\n >\n {children}\n </AvatarPrimitive.Fallback>\n )\n})\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName\n\nexport { Avatar, AvatarFallback,AvatarImage }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"avatar.js","names":[],"sources":["../../src/ui/avatar.tsx"],"sourcesContent":["'use client'\n\nimport * as AvatarPrimitive from \"@primitives/react-avatar\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { motion } from \"framer-motion\"\nimport * as React from \"react\"\n\nimport { MotionPreference } from '../motion/motion-preference'\nimport { Dot, type DotColor, type DotSize } from \"./dot\"\nimport { springs } from \"./lib/motion\"\nimport { cn } from \"./lib/utils\"\nimport { Skeleton } from \"./skeleton\"\n\n// ── Contexts so AvatarFallback can inherit shape & size from Avatar ──\ntype AvatarShape = 'circle' | 'square' | 'rounded'\ntype AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\nconst AvatarShapeContext = React.createContext<AvatarShape>('circle')\nconst AvatarSizeContext = React.createContext<AvatarSize>('md')\n\nexport const avatarVariants = cva(\n 'relative flex shrink-0 overflow-hidden',\n {\n variants: {\n size: {\n xs: 'h-ds-xs w-ds-xs',\n sm: 'h-ds-sm w-ds-sm',\n md: 'h-ds-md w-ds-md',\n lg: 'h-ds-lg w-ds-lg',\n xl: 'h-ds-xl w-ds-xl',\n },\n shape: {\n circle: 'rounded-pill',\n square: 'rounded-none',\n rounded: 'rounded-control',\n },\n },\n defaultVariants: { size: 'md', shape: 'circle' },\n }\n)\n\nexport type AvatarStatus = 'online' | 'offline' | 'busy' | 'away'\n\nexport type AvatarRing = 'none' | 'lead' | 'admin' | 'client'\n\n// Presence status → shared Dot intent colour.\nconst statusDotColor: Record<AvatarStatus, DotColor> = {\n online: 'success',\n offline: 'neutral',\n busy: 'error',\n away: 'warning',\n}\n\nconst statusLabelMap: Record<AvatarStatus, string> = {\n online: 'Online',\n offline: 'Offline',\n busy: 'Busy',\n away: 'Away',\n}\n\n/** Avatar size → Dot size (the status dot scales with the avatar). */\nconst statusDotSize: Record<AvatarSize, DotSize> = {\n xs: 'xs',\n sm: 'sm',\n md: 'md',\n lg: 'lg',\n xl: 'lg',\n}\n\n// ── Role ring ───────────────────────────────────────────────────────────────\n\n// Step 6, per the 2026-08-24 Figma refresh. Measurably fainter than step 7 —\n// lead drops 2.865 -> 2.095:1 on white — and the ring is the SOLE carrier of\n// role, with no label, icon or tooltip behind it. Shipped knowingly: all three\n// role colours were already under 3:1, so this deepens an existing shortfall\n// rather than creating one. deviation: AVATAR-ROLE-RING-STEP-6\nconst ringColorMap: Record<Exclude<AvatarRing, 'none'>, string> = {\n lead: 'ring-accent-6',\n admin: 'ring-warning-6',\n client: 'ring-info-6',\n}\n\nconst ringShapeMap: Record<string, string> = {\n circle: 'rounded-pill',\n square: 'rounded-none',\n rounded: 'rounded-control',\n}\n\n// ── Fallback font sizes that scale with avatar size ─────────────────────────\nconst fallbackTextSizeMap: Record<AvatarSize, string> = {\n xs: 'text-body-xs',\n sm: 'text-body-xs',\n md: 'text-body-sm',\n lg: 'text-body-md',\n xl: 'text-heading-xs',\n}\n\n// ── Deterministic fallback colors ───────────────────────────────────────────\n\nconst FALLBACK_COLORS = [\n { bg: 'bg-accent-3', text: 'text-accent-11' },\n { bg: 'bg-success-3', text: 'text-success-11' },\n { bg: 'bg-warning-3', text: 'text-warning-11' },\n { bg: 'bg-error-3', text: 'text-error-11' },\n { bg: 'bg-info-3', text: 'text-info-11' },\n { bg: 'bg-category-teal-3', text: 'text-category-teal-11' },\n { bg: 'bg-category-indigo-3', text: 'text-category-indigo-11' },\n { bg: 'bg-category-cyan-3', text: 'text-category-cyan-11' },\n] as const\n\n/**\n * Simple string hash that produces a deterministic index into FALLBACK_COLORS.\n * Uses djb2 for a reasonable distribution with short strings.\n */\nfunction getFallbackColor(seed: string): (typeof FALLBACK_COLORS)[number] {\n let hash = 5381\n for (let i = 0; i < seed.length; i++) {\n hash = ((hash << 5) + hash + seed.charCodeAt(i)) | 0\n }\n const index = Math.abs(hash) % FALLBACK_COLORS.length\n return FALLBACK_COLORS[index]\n}\n\n/**\n * Props for Avatar — a user/entity image container with size, shape, and presence-status variants.\n *\n * **Sizes:** `xs` | `sm` | `md` (default) | `lg` | `xl` — the status dot scales with the avatar size.\n *\n * **Shapes:** `circle` (default, round) | `square` (sharp corners) | `rounded` (rounded corners, for bots/apps)\n *\n * **Status dot:** `status=\"online\"` (green) | `\"offline\"` (muted) | `\"busy\"` (red) | `\"away\"` (amber).\n * The dot renders with `role=\"img\"` and an accessible `aria-label` — it is not purely decorative.\n *\n * **Role ring:** `ring=\"lead\"` (accent) | `\"admin\"` (warning) | `\"client\"` (info) — a colored ring\n * around the avatar indicating the user's role.\n *\n * **Badge:** `badge={5}` (number) | `badge=\"dot\"` (notification dot) | `badge={<Icon />}` (custom).\n * Numbers > 99 display as \"99+\". Badge is hidden when `0` or `undefined`.\n *\n * **Loading:** `loading={true}` shows a pulse skeleton placeholder.\n *\n * **Children:** Use `<AvatarImage>` for the photo and `<AvatarFallback>` for initials when the image fails.\n *\n * @example\n * // User avatar with photo and fallback initials:\n * <Avatar size=\"md\">\n * <AvatarImage src={user.avatarUrl} alt={user.name} />\n * <AvatarFallback>JD</AvatarFallback>\n * </Avatar>\n *\n * @example\n * // Online presence indicator in a team roster:\n * <Avatar size=\"lg\" status=\"online\">\n * <AvatarImage src={user.photoUrl} alt={user.name} />\n * <AvatarFallback>{user.initials}</AvatarFallback>\n * </Avatar>\n *\n * @example\n * // Square shape for a bot/integration logo:\n * <Avatar shape=\"square\" size=\"xl\">\n * <AvatarImage src=\"/logos/github.png\" alt=\"GitHub\" />\n * </Avatar>\n * // These are just a few ways — feel free to combine props creatively!\n */\nexport interface AvatarProps\n extends Omit<React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>, 'children'>,\n VariantProps<typeof avatarVariants> {\n /** Optional status indicator displayed as a dot at the bottom-right corner */\n status?: AvatarStatus\n /** Role ring color indicator */\n ring?: AvatarRing\n /** Badge overlay: number, 'dot', or custom ReactNode */\n badge?: number | 'dot' | React.ReactNode\n /** Show loading skeleton instead of content */\n loading?: boolean\n children?: React.ReactNode\n}\n\nconst Avatar = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Root>,\n AvatarProps\n>(({ className, size, shape, status, ring, badge, loading, children, ...props }, ref) => {\n const resolvedShape = shape ?? 'circle'\n\n // Build ring classes for the outer wrapper\n const ringClasses = ring && ring !== 'none'\n ? cn('ring-2 ring-offset-2 ring-offset-surface-panel', ringColorMap[ring], ringShapeMap[resolvedShape])\n : undefined\n\n // Loading skeleton — early return\n if (loading) {\n return (\n <span ref={ref} className={cn('relative inline-flex shrink-0', ringClasses)}>\n <Skeleton\n className={cn(avatarVariants({ size, shape }))}\n data-slot=\"avatar-skeleton\"\n />\n </span>\n )\n }\n\n // Determine whether to render badge\n const showBadge = badge !== undefined && badge !== 0\n\n return (\n <MotionPreference>\n <AvatarShapeContext.Provider value={resolvedShape}>\n <AvatarSizeContext.Provider value={size ?? 'md'}>\n <span className={cn('relative inline-flex shrink-0', ringClasses)}>\n <AvatarPrimitive.Root\n ref={ref}\n className={cn(avatarVariants({ size, shape }), className)}\n {...props}\n >\n {children}\n </AvatarPrimitive.Root>\n {status && (\n <span\n className=\"absolute bottom-0 right-0\"\n role=\"img\"\n aria-label={statusLabelMap[status]}\n >\n {/* Shared Dot primitive: colour + contrast ring; wrapper owns position + a11y. */}\n <Dot color={statusDotColor[status]} size={statusDotSize[size ?? 'md']} withBorder aria-hidden />\n </span>\n )}\n {showBadge && (\n badge === 'dot' ? (\n <span\n className=\"absolute -right-0.5 -top-0.5 h-ds-03 w-ds-03 rounded-pill bg-error-9 ring-2 ring-surface-panel\"\n data-slot=\"avatar-badge-dot\"\n aria-hidden=\"true\"\n />\n ) : typeof badge === 'number' ? (\n <motion.span\n initial={{ scale: 0 }}\n animate={{ scale: 1 }}\n transition={springs.smooth}\n className=\"absolute -right-1 -top-1 flex min-w-ds-05 items-center justify-center rounded-pill bg-error-9 px-1 text-[10px] font-bold leading-[16px] text-error-fg ring-2 ring-surface-panel\"\n data-slot=\"avatar-badge\"\n role=\"status\"\n aria-label={`${badge > 99 ? '99+' : badge} notifications`}\n >\n {badge > 99 ? '99+' : badge}\n </motion.span>\n ) : (\n <span className=\"absolute -right-1 -top-1\" data-slot=\"avatar-badge-custom\">\n {badge}\n </span>\n )\n )}\n </span>\n </AvatarSizeContext.Provider>\n </AvatarShapeContext.Provider>\n </MotionPreference>\n )\n})\nAvatar.displayName = AvatarPrimitive.Root.displayName\n\nconst AvatarImage = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Image>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n <MotionPreference>\n <motion.span\n initial={{ scale: 0.96 }}\n animate={{ opacity: 1, scale: 1 }}\n transition={springs.smooth}\n className=\"absolute inset-0 h-full w-full\"\n >\n <AvatarPrimitive.Image\n ref={ref}\n className={cn(\"aspect-square h-full w-full\", className)}\n {...props}\n />\n </motion.span>\n </MotionPreference>\n))\nAvatarImage.displayName = AvatarPrimitive.Image.displayName\n\nexport interface AvatarFallbackProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> {\n /**\n * Seed string for deterministic color selection.\n * Falls back to the text content of `children` if not provided.\n */\n colorSeed?: string\n}\n\nconst AvatarFallback = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Fallback>,\n AvatarFallbackProps\n>(({ className, colorSeed, children, ...props }, ref) => {\n // Derive seed: explicit colorSeed > children text content > empty string\n const childrenText = typeof children === 'string' ? children : ''\n const seed = colorSeed ?? childrenText\n const color = getFallbackColor(seed)\n\n // Inherit shape and size from parent Avatar via context\n const shape = React.useContext(AvatarShapeContext)\n const size = React.useContext(AvatarSizeContext)\n const shapeClass = shape === 'square' ? 'rounded-none' : shape === 'rounded' ? 'rounded-control' : 'rounded-pill'\n\n // Letter-spacing based on character count\n const tracking = childrenText.length === 1 ? 'tracking-wide' : 'tracking-normal'\n\n return (\n <AvatarPrimitive.Fallback\n ref={ref}\n data-slot=\"avatar-fallback\"\n className={cn(\n 'flex h-full w-full items-center justify-center font-semibold',\n shapeClass,\n fallbackTextSizeMap[size],\n color.bg,\n color.text,\n tracking,\n className,\n )}\n {...props}\n >\n {children}\n </AvatarPrimitive.Fallback>\n )\n})\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName\n\nexport { Avatar, AvatarFallback,AvatarImage }\n"],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Button } from './button.js';
|
|
2
|
+
import { IconInput } from './lib/icon-input.js';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
type ButtonColor = React.ComponentProps<typeof Button>['color'];
|
|
5
|
+
export interface BulkActionBarAction {
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: IconInput;
|
|
8
|
+
onClick: () => void;
|
|
9
|
+
/** Any Button color. @default 'accent' (or 'error' for destructive). */
|
|
10
|
+
color?: ButtonColor;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
/** Show a pending spinner on the action (e.g. slow bulk op in flight). */
|
|
13
|
+
loading?: boolean;
|
|
14
|
+
/** Show inline confirmation before executing the action. */
|
|
15
|
+
requiresConfirmation?: boolean;
|
|
16
|
+
/** Custom confirmation message. @default 'Are you sure?' */
|
|
17
|
+
confirmMessage?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface BulkActionBarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children' | 'color' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration' | 'style'> {
|
|
20
|
+
show: boolean;
|
|
21
|
+
count: number;
|
|
22
|
+
onClearSelection: () => void;
|
|
23
|
+
actions: BulkActionBarAction[];
|
|
24
|
+
/** Total items available for selection (enables "Select all"). */
|
|
25
|
+
totalCount?: number;
|
|
26
|
+
/** Called when the user clicks "Select all". */
|
|
27
|
+
onSelectAll?: () => void;
|
|
28
|
+
/**
|
|
29
|
+
* Where the bar sits, and — as a consequence — whether it is portalled.
|
|
30
|
+
*
|
|
31
|
+
* `bottom` / `top` pin it to the viewport and render through a portal to
|
|
32
|
+
* `document.body`. `inline` renders it in flow, in place, with no portal.
|
|
33
|
+
*
|
|
34
|
+
* Position and portalling are one prop rather than two because they are not
|
|
35
|
+
* independent: a portal escapes its ancestors' stacking context, so a
|
|
36
|
+
* portalled bar inside a Dialog or Sheet floats over the page rather than
|
|
37
|
+
* belonging to the overlay. `inline` is the answer there, and it can only
|
|
38
|
+
* work in flow.
|
|
39
|
+
*
|
|
40
|
+
* Every system surveyed before building this — Carbon, Polaris, Material —
|
|
41
|
+
* renders its bulk bar in flow, anchored to whatever holds the selection.
|
|
42
|
+
* The default stays `bottom` because that is what this component already did.
|
|
43
|
+
*
|
|
44
|
+
* @default 'bottom'
|
|
45
|
+
*/
|
|
46
|
+
placement?: BulkActionsPlacement;
|
|
47
|
+
}
|
|
48
|
+
/** Where the bar sits. `inline` is the only one that is not portalled. */
|
|
49
|
+
export type BulkActionsPlacement = 'bottom' | 'top' | 'inline';
|
|
50
|
+
/**
|
|
51
|
+
* BulkActionBar — floating selection toolbar. Follows the ARIA toolbar model:
|
|
52
|
+
* a single tab stop with roving focus (Arrow keys / Home / End) across ALL
|
|
53
|
+
* controls (Select-all, actions, Clear), focus landing on the real buttons so
|
|
54
|
+
* Enter/Space activate them. Arrow direction mirrors under `dir="rtl"`. Escape
|
|
55
|
+
* clears the selection (or cancels a pending confirmation).
|
|
56
|
+
*/
|
|
57
|
+
declare const BulkActionBar: React.ForwardRefExoticComponent<BulkActionBarProps & React.RefAttributes<HTMLDivElement>>;
|
|
58
|
+
export { BulkActionBar };
|
|
59
|
+
//# sourceMappingURL=bulk-action-bar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bulk-action-bar.d.ts","sourceRoot":"","sources":["../../src/ui/bulk-action-bar.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAEjC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAIjD,KAAK,WAAW,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,MAAM,CAAC,CAAC,OAAO,CAAC,CAAA;AAE/D,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,wEAAwE;IACxE,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,4DAA4D;IAC5D,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,4DAA4D;IAC5D,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,kBACf,SAAQ,IAAI,CACV,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAEpC,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,aAAa,GAAG,WAAW,GAAG,kBAAkB,GAAG,gBAAgB,GAAG,sBAAsB,GAAG,OAAO,CACzI;IACD,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B,OAAO,EAAE,mBAAmB,EAAE,CAAA;IAC9B,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IACxB;;;;;;;;;;;;;;;;;OAiBG;IACH,SAAS,CAAC,EAAE,oBAAoB,CAAA;CACjC;AAED,0EAA0E;AAC1E,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,KAAK,GAAG,QAAQ,CAAA;AAS9D;;;;;;GAMG;AACH,QAAA,MAAM,aAAa,2FAqLlB,CAAA;AAGD,OAAO,EAAE,aAAa,EAAE,CAAA"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { springs as e } from "./lib/motion.js";
|
|
3
|
+
import { cn as t } from "./lib/utils.js";
|
|
4
|
+
import { Icon as n } from "./icon.js";
|
|
5
|
+
import { BadgeCompound as r } from "../_chunks/badge-group.js";
|
|
6
|
+
import { Button as i } from "./button.js";
|
|
7
|
+
import * as a from "react";
|
|
8
|
+
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
9
|
+
import { createPortal as c } from "react-dom";
|
|
10
|
+
import { IconX as l } from "@tabler/icons-react";
|
|
11
|
+
import { AnimatePresence as u, motion as d, useReducedMotion as f } from "framer-motion";
|
|
12
|
+
//#region src/ui/bulk-action-bar.tsx
|
|
13
|
+
var p = {
|
|
14
|
+
bottom: "fixed bottom-ds-06 start-1/2 z-sticky -translate-x-1/2",
|
|
15
|
+
top: "fixed top-ds-06 start-1/2 z-sticky -translate-x-1/2",
|
|
16
|
+
inline: "relative"
|
|
17
|
+
}, m = a.forwardRef(function({ show: m, count: h, onClearSelection: g, actions: _, totalCount: v, onSelectAll: y, placement: b = "bottom", className: x, ...S }, C) {
|
|
18
|
+
let w = f(), [T, E] = a.useState(!1), [D, O] = a.useState(0), [k, A] = a.useState(null), j = a.useRef([]), M = a.useRef(null), N = v != null && v > h && !!y, P = +!!N, F = P + _.length + 1;
|
|
19
|
+
a.useEffect(() => {
|
|
20
|
+
E(!0);
|
|
21
|
+
}, []), a.useEffect(() => {
|
|
22
|
+
O(0), A(null);
|
|
23
|
+
}, [_.length]), a.useEffect(() => {
|
|
24
|
+
k != null && M.current?.focus();
|
|
25
|
+
}, [k]);
|
|
26
|
+
let I = (e) => {
|
|
27
|
+
let t = (e + F) % F;
|
|
28
|
+
O(t), j.current[t]?.focus();
|
|
29
|
+
};
|
|
30
|
+
function L(e) {
|
|
31
|
+
if (e.key === "Escape") {
|
|
32
|
+
e.preventDefault(), k == null ? g() : (A(null), I(D));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (k != null) return;
|
|
36
|
+
let t = e.currentTarget.dir === "rtl" || document.dir === "rtl", n = t ? "ArrowLeft" : "ArrowRight", r = t ? "ArrowRight" : "ArrowLeft";
|
|
37
|
+
e.key === n ? (e.preventDefault(), I(D + 1)) : e.key === r ? (e.preventDefault(), I(D - 1)) : e.key === "Home" ? (e.preventDefault(), I(0)) : e.key === "End" && (e.preventDefault(), I(F - 1));
|
|
38
|
+
}
|
|
39
|
+
let R = b !== "inline";
|
|
40
|
+
if (R && !T) return null;
|
|
41
|
+
let z = w ? { duration: 0 } : e.smooth, B = b === "top" ? {
|
|
42
|
+
y: -100,
|
|
43
|
+
opacity: 0
|
|
44
|
+
} : {
|
|
45
|
+
y: 100,
|
|
46
|
+
opacity: 0
|
|
47
|
+
}, V = /* @__PURE__ */ o(u, { children: m && /* @__PURE__ */ s(d.div, {
|
|
48
|
+
ref: C,
|
|
49
|
+
initial: w || !R ? { opacity: 0 } : B,
|
|
50
|
+
animate: w || !R ? { opacity: 1 } : {
|
|
51
|
+
y: 0,
|
|
52
|
+
opacity: 1
|
|
53
|
+
},
|
|
54
|
+
exit: w || !R ? { opacity: 0 } : B,
|
|
55
|
+
transition: z,
|
|
56
|
+
className: t(p[b], "flex items-center gap-ds-04 rounded-surface bg-surface-overlay px-ds-05 py-ds-03 shadow-floating", x),
|
|
57
|
+
role: "toolbar",
|
|
58
|
+
"aria-label": "Bulk actions",
|
|
59
|
+
onKeyDown: L,
|
|
60
|
+
...S,
|
|
61
|
+
children: [
|
|
62
|
+
/* @__PURE__ */ s(r, {
|
|
63
|
+
variant: "solid",
|
|
64
|
+
size: "sm",
|
|
65
|
+
children: [h, " selected"]
|
|
66
|
+
}),
|
|
67
|
+
N && /* @__PURE__ */ s(i, {
|
|
68
|
+
ref: (e) => {
|
|
69
|
+
j.current[0] = e;
|
|
70
|
+
},
|
|
71
|
+
variant: "link",
|
|
72
|
+
size: "xs",
|
|
73
|
+
tabIndex: D === 0 ? 0 : -1,
|
|
74
|
+
onClick: y,
|
|
75
|
+
children: ["Select all ", v]
|
|
76
|
+
}),
|
|
77
|
+
/* @__PURE__ */ o("div", {
|
|
78
|
+
className: "flex items-center gap-ds-02",
|
|
79
|
+
children: _.map((e, t) => {
|
|
80
|
+
let n = P + t;
|
|
81
|
+
return k === t ? /* @__PURE__ */ s("div", {
|
|
82
|
+
role: "group",
|
|
83
|
+
"aria-live": "assertive",
|
|
84
|
+
className: "flex items-center gap-ds-02",
|
|
85
|
+
children: [
|
|
86
|
+
/* @__PURE__ */ o("span", {
|
|
87
|
+
className: "whitespace-nowrap text-body-sm text-surface-fg-muted",
|
|
88
|
+
children: e.confirmMessage ?? "Are you sure?"
|
|
89
|
+
}),
|
|
90
|
+
/* @__PURE__ */ o(i, {
|
|
91
|
+
ref: M,
|
|
92
|
+
variant: "solid",
|
|
93
|
+
size: "sm",
|
|
94
|
+
color: e.color === "error" ? "error" : e.color ?? "accent",
|
|
95
|
+
onClick: () => {
|
|
96
|
+
A(null), e.onClick();
|
|
97
|
+
},
|
|
98
|
+
children: "Confirm"
|
|
99
|
+
}),
|
|
100
|
+
/* @__PURE__ */ o(i, {
|
|
101
|
+
variant: "ghost",
|
|
102
|
+
size: "sm",
|
|
103
|
+
onClick: () => {
|
|
104
|
+
A(null), I(n);
|
|
105
|
+
},
|
|
106
|
+
children: "Cancel"
|
|
107
|
+
})
|
|
108
|
+
]
|
|
109
|
+
}, e.label) : /* @__PURE__ */ o(i, {
|
|
110
|
+
ref: (e) => {
|
|
111
|
+
j.current[n] = e;
|
|
112
|
+
},
|
|
113
|
+
variant: e.color === "error" ? "solid" : "ghost",
|
|
114
|
+
size: "sm",
|
|
115
|
+
color: e.color ?? "accent",
|
|
116
|
+
disabled: e.disabled,
|
|
117
|
+
loading: e.loading,
|
|
118
|
+
startIcon: e.icon ?? void 0,
|
|
119
|
+
tabIndex: D === n ? 0 : -1,
|
|
120
|
+
onClick: e.requiresConfirmation ? () => A(t) : e.onClick,
|
|
121
|
+
children: e.label
|
|
122
|
+
}, e.label);
|
|
123
|
+
})
|
|
124
|
+
}),
|
|
125
|
+
/* @__PURE__ */ o(i, {
|
|
126
|
+
ref: (e) => {
|
|
127
|
+
j.current[F - 1] = e;
|
|
128
|
+
},
|
|
129
|
+
variant: "ghost",
|
|
130
|
+
size: "icon-sm",
|
|
131
|
+
tabIndex: D === F - 1 ? 0 : -1,
|
|
132
|
+
onClick: g,
|
|
133
|
+
"aria-label": "Clear selection",
|
|
134
|
+
children: /* @__PURE__ */ o(n, {
|
|
135
|
+
icon: l,
|
|
136
|
+
size: "sm"
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
]
|
|
140
|
+
}) });
|
|
141
|
+
return R ? c(V, document.body) : V;
|
|
142
|
+
});
|
|
143
|
+
m.displayName = "BulkActionBar";
|
|
144
|
+
//#endregion
|
|
145
|
+
export { m as BulkActionBar };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bulk-action-bar.js","names":[],"sources":["../../src/ui/bulk-action-bar.tsx"],"sourcesContent":["'use client'\n\nimport { IconX } from '@tabler/icons-react'\nimport { AnimatePresence, motion, useReducedMotion } from 'framer-motion'\nimport * as React from 'react'\nimport { createPortal } from 'react-dom'\n\nimport { Badge } from './badge'\nimport { Button } from './button'\nimport { Icon } from './icon'\nimport type { IconInput } from './lib/icon-input'\nimport { springs } from './lib/motion'\nimport { cn } from './lib/utils'\n\ntype ButtonColor = React.ComponentProps<typeof Button>['color']\n\nexport interface BulkActionBarAction {\n label: string\n icon?: IconInput\n onClick: () => void\n /** Any Button color. @default 'accent' (or 'error' for destructive). */\n color?: ButtonColor\n disabled?: boolean\n /** Show a pending spinner on the action (e.g. slow bulk op in flight). */\n loading?: boolean\n /** Show inline confirmation before executing the action. */\n requiresConfirmation?: boolean\n /** Custom confirmation message. @default 'Are you sure?' */\n confirmMessage?: string\n}\n\nexport interface BulkActionBarProps\n extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n // Keys whose types collide with framer-motion's HTMLMotionProps.\n 'children' | 'color' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration' | 'style'\n > {\n show: boolean\n count: number\n onClearSelection: () => void\n actions: BulkActionBarAction[]\n /** Total items available for selection (enables \"Select all\"). */\n totalCount?: number\n /** Called when the user clicks \"Select all\". */\n onSelectAll?: () => void\n /**\n * Where the bar sits, and — as a consequence — whether it is portalled.\n *\n * `bottom` / `top` pin it to the viewport and render through a portal to\n * `document.body`. `inline` renders it in flow, in place, with no portal.\n *\n * Position and portalling are one prop rather than two because they are not\n * independent: a portal escapes its ancestors' stacking context, so a\n * portalled bar inside a Dialog or Sheet floats over the page rather than\n * belonging to the overlay. `inline` is the answer there, and it can only\n * work in flow.\n *\n * Every system surveyed before building this — Carbon, Polaris, Material —\n * renders its bulk bar in flow, anchored to whatever holds the selection.\n * The default stays `bottom` because that is what this component already did.\n *\n * @default 'bottom'\n */\n placement?: BulkActionsPlacement\n}\n\n/** Where the bar sits. `inline` is the only one that is not portalled. */\nexport type BulkActionsPlacement = 'bottom' | 'top' | 'inline'\n\nconst PLACEMENT_CLASSES: Record<BulkActionsPlacement, string> = {\n bottom: 'fixed bottom-ds-06 start-1/2 z-sticky -translate-x-1/2',\n top: 'fixed top-ds-06 start-1/2 z-sticky -translate-x-1/2',\n // No `fixed`, no transform — it takes part in the layout it is placed in.\n inline: 'relative',\n}\n\n/**\n * BulkActionBar — floating selection toolbar. Follows the ARIA toolbar model:\n * a single tab stop with roving focus (Arrow keys / Home / End) across ALL\n * controls (Select-all, actions, Clear), focus landing on the real buttons so\n * Enter/Space activate them. Arrow direction mirrors under `dir=\"rtl\"`. Escape\n * clears the selection (or cancels a pending confirmation).\n */\nconst BulkActionBar = React.forwardRef<HTMLDivElement, BulkActionBarProps>(\n function BulkActionBar(\n {\n show,\n count,\n onClearSelection,\n actions,\n totalCount,\n onSelectAll,\n placement = 'bottom',\n className,\n ...props\n },\n forwardedRef,\n ) {\n const reduced = useReducedMotion()\n const [mounted, setMounted] = React.useState(false)\n const [focusedIndex, setFocusedIndex] = React.useState(0)\n const [confirming, setConfirming] = React.useState<number | null>(null)\n const controlRefs = React.useRef<Array<HTMLButtonElement | null>>([])\n const confirmRef = React.useRef<HTMLButtonElement | null>(null)\n\n const hasSelectAll = totalCount != null && totalCount > count && !!onSelectAll\n const selectAllOffset = hasSelectAll ? 1 : 0\n const total = selectAllOffset + actions.length + 1 // + Clear\n\n React.useEffect(() => { setMounted(true) }, [])\n React.useEffect(() => { setFocusedIndex(0); setConfirming(null) }, [actions.length])\n // Move focus to Confirm when a confirmation opens.\n React.useEffect(() => {\n if (confirming != null) confirmRef.current?.focus()\n }, [confirming])\n\n const focusAt = (i: number) => {\n const clamped = (i + total) % total\n setFocusedIndex(clamped)\n controlRefs.current[clamped]?.focus()\n }\n\n function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {\n if (e.key === 'Escape') {\n e.preventDefault()\n if (confirming != null) { setConfirming(null); focusAt(focusedIndex) }\n else onClearSelection()\n return\n }\n if (confirming != null) return // roving suspended during confirm\n const rtl = e.currentTarget.dir === 'rtl' || document.dir === 'rtl'\n const fwd = rtl ? 'ArrowLeft' : 'ArrowRight'\n const back = rtl ? 'ArrowRight' : 'ArrowLeft'\n if (e.key === fwd) { e.preventDefault(); focusAt(focusedIndex + 1) }\n else if (e.key === back) { e.preventDefault(); focusAt(focusedIndex - 1) }\n else if (e.key === 'Home') { e.preventDefault(); focusAt(0) }\n else if (e.key === 'End') { e.preventDefault(); focusAt(total - 1) }\n }\n\n const portalled = placement !== 'inline'\n\n // The mount gate exists only so `document.body` is available for the\n // portal. An inline bar has no portal, so gating it would cost a\n // server-render for no reason and flash on hydrate.\n if (portalled && !mounted) return null\n\n const slideTransition = reduced ? { duration: 0 } : springs.smooth\n // Slide from the edge it is pinned to; inline just fades, since there is\n // no edge for it to arrive from.\n const offscreen =\n placement === 'top' ? { y: -100, opacity: 0 } : { y: 100, opacity: 0 }\n\n const tree = (\n <AnimatePresence>\n {show && (\n <motion.div\n ref={forwardedRef}\n initial={reduced || !portalled ? { opacity: 0 } : offscreen}\n animate={reduced || !portalled ? { opacity: 1 } : { y: 0, opacity: 1 }}\n exit={reduced || !portalled ? { opacity: 0 } : offscreen}\n transition={slideTransition}\n className={cn(\n PLACEMENT_CLASSES[placement],\n 'flex items-center gap-ds-04 rounded-surface bg-surface-overlay px-ds-05 py-ds-03 shadow-floating',\n className,\n )}\n role=\"toolbar\"\n // A stable name describing PURPOSE, not state. The count is\n // already announced by the Badge below and by the live region on\n // \"Select all\"; putting it in the toolbar's accessible name too\n // means the name changes on every selection change, which a screen\n // reader re-announces. Overridable via aria-label.\n aria-label=\"Bulk actions\"\n onKeyDown={handleKeyDown}\n {...props}\n >\n <Badge variant=\"solid\" size=\"sm\">{count} selected</Badge>\n\n {hasSelectAll && (\n <Button\n ref={(el) => { controlRefs.current[0] = el }}\n variant=\"link\"\n size=\"xs\"\n tabIndex={focusedIndex === 0 ? 0 : -1}\n onClick={onSelectAll}\n >\n Select all {totalCount}\n </Button>\n )}\n\n <div className=\"flex items-center gap-ds-02\">\n {actions.map((action, i) => {\n const controlIndex = selectAllOffset + i\n const isConfirming = confirming === i\n if (isConfirming) {\n return (\n <div\n key={action.label}\n role=\"group\"\n aria-live=\"assertive\"\n className=\"flex items-center gap-ds-02\"\n >\n <span className=\"whitespace-nowrap text-body-sm text-surface-fg-muted\">\n {action.confirmMessage ?? 'Are you sure?'}\n </span>\n <Button\n ref={confirmRef}\n variant=\"solid\"\n size=\"sm\"\n color={action.color === 'error' ? 'error' : (action.color ?? 'accent')}\n onClick={() => { setConfirming(null); action.onClick() }}\n >\n Confirm\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => { setConfirming(null); focusAt(controlIndex) }}\n >\n Cancel\n </Button>\n </div>\n )\n }\n return (\n <Button\n key={action.label}\n ref={(el) => { controlRefs.current[controlIndex] = el }}\n // Destructive actions stay solid. Ghost is right for the\n // rest — the bar itself is already the affordance — but a\n // Delete that looks like every other button is the one\n // place where quietness costs something.\n variant={action.color === 'error' ? 'solid' : 'ghost'}\n size=\"sm\"\n color={action.color ?? 'accent'}\n disabled={action.disabled}\n loading={action.loading}\n startIcon={action.icon ?? undefined}\n tabIndex={focusedIndex === controlIndex ? 0 : -1}\n onClick={action.requiresConfirmation ? () => setConfirming(i) : action.onClick}\n >\n {action.label}\n </Button>\n )\n })}\n </div>\n\n <Button\n ref={(el) => { controlRefs.current[total - 1] = el }}\n variant=\"ghost\"\n size=\"icon-sm\"\n tabIndex={focusedIndex === total - 1 ? 0 : -1}\n onClick={onClearSelection}\n aria-label=\"Clear selection\"\n >\n <Icon icon={IconX} size=\"sm\" />\n </Button>\n </motion.div>\n )}\n </AnimatePresence>\n )\n\n return portalled ? createPortal(tree, document.body) : tree\n },\n)\nBulkActionBar.displayName = 'BulkActionBar'\n\nexport { BulkActionBar }\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkbox.d.ts","sourceRoot":"","sources":["../../src/ui/checkbox.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,iBAAiB,MAAM,4BAA4B,CAAA;AAE/D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,EAAE,KAAK,UAAU,EAAqB,MAAM,mBAAmB,CAAA;AAWtE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,WAAW,aAAc,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,iBAAiB,CAAC,IAAI,CAAC;IAClG,8GAA8G;IAC9G,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,qFAAqF;IACrF,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;CAC1B;AAcD,QAAA,MAAM,QAAQ,
|
|
1
|
+
{"version":3,"file":"checkbox.d.ts","sourceRoot":"","sources":["../../src/ui/checkbox.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,iBAAiB,MAAM,4BAA4B,CAAA;AAE/D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,EAAE,KAAK,UAAU,EAAqB,MAAM,mBAAmB,CAAA;AAWtE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,WAAW,aAAc,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,iBAAiB,CAAC,IAAI,CAAC;IAClG,8GAA8G;IAC9G,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,qFAAqF;IACrF,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;CAC1B;AAcD,QAAA,MAAM,QAAQ,yFA+GZ,CAAA;AAGF,OAAO,EAAE,QAAQ,EAAE,CAAA"}
|
package/dist/ui/checkbox.js
CHANGED
|
@@ -29,7 +29,7 @@ var u = {
|
|
|
29
29
|
checked: S || h ? k : void 0,
|
|
30
30
|
defaultChecked: !S && !h ? y : void 0,
|
|
31
31
|
onCheckedChange: j,
|
|
32
|
-
className: t("touch-target peer flex items-center justify-center shrink-0 rounded-control-inner", d[g], "border border-surface-border-interactive", "bg-surface-panel-hover", "transition-colors duration-fast-01 ease-productive-standard", "focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-accent-9 focus-visible:ring-offset-2", "disabled:cursor-not-allowed disabled:opacity-action-disabled", "data-[state=unchecked]:hover:
|
|
32
|
+
className: t("touch-target peer flex items-center justify-center shrink-0 rounded-control-inner", d[g], "border border-surface-border-interactive", "bg-surface-panel-hover", "transition-colors duration-fast-01 ease-productive-standard", "focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-accent-9 focus-visible:ring-offset-2", "disabled:cursor-not-allowed disabled:opacity-action-disabled", "data-[state=unchecked]:hover:bg-neutral-4", "data-[state=checked]:bg-accent-9 data-[state=checked]:border-accent-7 data-[state=checked]:text-accent-fg", "data-[state=indeterminate]:bg-accent-9 data-[state=indeterminate]:border-accent-7 data-[state=indeterminate]:text-accent-fg", E && u[E], p),
|
|
33
33
|
"aria-invalid": E === "error" || void 0,
|
|
34
34
|
"aria-describedby": D,
|
|
35
35
|
"aria-required": O || void 0,
|
package/dist/ui/checkbox.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkbox.js","names":[],"sources":["../../src/ui/checkbox.tsx"],"sourcesContent":["'use client'\n\nimport * as CheckboxPrimitive from '@primitives/react-checkbox'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { useFormField } from './form'\nimport { type FieldState, resolveFieldState } from './lib/field-state'\nimport { durations } from './lib/motion'\nimport { cn } from './lib/utils'\n\n/** Border + background tint per validation state (checked state uses accent regardless). */\nconst stateTintClasses: Record<Exclude<FieldState, 'default'>, string> = {\n error: 'border-error-7 bg-error-3',\n warning: 'border-warning-7 bg-warning-3',\n success: 'border-success-7 bg-success-3',\n}\n\n/**\n * Props for Checkbox — a Radix-powered accessible checkbox with error state styling and\n * an indeterminate state for \"select all\" row controls.\n *\n * **Indeterminate:** Pass `indeterminate={true}` to show the \"−\" dash (overrides `checked`).\n * This is correct for \"select all\" headers when only some rows are selected.\n *\n * **Controlled:** Use `checked` + `onCheckedChange` for controlled usage.\n * `onCheckedChange` receives `true | false | 'indeterminate'` from Radix.\n *\n * @example\n * // Basic controlled checkbox:\n * <Checkbox checked={agreed} onCheckedChange={(v) => setAgreed(v === true)} />\n *\n * @example\n * // Error state when required checkbox is not checked:\n * <Checkbox state={!termsAccepted ? 'error' : 'default'} checked={termsAccepted} onCheckedChange={(v) => setTerms(v === true)} />\n *\n * @example\n * // \"Select all\" checkbox with indeterminate state:\n * <Checkbox\n * indeterminate={someSelected && !allSelected}\n * checked={allSelected}\n * onCheckedChange={(v) => toggleAll(v === true)}\n * />\n * // These are just a few ways — feel free to combine props creatively!\n */\nexport interface CheckboxProps extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> {\n /** Validation/feedback state. `'error'` also sets `aria-invalid`. Inherited from `FormField` when omitted. */\n state?: FieldState\n indeterminate?: boolean\n /** Control size — `sm` (20px), `md` (24px, default, WCAG compliant), `lg` (28px). */\n size?: 'sm' | 'md' | 'lg'\n}\n\nconst checkboxSizeClasses = {\n sm: 'h-5 w-5',\n md: 'h-6 w-6',\n lg: 'h-7 w-7',\n} as const\n\nconst checkboxIconClasses = {\n sm: 'h-[14px] w-[14px]',\n md: 'h-[18px] w-[18px]',\n lg: 'h-5 w-5',\n} as const\n\nconst Checkbox = React.forwardRef<\n React.ElementRef<typeof CheckboxPrimitive.Root>,\n CheckboxProps\n>(({ className, state: stateProp, indeterminate, size = 'md', checked, onCheckedChange, defaultChecked, ...props }, ref) => {\n // Track internal state so AnimatePresence works for both controlled & uncontrolled usage\n const isControlled = checked !== undefined\n const [internalChecked, setInternalChecked] = React.useState<boolean | 'indeterminate'>(\n indeterminate ? 'indeterminate' : (defaultChecked ? true : false),\n )\n\n const fieldCtx = useFormField()\n const state = resolveFieldState(stateProp, fieldCtx.state)\n const ariaDescribedBy = props['aria-describedby'] ?? fieldCtx.helperTextId\n const ariaRequired = props['aria-required'] ?? fieldCtx.required\n\n const actualChecked = indeterminate ? 'indeterminate' : (isControlled ? checked : internalChecked)\n const isActive = actualChecked === true || actualChecked === 'indeterminate'\n\n const handleCheckedChange = React.useCallback(\n (value: boolean | 'indeterminate') => {\n if (!isControlled) setInternalChecked(value)\n onCheckedChange?.(value)\n },\n [isControlled, onCheckedChange],\n )\n\n return (\n <CheckboxPrimitive.Root\n ref={ref}\n checked={isControlled || indeterminate ? actualChecked : undefined}\n defaultChecked={!isControlled && !indeterminate ? defaultChecked : undefined}\n onCheckedChange={handleCheckedChange}\n className={cn(\n 'touch-target peer flex items-center justify-center shrink-0 rounded-control-inner',\n checkboxSizeClasses[size],\n 'border border-surface-border-interactive',\n 'bg-surface-panel-hover',\n 'transition-colors duration-fast-01 ease-productive-standard',\n 'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-accent-9 focus-visible:ring-offset-2',\n 'disabled:cursor-not-allowed disabled:opacity-action-disabled',\n
|
|
1
|
+
{"version":3,"file":"checkbox.js","names":[],"sources":["../../src/ui/checkbox.tsx"],"sourcesContent":["'use client'\n\nimport * as CheckboxPrimitive from '@primitives/react-checkbox'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { useFormField } from './form'\nimport { type FieldState, resolveFieldState } from './lib/field-state'\nimport { durations } from './lib/motion'\nimport { cn } from './lib/utils'\n\n/** Border + background tint per validation state (checked state uses accent regardless). */\nconst stateTintClasses: Record<Exclude<FieldState, 'default'>, string> = {\n error: 'border-error-7 bg-error-3',\n warning: 'border-warning-7 bg-warning-3',\n success: 'border-success-7 bg-success-3',\n}\n\n/**\n * Props for Checkbox — a Radix-powered accessible checkbox with error state styling and\n * an indeterminate state for \"select all\" row controls.\n *\n * **Indeterminate:** Pass `indeterminate={true}` to show the \"−\" dash (overrides `checked`).\n * This is correct for \"select all\" headers when only some rows are selected.\n *\n * **Controlled:** Use `checked` + `onCheckedChange` for controlled usage.\n * `onCheckedChange` receives `true | false | 'indeterminate'` from Radix.\n *\n * @example\n * // Basic controlled checkbox:\n * <Checkbox checked={agreed} onCheckedChange={(v) => setAgreed(v === true)} />\n *\n * @example\n * // Error state when required checkbox is not checked:\n * <Checkbox state={!termsAccepted ? 'error' : 'default'} checked={termsAccepted} onCheckedChange={(v) => setTerms(v === true)} />\n *\n * @example\n * // \"Select all\" checkbox with indeterminate state:\n * <Checkbox\n * indeterminate={someSelected && !allSelected}\n * checked={allSelected}\n * onCheckedChange={(v) => toggleAll(v === true)}\n * />\n * // These are just a few ways — feel free to combine props creatively!\n */\nexport interface CheckboxProps extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> {\n /** Validation/feedback state. `'error'` also sets `aria-invalid`. Inherited from `FormField` when omitted. */\n state?: FieldState\n indeterminate?: boolean\n /** Control size — `sm` (20px), `md` (24px, default, WCAG compliant), `lg` (28px). */\n size?: 'sm' | 'md' | 'lg'\n}\n\nconst checkboxSizeClasses = {\n sm: 'h-5 w-5',\n md: 'h-6 w-6',\n lg: 'h-7 w-7',\n} as const\n\nconst checkboxIconClasses = {\n sm: 'h-[14px] w-[14px]',\n md: 'h-[18px] w-[18px]',\n lg: 'h-5 w-5',\n} as const\n\nconst Checkbox = React.forwardRef<\n React.ElementRef<typeof CheckboxPrimitive.Root>,\n CheckboxProps\n>(({ className, state: stateProp, indeterminate, size = 'md', checked, onCheckedChange, defaultChecked, ...props }, ref) => {\n // Track internal state so AnimatePresence works for both controlled & uncontrolled usage\n const isControlled = checked !== undefined\n const [internalChecked, setInternalChecked] = React.useState<boolean | 'indeterminate'>(\n indeterminate ? 'indeterminate' : (defaultChecked ? true : false),\n )\n\n const fieldCtx = useFormField()\n const state = resolveFieldState(stateProp, fieldCtx.state)\n const ariaDescribedBy = props['aria-describedby'] ?? fieldCtx.helperTextId\n const ariaRequired = props['aria-required'] ?? fieldCtx.required\n\n const actualChecked = indeterminate ? 'indeterminate' : (isControlled ? checked : internalChecked)\n const isActive = actualChecked === true || actualChecked === 'indeterminate'\n\n const handleCheckedChange = React.useCallback(\n (value: boolean | 'indeterminate') => {\n if (!isControlled) setInternalChecked(value)\n onCheckedChange?.(value)\n },\n [isControlled, onCheckedChange],\n )\n\n return (\n <CheckboxPrimitive.Root\n ref={ref}\n checked={isControlled || indeterminate ? actualChecked : undefined}\n defaultChecked={!isControlled && !indeterminate ? defaultChecked : undefined}\n onCheckedChange={handleCheckedChange}\n className={cn(\n 'touch-target peer flex items-center justify-center shrink-0 rounded-control-inner',\n checkboxSizeClasses[size],\n 'border border-surface-border-interactive',\n 'bg-surface-panel-hover',\n 'transition-colors duration-fast-01 ease-productive-standard',\n 'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-accent-9 focus-visible:ring-offset-2',\n 'disabled:cursor-not-allowed disabled:opacity-action-disabled',\n // Hover darkens the box only. The edge keeps the default border colour\n // (\"changed the surface border colors same as default\", 2026-08-24) —\n // previously it also jumped to accent-7, which read as a pre-selection.\n 'data-[state=unchecked]:hover:bg-neutral-4',\n 'data-[state=checked]:bg-accent-9 data-[state=checked]:border-accent-7 data-[state=checked]:text-accent-fg',\n 'data-[state=indeterminate]:bg-accent-9 data-[state=indeterminate]:border-accent-7 data-[state=indeterminate]:text-accent-fg',\n state && stateTintClasses[state],\n className,\n )}\n aria-invalid={state === 'error' || undefined}\n aria-describedby={ariaDescribedBy}\n aria-required={ariaRequired || undefined}\n {...props}\n // Explicit id wins; otherwise adopt FormField's inputId so <Label htmlFor>\n // resolves. Without this the label renders, points at nothing, and the\n // control has NO accessible name — a screen reader announces an unnamed\n // checkbox even though the consumer wrote FormField + Label correctly.\n id={props.id ?? fieldCtx.inputId}\n >\n <AnimatePresence>\n {isActive && (\n <CheckboxPrimitive.Indicator forceMount asChild>\n <motion.span\n className=\"flex items-center justify-center text-current\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: durations.fast01 }}\n >\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n className={checkboxIconClasses[size]}\n >\n {actualChecked === 'indeterminate' ? (\n <motion.line\n x1=\"4\"\n y1=\"8\"\n x2=\"12\"\n y2=\"8\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n initial={{ pathLength: 0 }}\n animate={{ pathLength: 1 }}\n exit={{ pathLength: 0 }}\n transition={{ duration: durations.moderate02, ease: 'easeOut' }}\n />\n ) : (\n <motion.path\n d=\"M3.5 8.5l3 3 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n fill=\"none\"\n initial={{ pathLength: 0 }}\n animate={{ pathLength: 1 }}\n exit={{ pathLength: 0 }}\n transition={{ duration: durations.moderate02, ease: 'easeOut' }}\n />\n )}\n </svg>\n </motion.span>\n </CheckboxPrimitive.Indicator>\n )}\n </AnimatePresence>\n </CheckboxPrimitive.Root>\n )\n})\nCheckbox.displayName = CheckboxPrimitive.Root.displayName\n\nexport { Checkbox }\n"],"mappings":";;;;;;;;;;AAYA,IAAM,IAAmE;CACvE,OAAO;CACP,SAAS;CACT,SAAS;AACX,GAqCM,IAAsB;CAC1B,IAAI;CACJ,IAAI;CACJ,IAAI;AACN,GAEM,IAAsB;CAC1B,IAAI;CACJ,IAAI;CACJ,IAAI;AACN,GAEM,IAAW,EAAM,YAGpB,EAAE,cAAW,OAAO,GAAW,kBAAe,UAAO,MAAM,YAAS,oBAAiB,mBAAgB,GAAG,KAAS,MAAQ;CAE1H,IAAM,IAAe,MAAY,KAAA,GAC3B,CAAC,GAAiB,KAAsB,EAAM,SAClD,IAAgB,kBAAmB,GACrC,GAEM,IAAW,EAAa,GACxB,IAAQ,EAAkB,GAAW,EAAS,KAAK,GACnD,IAAkB,EAAM,uBAAuB,EAAS,cACxD,IAAe,EAAM,oBAAoB,EAAS,UAElD,IAAgB,IAAgB,kBAAmB,IAAe,IAAU,GAC5E,IAAW,MAAkB,MAAQ,MAAkB,iBAEvD,IAAsB,EAAM,aAC/B,MAAqC;EAEpC,AADK,KAAc,EAAmB,CAAK,GAC3C,IAAkB,CAAK;CACzB,GACA,CAAC,GAAc,CAAe,CAChC;CAEA,OACE,kBAAC,GAAD;EACO;EACL,SAAS,KAAgB,IAAgB,IAAgB,KAAA;EACzD,gBAAgB,CAAC,KAAgB,CAAC,IAAgB,IAAiB,KAAA;EACnE,iBAAiB;EACjB,WAAW,EACT,qFACA,EAAoB,IACpB,4CACA,0BACA,+DACA,6GACA,gEAIA,6CACA,6GACA,+HACA,KAAS,EAAiB,IAC1B,CACF;EACA,gBAAc,MAAU,WAAW,KAAA;EACnC,oBAAkB;EAClB,iBAAe,KAAgB,KAAA;EAC/B,GAAI;EAKJ,IAAI,EAAM,MAAM,EAAS;YAEzB,kBAAC,GAAD,EAAA,UACG,KACC,kBAAC,GAAD;GAA6B,YAAA;GAAW,SAAA;aACtC,kBAAC,EAAO,MAAR;IACE,WAAU;IACV,SAAS,EAAE,SAAS,EAAE;IACtB,SAAS,EAAE,SAAS,EAAE;IACtB,MAAM,EAAE,SAAS,EAAE;IACnB,YAAY,EAAE,UAAU,EAAU,OAAO;cAEzC,kBAAC,OAAD;KACE,OAAM;KACN,QAAO;KACP,SAAQ;KACR,MAAK;KACL,WAAW,EAAoB;eAE9B,MAAkB,kBACjB,kBAAC,EAAO,MAAR;MACE,IAAG;MACH,IAAG;MACH,IAAG;MACH,IAAG;MACH,QAAO;MACP,aAAY;MACZ,eAAc;MACd,SAAS,EAAE,YAAY,EAAE;MACzB,SAAS,EAAE,YAAY,EAAE;MACzB,MAAM,EAAE,YAAY,EAAE;MACtB,YAAY;OAAE,UAAU,EAAU;OAAY,MAAM;MAAU;KAC/D,CAAA,IAED,kBAAC,EAAO,MAAR;MACE,GAAE;MACF,QAAO;MACP,aAAY;MACZ,eAAc;MACd,gBAAe;MACf,MAAK;MACL,SAAS,EAAE,YAAY,EAAE;MACzB,SAAS,EAAE,YAAY,EAAE;MACzB,MAAM,EAAE,YAAY,EAAE;MACtB,YAAY;OAAE,UAAU,EAAU;OAAY,MAAM;MAAU;KAC/D,CAAA;IAEA,CAAA;GACM,CAAA;EACc,CAAA,EAEhB,CAAA;CACK,CAAA;AAE5B,CAAC;AACD,EAAS,cAAA,EAAqC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"combobox.d.ts","sourceRoot":"","sources":["../../src/ui/combobox.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEjE,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAM9B,OAAO,EAAE,KAAK,UAAU,EAAqB,MAAM,mBAAmB,CAAA;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"combobox.d.ts","sourceRoot":"","sources":["../../src/ui/combobox.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEjE,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAM9B,OAAO,EAAE,KAAK,UAAU,EAAqB,MAAM,mBAAmB,CAAA;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAajD,eAAO,MAAM,uBAAuB;;8EAuBnC,CAAA;AAED,MAAM,MAAM,YAAY,GAAG,WAAW,CAAC,YAAY,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAA;AAE5F,0EAA0E;AAC1E,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,QAAQ,CAAA;AAqCnD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,UAAU,iBAAkB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC;IACxF,+CAA+C;IAC/C,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,uDAAuD;IACvD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,oGAAoG;IACpG,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAA;IAC7E,4FAA4F;IAC5F,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,iFAAiF;IACjF,IAAI,CAAC,EAAE,YAAY,CAAA;IACnB,8GAA8G;IAC9G,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAA;CAC5B;AAED,UAAU,mBAAoB,SAAQ,iBAAiB;IACrD,QAAQ,CAAC,EAAE,KAAK,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACvC;AAED,UAAU,qBAAsB,SAAQ,iBAAiB;IACvD,QAAQ,EAAE,IAAI,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;CACzC;AAED,MAAM,MAAM,aAAa,GAAG,mBAAmB,GAAG,qBAAqB,CAAA;AAQvE,QAAA,MAAM,QAAQ,sFA6ab,CAAA;AAGD,OAAO,EAAE,QAAQ,EAAE,CAAA"}
|