@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.
- package/README.md +126 -0
- package/dist/appearance.d.ts +47 -0
- package/dist/appearance.js +3 -0
- package/dist/appearance.js.map +1 -0
- package/dist/components/theme/AppearancePickers.d.ts +97 -0
- package/dist/components/theme/AppearancePickers.js +542 -0
- package/dist/components/theme/AppearancePickers.js.map +1 -0
- package/dist/components/theme/HandDrawnFilter.d.ts +23 -0
- package/dist/components/theme/HandDrawnFilter.js +41 -0
- package/dist/components/theme/HandDrawnFilter.js.map +1 -0
- package/dist/components/theme/ImmersiveTilt.d.ts +3 -0
- package/dist/components/theme/ImmersiveTilt.js +113 -0
- package/dist/components/theme/ImmersiveTilt.js.map +1 -0
- package/dist/components/theme/StyleVignette.d.ts +60 -0
- package/dist/components/theme/StyleVignette.js +382 -0
- package/dist/components/theme/StyleVignette.js.map +1 -0
- package/dist/components/ui/Button.d.ts +32 -0
- package/dist/components/ui/Button.js +92 -0
- package/dist/components/ui/Button.js.map +1 -0
- package/dist/components/ui/Card.d.ts +33 -0
- package/dist/components/ui/Card.js +64 -0
- package/dist/components/ui/Card.js.map +1 -0
- package/dist/components/ui/ColorSwatchPicker.d.ts +19 -0
- package/dist/components/ui/ColorSwatchPicker.js +71 -0
- package/dist/components/ui/ColorSwatchPicker.js.map +1 -0
- package/dist/components/ui/Combobox.d.ts +84 -0
- package/dist/components/ui/Combobox.js +374 -0
- package/dist/components/ui/Combobox.js.map +1 -0
- package/dist/components/ui/EmptyState.d.ts +38 -0
- package/dist/components/ui/EmptyState.js +73 -0
- package/dist/components/ui/EmptyState.js.map +1 -0
- package/dist/components/ui/ErrorState.d.ts +43 -0
- package/dist/components/ui/ErrorState.js +169 -0
- package/dist/components/ui/ErrorState.js.map +1 -0
- package/dist/components/ui/FormField.d.ts +31 -0
- package/dist/components/ui/FormField.js +43 -0
- package/dist/components/ui/FormField.js.map +1 -0
- package/dist/components/ui/Input.d.ts +29 -0
- package/dist/components/ui/Input.js +105 -0
- package/dist/components/ui/Input.js.map +1 -0
- package/dist/components/ui/Modal.d.ts +84 -0
- package/dist/components/ui/Modal.js +153 -0
- package/dist/components/ui/Modal.js.map +1 -0
- package/dist/components/ui/MultiSelectPicker.d.ts +73 -0
- package/dist/components/ui/MultiSelectPicker.js +358 -0
- package/dist/components/ui/MultiSelectPicker.js.map +1 -0
- package/dist/components/ui/Pill.d.ts +49 -0
- package/dist/components/ui/Pill.js +107 -0
- package/dist/components/ui/Pill.js.map +1 -0
- package/dist/components/ui/Popover.d.ts +65 -0
- package/dist/components/ui/Popover.js +57 -0
- package/dist/components/ui/Popover.js.map +1 -0
- package/dist/components/ui/SectionLabel.d.ts +28 -0
- package/dist/components/ui/SectionLabel.js +27 -0
- package/dist/components/ui/SectionLabel.js.map +1 -0
- package/dist/components/ui/Segmented.d.ts +30 -0
- package/dist/components/ui/Segmented.js +65 -0
- package/dist/components/ui/Segmented.js.map +1 -0
- package/dist/components/ui/Spinner.d.ts +22 -0
- package/dist/components/ui/Spinner.js +39 -0
- package/dist/components/ui/Spinner.js.map +1 -0
- package/dist/components/ui/Switch.d.ts +34 -0
- package/dist/components/ui/Switch.js +50 -0
- package/dist/components/ui/Switch.js.map +1 -0
- package/dist/components/ui/Textarea.d.ts +19 -0
- package/dist/components/ui/Textarea.js +72 -0
- package/dist/components/ui/Textarea.js.map +1 -0
- package/dist/components/ui/Toast.d.ts +45 -0
- package/dist/components/ui/Toast.js +116 -0
- package/dist/components/ui/Toast.js.map +1 -0
- package/dist/components/ui/Tooltip.d.ts +25 -0
- package/dist/components/ui/Tooltip.js +43 -0
- package/dist/components/ui/Tooltip.js.map +1 -0
- package/dist/contexts/theme-context.d.ts +95 -0
- package/dist/contexts/theme-context.js +561 -0
- package/dist/contexts/theme-context.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +67 -0
- package/dist/specimen/TokensSpecimen.d.ts +13 -0
- package/dist/specimen/TokensSpecimen.js +1090 -0
- package/dist/specimen/TokensSpecimen.js.map +1 -0
- package/dist/theme/appearance-resolution.d.ts +59 -0
- package/dist/theme/appearance-resolution.js +379 -0
- package/dist/theme/appearance-resolution.js.map +1 -0
- package/dist/theme/init-script.d.ts +48 -0
- package/dist/theme/init-script.js +391 -0
- package/dist/theme/init-script.js.map +1 -0
- package/dist/theme/palettes.d.ts +146 -0
- package/dist/theme/palettes.js +85 -0
- package/dist/theme/palettes.js.map +1 -0
- package/dist/theme/styles.d.ts +351 -0
- package/dist/theme/styles.js +264 -0
- package/dist/theme/styles.js.map +1 -0
- package/dist/theme/tilt.d.ts +41 -0
- package/dist/theme/tilt.js +18 -0
- package/dist/theme/tilt.js.map +1 -0
- package/dist/theme/types.d.ts +61 -0
- package/dist/theme/types.js +234 -0
- package/dist/theme/types.js.map +1 -0
- package/dist/theme/typography.d.ts +144 -0
- package/dist/theme/typography.js +57 -0
- package/dist/theme/typography.js.map +1 -0
- package/dist/utils/cn.d.ts +16 -0
- package/dist/utils/cn.js +11 -0
- package/dist/utils/cn.js.map +1 -0
- package/package.json +60 -0
- 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 };
|