@devalok/shilp-sutra 0.37.0 → 0.37.1
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/composed/inline-edit.d.ts.map +1 -1
- package/dist/composed/inline-edit.js +42 -40
- package/dist/composed/inline-edit.js.map +1 -1
- package/dist/test-utils/conformance.d.ts +37 -0
- package/dist/test-utils/conformance.d.ts.map +1 -0
- package/docs/components/composed/activity-feed.md +6 -0
- package/docs/components/composed/avatar-group.md +8 -0
- package/docs/components/composed/bulk-action-bar.md +6 -0
- package/docs/components/composed/command-palette.md +7 -0
- package/docs/components/composed/confirm-dialog.md +6 -0
- package/docs/components/composed/content-card.md +6 -0
- package/docs/components/composed/date-picker.md +10 -0
- package/docs/components/composed/deadline-indicator.md +7 -0
- package/docs/components/composed/emoji-picker.md +8 -0
- package/docs/components/composed/empty-state.md +8 -0
- package/docs/components/composed/error-boundary.md +7 -0
- package/docs/components/composed/file-preview.md +8 -0
- package/docs/components/composed/filter-bar.md +8 -0
- package/docs/components/composed/form-section.md +6 -0
- package/docs/components/composed/global-loading.md +6 -0
- package/docs/components/composed/inline-edit.md +8 -0
- package/docs/components/composed/loading-skeleton.md +7 -0
- package/docs/components/composed/markdown-viewer.md +9 -0
- package/docs/components/composed/master-detail.md +7 -0
- package/docs/components/composed/member-picker.md +7 -0
- package/docs/components/composed/multi-select-popover.md +11 -0
- package/docs/components/composed/page-header.md +7 -0
- package/docs/components/composed/page-skeletons.md +10 -0
- package/docs/components/composed/priority-indicator.md +7 -0
- package/docs/components/composed/responsive-overlay.md +7 -0
- package/docs/components/composed/rich-chat-input.md +12 -0
- package/docs/components/composed/rich-text-editor.md +9 -0
- package/docs/components/composed/schedule-view.md +8 -0
- package/docs/components/composed/simple-tooltip.md +7 -0
- package/docs/components/composed/status-badge.md +10 -0
- package/docs/components/shell/app-command-palette.md +8 -0
- package/docs/components/shell/bottom-navbar.md +12 -0
- package/docs/components/shell/command-registry.md +7 -0
- package/docs/components/shell/link-context.md +15 -0
- package/docs/components/shell/notification-center.md +10 -0
- package/docs/components/shell/notification-preferences.md +7 -0
- package/docs/components/shell/sidebar.md +11 -0
- package/docs/components/shell/top-bar.md +11 -0
- package/docs/components/ui/accordion.md +10 -0
- package/docs/components/ui/alert-dialog.md +10 -1
- package/docs/components/ui/alert.md +10 -2
- package/docs/components/ui/aspect-ratio.md +7 -0
- package/docs/components/ui/autocomplete.md +7 -0
- package/docs/components/ui/avatar.md +10 -0
- package/docs/components/ui/badge-group.md +7 -0
- package/docs/components/ui/badge-indicator.md +7 -0
- package/docs/components/ui/badge.md +12 -0
- package/docs/components/ui/banner.md +7 -0
- package/docs/components/ui/breadcrumb.md +8 -0
- package/docs/components/ui/button-group.md +24 -4
- package/docs/components/ui/button-processing.md +5 -0
- package/docs/components/ui/button.md +9 -0
- package/docs/components/ui/card.md +16 -4
- package/docs/components/ui/charts.md +9 -0
- package/docs/components/ui/chat.md +9 -0
- package/docs/components/ui/checkbox.md +15 -1
- package/docs/components/ui/chip.md +9 -0
- package/docs/components/ui/code.md +7 -0
- package/docs/components/ui/collapsible.md +9 -0
- package/docs/components/ui/color-input.md +8 -0
- package/docs/components/ui/color-swatch.md +7 -0
- package/docs/components/ui/combobox.md +16 -3
- package/docs/components/ui/container.md +11 -0
- package/docs/components/ui/context-menu.md +8 -0
- package/docs/components/ui/data-table-body.md +4 -0
- package/docs/components/ui/data-table-bulk-actions.md +4 -0
- package/docs/components/ui/data-table-card.md +4 -0
- package/docs/components/ui/data-table-context.md +4 -0
- package/docs/components/ui/data-table-header.md +4 -0
- package/docs/components/ui/data-table-pagination.md +4 -0
- package/docs/components/ui/data-table-toolbar.md +7 -0
- package/docs/components/ui/data-table.md +25 -0
- package/docs/components/ui/devalok-grain.md +8 -0
- package/docs/components/ui/dialog.md +10 -0
- package/docs/components/ui/dropdown-menu.md +10 -0
- package/docs/components/ui/file-upload.md +8 -0
- package/docs/components/ui/form.md +21 -0
- package/docs/components/ui/hover-card.md +9 -0
- package/docs/components/ui/icon-button.md +7 -0
- package/docs/components/ui/icon-context.md +7 -0
- package/docs/components/ui/icon-group.md +9 -0
- package/docs/components/ui/icon.md +7 -0
- package/docs/components/ui/input-otp.md +29 -7
- package/docs/components/ui/input.md +7 -0
- package/docs/components/ui/label.md +7 -1
- package/docs/components/ui/link.md +7 -0
- package/docs/components/ui/menubar.md +9 -0
- package/docs/components/ui/navigation-menu.md +9 -0
- package/docs/components/ui/number-input.md +10 -0
- package/docs/components/ui/pagination.md +10 -0
- package/docs/components/ui/popover.md +9 -0
- package/docs/components/ui/progress-ring.md +7 -0
- package/docs/components/ui/progress.md +7 -0
- package/docs/components/ui/radio.md +27 -2
- package/docs/components/ui/search-input.md +8 -0
- package/docs/components/ui/segmented-control.md +8 -0
- package/docs/components/ui/select.md +14 -3
- package/docs/components/ui/separator.md +7 -0
- package/docs/components/ui/sheet.md +10 -0
- package/docs/components/ui/sidebar.md +30 -0
- package/docs/components/ui/skeleton.md +7 -0
- package/docs/components/ui/slider.md +15 -1
- package/docs/components/ui/spinner.md +7 -0
- package/docs/components/ui/split-button.md +11 -0
- package/docs/components/ui/stack.md +8 -0
- package/docs/components/ui/stat-card.md +9 -0
- package/docs/components/ui/status-dot.md +9 -0
- package/docs/components/ui/stepper.md +8 -0
- package/docs/components/ui/switch.md +8 -0
- package/docs/components/ui/table.md +8 -0
- package/docs/components/ui/tabs.md +10 -2
- package/docs/components/ui/text.md +9 -4
- package/docs/components/ui/textarea.md +7 -1
- package/docs/components/ui/toast.md +9 -0
- package/docs/components/ui/toaster.md +8 -0
- package/docs/components/ui/toggle-group.md +13 -4
- package/docs/components/ui/toggle.md +10 -2
- package/docs/components/ui/tooltip.md +11 -1
- package/docs/components/ui/tree-view.md +12 -0
- package/docs/components/ui/visually-hidden.md +7 -0
- package/llms-full.txt +1116 -44
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inline-edit.d.ts","sourceRoot":"","sources":["../../src/composed/inline-edit.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAU9B,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC;IAC3F,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,+EAA+E;IAC/E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAUD,QAAA,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"inline-edit.d.ts","sourceRoot":"","sources":["../../src/composed/inline-edit.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAU9B,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC;IAC3F,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,+EAA+E;IAC/E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAUD,QAAA,MAAM,UAAU,wFAyJd,CAAA;AAGF,OAAO,EAAE,UAAU,EAAE,CAAA"}
|
|
@@ -6,86 +6,88 @@ import * as r from "react";
|
|
|
6
6
|
import { jsx as i, jsxs as a } from "react/jsx-runtime";
|
|
7
7
|
import { IconPencil as o } from "@tabler/icons-react";
|
|
8
8
|
//#region src/composed/inline-edit.tsx
|
|
9
|
-
var s = r.forwardRef(({ value: s, onSave: c, placeholder: l = "Click to edit", textClassName: u, readOnly: d = !1, maxLength: f, saving: p = !1, className: m,
|
|
10
|
-
let [
|
|
9
|
+
var s = r.forwardRef(({ value: s, onSave: c, placeholder: l = "Click to edit", textClassName: u, readOnly: d = !1, maxLength: f, saving: p = !1, className: m, "aria-label": h, "aria-labelledby": g, ..._ }, v) => {
|
|
10
|
+
let [y, b] = r.useState(!1), [x, S] = r.useState(!1), C = r.useRef(null), w = r.useRef(s), T = p || y, E = !s;
|
|
11
11
|
r.useEffect(() => {
|
|
12
|
-
!
|
|
13
|
-
}, [s,
|
|
14
|
-
function
|
|
15
|
-
d ||
|
|
16
|
-
if (!
|
|
12
|
+
!x && C.current && (C.current.textContent = s);
|
|
13
|
+
}, [s, x]);
|
|
14
|
+
function D() {
|
|
15
|
+
d || T || (S(!0), w.current = s, requestAnimationFrame(() => {
|
|
16
|
+
if (!C.current) return;
|
|
17
17
|
let e = document.createRange();
|
|
18
|
-
e.selectNodeContents(
|
|
18
|
+
e.selectNodeContents(C.current);
|
|
19
19
|
let t = window.getSelection();
|
|
20
20
|
t?.removeAllRanges(), t?.addRange(e);
|
|
21
21
|
}));
|
|
22
22
|
}
|
|
23
|
-
async function
|
|
24
|
-
if (!
|
|
25
|
-
|
|
26
|
-
let e = (
|
|
27
|
-
if (
|
|
23
|
+
async function O() {
|
|
24
|
+
if (!C.current) return;
|
|
25
|
+
S(!1);
|
|
26
|
+
let e = (C.current.textContent ?? "").trim(), t = f ? e.slice(0, f) : e;
|
|
27
|
+
if (C.current.textContent = t || s, t === s) return;
|
|
28
28
|
let n = c(t);
|
|
29
29
|
if (n instanceof Promise) {
|
|
30
|
-
|
|
30
|
+
b(!0);
|
|
31
31
|
try {
|
|
32
32
|
await n;
|
|
33
33
|
} catch {
|
|
34
|
-
|
|
34
|
+
C.current && (C.current.textContent = s);
|
|
35
35
|
} finally {
|
|
36
|
-
|
|
36
|
+
b(!1);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
function
|
|
41
|
-
|
|
40
|
+
function k() {
|
|
41
|
+
C.current && (C.current.textContent = w.current, S(!1), C.current.blur());
|
|
42
42
|
}
|
|
43
|
-
function
|
|
44
|
-
e.key === "Escape" ? (e.preventDefault(),
|
|
43
|
+
function A(e) {
|
|
44
|
+
e.key === "Escape" ? (e.preventDefault(), k()) : e.key === "Enter" && (e.preventDefault(), O());
|
|
45
45
|
}
|
|
46
|
-
function
|
|
47
|
-
if (!
|
|
48
|
-
let e =
|
|
46
|
+
function j() {
|
|
47
|
+
if (!C.current || !f) return;
|
|
48
|
+
let e = C.current.textContent ?? "";
|
|
49
49
|
if (e.length > f) {
|
|
50
|
-
|
|
50
|
+
C.current.textContent = e.slice(0, f);
|
|
51
51
|
let t = document.createRange();
|
|
52
|
-
t.selectNodeContents(
|
|
52
|
+
t.selectNodeContents(C.current), t.collapse(!1);
|
|
53
53
|
let n = window.getSelection();
|
|
54
54
|
n?.removeAllRanges(), n?.addRange(t);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
function
|
|
57
|
+
function M(e) {
|
|
58
58
|
e.preventDefault();
|
|
59
59
|
let t = e.clipboardData.getData("text/plain");
|
|
60
60
|
document.execCommand("insertText", !1, t);
|
|
61
61
|
}
|
|
62
62
|
return /* @__PURE__ */ a("div", {
|
|
63
|
-
ref:
|
|
63
|
+
ref: v,
|
|
64
64
|
className: e("group inline-flex items-center gap-ds-02", m),
|
|
65
|
-
...
|
|
65
|
+
..._,
|
|
66
66
|
children: [
|
|
67
67
|
/* @__PURE__ */ i("span", {
|
|
68
|
-
ref:
|
|
68
|
+
ref: C,
|
|
69
69
|
role: d ? void 0 : "textbox",
|
|
70
|
-
contentEditable: !d && !
|
|
70
|
+
contentEditable: !d && !T,
|
|
71
71
|
suppressContentEditableWarning: !0,
|
|
72
72
|
tabIndex: d ? void 0 : 0,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
"aria-label": d ? void 0 : h ?? (g ? void 0 : l),
|
|
74
|
+
"aria-labelledby": d ? void 0 : g,
|
|
75
|
+
onFocus: D,
|
|
76
|
+
onBlur: O,
|
|
77
|
+
onKeyDown: A,
|
|
78
|
+
onInput: j,
|
|
79
|
+
onPaste: M,
|
|
80
|
+
spellCheck: x,
|
|
81
|
+
className: e("font-sans text-surface-fg outline-hidden", u, !d && !x && "cursor-text rounded-ds-sm -mx-ds-01 px-ds-01 hover:bg-surface-raised-hover transition-colors duration-fast-01", x && "rounded-ds-sm -mx-ds-01 px-ds-01 bg-surface-raised-hover ring-1 ring-accent-7", E && !x && "text-surface-fg-subtle italic", d && "cursor-default"),
|
|
80
82
|
"data-placeholder": l,
|
|
81
|
-
children: s || (
|
|
83
|
+
children: s || (x ? "" : l)
|
|
82
84
|
}),
|
|
83
|
-
!d && !
|
|
85
|
+
!d && !x && !T && /* @__PURE__ */ i(n, {
|
|
84
86
|
icon: o,
|
|
85
87
|
size: "xs",
|
|
86
88
|
className: "text-surface-fg-subtle opacity-0 group-hover:opacity-100 transition-opacity duration-fast-01 shrink-0"
|
|
87
89
|
}),
|
|
88
|
-
|
|
90
|
+
T && /* @__PURE__ */ i(t, { size: "sm" })
|
|
89
91
|
]
|
|
90
92
|
});
|
|
91
93
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inline-edit.js","names":[],"sources":["../../src/composed/inline-edit.tsx"],"sourcesContent":["'use client'\n\nimport { IconPencil } from '@tabler/icons-react'\nimport * as React from 'react'\n\nimport { Icon } from '../ui/icon'\nimport { cn } from '../ui/lib/utils'\nimport { Spinner } from '../ui/spinner'\n\n// ============================================================\n// Types\n// ============================================================\n\nexport interface InlineEditProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSave'> {\n value: string\n onSave: (newValue: string) => void | Promise<void>\n placeholder?: string\n /** CSS class applied to the editable text (e.g. 'text-ds-lg font-semibold') */\n textClassName?: string\n readOnly?: boolean\n maxLength?: number\n /** External saving state — shows spinner */\n saving?: boolean\n}\n\n// ============================================================\n// InlineEdit — contentEditable approach\n//\n// No mode switch. No input field. The text IS the editor.\n// Click → cursor appears in the text. Type. Enter saves. Escape reverts.\n// Like Notion, Linear, Figma layer names.\n// ============================================================\n\nconst InlineEdit = React.forwardRef<HTMLDivElement, InlineEditProps>(({\n value,\n onSave,\n placeholder = 'Click to edit',\n textClassName,\n readOnly = false,\n maxLength,\n saving: savingProp = false,\n className,\n ...props\n}, ref) => {\n const [saving, setSaving] = React.useState(false)\n const [focused, setFocused] = React.useState(false)\n const editRef = React.useRef<HTMLSpanElement>(null)\n const snapshotRef = React.useRef(value)\n\n const isSaving = savingProp || saving\n const isEmpty = !value\n\n // Sync DOM text when value changes externally (and not focused)\n React.useEffect(() => {\n if (!focused && editRef.current) {\n editRef.current.textContent = value\n }\n }, [value, focused])\n\n function handleFocus() {\n if (readOnly || isSaving) return\n setFocused(true)\n snapshotRef.current = value\n\n // Select all text on focus (like clicking a file name in Finder)\n requestAnimationFrame(() => {\n if (!editRef.current) return\n const range = document.createRange()\n range.selectNodeContents(editRef.current)\n const sel = window.getSelection()\n sel?.removeAllRanges()\n sel?.addRange(range)\n })\n }\n\n async function commit() {\n if (!editRef.current) return\n setFocused(false)\n const raw = editRef.current.textContent ?? ''\n const trimmed = raw.trim()\n\n // Enforce maxLength\n const final = maxLength ? trimmed.slice(0, maxLength) : trimmed\n\n // Reset DOM to clean value\n editRef.current.textContent = final || value\n\n if (final === value) return\n const result = onSave(final)\n if (result instanceof Promise) {\n setSaving(true)\n try {\n await result\n } catch {\n // Revert on error\n if (editRef.current) editRef.current.textContent = value\n } finally {\n setSaving(false)\n }\n }\n }\n\n function cancel() {\n if (!editRef.current) return\n editRef.current.textContent = snapshotRef.current\n setFocused(false)\n editRef.current.blur()\n }\n\n function handleKeyDown(e: React.KeyboardEvent) {\n if (e.key === 'Escape') {\n e.preventDefault()\n cancel()\n } else if (e.key === 'Enter') {\n e.preventDefault()\n commit()\n }\n }\n\n function handleInput() {\n if (!editRef.current || !maxLength) return\n const text = editRef.current.textContent ?? ''\n if (text.length > maxLength) {\n editRef.current.textContent = text.slice(0, maxLength)\n // Move cursor to end\n const range = document.createRange()\n range.selectNodeContents(editRef.current)\n range.collapse(false)\n const sel = window.getSelection()\n sel?.removeAllRanges()\n sel?.addRange(range)\n }\n }\n\n // Prevent paste from inserting rich content\n function handlePaste(e: React.ClipboardEvent) {\n e.preventDefault()\n const text = e.clipboardData.getData('text/plain')\n document.execCommand('insertText', false, text)\n }\n\n return (\n <div ref={ref} className={cn('group inline-flex items-center gap-ds-02', className)} {...props}>\n <span\n ref={editRef}\n role={readOnly ? undefined : 'textbox'}\n contentEditable={!readOnly && !isSaving}\n suppressContentEditableWarning\n tabIndex={readOnly ? undefined : 0}\n onFocus={handleFocus}\n onBlur={commit}\n onKeyDown={handleKeyDown}\n onInput={handleInput}\n onPaste={handlePaste}\n spellCheck={focused}\n className={cn(\n 'font-sans text-surface-fg outline-hidden',\n textClassName,\n // Idle: subtle hover hint\n !readOnly && !focused && 'cursor-text rounded-ds-sm -mx-ds-01 px-ds-01 hover:bg-surface-raised-hover transition-colors duration-fast-01',\n // Focused: subtle underline to indicate editing\n focused && 'rounded-ds-sm -mx-ds-01 px-ds-01 bg-surface-raised-hover ring-1 ring-accent-7',\n // Empty: show placeholder styling\n isEmpty && !focused && 'text-surface-fg-subtle italic',\n // Read-only\n readOnly && 'cursor-default',\n )}\n data-placeholder={placeholder}\n >\n {value || (focused ? '' : placeholder)}\n </span>\n {!readOnly && !focused && !isSaving && (\n <Icon icon={IconPencil} size=\"xs\" className=\"text-surface-fg-subtle opacity-0 group-hover:opacity-100 transition-opacity duration-fast-01 shrink-0\" />\n )}\n {isSaving && <Spinner size=\"sm\" />}\n </div>\n )\n})\nInlineEdit.displayName = 'InlineEdit'\n\nexport { InlineEdit }\n"],"mappings":";;;;;;;;AAiCA,IAAM,IAAa,EAAM,YAA6C,EACpE,UACA,WACA,iBAAc,iBACd,kBACA,cAAW,IACX,cACA,QAAQ,IAAa,IACrB,
|
|
1
|
+
{"version":3,"file":"inline-edit.js","names":[],"sources":["../../src/composed/inline-edit.tsx"],"sourcesContent":["'use client'\n\nimport { IconPencil } from '@tabler/icons-react'\nimport * as React from 'react'\n\nimport { Icon } from '../ui/icon'\nimport { cn } from '../ui/lib/utils'\nimport { Spinner } from '../ui/spinner'\n\n// ============================================================\n// Types\n// ============================================================\n\nexport interface InlineEditProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSave'> {\n value: string\n onSave: (newValue: string) => void | Promise<void>\n placeholder?: string\n /** CSS class applied to the editable text (e.g. 'text-ds-lg font-semibold') */\n textClassName?: string\n readOnly?: boolean\n maxLength?: number\n /** External saving state — shows spinner */\n saving?: boolean\n}\n\n// ============================================================\n// InlineEdit — contentEditable approach\n//\n// No mode switch. No input field. The text IS the editor.\n// Click → cursor appears in the text. Type. Enter saves. Escape reverts.\n// Like Notion, Linear, Figma layer names.\n// ============================================================\n\nconst InlineEdit = React.forwardRef<HTMLDivElement, InlineEditProps>(({\n value,\n onSave,\n placeholder = 'Click to edit',\n textClassName,\n readOnly = false,\n maxLength,\n saving: savingProp = false,\n className,\n // aria-label / aria-labelledby apply to the editable textbox (the inner\n // span), not the outer wrapper div. Intercept them from ...props so they\n // land on the role=\"textbox\" element that actually needs the name.\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n ...props\n}, ref) => {\n const [saving, setSaving] = React.useState(false)\n const [focused, setFocused] = React.useState(false)\n const editRef = React.useRef<HTMLSpanElement>(null)\n const snapshotRef = React.useRef(value)\n\n const isSaving = savingProp || saving\n const isEmpty = !value\n\n // Sync DOM text when value changes externally (and not focused)\n React.useEffect(() => {\n if (!focused && editRef.current) {\n editRef.current.textContent = value\n }\n }, [value, focused])\n\n function handleFocus() {\n if (readOnly || isSaving) return\n setFocused(true)\n snapshotRef.current = value\n\n // Select all text on focus (like clicking a file name in Finder)\n requestAnimationFrame(() => {\n if (!editRef.current) return\n const range = document.createRange()\n range.selectNodeContents(editRef.current)\n const sel = window.getSelection()\n sel?.removeAllRanges()\n sel?.addRange(range)\n })\n }\n\n async function commit() {\n if (!editRef.current) return\n setFocused(false)\n const raw = editRef.current.textContent ?? ''\n const trimmed = raw.trim()\n\n // Enforce maxLength\n const final = maxLength ? trimmed.slice(0, maxLength) : trimmed\n\n // Reset DOM to clean value\n editRef.current.textContent = final || value\n\n if (final === value) return\n const result = onSave(final)\n if (result instanceof Promise) {\n setSaving(true)\n try {\n await result\n } catch {\n // Revert on error\n if (editRef.current) editRef.current.textContent = value\n } finally {\n setSaving(false)\n }\n }\n }\n\n function cancel() {\n if (!editRef.current) return\n editRef.current.textContent = snapshotRef.current\n setFocused(false)\n editRef.current.blur()\n }\n\n function handleKeyDown(e: React.KeyboardEvent) {\n if (e.key === 'Escape') {\n e.preventDefault()\n cancel()\n } else if (e.key === 'Enter') {\n e.preventDefault()\n commit()\n }\n }\n\n function handleInput() {\n if (!editRef.current || !maxLength) return\n const text = editRef.current.textContent ?? ''\n if (text.length > maxLength) {\n editRef.current.textContent = text.slice(0, maxLength)\n // Move cursor to end\n const range = document.createRange()\n range.selectNodeContents(editRef.current)\n range.collapse(false)\n const sel = window.getSelection()\n sel?.removeAllRanges()\n sel?.addRange(range)\n }\n }\n\n // Prevent paste from inserting rich content\n function handlePaste(e: React.ClipboardEvent) {\n e.preventDefault()\n const text = e.clipboardData.getData('text/plain')\n document.execCommand('insertText', false, text)\n }\n\n return (\n <div ref={ref} className={cn('group inline-flex items-center gap-ds-02', className)} {...props}>\n <span\n ref={editRef}\n role={readOnly ? undefined : 'textbox'}\n contentEditable={!readOnly && !isSaving}\n suppressContentEditableWarning\n tabIndex={readOnly ? undefined : 0}\n // Fall back to the placeholder when no explicit label is supplied —\n // screen readers need _something_ on role=\"textbox\".\n aria-label={readOnly ? undefined : (ariaLabel ?? (ariaLabelledBy ? undefined : placeholder))}\n aria-labelledby={readOnly ? undefined : ariaLabelledBy}\n onFocus={handleFocus}\n onBlur={commit}\n onKeyDown={handleKeyDown}\n onInput={handleInput}\n onPaste={handlePaste}\n spellCheck={focused}\n className={cn(\n 'font-sans text-surface-fg outline-hidden',\n textClassName,\n // Idle: subtle hover hint\n !readOnly && !focused && 'cursor-text rounded-ds-sm -mx-ds-01 px-ds-01 hover:bg-surface-raised-hover transition-colors duration-fast-01',\n // Focused: subtle underline to indicate editing\n focused && 'rounded-ds-sm -mx-ds-01 px-ds-01 bg-surface-raised-hover ring-1 ring-accent-7',\n // Empty: show placeholder styling\n isEmpty && !focused && 'text-surface-fg-subtle italic',\n // Read-only\n readOnly && 'cursor-default',\n )}\n data-placeholder={placeholder}\n >\n {value || (focused ? '' : placeholder)}\n </span>\n {!readOnly && !focused && !isSaving && (\n <Icon icon={IconPencil} size=\"xs\" className=\"text-surface-fg-subtle opacity-0 group-hover:opacity-100 transition-opacity duration-fast-01 shrink-0\" />\n )}\n {isSaving && <Spinner size=\"sm\" />}\n </div>\n )\n})\nInlineEdit.displayName = 'InlineEdit'\n\nexport { InlineEdit }\n"],"mappings":";;;;;;;;AAiCA,IAAM,IAAa,EAAM,YAA6C,EACpE,UACA,WACA,iBAAc,iBACd,kBACA,cAAW,IACX,cACA,QAAQ,IAAa,IACrB,cAIA,cAAc,GACd,mBAAmB,GACnB,GAAG,KACF,MAAQ;CACT,IAAM,CAAC,GAAQ,KAAa,EAAM,SAAS,GAAM,EAC3C,CAAC,GAAS,KAAc,EAAM,SAAS,GAAM,EAC7C,IAAU,EAAM,OAAwB,KAAK,EAC7C,IAAc,EAAM,OAAO,EAAM,EAEjC,IAAW,KAAc,GACzB,IAAU,CAAC;AAGjB,GAAM,gBAAgB;AACpB,EAAI,CAAC,KAAW,EAAQ,YACtB,EAAQ,QAAQ,cAAc;IAE/B,CAAC,GAAO,EAAQ,CAAC;CAEpB,SAAS,IAAc;AACjB,OAAY,MAChB,EAAW,GAAK,EAChB,EAAY,UAAU,GAGtB,4BAA4B;AAC1B,OAAI,CAAC,EAAQ,QAAS;GACtB,IAAM,IAAQ,SAAS,aAAa;AACpC,KAAM,mBAAmB,EAAQ,QAAQ;GACzC,IAAM,IAAM,OAAO,cAAc;AAEjC,GADA,GAAK,iBAAiB,EACtB,GAAK,SAAS,EAAM;IACpB;;CAGJ,eAAe,IAAS;AACtB,MAAI,CAAC,EAAQ,QAAS;AACtB,IAAW,GAAM;EAEjB,IAAM,KADM,EAAQ,QAAQ,eAAe,IACvB,MAAM,EAGpB,IAAQ,IAAY,EAAQ,MAAM,GAAG,EAAU,GAAG;AAKxD,MAFA,EAAQ,QAAQ,cAAc,KAAS,GAEnC,MAAU,EAAO;EACrB,IAAM,IAAS,EAAO,EAAM;AAC5B,MAAI,aAAkB,SAAS;AAC7B,KAAU,GAAK;AACf,OAAI;AACF,UAAM;WACA;AAEN,IAAI,EAAQ,YAAS,EAAQ,QAAQ,cAAc;aAC3C;AACR,MAAU,GAAM;;;;CAKtB,SAAS,IAAS;AACX,IAAQ,YACb,EAAQ,QAAQ,cAAc,EAAY,SAC1C,EAAW,GAAM,EACjB,EAAQ,QAAQ,MAAM;;CAGxB,SAAS,EAAc,GAAwB;AAC7C,EAAI,EAAE,QAAQ,YACZ,EAAE,gBAAgB,EAClB,GAAQ,IACC,EAAE,QAAQ,YACnB,EAAE,gBAAgB,EAClB,GAAQ;;CAIZ,SAAS,IAAc;AACrB,MAAI,CAAC,EAAQ,WAAW,CAAC,EAAW;EACpC,IAAM,IAAO,EAAQ,QAAQ,eAAe;AAC5C,MAAI,EAAK,SAAS,GAAW;AAC3B,KAAQ,QAAQ,cAAc,EAAK,MAAM,GAAG,EAAU;GAEtD,IAAM,IAAQ,SAAS,aAAa;AAEpC,GADA,EAAM,mBAAmB,EAAQ,QAAQ,EACzC,EAAM,SAAS,GAAM;GACrB,IAAM,IAAM,OAAO,cAAc;AAEjC,GADA,GAAK,iBAAiB,EACtB,GAAK,SAAS,EAAM;;;CAKxB,SAAS,EAAY,GAAyB;AAC5C,IAAE,gBAAgB;EAClB,IAAM,IAAO,EAAE,cAAc,QAAQ,aAAa;AAClD,WAAS,YAAY,cAAc,IAAO,EAAK;;AAGjD,QACE,kBAAC,OAAD;EAAU;EAAK,WAAW,EAAG,4CAA4C,EAAU;EAAE,GAAI;YAAzF;GACE,kBAAC,QAAD;IACE,KAAK;IACL,MAAM,IAAW,KAAA,IAAY;IAC7B,iBAAiB,CAAC,KAAY,CAAC;IAC/B,gCAAA;IACA,UAAU,IAAW,KAAA,IAAY;IAGjC,cAAY,IAAW,KAAA,IAAa,MAAc,IAAiB,KAAA,IAAY;IAC/E,mBAAiB,IAAW,KAAA,IAAY;IACxC,SAAS;IACT,QAAQ;IACR,WAAW;IACX,SAAS;IACT,SAAS;IACT,YAAY;IACZ,WAAW,EACT,4CACA,GAEA,CAAC,KAAY,CAAC,KAAW,iHAEzB,KAAW,iFAEX,KAAW,CAAC,KAAW,iCAEvB,KAAY,iBACb;IACD,oBAAkB;cAEjB,MAAU,IAAU,KAAK;IACrB,CAAA;GACN,CAAC,KAAY,CAAC,KAAW,CAAC,KACzB,kBAAC,GAAD;IAAM,MAAM;IAAY,MAAK;IAAK,WAAU;IAA0G,CAAA;GAEvJ,KAAY,kBAAC,GAAD,EAAS,MAAK,MAAO,CAAA;GAC9B;;EAER;AACF,EAAW,cAAc"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Shared conformance test suite for design-system components.
|
|
5
|
+
*
|
|
6
|
+
* Runs the orthogonal checks that every component must satisfy (ref forwarding,
|
|
7
|
+
* className merging, HTML attribute spread, axe audit, variant/size/color smoke
|
|
8
|
+
* checks). Replaces per-file repetition of the same tests.
|
|
9
|
+
*
|
|
10
|
+
* Modelled on Base UI's `describeConformance` and Mantine's
|
|
11
|
+
* `itSupportsSystemProps` — one helper call per component instead of N tests per
|
|
12
|
+
* prop per file.
|
|
13
|
+
*/
|
|
14
|
+
export interface ConformanceOptions {
|
|
15
|
+
/** Smoke-test that each variant renders without throwing. Pass the exact list from CVA. */
|
|
16
|
+
variants?: readonly string[];
|
|
17
|
+
/** Smoke-test that each size renders without throwing. */
|
|
18
|
+
sizes?: readonly string[];
|
|
19
|
+
/** Smoke-test that each color/intent renders without throwing. */
|
|
20
|
+
colors?: readonly string[];
|
|
21
|
+
/** Skip specific checks when they don't apply (e.g. `['ref']` for components that don't forwardRef). */
|
|
22
|
+
skip?: ReadonlyArray<'ref' | 'className' | 'attrs' | 'axe' | 'variants' | 'sizes' | 'colors'>;
|
|
23
|
+
}
|
|
24
|
+
type RenderFn = (props?: Record<string, unknown>) => React.ReactElement;
|
|
25
|
+
/**
|
|
26
|
+
* Registers a `describe('conformance')` block in the caller's test file.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* describeConformance('Button', (props) => <Button {...props}>hi</Button>, {
|
|
30
|
+
* variants: ['solid', 'soft', 'outline', 'ghost', 'link'],
|
|
31
|
+
* sizes: ['xs', 'sm', 'md', 'lg'],
|
|
32
|
+
* colors: ['accent', 'error', 'success', 'warning', 'neutral'],
|
|
33
|
+
* })
|
|
34
|
+
*/
|
|
35
|
+
export declare function describeConformance(name: string, renderComponent: RenderFn, options?: ConformanceOptions): void;
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=conformance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../../src/test-utils/conformance.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,kBAAkB;IACjC,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5B,0DAA0D;IAC1D,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACzB,kEAAkE;IAClE,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1B,wGAAwG;IACxG,IAAI,CAAC,EAAE,aAAa,CAAC,KAAK,GAAG,WAAW,GAAG,OAAO,GAAG,KAAK,GAAG,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAC,CAAA;CAC9F;AAED,KAAK,QAAQ,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,KAAK,CAAC,YAAY,CAAA;AAEvE;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,QAAQ,EACzB,OAAO,GAAE,kBAAuB,QAoEjC"}
|
|
@@ -35,6 +35,12 @@
|
|
|
35
35
|
## Exported Utilities
|
|
36
36
|
groupItemsByTime(items: ActivityItem[], labels?: GroupLabels) — pure function that buckets items into time groups; returns { label: string, items: ActivityItem[] }[]
|
|
37
37
|
|
|
38
|
+
## Composability
|
|
39
|
+
- **Built from ui primitives:** Avatar (actor), Button (Load more), Skeleton (loading), Text (body). Override via `renderItem` to use your own primitives per row.
|
|
40
|
+
- **renderItem** is the composition hook — return your own JSX or `undefined` to fall back to default ActivityEntry. Timeline dot + layout wrapper stay consistent.
|
|
41
|
+
- **groupBy="time"** wraps items in Today/Yesterday/This Week/Older buckets; `groupItemsByTime()` export reusable for custom renderers.
|
|
42
|
+
- **Pagination is consumer-driven** — `hasMore` + `onLoadMore` for server-side, or `maxInitialItems` + "Show all" toggle for client-side truncation.
|
|
43
|
+
|
|
38
44
|
## Gotchas
|
|
39
45
|
- `items` is required — passing an empty array renders the `emptyState` content
|
|
40
46
|
- `color` on each item controls the timeline dot color
|
|
@@ -38,6 +38,14 @@
|
|
|
38
38
|
/>
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
## Composability
|
|
42
|
+
- **Built on ui/Avatar** — each slot is an Avatar with overlap + border + optional Tooltip.
|
|
43
|
+
- **Wraps TooltipProvider internally** — don't add another.
|
|
44
|
+
- **renderAvatar** escape hatch for custom shapes. Wrapper handles overlap/border positioning; your renderer sets size + shape.
|
|
45
|
+
- **borderColor** must match the surface the group sits on (`surface-raised` on Card, `surface-base` on page bg). Mismatch = visible seam.
|
|
46
|
+
- **Overflow:** +N badge with optional `onOverflowClick` → pair with a Popover/Sheet for "show all members".
|
|
47
|
+
- Per-user `ring` (lead/admin/client) surfaces Avatar's ring semantic at group level — use for role/presence hints.
|
|
48
|
+
|
|
41
49
|
## Gotchas
|
|
42
50
|
- Wraps TooltipProvider internally — no need to add one yourself
|
|
43
51
|
- Users beyond `max` are collapsed into a "+N" overflow badge
|
|
@@ -34,6 +34,12 @@
|
|
|
34
34
|
/>
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
## Composability
|
|
38
|
+
- **Standalone floating toolbar** — use with DataTable, TreeView, or any selection-capable UI.
|
|
39
|
+
- **DataTable auto-integration:** DataTable's `bulkActions` prop renders this internally — typically you don't render BulkActionBar directly when using DataTable.
|
|
40
|
+
- **Data-driven actions:** `{ label, icon, onClick, color, disabled }[]`. For destructive confirmation, call ConfirmDialog from the onClick handler.
|
|
41
|
+
- **Portal to body + fixed bottom-center z-50** — independent of parent layout. Check for other fixed elements that might overlap.
|
|
42
|
+
|
|
37
43
|
## Gotchas
|
|
38
44
|
- Renders via `createPortal` into `document.body` — will not appear during SSR (mounts only client-side)
|
|
39
45
|
- Positioned fixed at bottom-center with `z-50`; ensure no other fixed elements conflict
|
|
@@ -25,6 +25,13 @@ CommandItem shape: { id, label, description?, icon?, shortcut?, onSelect: () =>
|
|
|
25
25
|
/>
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
## Composability
|
|
29
|
+
- **Built on Dialog (portal) + cmdk-style fuzzy matching.** Global Ctrl+K / Cmd+K by default; customize via `keybinding`.
|
|
30
|
+
- **Groups + items are data-driven.** Each item: `{ id, label, icon?, shortcut?, description?, onSelect }`. Labels accept ReactNode for rich rendering; pair with `filterValue` for typeahead matching against a text representation.
|
|
31
|
+
- **renderLabel hook** lets you highlight matched characters in query (`(query) => ReactNode`).
|
|
32
|
+
- **Controlled / uncontrolled** via `open`+`onOpenChange` or `defaultOpen`.
|
|
33
|
+
- **AppCommandPalette (shell)** is the app-wide variant with CommandRegistry — use that for global palettes with dynamically registered commands.
|
|
34
|
+
|
|
28
35
|
## Gotchas
|
|
29
36
|
- Opens with Ctrl+K / Cmd+K by default
|
|
30
37
|
- Items animate in with staggered slide-up (30ms delay cascade); groups fade in; active item icon/shortcut highlight in interactive color (v0.15.0)
|
|
@@ -33,6 +33,12 @@ const [open, setOpen] = useState(false)
|
|
|
33
33
|
/>
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
## Composability
|
|
37
|
+
- **Built on AlertDialog** — non-dismissible (no click-outside, Cancel-first focus), portal-rendered.
|
|
38
|
+
- **Controlled-only.** Parent owns `open` + `onOpenChange`.
|
|
39
|
+
- **onConfirm can be async.** `loading=true` replaces confirm text with "Processing..." and disables both buttons. Typical: setLoading(true) → await action() → setLoading(false) + setOpen(false).
|
|
40
|
+
- **Stays open after confirm** — close via onOpenChange. Keeps the modal up on errors or for chained confirmations.
|
|
41
|
+
|
|
36
42
|
## Gotchas
|
|
37
43
|
- Dialog stays open after confirm — consumer must close it via `onOpenChange`
|
|
38
44
|
- Built on AlertDialog internally
|
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
</ContentCard>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
## Composability
|
|
27
|
+
- **Server-safe higher-level Card** — wraps ui/Card with conventional header/title/actions/footer slots.
|
|
28
|
+
- **When to use:** List rows, dashboard tiles, any "card with title + optional actions + body + optional footer" pattern. Use raw ui/Card for custom layouts or when you need accent bars.
|
|
29
|
+
- **Slot precedence:** `header` beats `headerTitle`. Use `headerTitle` for simple strings, `header` for rich content.
|
|
30
|
+
- **padding variants** (default/compact/spacious/none) set once on ContentCard; the underlying Card size cascade propagates.
|
|
31
|
+
|
|
26
32
|
## Gotchas
|
|
27
33
|
- Server-safe: can be imported directly in Next.js Server Components
|
|
28
34
|
- Use `headerTitle` for simple text headers; use `header` for custom header content
|
|
@@ -111,6 +111,16 @@ PresetKey: 'today' | 'yesterday' | 'last7days' | 'last30days' | 'thisMonth' | 'l
|
|
|
111
111
|
<TimePicker value={time} onChange={setTime} format="24h" minuteStep={15} />
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
## Composability
|
|
115
|
+
- **7 sub-components in one kit:** DatePicker, DateRangePicker, DateTimePicker, TimePicker, CalendarGrid, MonthPicker, YearPicker, Presets. Built on Popover (trigger + calendar content) + date-fns.
|
|
116
|
+
- **Layered abstractions:**
|
|
117
|
+
- High-level: DatePicker / DateRangePicker / DateTimePicker / TimePicker (use these 95% of the time — Popover-wrapped, single-field pickers with formatted value display).
|
|
118
|
+
- Low-level: CalendarGrid / MonthPicker / YearPicker (use for custom picker layouts, inline calendars, dashboards with embedded date selection).
|
|
119
|
+
- **useCalendar hook** manages month navigation state — use it when building custom picker UI (e.g. inline calendar with custom header).
|
|
120
|
+
- **Presets component** is standalone — drop it next to a DateRangePicker's calendar for quick-select sidebars (Today / Last 7 days / This month / etc.).
|
|
121
|
+
- **Events on calendar:** CalendarGrid accepts `events: { date, color, label }[]` — renders up to 3 colored dots per day cell. Useful for availability / booked-days highlights.
|
|
122
|
+
- **min/max/disabledDates** are shared contract across the whole family — pass the same bounds config to any date picker variant.
|
|
123
|
+
|
|
114
124
|
## Gotchas
|
|
115
125
|
- TimePicker stores time inside a Date object — only hours/minutes/seconds are meaningful
|
|
116
126
|
- CalendarGrid is a low-level building block — prefer DatePicker/DateRangePicker for most use cases
|
|
@@ -21,6 +21,13 @@
|
|
|
21
21
|
<DeadlineIndicator deadline={task.dueDate} warningThreshold={2880} criticalThreshold={480} />
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
## Composability
|
|
25
|
+
- **Server-safe inline status** — renders a colored text label ("2d left" / "3h left" / "Overdue by 1d") with optional clock icon prefix.
|
|
26
|
+
- **Threshold-driven color:** `warningThreshold` + `criticalThreshold` drive the green→yellow→red progression. Tune per use case (billing deadlines vs. task due dates have different urgency cadences).
|
|
27
|
+
- **Composes inside Card, StatusBadge, DataTable cells** — anywhere a short inline deadline string fits.
|
|
28
|
+
- **Doesn't live-update** — uses `Date.now()` at render time. For ticking timestamps, re-render via a parent interval or use a dedicated "time ago" library.
|
|
29
|
+
- For static absolute timestamps (not relative), set `format="absolute"` — useful when you want the exact date rendered with semantic color coding.
|
|
30
|
+
|
|
24
31
|
## Gotchas
|
|
25
32
|
- Color is semantic: green (on-track) -> yellow (warning threshold) -> red (critical/overdue)
|
|
26
33
|
- Overdue deadlines show bold red text with "Overdue by Xd/h/m"
|
|
@@ -37,6 +37,14 @@ EmojiPicker, EmojiPickerPopover
|
|
|
37
37
|
<EmojiPicker onSelect={handleEmoji} theme="dark" />
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
## Composability
|
|
41
|
+
- **Two exports:** `EmojiPicker` (inline grid, no trigger) and `EmojiPickerPopover` (trigger + popover wrapper). Use EmojiPickerPopover 95% of the time — trigger-on-click is the standard UX.
|
|
42
|
+
- **Wraps @emoji-mart/react**, lazy-loaded with a Skeleton placeholder while the ~200KB bundle fetches. Don't pre-import unless you need it eagerly.
|
|
43
|
+
- **Trigger composition:** EmojiPickerPopover's `children` is the trigger — wrap any Button/IconButton. Typical pairing is an icon-only IconButton with a 😀 label.
|
|
44
|
+
- **TipTap integration:** Use `createEmojiSuggestion(set?)` factory to create a TipTap suggestion plugin that opens the picker on typing `:emoji`. Works with RichChatInput and RichTextEditor.
|
|
45
|
+
- **Theme matching:** `theme="auto"` reads the `.dark` class on `<html>` — matches the DS dark mode toggle automatically. Override with explicit light/dark.
|
|
46
|
+
- **Emoji sets:** Pass `set="apple" | "google" | ...` for consistent cross-platform emoji art (defaults to native OS glyphs).
|
|
47
|
+
|
|
40
48
|
## Gotchas
|
|
41
49
|
- Wraps `@emoji-mart/react` which is lazy-loaded — shows a Skeleton placeholder while loading
|
|
42
50
|
- `theme="auto"` reads the `.dark` class on `<html>` to pick light/dark
|
|
@@ -27,6 +27,14 @@ Note: EmptyState was server-safe prior to v0.18.0 but is NO LONGER server-safe d
|
|
|
27
27
|
/>
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
## Composability
|
|
31
|
+
- **Centered block for "no data" / "no results" states.** Renders icon + title + description + optional action slot.
|
|
32
|
+
- **Icon composition:** Accepts both JSX (`<IconFolder />`) and component references (`IconFolder`). Component refs auto-instantiate with correct sizing. Default icon is the Devalok chakra.
|
|
33
|
+
- **DataTable integration:** Pass an EmptyState component to DataTable's `emptyState` prop; takes precedence over `noResultsText` string.
|
|
34
|
+
- **compact mode** for embedding inside Cards, sidebars, narrow sections. Defaults iconSize to 'sm' in compact mode automatically.
|
|
35
|
+
- **Action slot** typically holds a single primary Button ("Create your first task", "Invite members"). For multi-action, use a div + flex with gap.
|
|
36
|
+
- No longer server-safe (Framer Motion entrance animation).
|
|
37
|
+
|
|
30
38
|
## Gotchas
|
|
31
39
|
- `icon` accepts both JSX elements (`<IconFolder />`) and component references (`IconFolder`). Component references are auto-instantiated with correct sizing classes.
|
|
32
40
|
- `iconSize` controls icon dimensions regardless of icon type. When `compact=true` and no `iconSize`, defaults to `'sm'`.
|
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
<ErrorDisplay error={error} onReset={() => refetch()} />
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## Composability
|
|
20
|
+
- **ErrorDisplay, not ErrorBoundary** — confusingly, the import path is `error-boundary` but the component is `ErrorDisplay`. It renders an error UI; it does NOT catch errors. Pair it with your own ErrorBoundary (from react-error-boundary, Next.js error.tsx, etc.) as the fallback UI.
|
|
21
|
+
- **Auto-detects HTTP status codes** (404, 403, 500) when the `error` object has a `status` field — shows appropriate icon + message.
|
|
22
|
+
- **Dev-only stack trace:** Renders the stack trace only when `process.env.NODE_ENV !== 'production'`. Production users see a clean error page.
|
|
23
|
+
- **onReset for retry:** Pass a callback that re-fetches / resets state. Commonly wired to a react-query `refetch` or router `replace(...)`.
|
|
24
|
+
- Composes with EmptyState for "no data" states (which aren't really errors) — use EmptyState for empty, ErrorDisplay for failed.
|
|
25
|
+
|
|
19
26
|
## Gotchas
|
|
20
27
|
- Auto-detects HTTP status codes (404, 403, 500) and shows appropriate icon/message
|
|
21
28
|
- Shows stack trace in development mode only
|
|
@@ -32,6 +32,14 @@
|
|
|
32
32
|
<FilePreview url="https://youtube.com/watch?v=..." />
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
## Composability
|
|
36
|
+
- **Type-auto-detection** from URL + mimeType routes to the right renderer (image / pdf / video / audio / embed). For ambiguous URLs, pass `type` explicitly.
|
|
37
|
+
- **Heavy deps are lazy-loaded** — react-pdf (PDF), react-zoom-pan-pinch (Image zoom) are only fetched when first needed. Skeleton placeholder while loading. Don't pre-import.
|
|
38
|
+
- **Composes inside Dialog/Sheet** — common pattern: thumbnail in a list → click to open a Dialog with `<FilePreview>` filling it.
|
|
39
|
+
- **Embed URL normalization:** YouTube/Vimeo/Figma/Loom URLs auto-convert to embed format. Pass the original share URL; FilePreview handles it.
|
|
40
|
+
- **PDF worker from unpkg CDN** — for offline apps, override `pdfjs.GlobalWorkerOptions.workerSrc` in your app entry.
|
|
41
|
+
- **Download + error fallback** built in — if the file fails to load, users still get a download link.
|
|
42
|
+
|
|
35
43
|
## Gotchas
|
|
36
44
|
- Image/PDF lazy-loaded (Skeleton on first render)
|
|
37
45
|
- PDF worker from unpkg CDN — configure workerSrc for offline apps
|
|
@@ -51,6 +51,14 @@ FilterBar, FilterSelect, FilterMultiSelect
|
|
|
51
51
|
</FilterBar>
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
## Composability
|
|
55
|
+
- **FilterBar + FilterSelect + FilterMultiSelect** — three-part kit. FilterBar is the toolbar container; FilterSelect/FilterMultiSelect are the individual filter controls.
|
|
56
|
+
- **size propagates via context** from FilterBar to every FilterSelect/FilterMultiSelect child. Don't set size on individual filters.
|
|
57
|
+
- **Children MUST be direct** — the size cascade breaks if filters are wrapped in extra divs. Use React fragments or let them be direct children.
|
|
58
|
+
- **Active filter highlight:** FilterSelect/FilterMultiSelect auto-show an accent border when their value is set (non-empty array for multi, non-"all" for single).
|
|
59
|
+
- **Pair with DataTable or any list:** FilterBar sits above a DataTable/list; `searchValue` + `onSearchChange` drive global filter; individual filters drive column filters via your own state management.
|
|
60
|
+
- **"Clear all" convention:** Passing `onClearAll` renders a Reset button that's your escape hatch — implement it to clear all filter state in one call.
|
|
61
|
+
|
|
54
62
|
## Gotchas
|
|
55
63
|
- FilterSelect and FilterMultiSelect must be direct children of FilterBar to inherit the size context
|
|
56
64
|
- FilterSelect uses `"all"` as the sentinel value for "no filter" — do not use `"all"` as a real option value
|
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
</FormSection>
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
## Composability
|
|
30
|
+
- **Visual grouping of related FormFields.** Wraps its children with a title + description + horizontal rule separator. No state, no context.
|
|
31
|
+
- **collapsible mode** wraps content in a Collapsible — useful for "Advanced settings" or optional form sections. `defaultOpen={false}` for initially-closed.
|
|
32
|
+
- **Pairs with FormField:** Each form control inside should be a FormField for consistent label + helper text + validation. FormSection doesn't auto-wrap; you still structure each field yourself.
|
|
33
|
+
- **Not a page-level header** — for a full form's main heading use PageHeader. FormSection is mid-form, between field groups.
|
|
34
|
+
|
|
29
35
|
## Gotchas
|
|
30
36
|
- `defaultOpen` only applies when `collapsible={true}` — otherwise the section is always open
|
|
31
37
|
- Renders a horizontal rule between the header and content automatically
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
<GlobalLoading isLoading={isNavigating} />
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## Composability
|
|
19
|
+
- **Thin top-of-viewport progress bar** — for route-level navigation indicators (NProgress-style). Fixed at the top; z-toast layer.
|
|
20
|
+
- **Wire to router events:** Pair with Next.js `useRouter` + navigation events, or react-router's `useNavigation`, or a custom global loading state in Redux/Zustand. Set `isLoading={true}` during transit, `false` when landed.
|
|
21
|
+
- **Not for in-page loading** — use Spinner, Skeleton, or LoadingSkeleton for component-level loading states. GlobalLoading is strictly for cross-route / full-page transitions.
|
|
22
|
+
- **Auto-unmounts when isLoading=false** — no need to manage visibility via classes or conditional rendering.
|
|
23
|
+
|
|
18
24
|
## Gotchas
|
|
19
25
|
- Fixed-position bar at top of viewport (z-toast layer)
|
|
20
26
|
- Renders nothing when `isLoading` is false
|
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
/>
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
## Composability
|
|
29
|
+
- **contentEditable-based** — the text IS the editor (Notion / Linear / Figma layer-name pattern). No separate input field appears.
|
|
30
|
+
- **Keyboard contract:** Click to focus (auto-selects text, like Finder rename). Type to edit. Enter saves, Escape reverts. Paste strips rich content.
|
|
31
|
+
- **Async save:** `onSave` can return a Promise — InlineEdit shows a spinner and disables editing while pending. On rejection, text reverts to the original value automatically.
|
|
32
|
+
- **Accessibility:** Accepts `aria-label` and `aria-labelledby` (forwarded to the role="textbox" span) — required when the text isn't self-descriptive. Falls back to `placeholder` as label when neither is provided.
|
|
33
|
+
- **Not inside FormField** — InlineEdit is for in-place editing of existing content (task title, project name); use regular FormField + Input for traditional forms.
|
|
34
|
+
- **textClassName for typography control:** Pass `"text-ds-lg font-semibold"` to make it look like a heading without changing the underlying element.
|
|
35
|
+
|
|
28
36
|
## Gotchas
|
|
29
37
|
- Uses contentEditable — the text IS the editor. No input field appears.
|
|
30
38
|
- Click to focus → cursor appears in text. Type to edit. Enter saves. Escape reverts.
|
|
@@ -39,6 +39,13 @@ Exports: CardSkeleton, TableSkeleton, BoardSkeleton, ListSkeleton
|
|
|
39
39
|
<ListSkeleton rows={10} showAvatar={false} />
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
## Composability
|
|
43
|
+
- **Pre-composed skeleton layouts** — CardSkeleton, TableSkeleton, BoardSkeleton, ListSkeleton. Each mimics the shape of a common DS layout so users see meaningful loading placeholders.
|
|
44
|
+
- **Built on ui/Skeleton** — for custom loading layouts, use Skeleton directly (rectangle/circle/text variants). These composed versions are just opinionated shape combinations.
|
|
45
|
+
- **Server-safe** — use during SSR for initial-paint skeletons in Next.js app router (while server data streams in).
|
|
46
|
+
- **Pairs with conditional rendering:** `{isLoading ? <TableSkeleton rows={8} /> : <DataTable data={rows} />}`.
|
|
47
|
+
- **PageSkeletons (separate file)** provides full-page placeholders (DashboardSkeleton, ProjectListSkeleton, TaskDetailSkeleton) — use those for route-level loading states.
|
|
48
|
+
|
|
42
49
|
## Gotchas
|
|
43
50
|
- Server-safe: can be imported directly in Next.js Server Components
|
|
44
51
|
- These are pre-composed skeleton layouts — for individual skeleton shapes, use the `Skeleton` UI component
|
|
@@ -20,6 +20,15 @@
|
|
|
20
20
|
<MarkdownViewer content={trustedHtml} allowHtml />
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
## Composability
|
|
24
|
+
- **Read-only markdown renderer.** For editing, use RichTextEditor (TipTap-based). MarkdownViewer is strictly for display.
|
|
25
|
+
- **Built on react-markdown + remark-gfm** — GFM tables, strikethrough, task lists supported out of the box.
|
|
26
|
+
- **Syntax highlighting is lazy:** Code blocks with a language fence (```ts, ```python) lazy-load react-syntax-highlighter. First render shows a plain `<pre>` fallback. Don't pre-import.
|
|
27
|
+
- **Security posture:** Raw HTML is stripped by default — `allowHtml={true}` must be explicit, and ONLY for trusted content (XSS vector otherwise).
|
|
28
|
+
- **Links open external by default** (`target="_blank"` + `rel="noopener noreferrer"`). Override via `linkTarget`.
|
|
29
|
+
- **compact mode** for inline use (comments, message bubbles). Default spacing is for article-body content.
|
|
30
|
+
- **Pairs with Chat's Message.Body** — render markdown from user messages safely. Always keep `allowHtml={false}` for user-generated content.
|
|
31
|
+
|
|
23
32
|
## Gotchas
|
|
24
33
|
- Code blocks with a language fence are syntax-highlighted via `react-syntax-highlighter` (lazy-loaded) — the first render shows a plain `<pre>` fallback
|
|
25
34
|
- GFM (tables, strikethrough, task lists) is supported via `remark-gfm`
|
|
@@ -42,6 +42,13 @@ MasterDetail (root), MasterDetail.List, MasterDetail.Detail, MasterDetail.ListIt
|
|
|
42
42
|
</MasterDetail>
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
## Composability
|
|
46
|
+
- **Responsive list+detail layout.** Desktop: side-by-side panels. Mobile (below `breakpoint`): stacked, mutually exclusive (list OR detail, controlled by `selected`).
|
|
47
|
+
- **Compound structure:** `MasterDetail.List` contains `MasterDetail.ListItem[]` (interactive). `MasterDetail.Detail` holds the currently-selected view.
|
|
48
|
+
- **onBack is required for mobile** — renders the back button in Detail pane. Omitting it leaves users stranded once they drill into an item on mobile.
|
|
49
|
+
- **SSR gotcha:** Uses `window.matchMedia` — initial SSR render picks desktop mode; hydrates to mobile mode if viewport is narrow. If that causes layout shift, consider rendering this only after mount (via `useState(false)` + `useEffect`).
|
|
50
|
+
- **Pairs with EmptyState** — render EmptyState inside Detail when `selected === null` on desktop ("Pick an item to get started").
|
|
51
|
+
|
|
45
52
|
## Gotchas
|
|
46
53
|
- On mobile (below breakpoint), List and Detail are mutually exclusive — selecting an item hides the list
|
|
47
54
|
- The `onBack` callback is required for the mobile back button to appear in the Detail pane
|
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
</MemberPicker>
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Composability
|
|
26
|
+
- **Trigger + Popover + search list.** Pass any button/element as `children` to act as the trigger. Typical pairing: outline Button that says "Assign" or an IconButton with avatar overflow.
|
|
27
|
+
- **Single vs multi:** `multiple={true}` allows multiple selections; `false` (default) replaces the previous selection. `selectedIds` is always an array in both modes — consumer controls semantics.
|
|
28
|
+
- **onSelect with a single ID:** Fires once per click with one memberId. Toggle logic in multi mode is consumer responsibility (derive the new array from the click).
|
|
29
|
+
- **Avatar display:** Uses ui/Avatar internally — supply `avatar` URL in the member object, fallback to initials from `name`.
|
|
30
|
+
- **For general-purpose multi-select** (non-members), use MultiSelectPopover which has the same popover+search pattern without member-specific avatar rendering.
|
|
31
|
+
|
|
25
32
|
## Gotchas
|
|
26
33
|
- `children` is used as the trigger element (e.g., a Button)
|
|
27
34
|
- `onSelect` is called with a single `memberId` — toggle logic is up to the consumer
|
|
@@ -47,6 +47,17 @@
|
|
|
47
47
|
</MultiSelectPopover>
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## Composability
|
|
51
|
+
- **Generalized multi-select popover** — picks from a fixed list (items) or grouped list (groups), with search, async search, and custom rendering.
|
|
52
|
+
- **Items vs groups (mutually exclusive):** Pass `items` for flat lists, `groups` for sectioned lists. Don't pass both.
|
|
53
|
+
- **Async search via onSearch:** When provided, local filtering is disabled — the callback owns filtering and returns a new list. `searchDebounce` (default 300ms) throttles calls.
|
|
54
|
+
- **renderItem escape hatch:** Pass `(item, selected) => ReactNode` for custom item rendering (avatar + multi-line descriptions, etc.). Built-in default renders image + label + description.
|
|
55
|
+
- **maxSelections behavior:** At the limit, clicking a new item REPLACES the oldest selection (FIFO). `maxSelections={1}` effectively acts as single-select.
|
|
56
|
+
- **MultiSelectPopover vs Combobox vs MemberPicker:**
|
|
57
|
+
- Combobox = form-field multi-select (typeahead + selection in place)
|
|
58
|
+
- MultiSelectPopover = button-triggered popup for bulk selection (good for "Assign to" / "Add tags" scenarios)
|
|
59
|
+
- MemberPicker = MultiSelectPopover specialized for team-member UI
|
|
60
|
+
|
|
50
61
|
## Gotchas
|
|
51
62
|
- Supply either `items` (flat) or `groups` (sectioned), not both
|
|
52
63
|
- When `onSearch` is provided, local filtering is disabled — the callback must return results
|
|
@@ -28,6 +28,13 @@
|
|
|
28
28
|
/>
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
## Composability
|
|
32
|
+
- **Server-safe page-top header** with title + subtitle + breadcrumbs + actions slot. Renders Breadcrumb internally from the `breadcrumbs` array.
|
|
33
|
+
- **Auto-derived title:** If `title` is omitted, the last breadcrumb's `label` becomes the h1. Saves duplication when the page title matches the breadcrumb leaf.
|
|
34
|
+
- **Breadcrumb contract:** Last item should NOT have an `href` (it's the current page — renders as BreadcrumbPage, not a link). Items with `href` render as BreadcrumbLink.
|
|
35
|
+
- **actions slot:** Typically one to three buttons (Save, Cancel, More). For larger action groups, use ButtonGroup or a toolbar.
|
|
36
|
+
- **Not for tabs / sub-navigation** — pair with Tabs rendered below PageHeader for that.
|
|
37
|
+
|
|
31
38
|
## Gotchas
|
|
32
39
|
- Server-safe: can be imported directly in Next.js Server Components
|
|
33
40
|
- If `title` is omitted, the last breadcrumb's `label` is used as the page title
|
|
@@ -19,6 +19,16 @@ Exports: DashboardSkeleton, ProjectListSkeleton, TaskDetailSkeleton
|
|
|
19
19
|
<TaskDetailSkeleton />
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
## Composability
|
|
23
|
+
- **Full-page skeleton layouts** for route-level loading states. Each mimics a common page shape (dashboard tiles, project list with filters, task detail with sidebar).
|
|
24
|
+
- **Server-safe** — use in Next.js app router `loading.tsx` files for instant route-transition feedback while data streams.
|
|
25
|
+
- **Built on LoadingSkeleton + ui/Skeleton** — these just assemble the pre-built regional skeletons into page-shaped layouts.
|
|
26
|
+
- **When to use which skeleton tier:**
|
|
27
|
+
- `<Skeleton>` (ui) — single shape for a single element
|
|
28
|
+
- `<CardSkeleton>` / `<TableSkeleton>` (LoadingSkeleton) — individual region shape
|
|
29
|
+
- `<DashboardSkeleton>` / etc. (PageSkeletons) — full page placeholder
|
|
30
|
+
- **Fixed layout structure** — the className prop adjusts the outer container, but internal layout isn't customizable. For custom page skeletons, compose LoadingSkeleton pieces yourself.
|
|
31
|
+
|
|
22
32
|
## Gotchas
|
|
23
33
|
- Server-safe: can be imported directly in Next.js Server Components
|
|
24
34
|
- These are full-page skeleton layouts — for smaller skeleton sections, use LoadingSkeleton components
|
|
@@ -19,6 +19,13 @@ Priority = 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT' | 'low' | 'medium' | 'high' | 'u
|
|
|
19
19
|
<PriorityIndicator priority="low" display="compact" />
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
## Composability
|
|
23
|
+
- **Server-safe priority label** — icon + color + text for task / issue priority.
|
|
24
|
+
- **Composes inside list rows, DataTable cells, Card headers, task panels** — anywhere a priority flag fits.
|
|
25
|
+
- **display="compact"** shows only the icon (with priority text as title attribute for tooltip). Use in tight cells; use `display="full"` (default) in free space.
|
|
26
|
+
- **Case-insensitive priority** — accepts both UPPERCASE (LOW/MEDIUM/HIGH/URGENT) and lowercase. Designed to match both backend conventions without manual coercion.
|
|
27
|
+
- Color semantics: LOW=success, MEDIUM=warning, HIGH=error, URGENT=error with bolder icon.
|
|
28
|
+
|
|
22
29
|
## Gotchas
|
|
23
30
|
- Case-insensitive — "low" and "LOW" both work
|
|
24
31
|
- Server-safe: can be imported directly in Next.js Server Components
|