@open-mercato/ui 0.7.1-develop.7193.1.910a5b0a1e → 0.7.1-develop.7195.1.b5efd1d51a

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 (49) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/backend/AppShell.js +66 -7
  3. package/dist/backend/AppShell.js.map +2 -2
  4. package/dist/backend/CrudForm.js +14 -8
  5. package/dist/backend/CrudForm.js.map +2 -2
  6. package/dist/backend/DataTable.js +108 -11
  7. package/dist/backend/DataTable.js.map +2 -2
  8. package/dist/backend/forms/ActionsDropdown.js +35 -5
  9. package/dist/backend/forms/ActionsDropdown.js.map +2 -2
  10. package/dist/backend/forms/index.js.map +2 -2
  11. package/dist/backend/icons/lucideRegistry.generated.js +6 -0
  12. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  13. package/dist/backend/injection/spotIds.js +2 -0
  14. package/dist/backend/injection/spotIds.js.map +2 -2
  15. package/dist/backend/inputs/DurationInput.js +182 -0
  16. package/dist/backend/inputs/DurationInput.js.map +7 -0
  17. package/dist/backend/inputs/EventSelect.js.map +2 -2
  18. package/dist/backend/inputs/index.js +1 -0
  19. package/dist/backend/inputs/index.js.map +2 -2
  20. package/dist/backend/nav/navBadges.js +38 -0
  21. package/dist/backend/nav/navBadges.js.map +7 -0
  22. package/dist/primitives/segmented-control.js +4 -4
  23. package/dist/primitives/segmented-control.js.map +1 -1
  24. package/dist/primitives/sheet.js +21 -11
  25. package/dist/primitives/sheet.js.map +2 -2
  26. package/dist/primitives/textarea.js +54 -4
  27. package/dist/primitives/textarea.js.map +2 -2
  28. package/package.json +3 -3
  29. package/src/backend/AppShell.tsx +106 -2
  30. package/src/backend/CrudForm.tsx +28 -9
  31. package/src/backend/DataTable.tsx +176 -22
  32. package/src/backend/__tests__/CrudForm.density.test.tsx +113 -0
  33. package/src/backend/__tests__/DataTable.propBulkActions.test.tsx +200 -0
  34. package/src/backend/__tests__/DataTable.render.test.tsx +76 -0
  35. package/src/backend/__tests__/navBadges.test.ts +57 -0
  36. package/src/backend/forms/ActionsDropdown.tsx +73 -6
  37. package/src/backend/forms/index.ts +1 -1
  38. package/src/backend/icons/lucideRegistry.generated.tsx +6 -0
  39. package/src/backend/injection/spotIds.ts +8 -0
  40. package/src/backend/inputs/DurationInput.tsx +207 -0
  41. package/src/backend/inputs/EventSelect.tsx +2 -0
  42. package/src/backend/inputs/__tests__/DurationInput.test.tsx +220 -0
  43. package/src/backend/inputs/index.ts +1 -0
  44. package/src/backend/nav/navBadges.ts +61 -0
  45. package/src/primitives/__tests__/segmented-control.test.tsx +2 -2
  46. package/src/primitives/__tests__/textarea.test.tsx +50 -0
  47. package/src/primitives/segmented-control.tsx +7 -7
  48. package/src/primitives/sheet.tsx +7 -1
  49. package/src/primitives/textarea.tsx +77 -4
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ const badges = /* @__PURE__ */ new Map();
4
+ const listeners = /* @__PURE__ */ new Set();
5
+ function emit() {
6
+ for (const listener of listeners) listener();
7
+ }
8
+ function setNavBadge(href, badge) {
9
+ if (!href) return;
10
+ if (!badge || !Number.isFinite(badge.count) || badge.count <= 0) {
11
+ if (badges.delete(href)) emit();
12
+ return;
13
+ }
14
+ const current = badges.get(href);
15
+ if (current && current.count === badge.count && current.tone === badge.tone && current.label === badge.label) return;
16
+ badges.set(href, badge);
17
+ emit();
18
+ }
19
+ function clearNavBadge(href) {
20
+ setNavBadge(href, null);
21
+ }
22
+ function subscribe(listener) {
23
+ listeners.add(listener);
24
+ return () => {
25
+ listeners.delete(listener);
26
+ };
27
+ }
28
+ function useNavBadge(href) {
29
+ const getSnapshot = React.useCallback(() => badges.get(href) ?? null, [href]);
30
+ const getServerSnapshot = React.useCallback(() => null, []);
31
+ return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
32
+ }
33
+ export {
34
+ clearNavBadge,
35
+ setNavBadge,
36
+ useNavBadge
37
+ };
38
+ //# sourceMappingURL=navBadges.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/nav/navBadges.ts"],
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\n\n/**\n * Live counts rendered on sidebar nav items (\"3 waiting for you\").\n *\n * Why this is a client-side store rather than a field on the chrome payload:\n * `/api/auth/admin/nav` is cached for 30 minutes behind a module-surface\n * fingerprint, so a per-user count served through it would be both stale and\n * cache-poisoning. Core owns the capability and the rendering; whichever module\n * owns the number mounts a headless widget into `shell:sidebar:badges` and\n * publishes here.\n *\n * Counts are advisory: a nav item renders exactly what it is given, and an\n * absent or zero count renders nothing at all \u2014 never a \"0\" chip.\n */\nexport type NavBadgeTone = 'default' | 'attention'\n\nexport type NavBadge = {\n count: number\n tone?: NavBadgeTone\n /** Accessible label; falls back to the raw count when omitted. */\n label?: string\n}\n\nconst badges = new Map<string, NavBadge>()\nconst listeners = new Set<() => void>()\n\nfunction emit(): void {\n for (const listener of listeners) listener()\n}\n\nexport function setNavBadge(href: string, badge: NavBadge | null): void {\n if (!href) return\n if (!badge || !Number.isFinite(badge.count) || badge.count <= 0) {\n if (badges.delete(href)) emit()\n return\n }\n const current = badges.get(href)\n if (current && current.count === badge.count && current.tone === badge.tone && current.label === badge.label) return\n badges.set(href, badge)\n emit()\n}\n\nexport function clearNavBadge(href: string): void {\n setNavBadge(href, null)\n}\n\nfunction subscribe(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\nexport function useNavBadge(href: string): NavBadge | null {\n const getSnapshot = React.useCallback(() => badges.get(href) ?? null, [href])\n const getServerSnapshot = React.useCallback(() => null, [])\n return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)\n}\n"],
5
+ "mappings": ";AAEA,YAAY,WAAW;AAwBvB,MAAM,SAAS,oBAAI,IAAsB;AACzC,MAAM,YAAY,oBAAI,IAAgB;AAEtC,SAAS,OAAa;AACpB,aAAW,YAAY,UAAW,UAAS;AAC7C;AAEO,SAAS,YAAY,MAAc,OAA8B;AACtE,MAAI,CAAC,KAAM;AACX,MAAI,CAAC,SAAS,CAAC,OAAO,SAAS,MAAM,KAAK,KAAK,MAAM,SAAS,GAAG;AAC/D,QAAI,OAAO,OAAO,IAAI,EAAG,MAAK;AAC9B;AAAA,EACF;AACA,QAAM,UAAU,OAAO,IAAI,IAAI;AAC/B,MAAI,WAAW,QAAQ,UAAU,MAAM,SAAS,QAAQ,SAAS,MAAM,QAAQ,QAAQ,UAAU,MAAM,MAAO;AAC9G,SAAO,IAAI,MAAM,KAAK;AACtB,OAAK;AACP;AAEO,SAAS,cAAc,MAAoB;AAChD,cAAY,MAAM,IAAI;AACxB;AAEA,SAAS,UAAU,UAAkC;AACnD,YAAU,IAAI,QAAQ;AACtB,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAEO,SAAS,YAAY,MAA+B;AACzD,QAAM,cAAc,MAAM,YAAY,MAAM,OAAO,IAAI,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC;AAC5E,QAAM,oBAAoB,MAAM,YAAY,MAAM,MAAM,CAAC,CAAC;AAC1D,SAAO,MAAM,qBAAqB,WAAW,aAAa,iBAAiB;AAC7E;",
6
+ "names": []
7
+ }
@@ -9,8 +9,8 @@ const SegmentedControlContext = React.createContext({
9
9
  disabled: false
10
10
  });
