@motir/design-system 0.4.3 → 0.5.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/dist/components/ui/Segmented.d.ts +16 -1
- package/dist/components/ui/Segmented.js +10 -4
- package/dist/components/ui/Segmented.js.map +1 -1
- package/dist/components/ui/Textarea.d.ts +20 -1
- package/dist/components/ui/Textarea.js +43 -6
- package/dist/components/ui/Textarea.js.map +1 -1
- package/package.json +1 -1
- package/theme.css +5 -0
|
@@ -17,7 +17,7 @@ interface SegmentedOption<T extends string> {
|
|
|
17
17
|
/** Tooltip for the option (e.g. why a seam option is disabled). */
|
|
18
18
|
title?: string;
|
|
19
19
|
}
|
|
20
|
-
declare function Segmented<T extends string>({ options, value, onChange, label, className, disabled, }: {
|
|
20
|
+
declare function Segmented<T extends string>({ options, value, onChange, label, className, disabled, fill, }: {
|
|
21
21
|
options: SegmentedOption<T>[];
|
|
22
22
|
value: T;
|
|
23
23
|
onChange: (value: T) => void;
|
|
@@ -25,6 +25,21 @@ declare function Segmented<T extends string>({ options, value, onChange, label,
|
|
|
25
25
|
label: string;
|
|
26
26
|
className?: string;
|
|
27
27
|
disabled?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* FILL the container instead of sizing to the options (MOTIR-6200, for
|
|
30
|
+
* MOTIR-6199): the track becomes `w-full` and the segments divide it evenly.
|
|
31
|
+
*
|
|
32
|
+
* WHY IT IS A VARIANT AND NOT THE DEFAULT. The track is `inline-flex` with no
|
|
33
|
+
* wrap and no shrink, so its width is `max-content` — fine in a toolbar that
|
|
34
|
+
* can grow, and unsurvivable in a FIXED rail. The Difficulty editor sits in an
|
|
35
|
+
* 18rem item-page rail and a 300px quick-view rail, and when its scale gained a
|
|
36
|
+
* fourth member the track overran both. Filling makes the width the
|
|
37
|
+
* CONTAINER's, so it holds at any label set, at any `--spacing-control-x`
|
|
38
|
+
* (10 / 12 / 14px across the shipped styles) and at a fifth member.
|
|
39
|
+
*
|
|
40
|
+
* Off by default: every other call site keeps byte-identical markup.
|
|
41
|
+
*/
|
|
42
|
+
fill?: boolean;
|
|
28
43
|
}): react_jsx_runtime.JSX.Element;
|
|
29
44
|
|
|
30
45
|
export { Segmented, type SegmentedOption };
|
|
@@ -12,7 +12,8 @@ function Segmented({
|
|
|
12
12
|
onChange,
|
|
13
13
|
label,
|
|
14
14
|
className,
|
|
15
|
-
disabled
|
|
15
|
+
disabled,
|
|
16
|
+
fill
|
|
16
17
|
}) {
|
|
17
18
|
return /* @__PURE__ */ jsx(
|
|
18
19
|
"div",
|
|
@@ -20,7 +21,8 @@ function Segmented({
|
|
|
20
21
|
role: "group",
|
|
21
22
|
"aria-label": label,
|
|
22
23
|
className: cn(
|
|
23
|
-
"
|
|
24
|
+
"items-center gap-0.5 rounded-(--radius-btn) border border-(--el-border) bg-(--el-tabnav-track) p-0.5",
|
|
25
|
+
fill ? "flex w-full" : "inline-flex",
|
|
24
26
|
className
|
|
25
27
|
),
|
|
26
28
|
children: options.map((opt) => {
|
|
@@ -42,7 +44,11 @@ function Segmented({
|
|
|
42
44
|
// `--radius-control` breaks when a style makes `--radius-btn` a full
|
|
43
45
|
// pill (soft-playful / retrofuturism: pill track, but a small-radius
|
|
44
46
|
// chip floating inside it). Mirrors AppearancePickers' option radius.
|
|
45
|
-
"inline-flex h-(--height-control) items-center gap-1.5 rounded-[calc(var(--radius-btn)-2px)]
|
|
47
|
+
"inline-flex h-(--height-control) items-center gap-1.5 rounded-[calc(var(--radius-btn)-2px)] text-[13px] font-medium transition-colors",
|
|
48
|
+
// In FILL mode the segment's width comes from the track, so the
|
|
49
|
+
// per-style control padding no longer sizes it — keeping it would
|
|
50
|
+
// re-introduce the token dependency the variant exists to remove.
|
|
51
|
+
fill ? "min-w-0 flex-1 justify-center px-1" : "px-(--spacing-control-x)",
|
|
46
52
|
"focus-visible:ring-2 focus-visible:ring-(--focus-ring-color) focus-visible:outline-none",
|
|
47
53
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
48
54
|
active ? "bg-(--el-page-bg) text-(--el-text-strong) shadow-(--shadow-subtle)" : "text-(--el-text-secondary) hover:text-(--el-text)"
|
|
@@ -59,7 +65,7 @@ function Segmented({
|
|
|
59
65
|
children: opt.icon
|
|
60
66
|
}
|
|
61
67
|
) : null,
|
|
62
|
-
opt.label,
|
|
68
|
+
fill ? /* @__PURE__ */ jsx("span", { className: "truncate", children: opt.label }) : opt.label,
|
|
63
69
|
opt.trailing != null ? /* @__PURE__ */ jsx(
|
|
64
70
|
"span",
|
|
65
71
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/Segmented.tsx"],"names":[],"mappings":";;;;AAcO,SAAS,MAAM,MAAA,EAA8B;AAClD,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC7B;ACqBO,SAAS,SAAA,CAA4B;AAAA,EAC1C,OAAA;AAAA,EACA,KAAA;AAAA,EACA,QAAA;AAAA,EACA,KAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF,CAAA,
|
|
1
|
+
{"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/Segmented.tsx"],"names":[],"mappings":";;;;AAcO,SAAS,MAAM,MAAA,EAA8B;AAClD,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC7B;ACqBO,SAAS,SAAA,CAA4B;AAAA,EAC1C,OAAA;AAAA,EACA,KAAA;AAAA,EACA,QAAA;AAAA,EACA,KAAA;AAAA,EACA,SAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAAA,EAuBG;AACD,EAAA,uBACE,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,IAAA,EAAK,OAAA;AAAA,MACL,YAAA,EAAY,KAAA;AAAA,MACZ,SAAA,EAAW,EAAA;AAAA,QACT,sGAAA;AAAA,QACA,OAAO,aAAA,GAAgB,aAAA;AAAA,QACvB;AAAA,OACF;AAAA,MAEC,QAAA,EAAA,OAAA,CAAQ,GAAA,CAAI,CAAC,GAAA,KAAQ;AACpB,QAAA,MAAM,MAAA,GAAS,IAAI,KAAA,KAAU,KAAA;AAC7B,QAAA,uBACE,IAAA;AAAA,UAAC,QAAA;AAAA,UAAA;AAAA,YAEC,IAAA,EAAK,QAAA;AAAA,YACL,cAAA,EAAc,MAAA;AAAA,YACd,QAAA,EAAU,YAAY,GAAA,CAAI,QAAA;AAAA,YAC1B,OAAO,GAAA,CAAI,KAAA;AAAA,YACX,SAAS,MAAM;AACb,cAAA,IAAI,CAAC,MAAA,EAAQ,QAAA,CAAS,GAAA,CAAI,KAAK,CAAA;AAAA,YACjC,CAAA;AAAA,YACA,SAAA,EAAW,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAOT,uIAAA;AAAA;AAAA;AAAA;AAAA,cAIA,OAAO,oCAAA,GAAuC,0BAAA;AAAA,cAC9C,yFAAA;AAAA,cACA,iDAAA;AAAA,cACA,SACI,oEAAA,GACA;AAAA,aACN;AAAA,YAQC,QAAA,EAAA;AAAA,cAAA,GAAA,CAAI,IAAA,mBACH,GAAA;AAAA,gBAAC,MAAA;AAAA,gBAAA;AAAA,kBACC,aAAA,EAAW,IAAA;AAAA,kBACX,SAAA,EAAW,EAAA;AAAA,oBACT,qEAAA;AAAA,oBACA,SAAS,2BAAA,GAA8B;AAAA,mBACzC;AAAA,kBAEC,QAAA,EAAA,GAAA,CAAI;AAAA;AAAA,eACP,GACE,IAAA;AAAA,cAMH,IAAA,uBAAQ,MAAA,EAAA,EAAK,SAAA,EAAU,YAAY,QAAA,EAAA,GAAA,CAAI,KAAA,EAAM,IAAU,GAAA,CAAI,KAAA;AAAA,cAC3D,GAAA,CAAI,YAAY,IAAA,mBACf,GAAA;AAAA,gBAAC,MAAA;AAAA,gBAAA;AAAA,kBACC,SAAA,EAAW,EAAA;AAAA,oBACT,kDAAA;AAAA,oBACA,SAAS,2BAAA,GAA8B;AAAA,mBACzC;AAAA,kBAEC,QAAA,EAAA,GAAA,CAAI;AAAA;AAAA,eACP,GACE;AAAA;AAAA,WAAA;AAAA,UA3DC,GAAA,CAAI;AAAA,SA4DX;AAAA,MAEJ,CAAC;AAAA;AAAA,GACH;AAEJ","file":"Segmented.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 { cn } from '../../utils/cn';\n\n// Segmented — a single-select inline control: a row of mutually-exclusive\n// buttons where the active option is raised (`aria-pressed`). The accessible,\n// space-cheap alternative to a `<select>` when the option set is small and worth\n// showing at a glance (the board group-by, Subtask 3.3.5; reusable beyond it).\n//\n// Built FROM the design system per `design/boards/swimlanes-wip.mock.html` (the\n// `.seg` block): an `--el-surface` track (`--radius-btn`) with a 2px inset, each\n// option a `calc(--radius-btn - 2px)` button so it NESTS in the track at any style\n// (a fixed `--radius-control` floats wrong when a style pills `--radius-btn`); the\n// pressed option gets the `--el-page-bg` raised fill + `--shadow-subtle` and its\n// leading glyph takes the `--el-accent` hue.\n// Colour via `--el-*`, shape via element-semantic tokens (the colour + shape\n// swap rules). A11y: a labelled `role=\"group\"`; each option is a real `<button>`\n// carrying `aria-pressed`, so it is keyboard-operable and announced as a toggle.\n\nexport interface SegmentedOption<T extends string> {\n value: T;\n label: string;\n /** Optional leading glyph (decorative — the label carries the accessible name). */\n icon?: ReactNode;\n /** Optional trailing content after the label (e.g. the notification drawer's\n * unread count on the Direct tab, Subtask 5.7.5). Decorative — faint when the\n * option is inactive, the `--el-accent` hue when active (mirrors the leading\n * glyph's active treatment). */\n trailing?: ReactNode;\n /** Disable just this option — a forward-compatible seam (e.g. the Activity\n * card's History filter, Story 5.5's slot) drawn present-but-inert. */\n disabled?: boolean;\n /** Tooltip for the option (e.g. why a seam option is disabled). */\n title?: string;\n}\n\nexport function Segmented<T extends string>({\n options,\n value,\n onChange,\n label,\n className,\n disabled,\n fill,\n}: {\n options: SegmentedOption<T>[];\n value: T;\n onChange: (value: T) => void;\n /** Accessible group name (e.g. \"Swimlane group by\") — not rendered visually. */\n label: string;\n className?: string;\n disabled?: boolean;\n /**\n * FILL the container instead of sizing to the options (MOTIR-6200, for\n * MOTIR-6199): the track becomes `w-full` and the segments divide it evenly.\n *\n * WHY IT IS A VARIANT AND NOT THE DEFAULT. The track is `inline-flex` with no\n * wrap and no shrink, so its width is `max-content` — fine in a toolbar that\n * can grow, and unsurvivable in a FIXED rail. The Difficulty editor sits in an\n * 18rem item-page rail and a 300px quick-view rail, and when its scale gained a\n * fourth member the track overran both. Filling makes the width the\n * CONTAINER's, so it holds at any label set, at any `--spacing-control-x`\n * (10 / 12 / 14px across the shipped styles) and at a fifth member.\n *\n * Off by default: every other call site keeps byte-identical markup.\n */\n fill?: boolean;\n}) {\n return (\n <div\n role=\"group\"\n aria-label={label}\n className={cn(\n 'items-center gap-0.5 rounded-(--radius-btn) border border-(--el-border) bg-(--el-tabnav-track) p-0.5',\n fill ? 'flex w-full' : 'inline-flex',\n className,\n )}\n >\n {options.map((opt) => {\n const active = opt.value === value;\n return (\n <button\n key={opt.value}\n type=\"button\"\n aria-pressed={active}\n disabled={disabled || opt.disabled}\n title={opt.title}\n onClick={() => {\n if (!active) onChange(opt.value);\n }}\n className={cn(\n // The segment radius nests inside the track: the track is\n // `rounded-(--radius-btn)` with a 2px (`p-0.5`) inset, so a segment\n // fits the shell only at `--radius-btn - 2px`. Using a fixed\n // `--radius-control` breaks when a style makes `--radius-btn` a full\n // pill (soft-playful / retrofuturism: pill track, but a small-radius\n // chip floating inside it). Mirrors AppearancePickers' option radius.\n 'inline-flex h-(--height-control) items-center gap-1.5 rounded-[calc(var(--radius-btn)-2px)] text-[13px] font-medium transition-colors',\n // In FILL mode the segment's width comes from the track, so the\n // per-style control padding no longer sizes it — keeping it would\n // re-introduce the token dependency the variant exists to remove.\n fill ? 'min-w-0 flex-1 justify-center px-1' : 'px-(--spacing-control-x)',\n 'focus-visible:ring-2 focus-visible:ring-(--focus-ring-color) focus-visible:outline-none',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n active\n ? 'bg-(--el-page-bg) text-(--el-text-strong) shadow-(--shadow-subtle)'\n : 'text-(--el-text-secondary) hover:text-(--el-text)',\n )}\n >\n {/* The icon and the trailing count take their ink HERE rather than\n through the segment's `[&_.seg-*]:…` descendant variants\n (MOTIR-2475). Two things follow: the glyph says it is\n `aria-hidden`, so its faint ink is one of the token's legitimate\n jobs; and the trailing COUNT — text a reader reads — is no\n longer painted at 2.4:1 on an inactive segment. */}\n {opt.icon ? (\n <span\n aria-hidden\n className={cn(\n 'seg-ic inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center',\n active ? 'text-(--el-tabnav-active)' : 'text-(--el-text-faint)',\n )}\n >\n {opt.icon}\n </span>\n ) : null}\n {/* FILL mode gives each segment a share of the track rather than its\n own content width, so a long label has to yield rather than push.\n Truncating needs an element to truncate, hence the span — added\n ONLY in fill mode, so every other call site's markup is\n unchanged. */}\n {fill ? <span className=\"truncate\">{opt.label}</span> : opt.label}\n {opt.trailing != null ? (\n <span\n className={cn(\n 'seg-trail text-[11px] font-semibold tabular-nums',\n active ? 'text-(--el-tabnav-active)' : 'text-(--el-text-secondary)',\n )}\n >\n {opt.trailing}\n </span>\n ) : null}\n </button>\n );\n })}\n </div>\n );\n}\n"]}
|
|
@@ -4,15 +4,34 @@ import { TextareaHTMLAttributes } from 'react';
|
|
|
4
4
|
/**
|
|
5
5
|
* Textarea — multi-line text field. Mirrors Input's props (label, error,
|
|
6
6
|
* helperText) but no prefix/suffix slots (uncommon shape for textareas).
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
|
+
* **Auto-resize is OPT-IN.** Pass `rows` alone and the field is a fixed height,
|
|
9
|
+
* exactly as it has always been. Pass `autoGrow` and `rows` becomes the MINIMUM:
|
|
10
|
+
* the field grows with its content, line by line, up to `maxRows`, then stops
|
|
11
|
+
* and scrolls inside itself. A chat composer passes `rows={1}`.
|
|
8
12
|
*
|
|
9
13
|
* @example
|
|
10
14
|
* <Textarea label="Description" rows={4} helperText="Max 500 chars" />
|
|
15
|
+
* <Textarea autoGrow rows={1} maxRows={8} aria-label="Message" />
|
|
11
16
|
*/
|
|
12
17
|
interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
13
18
|
label?: string;
|
|
14
19
|
error?: string;
|
|
15
20
|
helperText?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Grow with the content instead of standing at a fixed `rows`. Default
|
|
23
|
+
* `false`, so every existing caller renders byte-for-byte as before.
|
|
24
|
+
*
|
|
25
|
+
* With it, `rows` is the MINIMUM height and the manual resize handle is
|
|
26
|
+
* removed — a hand-dragged height is overwritten by the next keystroke's
|
|
27
|
+
* measurement, so the two cannot both be offered.
|
|
28
|
+
*/
|
|
29
|
+
autoGrow?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The cap, in rows, for an `autoGrow` field. Omitted means no cap: the field
|
|
32
|
+
* grows for as long as its content does. Meaningless without `autoGrow`.
|
|
33
|
+
*/
|
|
34
|
+
maxRows?: number;
|
|
16
35
|
}
|
|
17
36
|
declare const Textarea: react.ForwardRefExoticComponent<TextareaProps & react.RefAttributes<HTMLTextAreaElement>>;
|
|
18
37
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
import { forwardRef, useId, useRef, useCallback, useLayoutEffect } from 'react';
|
|
2
3
|
import { clsx } from 'clsx';
|
|
3
4
|
import { twMerge } from 'tailwind-merge';
|
|
4
5
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
6
|
|
|
6
|
-
// src/components/ui/Textarea.tsx
|
|
7
7
|
function cn(...inputs) {
|
|
8
8
|
return twMerge(clsx(inputs));
|
|
9
9
|
}
|
|
@@ -38,22 +38,55 @@ function FormField({
|
|
|
38
38
|
function describedById(htmlFor, error, helperText) {
|
|
39
39
|
return error ? `${htmlFor}-error` : helperText ? `${htmlFor}-helper` : void 0;
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
function resize(el, rows, maxRows) {
|
|
42
|
+
const cs = getComputedStyle(el);
|
|
43
|
+
const parsedLineHeight = Number.parseFloat(cs.lineHeight);
|
|
44
|
+
const lineHeight = Number.isFinite(parsedLineHeight) ? parsedLineHeight : Number.parseFloat(cs.fontSize) * 1.2;
|
|
45
|
+
const padding = Number.parseFloat(cs.paddingTop) + Number.parseFloat(cs.paddingBottom);
|
|
46
|
+
const border = cs.boxSizing === "border-box" ? Number.parseFloat(cs.borderTopWidth) + Number.parseFloat(cs.borderBottomWidth) : 0;
|
|
47
|
+
const min = rows * lineHeight + padding + border;
|
|
48
|
+
const max = maxRows === void 0 ? Infinity : maxRows * lineHeight + padding + border;
|
|
49
|
+
el.style.height = "auto";
|
|
50
|
+
const needed = el.scrollHeight + border;
|
|
51
|
+
const next = Math.min(Math.max(needed, min), max);
|
|
52
|
+
el.style.height = `${next}px`;
|
|
53
|
+
el.style.overflowY = needed > max ? "auto" : "hidden";
|
|
54
|
+
}
|
|
55
|
+
var Textarea = forwardRef(function Textarea2({ label, error, helperText, id, className, disabled, rows = 3, autoGrow, maxRows, ...rest }, ref) {
|
|
42
56
|
const autoId = useId();
|
|
43
57
|
const textareaId = id ?? autoId;
|
|
44
58
|
const describedBy = describedById(textareaId, error, helperText);
|
|
45
59
|
const hasError = Boolean(error);
|
|
60
|
+
const innerRef = useRef(null);
|
|
61
|
+
const setRefs = useCallback(
|
|
62
|
+
(node) => {
|
|
63
|
+
innerRef.current = node;
|
|
64
|
+
if (typeof ref === "function") ref(node);
|
|
65
|
+
else if (ref) ref.current = node;
|
|
66
|
+
},
|
|
67
|
+
[ref]
|
|
68
|
+
);
|
|
69
|
+
useLayoutEffect(() => {
|
|
70
|
+
const el = innerRef.current;
|
|
71
|
+
if (!el || !autoGrow) return;
|
|
72
|
+
resize(el, rows, maxRows);
|
|
73
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
74
|
+
const observer = new ResizeObserver(() => resize(el, rows, maxRows));
|
|
75
|
+
observer.observe(el);
|
|
76
|
+
return () => observer.disconnect();
|
|
77
|
+
}, [autoGrow, rows, maxRows, rest.value, rest.defaultValue]);
|
|
46
78
|
return /* @__PURE__ */ jsx(FormField, { label, error, helperText, htmlFor: textareaId, children: /* @__PURE__ */ jsx(
|
|
47
79
|
"textarea",
|
|
48
80
|
{
|
|
49
|
-
ref,
|
|
81
|
+
ref: setRefs,
|
|
50
82
|
id: textareaId,
|
|
51
83
|
rows,
|
|
52
84
|
"aria-invalid": hasError || void 0,
|
|
53
85
|
"aria-describedby": describedBy,
|
|
54
86
|
disabled,
|
|
55
87
|
className: cn(
|
|
56
|
-
"w-full
|
|
88
|
+
"w-full rounded-(--radius-input) border bg-(--el-page-bg)",
|
|
89
|
+
autoGrow ? "resize-none" : "resize-y",
|
|
57
90
|
"px-(--spacing-input-x) py-(--spacing-input-y)",
|
|
58
91
|
"font-sans text-sm text-(--el-text) placeholder:text-(--el-text-muted)",
|
|
59
92
|
"transition-colors",
|
|
@@ -62,7 +95,11 @@ var Textarea = forwardRef(function Textarea2({ label, error, helperText, id, cla
|
|
|
62
95
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
63
96
|
className
|
|
64
97
|
),
|
|
65
|
-
...rest
|
|
98
|
+
...rest,
|
|
99
|
+
onInput: (event) => {
|
|
100
|
+
if (autoGrow) resize(event.currentTarget, rows, maxRows);
|
|
101
|
+
rest.onInput?.(event);
|
|
102
|
+
}
|
|
66
103
|
}
|
|
67
104
|
) });
|
|
68
105
|
});
|
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/FormField.tsx","../../../src/components/ui/Textarea.tsx"],"names":["describedById","Textarea","jsx"],"mappings":"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","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","'use client';\n\nimport {\n forwardRef,\n useCallback,\n useId,\n useLayoutEffect,\n useRef,\n type TextareaHTMLAttributes,\n} 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 *\n * **Auto-resize is OPT-IN.** Pass `rows` alone and the field is a fixed height,\n * exactly as it has always been. Pass `autoGrow` and `rows` becomes the MINIMUM:\n * the field grows with its content, line by line, up to `maxRows`, then stops\n * and scrolls inside itself. A chat composer passes `rows={1}`.\n *\n * @example\n * <Textarea label=\"Description\" rows={4} helperText=\"Max 500 chars\" />\n * <Textarea autoGrow rows={1} maxRows={8} aria-label=\"Message\" />\n */\nexport interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {\n label?: string;\n error?: string;\n helperText?: string;\n /**\n * Grow with the content instead of standing at a fixed `rows`. Default\n * `false`, so every existing caller renders byte-for-byte as before.\n *\n * With it, `rows` is the MINIMUM height and the manual resize handle is\n * removed — a hand-dragged height is overwritten by the next keystroke's\n * measurement, so the two cannot both be offered.\n */\n autoGrow?: boolean;\n /**\n * The cap, in rows, for an `autoGrow` field. Omitted means no cap: the field\n * grows for as long as its content does. Meaningless without `autoGrow`.\n */\n maxRows?: number;\n}\n\n/**\n * Measure and clamp — in JS, deliberately, not on `field-sizing: content`\n * alone. That property has not shipped in every engine the product supports;\n * this path behaves the same everywhere and is testable in jsdom by stubbing\n * `scrollHeight`.\n *\n * The line height, padding and border are read from the COMPUTED style rather\n * than assumed, so the clamp stays exact under every palette, type scale and\n * density the design system's axes produce.\n */\nfunction resize(el: HTMLTextAreaElement, rows: number, maxRows: number | undefined): void {\n const cs = getComputedStyle(el);\n // `normal` has no pixel value to clamp against; fall back to the font size's\n // usual ratio rather than guessing a constant.\n const parsedLineHeight = Number.parseFloat(cs.lineHeight);\n const lineHeight = Number.isFinite(parsedLineHeight)\n ? parsedLineHeight\n : Number.parseFloat(cs.fontSize) * 1.2;\n const padding = Number.parseFloat(cs.paddingTop) + Number.parseFloat(cs.paddingBottom);\n // `box-sizing: border-box` is the design system's default, so the border is\n // inside the height we set and has to be added back to the content we measure.\n const border =\n cs.boxSizing === 'border-box'\n ? Number.parseFloat(cs.borderTopWidth) + Number.parseFloat(cs.borderBottomWidth)\n : 0;\n\n const min = rows * lineHeight + padding + border;\n const max = maxRows === undefined ? Infinity : maxRows * lineHeight + padding + border;\n\n // Reset first: `scrollHeight` reports the CURRENT box when the content fits,\n // so a field that has already grown would never shrink back.\n el.style.height = 'auto';\n const needed = el.scrollHeight + border;\n const next = Math.min(Math.max(needed, min), max);\n el.style.height = `${next}px`;\n // Below the cap there is nothing to scroll, and a scrollbar that appears for\n // one frame while growing is the tell that this is done wrong.\n el.style.overflowY = needed > max ? 'auto' : 'hidden';\n}\n\nexport const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(\n { label, error, helperText, id, className, disabled, rows = 3, autoGrow, maxRows, ...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 const innerRef = useRef<HTMLTextAreaElement | null>(null);\n\n // The caller's ref still has to reach the <textarea> — the planning composer\n // builds its key handling on it — so the two are merged rather than replaced.\n const setRefs = useCallback(\n (node: HTMLTextAreaElement | null) => {\n innerRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) ref.current = node;\n },\n [ref],\n );\n\n // LAYOUT effect, not a plain one: a field mounted with a pre-filled value must\n // paint at its final height, and an ordinary effect lets the browser paint one\n // line first — the flash a seeded composer would show on every open.\n useLayoutEffect(() => {\n const el = innerRef.current;\n if (!el || !autoGrow) return;\n resize(el, rows, maxRows);\n // The height follows the VALUE whoever changes it — typing, a paste, a\n // controlled value set by the parent (a prefill, or a clear to '' after\n // send) — and `rest.value` is what re-runs this for the controlled case.\n // It also follows the WIDTH, because wrapping changes with width, which no\n // value change reports.\n if (typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(() => resize(el, rows, maxRows));\n observer.observe(el);\n return () => observer.disconnect();\n }, [autoGrow, rows, maxRows, rest.value, rest.defaultValue]);\n\n return (\n <FormField label={label} error={error} helperText={helperText} htmlFor={textareaId}>\n <textarea\n ref={setRefs}\n id={textareaId}\n rows={rows}\n aria-invalid={hasError || undefined}\n aria-describedby={describedBy}\n disabled={disabled}\n className={cn(\n 'w-full rounded-(--radius-input) border bg-(--el-page-bg)',\n autoGrow ? 'resize-none' : 'resize-y',\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 // AFTER the spread, deliberately: it must not be replaceable, because\n // the height is the primitive's own contract. A caller's own `onInput`\n // still runs — it is called through, not dropped.\n onInput={(event) => {\n if (autoGrow) resize(event.currentTarget, rows, maxRows);\n rest.onInput?.(event);\n }}\n />\n </FormField>\n );\n});\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motir/design-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Motir's 3-axis design system (colour · style · type): distributable token CSS, the axis registries, the theme-apply API, and framework-agnostic React primitives. Shared by motir-core and planner-scaffolded products.",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"repository": {
|
package/theme.css
CHANGED
|
@@ -3654,6 +3654,11 @@
|
|
|
3654
3654
|
--el-type-verification: var(--color-accent-orange); /* orange — with review (Investigate) */
|
|
3655
3655
|
--el-type-legal: var(--color-charcoal); /* ink — with decision (Govern) */
|
|
3656
3656
|
|
|
3657
|
+
/* ADR Amendment 3 (MOTIR-5886) admits `choice`; MOTIR-5888's design gives it
|
|
3658
|
+
the same precedent — `decision`'s charcoal, its OWN token, and a glyph
|
|
3659
|
+
(`signpost`) that tells the two apart. */
|
|
3660
|
+
--el-type-choice: var(--color-charcoal); /* ink — with decision (Govern) */
|
|
3661
|
+
|
|
3657
3662
|
/* ── Chart series (Story 4.6 · the reusable viz Story 6.3 reuses) ──
|
|
3658
3663
|
Every chart colour routes through one of these, NEVER a raw --color-*
|
|
3659
3664
|
in chart code (the per-component growth pattern). They map to Tier-0
|