@godxjp/ui 23.3.0 → 23.4.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 (80) hide show
  1. package/dist/components/data-display/card.d.ts +87 -1
  2. package/dist/components/data-display/card.js +88 -15
  3. package/dist/components/data-display/index.d.ts +5 -1
  4. package/dist/components/data-display/index.js +5 -0
  5. package/dist/components/data-display/thought-chain.d.ts +42 -0
  6. package/dist/components/data-display/thought-chain.js +177 -0
  7. package/dist/components/data-display/welcome.d.ts +26 -0
  8. package/dist/components/data-display/welcome.js +37 -0
  9. package/dist/components/data-entry/attachments.d.ts +19 -0
  10. package/dist/components/data-entry/attachments.js +322 -0
  11. package/dist/components/data-entry/index.d.ts +2 -0
  12. package/dist/components/data-entry/index.js +2 -0
  13. package/dist/components/feedback/dialog.d.ts +10 -7
  14. package/dist/components/feedback/dialog.js +40 -17
  15. package/dist/components/feedback/index.d.ts +1 -1
  16. package/dist/components/general/actions.d.ts +59 -0
  17. package/dist/components/general/actions.js +253 -0
  18. package/dist/components/general/activity.js +1 -0
  19. package/dist/components/general/float-button.d.ts +41 -0
  20. package/dist/components/general/float-button.js +336 -0
  21. package/dist/components/general/index.d.ts +6 -2
  22. package/dist/components/general/index.js +12 -1
  23. package/dist/components/general/typography.d.ts +79 -4
  24. package/dist/components/general/typography.js +598 -54
  25. package/dist/components/layout/draggable-panel.d.ts +18 -0
  26. package/dist/components/layout/draggable-panel.js +189 -0
  27. package/dist/components/layout/index.d.ts +2 -0
  28. package/dist/components/layout/index.js +2 -0
  29. package/dist/components/navigation/conversations.d.ts +39 -0
  30. package/dist/components/navigation/conversations.js +371 -0
  31. package/dist/components/navigation/index.d.ts +2 -0
  32. package/dist/components/navigation/index.js +2 -0
  33. package/dist/components/ui/card.js +1 -0
  34. package/dist/i18n/messages/en.json +53 -0
  35. package/dist/i18n/messages/ja.json +53 -0
  36. package/dist/i18n/messages/vi.json +53 -0
  37. package/dist/props/components/data-display.prop.d.ts +143 -1
  38. package/dist/props/components/data-entry.prop.d.ts +76 -0
  39. package/dist/props/components/feedback.prop.d.ts +34 -1
  40. package/dist/props/components/general.prop.d.ts +440 -3
  41. package/dist/props/components/index.d.ts +2 -2
  42. package/dist/props/components/layout.prop.d.ts +63 -1
  43. package/dist/props/components/navigation.prop.d.ts +150 -0
  44. package/dist/props/registry.d.ts +426 -0
  45. package/dist/props/registry.js +546 -0
  46. package/dist/props/vocabulary/index.d.ts +1 -1
  47. package/dist/props/vocabulary/interaction.prop.d.ts +113 -0
  48. package/dist/styles/card-layout.css +30 -0
  49. package/dist/styles/core.css +1 -0
  50. package/dist/styles/data-display-layout.css +236 -0
  51. package/dist/styles/float-button-layout.css +168 -0
  52. package/dist/styles/index.css +1 -0
  53. package/dist/styles/layout.css +196 -0
  54. package/dist/styles/navigation-layout.css +139 -0
  55. package/dist/styles/text-layout.css +156 -0
  56. package/dist/tokens/base.css +7 -0
  57. package/dist/tokens/components/actions.css +14 -0
  58. package/dist/tokens/components/attachments.css +15 -0
  59. package/dist/tokens/components/conversations.css +28 -0
  60. package/dist/tokens/components/draggable-panel.css +25 -0
  61. package/dist/tokens/components/float-button.css +39 -0
  62. package/dist/tokens/components/text.css +13 -0
  63. package/dist/tokens/components/thought-chain.css +31 -0
  64. package/dist/tokens/components/welcome.css +19 -0
  65. package/dist/tokens/foundation.css +2 -0
  66. package/docs/DESIGN-AUTHORITY.md +89 -0
  67. package/docs/FRAME-COVERAGE-REPORT.md +5 -2
  68. package/docs/data-display/card/examples/tab-list.tsx +97 -0
  69. package/docs/data-display/thought-chain.tsx +176 -0
  70. package/docs/data-display/welcome.tsx +115 -0
  71. package/docs/data-entry/attachments.tsx +160 -0
  72. package/docs/feedback/dialog.tsx +50 -0
  73. package/docs/general/actions.tsx +175 -0
  74. package/docs/general/float-button.tsx +133 -0
  75. package/docs/general/typography.tsx +63 -1
  76. package/docs/layout/draggable-panel.tsx +127 -0
  77. package/docs/navigation/conversations.tsx +176 -0
  78. package/docs/roadmap/parity-audit-layout-navigation-general.md +17 -16
  79. package/package.json +3 -3
  80. package/scripts/ui-audit.mjs +66 -2
