@motir/design-system 0.1.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.
Files changed (107) hide show
  1. package/README.md +126 -0
  2. package/dist/appearance.d.ts +47 -0
  3. package/dist/appearance.js +3 -0
  4. package/dist/appearance.js.map +1 -0
  5. package/dist/components/theme/AppearancePickers.d.ts +97 -0
  6. package/dist/components/theme/AppearancePickers.js +542 -0
  7. package/dist/components/theme/AppearancePickers.js.map +1 -0
  8. package/dist/components/theme/HandDrawnFilter.d.ts +23 -0
  9. package/dist/components/theme/HandDrawnFilter.js +41 -0
  10. package/dist/components/theme/HandDrawnFilter.js.map +1 -0
  11. package/dist/components/theme/ImmersiveTilt.d.ts +3 -0
  12. package/dist/components/theme/ImmersiveTilt.js +113 -0
  13. package/dist/components/theme/ImmersiveTilt.js.map +1 -0
  14. package/dist/components/theme/StyleVignette.d.ts +60 -0
  15. package/dist/components/theme/StyleVignette.js +382 -0
  16. package/dist/components/theme/StyleVignette.js.map +1 -0
  17. package/dist/components/ui/Button.d.ts +32 -0
  18. package/dist/components/ui/Button.js +92 -0
  19. package/dist/components/ui/Button.js.map +1 -0
  20. package/dist/components/ui/Card.d.ts +33 -0
  21. package/dist/components/ui/Card.js +64 -0
  22. package/dist/components/ui/Card.js.map +1 -0
  23. package/dist/components/ui/ColorSwatchPicker.d.ts +19 -0
  24. package/dist/components/ui/ColorSwatchPicker.js +71 -0
  25. package/dist/components/ui/ColorSwatchPicker.js.map +1 -0
  26. package/dist/components/ui/Combobox.d.ts +84 -0
  27. package/dist/components/ui/Combobox.js +374 -0
  28. package/dist/components/ui/Combobox.js.map +1 -0
  29. package/dist/components/ui/EmptyState.d.ts +38 -0
  30. package/dist/components/ui/EmptyState.js +73 -0
  31. package/dist/components/ui/EmptyState.js.map +1 -0
  32. package/dist/components/ui/ErrorState.d.ts +43 -0
  33. package/dist/components/ui/ErrorState.js +169 -0
  34. package/dist/components/ui/ErrorState.js.map +1 -0
  35. package/dist/components/ui/FormField.d.ts +31 -0
  36. package/dist/components/ui/FormField.js +43 -0
  37. package/dist/components/ui/FormField.js.map +1 -0
  38. package/dist/components/ui/Input.d.ts +29 -0
  39. package/dist/components/ui/Input.js +105 -0
  40. package/dist/components/ui/Input.js.map +1 -0
  41. package/dist/components/ui/Modal.d.ts +84 -0
  42. package/dist/components/ui/Modal.js +153 -0
  43. package/dist/components/ui/Modal.js.map +1 -0
  44. package/dist/components/ui/MultiSelectPicker.d.ts +73 -0
  45. package/dist/components/ui/MultiSelectPicker.js +358 -0
  46. package/dist/components/ui/MultiSelectPicker.js.map +1 -0
  47. package/dist/components/ui/Pill.d.ts +49 -0
  48. package/dist/components/ui/Pill.js +107 -0
  49. package/dist/components/ui/Pill.js.map +1 -0
  50. package/dist/components/ui/Popover.d.ts +65 -0
  51. package/dist/components/ui/Popover.js +57 -0
  52. package/dist/components/ui/Popover.js.map +1 -0
  53. package/dist/components/ui/SectionLabel.d.ts +28 -0
  54. package/dist/components/ui/SectionLabel.js +27 -0
  55. package/dist/components/ui/SectionLabel.js.map +1 -0
  56. package/dist/components/ui/Segmented.d.ts +30 -0
  57. package/dist/components/ui/Segmented.js +65 -0
  58. package/dist/components/ui/Segmented.js.map +1 -0
  59. package/dist/components/ui/Spinner.d.ts +22 -0
  60. package/dist/components/ui/Spinner.js +39 -0
  61. package/dist/components/ui/Spinner.js.map +1 -0
  62. package/dist/components/ui/Switch.d.ts +34 -0
  63. package/dist/components/ui/Switch.js +50 -0
  64. package/dist/components/ui/Switch.js.map +1 -0
  65. package/dist/components/ui/Textarea.d.ts +19 -0
  66. package/dist/components/ui/Textarea.js +72 -0
  67. package/dist/components/ui/Textarea.js.map +1 -0
  68. package/dist/components/ui/Toast.d.ts +45 -0
  69. package/dist/components/ui/Toast.js +116 -0
  70. package/dist/components/ui/Toast.js.map +1 -0
  71. package/dist/components/ui/Tooltip.d.ts +25 -0
  72. package/dist/components/ui/Tooltip.js +43 -0
  73. package/dist/components/ui/Tooltip.js.map +1 -0
  74. package/dist/contexts/theme-context.d.ts +95 -0
  75. package/dist/contexts/theme-context.js +561 -0
  76. package/dist/contexts/theme-context.js.map +1 -0
  77. package/dist/index.d.ts +41 -0
  78. package/dist/index.js +67 -0
  79. package/dist/specimen/TokensSpecimen.d.ts +13 -0
  80. package/dist/specimen/TokensSpecimen.js +1090 -0
  81. package/dist/specimen/TokensSpecimen.js.map +1 -0
  82. package/dist/theme/appearance-resolution.d.ts +59 -0
  83. package/dist/theme/appearance-resolution.js +379 -0
  84. package/dist/theme/appearance-resolution.js.map +1 -0
  85. package/dist/theme/init-script.d.ts +48 -0
  86. package/dist/theme/init-script.js +391 -0
  87. package/dist/theme/init-script.js.map +1 -0
  88. package/dist/theme/palettes.d.ts +146 -0
  89. package/dist/theme/palettes.js +85 -0
  90. package/dist/theme/palettes.js.map +1 -0
  91. package/dist/theme/styles.d.ts +351 -0
  92. package/dist/theme/styles.js +264 -0
  93. package/dist/theme/styles.js.map +1 -0
  94. package/dist/theme/tilt.d.ts +41 -0
  95. package/dist/theme/tilt.js +18 -0
  96. package/dist/theme/tilt.js.map +1 -0
  97. package/dist/theme/types.d.ts +61 -0
  98. package/dist/theme/types.js +234 -0
  99. package/dist/theme/types.js.map +1 -0
  100. package/dist/theme/typography.d.ts +144 -0
  101. package/dist/theme/typography.js +57 -0
  102. package/dist/theme/typography.js.map +1 -0
  103. package/dist/utils/cn.d.ts +16 -0
  104. package/dist/utils/cn.js +11 -0
  105. package/dist/utils/cn.js.map +1 -0
  106. package/package.json +60 -0
  107. package/theme.css +3924 -0
