@tangle-network/ui 11.12.0 → 11.13.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/CHANGELOG.md +24 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/auth/auth.d.ts +53 -0
- package/dist/{auth-DOETfnd9.js → auth/auth.js} +7 -37
- package/dist/auth/login-layout.d.ts +20 -0
- package/dist/auth/login-layout.js +36 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +2 -1
- package/dist/chat/agent-timeline.d.ts +83 -0
- package/dist/chat/agent-timeline.js +236 -0
- package/dist/chat/chat-container.d.ts +51 -0
- package/dist/chat/chat-container.js +309 -0
- package/dist/chat/chat-message.d.ts +22 -0
- package/dist/chat/chat-message.js +38 -0
- package/dist/chat/message-list.d.ts +32 -0
- package/dist/chat/message-list.js +37 -0
- package/dist/chat/thinking-indicator.d.ts +6 -0
- package/dist/chat/thinking-indicator.js +38 -0
- package/dist/chat/user-message.d.ts +21 -0
- package/dist/chat/user-message.js +42 -0
- package/dist/chat.d.ts +6 -1
- package/dist/chat.js +6 -1
- package/dist/editor/document-editor-pane.d.ts +37 -0
- package/dist/editor/document-editor-pane.js +199 -0
- package/dist/editor/editor-lazy.js +37 -0
- package/dist/editor/editor-loading.js +21 -0
- package/dist/editor/editor-peers.d.ts +6 -0
- package/dist/editor/editor-peers.js +155 -0
- package/dist/editor/editor-provider.d.ts +97 -0
- package/dist/editor/editor-provider.js +253 -0
- package/dist/editor/editor-toolbar.d.ts +11 -0
- package/dist/editor/editor-toolbar.js +106 -0
- package/dist/editor/markdown-conversion.js +23 -0
- package/dist/editor/markdown-document-editor.js +103 -0
- package/dist/editor/tiptap-editor.d.ts +45 -0
- package/dist/editor/tiptap-editor.js +271 -0
- package/dist/editor/use-editor.d.ts +67 -0
- package/dist/editor/use-editor.js +158 -0
- package/dist/editor.d.ts +6 -119
- package/dist/editor.js +5 -1
- package/dist/files/file-artifact-pane.d.ts +47 -0
- package/dist/files/file-artifact-pane.js +114 -0
- package/dist/files/file-format.d.ts +51 -0
- package/dist/{file-format-BgXbr-Ib.js → files/file-format.js} +1 -1
- package/dist/files/file-preview.d.ts +27 -0
- package/dist/files/file-preview.js +359 -0
- package/dist/files/file-tabs.d.ts +19 -0
- package/dist/files/file-tabs.js +48 -0
- package/dist/files/file-tree.d.ts +31 -0
- package/dist/files/file-tree.js +135 -0
- package/dist/files/rich-file-tree.d.ts +62 -0
- package/dist/files/rich-file-tree.js +122 -0
- package/dist/files.d.ts +6 -1
- package/dist/files.js +6 -2
- package/dist/hooks/use-auth.d.ts +40 -0
- package/dist/hooks/use-auth.js +91 -0
- package/dist/hooks/use-auto-scroll.d.ts +11 -0
- package/dist/hooks/use-auto-scroll.js +48 -0
- package/dist/hooks/use-dropdown-menu.d.ts +12 -0
- package/dist/hooks/use-dropdown-menu.js +31 -0
- package/dist/hooks/use-live-time.d.ts +7 -0
- package/dist/hooks/use-live-time.js +22 -0
- package/dist/hooks/use-realtime-session.d.ts +32 -0
- package/dist/hooks/use-realtime-session.js +235 -0
- package/dist/hooks/use-run-collapse-state.d.ts +13 -0
- package/dist/hooks/use-run-collapse-state.js +24 -0
- package/dist/hooks/use-run-groups.d.ts +11 -0
- package/dist/hooks/use-run-groups.js +85 -0
- package/dist/hooks/use-sdk-session.d.ts +59 -0
- package/dist/hooks/use-sdk-session.js +322 -0
- package/dist/hooks/use-sse-stream.d.ts +124 -0
- package/dist/hooks/use-sse-stream.js +229 -0
- package/dist/hooks/use-tool-call-stream.d.ts +23 -0
- package/dist/hooks/use-tool-call-stream.js +75 -0
- package/dist/hooks.d.ts +11 -49
- package/dist/hooks.js +10 -110
- package/dist/index.d.ts +99 -25
- package/dist/index.js +86 -36
- package/dist/lib/focus.d.ts +55 -0
- package/dist/{focus-BcE_SHqz.js → lib/focus.js} +1 -1
- package/dist/lib/utils.d.ts +4 -0
- package/dist/{utils-C_uf36nf.js → lib/utils.js} +1 -1
- package/dist/{code-block-B9miUPht.d.ts → markdown/code-block.d.ts} +5 -6
- package/dist/{code-block-B36KVUSN.js → markdown/code-block.js} +60 -55
- package/dist/{markdown-C0atSabr.d.ts → markdown/markdown.d.ts} +4 -5
- package/dist/{markdown-Db08KAYF.js → markdown/markdown.js} +3 -3
- package/dist/markdown.d.ts +2 -2
- package/dist/markdown.js +2 -2
- package/dist/{openui-artifact-renderer-fkBQaoLO.d.ts → openui/openui-artifact-renderer.d.ts} +20 -21
- package/dist/{openui-artifact-renderer-DG_i2lOR.js → openui/openui-artifact-renderer.js} +9 -7
- package/dist/{schema-BpW9rSSG.js → openui/schema-compiled.js} +2 -354
- package/dist/{schema-DAstk-J4.d.ts → openui/schema-data.d.ts} +3 -24
- package/dist/openui/schema-data.js +206 -0
- package/dist/openui-schema.d.ts +22 -3
- package/dist/openui-schema.js +138 -1
- package/dist/openui.d.ts +3 -2
- package/dist/openui.js +3 -2
- package/dist/{artifact-pane-BL_DFWNl.d.ts → primitives/artifact-pane.d.ts} +3 -4
- package/dist/{artifact-pane-DMXT9a8U.js → primitives/artifact-pane.js} +2 -2
- package/dist/primitives/avatar.d.ts +8 -0
- package/dist/primitives/avatar.js +25 -0
- package/dist/primitives/badge.d.ts +22 -0
- package/dist/primitives/badge.js +47 -0
- package/dist/{button-yoIv77e2.d.ts → primitives/button.d.ts} +2 -2
- package/dist/primitives/button.js +113 -0
- package/dist/primitives/card.d.ts +18 -0
- package/dist/primitives/card.js +52 -0
- package/dist/primitives/code-block.d.ts +6 -0
- package/dist/primitives/code-block.js +13 -0
- package/dist/primitives/dialog.d.ts +23 -0
- package/dist/primitives/dialog.js +59 -0
- package/dist/primitives/drop-zone.d.ts +23 -0
- package/dist/primitives/drop-zone.js +104 -0
- package/dist/primitives/dropdown-menu.d.ts +29 -0
- package/dist/{dropdown-menu-BpcLa5vF.js → primitives/dropdown-menu.js} +2 -23
- package/dist/primitives/empty-state.d.ts +11 -0
- package/dist/primitives/empty-state.js +32 -0
- package/dist/primitives/heading.d.ts +23 -0
- package/dist/primitives/heading.js +38 -0
- package/dist/primitives/input.d.ts +23 -0
- package/dist/primitives/input.js +110 -0
- package/dist/primitives/label.d.ts +6 -0
- package/dist/primitives/label.js +16 -0
- package/dist/primitives/metric-strip.d.ts +48 -0
- package/dist/primitives/metric-strip.js +52 -0
- package/dist/primitives/page-header.d.ts +25 -0
- package/dist/primitives/page-header.js +47 -0
- package/dist/primitives/page-shell.d.ts +14 -0
- package/dist/primitives/page-shell.js +17 -0
- package/dist/primitives/progress.d.ts +9 -0
- package/dist/primitives/progress.js +29 -0
- package/dist/primitives/segmented-control.d.ts +40 -0
- package/dist/primitives/segmented-control.js +57 -0
- package/dist/primitives/select.d.ts +15 -0
- package/dist/primitives/select.js +74 -0
- package/dist/primitives/sidebar-drop-zone.d.ts +21 -0
- package/dist/primitives/sidebar-drop-zone.js +88 -0
- package/dist/primitives/skeleton.d.ts +10 -0
- package/dist/primitives/skeleton.js +50 -0
- package/dist/primitives/stat-card.d.ts +16 -0
- package/dist/primitives/stat-card.js +60 -0
- package/dist/primitives/status-pill.d.ts +44 -0
- package/dist/primitives/status-pill.js +119 -0
- package/dist/primitives/switch.d.ts +6 -0
- package/dist/primitives/switch.js +16 -0
- package/dist/primitives/table.d.ts +22 -0
- package/dist/primitives/table.js +63 -0
- package/dist/primitives/tabs.d.ts +13 -0
- package/dist/{tabs-BVHR6NkO.js → primitives/tabs.js} +3 -3
- package/dist/primitives/terminal-display.d.ts +25 -0
- package/dist/primitives/terminal-display.js +109 -0
- package/dist/primitives/text-shimmer.d.ts +16 -0
- package/dist/{text-shimmer-BUUo6I-g.js → primitives/text-shimmer.js} +2 -2
- package/dist/primitives/theme-toggle.d.ts +11 -0
- package/dist/primitives/theme-toggle.js +154 -0
- package/dist/primitives/toast.d.ts +29 -0
- package/dist/primitives/toast.js +142 -0
- package/dist/primitives/toolbar.d.ts +55 -0
- package/dist/primitives/toolbar.js +37 -0
- package/dist/primitives/upload-progress.d.ts +23 -0
- package/dist/primitives/upload-progress.js +72 -0
- package/dist/primitives.d.ts +35 -5
- package/dist/primitives.js +35 -9
- package/dist/{index-COgqsFAQ.d.ts → redaction/redacted-document.d.ts} +6 -7
- package/dist/{redaction-CGGB2ENG.js → redaction/redacted-document.js} +2 -2
- package/dist/redaction.d.ts +1 -1
- package/dist/redaction.js +1 -1
- package/dist/run/expandable-content.js +41 -0
- package/dist/run/expanded-tool-detail.d.ts +12 -0
- package/dist/run/expanded-tool-detail.js +163 -0
- package/dist/run/inline-thinking-item.d.ts +11 -0
- package/dist/run/inline-thinking-item.js +60 -0
- package/dist/run/inline-tool-item.d.ts +25 -0
- package/dist/run/inline-tool-item.js +63 -0
- package/dist/run/run-group.d.ts +26 -0
- package/dist/{run-group-BWGeesgs.js → run/run-group.js} +9 -60
- package/dist/run/run-item-primitives.d.ts +5 -0
- package/dist/{run-ZsIKPA6J.js → run/run-item-primitives.js} +2 -2
- package/dist/run/run-row-shell.d.ts +53 -0
- package/dist/run/run-row-shell.js +114 -0
- package/dist/{tool-call-feed-BeB1_4pX.d.ts → run/tool-call-feed.d.ts} +7 -12
- package/dist/{tool-call-feed-BpmMLpPX.js → run/tool-call-feed.js} +4 -4
- package/dist/run/tool-call-step.d.ts +5 -0
- package/dist/run/tool-call-step.js +82 -0
- package/dist/run.d.ts +8 -2
- package/dist/run.js +7 -4
- package/dist/sdk-hooks.d.ts +8 -1
- package/dist/sdk-hooks.js +8 -2
- package/dist/stores/active-sessions-store.d.ts +83 -0
- package/dist/{active-sessions-store-Gt1R0rFG.js → stores/active-sessions-store.js} +1 -1
- package/dist/stores/chat-store.d.ts +14 -0
- package/dist/stores/chat-store.js +33 -0
- package/dist/stores.d.ts +3 -16
- package/dist/stores.js +2 -33
- package/dist/tool-previews/command-preview.d.ts +11 -0
- package/dist/tool-previews/command-preview.js +91 -0
- package/dist/tool-previews/diff-preview.d.ts +7 -0
- package/dist/tool-previews/diff-preview.js +76 -0
- package/dist/tool-previews/glob-results-preview.d.ts +7 -0
- package/dist/tool-previews/glob-results-preview.js +53 -0
- package/dist/tool-previews/grep-results-preview.d.ts +7 -0
- package/dist/tool-previews/grep-results-preview.js +99 -0
- package/dist/tool-previews/preview-primitives.js +58 -0
- package/dist/tool-previews/question-preview.d.ts +7 -0
- package/dist/tool-previews/question-preview.js +63 -0
- package/dist/tool-previews/web-search-preview.d.ts +7 -0
- package/dist/tool-previews/web-search-preview.js +78 -0
- package/dist/tool-previews/write-file-preview.d.ts +11 -0
- package/dist/tool-previews/write-file-preview.js +52 -0
- package/dist/tool-previews.d.ts +7 -1
- package/dist/tool-previews.js +7 -1
- package/dist/{branding-26d5ARIr.d.ts → types/branding.d.ts} +2 -3
- package/dist/{message-vhUI0c5G.d.ts → types/message.d.ts} +2 -3
- package/dist/{parts-5mhy9yJ3.d.ts → types/parts.d.ts} +8 -9
- package/dist/{run-OyWmIDqu.d.ts → types/run.d.ts} +9 -10
- package/dist/{tool-display-DgmL5rIU.d.ts → types/tool-display.d.ts} +5 -6
- package/dist/types.d.ts +5 -5
- package/dist/utils/copy-text.d.ts +7 -0
- package/dist/{utils-Bl_CZ0Qp.js → utils/copy-text.js} +1 -11
- package/dist/utils/format.d.ts +22 -0
- package/dist/{format-DfcLWqMa.js → utils/format.js} +1 -1
- package/dist/utils/time-ago.d.ts +3 -0
- package/dist/utils/time-ago.js +11 -0
- package/dist/utils/tool-display.d.ts +16 -0
- package/dist/{tool-display-BAuAVa43.js → utils/tool-display.js} +1 -1
- package/dist/utils.d.ts +6 -1
- package/dist/utils.js +6 -5
- package/package.json +2 -2
- package/src/files/rich-file-tree.stories.tsx +1 -1
- package/src/files/rich-file-tree.tsx +12 -9
- package/src/markdown/code-block.test.tsx +80 -2
- package/src/markdown/code-block.tsx +76 -80
- package/src/primitives/button.stories.tsx +37 -1
- package/src/primitives/button.test.tsx +230 -0
- package/src/primitives/button.tsx +62 -14
- package/src/primitives/card-anatomy.stories.tsx +51 -0
- package/src/primitives/card.test.tsx +71 -0
- package/src/primitives/card.tsx +25 -13
- package/src/primitives/heading.stories.tsx +35 -0
- package/src/primitives/heading.test.tsx +56 -0
- package/src/primitives/heading.tsx +76 -0
- package/src/primitives/index.ts +8 -0
- package/src/primitives/input.stories.tsx +28 -0
- package/src/primitives/input.test.tsx +161 -0
- package/src/primitives/input.tsx +34 -28
- package/src/primitives/page-header.stories.tsx +51 -0
- package/src/primitives/page-header.test.tsx +48 -0
- package/src/primitives/page-header.tsx +35 -35
- package/src/primitives/page-shell.stories.tsx +89 -0
- package/src/primitives/page-shell.test.tsx +24 -0
- package/src/primitives/page-shell.tsx +26 -0
- package/src/primitives/table-wrapper.stories.tsx +54 -0
- package/src/primitives/table.test.tsx +72 -0
- package/src/primitives/table.tsx +33 -12
- package/src/primitives/theme-toggle.test.tsx +167 -0
- package/src/primitives/theme-toggle.tsx +111 -39
- package/dist/active-sessions-store-nl70ZnMC.d.ts +0 -84
- package/dist/button-C87nEe2R.js +0 -78
- package/dist/chat-DDvF2oGQ.js +0 -672
- package/dist/diff-preview-BcK3nRXb.js +0 -533
- package/dist/document-editor-pane-BilJfiCo.d.ts +0 -136
- package/dist/document-editor-pane-POsq5Q0o.js +0 -1279
- package/dist/files-hywocVZz.js +0 -753
- package/dist/index-2UntyjBb.d.ts +0 -52
- package/dist/index-BgUHilZm.d.ts +0 -108
- package/dist/index-CJ8wRfiC.d.ts +0 -236
- package/dist/index-CPDET_s1.d.ts +0 -549
- package/dist/index-Cg6gYAqh.d.ts +0 -73
- package/dist/index-Cs7j8V7l.d.ts +0 -203
- package/dist/index-DjKxO357.d.ts +0 -128
- package/dist/primitives-Bx59fT-U.js +0 -1239
- package/dist/table-Bss73DhB.js +0 -146
- package/dist/tool-call-step-DNebqhmX.js +0 -434
- package/dist/use-auto-scroll-C5Pjeqon.js +0 -153
- package/dist/use-sse-stream-DjqRuDft.js +0 -884
- package/dist/use-sse-stream-Dm6R5YWz.d.ts +0 -282
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Slot } from "@radix-ui/react-slot";
|
|
2
|
-
import { cva
|
|
2
|
+
import { cva } from "class-variance-authority";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { focusRing } from "../lib/focus";
|
|
5
5
|
import { cn } from "../lib/utils";
|
|
6
6
|
|
|
7
7
|
const buttonVariants = cva(
|
|
8
|
-
`inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium text-sm transition-all ${focusRing} disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`,
|
|
8
|
+
`inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium text-sm transition-all ${focusRing} disabled:pointer-events-none disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 motion-reduce:transition-none motion-reduce:active:scale-100 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`,
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
|
@@ -47,6 +47,36 @@ export interface ButtonProps
|
|
|
47
47
|
children?: React.ReactNode;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
type ButtonChildProps = React.HTMLAttributes<HTMLElement> & {
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
href?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function preventActivation(event: React.SyntheticEvent) {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
event.stopPropagation();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function disabledHandlers(props: React.HTMLAttributes<HTMLElement>) {
|
|
61
|
+
const guardKey = (handler?: React.KeyboardEventHandler<HTMLElement>) =>
|
|
62
|
+
(event: React.KeyboardEvent<HTMLElement>) => {
|
|
63
|
+
if (event.key === "Enter" || event.key === " ") preventActivation(event);
|
|
64
|
+
else handler?.(event);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
onClickCapture: preventActivation,
|
|
69
|
+
onAuxClickCapture: preventActivation,
|
|
70
|
+
onDoubleClickCapture: preventActivation,
|
|
71
|
+
onPointerDownCapture: preventActivation,
|
|
72
|
+
onPointerUpCapture: preventActivation,
|
|
73
|
+
onMouseDownCapture: preventActivation,
|
|
74
|
+
onMouseUpCapture: preventActivation,
|
|
75
|
+
onKeyDownCapture: guardKey(props.onKeyDownCapture),
|
|
76
|
+
onKeyUpCapture: guardKey(props.onKeyUpCapture),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
50
80
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
51
81
|
(
|
|
52
82
|
{
|
|
@@ -61,36 +91,54 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
61
91
|
},
|
|
62
92
|
ref,
|
|
63
93
|
) => {
|
|
64
|
-
const Comp = asChild ? Slot : "button";
|
|
65
|
-
|
|
66
|
-
// When using asChild, we can't add the loading spinner as it would create multiple children
|
|
67
|
-
// which breaks Slot's single-child requirement
|
|
68
94
|
if (asChild) {
|
|
95
|
+
const child = React.isValidElement<ButtonChildProps>(children) ? children : null;
|
|
96
|
+
const isDisabled = disabled || loading || child?.props.disabled;
|
|
97
|
+
|
|
98
|
+
// Slot composes child handlers first. Guard BOTH capture paths so neither
|
|
99
|
+
// child nor Button activation handlers run, without changing enabled order.
|
|
100
|
+
// Keep slotted content intact: its action name must survive loading too.
|
|
69
101
|
return (
|
|
70
|
-
<
|
|
102
|
+
<Slot
|
|
71
103
|
className={cn(buttonVariants({ variant, size, className }))}
|
|
72
104
|
ref={ref}
|
|
73
105
|
{...props}
|
|
106
|
+
{...(isDisabled ? disabledHandlers(props) : {})}
|
|
74
107
|
>
|
|
75
|
-
{
|
|
76
|
-
|
|
108
|
+
{child ? React.cloneElement(child, {
|
|
109
|
+
...(loading ? { "aria-busy": true } : {}),
|
|
110
|
+
...(isDisabled ? {
|
|
111
|
+
"aria-disabled": true,
|
|
112
|
+
...(child.type === "button" ? { disabled: true } : { tabIndex: -1 }),
|
|
113
|
+
// Removing href also prevents native context-menu / middle-click
|
|
114
|
+
// navigation. Keep link semantics while it is unavailable.
|
|
115
|
+
...(child.type === "a" ? {
|
|
116
|
+
href: undefined,
|
|
117
|
+
role: child.props.role ?? props.role ?? "link",
|
|
118
|
+
} : {}),
|
|
119
|
+
...disabledHandlers(child.props),
|
|
120
|
+
} : {}),
|
|
121
|
+
}) : children}
|
|
122
|
+
</Slot>
|
|
77
123
|
);
|
|
78
124
|
}
|
|
79
125
|
|
|
80
126
|
return (
|
|
81
|
-
<
|
|
127
|
+
<button
|
|
82
128
|
className={cn(buttonVariants({ variant, size, className }))}
|
|
83
129
|
ref={ref}
|
|
84
|
-
disabled={disabled || loading}
|
|
85
130
|
{...props}
|
|
131
|
+
disabled={disabled || loading}
|
|
132
|
+
aria-busy={loading || props["aria-busy"]}
|
|
86
133
|
>
|
|
87
134
|
{loading && (
|
|
88
135
|
<svg
|
|
89
|
-
className="mr-2 -ml-1 h-4 w-4 animate-spin"
|
|
136
|
+
className="mr-2 -ml-1 h-4 w-4 animate-spin motion-reduce:animate-none"
|
|
137
|
+
aria-hidden="true"
|
|
138
|
+
focusable="false"
|
|
90
139
|
fill="none"
|
|
91
140
|
viewBox="0 0 24 24"
|
|
92
141
|
>
|
|
93
|
-
<title>Loading spinner</title>
|
|
94
142
|
<circle
|
|
95
143
|
className="opacity-25"
|
|
96
144
|
cx="12"
|
|
@@ -107,7 +155,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
107
155
|
</svg>
|
|
108
156
|
)}
|
|
109
157
|
{children}
|
|
110
|
-
</
|
|
158
|
+
</button>
|
|
111
159
|
);
|
|
112
160
|
},
|
|
113
161
|
);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Button } from "./button";
|
|
4
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./card";
|
|
5
|
+
import { Heading } from "./heading";
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "Primitives/Card/Anatomy",
|
|
9
|
+
component: Card,
|
|
10
|
+
parameters: { layout: "padded" },
|
|
11
|
+
} satisfies Meta<typeof Card>;
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof meta>;
|
|
14
|
+
|
|
15
|
+
export const AlignedParts: Story = {
|
|
16
|
+
render: () => <div className="max-w-lg space-y-6">
|
|
17
|
+
<Heading variant="page">Activity details</Heading>
|
|
18
|
+
<Card data-anatomy="full">
|
|
19
|
+
<CardHeader data-part="header"><CardTitle as="h2">Latest result</CardTitle><CardDescription>One heading level below the page.</CardDescription></CardHeader>
|
|
20
|
+
<CardContent data-part="content">The header, body and footer share one horizontal gutter.</CardContent>
|
|
21
|
+
<CardFooter data-part="footer"><Button type="button" variant="outline">View result</Button></CardFooter>
|
|
22
|
+
</Card>
|
|
23
|
+
<Card data-anatomy="standalone"><CardContent data-part="content">Content-only cards retain their top padding.</CardContent></Card>
|
|
24
|
+
</div>,
|
|
25
|
+
play: ({ canvasElement }) => {
|
|
26
|
+
const parts = Array.from(canvasElement.querySelectorAll<HTMLElement>('[data-anatomy="full"] > [data-part]'));
|
|
27
|
+
const styles = parts.map((part) => getComputedStyle(part));
|
|
28
|
+
const gutter = styles[0]?.paddingLeft;
|
|
29
|
+
if (!gutter || parseFloat(gutter) <= 0 || styles.some((style) => style.paddingLeft !== gutter || style.paddingRight !== gutter)) {
|
|
30
|
+
throw new Error("Card parts do not share a nonzero horizontal gutter");
|
|
31
|
+
}
|
|
32
|
+
if (parseFloat(styles[1]!.paddingTop) !== 0 || parseFloat(styles[2]!.paddingTop) !== 0) {
|
|
33
|
+
throw new Error("Stacked card parts duplicate top padding");
|
|
34
|
+
}
|
|
35
|
+
const standalone = canvasElement.querySelector<HTMLElement>('[data-anatomy="standalone"] > [data-part]')!;
|
|
36
|
+
const style = getComputedStyle(standalone);
|
|
37
|
+
if (parseFloat(style.paddingTop) <= 0 || style.paddingTop !== style.paddingBottom) {
|
|
38
|
+
throw new Error("Content-only card lost its top padding");
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function ActionCard() {
|
|
44
|
+
const [count, setCount] = useState(0);
|
|
45
|
+
return <Card hover className="max-w-sm">
|
|
46
|
+
<CardHeader><CardTitle as="h2">Decorative hover, native action</CardTitle><CardDescription>Tab to the button; Enter and Space both work.</CardDescription></CardHeader>
|
|
47
|
+
<CardContent><p role="status">Opened {count} times</p></CardContent>
|
|
48
|
+
<CardFooter><Button type="button" onClick={() => setCount((value) => value + 1)}>Open details</Button></CardFooter>
|
|
49
|
+
</Card>;
|
|
50
|
+
}
|
|
51
|
+
export const KeyboardAction: Story = { render: () => <ActionCard /> };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps } from "./card";
|
|
6
|
+
|
|
7
|
+
describe("Card anatomy", () => {
|
|
8
|
+
it("keeps the existing default title tag and heading ref", () => {
|
|
9
|
+
const ref = createRef<HTMLHeadingElement>();
|
|
10
|
+
render(<CardTitle ref={ref} id="card-title">Details</CardTitle>);
|
|
11
|
+
expect(ref.current).toBe(screen.getByRole("heading", { level: 3 }));
|
|
12
|
+
expect(ref.current).toHaveAttribute("id", "card-title");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it.each([1, 2, 3, 4, 5, 6] as const)("allows an h%i without changing its visual treatment", (level) => {
|
|
16
|
+
const { rerender } = render(<CardTitle>Details</CardTitle>);
|
|
17
|
+
const classes = screen.getByRole("heading").className;
|
|
18
|
+
rerender(<CardTitle as={`h${level}` as CardTitleProps["as"]}>Details</CardTitle>);
|
|
19
|
+
expect(screen.getByRole("heading", { level }).className).toBe(classes);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("preserves native attributes and caller classes", () => {
|
|
23
|
+
render(<CardTitle as="h2" role="presentation" className="text-xl" data-kind="summary">Summary</CardTitle>);
|
|
24
|
+
const title = screen.getByText("Summary");
|
|
25
|
+
expect(title).toHaveAttribute("role", "presentation");
|
|
26
|
+
expect(title).toHaveAttribute("data-kind", "summary");
|
|
27
|
+
expect(title).toHaveClass("text-xl");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("uses the same gutter on every part and only removes subsequent top padding", () => {
|
|
31
|
+
render(<Card>
|
|
32
|
+
<CardHeader data-testid="header"><CardTitle>Details</CardTitle><CardDescription>Prose</CardDescription></CardHeader>
|
|
33
|
+
<CardContent data-testid="content">Body</CardContent>
|
|
34
|
+
<CardFooter data-testid="footer">Footer</CardFooter>
|
|
35
|
+
</Card>);
|
|
36
|
+
for (const part of ["header", "content", "footer"]) {
|
|
37
|
+
expect(screen.getByTestId(part)).toHaveClass("p-4");
|
|
38
|
+
expect(screen.getByTestId(part)).not.toHaveClass("p-6");
|
|
39
|
+
}
|
|
40
|
+
expect(screen.getByTestId("content")).toHaveClass("[&:not(:first-child)]:pt-0");
|
|
41
|
+
expect(screen.getByTestId("footer")).toHaveClass("[&:not(:first-child)]:pt-0");
|
|
42
|
+
// These are class/DOM contracts, not a browser-computed padding assertion.
|
|
43
|
+
expect(screen.getByText("Prose").tagName).toBe("P");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("does not strip the top padding unconditionally from content-only cards", () => {
|
|
47
|
+
const ref = createRef<HTMLDivElement>();
|
|
48
|
+
render(<Card><CardContent ref={ref}>Standalone</CardContent></Card>);
|
|
49
|
+
expect(ref.current).toHaveClass("p-4");
|
|
50
|
+
expect(ref.current).not.toHaveClass("pt-0");
|
|
51
|
+
expect(ref.current?.matches(":first-child")).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("keeps hover decorative and leaves keyboard activation to a real button", async () => {
|
|
55
|
+
const user = userEvent.setup();
|
|
56
|
+
const activate = vi.fn();
|
|
57
|
+
const ref = createRef<HTMLDivElement>();
|
|
58
|
+
render(<Card ref={ref} hover>
|
|
59
|
+
<CardContent><button type="button" onClick={activate}>Open details</button></CardContent>
|
|
60
|
+
</Card>);
|
|
61
|
+
expect(ref.current?.tagName).toBe("DIV");
|
|
62
|
+
expect(ref.current).not.toHaveAttribute("tabindex");
|
|
63
|
+
expect(ref.current).not.toHaveAttribute("role");
|
|
64
|
+
expect(ref.current).not.toHaveClass("cursor-pointer");
|
|
65
|
+
await user.tab();
|
|
66
|
+
expect(screen.getByRole("button", { name: "Open details" })).toHaveFocus();
|
|
67
|
+
await user.keyboard("{Enter}");
|
|
68
|
+
await user.keyboard(" ");
|
|
69
|
+
expect(activate).toHaveBeenCalledTimes(2);
|
|
70
|
+
});
|
|
71
|
+
});
|
package/src/primitives/card.tsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { cn } from "../lib/utils";
|
|
3
|
+
import { Heading } from "./heading";
|
|
3
4
|
|
|
4
5
|
const Card = React.forwardRef<
|
|
5
6
|
HTMLDivElement,
|
|
6
7
|
React.HTMLAttributes<HTMLDivElement> & {
|
|
7
8
|
variant?: "default" | "glass" | "sandbox" | "elevated";
|
|
9
|
+
/** Decorative border feedback only. Put actions in a native button or link. */
|
|
8
10
|
hover?: boolean;
|
|
9
11
|
}
|
|
10
12
|
>(({ className, variant = "default", hover = false, ...props }, ref) => {
|
|
@@ -22,7 +24,7 @@ const Card = React.forwardRef<
|
|
|
22
24
|
"rounded-[var(--radius-lg)] border text-card-foreground transition-[border-color,box-shadow]",
|
|
23
25
|
"duration-[var(--transition-default)]",
|
|
24
26
|
variants[variant],
|
|
25
|
-
hover && "
|
|
27
|
+
hover && "hover:border-[var(--border-strong)]",
|
|
26
28
|
className,
|
|
27
29
|
)}
|
|
28
30
|
{...props}
|
|
@@ -43,16 +45,22 @@ const CardHeader = React.forwardRef<
|
|
|
43
45
|
));
|
|
44
46
|
CardHeader.displayName = "CardHeader";
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
export interface CardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
|
49
|
+
/** Heading level appropriate to the surrounding document; h3 remains the default. */
|
|
50
|
+
as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const CardTitle = React.forwardRef<HTMLHeadingElement, CardTitleProps>(
|
|
54
|
+
({ className, as = "h3", ...props }, ref) => (
|
|
55
|
+
<Heading
|
|
56
|
+
ref={ref}
|
|
57
|
+
variant="subsection"
|
|
58
|
+
as={as}
|
|
59
|
+
className={className}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
),
|
|
63
|
+
);
|
|
56
64
|
CardTitle.displayName = "CardTitle";
|
|
57
65
|
|
|
58
66
|
const CardDescription = React.forwardRef<
|
|
@@ -71,7 +79,11 @@ const CardContent = React.forwardRef<
|
|
|
71
79
|
HTMLDivElement,
|
|
72
80
|
React.HTMLAttributes<HTMLDivElement>
|
|
73
81
|
>(({ className, ...props }, ref) => (
|
|
74
|
-
<div
|
|
82
|
+
<div
|
|
83
|
+
ref={ref}
|
|
84
|
+
className={cn("p-4 [&:not(:first-child)]:pt-0", className)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
75
87
|
));
|
|
76
88
|
CardContent.displayName = "CardContent";
|
|
77
89
|
|
|
@@ -81,7 +93,7 @@ const CardFooter = React.forwardRef<
|
|
|
81
93
|
>(({ className, ...props }, ref) => (
|
|
82
94
|
<div
|
|
83
95
|
ref={ref}
|
|
84
|
-
className={cn("flex items-center p-
|
|
96
|
+
className={cn("flex flex-wrap items-center gap-2 p-4 [&:not(:first-child)]:pt-0", className)}
|
|
85
97
|
{...props}
|
|
86
98
|
/>
|
|
87
99
|
));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Heading } from "./heading";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Primitives/Heading",
|
|
6
|
+
component: Heading,
|
|
7
|
+
parameters: { layout: "padded" },
|
|
8
|
+
args: { variant: "page", children: "Activity across your workspace" },
|
|
9
|
+
argTypes: {
|
|
10
|
+
variant: { control: "select", options: ["display", "hero", "page", "section", "subsection", "eyebrow"] },
|
|
11
|
+
as: { control: "select", options: ["h1", "h2", "h3", "h4", "h5", "h6", "p"] },
|
|
12
|
+
},
|
|
13
|
+
} satisfies Meta<typeof Heading>;
|
|
14
|
+
export default meta;
|
|
15
|
+
type Story = StoryObj<typeof meta>;
|
|
16
|
+
|
|
17
|
+
export const Default: Story = {};
|
|
18
|
+
export const IndependentSemantics: Story = {
|
|
19
|
+
args: { variant: "page", as: "h2", children: "Page-sized title in a nested surface" },
|
|
20
|
+
};
|
|
21
|
+
export const Hierarchy: Story = {
|
|
22
|
+
render: () => <main className="space-y-6">
|
|
23
|
+
<Heading variant="eyebrow">Workspace</Heading>
|
|
24
|
+
<Heading variant="page">Activity</Heading>
|
|
25
|
+
<section className="space-y-4">
|
|
26
|
+
<Heading variant="section">Recent items</Heading>
|
|
27
|
+
<Heading variant="subsection">Build details</Heading>
|
|
28
|
+
<p className="text-muted-foreground">Eyebrows are labels, not extra headings.</p>
|
|
29
|
+
</section>
|
|
30
|
+
</main>,
|
|
31
|
+
play: ({ canvasElement }) => {
|
|
32
|
+
const tags = Array.from(canvasElement.querySelectorAll("h1,h2,h3,h4,h5,h6")).map((node) => node.tagName).join(",");
|
|
33
|
+
if (tags !== "H1,H2,H3") throw new Error(`Unexpected heading hierarchy: ${tags}`);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { Heading, type HeadingVariant } from "./heading";
|
|
5
|
+
|
|
6
|
+
const roles: [HeadingVariant, string][] = [
|
|
7
|
+
["display", "H1"], ["hero", "H2"], ["page", "H1"],
|
|
8
|
+
["section", "H2"], ["subsection", "H3"], ["eyebrow", "P"],
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
describe("Heading", () => {
|
|
12
|
+
it.each(roles)("gives %s its existing default element", (variant, tag) => {
|
|
13
|
+
render(<Heading variant={variant}>Title</Heading>);
|
|
14
|
+
expect(screen.getByText("Title").tagName).toBe(tag);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it.each(roles)("maps Sandbox's %s role without emitting an invalid ARIA role", (role, tag) => {
|
|
18
|
+
render(<Heading role={role}>Title</Heading>);
|
|
19
|
+
const title = screen.getByText("Title");
|
|
20
|
+
expect(title.tagName).toBe(tag);
|
|
21
|
+
expect(title).not.toHaveAttribute("role");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("changes semantics without changing the visual variant", () => {
|
|
25
|
+
const { rerender } = render(<Heading variant="page">Title</Heading>);
|
|
26
|
+
const classes = screen.getByRole("heading", { level: 1 }).className;
|
|
27
|
+
rerender(<Heading variant="page" as="h4">Title</Heading>);
|
|
28
|
+
expect(screen.getByRole("heading", { level: 4 }).className).toBe(classes);
|
|
29
|
+
expect(screen.queryByRole("heading", { level: 1 })).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("lets an explicit variant win over the existing visual-role input", () => {
|
|
33
|
+
render(<Heading variant="section" role="page">Title</Heading>);
|
|
34
|
+
expect(screen.getByRole("heading", { level: 2 })).not.toHaveAttribute("role");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("forwards native roles, refs and HTML attributes", () => {
|
|
38
|
+
const ref = createRef<HTMLElement>();
|
|
39
|
+
render(<Heading ref={ref} variant="eyebrow" as="div" role="note" id="scope" className="custom" data-scope="all">Scope</Heading>);
|
|
40
|
+
const note = screen.getByRole("note");
|
|
41
|
+
expect(ref.current).toBe(note);
|
|
42
|
+
expect(note).toHaveAttribute("id", "scope");
|
|
43
|
+
expect(note).toHaveAttribute("data-scope", "all");
|
|
44
|
+
expect(note).toHaveClass("custom");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("keeps a page, section and nested title in document order", () => {
|
|
48
|
+
render(<main>
|
|
49
|
+
<Heading variant="page">Activity</Heading>
|
|
50
|
+
<Heading variant="eyebrow">Last day</Heading>
|
|
51
|
+
<Heading variant="section">Recent activity</Heading>
|
|
52
|
+
<Heading variant="subsection">Details</Heading>
|
|
53
|
+
</main>);
|
|
54
|
+
expect(screen.getAllByRole("heading").map((node) => node.tagName)).toEqual(["H1", "H2", "H3"]);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
/** Visual roles ported from sandbox-ui/src/primitives/heading.tsx. */
|
|
5
|
+
export type HeadingVariant =
|
|
6
|
+
| "display"
|
|
7
|
+
| "hero"
|
|
8
|
+
| "page"
|
|
9
|
+
| "section"
|
|
10
|
+
| "subsection"
|
|
11
|
+
| "eyebrow";
|
|
12
|
+
|
|
13
|
+
// Keep Brand's existing size, leading, tracking and weight tokens. The literal
|
|
14
|
+
// fallbacks are the existing Sandbox treatment, not a second type scale.
|
|
15
|
+
const VARIANT_CLASS: Record<HeadingVariant, string> = {
|
|
16
|
+
display:
|
|
17
|
+
"font-display font-bold text-foreground text-[length:var(--font-size-display,3rem)] leading-[var(--line-height-display,1.05)] tracking-[var(--tracking-tight,-0.02em)]",
|
|
18
|
+
hero: "font-display font-bold text-foreground text-[length:var(--font-size-hero,2.5rem)] leading-[1.1] tracking-[var(--tracking-tight,-0.02em)]",
|
|
19
|
+
page: "font-display font-semibold text-foreground text-[length:var(--font-size-3xl,1.875rem)] leading-[var(--line-height-heading,1.15)] tracking-[var(--tracking-tight,-0.02em)]",
|
|
20
|
+
section:
|
|
21
|
+
"font-display font-semibold text-foreground text-[length:var(--font-size-xl,1.25rem)] leading-[1.3] tracking-[var(--tracking-snug,-0.01em)]",
|
|
22
|
+
subsection:
|
|
23
|
+
"font-semibold text-foreground text-[length:var(--font-size-lg,1rem)] leading-[1.4]",
|
|
24
|
+
eyebrow:
|
|
25
|
+
"font-semibold uppercase text-muted-foreground text-[length:var(--font-size-sm,0.75rem)] leading-none tracking-[var(--tracking-wide,0.08em)]",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const DEFAULT_TAG: Record<HeadingVariant, React.ElementType> = {
|
|
29
|
+
display: "h1",
|
|
30
|
+
hero: "h2",
|
|
31
|
+
page: "h1",
|
|
32
|
+
section: "h2",
|
|
33
|
+
subsection: "h3",
|
|
34
|
+
eyebrow: "p",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export interface HeadingProps
|
|
38
|
+
extends Omit<React.HTMLAttributes<HTMLElement>, "role"> {
|
|
39
|
+
/** Visual treatment, independent of the rendered element. Defaults to section. */
|
|
40
|
+
variant?: HeadingVariant;
|
|
41
|
+
/** Semantic element; changing it does not change the visual variant. */
|
|
42
|
+
as?: React.ElementType;
|
|
43
|
+
/**
|
|
44
|
+
* Native ARIA role, or Sandbox's existing visual-role input. Only the six
|
|
45
|
+
* legacy visual values are consumed; they are never emitted as ARIA roles.
|
|
46
|
+
* Prefer variant for new code. An explicit variant wins over a legacy role.
|
|
47
|
+
*/
|
|
48
|
+
role?: React.AriaRole | HeadingVariant;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* One renderer for page, section and card titles. Use at most one default
|
|
53
|
+
* display/page h1 per page; choose `as` according to the document hierarchy.
|
|
54
|
+
*/
|
|
55
|
+
const Heading = React.forwardRef<HTMLElement, HeadingProps>(
|
|
56
|
+
({ variant, role, as, className, ...props }, ref) => {
|
|
57
|
+
const legacyVariant =
|
|
58
|
+
role && Object.prototype.hasOwnProperty.call(VARIANT_CLASS, role)
|
|
59
|
+
? (role as HeadingVariant)
|
|
60
|
+
: undefined;
|
|
61
|
+
const visualVariant = variant ?? legacyVariant ?? "section";
|
|
62
|
+
const Tag = as ?? DEFAULT_TAG[visualVariant];
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Tag
|
|
66
|
+
ref={ref}
|
|
67
|
+
className={cn("[overflow-wrap:anywhere]", VARIANT_CLASS[visualVariant], className)}
|
|
68
|
+
role={legacyVariant ? undefined : role}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
Heading.displayName = "Heading";
|
|
75
|
+
|
|
76
|
+
export { Heading };
|
package/src/primitives/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export {
|
|
|
9
9
|
CardDescription,
|
|
10
10
|
CardContent,
|
|
11
11
|
} from "./card";
|
|
12
|
+
export type { CardTitleProps } from "./card";
|
|
12
13
|
|
|
13
14
|
export {
|
|
14
15
|
Dialog,
|
|
@@ -72,6 +73,7 @@ export {
|
|
|
72
73
|
TableCell,
|
|
73
74
|
TableCaption,
|
|
74
75
|
} from "./table";
|
|
76
|
+
export type { TableProps } from "./table";
|
|
75
77
|
|
|
76
78
|
export { Tabs, TabsList, TabsTrigger, TabsContent } from "./tabs";
|
|
77
79
|
|
|
@@ -122,6 +124,12 @@ export type { CodeBlockProps, InlineCodeProps } from "./code-block";
|
|
|
122
124
|
|
|
123
125
|
export { ThemeToggle, useTheme } from "./theme-toggle";
|
|
124
126
|
|
|
127
|
+
export { Heading } from "./heading";
|
|
128
|
+
export type { HeadingProps, HeadingVariant } from "./heading";
|
|
129
|
+
|
|
130
|
+
export { PageShell } from "./page-shell";
|
|
131
|
+
export type { PageShellProps } from "./page-shell";
|
|
132
|
+
|
|
125
133
|
export { PageHeader } from "./page-header";
|
|
126
134
|
export type { PageHeaderProps } from "./page-header";
|
|
127
135
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { useId } from 'react'
|
|
2
3
|
import { Input, Textarea } from './input'
|
|
4
|
+
import { Label } from './label'
|
|
3
5
|
|
|
4
6
|
const meta: Meta<typeof Input> = {
|
|
5
7
|
title: 'Primitives/Input',
|
|
@@ -111,3 +113,29 @@ export const Overview: Story = {
|
|
|
111
113
|
</div>
|
|
112
114
|
),
|
|
113
115
|
}
|
|
116
|
+
|
|
117
|
+
export const FieldRelationships: Story = {
|
|
118
|
+
parameters: {
|
|
119
|
+
docs: {
|
|
120
|
+
description: {
|
|
121
|
+
story: 'Repeated labels must focus their own fields. Inspect accessible descriptions: caller help, hints, and errors coexist. The final field uses an explicit ID and the standalone shared Label.',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
render: function FieldRelationships() {
|
|
126
|
+
const id = useId()
|
|
127
|
+
const descriptionId = `${id}-instructions`
|
|
128
|
+
return (
|
|
129
|
+
<div className="flex w-96 flex-col gap-4">
|
|
130
|
+
<p id={descriptionId}>Shared instructions: choose a descriptive name.</p>
|
|
131
|
+
<Input label="Name" hint="First name." aria-describedby={descriptionId} />
|
|
132
|
+
<Input label="Name" hint="Second name." error="A name is required." aria-describedby={descriptionId} />
|
|
133
|
+
<Textarea label="Name" hint="Explain the name." error="More detail is needed." aria-describedby={descriptionId} />
|
|
134
|
+
<div className="space-y-1.5">
|
|
135
|
+
<Label htmlFor={`${id}-explicit`}>External label</Label>
|
|
136
|
+
<Input id={`${id}-explicit`} hint="Explicit field ID is preserved." />
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
},
|
|
141
|
+
}
|