@@ -1,13 +1,379 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1
3
  import * as React from "react";
4
+ import { Check, Copy, LoaderCircle, Pencil } from "lucide-react";
2
5
  import { cn } from "../../lib/utils.js";
3
- const Text = React.forwardRef(
4
- ({
5
- as = "span",
6
+ import { useTranslation } from "../../i18n/use-translation.js";
7
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../feedback/tooltip.js";
8
+ import { Textarea } from "../data-entry/textarea.js";
9
+ const COPIED_RESET_MS = 3e3;
10
+ const DECORATION_ELEMENTS = [
11
+ ["strong", "strong"],
12
+ ["underline", "u"],
13
+ ["delete", "del"],
14
+ ["code", "code"],
15
+ ["mark", "mark"],
16
+ ["keyboard", "kbd"],
17
+ ["italic", "i"]
18
+ ];
19
+ function toList(value) {
20
+ if (value === void 0) return [];
21
+ return Array.isArray(value) ? value : [value];
22
+ }
23
+ function getNode(node, fallback, needDom = false) {
24
+ if (node === true || node === void 0) return fallback;
25
+ if (node === false) return needDom ? fallback : null;
26
+ return node || (needDom ? fallback : null);
27
+ }
28
+ function toCopyConfigList(value) {
29
+ if (value === false) return [false, false];
30
+ return toList(value);
31
+ }
32
+ function useMergedConfig(propConfig, template) {
33
+ const support = Boolean(propConfig);
34
+ return React.useMemo(() => {
35
+ const isObject = propConfig !== null && typeof propConfig === "object";
36
+ return [support, { ...template, ...support && isObject ? propConfig : null }];
37
+ }, [support, propConfig]);
38
+ }
39
+ function useControlledFlag(initial, controlled) {
40
+ const [internal, setInternal] = React.useState(initial);
41
+ return [controlled ?? internal, setInternal];
42
+ }
43
+ const TYPE_TO_TONE = {
44
+ secondary: "muted",
45
+ success: "success",
46
+ warning: "warning",
47
+ danger: "destructive"
48
+ };
49
+ async function writeClipboard(text, format) {
50
+ if (format === "text/html" && typeof ClipboardItem !== "undefined") {
51
+ await navigator.clipboard.write([
52
+ new ClipboardItem({
53
+ "text/html": new Blob([text], { type: "text/html" }),
54
+ "text/plain": new Blob([text], { type: "text/plain" })
55
+ })
56
+ ]);
57
+ return;
58
+ }
59
+ await navigator.clipboard.writeText(text);
60
+ }
61
+ function childrenToText(children) {
62
+ return React.Children.toArray(children).map((node) => typeof node === "string" || typeof node === "number" ? String(node) : "").join("");
63
+ }
64
+ function ActionTooltip({ title, children }) {
65
+ if (title === null || title === void 0 || title === false || title === "") return children;
66
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
67
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children }),
68
+ /* @__PURE__ */ jsx(TooltipContent, { children: title })
69
+ ] });
70
+ }
71
+ const TypographyAction = React.forwardRef(({ slot, className, ...props }, ref) => /* @__PURE__ */ jsx(
72
+ "button",
73
+ {
74
+ ref,
75
+ type: "button",
76
+ "data-slot": slot,
77
+ className: cn("ui-typography-action", className),
78
+ ...props
79
+ }
80
+ ));
81
+ TypographyAction.displayName = "TypographyAction";
82
+ function TypographyEditor({
83
+ value,
84
+ ariaLabel,
85
+ maxLength,
86
+ autoSize = true,
87
+ enterIcon,
88
+ className,
89
+ style,
90
+ onSave,
91
+ onCancel,
92
+ onEnd
93
+ }) {
94
+ const ref = React.useRef(null);
95
+ const inComposition = React.useRef(false);
96
+ const lastKey = React.useRef(null);
97
+ const [current, setCurrent] = React.useState(value);
98
+ React.useEffect(() => {
99
+ setCurrent(value);
100
+ }, [value]);
101
+ React.useEffect(() => {
102
+ const node = ref.current;
103
+ if (!node) return;
104
+ node.focus();
105
+ node.setSelectionRange(node.value.length, node.value.length);
106
+ }, []);
107
+ const confirm = () => {
108
+ onSave(current.trim());
109
+ };
110
+ return /* @__PURE__ */ jsxs("div", { "data-slot": "typography-edit", className: cn("ui-typography-edit", className), style, children: [
111
+ /* @__PURE__ */ jsx(
112
+ Textarea,
113
+ {
114
+ ref,
115
+ variant: "borderless",
116
+ className: "ui-typography-edit-textarea",
117
+ rows: 1,
118
+ autoSize,
119
+ maxLength,
120
+ "aria-label": ariaLabel,
121
+ value: current,
122
+ onChange: (event) => {
123
+ setCurrent(event.target.value.replace(/[\n\r]/g, ""));
124
+ },
125
+ onCompositionStart: () => {
126
+ inComposition.current = true;
127
+ },
128
+ onCompositionEnd: () => {
129
+ inComposition.current = false;
130
+ },
131
+ onKeyDown: (event) => {
132
+ if (inComposition.current) return;
133
+ lastKey.current = event.key;
134
+ },
135
+ onKeyUp: (event) => {
136
+ if (lastKey.current !== event.key || inComposition.current || event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
137
+ return;
138
+ }
139
+ if (event.key === "Enter") {
140
+ confirm();
141
+ onEnd?.();
142
+ } else if (event.key === "Escape") {
143
+ onCancel();
144
+ }
145
+ },
146
+ onBlur: confirm
147
+ }
148
+ ),
149
+ enterIcon !== null ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "ui-typography-edit-confirm", children: enterIcon ?? "\u23CE" }) : null
150
+ ] });
151
+ }
152
+ function useTypographyBlock(props, allowRows) {
153
+ const { t } = useTranslation();
154
+ const { children, copyable, editable, ellipsis, actions, disabled } = props;
155
+ const placement = actions?.placement ?? "end";
156
+ const [enableEdit, editConfig] = useMergedConfig(editable);
157
+ const [editing, setEditing] = useControlledFlag(false, editConfig.editing);
158
+ const triggerType = editConfig.triggerType ?? ["icon"];
159
+ const editButtonRef = React.useRef(null);
160
+ const wasEditing = React.useRef(false);
161
+ React.useEffect(() => {
162
+ if (!editing && wasEditing.current) editButtonRef.current?.focus();
163
+ wasEditing.current = editing;
164
+ }, [editing]);
165
+ const startEditing = (event) => {
166
+ event?.preventDefault();
167
+ editConfig.onStart?.();
168
+ setEditing(true);
169
+ };
170
+ const [enableCopy, copyConfig] = useMergedConfig(copyable);
171
+ const [copied, setCopied] = React.useState(false);
172
+ const [copyLoading, setCopyLoading] = React.useState(false);
173
+ const copyTimer = React.useRef(null);
174
+ React.useEffect(
175
+ () => () => {
176
+ if (copyTimer.current) clearTimeout(copyTimer.current);
177
+ },
178
+ []
179
+ );
180
+ const onCopyClick = async (event) => {
181
+ event.preventDefault();
182
+ event.stopPropagation();
183
+ setCopyLoading(true);
184
+ try {
185
+ const configured = typeof copyConfig.text === "function" ? await copyConfig.text() : copyConfig.text;
186
+ await writeClipboard(configured || childrenToText(children) || "", copyConfig.format);
187
+ setCopyLoading(false);
188
+ setCopied(true);
189
+ if (copyTimer.current) clearTimeout(copyTimer.current);
190
+ copyTimer.current = setTimeout(() => {
191
+ setCopied(false);
192
+ }, COPIED_RESET_MS);
193
+ copyConfig.onCopy?.(event);
194
+ } catch {
195
+ setCopyLoading(false);
196
+ }
197
+ };
198
+ const [enableEllipsis, ellipsisConfig] = useMergedConfig(ellipsis);
199
+ const [expanded, setExpanded] = useControlledFlag(
200
+ ellipsisConfig.defaultExpanded ?? false,
201
+ ellipsisConfig.expanded
202
+ );
203
+ const rows = allowRows ? ellipsisConfig.rows ?? 1 : 1;
204
+ const expandable = allowRows ? ellipsisConfig.expandable : void 0;
205
+ const clamping = enableEllipsis && (!expanded || expandable === "collapsible");
206
+ const [isEllipsis, setIsEllipsis] = React.useState(false);
207
+ const elementRef = React.useRef(null);
208
+ const onEllipsisRef = React.useRef(ellipsisConfig.onEllipsis);
209
+ onEllipsisRef.current = ellipsisConfig.onEllipsis;
210
+ const measure = React.useCallback(() => {
211
+ const node = elementRef.current;
212
+ if (!node || !enableEllipsis) return;
213
+ const next = rows > 1 ? node.scrollHeight > node.clientHeight : node.scrollWidth > node.clientWidth;
214
+ setIsEllipsis((prev) => {
215
+ if (prev === next) return prev;
216
+ onEllipsisRef.current?.(next);
217
+ return next;
218
+ });
219
+ }, [enableEllipsis, rows]);
220
+ const measureRef = React.useCallback((node) => {
221
+ elementRef.current = node;
222
+ }, []);
223
+ React.useLayoutEffect(() => {
224
+ measure();
225
+ }, [measure, children, expanded, clamping]);
226
+ React.useEffect(() => {
227
+ const node = elementRef.current;
228
+ if (!node || !enableEllipsis || typeof ResizeObserver === "undefined") return void 0;
229
+ const observer = new ResizeObserver(() => {
230
+ measure();
231
+ });
232
+ observer.observe(node);
233
+ return () => {
234
+ observer.disconnect();
235
+ };
236
+ }, [measure, enableEllipsis]);
237
+ const onExpandClick = (event) => {
238
+ const next = !expanded;
239
+ setExpanded(next);
240
+ ellipsisConfig.onExpand?.(event, { expanded: next });
241
+ };
242
+ const copyLabels = toCopyConfigList(copyConfig.tooltips);
243
+ const copyIcons = toCopyConfigList(copyConfig.icon);
244
+ const systemCopyLabel = copied ? t("ui.typography.copied") : t("ui.typography.copy");
245
+ const copyTitle = getNode(copyLabels[copied ? 1 : 0], systemCopyLabel);
246
+ const copyAriaLabel = typeof copyTitle === "string" ? copyTitle : systemCopyLabel;
247
+ const copyNode = enableCopy ? /* @__PURE__ */ jsx(ActionTooltip, { title: copyTitle, children: /* @__PURE__ */ jsx(
248
+ TypographyAction,
249
+ {
250
+ slot: "typography-copy",
251
+ "data-copied": copied ? "" : void 0,
252
+ "aria-label": copyAriaLabel,
253
+ tabIndex: copyConfig.tabIndex,
254
+ onClick: onCopyClick,
255
+ children: copied ? getNode(copyIcons[1], /* @__PURE__ */ jsx(Check, { "aria-hidden": true, className: "ui-typography-icon" }), true) : getNode(
256
+ copyIcons[0],
257
+ copyLoading ? /* @__PURE__ */ jsx(LoaderCircle, { "aria-hidden": true, className: "ui-typography-icon ui-typography-icon-spin" }) : /* @__PURE__ */ jsx(Copy, { "aria-hidden": true, className: "ui-typography-icon" }),
258
+ true
259
+ )
260
+ }
261
+ ) }, "copy") : null;
262
+ const editTitle = toList(editConfig.tooltip)[0] ?? t("ui.typography.edit");
263
+ const editNode = enableEdit && triggerType.includes("icon") ? /* @__PURE__ */ jsx(ActionTooltip, { title: editConfig.tooltip === false ? null : editTitle, children: /* @__PURE__ */ jsx(
264
+ TypographyAction,
265
+ {
266
+ ref: editButtonRef,
267
+ slot: "typography-edit-trigger",
268
+ "aria-label": typeof editTitle === "string" ? editTitle : t("ui.typography.edit"),
269
+ tabIndex: editConfig.tabIndex,
270
+ onClick: startEditing,
271
+ children: editConfig.icon ?? /* @__PURE__ */ jsx(Pencil, { "aria-hidden": true, className: "ui-typography-icon" })
272
+ }
273
+ ) }, "edit") : null;
274
+ const showExpand = Boolean(expandable) && (isEllipsis || expanded);
275
+ const expandSymbol = typeof ellipsisConfig.symbol === "function" ? ellipsisConfig.symbol(expanded) : ellipsisConfig.symbol ?? (expanded ? t("ui.typography.collapse") : t("ui.typography.expand"));
276
+ const expandNode = showExpand ? /* @__PURE__ */ jsx(
277
+ TypographyAction,
278
+ {
279
+ slot: expanded ? "typography-collapse" : "typography-expand",
280
+ "aria-expanded": expanded,
281
+ "aria-label": expanded ? t("ui.typography.collapse") : t("ui.typography.expand"),
282
+ onClick: onExpandClick,
283
+ children: expandSymbol
284
+ },
285
+ "expand"
286
+ ) : null;
287
+ const hasActions = Boolean(copyNode || editNode || expandNode);
288
+ const actionsNode = hasActions ? /* @__PURE__ */ jsxs(
289
+ "span",
290
+ {
291
+ "data-slot": "typography-actions",
292
+ "data-placement": placement,
293
+ className: "ui-typography-actions",
294
+ children: [
295
+ expandNode,
296
+ editNode,
297
+ copyNode
298
+ ]
299
+ },
300
+ "actions"
301
+ ) : null;
302
+ let decorated = children;
303
+ for (const [flag, tag] of DECORATION_ELEMENTS) {
304
+ if (props[flag]) decorated = React.createElement(tag, {}, decorated);
305
+ }
306
+ const suffix = ellipsisConfig.suffix;
307
+ const wrapsContent = clamping && Boolean(hasActions || suffix !== void 0);
308
+ const ellipsisTooltip = ellipsisConfig.tooltip === true ? editConfig.text ?? children : ellipsisConfig.tooltip;
309
+ const attrs = {
310
+ "data-disabled": disabled ? "" : void 0,
311
+ "data-ellipsis": enableEllipsis ? "" : void 0,
312
+ "data-expanded": enableEllipsis && expanded ? "" : void 0
313
+ };
314
+ return {
315
+ editing: enableEdit && editing,
316
+ editor: /* @__PURE__ */ jsx(
317
+ TypographyEditor,
318
+ {
319
+ value: editConfig.text ?? (typeof children === "string" ? children : ""),
320
+ ariaLabel: typeof editConfig.text === "string" ? editConfig.text : void 0,
321
+ maxLength: editConfig.maxLength,
322
+ autoSize: editConfig.autoSize,
323
+ enterIcon: editConfig.enterIcon,
324
+ onSave: (value) => {
325
+ editConfig.onChange?.(value);
326
+ setEditing(false);
327
+ },
328
+ onCancel: () => {
329
+ editConfig.onCancel?.();
330
+ setEditing(false);
331
+ },
332
+ onEnd: editConfig.onEnd
333
+ }
334
+ ),
335
+ leading: placement === "start" ? actionsNode : null,
336
+ text: decorated,
337
+ trailing: /* @__PURE__ */ jsxs(Fragment, { children: [
338
+ suffix,
339
+ placement === "start" ? null : actionsNode
340
+ ] }),
341
+ attrs,
342
+ measureRef,
343
+ wrapsContent,
344
+ onTextClick: enableEdit && triggerType.includes("text") ? startEditing : void 0,
345
+ ellipsisTooltip,
346
+ isEllipsis
347
+ };
348
+ }
349
+ function usesBlockBehaviour(props) {
350
+ if (props.copyable || props.editable || props.ellipsis || props.actions || props.disabled) {
351
+ return true;
352
+ }
353
+ return DECORATION_ELEMENTS.some(([flag]) => props[flag]);
354
+ }
355
+ function ellipsisRows(ellipsis) {
356
+ if (!ellipsis || ellipsis === true) return 1;
357
+ const rows = ellipsis.rows;
358
+ return typeof rows === "number" && rows >= 1 ? Math.floor(rows) : 1;
359
+ }
360
+ function composeRefs(measureRef, forwarded) {
361
+ return (node) => {
362
+ measureRef(node);
363
+ if (typeof forwarded === "function") forwarded(node);
364
+ else if (forwarded) forwarded.current = node;
365
+ };
366
+ }
367
+ const TextBase = React.forwardRef((props, ref) => {
368
+ const {
369
+ as,
370
+ component,
6
371
  asChild = false,
7
372
  size = "sm",
8
373
  // `link` is an affordance, not a colour: it only moves the DEFAULT tone, so
9
374
  // `link tone="destructive"` is a destructive link rather than an argument between two rules.
10
375
  tone,
376
+ type,
11
377
  weight = "regular",
12
378
  align,
13
379
  truncate,
@@ -21,60 +387,144 @@ const Text = React.forwardRef(
21
387
  className,
22
388
  style,
23
389
  children,
24
- ...props
25
- }, ref) => {
26
- const clampLines = typeof clamp === "number" && Number.isFinite(clamp) && clamp >= 1 ? Math.floor(clamp) : void 0;
27
- const truncating = truncate === true && clampLines === void 0;
28
- if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
29
- if (clamp !== void 0 && clampLines === void 0) {
30
- console.warn(
31
- `Text: \`clamp\` must be a finite number \u2265 1 (got ${String(clamp)}); ignored.`
32
- );
33
- }
34
- if (truncate && clampLines !== void 0) {
35
- console.warn(
36
- "Text: `truncate` and `clamp` are mutually exclusive \u2014 `clamp` takes precedence; drop `truncate`."
37
- );
38
- }
39
- if (whitespace === "pre-wrap" && truncating) {
40
- console.warn(
41
- 'Text: `truncate` and `whitespace="pre-wrap"` are mutually exclusive \u2014 `truncate` takes precedence (one line, one ellipsis); use `clamp` to keep preserved line breaks and still bound the height.'
42
- );
43
- }
390
+ allowRows = false,
391
+ // antd block behaviour — read by `useTypographyBlock`, never spread onto the DOM.
392
+ copyable: _copyable,
393
+ editable: _editable,
394
+ ellipsis,
395
+ actions: _actions,
396
+ disabled,
397
+ code: _code,
398
+ mark: _mark,
399
+ underline: _underline,
400
+ delete: _delete,
401
+ strong: _strong,
402
+ keyboard: _keyboard,
403
+ italic: _italic,
404
+ ...rest
405
+ } = props;
406
+ const block = useTypographyBlock(props, allowRows);
407
+ const element = as ?? component ?? "span";
408
+ const clampLines = typeof clamp === "number" && Number.isFinite(clamp) && clamp >= 1 ? Math.floor(clamp) : void 0;
409
+ const ellipsisRowCount = ellipsis ? ellipsisRows(ellipsis) : 0;
410
+ const effectiveClamp = ellipsisRowCount > 1 ? ellipsisRowCount : ellipsis ? void 0 : clampLines;
411
+ const truncating = (truncate === true || ellipsisRowCount === 1) && effectiveClamp === void 0;
412
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
413
+ if (clamp !== void 0 && clampLines === void 0) {
414
+ console.warn(`Text: \`clamp\` must be a finite number \u2265 1 (got ${String(clamp)}); ignored.`);
44
415
  }
45
- const typography = {
46
- "data-slot": "text",
47
- "data-size": size,
48
- "data-tone": tone ?? (link ? "primary" : "default"),
49
- "data-link": link ? "" : void 0,
50
- "data-weight": weight,
51
- "data-align": align,
52
- "data-truncate": truncating ? "" : void 0,
53
- "data-clamp": clampLines !== void 0 ? "" : void 0,
54
- // Inert default: `normal` is CSS's own behaviour, so it emits no attribute and there is no
55
- // `[data-whitespace="normal"]` rule to lose a specificity argument with anything.
56
- "data-whitespace": whitespace === "pre-wrap" && !truncating ? "pre-wrap" : void 0,
57
- style: clampLines !== void 0 ? { ...style, "--text-clamp": clampLines } : style,
58
- "data-tabular": tabular ? "" : void 0,
59
- "data-decoration": decoration,
60
- "data-chip": chip ? "" : void 0,
61
- "data-mono": mono ? "" : void 0,
62
- className: cn("ui-text", className),
63
- ...props
64
- };
65
- if (asChild) {
66
- const child = React.Children.only(children);
67
- return React.cloneElement(child, {
68
- ...typography,
69
- ...child.props,
70
- ref,
71
- className: cn(typography.className, child.props.className)
72
- });
416
+ if (truncate && clampLines !== void 0) {
417
+ console.warn(
418
+ "Text: `truncate` and `clamp` are mutually exclusive \u2014 `clamp` takes precedence; drop `truncate`."
419
+ );
420
+ }
421
+ if (whitespace === "pre-wrap" && truncating) {
422
+ console.warn(
423
+ 'Text: `truncate` and `whitespace="pre-wrap"` are mutually exclusive \u2014 `truncate` takes precedence (one line, one ellipsis); use `clamp` to keep preserved line breaks and still bound the height.'
424
+ );
425
+ }
426
+ if (ellipsis && (truncate || clamp !== void 0)) {
427
+ console.warn(
428
+ "Text: `ellipsis` (antd) and `truncate`/`clamp` are the same axis \u2014 `ellipsis` takes precedence; drop the other."
429
+ );
430
+ }
431
+ if (tone && type) {
432
+ console.warn(
433
+ "Text: `tone` and `type` (antd) are the same axis \u2014 `tone` takes precedence; drop `type`."
434
+ );
73
435
  }
74
- return React.createElement(as, { ref, ...typography }, children);
75
436
  }
76
- );
437
+ const resolvedTone = tone ?? (type ? TYPE_TO_TONE[type] : void 0) ?? (link ? "primary" : "default");
438
+ const clampStyle = effectiveClamp !== void 0 ? { ...style, "--text-clamp": effectiveClamp } : style;
439
+ const typography = {
440
+ "data-slot": "text",
441
+ "data-size": size,
442
+ "data-tone": resolvedTone,
443
+ "data-link": link ? "" : void 0,
444
+ "data-weight": weight,
445
+ "data-align": align,
446
+ // When the action cluster or a suffix is present the clamp moves ONTO the inner wrapper, so the
447
+ // buttons are not eaten by the clamped box. Everything else keeps the attribute where it has
448
+ // always been, on the element itself.
449
+ "data-truncate": truncating && !block.wrapsContent ? "" : void 0,
450
+ "data-clamp": effectiveClamp !== void 0 && !block.wrapsContent ? "" : void 0,
451
+ // Inert default: `normal` is CSS's own behaviour, so it emits no attribute and there is no
452
+ // `[data-whitespace="normal"]` rule to lose a specificity argument with anything.
453
+ "data-whitespace": whitespace === "pre-wrap" && !truncating ? "pre-wrap" : void 0,
454
+ style: clampStyle,
455
+ "data-tabular": tabular ? "" : void 0,
456
+ "data-decoration": decoration,
457
+ "data-chip": chip ? "" : void 0,
458
+ "data-mono": mono ? "" : void 0,
459
+ "aria-disabled": disabled ? true : void 0,
460
+ ...block.attrs,
461
+ onClick: block.onTextClick,
462
+ className: cn("ui-text", className),
463
+ ...rest
464
+ };
465
+ if (block.editing) return block.editor;
466
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
467
+ block.leading,
468
+ block.wrapsContent ? /* @__PURE__ */ jsx(
469
+ "span",
470
+ {
471
+ "data-slot": "typography-content",
472
+ "data-truncate": truncating ? "" : void 0,
473
+ "data-clamp": effectiveClamp !== void 0 ? "" : void 0,
474
+ className: "ui-typography-content",
475
+ style: clampStyle,
476
+ children: block.text
477
+ }
478
+ ) : block.text,
479
+ block.trailing
480
+ ] });
481
+ if (asChild) {
482
+ const child = React.Children.only(children);
483
+ const cloned = {
484
+ ...typography,
485
+ ...child.props,
486
+ ref,
487
+ className: cn(typography.className, child.props.className)
488
+ };
489
+ return usesBlockBehaviour(props) ? React.cloneElement(child, cloned, body) : React.cloneElement(child, cloned);
490
+ }
491
+ return React.createElement(
492
+ element,
493
+ { ref: composeRefs(block.measureRef, ref), ...typography },
494
+ body
495
+ );
496
+ });
497
+ TextBase.displayName = "TextBase";
498
+ const Text = React.forwardRef((props, ref) => /* @__PURE__ */ jsx(TextBase, { ref, ...props }));
77
499
  Text.displayName = "Text";