@@ -0,0 +1,153 @@
1
+ 'use client';
2
+ import { forwardRef } from 'react';
3
+ import * as Dialog from '@radix-ui/react-dialog';
4
+ import { X } from 'lucide-react';
5
+ import { cva } from 'class-variance-authority';
6
+ import { clsx } from 'clsx';
7
+ import { twMerge } from 'tailwind-merge';
8
+ import { jsx, jsxs } from 'react/jsx-runtime';
9
+
10
+ function cn(...inputs) {
11
+ return twMerge(clsx(inputs));
12
+ }
13
+ var contentVariants = cva(
14
+ cn(
15
+ "fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2",
16
+ "w-[90vw] rounded-(--radius-modal) bg-(--el-page-bg)",
17
+ "shadow-(--shadow-modal) border border-(--el-border)",
18
+ "p-(--spacing-card-padding)",
19
+ "focus:outline-none",
20
+ // Never exceed the viewport: cap height and lay out as a column so a
21
+ // consumer can wrap its fields in `Modal.Body` (which owns the
22
+ // `flex-1 overflow-y-auto` scroll recipe) and pin a `Modal.Footer`,
23
+ // instead of the dialog growing off-screen (e.g. the create modal's
24
+ // expandable Explanation pushing the Create button out of view).
25
+ "flex max-h-[90vh] flex-col overflow-hidden"
26
+ ),
27
+ {
28
+ variants: {
29
+ // Literal widths, NOT the max-w-sm/md/lg utilities: the design
30
+ // system's @theme defines --spacing-sm/md/lg (12/16/20px), and
31
+ // Tailwind v4 resolves `max-w-{key}` against the --spacing-* scale
32
+ // when that key exists — so `max-w-md` would collapse the modal to
33
+ // 16px wide. Pinning the rem values (Tailwind's stock sm/md/lg) keeps
34
+ // the design-system token set untouched and the dialog readable.
35
+ size: {
36
+ sm: "max-w-[24rem]",
37
+ md: "max-w-[28rem]",
38
+ lg: "max-w-[32rem]",
39
+ // The large peek surface (Subtask 2.5.19's quick view) — a generous
40
+ // dialog that takes a big part of the screen; pairs with a height +
41
+ // p-0 className on the consumer side. Stock Tailwind 4xl rem value, so
42
+ // the design-system --spacing-* scale stays untouched (see the comment
43
+ // above on why we pin rems instead of max-w-{key}).
44
+ xl: "max-w-[58rem]",
45
+ // The whole viewport (Subtask 5.2.6's attachment preview lightbox —
46
+ // the 2.5.19 growth pattern one step further). Like `xl`, the variant
47
+ // only lifts the size caps; the consumer pairs it with bg/border/
48
+ // radius/padding overrides to drop the panel chrome it doesn't want.
49
+ full: "h-dvh max-h-dvh w-screen max-w-none"
50
+ }
51
+ },
52
+ defaultVariants: { size: "md" }
53
+ }
54
+ );
55
+ function ModalRoot({
56
+ open,
57
+ onOpenChange,
58
+ title,
59
+ description,
60
+ size,
61
+ hideClose,
62
+ srTitle,
63
+ className,
64
+ overlayClassName,
65
+ role,
66
+ children,
67
+ closeLabel = "Close",
68
+ dialogLabel = "Dialog"
69
+ }) {
70
+ return /* @__PURE__ */ jsx(Dialog.Root, { open, onOpenChange, children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
71
+ /* @__PURE__ */ jsx(
72
+ Dialog.Overlay,
73
+ {
74
+ "data-surface": "overlay",
75
+ className: cn("fixed inset-0 z-40 bg-(--el-overlay-scrim)", overlayClassName)
76
+ }
77
+ ),
78
+ /* @__PURE__ */ jsxs(
79
+ Dialog.Content,
80
+ {
81
+ className: cn(contentVariants({ size }), className),
82
+ "data-surface": "modal",
83
+ ...role ? { role } : {},
84
+ ...description ? {} : { "aria-describedby": void 0 },
85
+ children: [
86
+ title || description ? /* @__PURE__ */ jsxs("div", { className: "mb-(--spacing-md) shrink-0", children: [
87
+ title ? /* @__PURE__ */ jsx(Dialog.Title, { className: "font-serif text-xl font-semibold text-(--el-text)", children: title }) : null,
88
+ description ? /* @__PURE__ */ jsx(Dialog.Description, { className: "text-(--el-text-subtitle) mt-1 font-sans text-sm", children: description }) : null
89
+ ] }) : (
90
+ // Radix requires Title for a11y; provide a visually-hidden one if
91
+ // missing. `srTitle` lets a consumer with its own visible heading
92
+ // still give the dialog a meaningful accessible name.
93
+ /* @__PURE__ */ jsx(Dialog.Title, { className: "sr-only", children: srTitle ?? dialogLabel })
94
+ ),
95
+ children,
96
+ !hideClose ? /* @__PURE__ */ jsx(
97
+ Dialog.Close,
98
+ {
99
+ "aria-label": closeLabel,
100
+ className: "text-(--el-icon-muted) hover:text-(--el-text) absolute right-3 top-3 rounded-(--radius-control) p-(--spacing-icon-btn) transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring-color)",
101
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
102
+ }
103
+ ) : null
104
+ ]
105
+ }
106
+ )
107
+ ] }) });
108
+ }
109
+ var ModalFooter = forwardRef(
110
+ function ModalFooter2({ className, children, ...rest }, ref) {
111
+ return /* @__PURE__ */ jsx(
112
+ "div",
113
+ {
114
+ ref,
115
+ className: cn(
116
+ "border-(--el-border) mt-(--spacing-md) flex items-center justify-end gap-2 border-t pt-(--spacing-md)",
117
+ className
118
+ ),
119
+ ...rest,
120
+ children
121
+ }
122
+ );
123
+ }
124
+ );
125
+ var ModalBody = forwardRef(
126
+ function ModalBody2({ className, children, ...rest }, ref) {
127
+ return /* @__PURE__ */ jsx(
128
+ "div",
129
+ {
130
+ ref,
131
+ className: cn(
132
+ "flex min-h-0 flex-1 flex-col overflow-y-auto",
133
+ // Ring-safe inset (see the component doc above): padding ≥ the focus
134
+ // ring's ~4px overhang + an equal negative margin to keep the gutter.
135
+ "-m-1.5 p-1.5",
136
+ className
137
+ ),
138
+ ...rest,
139
+ children
140
+ }
141
+ );
142
+ }
143
+ );
144
+ var ModalTrigger = Dialog.Trigger;
145
+ var Modal = Object.assign(ModalRoot, {
146
+ Body: ModalBody,
147
+ Footer: ModalFooter,
148
+ Trigger: ModalTrigger
149
+ });
150
+
151
+ export { Modal };
152
+ //# sourceMappingURL=Modal.js.map
153
+ //# sourceMappingURL=Modal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/Modal.tsx"],"names":["ModalFooter","ModalBody"],"mappings":";;;;;;;;AAcO,SAAS,MAAM,MAAA,EAA8B;AAClD,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC7B;ACgBA,IAAM,eAAA,GAAkB,GAAA;AAAA,EACtB,EAAA;AAAA,IACE,+DAAA;AAAA,IACA,qDAAA;AAAA,IACA,qDAAA;AAAA,IACA,4BAAA;AAAA,IACA,oBAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA;AAAA,GACF;AAAA,EACA;AAAA,IACE,QAAA,EAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOR,IAAA,EAAM;AAAA,QACJ,EAAA,EAAI,eAAA;AAAA,QACJ,EAAA,EAAI,eAAA;AAAA,QACJ,EAAA,EAAI,eAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAMJ,EAAA,EAAI,eAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAKJ,IAAA,EAAM;AAAA;AACR,KACF;AAAA,IACA,eAAA,EAAiB,EAAE,IAAA,EAAM,IAAA;AAAK;AAElC,CAAA;AA6CA,SAAS,SAAA,CAAU;AAAA,EACjB,IAAA;AAAA,EACA,YAAA;AAAA,EACA,KAAA;AAAA,EACA,WAAA;AAAA,EACA,IAAA;AAAA,EACA,SAAA;AAAA,EACA,OAAA;AAAA,EACA,SAAA;AAAA,EACA,gBAAA;AAAA,EACA,IAAA;AAAA,EACA,QAAA;AAAA,EACA,UAAA,GAAa,OAAA;AAAA,EACb,WAAA,GAAc;AAChB,CAAA,EAAe;AACb,EAAA,2BACU,MAAA,CAAA,IAAA,EAAP,EAAY,MAAY,YAAA,EACvB,QAAA,kBAAA,IAAA,CAAQ,eAAP,EAKC,QAAA,EAAA;AAAA,oBAAA,GAAA;AAAA,MAAQ,MAAA,CAAA,OAAA;AAAA,MAAP;AAAA,QACC,cAAA,EAAa,SAAA;AAAA,QACb,SAAA,EAAW,EAAA,CAAG,4CAAA,EAA8C,gBAAgB;AAAA;AAAA,KAC9E;AAAA,oBAWA,IAAA;AAAA,MAAQ,MAAA,CAAA,OAAA;AAAA,MAAP;AAAA,QACC,WAAW,EAAA,CAAG,eAAA,CAAgB,EAAE,IAAA,EAAM,GAAG,SAAS,CAAA;AAAA,QAGlD,cAAA,EAAa,OAAA;AAAA,QAGZ,GAAI,IAAA,GAAO,EAAE,IAAA,KAAS,EAAC;AAAA,QAIvB,GAAI,WAAA,GAAc,EAAC,GAAI,EAAE,oBAAoB,MAAA,EAAU;AAAA,QAEvD,QAAA,EAAA;AAAA,UAAA,KAAA,IAAS,WAAA,mBACR,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,4BAAA,EACZ,QAAA,EAAA;AAAA,YAAA,KAAA,uBACS,MAAA,CAAA,KAAA,EAAP,EAAa,SAAA,EAAU,mDAAA,EACrB,iBACH,CAAA,GACE,IAAA;AAAA,YACH,8BACC,GAAA,CAAQ,MAAA,CAAA,WAAA,EAAP,EAAmB,SAAA,EAAU,kDAAA,EAC3B,uBACH,CAAA,GACE;AAAA,WAAA,EACN,CAAA;AAAA;AAAA;AAAA;AAAA,gCAKQ,MAAA,CAAA,KAAA,EAAP,EAAa,SAAA,EAAU,SAAA,EAAW,qBAAW,WAAA,EAAY;AAAA,WAAA;AAAA,UAE3D,QAAA;AAAA,UACA,CAAC,SAAA,mBACA,GAAA;AAAA,YAAQ,MAAA,CAAA,KAAA;AAAA,YAAP;AAAA,cACC,YAAA,EAAY,UAAA;AAAA,cACZ,SAAA,EAAU,kOAAA;AAAA,cAEV,QAAA,kBAAA,GAAA,CAAC,CAAA,EAAA,EAAE,SAAA,EAAU,SAAA,EAAU;AAAA;AAAA,WACzB,GACE;AAAA;AAAA;AAAA;AACN,GAAA,EACF,CAAA,EACF,CAAA;AAEJ;AAEA,IAAM,WAAA,GAAc,UAAA;AAAA,EAClB,SAASA,aAAY,EAAE,SAAA,EAAW,UAAU,GAAG,IAAA,IAAQ,GAAA,EAAK;AAC1D,IAAA,uBACE,GAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,GAAA;AAAA,QACA,SAAA,EAAW,EAAA;AAAA,UACT,uGAAA;AAAA,UACA;AAAA,SACF;AAAA,QACC,GAAG,IAAA;AAAA,QAEH;AAAA;AAAA,KACH;AAAA,EAEJ;AACF,CAAA;AAqBA,IAAM,SAAA,GAAY,UAAA;AAAA,EAChB,SAASC,WAAU,EAAE,SAAA,EAAW,UAAU,GAAG,IAAA,IAAQ,GAAA,EAAK;AACxD,IAAA,uBACE,GAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,GAAA;AAAA,QACA,SAAA,EAAW,EAAA;AAAA,UACT,8CAAA;AAAA;AAAA;AAAA,UAGA,cAAA;AAAA,UACA;AAAA,SACF;AAAA,QACC,GAAG,IAAA;AAAA,QAEH;AAAA;AAAA,KACH;AAAA,EAEJ;AACF,CAAA;AAGA,IAAM,YAAA,GAAsB,MAAA,CAAA,OAAA;AAErB,IAAM,KAAA,GAAQ,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW;AAAA,EAC5C,IAAA,EAAM,SAAA;AAAA,EACN,MAAA,EAAQ,WAAA;AAAA,EACR,OAAA,EAAS;AACX,CAAC","file":"Modal.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * Merge Tailwind class names without conflicts.\n *\n * `clsx` handles conditional inclusion (booleans, undefineds, arrays);\n * `twMerge` resolves conflicting Tailwind classes (later wins, e.g.\n * `px-2 px-4` → `px-4`). Together they are the de facto standard for\n * composing Tailwind classes in React components.\n *\n * @example\n * cn('px-2 py-1', isActive && 'bg-primary', className)\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","'use client';\n\nimport { forwardRef, type ReactNode } from 'react';\nimport * as Dialog from '@radix-ui/react-dialog';\nimport { X } from 'lucide-react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\n\n/**\n * Modal — accessible dialog wrapping @radix-ui/react-dialog.\n *\n * Radix handles focus trap, ESC-to-close, click-outside-to-close, and\n * focus-return-on-close out of the box. We style on top.\n *\n * Open/close is controlled by the consumer via `open` + `onOpenChange`.\n *\n * For a scrollable body with a pinned footer, wrap the fields in `Modal.Body`\n * (it owns the `flex-1 overflow-y-auto` scroll recipe and keeps focus rings\n * from clipping against the scroll edge — see `ModalBody`).\n *\n * @example\n * const [open, setOpen] = useState(false);\n * <Modal open={open} onOpenChange={setOpen} title=\"Confirm\" size=\"md\">\n * <Modal.Body className=\"gap-4\">\n * <Input label=\"Name\" autoFocus />\n * </Modal.Body>\n * <Modal.Footer>\n * <Button variant=\"ghost\" onClick={() => setOpen(false)}>Cancel</Button>\n * <Button variant=\"primary\" onClick={confirm}>Confirm</Button>\n * </Modal.Footer>\n * </Modal>\n */\nconst contentVariants = cva(\n cn(\n 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',\n 'w-[90vw] rounded-(--radius-modal) bg-(--el-page-bg)',\n 'shadow-(--shadow-modal) border border-(--el-border)',\n 'p-(--spacing-card-padding)',\n 'focus:outline-none',\n // Never exceed the viewport: cap height and lay out as a column so a\n // consumer can wrap its fields in `Modal.Body` (which owns the\n // `flex-1 overflow-y-auto` scroll recipe) and pin a `Modal.Footer`,\n // instead of the dialog growing off-screen (e.g. the create modal's\n // expandable Explanation pushing the Create button out of view).\n 'flex max-h-[90vh] flex-col overflow-hidden',\n ),\n {\n variants: {\n // Literal widths, NOT the max-w-sm/md/lg utilities: the design\n // system's @theme defines --spacing-sm/md/lg (12/16/20px), and\n // Tailwind v4 resolves `max-w-{key}` against the --spacing-* scale\n // when that key exists — so `max-w-md` would collapse the modal to\n // 16px wide. Pinning the rem values (Tailwind's stock sm/md/lg) keeps\n // the design-system token set untouched and the dialog readable.\n size: {\n sm: 'max-w-[24rem]',\n md: 'max-w-[28rem]',\n lg: 'max-w-[32rem]',\n // The large peek surface (Subtask 2.5.19's quick view) — a generous\n // dialog that takes a big part of the screen; pairs with a height +\n // p-0 className on the consumer side. Stock Tailwind 4xl rem value, so\n // the design-system --spacing-* scale stays untouched (see the comment\n // above on why we pin rems instead of max-w-{key}).\n xl: 'max-w-[58rem]',\n // The whole viewport (Subtask 5.2.6's attachment preview lightbox —\n // the 2.5.19 growth pattern one step further). Like `xl`, the variant\n // only lifts the size caps; the consumer pairs it with bg/border/\n // radius/padding overrides to drop the panel chrome it doesn't want.\n full: 'h-dvh max-h-dvh w-screen max-w-none',\n },\n },\n defaultVariants: { size: 'md' },\n },\n);\n\nexport interface ModalProps extends VariantProps<typeof contentVariants> {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title?: string;\n description?: string;\n children?: ReactNode;\n /** Hide the default close (×) button in the corner. */\n hideClose?: boolean;\n /**\n * Accessible name for a dialog that renders its OWN visible heading (so no\n * `title` is passed). Radix requires a Dialog.Title; without `title`/`srTitle`\n * we fall back to the generic \"Dialog\". Supply this to label the dialog with\n * something meaningful (e.g. the previewed issue's key) while keeping the\n * heading inside `children` (Subtask 2.5.19's quick-view peek).\n */\n srTitle?: string;\n className?: string;\n /**\n * Extra classes merged onto the backdrop — e.g. the attachment preview\n * lightbox (Subtask 5.2.6) deepens the default `--el-overlay-scrim` to\n * `bg-black/80` per `design/work-items/attachments.mock.html` panel 6.\n */\n overlayClassName?: string;\n /**\n * The dialog's ARIA role. Defaults to Radix's `dialog`; a DESTRUCTIVE confirm\n * passes `alertdialog` (Subtask 2.8.4's delete confirm) so assistive tech\n * treats it as an alert that interrupts and announces its description.\n */\n role?: 'dialog' | 'alertdialog';\n /**\n * Accessible label for the corner close (×) button. Defaults to `'Close'`.\n * i18n is the consuming app's concern — the design-system package is\n * framework-agnostic (no `next-intl` dependency), so a localized app passes\n * the translated string here (motir-core wires it from its `common.close`).\n */\n closeLabel?: string;\n /**\n * Fallback accessible name when neither `title` nor `srTitle` is given.\n * Defaults to `'Dialog'`. See `closeLabel` for the i18n rationale.\n */\n dialogLabel?: string;\n}\n\nfunction ModalRoot({\n open,\n onOpenChange,\n title,\n description,\n size,\n hideClose,\n srTitle,\n className,\n overlayClassName,\n role,\n children,\n closeLabel = 'Close',\n dialogLabel = 'Dialog',\n}: ModalProps) {\n return (\n <Dialog.Root open={open} onOpenChange={onOpenChange}>\n <Dialog.Portal>\n {/* `data-surface=\"overlay\"` lets the glassmorphism style turn this dark\n scrim into a LIGHT, page-blurring backdrop, so the frosted modal\n refracts the (blurred) page behind it instead of darkness — the\n point of a glass modal. Inert for every other style. */}\n <Dialog.Overlay\n data-surface=\"overlay\"\n className={cn('fixed inset-0 z-40 bg-(--el-overlay-scrim)', overlayClassName)}\n />\n {/*\n PRODECT_FINDINGS #8: Radix checks for a describing element\n (Dialog.Description → aria-describedby) independently of the Title\n check. When no `description` prop is passed we render no\n Dialog.Description, so Radix logs a \"Missing Description or\n aria-describedby={undefined}\" warning. We pick the explicit opt-out\n (aria-describedby={undefined}) rather than emitting an empty sr-only\n Description — an empty description announces nothing useful and is\n worse for screen-reader users than declaring the dialog has none.\n */}\n <Dialog.Content\n className={cn(contentVariants({ size }), className)}\n // `data-surface` lets a surface-material style (glassmorphism) frost\n // the dialog panel — see globals.css's material layer.\n data-surface=\"modal\"\n // A destructive confirm opts into `alertdialog` (2.8.4); otherwise\n // Radix's default `dialog` role applies.\n {...(role ? { role } : {})}\n // Only opt out when there's no description. When `description` is\n // set we omit the prop entirely so Radix keeps auto-wiring\n // aria-describedby to the rendered Dialog.Description's id.\n {...(description ? {} : { 'aria-describedby': undefined })}\n >\n {title || description ? (\n <div className=\"mb-(--spacing-md) shrink-0\">\n {title ? (\n <Dialog.Title className=\"font-serif text-xl font-semibold text-(--el-text)\">\n {title}\n </Dialog.Title>\n ) : null}\n {description ? (\n <Dialog.Description className=\"text-(--el-text-subtitle) mt-1 font-sans text-sm\">\n {description}\n </Dialog.Description>\n ) : null}\n </div>\n ) : (\n // Radix requires Title for a11y; provide a visually-hidden one if\n // missing. `srTitle` lets a consumer with its own visible heading\n // still give the dialog a meaningful accessible name.\n <Dialog.Title className=\"sr-only\">{srTitle ?? dialogLabel}</Dialog.Title>\n )}\n {children}\n {!hideClose ? (\n <Dialog.Close\n aria-label={closeLabel}\n className=\"text-(--el-icon-muted) hover:text-(--el-text) absolute right-3 top-3 rounded-(--radius-control) p-(--spacing-icon-btn) transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring-color)\"\n >\n <X className=\"h-4 w-4\" />\n </Dialog.Close>\n ) : null}\n </Dialog.Content>\n </Dialog.Portal>\n </Dialog.Root>\n );\n}\n\nconst ModalFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function ModalFooter({ className, children, ...rest }, ref) {\n return (\n <div\n ref={ref}\n className={cn(\n 'border-(--el-border) mt-(--spacing-md) flex items-center justify-end gap-2 border-t pt-(--spacing-md)',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n );\n },\n);\n\n/**\n * ModalBody — the scrollable field area of a modal. Owns the scroll recipe\n * once so consumers don't re-derive it (and re-introduce the focus-ring clip):\n * it fills the remaining column height (the panel caps at `max-h-[90vh]`) and\n * scrolls, so a sibling `Modal.Footer` stays pinned.\n *\n * Ring-safe by construction. `Input` draws its focus ring as\n * `focus-within:ring-2 … ring-offset-2` — a box-shadow extending ~4px OUTSIDE\n * the field's border box. Per the CSS spec, `overflow-y: auto` forces the\n * computed `overflow-x` from `visible` to `auto`, so this body is a clip box on\n * BOTH axes; a full-width field's ring overhang would be painted outside it and\n * silently clipped (rings are box-shadows — they never grow the layout box, so\n * nothing scrolls, the paint is just cut). We pad the scroll container by more\n * than the overhang (`p-1.5` = 6px > 4px) and pull an equal negative margin\n * (`-m-1.5`), so the ring gets room while the body's visual gutter — and the\n * fields' alignment with the modal title/footer — is unchanged.\n *\n * Pass `gap-*` (and any extra layout) via `className`.\n */\nconst ModalBody = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n function ModalBody({ className, children, ...rest }, ref) {\n return (\n <div\n ref={ref}\n className={cn(\n 'flex min-h-0 flex-1 flex-col overflow-y-auto',\n // Ring-safe inset (see the component doc above): padding ≥ the focus\n // ring's ~4px overhang + an equal negative margin to keep the gutter.\n '-m-1.5 p-1.5',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n );\n },\n);\n\n/** Convenience trigger — wires children to Radix's DialogTrigger. */\nconst ModalTrigger = Dialog.Trigger;\n\nexport const Modal = Object.assign(ModalRoot, {\n Body: ModalBody,\n Footer: ModalFooter,\n Trigger: ModalTrigger,\n});\n"]}
@@ -0,0 +1,73 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ComponentType, ReactNode } from 'react';
3
+
4
+ /** One of the six `--el-tint-*` pastel token names (chip + swatch colour). */
5
+ type MultiSelectTint = 'peach' | 'rose' | 'mint' | 'lavender' | 'sky' | 'yellow';
6
+ interface MultiSelectOption {
7
+ id: string;
8
+ label: string;
9
+ /** Pastel tint for the chip + the option row's swatch dot; absent = neutral. */
10
+ tint?: MultiSelectTint;
11
+ /** Glyph for the option row's 22px slot and the chip (e.g. the component glyph). */
12
+ glyph?: ComponentType<{
13
+ className?: string;
14
+ }>;
15
+ }
16
+ interface MultiSelectPickerProps {
17
+ /** The selected values, rendered as chips. */
18
+ values: MultiSelectOption[];
19
+ /** The current option set — the caller filters/fetches; no fetching inside. */
20
+ options: MultiSelectOption[];
21
+ /** Toggle an option from the listbox (add when unselected, remove when selected). */
22
+ onToggle: (option: MultiSelectOption) => void;
23
+ /** Remove a chip (the × button / Backspace-on-empty path). */
24
+ onRemove: (value: MultiSelectOption) => void;
25
+ /**
26
+ * Folksonomy create — when present, a query matching no option (and no
27
+ * selected value) case-insensitively appends the create-row.
28
+ */
29
+ onCreate?: (name: string) => void;
30
+ /** The controlled filter query (the caller debounces its fetch off this). */
31
+ query: string;
32
+ onQueryChange: (query: string) => void;
33
+ /** Per-value cap — at the cap the input disables; chips stay removable. */
34
+ cap?: number;
35
+ /** Accessible name for the input + the listbox. */
36
+ label: string;
37
+ placeholder: string;
38
+ /** Localized create-row text for the current query (e.g. "Create 'perf-q3'"). */
39
+ createLabel?: (query: string) => string;
40
+ /** Localized accessible name for a chip's remove button. */
41
+ removeLabel: (label: string) => string;
42
+ /** Listbox empty state (e.g. "No components defined"). */
43
+ emptyText?: string;
44
+ /** Quiet line under the box (cap notice, admin link) — the mock's field-hint. */
45
+ hint?: ReactNode;
46
+ /** Inline error (the typed 422), announced via role="alert". */
47
+ error?: string | null;
48
+ /** Disables the whole control (a pending action). */
49
+ disabled?: boolean;
50
+ /** Extra classes for the chip-input BOX (the bordered element) — e.g. the
51
+ * filter builder's dashed pending-row treatment (Subtask 6.1.4). */
52
+ className?: string;
53
+ }
54
+ /**
55
+ * A value chip — exported so the rail cards' DISPLAY / read-only modes render
56
+ * the exact same chip the picker edits (mock panels 0 / 2 / 3). Neutral by
57
+ * default (the Pill-neutral colours); a `tint` puts the hue in the background
58
+ * with `--el-text-strong` text (finding #35 AA). With `onRemove` the trailing
59
+ * × renders (12px, accessible name via `removeLabel`).
60
+ */
61
+ declare function ValueChip({ option, onRemove, removeLabel, disabled, }: {
62
+ option: MultiSelectOption;
63
+ onRemove?: (value: MultiSelectOption) => void;
64
+ removeLabel?: (label: string) => string;
65
+ disabled?: boolean;
66
+ }): react_jsx_runtime.JSX.Element;
67
+ /** A pulse-skeleton chip (the mock's panel-6 loading grammar, at chip size). */
68
+ declare function ChipSkeleton({ className }: {
69
+ className?: string;
70
+ }): react_jsx_runtime.JSX.Element;
71
+ declare function MultiSelectPicker({ values, options, onToggle, onRemove, onCreate, query, onQueryChange, cap, label, placeholder, createLabel, removeLabel, emptyText, hint, error, disabled, className, }: MultiSelectPickerProps): react_jsx_runtime.JSX.Element;
72
+
73
+ export { ChipSkeleton, type MultiSelectOption, MultiSelectPicker, type MultiSelectPickerProps, type MultiSelectTint, ValueChip };
@@ -0,0 +1,358 @@
1
+ 'use client';
2
+ import { useId, useRef, useState, useEffect, useCallback, useSyncExternalStore, useLayoutEffect } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { X, Check, Plus, TriangleAlert } from 'lucide-react';
5
+ import { clsx } from 'clsx';
6
+ import { twMerge } from 'tailwind-merge';
7
+ import { jsxs, jsx } from 'react/jsx-runtime';
8
+
9
+ function cn(...inputs) {
10
+ return twMerge(clsx(inputs));
11
+ }
12
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
13
+ var subscribeNoop = () => () => {
14
+ };
15
+ function useMounted() {
16
+ return useSyncExternalStore(
17
+ subscribeNoop,
18
+ () => true,
19
+ () => false
20
+ );
21
+ }
22
+ var TINT_CHIP = {
23
+ peach: "bg-(--el-label-1)",
24
+ rose: "bg-(--el-label-2)",
25
+ mint: "bg-(--el-label-3)",
26
+ lavender: "bg-(--el-label-4)",
27
+ sky: "bg-(--el-label-5)",
28
+ yellow: "bg-(--el-label-6)"
29
+ };
30
+ function ValueChip({
31
+ option,
32
+ onRemove,
33
+ removeLabel,
34
+ disabled
35
+ }) {
36
+ const Glyph = option.glyph;
37
+ return /* @__PURE__ */ jsxs(
38
+ "span",
39
+ {
40
+ className: cn(
41
+ "inline-flex max-w-full items-center gap-1.5 rounded-(--radius-badge) px-(--spacing-chip-x) py-(--spacing-chip-y) font-sans text-xs font-medium",
42
+ option.tint ? cn(TINT_CHIP[option.tint], "text-(--el-text-strong)") : "bg-(--el-chip-bg) text-(--el-text-secondary) border border-(--el-chip-border)"
43
+ ),
44
+ children: [
45
+ Glyph ? /* @__PURE__ */ jsx(Glyph, { className: "h-3 w-3 shrink-0 text-(--el-icon-muted)", "aria-hidden": true }) : null,
46
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: option.label }),
47
+ onRemove ? /* @__PURE__ */ jsx(
48
+ "button",
49
+ {
50
+ type: "button",
51
+ disabled,
52
+ onClick: () => onRemove(option),
53
+ "aria-label": removeLabel ? removeLabel(option.label) : option.label,
54
+ className: cn(
55
+ "-mr-1 inline-flex items-center justify-center rounded-(--radius-badge) p-px focus-visible:ring-2 focus-visible:ring-(--focus-ring-color) focus-visible:outline-none",
56
+ option.tint ? "text-(--el-text-strong) opacity-65 hover:bg-(--el-text-strong)/10 hover:opacity-100" : "text-(--el-icon-muted) hover:bg-(--el-muted) hover:text-(--el-text)"
57
+ ),
58
+ children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3", "aria-hidden": true })
59
+ }
60
+ ) : null
61
+ ]
62
+ }
63
+ );
64
+ }
65
+ function ChipSkeleton({ className }) {
66
+ return /* @__PURE__ */ jsx(
67
+ "span",
68
+ {
69
+ className: cn("h-[22px] animate-pulse rounded-(--radius-badge) bg-(--el-muted)", className),
70
+ "aria-hidden": true
71
+ }
72
+ );
73
+ }
74
+ function MultiSelectPicker({
75
+ values,
76
+ options,
77
+ onToggle,
78
+ onRemove,
79
+ onCreate,
80
+ query,
81
+ onQueryChange,
82
+ cap,
83
+ label,
84
+ placeholder,
85
+ createLabel,
86
+ removeLabel,
87
+ emptyText,
88
+ hint,
89
+ error,
90
+ disabled,
91
+ className
92
+ }) {
93
+ const baseId = useId();
94
+ const listId = `${baseId}-listbox`;
95
+ const rootRef = useRef(null);
96
+ const boxRef = useRef(null);
97
+ const inputRef = useRef(null);
98
+ const menuRef = useRef(null);
99
+ const [open, setOpen] = useState(false);
100
+ const [activeIndex, setActiveIndex] = useState(0);
101
+ const mounted = useMounted();
102
+ const [inModal, setInModal] = useState(false);
103
+ const [menuStyle, setMenuStyle] = useState(null);
104
+ const [listMaxHeight, setListMaxHeight] = useState(256);
105
+ const atCap = cap != null && values.length >= cap;
106
+ const inputDisabled = disabled || atCap;
107
+ const trimmed = query.trim();
108
+ const lower = trimmed.toLowerCase();
109
+ const hasExactMatch = options.some((o) => o.label.toLowerCase() === lower) || values.some((v) => v.label.toLowerCase() === lower);
110
+ const showCreate = !!onCreate && trimmed.length > 0 && !hasExactMatch;
111
+ const rowCount = options.length + (showCreate ? 1 : 0);
112
+ const clampedActive = rowCount === 0 ? 0 : Math.min(activeIndex, rowCount - 1);
113
+ const activeId = open && rowCount > 0 ? `${baseId}-opt-${clampedActive}` : void 0;
114
+ useEffect(() => {
115
+ if (!open) return;
116
+ function onPointerDown(e) {
117
+ const t = e.target;
118
+ if (rootRef.current?.contains(t) || menuRef.current?.contains(t)) return;
119
+ setOpen(false);
120
+ }
121
+ document.addEventListener("pointerdown", onPointerDown);
122
+ return () => document.removeEventListener("pointerdown", onPointerDown);
123
+ }, [open]);
124
+ const updatePosition = useCallback(() => {
125
+ const box = boxRef.current;
126
+ if (!box) return;
127
+ const rect = box.getBoundingClientRect();
128
+ const gap = 4;
129
+ const viewportH = window.innerHeight;
130
+ const spaceBelow = viewportH - rect.bottom - gap;
131
+ const spaceAbove = rect.top - gap;
132
+ const placeBelow = spaceBelow >= spaceAbove;
133
+ const avail = Math.max(120, placeBelow ? spaceBelow : spaceAbove);
134
+ setListMaxHeight(Math.max(80, Math.min(256, avail - 8)));
135
+ const style = {
136
+ position: "fixed",
137
+ left: Math.round(rect.left),
138
+ minWidth: Math.round(rect.width)
139
+ };
140
+ if (placeBelow) style.top = Math.round(rect.bottom + gap);
141
+ else style.bottom = Math.round(viewportH - rect.top + gap);
142
+ setMenuStyle(style);
143
+ }, []);
144
+ useIsomorphicLayoutEffect(() => {
145
+ if (!open) return;
146
+ setInModal(!!boxRef.current?.closest('[data-surface="modal"],[aria-modal="true"]'));
147
+ }, [open]);
148
+ useIsomorphicLayoutEffect(() => {
149
+ if (!(open && mounted) || inModal) return;
150
+ updatePosition();
151
+ const onReflow = () => updatePosition();
152
+ window.addEventListener("scroll", onReflow, true);
153
+ window.addEventListener("resize", onReflow);
154
+ return () => {
155
+ window.removeEventListener("scroll", onReflow, true);
156
+ window.removeEventListener("resize", onReflow);
157
+ };
158
+ }, [open, mounted, inModal, updatePosition, rowCount]);
159
+ function commitRow(index) {
160
+ const option = options[index];
161
+ if (option) onToggle(option);
162
+ else if (showCreate) onCreate?.(trimmed);
163
+ }
164
+ function onKeyDown(e) {
165
+ if (e.key === "Escape") {
166
+ if (open) {
167
+ e.preventDefault();
168
+ e.stopPropagation();
169
+ setOpen(false);
170
+ }
171
+ return;
172
+ }
173
+ if (e.key === "Backspace" && query.length === 0) {
174
+ const last = values[values.length - 1];
175
+ if (last) onRemove(last);
176
+ return;
177
+ }
178
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
179
+ e.preventDefault();
180
+ if (!open) {
181
+ setOpen(true);
182
+ return;
183
+ }
184
+ if (rowCount === 0) return;
185
+ const delta = e.key === "ArrowDown" ? 1 : -1;
186
+ setActiveIndex((clampedActive + delta + rowCount) % rowCount);
187
+ return;
188
+ }
189
+ if (e.key === "Enter") {
190
+ e.preventDefault();
191
+ if (open && rowCount > 0) commitRow(clampedActive);
192
+ }
193
+ }
194
+ const wrapMenu = (el) => inModal ? el : createPortal(el, document.body);
195
+ return (
196
+ // `data-inner-dismiss` (set while the listbox is open) lets an enclosing
197
+ // Radix layer's `onEscapeKeyDown` defer to us: Radix listens for Escape
198
+ // in the CAPTURE phase at the document, so our own bubble-phase handler
199
+ // alone can't stop it from dismissing the whole popover/dialog first
200
+ // (the 6.1.4 filter builder hit exactly this).
201
+ /* @__PURE__ */ jsxs("div", { ref: rootRef, "data-inner-dismiss": open ? true : void 0, className: "relative font-sans", children: [
202
+ /* @__PURE__ */ jsxs(
203
+ "div",
204
+ {
205
+ ref: boxRef,
206
+ onClick: () => inputRef.current?.focus(),
207
+ className: cn(
208
+ "flex min-h-(--height-input) cursor-text flex-wrap items-center gap-1.5 rounded-(--radius-input) border border-(--el-border) bg-(--el-page-bg) px-(--spacing-control-x) py-(--spacing-control-y)",
209
+ "focus-within:ring-2 focus-within:ring-(--focus-ring-color) focus-within:ring-offset-1",
210
+ inputDisabled && "cursor-not-allowed bg-(--el-surface-soft)",
211
+ className
212
+ ),
213
+ children: [
214
+ values.map((v) => /* @__PURE__ */ jsx(
215
+ ValueChip,
216
+ {
217
+ option: v,
218
+ onRemove,
219
+ removeLabel,
220
+ disabled
221
+ },
222
+ v.id
223
+ )),
224
+ /* @__PURE__ */ jsx(
225
+ "input",
226
+ {
227
+ ref: inputRef,
228
+ role: "combobox",
229
+ "aria-expanded": open,
230
+ "aria-controls": open ? listId : void 0,
231
+ "aria-activedescendant": activeId,
232
+ "aria-autocomplete": "list",
233
+ "aria-label": label,
234
+ disabled: inputDisabled,
235
+ value: query,
236
+ placeholder: values.length === 0 ? placeholder : void 0,
237
+ onChange: (e) => {
238
+ onQueryChange(e.target.value);
239
+ setOpen(true);
240
+ setActiveIndex(0);
241
+ },
242
+ onFocus: () => setOpen(true),
243
+ onKeyDown,
244
+ className: "min-w-[60px] flex-1 border-0 bg-transparent py-0.5 text-sm text-(--el-text) outline-none placeholder:text-(--el-text-muted) disabled:cursor-not-allowed"
245
+ }
246
+ )
247
+ ]
248
+ }
249
+ ),
250
+ open && mounted ? wrapMenu(
251
+ /* @__PURE__ */ jsxs(
252
+ "div",
253
+ {
254
+ ref: menuRef,
255
+ id: listId,
256
+ role: "listbox",
257
+ "aria-multiselectable": "true",
258
+ "aria-label": label,
259
+ "data-menu-surface": "",
260
+ style: inModal ? { maxHeight: listMaxHeight } : {
261
+ ...menuStyle ?? { position: "fixed", visibility: "hidden" },
262
+ maxHeight: listMaxHeight
263
+ },
264
+ className: cn(
265
+ "w-max max-w-72 min-w-60 overflow-y-auto rounded-(--radius-card) border border-(--el-border) bg-(--el-page-bg) p-1 shadow-(--shadow-elevated)",
266
+ inModal ? "absolute top-full left-0 z-10 mt-1 max-h-64" : "z-50"
267
+ ),
268
+ children: [
269
+ options.map((opt, i) => {
270
+ const selected = values.some((v) => v.id === opt.id);
271
+ const Glyph = opt.glyph;
272
+ return /* @__PURE__ */ jsxs(
273
+ "button",
274
+ {
275
+ id: `${baseId}-opt-${i}`,
276
+ type: "button",
277
+ role: "option",
278
+ "aria-selected": selected,
279
+ disabled,
280
+ onMouseDown: (e) => e.preventDefault(),
281
+ onClick: () => commitRow(i),
282
+ onMouseEnter: () => setActiveIndex(i),
283
+ className: cn(
284
+ "flex w-full items-center gap-2.5 rounded-(--radius-control) px-(--spacing-control-x) py-(--spacing-control-y) text-left text-sm text-(--el-text) hover:bg-(--el-option-active-bg) focus-visible:outline-none",
285
+ i === clampedActive && "bg-(--el-option-active-bg)"
286
+ ),
287
+ children: [
288
+ /* @__PURE__ */ jsx("span", { className: "flex w-[22px] shrink-0 items-center justify-center text-(--el-icon-muted)", children: opt.tint ? /* @__PURE__ */ jsx(
289
+ "span",
290
+ {
291
+ "aria-hidden": true,
292
+ className: cn(
293
+ "h-2.5 w-2.5 rounded-full border border-(--el-border)",
294
+ TINT_CHIP[opt.tint]
295
+ )
296
+ }
297
+ ) : Glyph ? /* @__PURE__ */ jsx(Glyph, { className: "h-3.5 w-3.5", "aria-hidden": true }) : null }),
298
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: opt.label }),
299
+ /* @__PURE__ */ jsx(
300
+ Check,
301
+ {
302
+ "aria-hidden": true,
303
+ className: cn(
304
+ "h-4 w-4 shrink-0 text-(--el-accent-on-surface)",
305
+ selected ? "opacity-100" : "opacity-0"
306
+ )
307
+ }
308
+ )
309
+ ]
310
+ },
311
+ opt.id
312
+ );
313
+ }),
314
+ showCreate ? /* @__PURE__ */ jsxs(
315
+ "button",
316
+ {
317
+ id: `${baseId}-opt-${options.length}`,
318
+ type: "button",
319
+ role: "option",
320
+ "aria-selected": false,
321
+ disabled,
322
+ onMouseDown: (e) => e.preventDefault(),
323
+ onClick: () => commitRow(options.length),
324
+ onMouseEnter: () => setActiveIndex(options.length),
325
+ className: cn(
326
+ "flex w-full items-center gap-2.5 rounded-(--radius-control) px-(--spacing-control-x) py-(--spacing-control-y) text-left text-sm text-(--el-text) hover:bg-(--el-option-active-bg) focus-visible:outline-none",
327
+ clampedActive === options.length && "bg-(--el-option-active-bg)"
328
+ ),
329
+ children: [
330
+ /* @__PURE__ */ jsx("span", { className: "flex w-[22px] shrink-0 items-center justify-center text-(--el-accent-on-surface)", children: /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5", "aria-hidden": true }) }),
331
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: createLabel ? createLabel(trimmed) : trimmed })
332
+ ]
333
+ }
334
+ ) : null,
335
+ options.length === 0 && !showCreate ? /* @__PURE__ */ jsx("div", { className: "px-(--spacing-control-x) py-(--spacing-control-y) text-sm text-(--el-text-muted)", children: emptyText }) : null
336
+ ]
337
+ }
338
+ )
339
+ ) : null,
340
+ error ? /* @__PURE__ */ jsxs(
341
+ "p",
342
+ {
343
+ role: "alert",
344
+ className: "mt-1.5 flex items-center gap-1.5 font-sans text-xs text-(--el-danger)",
345
+ children: [
346
+ /* @__PURE__ */ jsx(TriangleAlert, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": true }),
347
+ error
348
+ ]
349
+ }
350
+ ) : null,
351
+ !error && hint ? /* @__PURE__ */ jsx("div", { className: "mt-1.5 font-sans text-xs text-(--el-text-helper)", children: hint }) : null
352
+ ] })
353
+ );
354
+ }
355
+
356
+ export { ChipSkeleton, MultiSelectPicker, ValueChip };
357
+ //# sourceMappingURL=MultiSelectPicker.js.map
358
+ //# sourceMappingURL=MultiSelectPicker.js.map