11
11
  const trackVariants = cva(
12
- // Pill-shaped track with subtle inner padding so selected items render
13
- // a smaller inner pill (the iOS slide-thumb effect). Track is muted —
12
+ // Rounded-rectangle track (DS Open Mercato node 2604:114) with subtle inner
13
+ // padding so the selected item renders as a smaller inner card. Track is muted —
14
14
  // selected item raises with bg-background + shadow-sm. We use full
15
15
  // `bg-muted` (not /40) so the contrast between track and a selected
16
16
  // bg-background item stays visible in the light theme; in dark mode
@@ -21,7 +21,7 @@ const trackVariants = cva(
21
21
  // sm → track h-7 (28px) − 2px border − 2px padding (p-px ×2) = 24px → matches item h-6
22
22
  // Using `p-0.5` (2px each side = 4px total) instead breaks both sizes by
23
23
  // 2px so the selected pill clips top and bottom against the track border.
24
- "inline-flex w-fit gap-0 rounded-full border border-input bg-muted p-px transition-colors",
24
+ "inline-flex w-fit gap-0 rounded-lg border border-input bg-muted p-px transition-colors",
25
25
  {
26
26
  variants: {
27
27
  size: {
@@ -46,7 +46,7 @@ const itemVariants = cva(
46
46
  // shadow, important when the surrounding theme is high-key and the
47
47
  // shadow alone is hard to read). Unselected text is muted; hover
48
48
  // only nudges color (no bg change — keeps the track flat).
49
- "inline-flex items-center justify-center rounded-full font-medium transition-all outline-none focus-visible:shadow-focus disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-background data-[state=checked]:text-foreground data-[state=checked]:font-semibold data-[state=checked]:shadow-sm data-[state=unchecked]:bg-transparent data-[state=unchecked]:text-muted-foreground data-[state=unchecked]:hover:text-foreground",
49
+ "inline-flex items-center justify-center rounded-md font-medium transition-all outline-none focus-visible:shadow-focus disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-background data-[state=checked]:text-foreground data-[state=checked]:font-semibold data-[state=checked]:shadow-sm data-[state=unchecked]:bg-transparent data-[state=unchecked]:text-muted-foreground data-[state=unchecked]:hover:text-foreground",
50
50
  {
51
51
  variants: {
52
52
  size: {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/primitives/segmented-control.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport * as RadioGroupPrimitive from '@radix-ui/react-radio-group'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\n\n/**\n * iOS-style segmented control per Figma `Switch / Chart / Cryptocurrency`\n * (component set id `199963:1442` in DS Open Mercato). Renders a single\n * track with N items where exactly one is selected at a time. Selecting\n * a new item fires `onValueChange`.\n *\n * Use for **mutually-exclusive view state** \u2014 list page filters like\n * \"All / Active / Archived\", chart period selectors, layout toggles\n * (List / Grid). For *related actions* (each does something different),\n * reach for `ButtonGroup` instead.\n *\n * Built on Radix `RadioGroup` so we inherit the radio-group ARIA contract\n * (`role=\"radiogroup\"`, `role=\"radio\"` on items, arrow-key navigation,\n * roving tabindex) for free. No new dependency \u2014 Radix RadioGroup is\n * already installed via the `Radio` primitive.\n *\n * ```tsx\n * const [view, setView] = React.useState('all')\n * <SegmentedControl value={view} onValueChange={setView} aria-label=\"View filter\">\n * <SegmentedControlItem value=\"all\">All</SegmentedControlItem>\n * <SegmentedControlItem value=\"active\">Active</SegmentedControlItem>\n * <SegmentedControlItem value=\"archived\">Archived</SegmentedControlItem>\n * </SegmentedControl>\n * ```\n *\n * Sizes:\n * - `default` (h-8 / 32px) \u2014 standard toolbar density.\n * - `sm` (h-7 / 28px) \u2014 tighter; pair with `text-xs`.\n */\n\ntype SegmentedControlContextValue = {\n size: 'sm' | 'default'\n disabled?: boolean\n}\n\nconst SegmentedControlContext = React.createContext<SegmentedControlContextValue>({\n size: 'default',\n disabled: false,\n})\n\nconst trackVariants = cva(\n // Pill-shaped track with subtle inner padding so selected items render\n // a smaller inner pill (the iOS slide-thumb effect). Track is muted \u2014\n // selected item raises with bg-background + shadow-sm. We use full\n // `bg-muted` (not /40) so the contrast between track and a selected\n // bg-background item stays visible in the light theme; in dark mode\n // the token already darkens further so the contrast holds.\n //\n // Height math (box-border on every element):\n // default \u2192 track h-8 (32px) \u2212 2px border \u2212 2px padding (p-px \u00D72) = 28px \u2192 matches item h-7\n // sm \u2192 track h-7 (28px) \u2212 2px border \u2212 2px padding (p-px \u00D72) = 24px \u2192 matches item h-6\n // Using `p-0.5` (2px each side = 4px total) instead breaks both sizes by\n // 2px so the selected pill clips top and bottom against the track border.\n 'inline-flex w-fit gap-0 rounded-full border border-input bg-muted p-px transition-colors',\n {\n variants: {\n size: {\n sm: 'h-7',\n default: 'h-8',\n },\n disabled: {\n true: 'cursor-not-allowed opacity-60',\n false: '',\n },\n },\n defaultVariants: {\n size: 'default',\n disabled: false,\n },\n },\n)\n\nconst itemVariants = cva(\n // Items are pills that fill the track minus its 2px inner padding.\n // Selected state lifts via bg-background + shadow-sm + font-semibold\n // (the font-weight bump gives a secondary visual cue beyond color +\n // shadow, important when the surrounding theme is high-key and the\n // shadow alone is hard to read). Unselected text is muted; hover\n // only nudges color (no bg change \u2014 keeps the track flat).\n 'inline-flex items-center justify-center rounded-full font-medium ' +\n 'transition-all outline-none focus-visible:shadow-focus ' +\n 'disabled:cursor-not-allowed disabled:opacity-50 ' +\n 'data-[state=checked]:bg-background data-[state=checked]:text-foreground data-[state=checked]:font-semibold data-[state=checked]:shadow-sm ' +\n 'data-[state=unchecked]:bg-transparent data-[state=unchecked]:text-muted-foreground data-[state=unchecked]:hover:text-foreground',\n {\n variants: {\n size: {\n sm: 'h-6 px-2.5 text-xs',\n default: 'h-7 px-3 text-sm',\n },\n },\n defaultVariants: {\n size: 'default',\n },\n },\n)\n\nexport type SegmentedControlProps = Omit<\n React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>,\n 'orientation'\n> &\n VariantProps<typeof trackVariants> & {\n /** Optional screen-reader label for the radio group. */\n 'aria-label'?: string\n }\n\nexport const SegmentedControl = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Root>,\n SegmentedControlProps\n>(({ className, size, disabled, children, ...props }, ref) => {\n const ctx = React.useMemo<SegmentedControlContextValue>(\n () => ({ size: size ?? 'default', disabled: disabled ?? false }),\n [size, disabled],\n )\n return (\n <SegmentedControlContext.Provider value={ctx}>\n <RadioGroupPrimitive.Root\n ref={ref}\n orientation=\"horizontal\"\n disabled={disabled ?? undefined}\n data-slot=\"segmented-control\"\n className={cn(trackVariants({ size, disabled }), className)}\n {...props}\n >\n {children}\n </RadioGroupPrimitive.Root>\n </SegmentedControlContext.Provider>\n )\n})\nSegmentedControl.displayName = 'SegmentedControl'\n\nexport type SegmentedControlItemProps = React.ComponentPropsWithoutRef<\n typeof RadioGroupPrimitive.Item\n>\n\nexport const SegmentedControlItem = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Item>,\n SegmentedControlItemProps\n>(({ className, children, ...props }, ref) => {\n const { size } = React.useContext(SegmentedControlContext)\n return (\n <RadioGroupPrimitive.Item\n ref={ref}\n data-slot=\"segmented-control-item\"\n className={cn(itemVariants({ size }), className)}\n {...props}\n >\n {children}\n </RadioGroupPrimitive.Item>\n )\n})\nSegmentedControlItem.displayName = 'SegmentedControlItem'\n\nexport { trackVariants as segmentedControlTrackVariants, itemVariants as segmentedControlItemVariants }\n"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport * as RadioGroupPrimitive from '@radix-ui/react-radio-group'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\n\n/**\n * Segmented control per Figma DS Open Mercato (node `2604:114`): a muted\n * rounded-rectangle track with a raised white card for the selected segment.\n * Renders a single track with N items where exactly one is selected at a time. Selecting\n * a new item fires `onValueChange`.\n *\n * Use for **mutually-exclusive view state** \u2014 list page filters like\n * \"All / Active / Archived\", chart period selectors, layout toggles\n * (List / Grid). For *related actions* (each does something different),\n * reach for `ButtonGroup` instead.\n *\n * Built on Radix `RadioGroup` so we inherit the radio-group ARIA contract\n * (`role=\"radiogroup\"`, `role=\"radio\"` on items, arrow-key navigation,\n * roving tabindex) for free. No new dependency \u2014 Radix RadioGroup is\n * already installed via the `Radio` primitive.\n *\n * ```tsx\n * const [view, setView] = React.useState('all')\n * <SegmentedControl value={view} onValueChange={setView} aria-label=\"View filter\">\n * <SegmentedControlItem value=\"all\">All</SegmentedControlItem>\n * <SegmentedControlItem value=\"active\">Active</SegmentedControlItem>\n * <SegmentedControlItem value=\"archived\">Archived</SegmentedControlItem>\n * </SegmentedControl>\n * ```\n *\n * Sizes:\n * - `default` (h-8 / 32px) \u2014 standard toolbar density.\n * - `sm` (h-7 / 28px) \u2014 tighter; pair with `text-xs`.\n */\n\ntype SegmentedControlContextValue = {\n size: 'sm' | 'default'\n disabled?: boolean\n}\n\nconst SegmentedControlContext = React.createContext<SegmentedControlContextValue>({\n size: 'default',\n disabled: false,\n})\n\nconst trackVariants = cva(\n // Rounded-rectangle track (DS Open Mercato node 2604:114) with subtle inner\n // padding so the selected item renders as a smaller inner card. Track is muted \u2014\n // selected item raises with bg-background + shadow-sm. We use full\n // `bg-muted` (not /40) so the contrast between track and a selected\n // bg-background item stays visible in the light theme; in dark mode\n // the token already darkens further so the contrast holds.\n //\n // Height math (box-border on every element):\n // default \u2192 track h-8 (32px) \u2212 2px border \u2212 2px padding (p-px \u00D72) = 28px \u2192 matches item h-7\n // sm \u2192 track h-7 (28px) \u2212 2px border \u2212 2px padding (p-px \u00D72) = 24px \u2192 matches item h-6\n // Using `p-0.5` (2px each side = 4px total) instead breaks both sizes by\n // 2px so the selected pill clips top and bottom against the track border.\n 'inline-flex w-fit gap-0 rounded-lg border border-input bg-muted p-px transition-colors',\n {\n variants: {\n size: {\n sm: 'h-7',\n default: 'h-8',\n },\n disabled: {\n true: 'cursor-not-allowed opacity-60',\n false: '',\n },\n },\n defaultVariants: {\n size: 'default',\n disabled: false,\n },\n },\n)\n\nconst itemVariants = cva(\n // Items are pills that fill the track minus its 2px inner padding.\n // Selected state lifts via bg-background + shadow-sm + font-semibold\n // (the font-weight bump gives a secondary visual cue beyond color +\n // shadow, important when the surrounding theme is high-key and the\n // shadow alone is hard to read). Unselected text is muted; hover\n // only nudges color (no bg change \u2014 keeps the track flat).\n 'inline-flex items-center justify-center rounded-md font-medium ' +\n 'transition-all outline-none focus-visible:shadow-focus ' +\n 'disabled:cursor-not-allowed disabled:opacity-50 ' +\n 'data-[state=checked]:bg-background data-[state=checked]:text-foreground data-[state=checked]:font-semibold data-[state=checked]:shadow-sm ' +\n 'data-[state=unchecked]:bg-transparent data-[state=unchecked]:text-muted-foreground data-[state=unchecked]:hover:text-foreground',\n {\n variants: {\n size: {\n sm: 'h-6 px-2.5 text-xs',\n default: 'h-7 px-3 text-sm',\n },\n },\n defaultVariants: {\n size: 'default',\n },\n },\n)\n\nexport type SegmentedControlProps = Omit<\n React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>,\n 'orientation'\n> &\n VariantProps<typeof trackVariants> & {\n /** Optional screen-reader label for the radio group. */\n 'aria-label'?: string\n }\n\nexport const SegmentedControl = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Root>,\n SegmentedControlProps\n>(({ className, size, disabled, children, ...props }, ref) => {\n const ctx = React.useMemo<SegmentedControlContextValue>(\n () => ({ size: size ?? 'default', disabled: disabled ?? false }),\n [size, disabled],\n )\n return (\n <SegmentedControlContext.Provider value={ctx}>\n <RadioGroupPrimitive.Root\n ref={ref}\n orientation=\"horizontal\"\n disabled={disabled ?? undefined}\n data-slot=\"segmented-control\"\n className={cn(trackVariants({ size, disabled }), className)}\n {...props}\n >\n {children}\n </RadioGroupPrimitive.Root>\n </SegmentedControlContext.Provider>\n )\n})\nSegmentedControl.displayName = 'SegmentedControl'\n\nexport type SegmentedControlItemProps = React.ComponentPropsWithoutRef<\n typeof RadioGroupPrimitive.Item\n>\n\nexport const SegmentedControlItem = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Item>,\n SegmentedControlItemProps\n>(({ className, children, ...props }, ref) => {\n const { size } = React.useContext(SegmentedControlContext)\n return (\n <RadioGroupPrimitive.Item\n ref={ref}\n data-slot=\"segmented-control-item\"\n className={cn(itemVariants({ size }), className)}\n {...props}\n >\n {children}\n </RadioGroupPrimitive.Item>\n )\n})\nSegmentedControlItem.displayName = 'SegmentedControlItem'\n\nexport { trackVariants as segmentedControlTrackVariants, itemVariants as segmentedControlItemVariants }\n"],
5
5
  "mappings": ";AA4HM;AA1HN,YAAY,WAAW;AACvB,YAAY,yBAAyB;AACrC,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAqCnB,MAAM,0BAA0B,MAAM,cAA4C;AAAA,EAChF,MAAM;AAAA,EACN,UAAU;AACZ,CAAC;AAED,MAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAapB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,SAAS;AAAA,MACX;AAAA,MACA,UAAU;AAAA,QACR,MAAM;AAAA,QACN,OAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,MAAM;AAAA,MACN,UAAU;AAAA,IACZ;AAAA,EACF;AACF;AAEA,MAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOnB;AAAA,EAKA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,SAAS;AAAA,MACX;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAWO,MAAM,mBAAmB,MAAM,WAGpC,CAAC,EAAE,WAAW,MAAM,UAAU,UAAU,GAAG,MAAM,GAAG,QAAQ;AAC5D,QAAM,MAAM,MAAM;AAAA,IAChB,OAAO,EAAE,MAAM,QAAQ,WAAW,UAAU,YAAY,MAAM;AAAA,IAC9D,CAAC,MAAM,QAAQ;AAAA,EACjB;AACA,SACE,oBAAC,wBAAwB,UAAxB,EAAiC,OAAO,KACvC;AAAA,IAAC,oBAAoB;AAAA,IAApB;AAAA,MACC;AAAA,MACA,aAAY;AAAA,MACZ,UAAU,YAAY;AAAA,MACtB,aAAU;AAAA,MACV,WAAW,GAAG,cAAc,EAAE,MAAM,SAAS,CAAC,GAAG,SAAS;AAAA,MACzD,GAAG;AAAA,MAEH;AAAA;AAAA,EACH,GACF;AAEJ,CAAC;AACD,iBAAiB,cAAc;AAMxB,MAAM,uBAAuB,MAAM,WAGxC,CAAC,EAAE,WAAW,UAAU,GAAG,MAAM,GAAG,QAAQ;AAC5C,QAAM,EAAE,KAAK,IAAI,MAAM,WAAW,uBAAuB;AACzD,SACE;AAAA,IAAC,oBAAoB;AAAA,IAApB;AAAA,MACC;AAAA,MACA,aAAU;AAAA,MACV,WAAW,GAAG,aAAa,EAAE,KAAK,CAAC,GAAG,SAAS;AAAA,MAC9C,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ,CAAC;AACD,qBAAqB,cAAc;",
6
6
  "names": []
7
7
  }
@@ -12,6 +12,7 @@ const SheetPortal = DialogPrimitive.Portal;
12
12
  const SheetOverlay = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
13
13
  DialogPrimitive.Overlay,
14
14
  {
15
+ "data-slot": "sheet-overlay",
15
16
  className: cn(
16
17
  "fixed inset-x-0 bottom-0 top-[var(--topbar-height,0px)] z-overlay bg-black/40 backdrop-blur-sm",
17
18
  "data-[state=open]:animate-in data-[state=closed]:animate-out",
@@ -45,17 +46,26 @@ const sheetVariants = cva(
45
46
  );
46
47
  const SheetContent = React.forwardRef(({ side = "right", className, children, hideClose = false, closeLabel = "Close", ...props }, ref) => /* @__PURE__ */ jsxs(SheetPortal, { children: [
47
48
  /* @__PURE__ */ jsx(SheetOverlay, {}),
48
- /* @__PURE__ */ jsxs(DialogPrimitive.Content, { ref, className: cn(sheetVariants({ side }), className), ...props, children: [
49
- children,
50
- !hideClose ? /* @__PURE__ */ jsx(
51
- DialogPrimitive.Close,
52
- {
53
- className: "absolute right-4 top-4 inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:shadow-focus",
54
- "aria-label": closeLabel,
55
- children: /* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": "true" })
56
- }
57
- ) : null
58
- ] })
49
+ /* @__PURE__ */ jsxs(
50
+ DialogPrimitive.Content,
51
+ {
52
+ ref,
53
+ "data-slot": "sheet-content",
54
+ className: cn(sheetVariants({ side }), className),
55
+ ...props,
56
+ children: [
57
+ children,
58
+ !hideClose ? /* @__PURE__ */ jsx(
59
+ DialogPrimitive.Close,
60
+ {
61
+ className: "absolute right-4 top-4 inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:shadow-focus",
62
+ "aria-label": closeLabel,
63
+ children: /* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": "true" })
64
+ }
65
+ ) : null
66
+ ]
67
+ }
68
+ )
59
69
  ] }));
60
70
  SheetContent.displayName = DialogPrimitive.Content.displayName;
61
71
  const SheetHeader = React.forwardRef(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/primitives/sheet.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { X } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nconst Sheet = DialogPrimitive.Root\n\nconst SheetTrigger = DialogPrimitive.Trigger\n\nconst SheetClose = DialogPrimitive.Close\n\nconst SheetPortal = DialogPrimitive.Portal\n\nconst SheetOverlay = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n className={cn(\n 'fixed inset-x-0 bottom-0 top-[var(--topbar-height,0px)] z-overlay bg-black/40 backdrop-blur-sm',\n 'data-[state=open]:animate-in data-[state=closed]:animate-out',\n 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n className,\n )}\n {...props}\n ref={ref}\n />\n))\nSheetOverlay.displayName = DialogPrimitive.Overlay.displayName\n\nconst sheetVariants = cva(\n cn(\n 'fixed z-modal flex flex-col gap-4 bg-background shadow-lg transition ease-in-out',\n 'data-[state=open]:animate-in data-[state=closed]:animate-out',\n 'data-[state=closed]:duration-200 data-[state=open]:duration-300',\n ),\n {\n variants: {\n side: {\n top:\n 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',\n bottom:\n 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',\n left:\n 'top-[var(--topbar-height,0px)] bottom-0 left-0 w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-md',\n right:\n 'top-[var(--topbar-height,0px)] bottom-0 right-0 w-full sm:max-w-md border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',\n },\n },\n defaultVariants: {\n side: 'right',\n },\n }\n)\n\nexport type SheetContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &\n VariantProps<typeof sheetVariants> & {\n /** Hide the built-in close (X) button \u2014 useful when the consumer renders its own close affordance. */\n hideClose?: boolean\n /** Override the aria-label for the built-in close button. */\n closeLabel?: string\n }\n\n/**\n * SheetContent \u2014 side-anchored Radix Dialog wrapper.\n *\n * Topbar integration: the `left` and `right` variants anchor at\n * `top: var(--topbar-height, 0px)` so a sticky app topbar stays visible above\n * the panel. Consumers that render inside `AppShell` get the correct offset\n * because the shell sets `--topbar-height` on the outer container. Surfaces\n * outside the shell (portal modals, marketing pages, standalone embeds) keep\n * the default `0px` fallback and align to the viewport top \u2014 that is the\n * correct behavior when there is no topbar to clear.\n *\n * To opt into the offset from your own layout, set the CSS variable on any\n * ancestor of `<SheetContent>`:\n *\n * ```css\n * .my-shell { --topbar-height: 60px; }\n * ```\n */\n\nconst SheetContent = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Content>,\n SheetContentProps\n>(({ side = 'right', className, children, hideClose = false, closeLabel = 'Close', ...props }, ref) => (\n <SheetPortal>\n <SheetOverlay />\n <DialogPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>\n {children}\n {!hideClose ? (\n <DialogPrimitive.Close\n className=\"absolute right-4 top-4 inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:shadow-focus\"\n aria-label={closeLabel}\n >\n <X className=\"size-4\" aria-hidden=\"true\" />\n </DialogPrimitive.Close>\n ) : null}\n </DialogPrimitive.Content>\n </SheetPortal>\n))\nSheetContent.displayName = DialogPrimitive.Content.displayName\n\nconst SheetHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn('flex flex-col gap-1.5 border-b px-4 py-3 pr-12 text-left', className)}\n {...props}\n />\n ),\n)\nSheetHeader.displayName = 'SheetHeader'\n\nconst SheetFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn('flex flex-col-reverse gap-2 border-t px-4 py-3 sm:flex-row sm:justify-end', className)}\n {...props}\n />\n ),\n)\nSheetFooter.displayName = 'SheetFooter'\n\nconst SheetTitle = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn('text-base font-semibold text-foreground', className)}\n {...props}\n />\n))\nSheetTitle.displayName = DialogPrimitive.Title.displayName\n\nconst SheetDescription = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn('text-sm text-muted-foreground', className)}\n {...props}\n />\n))\nSheetDescription.displayName = DialogPrimitive.Description.displayName\n\nexport {\n Sheet,\n SheetClose,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetOverlay,\n SheetPortal,\n SheetTitle,\n SheetTrigger,\n sheetVariants,\n}\n"],
5
- "mappings": ";AAoBE,cAuEE,YAvEF;AAlBF,YAAY,WAAW;AACvB,YAAY,qBAAqB;AACjC,SAAS,WAA8B;AACvC,SAAS,SAAS;AAClB,SAAS,UAAU;AAEnB,MAAM,QAAQ,gBAAgB;AAE9B,MAAM,eAAe,gBAAgB;AAErC,MAAM,aAAa,gBAAgB;AAEnC,MAAM,cAAc,gBAAgB;AAEpC,MAAM,eAAe,MAAM,WAGzB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACC,GAAG;AAAA,IACJ;AAAA;AACF,CACD;AACD,aAAa,cAAc,gBAAgB,QAAQ;AAEnD,MAAM,gBAAgB;AAAA,EACpB;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,KACE;AAAA,QACF,QACE;AAAA,QACF,MACE;AAAA,QACF,OACE;AAAA,MACJ;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,MAAM;AAAA,IACR;AAAA,EACF;AACF;AA6BA,MAAM,eAAe,MAAM,WAGzB,CAAC,EAAE,OAAO,SAAS,WAAW,UAAU,YAAY,OAAO,aAAa,SAAS,GAAG,MAAM,GAAG,QAC7F,qBAAC,eACC;AAAA,sBAAC,gBAAa;AAAA,EACd,qBAAC,gBAAgB,SAAhB,EAAwB,KAAU,WAAW,GAAG,cAAc,EAAE,KAAK,CAAC,GAAG,SAAS,GAAI,GAAG,OACvF;AAAA;AAAA,IACA,CAAC,YACA;AAAA,MAAC,gBAAgB;AAAA,MAAhB;AAAA,QACC,WAAU;AAAA,QACV,cAAY;AAAA,QAEZ,8BAAC,KAAE,WAAU,UAAS,eAAY,QAAO;AAAA;AAAA,IAC3C,IACE;AAAA,KACN;AAAA,GACF,CACD;AACD,aAAa,cAAc,gBAAgB,QAAQ;AAEnD,MAAM,cAAc,MAAM;AAAA,EACxB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QACxB;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAW,GAAG,4DAA4D,SAAS;AAAA,MAClF,GAAG;AAAA;AAAA,EACN;AAEJ;AACA,YAAY,cAAc;AAE1B,MAAM,cAAc,MAAM;AAAA,EACxB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QACxB;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAW,GAAG,6EAA6E,SAAS;AAAA,MACnG,GAAG;AAAA;AAAA,EACN;AAEJ;AACA,YAAY,cAAc;AAE1B,MAAM,aAAa,MAAM,WAGvB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IACA,WAAW,GAAG,2CAA2C,SAAS;AAAA,IACjE,GAAG;AAAA;AACN,CACD;AACD,WAAW,cAAc,gBAAgB,MAAM;AAE/C,MAAM,mBAAmB,MAAM,WAG7B,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IACA,WAAW,GAAG,iCAAiC,SAAS;AAAA,IACvD,GAAG;AAAA;AACN,CACD;AACD,iBAAiB,cAAc,gBAAgB,YAAY;",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { X } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nconst Sheet = DialogPrimitive.Root\n\nconst SheetTrigger = DialogPrimitive.Trigger\n\nconst SheetClose = DialogPrimitive.Close\n\nconst SheetPortal = DialogPrimitive.Portal\n\nconst SheetOverlay = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n data-slot=\"sheet-overlay\"\n className={cn(\n 'fixed inset-x-0 bottom-0 top-[var(--topbar-height,0px)] z-overlay bg-black/40 backdrop-blur-sm',\n 'data-[state=open]:animate-in data-[state=closed]:animate-out',\n 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n className,\n )}\n {...props}\n ref={ref}\n />\n))\nSheetOverlay.displayName = DialogPrimitive.Overlay.displayName\n\nconst sheetVariants = cva(\n cn(\n 'fixed z-modal flex flex-col gap-4 bg-background shadow-lg transition ease-in-out',\n 'data-[state=open]:animate-in data-[state=closed]:animate-out',\n 'data-[state=closed]:duration-200 data-[state=open]:duration-300',\n ),\n {\n variants: {\n side: {\n top:\n 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',\n bottom:\n 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',\n left:\n 'top-[var(--topbar-height,0px)] bottom-0 left-0 w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-md',\n right:\n 'top-[var(--topbar-height,0px)] bottom-0 right-0 w-full sm:max-w-md border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',\n },\n },\n defaultVariants: {\n side: 'right',\n },\n }\n)\n\nexport type SheetContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &\n VariantProps<typeof sheetVariants> & {\n /** Hide the built-in close (X) button \u2014 useful when the consumer renders its own close affordance. */\n hideClose?: boolean\n /** Override the aria-label for the built-in close button. */\n closeLabel?: string\n }\n\n/**\n * SheetContent \u2014 side-anchored Radix Dialog wrapper.\n *\n * Topbar integration: the `left` and `right` variants anchor at\n * `top: var(--topbar-height, 0px)` so a sticky app topbar stays visible above\n * the panel. Consumers that render inside `AppShell` get the correct offset\n * because the shell sets `--topbar-height` on the outer container. Surfaces\n * outside the shell (portal modals, marketing pages, standalone embeds) keep\n * the default `0px` fallback and align to the viewport top \u2014 that is the\n * correct behavior when there is no topbar to clear.\n *\n * To opt into the offset from your own layout, set the CSS variable on any\n * ancestor of `<SheetContent>`:\n *\n * ```css\n * .my-shell { --topbar-height: 60px; }\n * ```\n */\n\nconst SheetContent = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Content>,\n SheetContentProps\n>(({ side = 'right', className, children, hideClose = false, closeLabel = 'Close', ...props }, ref) => (\n <SheetPortal>\n <SheetOverlay />\n <DialogPrimitive.Content\n ref={ref}\n data-slot=\"sheet-content\"\n className={cn(sheetVariants({ side }), className)}\n {...props}\n >\n {children}\n {!hideClose ? (\n <DialogPrimitive.Close\n className=\"absolute right-4 top-4 inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:shadow-focus\"\n aria-label={closeLabel}\n >\n <X className=\"size-4\" aria-hidden=\"true\" />\n </DialogPrimitive.Close>\n ) : null}\n </DialogPrimitive.Content>\n </SheetPortal>\n))\nSheetContent.displayName = DialogPrimitive.Content.displayName\n\nconst SheetHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn('flex flex-col gap-1.5 border-b px-4 py-3 pr-12 text-left', className)}\n {...props}\n />\n ),\n)\nSheetHeader.displayName = 'SheetHeader'\n\nconst SheetFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn('flex flex-col-reverse gap-2 border-t px-4 py-3 sm:flex-row sm:justify-end', className)}\n {...props}\n />\n ),\n)\nSheetFooter.displayName = 'SheetFooter'\n\nconst SheetTitle = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn('text-base font-semibold text-foreground', className)}\n {...props}\n />\n))\nSheetTitle.displayName = DialogPrimitive.Title.displayName\n\nconst SheetDescription = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn('text-sm text-muted-foreground', className)}\n {...props}\n />\n))\nSheetDescription.displayName = DialogPrimitive.Description.displayName\n\nexport {\n Sheet,\n SheetClose,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetOverlay,\n SheetPortal,\n SheetTitle,\n SheetTrigger,\n sheetVariants,\n}\n"],
5
+ "mappings": ";AAoBE,cAwEE,YAxEF;AAlBF,YAAY,WAAW;AACvB,YAAY,qBAAqB;AACjC,SAAS,WAA8B;AACvC,SAAS,SAAS;AAClB,SAAS,UAAU;AAEnB,MAAM,QAAQ,gBAAgB;AAE9B,MAAM,eAAe,gBAAgB;AAErC,MAAM,aAAa,gBAAgB;AAEnC,MAAM,cAAc,gBAAgB;AAEpC,MAAM,eAAe,MAAM,WAGzB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC,aAAU;AAAA,IACV,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACC,GAAG;AAAA,IACJ;AAAA;AACF,CACD;AACD,aAAa,cAAc,gBAAgB,QAAQ;AAEnD,MAAM,gBAAgB;AAAA,EACpB;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,KACE;AAAA,QACF,QACE;AAAA,QACF,MACE;AAAA,QACF,OACE;AAAA,MACJ;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,MAAM;AAAA,IACR;AAAA,EACF;AACF;AA6BA,MAAM,eAAe,MAAM,WAGzB,CAAC,EAAE,OAAO,SAAS,WAAW,UAAU,YAAY,OAAO,aAAa,SAAS,GAAG,MAAM,GAAG,QAC7F,qBAAC,eACC;AAAA,sBAAC,gBAAa;AAAA,EACd;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC;AAAA,MACA,aAAU;AAAA,MACV,WAAW,GAAG,cAAc,EAAE,KAAK,CAAC,GAAG,SAAS;AAAA,MAC/C,GAAG;AAAA,MAEH;AAAA;AAAA,QACA,CAAC,YACA;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YACC,WAAU;AAAA,YACV,cAAY;AAAA,YAEZ,8BAAC,KAAE,WAAU,UAAS,eAAY,QAAO;AAAA;AAAA,QAC3C,IACE;AAAA;AAAA;AAAA,EACN;AAAA,GACF,CACD;AACD,aAAa,cAAc,gBAAgB,QAAQ;AAEnD,MAAM,cAAc,MAAM;AAAA,EACxB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QACxB;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAW,GAAG,4DAA4D,SAAS;AAAA,MAClF,GAAG;AAAA;AAAA,EACN;AAEJ;AACA,YAAY,cAAc;AAE1B,MAAM,cAAc,MAAM;AAAA,EACxB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QACxB;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAW,GAAG,6EAA6E,SAAS;AAAA,MACnG,GAAG;AAAA;AAAA,EACN;AAEJ;AACA,YAAY,cAAc;AAE1B,MAAM,aAAa,MAAM,WAGvB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IACA,WAAW,GAAG,2CAA2C,SAAS;AAAA,IACjE,GAAG;AAAA;AACN,CACD;AACD,WAAW,cAAc,gBAAgB,MAAM;AAE/C,MAAM,mBAAmB,MAAM,WAG7B,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IACA,WAAW,GAAG,iCAAiC,SAAS;AAAA,IACvD,GAAG;AAAA;AACN,CACD;AACD,iBAAiB,cAAc,gBAAgB,YAAY;",
6
6
  "names": []
7
7
  }
@@ -3,29 +3,79 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { cn } from "@open-mercato/shared/lib/utils";
5
5
  const baseTextareaClass = "flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-colors placeholder:text-muted-foreground outline-none focus-visible:outline-none focus-visible:shadow-focus focus-visible:border-foreground hover:bg-muted/40 disabled:cursor-not-allowed disabled:bg-bg-disabled disabled:border-border-disabled disabled:shadow-none disabled:hover:bg-bg-disabled aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-visible:border-destructive resize-y min-h-[80px]";
6
+ const DEFAULT_AUTO_RESIZE_MAX_ROWS = 12;
7
+ const FALLBACK_LINE_HEIGHT_PX = 20;
8
+ function readPixels(value) {
9
+ const parsed = Number.parseFloat(value);
10
+ return Number.isFinite(parsed) ? parsed : 0;
11
+ }
12
+ function applyAutoResize(element, maxRows) {
13
+ if (!element) return;
14
+ const computed = typeof window !== "undefined" ? window.getComputedStyle(element) : null;
15
+ const lineHeight = computed ? readPixels(computed.lineHeight) || FALLBACK_LINE_HEIGHT_PX : FALLBACK_LINE_HEIGHT_PX;
16
+ const padding = computed ? readPixels(computed.paddingTop) + readPixels(computed.paddingBottom) : 0;
17
+ const border = computed ? readPixels(computed.borderTopWidth) + readPixels(computed.borderBottomWidth) : 0;
18
+ const maxContentHeight = lineHeight * Math.max(1, maxRows) + padding;
19
+ element.style.height = "auto";
20
+ const contentHeight = element.scrollHeight;
21
+ const cappedHeight = Math.min(contentHeight, maxContentHeight);
22
+ element.style.height = `${cappedHeight + border}px`;
23
+ element.style.overflowY = contentHeight > maxContentHeight ? "auto" : "hidden";
24
+ }
6
25
  const Textarea = React.forwardRef(
7
- ({ className, showCount, wrapperClassName, value, defaultValue, maxLength, onChange, ...props }, ref) => {
26
+ ({
27
+ className,
28
+ showCount,
29
+ wrapperClassName,
30
+ autoResize,
31
+ maxRows = DEFAULT_AUTO_RESIZE_MAX_ROWS,
32
+ value,
33
+ defaultValue,
34
+ maxLength,
35
+ onChange,
36
+ ...props
37
+ }, ref) => {
8
38
  const [internalValue, setInternalValue] = React.useState(
9
39
  typeof defaultValue === "string" ? defaultValue : typeof value === "string" ? value : ""
10
40
  );
11
41
  const isControlled = value !== void 0;
12
42
  const currentValue = isControlled ? String(value ?? "") : internalValue;
43
+ const elementRef = React.useRef(null);
44
+ const setElementRef = React.useCallback(
45
+ (node) => {
46
+ elementRef.current = node;
47
+ if (typeof ref === "function") ref(node);
48
+ else if (ref) ref.current = node;
49
+ },
50
+ [ref]
51
+ );
52
+ React.useLayoutEffect(() => {
53
+ if (!autoResize) return;
54
+ applyAutoResize(elementRef.current, maxRows);
55
+ }, [autoResize, maxRows, currentValue]);
56
+ React.useEffect(() => {
57
+ if (!autoResize || typeof window === "undefined") return;
58
+ const onWindowResize = () => applyAutoResize(elementRef.current, maxRows);
59
+ window.addEventListener("resize", onWindowResize);
60
+ return () => window.removeEventListener("resize", onWindowResize);
61
+ }, [autoResize, maxRows]);
13
62
  const handleChange = React.useCallback(
14
63
  (event) => {
15
64
  if (!isControlled) setInternalValue(event.target.value);
65
+ if (autoResize) applyAutoResize(event.currentTarget, maxRows);
16
66
  onChange?.(event);
17
67
  },
18
- [isControlled, onChange]
68
+ [isControlled, onChange, autoResize, maxRows]
19
69
  );
20
70
  const textarea = /* @__PURE__ */ jsx(
21
71
  "textarea",
22
72
  {
23
- ref,
73
+ ref: setElementRef,
24
74
  value,
25
75
  defaultValue: isControlled ? void 0 : defaultValue,
26
76
  maxLength,
27
77
  onChange: handleChange,
28
- className: cn(baseTextareaClass, className),
78
+ className: cn(baseTextareaClass, autoResize && "resize-none overflow-hidden", className),
29
79
  ...props
30
80
  }
31
81
  );
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/primitives/textarea.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nconst baseTextareaClass =\n 'flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-colors placeholder:text-muted-foreground outline-none focus-visible:outline-none focus-visible:shadow-focus focus-visible:border-foreground hover:bg-muted/40 disabled:cursor-not-allowed disabled:bg-bg-disabled disabled:border-border-disabled disabled:shadow-none disabled:hover:bg-bg-disabled aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-visible:border-destructive resize-y min-h-[80px]'\n\nexport type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & {\n /** Show character counter (`current/max`) below the textarea. Requires `maxLength`. */\n showCount?: boolean\n /** Optional className applied to the outer wrapper (when counter is shown). */\n wrapperClassName?: string\n}\n\nexport const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n ({ className, showCount, wrapperClassName, value, defaultValue, maxLength, onChange, ...props }, ref) => {\n const [internalValue, setInternalValue] = React.useState<string>(\n typeof defaultValue === 'string' ? defaultValue : typeof value === 'string' ? value : ''\n )\n\n const isControlled = value !== undefined\n const currentValue = isControlled ? String(value ?? '') : internalValue\n\n const handleChange = React.useCallback(\n (event: React.ChangeEvent<HTMLTextAreaElement>) => {\n if (!isControlled) setInternalValue(event.target.value)\n onChange?.(event)\n },\n [isControlled, onChange]\n )\n\n const textarea = (\n <textarea\n ref={ref}\n value={value}\n defaultValue={isControlled ? undefined : defaultValue}\n maxLength={maxLength}\n onChange={handleChange}\n className={cn(baseTextareaClass, className)}\n {...props}\n />\n )\n\n if (!showCount) return textarea\n\n const length = currentValue.length\n const max = typeof maxLength === 'number' ? maxLength : undefined\n const isError = max != null && length > max\n const isDisabled = props.disabled\n\n return (\n <div className={cn('flex flex-col gap-1', wrapperClassName)}>\n {textarea}\n <div className=\"flex justify-end\">\n <span\n className={cn(\n 'text-overline uppercase',\n isDisabled\n ? 'text-text-disabled'\n : isError\n ? 'text-destructive'\n : 'text-muted-foreground'\n )}\n aria-live=\"polite\"\n >\n {max != null ? `${length}/${max}` : `${length}`}\n </span>\n </div>\n </div>\n )\n }\n)\n\nTextarea.displayName = 'Textarea'\n"],
5
- "mappings": ";AAkCM,cAmBA,YAnBA;AAhCN,YAAY,WAAW;AAEvB,SAAS,UAAU;AAEnB,MAAM,oBACJ;AASK,MAAM,WAAW,MAAM;AAAA,EAC5B,CAAC,EAAE,WAAW,WAAW,kBAAkB,OAAO,cAAc,WAAW,UAAU,GAAG,MAAM,GAAG,QAAQ;AACvG,UAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM;AAAA,MAC9C,OAAO,iBAAiB,WAAW,eAAe,OAAO,UAAU,WAAW,QAAQ;AAAA,IACxF;AAEA,UAAM,eAAe,UAAU;AAC/B,UAAM,eAAe,eAAe,OAAO,SAAS,EAAE,IAAI;AAE1D,UAAM,eAAe,MAAM;AAAA,MACzB,CAAC,UAAkD;AACjD,YAAI,CAAC,aAAc,kBAAiB,MAAM,OAAO,KAAK;AACtD,mBAAW,KAAK;AAAA,MAClB;AAAA,MACA,CAAC,cAAc,QAAQ;AAAA,IACzB;AAEA,UAAM,WACJ;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,cAAc,eAAe,SAAY;AAAA,QACzC;AAAA,QACA,UAAU;AAAA,QACV,WAAW,GAAG,mBAAmB,SAAS;AAAA,QACzC,GAAG;AAAA;AAAA,IACN;AAGF,QAAI,CAAC,UAAW,QAAO;AAEvB,UAAM,SAAS,aAAa;AAC5B,UAAM,MAAM,OAAO,cAAc,WAAW,YAAY;AACxD,UAAM,UAAU,OAAO,QAAQ,SAAS;AACxC,UAAM,aAAa,MAAM;AAEzB,WACE,qBAAC,SAAI,WAAW,GAAG,uBAAuB,gBAAgB,GACvD;AAAA;AAAA,MACD,oBAAC,SAAI,WAAU,oBACb;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,aACI,uBACA,UACE,qBACA;AAAA,UACR;AAAA,UACA,aAAU;AAAA,UAET,iBAAO,OAAO,GAAG,MAAM,IAAI,GAAG,KAAK,GAAG,MAAM;AAAA;AAAA,MAC/C,GACF;AAAA,OACF;AAAA,EAEJ;AACF;AAEA,SAAS,cAAc;",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nconst baseTextareaClass =\n 'flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-colors placeholder:text-muted-foreground outline-none focus-visible:outline-none focus-visible:shadow-focus focus-visible:border-foreground hover:bg-muted/40 disabled:cursor-not-allowed disabled:bg-bg-disabled disabled:border-border-disabled disabled:shadow-none disabled:hover:bg-bg-disabled aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-visible:border-destructive resize-y min-h-[80px]'\n\nconst DEFAULT_AUTO_RESIZE_MAX_ROWS = 12\nconst FALLBACK_LINE_HEIGHT_PX = 20\n\nfunction readPixels(value: string): number {\n const parsed = Number.parseFloat(value)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\n/**\n * Grow a textarea to fit its content, capped at `maxRows` rendered lines.\n * The cap is expressed in rows rather than pixels so it follows the element's\n * own type scale instead of a magic height, and the element only scrolls once\n * the cap is reached.\n */\nfunction applyAutoResize(element: HTMLTextAreaElement | null, maxRows: number): void {\n if (!element) return\n const computed = typeof window !== 'undefined' ? window.getComputedStyle(element) : null\n const lineHeight = computed ? readPixels(computed.lineHeight) || FALLBACK_LINE_HEIGHT_PX : FALLBACK_LINE_HEIGHT_PX\n const padding = computed ? readPixels(computed.paddingTop) + readPixels(computed.paddingBottom) : 0\n const border = computed ? readPixels(computed.borderTopWidth) + readPixels(computed.borderBottomWidth) : 0\n const maxContentHeight = lineHeight * Math.max(1, maxRows) + padding\n\n element.style.height = 'auto'\n const contentHeight = element.scrollHeight\n const cappedHeight = Math.min(contentHeight, maxContentHeight)\n element.style.height = `${cappedHeight + border}px`\n element.style.overflowY = contentHeight > maxContentHeight ? 'auto' : 'hidden'\n}\n\nexport type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & {\n /** Show character counter (`current/max`) below the textarea. Requires `maxLength`. */\n showCount?: boolean\n /** Optional className applied to the outer wrapper (when counter is shown). */\n wrapperClassName?: string\n /**\n * Grow the field to fit its content instead of scrolling inside a fixed box.\n * Disables the manual resize handle \u2014 the height is owned by the content.\n */\n autoResize?: boolean\n /** Rows to grow to before the field starts scrolling. Only used with `autoResize`. */\n maxRows?: number\n}\n\nexport const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n (\n {\n className,\n showCount,\n wrapperClassName,\n autoResize,\n maxRows = DEFAULT_AUTO_RESIZE_MAX_ROWS,\n value,\n defaultValue,\n maxLength,\n onChange,\n ...props\n },\n ref\n ) => {\n const [internalValue, setInternalValue] = React.useState<string>(\n typeof defaultValue === 'string' ? defaultValue : typeof value === 'string' ? value : ''\n )\n\n const isControlled = value !== undefined\n const currentValue = isControlled ? String(value ?? '') : internalValue\n\n const elementRef = React.useRef<HTMLTextAreaElement | null>(null)\n const setElementRef = React.useCallback(\n (node: HTMLTextAreaElement | null) => {\n elementRef.current = node\n if (typeof ref === 'function') ref(node)\n else if (ref) (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node\n },\n [ref]\n )\n\n React.useLayoutEffect(() => {\n if (!autoResize) return\n applyAutoResize(elementRef.current, maxRows)\n }, [autoResize, maxRows, currentValue])\n\n React.useEffect(() => {\n if (!autoResize || typeof window === 'undefined') return\n const onWindowResize = () => applyAutoResize(elementRef.current, maxRows)\n window.addEventListener('resize', onWindowResize)\n return () => window.removeEventListener('resize', onWindowResize)\n }, [autoResize, maxRows])\n\n const handleChange = React.useCallback(\n (event: React.ChangeEvent<HTMLTextAreaElement>) => {\n if (!isControlled) setInternalValue(event.target.value)\n if (autoResize) applyAutoResize(event.currentTarget, maxRows)\n onChange?.(event)\n },\n [isControlled, onChange, autoResize, maxRows]\n )\n\n const textarea = (\n <textarea\n ref={setElementRef}\n value={value}\n defaultValue={isControlled ? undefined : defaultValue}\n maxLength={maxLength}\n onChange={handleChange}\n className={cn(baseTextareaClass, autoResize && 'resize-none overflow-hidden', className)}\n {...props}\n />\n )\n\n if (!showCount) return textarea\n\n const length = currentValue.length\n const max = typeof maxLength === 'number' ? maxLength : undefined\n const isError = max != null && length > max\n const isDisabled = props.disabled\n\n return (\n <div className={cn('flex flex-col gap-1', wrapperClassName)}>\n {textarea}\n <div className=\"flex justify-end\">\n <span\n className={cn(\n 'text-overline uppercase',\n isDisabled\n ? 'text-text-disabled'\n : isError\n ? 'text-destructive'\n : 'text-muted-foreground'\n )}\n aria-live=\"polite\"\n >\n {max != null ? `${length}/${max}` : `${length}`}\n </span>\n </div>\n </div>\n )\n }\n)\n\nTextarea.displayName = 'Textarea'\n"],
5
+ "mappings": ";AA2GM,cAmBA,YAnBA;AAzGN,YAAY,WAAW;AAEvB,SAAS,UAAU;AAEnB,MAAM,oBACJ;AAEF,MAAM,+BAA+B;AACrC,MAAM,0BAA0B;AAEhC,SAAS,WAAW,OAAuB;AACzC,QAAM,SAAS,OAAO,WAAW,KAAK;AACtC,SAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC5C;AAQA,SAAS,gBAAgB,SAAqC,SAAuB;AACnF,MAAI,CAAC,QAAS;AACd,QAAM,WAAW,OAAO,WAAW,cAAc,OAAO,iBAAiB,OAAO,IAAI;AACpF,QAAM,aAAa,WAAW,WAAW,SAAS,UAAU,KAAK,0BAA0B;AAC3F,QAAM,UAAU,WAAW,WAAW,SAAS,UAAU,IAAI,WAAW,SAAS,aAAa,IAAI;AAClG,QAAM,SAAS,WAAW,WAAW,SAAS,cAAc,IAAI,WAAW,SAAS,iBAAiB,IAAI;AACzG,QAAM,mBAAmB,aAAa,KAAK,IAAI,GAAG,OAAO,IAAI;AAE7D,UAAQ,MAAM,SAAS;AACvB,QAAM,gBAAgB,QAAQ;AAC9B,QAAM,eAAe,KAAK,IAAI,eAAe,gBAAgB;AAC7D,UAAQ,MAAM,SAAS,GAAG,eAAe,MAAM;AAC/C,UAAQ,MAAM,YAAY,gBAAgB,mBAAmB,SAAS;AACxE;AAgBO,MAAM,WAAW,MAAM;AAAA,EAC5B,CACE;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,GACA,QACG;AACH,UAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM;AAAA,MAC9C,OAAO,iBAAiB,WAAW,eAAe,OAAO,UAAU,WAAW,QAAQ;AAAA,IACxF;AAEA,UAAM,eAAe,UAAU;AAC/B,UAAM,eAAe,eAAe,OAAO,SAAS,EAAE,IAAI;AAE1D,UAAM,aAAa,MAAM,OAAmC,IAAI;AAChE,UAAM,gBAAgB,MAAM;AAAA,MAC1B,CAAC,SAAqC;AACpC,mBAAW,UAAU;AACrB,YAAI,OAAO,QAAQ,WAAY,KAAI,IAAI;AAAA,iBAC9B,IAAK,CAAC,IAA2D,UAAU;AAAA,MACtF;AAAA,MACA,CAAC,GAAG;AAAA,IACN;AAEA,UAAM,gBAAgB,MAAM;AAC1B,UAAI,CAAC,WAAY;AACjB,sBAAgB,WAAW,SAAS,OAAO;AAAA,IAC7C,GAAG,CAAC,YAAY,SAAS,YAAY,CAAC;AAEtC,UAAM,UAAU,MAAM;AACpB,UAAI,CAAC,cAAc,OAAO,WAAW,YAAa;AAClD,YAAM,iBAAiB,MAAM,gBAAgB,WAAW,SAAS,OAAO;AACxE,aAAO,iBAAiB,UAAU,cAAc;AAChD,aAAO,MAAM,OAAO,oBAAoB,UAAU,cAAc;AAAA,IAClE,GAAG,CAAC,YAAY,OAAO,CAAC;AAExB,UAAM,eAAe,MAAM;AAAA,MACzB,CAAC,UAAkD;AACjD,YAAI,CAAC,aAAc,kBAAiB,MAAM,OAAO,KAAK;AACtD,YAAI,WAAY,iBAAgB,MAAM,eAAe,OAAO;AAC5D,mBAAW,KAAK;AAAA,MAClB;AAAA,MACA,CAAC,cAAc,UAAU,YAAY,OAAO;AAAA,IAC9C;AAEA,UAAM,WACJ;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL;AAAA,QACA,cAAc,eAAe,SAAY;AAAA,QACzC;AAAA,QACA,UAAU;AAAA,QACV,WAAW,GAAG,mBAAmB,cAAc,+BAA+B,SAAS;AAAA,QACtF,GAAG;AAAA;AAAA,IACN;AAGF,QAAI,CAAC,UAAW,QAAO;AAEvB,UAAM,SAAS,aAAa;AAC5B,UAAM,MAAM,OAAO,cAAc,WAAW,YAAY;AACxD,UAAM,UAAU,OAAO,QAAQ,SAAS;AACxC,UAAM,aAAa,MAAM;AAEzB,WACE,qBAAC,SAAI,WAAW,GAAG,uBAAuB,gBAAgB,GACvD;AAAA;AAAA,MACD,oBAAC,SAAI,WAAU,oBACb;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,aACI,uBACA,UACE,qBACA;AAAA,UACR;AAAA,UACA,aAAU;AAAA,UAET,iBAAO,OAAO,GAAG,MAAM,IAAI,GAAG,KAAK,GAAG,MAAM;AAAA;AAAA,MAC/C,GACF;AAAA,OACF;AAAA,EAEJ;AACF;AAEA,SAAS,cAAc;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.7.1-develop.7193.1.910a5b0a1e",
3
+ "version": "0.7.1-develop.7195.1.b5efd1d51a",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -155,14 +155,14 @@
155
155
  "remark-gfm": "^4.0.1"
156
156
  },
157
157
  "peerDependencies": {
158
- "@open-mercato/shared": "0.7.1-develop.7193.1.910a5b0a1e",
158
+ "@open-mercato/shared": "0.7.1-develop.7195.1.b5efd1d51a",
159
159
  "react": ">=18.0.0",
160
160
  "react-dom": ">=18.0.0",
161
161
  "react-is": ">=18.0.0"
162
162
  },
163
163
  "devDependencies": {
164
164
  "@figma/code-connect": "^1.3.4",
165
- "@open-mercato/shared": "0.7.1-develop.7193.1.910a5b0a1e",
165
+ "@open-mercato/shared": "0.7.1-develop.7195.1.b5efd1d51a",
166
166
  "@testing-library/dom": "^10.4.1",
167
167
  "@testing-library/jest-dom": "^7.0.1",
168
168
  "@testing-library/react": "^16.3.3",
@@ -37,6 +37,7 @@ import { readVersionedPreference, writeVersionedPreference } from '@open-mercato
37
37
  import { cloneSidebarGroups } from './sidebar/customization-helpers'
38
38
  import type { SectionNavGroup } from './section-page/types'
39
39
  import { InjectionSpot } from './injection/InjectionSpot'
40
+ import { useNavBadge } from './nav/navBadges'
40
41
  import {
41
42
  BackendRecordInjectionContextProvider,
42
43
  type RecordInjectionContext,
@@ -64,6 +65,7 @@ import {
64
65
  BACKEND_TOPBAR_ACTIONS_INJECTION_SPOT_ID,
65
66
  GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID,
66
67
  GLOBAL_SIDEBAR_STATUS_BADGES_INJECTION_SPOT_ID,
68
+ BACKEND_NAV_BADGES_INJECTION_SPOT_ID,
67
69
  } from './injection/spotIds'
68
70
 
69
71
  // Versioned-envelope discriminator for the persisted sidebar open/closed group
@@ -418,6 +420,32 @@ function sidebarBlockPadding(compact: boolean): string {
418
420
  return `${compact ? 'px-2' : 'px-3'} transition-[padding] duration-200 ease-out`
419
421
  }
420
422
 
423
+ /**
424
+ * The live count a module published for this nav item, or nothing at all.
425
+ *
426
+ * Zero and absent both render nothing: a "0" chip is noise dressed as
427
+ * information. Collapsed sidebars keep the chip — it is the only thing left
428
+ * saying something needs you.
429
+ */
430
+ function NavItemBadge({ href, compact }: { href: string; compact: boolean }) {
431
+ const badge = useNavBadge(href)
432
+ if (!badge || badge.count <= 0) return null
433
+ const display = badge.count > 99 ? '99+' : String(badge.count)
434
+ const tone = badge.tone === 'attention'
435
+ ? 'bg-status-warning-bg text-status-warning-text'
436
+ : 'bg-muted text-muted-foreground'
437
+ return (
438
+ <span
439
+ className={`ml-auto inline-flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-xs font-medium ${tone} ${
440
+ compact ? 'absolute right-0 top-0 -mr-1 -mt-1' : ''
441
+ }`}
442
+ aria-label={badge.label ?? display}
443
+ >
444
+ {display}
445
+ </span>
446
+ )
447
+ }
448
+
421
449
  // An InjectionSpot renders nothing when no widget is registered, so its wrapper must collapse
422
450
  // instead of surviving as an empty flex child that still costs a full `gap-3`.
423
451
  function sidebarInjectionWrapper(compact: boolean): string {
@@ -448,6 +476,38 @@ const HeaderContext = createContext<{
448
476
  setTitle: (t?: string) => void
449
477
  } | null>(null)
450
478
 
479
+ /**
480
+ * Additive contract that lets a page under the backend shell drive the desktop
481
+ * sidebar collapse without owning the shell. `requestCollapse(true)` forces the
482
+ * sidebar to render collapsed without overwriting the user's persisted manual
483
+ * preference; `releaseRequest()` drops the request and falls back to whatever
484
+ * the underlying state is (including any active settings/profile force-collapse).
485
+ */
486
+ export type SidebarCollapseContextValue = {
487
+ collapsed: boolean
488
+ setCollapsed: (next: boolean) => void
489
+ requestCollapse: (next: boolean) => void
490
+ releaseRequest: () => void
491
+ }
492
+
493
+ const noopSidebarCollapse: SidebarCollapseContextValue = {
494
+ collapsed: false,
495
+ setCollapsed: () => {},
496
+ requestCollapse: () => {},
497
+ releaseRequest: () => {},
498
+ }
499
+
500
+ const SidebarCollapseContext = createContext<SidebarCollapseContextValue | null>(null)
501
+
502
+ /**
503
+ * Read the backend shell's sidebar-collapse controls. Returns a safe no-op
504
+ * default when called outside `AppShell` so standalone renders and tests never
505
+ * crash.
506
+ */
507
+ export function useSidebarCollapse(): SidebarCollapseContextValue {
508
+ return useContext(SidebarCollapseContext) ?? noopSidebarCollapse
509
+ }
510
+
451
511
  export function ApplyBreadcrumb({ breadcrumb, title, titleKey }: { breadcrumb?: Array<{ label: string; href?: string; labelKey?: string }>; title?: string; titleKey?: string }) {
452
512
  const ctx = useContext(HeaderContext)
453
513
  const t = useT()
@@ -567,6 +627,17 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
567
627
  }, [mobileOpen])
568
628
  // Initialize from server-provided prop only to avoid hydration flicker
569
629
  const [collapsed, setCollapsed] = React.useState(sidebarCollapsedDefault)
630
+ // External collapse request (additive). A page under the shell can force the
631
+ // sidebar collapsed via `useSidebarCollapse().requestCollapse(true)` without
632
+ // touching `collapsed` (the user's persisted manual preference) — so nothing
633
+ // is written to localStorage/cookie and `releaseRequest()` restores the prior
634
+ // underlying state. Default `null` means "no request", keeping behavior
635
+ // byte-for-byte identical when no page consumes the context.
636
+ const [externalCollapseRequest, setExternalCollapseRequest] = React.useState<boolean | null>(null)
637
+ // Remember the value the user manually had before an external request took
638
+ // over, mirroring `collapsedBeforeSectionRef`. Not required for correctness
639
+ // (the request never mutates `collapsed`) but keeps the intent explicit.
640
+ const collapsedBeforeExternalRequestRef = React.useRef<boolean | null>(null)
570
641
  // Maintain internal nav state so we can augment it client-side
571
642
  const [navGroups, setNavGroups] = React.useState(resolvedGroups)
572
643
  const [openGroups, setOpenGroups] = React.useState<Record<string, boolean>>(() =>
@@ -582,9 +653,29 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
582
653
  if (!label) return false
583
654
  return label.toLowerCase().includes(navQueryNorm)
584
655
  }, [navQueryActive, navQueryNorm])
585
- const effectiveCollapsed = collapsed
656
+ // An active external collapse request wins over the manual/section state
657
+ // ("collapsed wins"); when released (`null`) we fall back to `collapsed`,
658
+ // which already reflects any settings/profile force-collapse.
659
+ const effectiveCollapsed = externalCollapseRequest === true ? true : collapsed
586
660
  const expandedSidebarWidth = '240px'
587
661
 
662
+ const requestCollapse = React.useCallback((next: boolean) => {
663
+ setExternalCollapseRequest((prev) => {
664
+ if (prev === null) collapsedBeforeExternalRequestRef.current = collapsed
665
+ return next
666
+ })
667
+ }, [collapsed])
668
+ const releaseRequest = React.useCallback(() => {
669
+ collapsedBeforeExternalRequestRef.current = null
670
+ setExternalCollapseRequest(null)
671
+ }, [])
672
+ const sidebarCollapseValue = React.useMemo<SidebarCollapseContextValue>(() => ({
673
+ collapsed: effectiveCollapsed,
674
+ setCollapsed: (next: boolean) => setCollapsed(next),
675
+ requestCollapse,
676
+ releaseRequest,
677
+ }), [effectiveCollapsed, requestCollapse, releaseRequest])
678
+
588
679
  // Track scroll position of the desktop sidebar's inner scroll container so we can
589
680
  // flip the affordance chevron between down/up (and hide it entirely when content
590
681
  // fits without scrolling). The inner div is rendered deep in renderSidebar /
@@ -918,6 +1009,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
918
1009
  )}
919
1010
  </span>
920
1011
  {!compact && <span className="truncate">{label}</span>}
1012
+ <NavItemBadge href={item.href} compact={compact} />
921
1013
  </Link>
922
1014
  {showChildren ? childItems.map((child) => renderSectionItem(child, depth + 1)) : null}
923
1015
  </React.Fragment>
@@ -1320,9 +1412,19 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
1320
1412
 
1321
1413
  return (
1322
1414
  <HeaderContext.Provider value={headerCtxValue}>
1415
+ <SidebarCollapseContext.Provider value={sidebarCollapseValue}>
1323
1416
  <div
1324
1417
  className={`relative min-h-svh lg:grid transition-[grid-template-columns] duration-200 ease-out ${gridColsClass}`}
1325
- style={{ '--topbar-height': '61px' } as React.CSSProperties}
1418
+ style={{
1419
+ '--topbar-height': '61px',
1420
+ // Left offset of the content column (sidebar, plus the section sidebar on
1421
+ // settings/profile routes). Exposed so full-bleed pages — e.g. the
1422
+ // workflow visual editor — can size themselves to the content column with
1423
+ // `calc(100vw - var(--app-content-offset))` instead of being capped by
1424
+ // `main`'s centered max-width. Only meaningful at `lg` and up, where the
1425
+ // grid (and the sidebar) is active.
1426
+ '--app-content-offset': isSectionView ? `calc(${asideWidth} + 240px)` : asideWidth,
1427
+ } as React.CSSProperties}
1326
1428
  >
1327
1429
  <a
1328
1430
  href="#main-content"
@@ -1506,6 +1608,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
1506
1608
  </header>
1507
1609
  <ProgressTopBar t={t} className="sticky top-0 z-sticky" completedAutoHideMs={progressCompletedAutoHideMs} />
1508
1610
  <main id="main-content" tabIndex={-1} className="flex-1 p-4 lg:p-6 mx-auto w-full max-w-screen-2xl">
1611
+ <InjectionSpot spotId={BACKEND_NAV_BADGES_INJECTION_SPOT_ID} context={injectionContext} />
1509
1612
  <InjectionSpot spotId={BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID} context={injectionContext} />
1510
1613
  <FlashMessages />
1511
1614
  <PartialIndexBanner />
@@ -1620,6 +1723,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
1620
1723
  )}
1621
1724
  </div>
1622
1725
  <UmesDevToolsPanel />
1726
+ </SidebarCollapseContext.Provider>
1623
1727
  </HeaderContext.Provider>
1624
1728
  )
1625
1729
  }