500
+ const Paragraph = React.forwardRef(
501
+ ({ as = "div", className, ...rest }, ref) => /* @__PURE__ */ jsx(
502
+ TextBase,
503
+ {
504
+ ref,
505
+ as,
506
+ allowRows: true,
507
+ className: cn("ui-typography-paragraph", className),
508
+ ...rest
509
+ }
510
+ )
511
+ );
512
+ Paragraph.displayName = "Paragraph";
513
+ const Link = React.forwardRef(
514
+ ({ as = "a", rel, target, className, ...rest }, ref) => /* @__PURE__ */ jsx(
515
+ TextBase,
516
+ {
517
+ ref,
518
+ as,
519
+ link: true,
520
+ target,
521
+ rel: rel === void 0 && target === "_blank" ? "noopener noreferrer" : rel,
522
+ className: cn("ui-typography-link", className),
523
+ ...rest
524
+ }
525
+ )
526
+ );
527
+ Link.displayName = "Link";
78
528
  const Heading = React.forwardRef(
79
529
  ({ level = 2, as, tone = "default", align, truncate, weight = "medium", className, ...props }, ref) => React.createElement(as ?? `h${level}`, {
80
530
  ref,
@@ -89,7 +539,101 @@ const Heading = React.forwardRef(
89
539
  })
90
540
  );
91
541
  Heading.displayName = "Heading";
542
+ const Title = React.forwardRef((props, ref) => {
543
+ const {
544
+ level = 1,
545
+ as,
546
+ component,
547
+ tone,
548
+ type,
549
+ align,
550
+ truncate,
551
+ weight = "medium",
552
+ className,
553
+ style,
554
+ children: _children,
555
+ ellipsis,
556
+ copyable: _copyable,
557
+ editable: _editable,
558
+ actions: _actions,
559
+ disabled,
560
+ code: _code,
561
+ mark: _mark,
562
+ underline: _underline,
563
+ delete: _delete,
564
+ keyboard: _keyboard,
565
+ italic: _italic,
566
+ ...rest
567
+ } = props;
568
+ const block = useTypographyBlock(props, true);
569
+ const safeLevel = level >= 1 && level <= 5 ? level : 1;
570
+ const element = as ?? component ?? `h${safeLevel}`;
571
+ const ellipsisRowCount = ellipsis ? ellipsisRows(ellipsis) : 0;
572
+ const effectiveClamp = ellipsisRowCount > 1 ? ellipsisRowCount : void 0;
573
+ const truncating = (truncate === true || ellipsisRowCount === 1) && effectiveClamp === void 0;
574
+ const clampStyle = effectiveClamp !== void 0 ? { ...style, "--text-clamp": effectiveClamp } : style;
575
+ if (block.editing) return block.editor;
576
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
577
+ block.leading,
578
+ block.wrapsContent ? /* @__PURE__ */ jsx(
579
+ "span",
580
+ {
581
+ "data-slot": "typography-content",
582
+ "data-truncate": truncating ? "" : void 0,
583
+ "data-clamp": effectiveClamp !== void 0 ? "" : void 0,
584
+ className: "ui-typography-content",
585
+ style: clampStyle,
586
+ children: block.text
587
+ }
588
+ ) : block.text,
589
+ block.trailing
590
+ ] });
591
+ return React.createElement(
592
+ element,
593
+ {
594
+ ref: composeRefs(block.measureRef, ref),
595
+ "data-slot": "heading",
596
+ "data-level": safeLevel,
597
+ "data-tone": tone ?? (type ? TYPE_TO_TONE[type] : void 0) ?? "default",
598
+ "data-align": align,
599
+ "data-weight": weight,
600
+ "data-truncate": truncating && !block.wrapsContent ? "" : void 0,
601
+ "data-clamp": effectiveClamp !== void 0 && !block.wrapsContent ? "" : void 0,
602
+ "aria-disabled": disabled ? true : void 0,
603
+ style: clampStyle,
604
+ ...block.attrs,
605
+ onClick: block.onTextClick,
606
+ className: cn("ui-heading", "ui-typography-title", className),
607
+ ...rest
608
+ },
609
+ body
610
+ );
611
+ });
612
+ Title.displayName = "Title";
613
+ const TypographyRoot = React.forwardRef(
614
+ ({ as, component, className, children, ...rest }, ref) => React.createElement(
615
+ as ?? component ?? "article",
616
+ {
617
+ ref,
618
+ "data-slot": "typography",
619
+ className: cn("ui-typography", className),
620
+ ...rest
621
+ },
622
+ children
623
+ )
624
+ );
625
+ TypographyRoot.displayName = "Typography";
626
+ const Typography = Object.assign(TypographyRoot, {
627
+ Text,
628
+ Title,
629
+ Paragraph,
630
+ Link
631
+ });
92
632
  export {
93
633
  Heading,
94
- Text
634
+ Link,
635
+ Paragraph,
636
+ Text,
637
+ Title,
638
+ Typography
95
639
  };