@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,39 @@
1
+ import { forwardRef } from 'react';
2
+ import { cva } from 'class-variance-authority';
3
+ import { clsx } from 'clsx';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import { jsx } from 'react/jsx-runtime';
6
+
7
+ // src/components/ui/Spinner.tsx
8
+ function cn(...inputs) {
9
+ return twMerge(clsx(inputs));
10
+ }
11
+ var spinnerVariants = cva(
12
+ "inline-block animate-spin rounded-full border-current border-t-transparent",
13
+ {
14
+ variants: {
15
+ size: {
16
+ sm: "h-4 w-4 border-[1.5px]",
17
+ md: "h-5 w-5 border-2",
18
+ lg: "h-8 w-8 border-[3px]"
19
+ }
20
+ },
21
+ defaultVariants: { size: "md" }
22
+ }
23
+ );
24
+ var Spinner = forwardRef(function Spinner2({ className, size, ...rest }, ref) {
25
+ return /* @__PURE__ */ jsx(
26
+ "span",
27
+ {
28
+ ref,
29
+ role: "status",
30
+ "aria-label": rest["aria-label"] ?? "Loading",
31
+ className: cn(spinnerVariants({ size }), className),
32
+ ...rest
33
+ }
34
+ );
35
+ });
36
+
37
+ export { Spinner };
38
+ //# sourceMappingURL=Spinner.js.map
39
+ //# sourceMappingURL=Spinner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/Spinner.tsx"],"names":["Spinner"],"mappings":";;;;;;;AAcO,SAAS,MAAM,MAAA,EAA8B;AAClD,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC7B;ACHA,IAAM,eAAA,GAAkB,GAAA;AAAA,EACtB,4EAAA;AAAA,EACA;AAAA,IACE,QAAA,EAAU;AAAA,MACR,IAAA,EAAM;AAAA,QACJ,EAAA,EAAI,wBAAA;AAAA,QACJ,EAAA,EAAI,kBAAA;AAAA,QACJ,EAAA,EAAI;AAAA;AACN,KACF;AAAA,IACA,eAAA,EAAiB,EAAE,IAAA,EAAM,IAAA;AAAK;AAElC,CAAA;AAKO,IAAM,OAAA,GAAU,UAAA,CAA0C,SAASA,QAAAA,CACxE,EAAE,WAAW,IAAA,EAAM,GAAG,IAAA,EAAK,EAC3B,GAAA,EACA;AACA,EAAA,uBACE,GAAA;AAAA,IAAC,MAAA;AAAA,IAAA;AAAA,MACC,GAAA;AAAA,MACA,IAAA,EAAK,QAAA;AAAA,MACL,YAAA,EAAY,IAAA,CAAK,YAAY,CAAA,IAAK,SAAA;AAAA,MAClC,WAAW,EAAA,CAAG,eAAA,CAAgB,EAAE,IAAA,EAAM,GAAG,SAAS,CAAA;AAAA,MACjD,GAAG;AAAA;AAAA,GACN;AAEJ,CAAC","file":"Spinner.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","import { forwardRef, type HTMLAttributes } from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\n\n/**\n * Spinner — pure CSS rotation indicator.\n *\n * Three sizes; works standalone or inside a Button's loading state.\n *\n * @example\n * <Spinner size=\"md\" />\n * <Spinner size=\"sm\" aria-label=\"Loading\" />\n */\nconst spinnerVariants = cva(\n 'inline-block animate-spin rounded-full border-current border-t-transparent',\n {\n variants: {\n size: {\n sm: 'h-4 w-4 border-[1.5px]',\n md: 'h-5 w-5 border-2',\n lg: 'h-8 w-8 border-[3px]',\n },\n },\n defaultVariants: { size: 'md' },\n },\n);\n\nexport interface SpinnerProps\n extends Omit<HTMLAttributes<HTMLSpanElement>, 'role'>, VariantProps<typeof spinnerVariants> {}\n\nexport const Spinner = forwardRef<HTMLSpanElement, SpinnerProps>(function Spinner(\n { className, size, ...rest },\n ref,\n) {\n return (\n <span\n ref={ref}\n role=\"status\"\n aria-label={rest['aria-label'] ?? 'Loading'}\n className={cn(spinnerVariants({ size }), className)}\n {...rest}\n />\n );\n});\n"]}
@@ -0,0 +1,34 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ /**
4
+ * Switch — a sliding on/off toggle (`role="switch"`), keyboard-operable.
5
+ *
6
+ * The shared primitive the design system names (notifications preferences
7
+ * matrix, automation rule enable, …). Colour flows through `--el-*`; the knob +
8
+ * track are genuinely-circular (`rounded-full`), the shape-rule carve-out for a
9
+ * pill control. Always pass an `aria-label` (or wire an external `<label>` via
10
+ * `id`) so the toggle is screen-reader-legible — a bare switch announces no
11
+ * purpose.
12
+ *
13
+ * @example
14
+ * <Switch checked={on} onCheckedChange={setOn} aria-label="Email for Mentioned" />
15
+ */
16
+ interface SwitchProps {
17
+ checked: boolean;
18
+ onCheckedChange: (next: boolean) => void;
19
+ disabled?: boolean;
20
+ /** Accessible name — required unless an external `<label htmlFor=id>` exists. */
21
+ 'aria-label'?: string;
22
+ /**
23
+ * Accessible name by reference — the id of a visible label element. Use this
24
+ * instead of `aria-label` when the label already renders on screen (so the
25
+ * name stays in sync with the visible text). One of `aria-label` /
26
+ * `aria-labelledby` should be set, else the switch announces no purpose.
27
+ */
28
+ 'aria-labelledby'?: string;
29
+ id?: string;
30
+ className?: string;
31
+ }
32
+ declare function Switch({ checked, onCheckedChange, disabled, id, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, }: SwitchProps): react_jsx_runtime.JSX.Element;
33
+
34
+ export { Switch, type SwitchProps };
@@ -0,0 +1,50 @@
1
+ import { clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ // src/utils/cn.ts
6
+ function cn(...inputs) {
7
+ return twMerge(clsx(inputs));
8
+ }
9
+ function Switch({
10
+ checked,
11
+ onCheckedChange,
12
+ disabled,
13
+ id,
14
+ className,
15
+ "aria-label": ariaLabel,
16
+ "aria-labelledby": ariaLabelledby
17
+ }) {
18
+ return /* @__PURE__ */ jsx(
19
+ "button",
20
+ {
21
+ type: "button",
22
+ role: "switch",
23
+ id,
24
+ "aria-checked": checked,
25
+ "aria-label": ariaLabel,
26
+ "aria-labelledby": ariaLabelledby,
27
+ disabled,
28
+ onClick: () => onCheckedChange(!checked),
29
+ className: cn(
30
+ "relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border transition-colors focus-visible:ring-2 focus-visible:ring-(--focus-ring-color) focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
31
+ checked ? "border-(--el-switch-on) bg-(--el-switch-on)" : "border-(--el-border-strong) bg-(--el-muted)",
32
+ className
33
+ ),
34
+ children: /* @__PURE__ */ jsx(
35
+ "span",
36
+ {
37
+ "aria-hidden": true,
38
+ className: cn(
39
+ "inline-block size-3.5 rounded-full bg-(--el-switch-knob) shadow-(--shadow-subtle) transition-transform",
40
+ checked ? "translate-x-[18px]" : "translate-x-0.5"
41
+ )
42
+ }
43
+ )
44
+ }
45
+ );
46
+ }
47
+
48
+ export { Switch };
49
+ //# sourceMappingURL=Switch.js.map
50
+ //# sourceMappingURL=Switch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/Switch.tsx"],"names":[],"mappings":";;;;;AAcO,SAAS,MAAM,MAAA,EAA8B;AAClD,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC7B;ACgBO,SAAS,MAAA,CAAO;AAAA,EACrB,OAAA;AAAA,EACA,eAAA;AAAA,EACA,QAAA;AAAA,EACA,EAAA;AAAA,EACA,SAAA;AAAA,EACA,YAAA,EAAc,SAAA;AAAA,EACd,iBAAA,EAAmB;AACrB,CAAA,EAAgB;AACd,EAAA,uBACE,GAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACC,IAAA,EAAK,QAAA;AAAA,MACL,IAAA,EAAK,QAAA;AAAA,MACL,EAAA;AAAA,MACA,cAAA,EAAc,OAAA;AAAA,MACd,YAAA,EAAY,SAAA;AAAA,MACZ,iBAAA,EAAiB,cAAA;AAAA,MACjB,QAAA;AAAA,MACA,OAAA,EAAS,MAAM,eAAA,CAAgB,CAAC,OAAO,CAAA;AAAA,MACvC,SAAA,EAAW,EAAA;AAAA,QACT,kOAAA;AAAA,QACA,UACI,6CAAA,GACA,6CAAA;AAAA,QACJ;AAAA,OACF;AAAA,MAEA,QAAA,kBAAA,GAAA;AAAA,QAAC,MAAA;AAAA,QAAA;AAAA,UACC,aAAA,EAAW,IAAA;AAAA,UACX,SAAA,EAAW,EAAA;AAAA,YACT,wGAAA;AAAA,YACA,UAAU,oBAAA,GAAuB;AAAA;AACnC;AAAA;AACF;AAAA,GACF;AAEJ","file":"Switch.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","import { cn } from '../../utils/cn';\n\n/**\n * Switch — a sliding on/off toggle (`role=\"switch\"`), keyboard-operable.\n *\n * The shared primitive the design system names (notifications preferences\n * matrix, automation rule enable, …). Colour flows through `--el-*`; the knob +\n * track are genuinely-circular (`rounded-full`), the shape-rule carve-out for a\n * pill control. Always pass an `aria-label` (or wire an external `<label>` via\n * `id`) so the toggle is screen-reader-legible — a bare switch announces no\n * purpose.\n *\n * @example\n * <Switch checked={on} onCheckedChange={setOn} aria-label=\"Email for Mentioned\" />\n */\nexport interface SwitchProps {\n checked: boolean;\n onCheckedChange: (next: boolean) => void;\n disabled?: boolean;\n /** Accessible name — required unless an external `<label htmlFor=id>` exists. */\n 'aria-label'?: string;\n /**\n * Accessible name by reference — the id of a visible label element. Use this\n * instead of `aria-label` when the label already renders on screen (so the\n * name stays in sync with the visible text). One of `aria-label` /\n * `aria-labelledby` should be set, else the switch announces no purpose.\n */\n 'aria-labelledby'?: string;\n id?: string;\n className?: string;\n}\n\nexport function Switch({\n checked,\n onCheckedChange,\n disabled,\n id,\n className,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n}: SwitchProps) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n id={id}\n aria-checked={checked}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n disabled={disabled}\n onClick={() => onCheckedChange(!checked)}\n className={cn(\n 'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border transition-colors focus-visible:ring-2 focus-visible:ring-(--focus-ring-color) focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',\n checked\n ? 'border-(--el-switch-on) bg-(--el-switch-on)'\n : 'border-(--el-border-strong) bg-(--el-muted)',\n className,\n )}\n >\n <span\n aria-hidden\n className={cn(\n 'inline-block size-3.5 rounded-full bg-(--el-switch-knob) shadow-(--shadow-subtle) transition-transform',\n checked ? 'translate-x-[18px]' : 'translate-x-0.5',\n )}\n />\n </button>\n );\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import * as react from 'react';
2
+ import { TextareaHTMLAttributes } from 'react';
3
+
4
+ /**
5
+ * Textarea — multi-line text field. Mirrors Input's props (label, error,
6
+ * helperText) but no prefix/suffix slots (uncommon shape for textareas).
7
+ * Auto-resize is intentionally NOT supported in v1 — pass `rows` to size.
8
+ *
9
+ * @example
10
+ * <Textarea label="Description" rows={4} helperText="Max 500 chars" />
11
+ */
12
+ interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
13
+ label?: string;
14
+ error?: string;
15
+ helperText?: string;
16
+ }
17
+ declare const Textarea: react.ForwardRefExoticComponent<TextareaProps & react.RefAttributes<HTMLTextAreaElement>>;
18
+
19
+ export { Textarea, type TextareaProps };
@@ -0,0 +1,72 @@
1
+ import { forwardRef, useId } from 'react';
2
+ import { clsx } from 'clsx';
3
+ import { twMerge } from 'tailwind-merge';
4
+ import { jsx, jsxs } from 'react/jsx-runtime';
5
+
6
+ // src/components/ui/Textarea.tsx
7
+ function cn(...inputs) {
8
+ return twMerge(clsx(inputs));
9
+ }
10
+ function FormField({
11
+ label,
12
+ error,
13
+ errorVariant = "text",
14
+ helperText,
15
+ htmlFor,
16
+ className,
17
+ children
18
+ }) {
19
+ const describedById2 = error ? `${htmlFor}-error` : helperText ? `${htmlFor}-helper` : void 0;
20
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-1.5", className), children: [
21
+ label ? /* @__PURE__ */ jsx("label", { htmlFor, className: "font-sans text-sm font-medium text-(--el-text)", children: label }) : null,
22
+ children,
23
+ error ? /* @__PURE__ */ jsx(
24
+ "p",
25
+ {
26
+ id: describedById2,
27
+ role: "alert",
28
+ className: cn(
29
+ "font-sans text-xs",
30
+ errorVariant === "box" && "bg-(--el-danger-surface) text-(--el-danger-surface-text) rounded-(--radius-control) px-(--spacing-tooltip-x) py-(--spacing-tooltip-y)"
31
+ ),
32
+ style: errorVariant === "box" ? void 0 : { color: "var(--el-danger)" },
33
+ children: error
34
+ }
35
+ ) : helperText ? /* @__PURE__ */ jsx("p", { id: describedById2, className: "text-(--el-text-helper) font-sans text-xs", children: helperText }) : null
36
+ ] });
37
+ }
38
+ function describedById(htmlFor, error, helperText) {
39
+ return error ? `${htmlFor}-error` : helperText ? `${htmlFor}-helper` : void 0;
40
+ }
41
+ var Textarea = forwardRef(function Textarea2({ label, error, helperText, id, className, disabled, rows = 3, ...rest }, ref) {
42
+ const autoId = useId();
43
+ const textareaId = id ?? autoId;
44
+ const describedBy = describedById(textareaId, error, helperText);
45
+ const hasError = Boolean(error);
46
+ return /* @__PURE__ */ jsx(FormField, { label, error, helperText, htmlFor: textareaId, children: /* @__PURE__ */ jsx(
47
+ "textarea",
48
+ {
49
+ ref,
50
+ id: textareaId,
51
+ rows,
52
+ "aria-invalid": hasError || void 0,
53
+ "aria-describedby": describedBy,
54
+ disabled,
55
+ className: cn(
56
+ "w-full resize-y rounded-(--radius-input) border bg-(--el-page-bg)",
57
+ "px-(--spacing-input-x) py-(--spacing-input-y)",
58
+ "font-sans text-sm text-(--el-text) placeholder:text-(--el-text-muted)",
59
+ "transition-colors",
60
+ "focus:outline-none focus:ring-2 focus:ring-(--focus-ring-color) focus:ring-offset-2 focus:ring-offset-background",
61
+ hasError ? "border-(--el-danger)" : "border-(--el-border-strong)",
62
+ "disabled:cursor-not-allowed disabled:opacity-50",
63
+ className
64
+ ),
65
+ ...rest
66
+ }
67
+ ) });
68
+ });
69
+
70
+ export { Textarea };
71
+ //# sourceMappingURL=Textarea.js.map
72
+ //# sourceMappingURL=Textarea.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/FormField.tsx","../../../src/components/ui/Textarea.tsx"],"names":["describedById","Textarea","jsx"],"mappings":";;;;;;AAcO,SAAS,MAAM,MAAA,EAA8B;AAClD,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC7B;ACWO,SAAS,SAAA,CAAU;AAAA,EACxB,KAAA;AAAA,EACA,KAAA;AAAA,EACA,YAAA,GAAe,MAAA;AAAA,EACf,UAAA;AAAA,EACA,OAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF,CAAA,EAAmB;AACjB,EAAA,MAAMA,cAAAA,GAAgB,QAAQ,CAAA,EAAG,OAAO,WAAW,UAAA,GAAa,CAAA,EAAG,OAAO,CAAA,OAAA,CAAA,GAAY,MAAA;AACtF,EAAA,4BACG,KAAA,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,uBAAA,EAAyB,SAAS,CAAA,EAClD,QAAA,EAAA;AAAA,IAAA,KAAA,uBACE,OAAA,EAAA,EAAM,OAAA,EAAkB,SAAA,EAAU,gDAAA,EAChC,iBACH,CAAA,GACE,IAAA;AAAA,IACH,QAAA;AAAA,IACA,KAAA,mBACC,GAAA;AAAA,MAAC,GAAA;AAAA,MAAA;AAAA,QACC,EAAA,EAAIA,cAAAA;AAAA,QACJ,IAAA,EAAK,OAAA;AAAA,QACL,SAAA,EAAW,EAAA;AAAA,UACT,mBAAA;AAAA,UACA,iBAAiB,KAAA,IACf;AAAA,SACJ;AAAA,QACA,OAAO,YAAA,KAAiB,KAAA,GAAQ,MAAA,GAAY,EAAE,OAAO,kBAAA,EAAmB;AAAA,QAEvE,QAAA,EAAA;AAAA;AAAA,KACH,GACE,6BACF,GAAA,CAAC,GAAA,EAAA,EAAE,IAAIA,cAAAA,EAAe,SAAA,EAAU,2CAAA,EAC7B,QAAA,EAAA,UAAA,EACH,CAAA,GACE;AAAA,GAAA,EACN,CAAA;AAEJ;AAGO,SAAS,aAAA,CACd,OAAA,EACA,KAAA,EACA,UAAA,EACoB;AACpB,EAAA,OAAO,QAAQ,CAAA,EAAG,OAAO,WAAW,UAAA,GAAa,CAAA,EAAG,OAAO,CAAA,OAAA,CAAA,GAAY,MAAA;AACzE;ACxDO,IAAM,WAAW,UAAA,CAA+C,SAASC,SAAAA,CAC9E,EAAE,OAAO,KAAA,EAAO,UAAA,EAAY,EAAA,EAAI,SAAA,EAAW,UAAU,IAAA,GAAO,CAAA,EAAG,GAAG,IAAA,IAClE,GAAA,EACA;AACA,EAAA,MAAM,SAAS,KAAA,EAAM;AACrB,EAAA,MAAM,aAAa,EAAA,IAAM,MAAA;AACzB,EAAA,MAAM,WAAA,GAAc,aAAA,CAAc,UAAA,EAAY,KAAA,EAAO,UAAU,CAAA;AAC/D,EAAA,MAAM,QAAA,GAAW,QAAQ,KAAK,CAAA;AAC9B,EAAA,uBACEC,IAAC,SAAA,EAAA,EAAU,KAAA,EAAc,OAAc,UAAA,EAAwB,OAAA,EAAS,YACtE,QAAA,kBAAAA,GAAAA;AAAA,IAAC,UAAA;AAAA,IAAA;AAAA,MACC,GAAA;AAAA,MACA,EAAA,EAAI,UAAA;AAAA,MACJ,IAAA;AAAA,MACA,gBAAc,QAAA,IAAY,MAAA;AAAA,MAC1B,kBAAA,EAAkB,WAAA;AAAA,MAClB,QAAA;AAAA,MACA,SAAA,EAAW,EAAA;AAAA,QACT,mEAAA;AAAA,QACA,+CAAA;AAAA,QACA,uEAAA;AAAA,QACA,mBAAA;AAAA,QACA,kHAAA;AAAA,QACA,WAAW,sBAAA,GAAyB,6BAAA;AAAA,QACpC,iDAAA;AAAA,QACA;AAAA,OACF;AAAA,MACC,GAAG;AAAA;AAAA,GACN,EACF,CAAA;AAEJ,CAAC","file":"Textarea.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","import { type ReactNode } from 'react';\nimport { cn } from '../../utils/cn';\n\n/**\n * Internal layout wrapper shared by Input and Textarea — handles the\n * label / control / helper-or-error stack pattern. Not exported from\n * the design system; consumers compose <Input> and <Textarea> directly.\n */\nexport interface FormFieldProps {\n /** Visible label rendered above the control. */\n label?: string;\n /** Error message — overrides helperText when present. Sets aria-invalid. */\n error?: string;\n /**\n * How the error message renders: the default `text` is the quiet danger\n * line; `box` is the rose-tint inline-error box (hue in the background,\n * strong text — finding #35), the 2.4.9 grammar inline editors use.\n */\n errorVariant?: 'text' | 'box';\n /** Helper text rendered below the control. */\n helperText?: string;\n /** id of the control element — used to associate label + describedby. */\n htmlFor: string;\n className?: string;\n children: ReactNode;\n}\n\nexport function FormField({\n label,\n error,\n errorVariant = 'text',\n helperText,\n htmlFor,\n className,\n children,\n}: FormFieldProps) {\n const describedById = error ? `${htmlFor}-error` : helperText ? `${htmlFor}-helper` : undefined;\n return (\n <div className={cn('flex flex-col gap-1.5', className)}>\n {label ? (\n <label htmlFor={htmlFor} className=\"font-sans text-sm font-medium text-(--el-text)\">\n {label}\n </label>\n ) : null}\n {children}\n {error ? (\n <p\n id={describedById}\n role=\"alert\"\n className={cn(\n 'font-sans text-xs',\n errorVariant === 'box' &&\n 'bg-(--el-danger-surface) text-(--el-danger-surface-text) rounded-(--radius-control) px-(--spacing-tooltip-x) py-(--spacing-tooltip-y)',\n )}\n style={errorVariant === 'box' ? undefined : { color: 'var(--el-danger)' }}\n >\n {error}\n </p>\n ) : helperText ? (\n <p id={describedById} className=\"text-(--el-text-helper) font-sans text-xs\">\n {helperText}\n </p>\n ) : null}\n </div>\n );\n}\n\n/** Returns the aria-describedby id that consumers should pass to the control. */\nexport function describedById(\n htmlFor: string,\n error: string | undefined,\n helperText: string | undefined,\n): string | undefined {\n return error ? `${htmlFor}-error` : helperText ? `${htmlFor}-helper` : undefined;\n}\n","import { forwardRef, useId, type TextareaHTMLAttributes } from 'react';\nimport { FormField, describedById } from './FormField';\nimport { cn } from '../../utils/cn';\n\n/**\n * Textarea — multi-line text field. Mirrors Input's props (label, error,\n * helperText) but no prefix/suffix slots (uncommon shape for textareas).\n * Auto-resize is intentionally NOT supported in v1 — pass `rows` to size.\n *\n * @example\n * <Textarea label=\"Description\" rows={4} helperText=\"Max 500 chars\" />\n */\nexport interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {\n label?: string;\n error?: string;\n helperText?: string;\n}\n\nexport const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(\n { label, error, helperText, id, className, disabled, rows = 3, ...rest },\n ref,\n) {\n const autoId = useId();\n const textareaId = id ?? autoId;\n const describedBy = describedById(textareaId, error, helperText);\n const hasError = Boolean(error);\n return (\n <FormField label={label} error={error} helperText={helperText} htmlFor={textareaId}>\n <textarea\n ref={ref}\n id={textareaId}\n rows={rows}\n aria-invalid={hasError || undefined}\n aria-describedby={describedBy}\n disabled={disabled}\n className={cn(\n 'w-full resize-y rounded-(--radius-input) border bg-(--el-page-bg)',\n 'px-(--spacing-input-x) py-(--spacing-input-y)',\n 'font-sans text-sm text-(--el-text) placeholder:text-(--el-text-muted)',\n 'transition-colors',\n 'focus:outline-none focus:ring-2 focus:ring-(--focus-ring-color) focus:ring-offset-2 focus:ring-offset-background',\n hasError ? 'border-(--el-danger)' : 'border-(--el-border-strong)',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n {...rest}\n />\n </FormField>\n );\n});\n"]}
@@ -0,0 +1,45 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as class_variance_authority_types from 'class-variance-authority/types';
3
+ import { ReactNode } from 'react';
4
+ import { VariantProps } from 'class-variance-authority';
5
+
6
+ /**
7
+ * Toast — transient notification with stacking, auto-dismiss, hover-to-pause.
8
+ *
9
+ * Wrap the app in `<ToastProvider>` once. Use `useToast()` from any client
10
+ * component to dispatch:
11
+ *
12
+ * @example
13
+ * const { toast } = useToast();
14
+ * toast({ variant: 'success', title: 'Saved', description: 'Changes synced.' });
15
+ */
16
+ declare const toastVariants: (props?: ({
17
+ variant?: "error" | "info" | "success" | "warning" | null | undefined;
18
+ } & class_variance_authority_types.ClassProp) | undefined) => string;
19
+ type ToastVariant = NonNullable<VariantProps<typeof toastVariants>['variant']>;
20
+ /** An optional inline action button (e.g. "Undo" after an archive). */
21
+ interface ToastAction {
22
+ label: string;
23
+ onClick: () => void;
24
+ }
25
+ interface ToastContextValue {
26
+ toast: (opts: {
27
+ variant?: ToastVariant;
28
+ title: string;
29
+ description?: string;
30
+ action?: ToastAction;
31
+ }) => void;
32
+ }
33
+ declare function ToastProvider({ children, closeLabel, }: {
34
+ children: ReactNode;
35
+ /**
36
+ * Accessible label for each toast's close (×) button. Defaults to `'Close'`.
37
+ * i18n is the consuming app's concern (the package carries no `next-intl`
38
+ * dependency), so a localized app passes the translated string — motir-core
39
+ * wires it from its `common.close`.
40
+ */
41
+ closeLabel?: string;
42
+ }): react_jsx_runtime.JSX.Element;
43
+ declare function useToast(): ToastContextValue;
44
+
45
+ export { ToastProvider, useToast };
@@ -0,0 +1,116 @@
1
+ 'use client';
2
+ import { createContext, useState, useCallback, useMemo, useContext } from 'react';
3
+ import * as RadixToast from '@radix-ui/react-toast';
4
+ import { XCircle, AlertCircle, CheckCircle2, Info, 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 toastVariants = cva(
14
+ cn(
15
+ "pointer-events-auto flex items-start gap-3",
16
+ "rounded-(--radius-card) border bg-(--el-page-bg)",
17
+ "px-(--spacing-md) py-(--spacing-sm)",
18
+ "shadow-(--shadow-elevated)",
19
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
20
+ "data-[state=open]:fade-in-0 data-[state=closed]:fade-out-80",
21
+ "data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x)",
22
+ "data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x)",
23
+ "data-[swipe=cancel]:transition-transform data-[swipe=end]:transition-transform"
24
+ ),
25
+ {
26
+ variants: {
27
+ variant: {
28
+ info: "border-(--el-info)",
29
+ success: "border-(--el-success)",
30
+ warning: "border-(--el-warning)",
31
+ error: "border-(--el-danger)"
32
+ }
33
+ },
34
+ defaultVariants: { variant: "info" }
35
+ }
36
+ );
37
+ var ToastContext = createContext(null);
38
+ var ICONS = {
39
+ info: Info,
40
+ success: CheckCircle2,
41
+ warning: AlertCircle,
42
+ error: XCircle
43
+ };
44
+ var ICON_COLOR_VAR = {
45
+ info: "var(--el-info)",
46
+ success: "var(--el-success)",
47
+ warning: "var(--el-warning)",
48
+ error: "var(--el-danger)"
49
+ };
50
+ function ToastProvider({
51
+ children,
52
+ closeLabel = "Close"
53
+ }) {
54
+ const [toasts, setToasts] = useState([]);
55
+ const toast = useCallback(
56
+ ({ variant = "info", title, description, action }) => {
57
+ const id = crypto.randomUUID();
58
+ setToasts((prev) => [...prev, { id, variant, title, description, action }]);
59
+ },
60
+ []
61
+ );
62
+ const remove = useCallback((id) => {
63
+ setToasts((prev) => prev.filter((t) => t.id !== id));
64
+ }, []);
65
+ const value = useMemo(() => ({ toast }), [toast]);
66
+ return /* @__PURE__ */ jsx(ToastContext.Provider, { value, children: /* @__PURE__ */ jsxs(RadixToast.Provider, { swipeDirection: "right", duration: 5e3, children: [
67
+ children,
68
+ toasts.map((t) => {
69
+ const Icon = ICONS[t.variant];
70
+ return /* @__PURE__ */ jsxs(
71
+ RadixToast.Root,
72
+ {
73
+ className: toastVariants({ variant: t.variant }),
74
+ onOpenChange: (open) => {
75
+ if (!open) remove(t.id);
76
+ },
77
+ children: [
78
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "mt-0.5 inline-flex", children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4", style: { color: ICON_COLOR_VAR[t.variant] } }) }),
79
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
80
+ /* @__PURE__ */ jsx(RadixToast.Title, { className: "font-sans text-sm font-medium text-(--el-text)", children: t.title }),
81
+ t.description ? /* @__PURE__ */ jsx(RadixToast.Description, { className: "text-(--el-text-muted) mt-0.5 font-sans text-xs", children: t.description }) : null
82
+ ] }),
83
+ t.action ? /* @__PURE__ */ jsx(
84
+ RadixToast.Action,
85
+ {
86
+ altText: t.action.label,
87
+ onClick: t.action.onClick,
88
+ className: "shrink-0 self-center rounded-(--radius-control) px-(--spacing-control-x) py-(--spacing-control-y) font-sans text-xs font-semibold text-(--el-link) hover:bg-(--el-surface) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring-color)",
89
+ children: t.action.label
90
+ }
91
+ ) : null,
92
+ /* @__PURE__ */ jsx(
93
+ RadixToast.Close,
94
+ {
95
+ "aria-label": closeLabel,
96
+ className: "text-(--el-text-muted) hover:text-(--el-text) rounded-(--radius-control) p-(--spacing-icon-btn) transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring-color)",
97
+ children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
98
+ }
99
+ )
100
+ ]
101
+ },
102
+ t.id
103
+ );
104
+ }),
105
+ /* @__PURE__ */ jsx(RadixToast.Viewport, { className: "fixed bottom-0 right-0 z-50 m-4 flex w-96 max-w-[100vw] flex-col gap-2 outline-none" })
106
+ ] }) });
107
+ }
108
+ var NO_OP_TOAST = { toast: () => {
109
+ } };
110
+ function useToast() {
111
+ return useContext(ToastContext) ?? NO_OP_TOAST;
112
+ }
113
+
114
+ export { ToastProvider, useToast };
115
+ //# sourceMappingURL=Toast.js.map
116
+ //# sourceMappingURL=Toast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/Toast.tsx"],"names":[],"mappings":"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","file":"Toast.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 { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react';\nimport * as RadixToast from '@radix-ui/react-toast';\nimport { AlertCircle, CheckCircle2, Info, X, XCircle } from 'lucide-react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\n\n/**\n * Toast — transient notification with stacking, auto-dismiss, hover-to-pause.\n *\n * Wrap the app in `<ToastProvider>` once. Use `useToast()` from any client\n * component to dispatch:\n *\n * @example\n * const { toast } = useToast();\n * toast({ variant: 'success', title: 'Saved', description: 'Changes synced.' });\n */\nconst toastVariants = cva(\n cn(\n 'pointer-events-auto flex items-start gap-3',\n 'rounded-(--radius-card) border bg-(--el-page-bg)',\n 'px-(--spacing-md) py-(--spacing-sm)',\n 'shadow-(--shadow-elevated)',\n 'data-[state=open]:animate-in data-[state=closed]:animate-out',\n 'data-[state=open]:fade-in-0 data-[state=closed]:fade-out-80',\n 'data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x)',\n 'data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x)',\n 'data-[swipe=cancel]:transition-transform data-[swipe=end]:transition-transform',\n ),\n {\n variants: {\n variant: {\n info: 'border-(--el-info)',\n success: 'border-(--el-success)',\n warning: 'border-(--el-warning)',\n error: 'border-(--el-danger)',\n },\n },\n defaultVariants: { variant: 'info' },\n },\n);\n\ntype ToastVariant = NonNullable<VariantProps<typeof toastVariants>['variant']>;\n\n/** An optional inline action button (e.g. \"Undo\" after an archive). */\ninterface ToastAction {\n label: string;\n onClick: () => void;\n}\n\ninterface ToastItem {\n id: string;\n variant: ToastVariant;\n title: string;\n description?: string;\n action?: ToastAction;\n}\n\ninterface ToastContextValue {\n toast: (opts: {\n variant?: ToastVariant;\n title: string;\n description?: string;\n action?: ToastAction;\n }) => void;\n}\n\nconst ToastContext = createContext<ToastContextValue | null>(null);\n\nconst ICONS: Record<ToastVariant, typeof Info> = {\n info: Info,\n success: CheckCircle2,\n warning: AlertCircle,\n error: XCircle,\n};\n\nconst ICON_COLOR_VAR: Record<ToastVariant, string> = {\n info: 'var(--el-info)',\n success: 'var(--el-success)',\n warning: 'var(--el-warning)',\n error: 'var(--el-danger)',\n};\n\nexport function ToastProvider({\n children,\n closeLabel = 'Close',\n}: {\n children: ReactNode;\n /**\n * Accessible label for each toast's close (×) button. Defaults to `'Close'`.\n * i18n is the consuming app's concern (the package carries no `next-intl`\n * dependency), so a localized app passes the translated string — motir-core\n * wires it from its `common.close`.\n */\n closeLabel?: string;\n}) {\n const [toasts, setToasts] = useState<ToastItem[]>([]);\n\n const toast = useCallback<ToastContextValue['toast']>(\n ({ variant = 'info', title, description, action }) => {\n const id = crypto.randomUUID();\n setToasts((prev) => [...prev, { id, variant, title, description, action }]);\n },\n [],\n );\n\n const remove = useCallback((id: string) => {\n setToasts((prev) => prev.filter((t) => t.id !== id));\n }, []);\n\n const value = useMemo(() => ({ toast }), [toast]);\n\n return (\n <ToastContext.Provider value={value}>\n <RadixToast.Provider swipeDirection=\"right\" duration={5000}>\n {children}\n {toasts.map((t) => {\n const Icon = ICONS[t.variant];\n return (\n <RadixToast.Root\n key={t.id}\n className={toastVariants({ variant: t.variant })}\n onOpenChange={(open) => {\n if (!open) remove(t.id);\n }}\n >\n <span aria-hidden className=\"mt-0.5 inline-flex\">\n <Icon className=\"h-4 w-4\" style={{ color: ICON_COLOR_VAR[t.variant] }} />\n </span>\n <div className=\"flex-1\">\n <RadixToast.Title className=\"font-sans text-sm font-medium text-(--el-text)\">\n {t.title}\n </RadixToast.Title>\n {t.description ? (\n <RadixToast.Description className=\"text-(--el-text-muted) mt-0.5 font-sans text-xs\">\n {t.description}\n </RadixToast.Description>\n ) : null}\n </div>\n {t.action ? (\n <RadixToast.Action\n altText={t.action.label}\n onClick={t.action.onClick}\n className=\"shrink-0 self-center rounded-(--radius-control) px-(--spacing-control-x) py-(--spacing-control-y) font-sans text-xs font-semibold text-(--el-link) hover:bg-(--el-surface) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring-color)\"\n >\n {t.action.label}\n </RadixToast.Action>\n ) : null}\n <RadixToast.Close\n aria-label={closeLabel}\n className=\"text-(--el-text-muted) hover:text-(--el-text) 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-3.5 w-3.5\" />\n </RadixToast.Close>\n </RadixToast.Root>\n );\n })}\n <RadixToast.Viewport className=\"fixed bottom-0 right-0 z-50 m-4 flex w-96 max-w-[100vw] flex-col gap-2 outline-none\" />\n </RadixToast.Provider>\n </ToastContext.Provider>\n );\n}\n\n// A no-op fallback for components rendered outside a ToastProvider — a unit test\n// or a surface mounted without the shell. The production root layout always\n// mounts the provider, so this only ever spares a deeply-embedded control (the\n// 2.8.4 ⋯ menu inside a board card / list row) from a hard provider dependency;\n// a stray toast outside the shell silently no-ops rather than crashing render.\nconst NO_OP_TOAST: ToastContextValue = { toast: () => {} };\n\nexport function useToast(): ToastContextValue {\n return useContext(ToastContext) ?? NO_OP_TOAST;\n}\n"]}
@@ -0,0 +1,25 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ /**
5
+ * Tooltip — Radix-wrapped popover that appears on hover + focus.
6
+ *
7
+ * Requires a single TooltipProvider near the app root for delay coordination.
8
+ * For simple use, the default delay (700ms) is fine.
9
+ *
10
+ * @example
11
+ * <Tooltip content="Send message">
12
+ * <Button variant="ghost"><Send /></Button>
13
+ * </Tooltip>
14
+ */
15
+ interface TooltipProps {
16
+ content: ReactNode;
17
+ children: ReactNode;
18
+ side?: 'top' | 'right' | 'bottom' | 'left';
19
+ /** Delay before opening, in ms. Default 700 (Radix default). */
20
+ delayMs?: number;
21
+ className?: string;
22
+ }
23
+ declare function Tooltip({ content, children, side, delayMs, className, }: TooltipProps): react_jsx_runtime.JSX.Element;
24
+
25
+ export { Tooltip, type TooltipProps };
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+ import * as RadixTooltip from '@radix-ui/react-tooltip';
3
+ import { clsx } from 'clsx';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+
7
+ function cn(...inputs) {
8
+ return twMerge(clsx(inputs));
9
+ }
10
+ function Tooltip({
11
+ content,
12
+ children,
13
+ side = "top",
14
+ delayMs = 700,
15
+ className
16
+ }) {
17
+ return /* @__PURE__ */ jsx(RadixTooltip.Provider, { delayDuration: delayMs, children: /* @__PURE__ */ jsxs(RadixTooltip.Root, { children: [
18
+ /* @__PURE__ */ jsx(RadixTooltip.Trigger, { asChild: true, children }),
19
+ /* @__PURE__ */ jsx(RadixTooltip.Portal, { children: /* @__PURE__ */ jsxs(
20
+ RadixTooltip.Content,
21
+ {
22
+ side,
23
+ sideOffset: 6,
24
+ className: cn(
25
+ "z-50 select-none rounded-(--radius-control)",
26
+ "bg-(--el-tooltip-bg) text-(--el-tooltip-text)",
27
+ "px-(--spacing-tooltip-x) py-(--spacing-tooltip-y) font-sans text-xs",
28
+ "shadow-(--shadow-elevated)",
29
+ "data-[state=delayed-open]:animate-in data-[state=closed]:animate-out fade-in-0 fade-out-0",
30
+ className
31
+ ),
32
+ children: [
33
+ content,
34
+ /* @__PURE__ */ jsx(RadixTooltip.Arrow, { className: "fill-(--el-tooltip-bg)" })
35
+ ]
36
+ }
37
+ ) })
38
+ ] }) });
39
+ }
40
+
41
+ export { Tooltip };
42
+ //# sourceMappingURL=Tooltip.js.map
43
+ //# sourceMappingURL=Tooltip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/Tooltip.tsx"],"names":[],"mappings":";;;;;AAcO,SAAS,MAAM,MAAA,EAA8B;AAClD,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC7B;ACUO,SAAS,OAAA,CAAQ;AAAA,EACtB,OAAA;AAAA,EACA,QAAA;AAAA,EACA,IAAA,GAAO,KAAA;AAAA,EACP,OAAA,GAAU,GAAA;AAAA,EACV;AACF,CAAA,EAAiB;AACf,EAAA,2BACgB,YAAA,CAAA,QAAA,EAAb,EAAsB,eAAe,OAAA,EACpC,QAAA,kBAAA,IAAA,CAAc,mBAAb,EACC,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAc,YAAA,CAAA,OAAA,EAAb,EAAqB,OAAA,EAAO,IAAA,EAAE,QAAA,EAAS,CAAA;AAAA,oBACxC,GAAA,CAAc,qBAAb,EACC,QAAA,kBAAA,IAAA;AAAA,MAAc,YAAA,CAAA,OAAA;AAAA,MAAb;AAAA,QACC,IAAA;AAAA,QACA,UAAA,EAAY,CAAA;AAAA,QACZ,SAAA,EAAW,EAAA;AAAA,UACT,6CAAA;AAAA,UACA,+CAAA;AAAA,UACA,qEAAA;AAAA,UACA,4BAAA;AAAA,UACA,2FAAA;AAAA,UACA;AAAA,SACF;AAAA,QAEC,QAAA,EAAA;AAAA,UAAA,OAAA;AAAA,0BACD,GAAA,CAAc,YAAA,CAAA,KAAA,EAAb,EAAmB,SAAA,EAAU,wBAAA,EAAyB;AAAA;AAAA;AAAA,KACzD,EACF;AAAA,GAAA,EACF,CAAA,EACF,CAAA;AAEJ","file":"Tooltip.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 { type ReactNode } from 'react';\nimport * as RadixTooltip from '@radix-ui/react-tooltip';\nimport { cn } from '../../utils/cn';\n\n/**\n * Tooltip — Radix-wrapped popover that appears on hover + focus.\n *\n * Requires a single TooltipProvider near the app root for delay coordination.\n * For simple use, the default delay (700ms) is fine.\n *\n * @example\n * <Tooltip content=\"Send message\">\n * <Button variant=\"ghost\"><Send /></Button>\n * </Tooltip>\n */\nexport interface TooltipProps {\n content: ReactNode;\n children: ReactNode;\n side?: 'top' | 'right' | 'bottom' | 'left';\n /** Delay before opening, in ms. Default 700 (Radix default). */\n delayMs?: number;\n className?: string;\n}\n\nexport function Tooltip({\n content,\n children,\n side = 'top',\n delayMs = 700,\n className,\n}: TooltipProps) {\n return (\n <RadixTooltip.Provider delayDuration={delayMs}>\n <RadixTooltip.Root>\n <RadixTooltip.Trigger asChild>{children}</RadixTooltip.Trigger>\n <RadixTooltip.Portal>\n <RadixTooltip.Content\n side={side}\n sideOffset={6}\n className={cn(\n 'z-50 select-none rounded-(--radius-control)',\n 'bg-(--el-tooltip-bg) text-(--el-tooltip-text)',\n 'px-(--spacing-tooltip-x) py-(--spacing-tooltip-y) font-sans text-xs',\n 'shadow-(--shadow-elevated)',\n 'data-[state=delayed-open]:animate-in data-[state=closed]:animate-out fade-in-0 fade-out-0',\n className,\n )}\n >\n {content}\n <RadixTooltip.Arrow className=\"fill-(--el-tooltip-bg)\" />\n </RadixTooltip.Content>\n </RadixTooltip.Portal>\n </RadixTooltip.Root>\n </RadixTooltip.Provider>\n );\n}\n"]}
@@ -0,0 +1,95 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { ThemePattern, ResolvedThemePattern } from '../theme/types.js';
4
+ import { StyleId } from '../theme/styles.js';
5
+ import { PaletteId } from '../theme/palettes.js';
6
+ import { TypeId } from '../theme/typography.js';
7
+ import { AppliedAppearanceDto } from '../appearance.js';
8
+
9
+ /**
10
+ * Theme context for Motir's three-axis design system (style · palette · type),
11
+ * plus the light/dark pattern.
12
+ *
13
+ * `pattern` is what the user picked (system | light | dark).
14
+ * `resolvedPattern` is what's currently applied (light | dark) — these
15
+ * differ when pattern='system' and the OS preference resolves to one or
16
+ * the other.
17
+ *
18
+ * State is persisted to localStorage and re-applied to <html>'s
19
+ * data-attributes on every change. The init script in app/layout.tsx
20
+ * does the same work BEFORE React hydrates to avoid FOUC; this provider
21
+ * keeps the attributes in sync after hydration.
22
+ *
23
+ * Implementation note: this uses lazy `useState` initializers and
24
+ * `useSyncExternalStore` rather than `useEffect`+`setState`, per the
25
+ * React 19 / react-hooks/set-state-in-effect rule. Setting state inside
26
+ * effects causes cascading renders; subscribing to external systems
27
+ * (localStorage, matchMedia) via the right primitives avoids that.
28
+ */
29
+ interface ThemeContextValue {
30
+ pattern: ThemePattern;
31
+ resolvedPattern: ResolvedThemePattern;
32
+ /** The active named style (`data-style`) — Axis 2, independent of palette. */
33
+ styleId: StyleId;
34
+ /** The active named palette (`data-palette`) — Axis 1 (colour), independent of style. */
35
+ palette: PaletteId;
36
+ /**
37
+ * The EFFECTIVE type pairing (`data-type`) — Axis 3. Either the user's pinned
38
+ * choice, or (when unpinned) the active style's `defaultTypeId`. Independent
39
+ * of palette + style.
40
+ */
41
+ type: TypeId;
42
+ setPattern: (pattern: ThemePattern) => void;
43
+ setStyleId: (styleId: StyleId) => void;
44
+ setPalette: (palette: PaletteId) => void;
45
+ /** Pin an explicit type pairing (overrides the style's default until cleared). */
46
+ setType: (type: TypeId) => void;
47
+ /**
48
+ * Cross-device sync status (Subtask 7.3.62). `'idle'` while nothing has
49
+ * failed; `'error'` when the LAST attempt to persist a change to the account
50
+ * failed (a 401 / write failure) — the local switch still applied (it's in
51
+ * localStorage), so this only drives a quiet "couldn't sync" affordance. Always
52
+ * `'idle'` for an anonymous visitor, who never syncs. Clears back to `'idle'`
53
+ * on the next successful sync.
54
+ */
55
+ syncState: AppearanceSyncState;
56
+ }
57
+ /** The cross-device sync status surfaced to the Appearance pane (7.3.62). */
58
+ type AppearanceSyncState = 'idle' | 'error';
59
+ declare function ThemeProvider({ children, initialPreference, signedIn, }: {
60
+ children: ReactNode;
61
+ /**
62
+ * The signed-in user's APPLIED appearance, resolved server-side (Subtask
63
+ * 7.3.61). When present it SEEDS this island's state — the page-state
64
+ * contract: a client island must be GIVEN the server value, not left to
65
+ * re-read localStorage (which `useState` would do only at mount, ignoring the
66
+ * authoritative server pref and risking a flash from a stale value synced on
67
+ * another device). `null` (anonymous) keeps the original localStorage path.
68
+ */
69
+ initialPreference?: AppliedAppearanceDto | null;
70
+ /**
71
+ * Whether a user is signed in (Subtask 7.3.62). Only then does an axis change
72
+ * also PERSIST to the account via `/api/appearance-preference` (cross-device
73
+ * sync). Anonymous visitors keep the localStorage-only path and never fire a
74
+ * write — so they never hit a 401 or see a "couldn't sync" affordance. This is
75
+ * a SEPARATE signal from `initialPreference`: a signed-in user who has pinned
76
+ * nothing yet has `initialPreference === null` but must still sync their first
77
+ * pick (a row is created on the first PATCH).
78
+ */
79
+ signedIn?: boolean;
80
+ }): react_jsx_runtime.JSX.Element;
81
+ declare function useTheme(): ThemeContextValue;
82
+ /**
83
+ * Non-throwing variant of {@link useTheme}. Returns `null` when no
84
+ * `<ThemeProvider>` is mounted instead of throwing.
85
+ *
86
+ * Use this in low-level primitives that must also render outside the authed
87
+ * shell — e.g. the `MarkdownEditor` (Subtask 2.3.5) reads `resolvedPattern`
88
+ * to drive the underlying editor's `data-color-mode`, but it's also exercised
89
+ * on the public `/tokens` specimen and in component tests where wrapping every
90
+ * render in a provider would be noise. Callers fall back to a sensible default
91
+ * (`'light'`).
92
+ */
93
+ declare function useOptionalTheme(): ThemeContextValue | null;
94
+
95
+ export { type AppearanceSyncState, ThemeProvider, useOptionalTheme, useTheme };