@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.
Files changed (127) hide show
  1. package/dist/composed/inline-edit.d.ts.map +1 -1
  2. package/dist/composed/inline-edit.js +42 -40
  3. package/dist/composed/inline-edit.js.map +1 -1
  4. package/dist/test-utils/conformance.d.ts +37 -0
  5. package/dist/test-utils/conformance.d.ts.map +1 -0
  6. package/docs/components/composed/activity-feed.md +6 -0
  7. package/docs/components/composed/avatar-group.md +8 -0
  8. package/docs/components/composed/bulk-action-bar.md +6 -0
  9. package/docs/components/composed/command-palette.md +7 -0
  10. package/docs/components/composed/confirm-dialog.md +6 -0
  11. package/docs/components/composed/content-card.md +6 -0
  12. package/docs/components/composed/date-picker.md +10 -0
  13. package/docs/components/composed/deadline-indicator.md +7 -0
  14. package/docs/components/composed/emoji-picker.md +8 -0
  15. package/docs/components/composed/empty-state.md +8 -0
  16. package/docs/components/composed/error-boundary.md +7 -0
  17. package/docs/components/composed/file-preview.md +8 -0
  18. package/docs/components/composed/filter-bar.md +8 -0
  19. package/docs/components/composed/form-section.md +6 -0
  20. package/docs/components/composed/global-loading.md +6 -0
  21. package/docs/components/composed/inline-edit.md +8 -0
  22. package/docs/components/composed/loading-skeleton.md +7 -0
  23. package/docs/components/composed/markdown-viewer.md +9 -0
  24. package/docs/components/composed/master-detail.md +7 -0
  25. package/docs/components/composed/member-picker.md +7 -0
  26. package/docs/components/composed/multi-select-popover.md +11 -0
  27. package/docs/components/composed/page-header.md +7 -0
  28. package/docs/components/composed/page-skeletons.md +10 -0
  29. package/docs/components/composed/priority-indicator.md +7 -0
  30. package/docs/components/composed/responsive-overlay.md +7 -0
  31. package/docs/components/composed/rich-chat-input.md +12 -0
  32. package/docs/components/composed/rich-text-editor.md +9 -0
  33. package/docs/components/composed/schedule-view.md +8 -0
  34. package/docs/components/composed/simple-tooltip.md +7 -0
  35. package/docs/components/composed/status-badge.md +10 -0
  36. package/docs/components/shell/app-command-palette.md +8 -0
  37. package/docs/components/shell/bottom-navbar.md +12 -0
  38. package/docs/components/shell/command-registry.md +7 -0
  39. package/docs/components/shell/link-context.md +15 -0
  40. package/docs/components/shell/notification-center.md +10 -0
  41. package/docs/components/shell/notification-preferences.md +7 -0
  42. package/docs/components/shell/sidebar.md +11 -0
  43. package/docs/components/shell/top-bar.md +11 -0
  44. package/docs/components/ui/accordion.md +10 -0
  45. package/docs/components/ui/alert-dialog.md +10 -1
  46. package/docs/components/ui/alert.md +10 -2
  47. package/docs/components/ui/aspect-ratio.md +7 -0
  48. package/docs/components/ui/autocomplete.md +7 -0
  49. package/docs/components/ui/avatar.md +10 -0
  50. package/docs/components/ui/badge-group.md +7 -0
  51. package/docs/components/ui/badge-indicator.md +7 -0
  52. package/docs/components/ui/badge.md +12 -0
  53. package/docs/components/ui/banner.md +7 -0
  54. package/docs/components/ui/breadcrumb.md +8 -0
  55. package/docs/components/ui/button-group.md +24 -4
  56. package/docs/components/ui/button-processing.md +5 -0
  57. package/docs/components/ui/button.md +9 -0
  58. package/docs/components/ui/card.md +16 -4
  59. package/docs/components/ui/charts.md +9 -0
  60. package/docs/components/ui/chat.md +9 -0
  61. package/docs/components/ui/checkbox.md +15 -1
  62. package/docs/components/ui/chip.md +9 -0
  63. package/docs/components/ui/code.md +7 -0
  64. package/docs/components/ui/collapsible.md +9 -0
  65. package/docs/components/ui/color-input.md +8 -0
  66. package/docs/components/ui/color-swatch.md +7 -0
  67. package/docs/components/ui/combobox.md +16 -3
  68. package/docs/components/ui/container.md +11 -0
  69. package/docs/components/ui/context-menu.md +8 -0
  70. package/docs/components/ui/data-table-body.md +4 -0
  71. package/docs/components/ui/data-table-bulk-actions.md +4 -0
  72. package/docs/components/ui/data-table-card.md +4 -0
  73. package/docs/components/ui/data-table-context.md +4 -0
  74. package/docs/components/ui/data-table-header.md +4 -0
  75. package/docs/components/ui/data-table-pagination.md +4 -0
  76. package/docs/components/ui/data-table-toolbar.md +7 -0
  77. package/docs/components/ui/data-table.md +25 -0
  78. package/docs/components/ui/devalok-grain.md +8 -0
  79. package/docs/components/ui/dialog.md +10 -0
  80. package/docs/components/ui/dropdown-menu.md +10 -0
  81. package/docs/components/ui/file-upload.md +8 -0
  82. package/docs/components/ui/form.md +21 -0
  83. package/docs/components/ui/hover-card.md +9 -0
  84. package/docs/components/ui/icon-button.md +7 -0
  85. package/docs/components/ui/icon-context.md +7 -0
  86. package/docs/components/ui/icon-group.md +9 -0
  87. package/docs/components/ui/icon.md +7 -0
  88. package/docs/components/ui/input-otp.md +29 -7
  89. package/docs/components/ui/input.md +7 -0
  90. package/docs/components/ui/label.md +7 -1
  91. package/docs/components/ui/link.md +7 -0
  92. package/docs/components/ui/menubar.md +9 -0
  93. package/docs/components/ui/navigation-menu.md +9 -0
  94. package/docs/components/ui/number-input.md +10 -0
  95. package/docs/components/ui/pagination.md +10 -0
  96. package/docs/components/ui/popover.md +9 -0
  97. package/docs/components/ui/progress-ring.md +7 -0
  98. package/docs/components/ui/progress.md +7 -0
  99. package/docs/components/ui/radio.md +27 -2
  100. package/docs/components/ui/search-input.md +8 -0
  101. package/docs/components/ui/segmented-control.md +8 -0
  102. package/docs/components/ui/select.md +14 -3
  103. package/docs/components/ui/separator.md +7 -0
  104. package/docs/components/ui/sheet.md +10 -0
  105. package/docs/components/ui/sidebar.md +30 -0
  106. package/docs/components/ui/skeleton.md +7 -0
  107. package/docs/components/ui/slider.md +15 -1
  108. package/docs/components/ui/spinner.md +7 -0
  109. package/docs/components/ui/split-button.md +11 -0
  110. package/docs/components/ui/stack.md +8 -0
  111. package/docs/components/ui/stat-card.md +9 -0
  112. package/docs/components/ui/status-dot.md +9 -0
  113. package/docs/components/ui/stepper.md +8 -0
  114. package/docs/components/ui/switch.md +8 -0
  115. package/docs/components/ui/table.md +8 -0
  116. package/docs/components/ui/tabs.md +10 -2
  117. package/docs/components/ui/text.md +9 -4
  118. package/docs/components/ui/textarea.md +7 -1
  119. package/docs/components/ui/toast.md +9 -0
  120. package/docs/components/ui/toaster.md +8 -0
  121. package/docs/components/ui/toggle-group.md +13 -4
  122. package/docs/components/ui/toggle.md +10 -2
  123. package/docs/components/ui/tooltip.md +11 -1
  124. package/docs/components/ui/tree-view.md +12 -0
  125. package/docs/components/ui/visually-hidden.md +7 -0
  126. package/llms-full.txt +1116 -44
  127. 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,wFAgJd,CAAA;AAGF,OAAO,EAAE,UAAU,EAAE,CAAA"}
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, ...h }, g) => {
10
- let [_, v] = r.useState(!1), [y, b] = r.useState(!1), x = r.useRef(null), S = r.useRef(s), C = p || _, w = !s;
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
- !y && x.current && (x.current.textContent = s);
13
- }, [s, y]);
14
- function T() {
15
- d || C || (b(!0), S.current = s, requestAnimationFrame(() => {
16
- if (!x.current) return;
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(x.current);
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 E() {
24
- if (!x.current) return;
25
- b(!1);
26
- let e = (x.current.textContent ?? "").trim(), t = f ? e.slice(0, f) : e;
27
- if (x.current.textContent = t || s, t === s) return;
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
- v(!0);
30
+ b(!0);
31
31
  try {
32
32
  await n;
33
33
  } catch {
34
- x.current && (x.current.textContent = s);
34
+ C.current && (C.current.textContent = s);
35
35
  } finally {
36
- v(!1);
36
+ b(!1);
37
37
  }
38
38
  }
39
39
  }
40
- function D() {
41
- x.current && (x.current.textContent = S.current, b(!1), x.current.blur());
40
+ function k() {
41
+ C.current && (C.current.textContent = w.current, S(!1), C.current.blur());
42
42
  }
43
- function O(e) {
44
- e.key === "Escape" ? (e.preventDefault(), D()) : e.key === "Enter" && (e.preventDefault(), E());
43
+ function A(e) {
44
+ e.key === "Escape" ? (e.preventDefault(), k()) : e.key === "Enter" && (e.preventDefault(), O());
45
45
  }
46
- function k() {
47
- if (!x.current || !f) return;
48
- let e = x.current.textContent ?? "";
46
+ function j() {
47
+ if (!C.current || !f) return;
48
+ let e = C.current.textContent ?? "";
49
49
  if (e.length > f) {
50
- x.current.textContent = e.slice(0, f);
50
+ C.current.textContent = e.slice(0, f);
51
51
  let t = document.createRange();
52
- t.selectNodeContents(x.current), t.collapse(!1);
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 A(e) {
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: g,
63
+ ref: v,
64
64
  className: e("group inline-flex items-center gap-ds-02", m),
65
- ...h,
65
+ ..._,
66
66
  children: [
67
67
  /* @__PURE__ */ i("span", {
68
- ref: x,
68
+ ref: C,
69
69
  role: d ? void 0 : "textbox",
70
- contentEditable: !d && !C,
70
+ contentEditable: !d && !T,
71
71
  suppressContentEditableWarning: !0,
72
72
  tabIndex: d ? void 0 : 0,
73
- onFocus: T,
74
- onBlur: E,
75
- onKeyDown: O,
76
- onInput: k,
77
- onPaste: A,
78
- spellCheck: y,
79
- className: e("font-sans text-surface-fg outline-hidden", u, !d && !y && "cursor-text rounded-ds-sm -mx-ds-01 px-ds-01 hover:bg-surface-raised-hover transition-colors duration-fast-01", y && "rounded-ds-sm -mx-ds-01 px-ds-01 bg-surface-raised-hover ring-1 ring-accent-7", w && !y && "text-surface-fg-subtle italic", d && "cursor-default"),
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 || (y ? "" : l)
83
+ children: s || (x ? "" : l)
82
84
  }),
83
- !d && !y && !C && /* @__PURE__ */ i(n, {
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
- C && /* @__PURE__ */ i(t, { size: "sm" })
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":"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"}
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":"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"}
@@ -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