@fragments-sdk/ui 0.6.5 → 0.7.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/README.md +3 -3
- package/fragments.json +1 -1
- package/package.json +16 -3
- package/src/blocks/AIChat.block.ts +266 -0
- package/src/blocks/AccountSettings.block.ts +47 -0
- package/src/blocks/ActivityFeed.block.ts +38 -0
- package/src/blocks/AppShell.block.ts +175 -0
- package/src/blocks/CTABanner.block.ts +24 -0
- package/src/blocks/CardGrid.block.ts +22 -0
- package/src/blocks/ChatInterface.block.ts +87 -0
- package/src/blocks/ChatMessages.block.ts +35 -0
- package/src/blocks/CheckoutForm.block.ts +62 -0
- package/src/blocks/CodeExamples.block.ts +66 -0
- package/src/blocks/ConfirmDialog.block.ts +19 -0
- package/src/blocks/ContactForm.block.ts +28 -0
- package/src/blocks/ConversationWithHistory.block.ts +45 -0
- package/src/blocks/DashboardLayout.block.ts +73 -0
- package/src/blocks/DashboardNav.block.ts +183 -0
- package/src/blocks/DataTable.block.ts +29 -0
- package/src/blocks/EmptyState.block.ts +21 -0
- package/src/blocks/FAQSection.block.ts +35 -0
- package/src/blocks/FeatureGrid.block.ts +33 -0
- package/src/blocks/ForgotPassword.block.ts +26 -0
- package/src/blocks/FormLayout.block.ts +31 -0
- package/src/blocks/HeroSection.block.ts +31 -0
- package/src/blocks/InsetDashboardLayout.block.ts +79 -0
- package/src/blocks/LoginForm.block.ts +26 -0
- package/src/blocks/MetricDashboard.block.ts +38 -0
- package/src/blocks/NewsletterSignup.block.ts +26 -0
- package/src/blocks/NotificationList.block.ts +39 -0
- package/src/blocks/NotificationPreferences.block.ts +40 -0
- package/src/blocks/OrderSummary.block.ts +52 -0
- package/src/blocks/PricingComparison.block.ts +44 -0
- package/src/blocks/ProductCard.block.ts +33 -0
- package/src/blocks/ProfileEditForm.block.ts +51 -0
- package/src/blocks/RegistrationForm.block.ts +38 -0
- package/src/blocks/SearchResults.block.ts +39 -0
- package/src/blocks/SettingsPage.block.ts +58 -0
- package/src/blocks/SettingsPanel.block.ts +35 -0
- package/src/blocks/ShoppingCart.block.ts +46 -0
- package/src/blocks/StatsCard.block.ts +26 -0
- package/src/blocks/StreamingMessage.block.ts +24 -0
- package/src/blocks/TestimonialCard.block.ts +27 -0
- package/src/blocks/ThinkingStates.block.ts +48 -0
- package/src/blocks/UserProfileCard.block.ts +29 -0
- package/src/components/Box/Box.fragment.tsx +110 -0
- package/src/components/Box/Box.module.scss +39 -0
- package/src/components/Box/index.tsx +68 -1
- package/src/components/Breadcrumbs/Breadcrumbs.fragment.tsx +162 -0
- package/src/components/Breadcrumbs/Breadcrumbs.module.scss +120 -0
- package/src/components/Breadcrumbs/index.tsx +202 -0
- package/src/components/Chip/Chip.fragment.tsx +175 -0
- package/src/components/Chip/Chip.module.scss +174 -0
- package/src/components/Chip/index.tsx +151 -0
- package/src/components/Markdown/Markdown.fragment.tsx +226 -0
- package/src/components/Markdown/Markdown.module.scss +219 -0
- package/src/components/Markdown/index.tsx +106 -0
- package/src/components/Message/index.tsx +9 -2
- package/src/components/Prompt/index.tsx +2 -1
- package/src/components/Stack/Stack.fragment.tsx +16 -0
- package/src/components/Stack/Stack.module.scss +16 -0
- package/src/components/Stack/index.tsx +35 -1
- package/src/components/Table/Table.fragment.tsx +34 -0
- package/src/components/Table/Table.module.scss +35 -5
- package/src/components/Table/index.tsx +8 -2
- package/src/index.ts +17 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
3
|
+
|
|
4
|
+
// ============================================
|
|
5
|
+
// Markdown Prose Container
|
|
6
|
+
// ============================================
|
|
7
|
+
|
|
8
|
+
.markdown {
|
|
9
|
+
@include text-base;
|
|
10
|
+
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
11
|
+
word-wrap: break-word;
|
|
12
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
13
|
+
|
|
14
|
+
// ----------------------------------------
|
|
15
|
+
// Headings
|
|
16
|
+
// ----------------------------------------
|
|
17
|
+
|
|
18
|
+
h1,
|
|
19
|
+
h2,
|
|
20
|
+
h3,
|
|
21
|
+
h4,
|
|
22
|
+
h5,
|
|
23
|
+
h6 {
|
|
24
|
+
margin-top: var(--fui-space-4, $fui-space-4);
|
|
25
|
+
margin-bottom: var(--fui-space-2, $fui-space-2);
|
|
26
|
+
font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold);
|
|
27
|
+
line-height: var(--fui-line-height-tight, $fui-line-height-tight);
|
|
28
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
29
|
+
|
|
30
|
+
&:first-child {
|
|
31
|
+
margin-top: 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
h1 {
|
|
36
|
+
font-size: var(--fui-font-size-2xl, $fui-font-size-2xl);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
h2 {
|
|
40
|
+
font-size: var(--fui-font-size-xl, $fui-font-size-xl);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
h3 {
|
|
44
|
+
font-size: var(--fui-font-size-lg, $fui-font-size-lg);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
h4 {
|
|
48
|
+
font-size: var(--fui-font-size-base, $fui-font-size-base);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
h5,
|
|
52
|
+
h6 {
|
|
53
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ----------------------------------------
|
|
57
|
+
// Paragraphs
|
|
58
|
+
// ----------------------------------------
|
|
59
|
+
|
|
60
|
+
p {
|
|
61
|
+
margin: 0;
|
|
62
|
+
|
|
63
|
+
& + p {
|
|
64
|
+
margin-top: var(--fui-space-2, $fui-space-2);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ----------------------------------------
|
|
69
|
+
// Links
|
|
70
|
+
// ----------------------------------------
|
|
71
|
+
|
|
72
|
+
a {
|
|
73
|
+
color: var(--fui-color-accent, $fui-color-accent);
|
|
74
|
+
text-decoration: underline;
|
|
75
|
+
text-underline-offset: 2px;
|
|
76
|
+
|
|
77
|
+
&:hover {
|
|
78
|
+
opacity: 0.8;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ----------------------------------------
|
|
83
|
+
// Lists
|
|
84
|
+
// ----------------------------------------
|
|
85
|
+
|
|
86
|
+
ul,
|
|
87
|
+
ol {
|
|
88
|
+
margin: var(--fui-space-2, $fui-space-2) 0;
|
|
89
|
+
padding-left: var(--fui-space-4, $fui-space-4);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
li {
|
|
93
|
+
margin-bottom: var(--fui-space-1, $fui-space-1);
|
|
94
|
+
|
|
95
|
+
&:last-child {
|
|
96
|
+
margin-bottom: 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Nested lists
|
|
101
|
+
li > ul,
|
|
102
|
+
li > ol {
|
|
103
|
+
margin-top: var(--fui-space-1, $fui-space-1);
|
|
104
|
+
margin-bottom: 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ----------------------------------------
|
|
108
|
+
// Inline Code
|
|
109
|
+
// ----------------------------------------
|
|
110
|
+
|
|
111
|
+
code {
|
|
112
|
+
font-family: var(--fui-font-mono, $fui-font-mono);
|
|
113
|
+
font-size: 0.9em;
|
|
114
|
+
padding: 0.125em 0.375em;
|
|
115
|
+
border-radius: var(--fui-radius-sm, $fui-radius-sm);
|
|
116
|
+
background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ----------------------------------------
|
|
120
|
+
// Code Blocks
|
|
121
|
+
// ----------------------------------------
|
|
122
|
+
|
|
123
|
+
pre {
|
|
124
|
+
margin: var(--fui-space-2, $fui-space-2) 0;
|
|
125
|
+
padding: var(--fui-space-2, $fui-space-2);
|
|
126
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
127
|
+
background-color: var(--fui-code-bg, $fui-code-bg);
|
|
128
|
+
overflow-x: auto;
|
|
129
|
+
|
|
130
|
+
code {
|
|
131
|
+
padding: 0;
|
|
132
|
+
background: none;
|
|
133
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ----------------------------------------
|
|
138
|
+
// Blockquotes
|
|
139
|
+
// ----------------------------------------
|
|
140
|
+
|
|
141
|
+
blockquote {
|
|
142
|
+
margin: var(--fui-space-2, $fui-space-2) 0;
|
|
143
|
+
padding: var(--fui-space-1, $fui-space-1) var(--fui-space-3, $fui-space-3);
|
|
144
|
+
border-left: 3px solid var(--fui-border, $fui-border);
|
|
145
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
146
|
+
|
|
147
|
+
p {
|
|
148
|
+
margin: 0;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ----------------------------------------
|
|
153
|
+
// Horizontal Rule
|
|
154
|
+
// ----------------------------------------
|
|
155
|
+
|
|
156
|
+
hr {
|
|
157
|
+
margin: var(--fui-space-4, $fui-space-4) 0;
|
|
158
|
+
border: none;
|
|
159
|
+
border-top: 1px solid var(--fui-border, $fui-border);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ----------------------------------------
|
|
163
|
+
// Tables (GFM)
|
|
164
|
+
// ----------------------------------------
|
|
165
|
+
|
|
166
|
+
table {
|
|
167
|
+
width: 100%;
|
|
168
|
+
margin: var(--fui-space-2, $fui-space-2) 0;
|
|
169
|
+
border-collapse: collapse;
|
|
170
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
th,
|
|
174
|
+
td {
|
|
175
|
+
padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
|
|
176
|
+
border: 1px solid var(--fui-border, $fui-border);
|
|
177
|
+
text-align: left;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
th {
|
|
181
|
+
font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold);
|
|
182
|
+
background-color: var(--fui-bg-secondary, $fui-bg-secondary);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ----------------------------------------
|
|
186
|
+
// Images
|
|
187
|
+
// ----------------------------------------
|
|
188
|
+
|
|
189
|
+
img {
|
|
190
|
+
max-width: 100%;
|
|
191
|
+
height: auto;
|
|
192
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ----------------------------------------
|
|
196
|
+
// Task Lists (GFM)
|
|
197
|
+
// ----------------------------------------
|
|
198
|
+
|
|
199
|
+
input[type='checkbox'] {
|
|
200
|
+
margin-right: var(--fui-space-1, $fui-space-1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ----------------------------------------
|
|
204
|
+
// Strong & Emphasis
|
|
205
|
+
// ----------------------------------------
|
|
206
|
+
|
|
207
|
+
strong {
|
|
208
|
+
font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Remove top margin from first child and bottom margin from last child
|
|
212
|
+
> *:first-child {
|
|
213
|
+
margin-top: 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
> *:last-child {
|
|
217
|
+
margin-bottom: 0;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import styles from './Markdown.module.scss';
|
|
5
|
+
import '../../styles/globals.scss';
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================
|
|
10
|
+
|
|
11
|
+
export interface MarkdownProps {
|
|
12
|
+
/** Markdown string to render */
|
|
13
|
+
content: string;
|
|
14
|
+
/** Override map for markdown element components */
|
|
15
|
+
components?: Record<string, React.ComponentType<React.HTMLAttributes<HTMLElement>>>;
|
|
16
|
+
/** Additional class name */
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ============================================
|
|
21
|
+
// Lazy-loaded react-markdown
|
|
22
|
+
// ============================================
|
|
23
|
+
|
|
24
|
+
type ReactMarkdownType = React.ComponentType<{
|
|
25
|
+
children: string;
|
|
26
|
+
remarkPlugins?: unknown[];
|
|
27
|
+
components?: Record<string, React.ComponentType<unknown>>;
|
|
28
|
+
}>;
|
|
29
|
+
|
|
30
|
+
let ReactMarkdown: ReactMarkdownType | null = null;
|
|
31
|
+
let remarkGfm: unknown = null;
|
|
32
|
+
let loadAttempted = false;
|
|
33
|
+
let loadFailed = false;
|
|
34
|
+
|
|
35
|
+
function loadDeps() {
|
|
36
|
+
if (loadAttempted) return;
|
|
37
|
+
loadAttempted = true;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
|
+
ReactMarkdown = require('react-markdown').default || require('react-markdown');
|
|
42
|
+
} catch {
|
|
43
|
+
loadFailed = true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
48
|
+
remarkGfm = require('remark-gfm').default || require('remark-gfm');
|
|
49
|
+
} catch {
|
|
50
|
+
// remark-gfm is optional; markdown still works without it
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================
|
|
55
|
+
// Fallback renderer (plain text with paragraphs)
|
|
56
|
+
// ============================================
|
|
57
|
+
|
|
58
|
+
function FallbackRenderer({ content, className }: { content: string; className?: string }) {
|
|
59
|
+
const paragraphs = content.split(/\n{2,}/);
|
|
60
|
+
return (
|
|
61
|
+
<div className={className}>
|
|
62
|
+
{paragraphs.map((p, i) => (
|
|
63
|
+
<p key={i}>{p}</p>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================
|
|
70
|
+
// Component
|
|
71
|
+
// ============================================
|
|
72
|
+
|
|
73
|
+
export const Markdown = React.forwardRef<HTMLDivElement, MarkdownProps>(
|
|
74
|
+
function Markdown({ content, components: componentOverrides, className }, ref) {
|
|
75
|
+
loadDeps();
|
|
76
|
+
|
|
77
|
+
const classes = [styles.markdown, className].filter(Boolean).join(' ');
|
|
78
|
+
|
|
79
|
+
if (loadFailed || !ReactMarkdown) {
|
|
80
|
+
if (loadFailed && process.env.NODE_ENV === 'development') {
|
|
81
|
+
console.warn(
|
|
82
|
+
'[@fragments-sdk/ui] Markdown: react-markdown is not installed. ' +
|
|
83
|
+
'Install it with: npm install react-markdown remark-gfm'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return (
|
|
87
|
+
<div ref={ref} className={classes}>
|
|
88
|
+
<FallbackRenderer content={content} />
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const plugins = remarkGfm ? [remarkGfm] : [];
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div ref={ref} className={classes}>
|
|
97
|
+
<ReactMarkdown
|
|
98
|
+
remarkPlugins={plugins}
|
|
99
|
+
components={componentOverrides as Record<string, React.ComponentType<unknown>>}
|
|
100
|
+
>
|
|
101
|
+
{content}
|
|
102
|
+
</ReactMarkdown>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import styles from './Message.module.scss';
|
|
5
5
|
import '../../styles/globals.scss';
|
|
6
|
+
import { Markdown } from '../Markdown';
|
|
6
7
|
|
|
7
8
|
// ============================================
|
|
8
9
|
// Types
|
|
@@ -28,6 +29,8 @@ export interface MessageProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
28
29
|
|
|
29
30
|
export interface MessageContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
30
31
|
children: React.ReactNode;
|
|
32
|
+
/** When true, renders string children as markdown */
|
|
33
|
+
markdown?: boolean;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export interface MessageActionsProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
@@ -140,7 +143,7 @@ function SystemIcon() {
|
|
|
140
143
|
// Sub-components
|
|
141
144
|
// ============================================
|
|
142
145
|
|
|
143
|
-
function MessageContent({ children, className, ...htmlProps }: MessageContentProps) {
|
|
146
|
+
function MessageContent({ children, markdown, className, ...htmlProps }: MessageContentProps) {
|
|
144
147
|
const { status } = useMessageContext();
|
|
145
148
|
|
|
146
149
|
const classes = [
|
|
@@ -149,9 +152,13 @@ function MessageContent({ children, className, ...htmlProps }: MessageContentPro
|
|
|
149
152
|
className,
|
|
150
153
|
].filter(Boolean).join(' ');
|
|
151
154
|
|
|
155
|
+
const content = markdown && typeof children === 'string'
|
|
156
|
+
? <Markdown content={children} />
|
|
157
|
+
: children;
|
|
158
|
+
|
|
152
159
|
return (
|
|
153
160
|
<div {...htmlProps} className={classes}>
|
|
154
|
-
{
|
|
161
|
+
{content}
|
|
155
162
|
</div>
|
|
156
163
|
);
|
|
157
164
|
}
|
|
@@ -50,7 +50,7 @@ export interface PromptToolbarProps {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export interface PromptActionsProps {
|
|
53
|
-
children
|
|
53
|
+
children?: React.ReactNode;
|
|
54
54
|
className?: string;
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -313,6 +313,7 @@ function PromptToolbar({ children, className }: PromptToolbarProps) {
|
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
function PromptActions({ children, className }: PromptActionsProps) {
|
|
316
|
+
if (!children) return null;
|
|
316
317
|
const classes = [styles.actions, className].filter(Boolean).join(' ');
|
|
317
318
|
return <div className={classes}>{children}</div>;
|
|
318
319
|
}
|
|
@@ -72,6 +72,10 @@ export default defineSegment({
|
|
|
72
72
|
description: 'Allow items to wrap',
|
|
73
73
|
default: 'false',
|
|
74
74
|
},
|
|
75
|
+
separator: {
|
|
76
|
+
type: 'custom',
|
|
77
|
+
description: 'Render a separator between children. true = default 1px line, or pass a ReactNode for custom separators.',
|
|
78
|
+
},
|
|
75
79
|
as: {
|
|
76
80
|
type: 'enum',
|
|
77
81
|
description: 'HTML element to render',
|
|
@@ -93,6 +97,7 @@ export default defineSegment({
|
|
|
93
97
|
'align: start|center|end|stretch|baseline - cross-axis',
|
|
94
98
|
'justify: start|center|end|between - main-axis',
|
|
95
99
|
'wrap: boolean - allow wrapping',
|
|
100
|
+
'separator: boolean|ReactNode - divider between children',
|
|
96
101
|
'as: string - HTML element',
|
|
97
102
|
],
|
|
98
103
|
scenarioTags: [
|
|
@@ -179,6 +184,17 @@ export default defineSegment({
|
|
|
179
184
|
</Stack>
|
|
180
185
|
),
|
|
181
186
|
},
|
|
187
|
+
{
|
|
188
|
+
name: 'With Separator',
|
|
189
|
+
description: 'Default line separator between items',
|
|
190
|
+
render: () => (
|
|
191
|
+
<Stack gap="md" separator>
|
|
192
|
+
<div>Section One</div>
|
|
193
|
+
<div>Section Two</div>
|
|
194
|
+
<div>Section Three</div>
|
|
195
|
+
</Stack>
|
|
196
|
+
),
|
|
197
|
+
},
|
|
182
198
|
{
|
|
183
199
|
name: 'Semantic Element',
|
|
184
200
|
description: 'Using nav element for navigation',
|
|
@@ -118,3 +118,19 @@
|
|
|
118
118
|
.wrap {
|
|
119
119
|
flex-wrap: wrap;
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
// Separator
|
|
123
|
+
.separator {
|
|
124
|
+
flex-shrink: 0;
|
|
125
|
+
background-color: var(--fui-border, $fui-border);
|
|
126
|
+
|
|
127
|
+
&[data-orientation="horizontal"] {
|
|
128
|
+
height: 1px;
|
|
129
|
+
width: 100%;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
&[data-orientation="vertical"] {
|
|
133
|
+
width: 1px;
|
|
134
|
+
align-self: stretch;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -50,6 +50,12 @@ export interface StackProps {
|
|
|
50
50
|
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
|
|
51
51
|
justify?: 'start' | 'center' | 'end' | 'between';
|
|
52
52
|
wrap?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Render a separator between each child.
|
|
55
|
+
* - `true` renders a default 1px border line
|
|
56
|
+
* - A ReactNode renders custom content between children
|
|
57
|
+
*/
|
|
58
|
+
separator?: boolean | React.ReactNode;
|
|
53
59
|
as?: 'div' | 'section' | 'nav' | 'article' | 'aside' | 'header' | 'footer' | 'main' | 'ul' | 'ol';
|
|
54
60
|
className?: string;
|
|
55
61
|
style?: React.CSSProperties;
|
|
@@ -74,6 +80,7 @@ export const Stack = React.forwardRef<HTMLElement, StackProps>(
|
|
|
74
80
|
align,
|
|
75
81
|
justify,
|
|
76
82
|
wrap = false,
|
|
83
|
+
separator,
|
|
77
84
|
as: Component = 'div',
|
|
78
85
|
className,
|
|
79
86
|
style,
|
|
@@ -126,9 +133,36 @@ export const Stack = React.forwardRef<HTMLElement, StackProps>(
|
|
|
126
133
|
|
|
127
134
|
const mergedStyle = inlineStyle ? { ...inlineStyle, ...style } : style;
|
|
128
135
|
|
|
136
|
+
// Interleave separator between children when provided
|
|
137
|
+
let content: React.ReactNode = children;
|
|
138
|
+
if (separator) {
|
|
139
|
+
const validChildren = React.Children.toArray(children).filter(Boolean);
|
|
140
|
+
if (validChildren.length > 1) {
|
|
141
|
+
const resolvedDir = isResponsiveDirection(direction) ? (direction.base ?? 'column') : direction;
|
|
142
|
+
const separatorEl = separator === true ? (
|
|
143
|
+
<div
|
|
144
|
+
className={styles.separator}
|
|
145
|
+
data-orientation={resolvedDir === 'row' ? 'vertical' : 'horizontal'}
|
|
146
|
+
role="separator"
|
|
147
|
+
/>
|
|
148
|
+
) : separator;
|
|
149
|
+
|
|
150
|
+
const items: React.ReactNode[] = [];
|
|
151
|
+
validChildren.forEach((child, i) => {
|
|
152
|
+
items.push(child);
|
|
153
|
+
if (i < validChildren.length - 1) {
|
|
154
|
+
items.push(
|
|
155
|
+
<React.Fragment key={`sep-${i}`}>{separatorEl}</React.Fragment>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
content = items;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
129
163
|
return (
|
|
130
164
|
<Component ref={ref as React.Ref<never>} className={classes} style={mergedStyle}>
|
|
131
|
-
{
|
|
165
|
+
{content}
|
|
132
166
|
</Component>
|
|
133
167
|
);
|
|
134
168
|
}
|
|
@@ -111,6 +111,16 @@ export default defineSegment({
|
|
|
111
111
|
values: ['sm', 'md'],
|
|
112
112
|
default: 'md',
|
|
113
113
|
},
|
|
114
|
+
striped: {
|
|
115
|
+
type: 'boolean',
|
|
116
|
+
description: 'Show alternating row backgrounds',
|
|
117
|
+
default: 'false',
|
|
118
|
+
},
|
|
119
|
+
bordered: {
|
|
120
|
+
type: 'boolean',
|
|
121
|
+
description: 'Wrap table in a bordered container',
|
|
122
|
+
default: 'false',
|
|
123
|
+
},
|
|
114
124
|
},
|
|
115
125
|
|
|
116
126
|
relations: [
|
|
@@ -125,6 +135,8 @@ export default defineSegment({
|
|
|
125
135
|
'sortable: boolean - enable sorting',
|
|
126
136
|
'selectable: boolean - enable row selection',
|
|
127
137
|
'size: sm|md - table density',
|
|
138
|
+
'striped: boolean - alternating row backgrounds',
|
|
139
|
+
'bordered: boolean - bordered container',
|
|
128
140
|
],
|
|
129
141
|
scenarioTags: [
|
|
130
142
|
'data.table',
|
|
@@ -185,6 +197,28 @@ export default defineSegment({
|
|
|
185
197
|
/>
|
|
186
198
|
),
|
|
187
199
|
},
|
|
200
|
+
{
|
|
201
|
+
name: 'Striped',
|
|
202
|
+
description: 'Table with alternating row backgrounds',
|
|
203
|
+
render: () => (
|
|
204
|
+
<Table
|
|
205
|
+
columns={columns}
|
|
206
|
+
data={sampleUsers}
|
|
207
|
+
striped
|
|
208
|
+
/>
|
|
209
|
+
),
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'Bordered',
|
|
213
|
+
description: 'Table with bordered container',
|
|
214
|
+
render: () => (
|
|
215
|
+
<Table
|
|
216
|
+
columns={columns}
|
|
217
|
+
data={sampleUsers}
|
|
218
|
+
bordered
|
|
219
|
+
/>
|
|
220
|
+
),
|
|
221
|
+
},
|
|
188
222
|
{
|
|
189
223
|
name: 'Empty State',
|
|
190
224
|
description: 'Table with no data',
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
.sm {
|
|
34
34
|
.th,
|
|
35
35
|
.td {
|
|
36
|
-
padding: var(--fui-space-
|
|
36
|
+
padding: var(--fui-space-1, $fui-space-1) var(--fui-space-3, $fui-space-3);
|
|
37
37
|
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
.md {
|
|
42
42
|
.th,
|
|
43
43
|
.td {
|
|
44
|
-
padding: var(--fui-space-
|
|
44
|
+
padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
|
|
45
45
|
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
position: sticky;
|
|
52
52
|
top: 0;
|
|
53
53
|
z-index: 1;
|
|
54
|
-
background-color: var(--fui-bg-
|
|
54
|
+
background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
.headerRow {
|
|
@@ -138,7 +138,6 @@
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
.selected {
|
|
141
|
-
background-color: var(--fui-color-accent, $fui-color-accent);
|
|
142
141
|
background-color: rgba($fui-color-accent, 0.08);
|
|
143
142
|
|
|
144
143
|
&:hover {
|
|
@@ -152,6 +151,37 @@
|
|
|
152
151
|
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
153
152
|
}
|
|
154
153
|
|
|
154
|
+
// Striped rows
|
|
155
|
+
.striped {
|
|
156
|
+
.row:nth-child(even) {
|
|
157
|
+
background-color: var(--fui-bg-subtle, $fui-bg-subtle);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Hover and selected override stripe
|
|
161
|
+
.clickable:hover {
|
|
162
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.clickable:active {
|
|
166
|
+
background-color: var(--fui-bg-active, $fui-bg-active);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.selected {
|
|
170
|
+
background-color: rgba($fui-color-accent, 0.08);
|
|
171
|
+
|
|
172
|
+
&:hover {
|
|
173
|
+
background-color: rgba($fui-color-accent, 0.12);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Bordered
|
|
179
|
+
.bordered {
|
|
180
|
+
border: 1px solid var(--fui-border, $fui-border);
|
|
181
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
182
|
+
overflow: hidden;
|
|
183
|
+
}
|
|
184
|
+
|
|
155
185
|
// Empty state
|
|
156
186
|
.emptyState {
|
|
157
187
|
display: flex;
|
|
@@ -167,7 +197,7 @@
|
|
|
167
197
|
}
|
|
168
198
|
|
|
169
199
|
// Responsive: allow horizontal scroll on small screens
|
|
170
|
-
@
|
|
200
|
+
@include below-sm {
|
|
171
201
|
.wrapper {
|
|
172
202
|
margin-left: calc(-1 * var(--fui-space-4, $fui-space-4));
|
|
173
203
|
margin-right: calc(-1 * var(--fui-space-4, $fui-space-4));
|
|
@@ -45,6 +45,10 @@ export interface TableProps<T> extends Omit<React.HTMLAttributes<HTMLTableElemen
|
|
|
45
45
|
caption?: string;
|
|
46
46
|
/** Hide the caption visually but keep it for screen readers */
|
|
47
47
|
captionHidden?: boolean;
|
|
48
|
+
/** Show alternating row backgrounds */
|
|
49
|
+
striped?: boolean;
|
|
50
|
+
/** Wrap table in a bordered container */
|
|
51
|
+
bordered?: boolean;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
export function Table<T>({
|
|
@@ -63,6 +67,8 @@ export function Table<T>({
|
|
|
63
67
|
className,
|
|
64
68
|
caption,
|
|
65
69
|
captionHidden = false,
|
|
70
|
+
striped = false,
|
|
71
|
+
bordered = false,
|
|
66
72
|
'aria-label': ariaLabel,
|
|
67
73
|
'aria-describedby': ariaDescribedBy,
|
|
68
74
|
...htmlProps
|
|
@@ -95,7 +101,7 @@ export function Table<T>({
|
|
|
95
101
|
|
|
96
102
|
const isEmpty = data.length === 0;
|
|
97
103
|
|
|
98
|
-
const rootClasses = [styles.table, styles[size], className]
|
|
104
|
+
const rootClasses = [styles.table, styles[size], striped && styles.striped, className]
|
|
99
105
|
.filter(Boolean)
|
|
100
106
|
.join(' ');
|
|
101
107
|
|
|
@@ -119,7 +125,7 @@ export function Table<T>({
|
|
|
119
125
|
};
|
|
120
126
|
|
|
121
127
|
return (
|
|
122
|
-
<div className={styles.wrapper}>
|
|
128
|
+
<div className={[styles.wrapper, bordered && styles.bordered].filter(Boolean).join(' ')}>
|
|
123
129
|
<table
|
|
124
130
|
{...htmlProps}
|
|
125
131
|
className={rootClasses}
|
package/src/index.ts
CHANGED
|
@@ -406,15 +406,32 @@ export {
|
|
|
406
406
|
type ListboxEmptyProps,
|
|
407
407
|
} from './components/Listbox';
|
|
408
408
|
|
|
409
|
+
// Breadcrumbs
|
|
410
|
+
export {
|
|
411
|
+
Breadcrumbs,
|
|
412
|
+
BreadcrumbsRoot,
|
|
413
|
+
BreadcrumbsItem,
|
|
414
|
+
BreadcrumbsSeparator,
|
|
415
|
+
type BreadcrumbsProps,
|
|
416
|
+
type BreadcrumbsItemProps,
|
|
417
|
+
type BreadcrumbsSeparatorProps,
|
|
418
|
+
} from './components/Breadcrumbs';
|
|
419
|
+
|
|
409
420
|
// Box
|
|
410
421
|
export { Box, type BoxProps } from './components/Box';
|
|
411
422
|
|
|
423
|
+
// Chip
|
|
424
|
+
export { Chip, type ChipProps, type ChipGroupProps } from './components/Chip';
|
|
425
|
+
|
|
412
426
|
// VisuallyHidden
|
|
413
427
|
export { VisuallyHidden, type VisuallyHiddenProps } from './components/VisuallyHidden';
|
|
414
428
|
|
|
415
429
|
// Brand
|
|
416
430
|
export { BRAND, type Brand } from './brand';
|
|
417
431
|
|
|
432
|
+
// Markdown (AI Chat)
|
|
433
|
+
export { Markdown, type MarkdownProps } from './components/Markdown';
|
|
434
|
+
|
|
418
435
|
// Message (AI Chat)
|
|
419
436
|
export {
|
|
420
437
|
Message,
|