@fragments-sdk/ui 0.6.5 → 0.7.0
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/index.ts +17 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { Breadcrumbs } from '.';
|
|
4
|
+
|
|
5
|
+
export default defineSegment({
|
|
6
|
+
component: Breadcrumbs,
|
|
7
|
+
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'Breadcrumbs',
|
|
10
|
+
description: 'Breadcrumb navigation showing the current page location within a hierarchy. Helps users navigate back through parent pages.',
|
|
11
|
+
category: 'navigation',
|
|
12
|
+
status: 'stable',
|
|
13
|
+
tags: ['breadcrumbs', 'navigation', 'hierarchy', 'wayfinding'],
|
|
14
|
+
since: '0.7.0',
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
usage: {
|
|
18
|
+
when: [
|
|
19
|
+
'Showing hierarchical page location (e.g., Home > Category > Product)',
|
|
20
|
+
'Allowing quick navigation to parent pages',
|
|
21
|
+
'Multi-level content structures like documentation or e-commerce',
|
|
22
|
+
],
|
|
23
|
+
whenNot: [
|
|
24
|
+
'Flat navigation with no hierarchy (use Tabs or Header nav)',
|
|
25
|
+
'Step-by-step wizards (use Stepper)',
|
|
26
|
+
'Primary navigation (use Sidebar or Header)',
|
|
27
|
+
],
|
|
28
|
+
guidelines: [
|
|
29
|
+
'Always include the current page as the last, non-linked item',
|
|
30
|
+
'Keep labels short and descriptive',
|
|
31
|
+
'Use maxItems to collapse long paths, keeping first and last visible',
|
|
32
|
+
'The separator defaults to "/" but can be customized',
|
|
33
|
+
],
|
|
34
|
+
accessibility: [
|
|
35
|
+
'Uses <nav aria-label="Breadcrumb"> for landmark navigation',
|
|
36
|
+
'Current page is marked with aria-current="page"',
|
|
37
|
+
'Separators are hidden from screen readers with aria-hidden',
|
|
38
|
+
'Ellipsis button has aria-label for collapsed items',
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
props: {
|
|
43
|
+
children: {
|
|
44
|
+
type: 'node',
|
|
45
|
+
description: 'Breadcrumb items (use Breadcrumbs.Item)',
|
|
46
|
+
required: true,
|
|
47
|
+
},
|
|
48
|
+
separator: {
|
|
49
|
+
type: 'node',
|
|
50
|
+
description: 'Custom separator between items',
|
|
51
|
+
default: '"/"',
|
|
52
|
+
},
|
|
53
|
+
maxItems: {
|
|
54
|
+
type: 'number',
|
|
55
|
+
description: 'Maximum visible items before collapsing middle items with ellipsis',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
relations: [
|
|
60
|
+
{ component: 'Tabs', relationship: 'alternative', note: 'Use Tabs for flat, non-hierarchical navigation' },
|
|
61
|
+
{ component: 'Sidebar', relationship: 'complementary', note: 'Breadcrumbs complement sidebar navigation for deep hierarchies' },
|
|
62
|
+
],
|
|
63
|
+
|
|
64
|
+
contract: {
|
|
65
|
+
propsSummary: [
|
|
66
|
+
'separator: ReactNode - custom separator (default "/")',
|
|
67
|
+
'maxItems: number - collapse middle items with ellipsis',
|
|
68
|
+
'Breadcrumbs.Item href: string - makes item a link',
|
|
69
|
+
'Breadcrumbs.Item current: boolean - marks current page',
|
|
70
|
+
'Breadcrumbs.Item icon: ReactNode - icon before label',
|
|
71
|
+
],
|
|
72
|
+
scenarioTags: [
|
|
73
|
+
'navigation.breadcrumbs',
|
|
74
|
+
'navigation.hierarchy',
|
|
75
|
+
'wayfinding.location',
|
|
76
|
+
],
|
|
77
|
+
a11yRules: ['A11Y_NAV_LANDMARK', 'A11Y_ARIA_CURRENT'],
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
ai: {
|
|
81
|
+
compositionPattern: 'compound',
|
|
82
|
+
subComponents: ['Item', 'Separator'],
|
|
83
|
+
requiredChildren: ['Item'],
|
|
84
|
+
commonPatterns: [
|
|
85
|
+
'<Breadcrumbs><Breadcrumbs.Item href="/">Home</Breadcrumbs.Item><Breadcrumbs.Item href="/products">Products</Breadcrumbs.Item><Breadcrumbs.Item current>Widget</Breadcrumbs.Item></Breadcrumbs>',
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
variants: [
|
|
90
|
+
{
|
|
91
|
+
name: 'Default',
|
|
92
|
+
description: 'Basic breadcrumb navigation',
|
|
93
|
+
render: () => (
|
|
94
|
+
<Breadcrumbs>
|
|
95
|
+
<Breadcrumbs.Item href="#">Home</Breadcrumbs.Item>
|
|
96
|
+
<Breadcrumbs.Item href="#">Products</Breadcrumbs.Item>
|
|
97
|
+
<Breadcrumbs.Item href="#">Category</Breadcrumbs.Item>
|
|
98
|
+
<Breadcrumbs.Item current>Current Page</Breadcrumbs.Item>
|
|
99
|
+
</Breadcrumbs>
|
|
100
|
+
),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'With Icons',
|
|
104
|
+
description: 'Breadcrumbs with icons on items',
|
|
105
|
+
render: () => (
|
|
106
|
+
<Breadcrumbs>
|
|
107
|
+
<Breadcrumbs.Item
|
|
108
|
+
href="#"
|
|
109
|
+
icon={
|
|
110
|
+
<svg viewBox="0 0 16 16" fill="currentColor">
|
|
111
|
+
<path d="M8 1.25l-7 6v7.5c0 .138.112.25.25.25H5.5V10h5v5h4.25a.25.25 0 0 0 .25-.25v-7.5l-7-6z" />
|
|
112
|
+
</svg>
|
|
113
|
+
}
|
|
114
|
+
>
|
|
115
|
+
Home
|
|
116
|
+
</Breadcrumbs.Item>
|
|
117
|
+
<Breadcrumbs.Item
|
|
118
|
+
href="#"
|
|
119
|
+
icon={
|
|
120
|
+
<svg viewBox="0 0 16 16" fill="currentColor">
|
|
121
|
+
<path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75z" />
|
|
122
|
+
</svg>
|
|
123
|
+
}
|
|
124
|
+
>
|
|
125
|
+
Documents
|
|
126
|
+
</Breadcrumbs.Item>
|
|
127
|
+
<Breadcrumbs.Item current>Report.pdf</Breadcrumbs.Item>
|
|
128
|
+
</Breadcrumbs>
|
|
129
|
+
),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'Collapsed',
|
|
133
|
+
description: 'Long breadcrumb trail collapsed with ellipsis',
|
|
134
|
+
render: () => (
|
|
135
|
+
<Breadcrumbs maxItems={3}>
|
|
136
|
+
<Breadcrumbs.Item href="#">Home</Breadcrumbs.Item>
|
|
137
|
+
<Breadcrumbs.Item href="#">Category</Breadcrumbs.Item>
|
|
138
|
+
<Breadcrumbs.Item href="#">Subcategory</Breadcrumbs.Item>
|
|
139
|
+
<Breadcrumbs.Item href="#">Section</Breadcrumbs.Item>
|
|
140
|
+
<Breadcrumbs.Item current>Current Page</Breadcrumbs.Item>
|
|
141
|
+
</Breadcrumbs>
|
|
142
|
+
),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'Custom Separator',
|
|
146
|
+
description: 'Breadcrumbs with a custom chevron separator',
|
|
147
|
+
render: () => (
|
|
148
|
+
<Breadcrumbs
|
|
149
|
+
separator={
|
|
150
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
|
151
|
+
<path fillRule="evenodd" d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06z" />
|
|
152
|
+
</svg>
|
|
153
|
+
}
|
|
154
|
+
>
|
|
155
|
+
<Breadcrumbs.Item href="#">Home</Breadcrumbs.Item>
|
|
156
|
+
<Breadcrumbs.Item href="#">Settings</Breadcrumbs.Item>
|
|
157
|
+
<Breadcrumbs.Item current>Profile</Breadcrumbs.Item>
|
|
158
|
+
</Breadcrumbs>
|
|
159
|
+
),
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
|
|
3
|
+
// Root nav wrapper
|
|
4
|
+
.root {
|
|
5
|
+
font-family: var(--fui-font-sans, $fui-font-sans);
|
|
6
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
7
|
+
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Ordered list container
|
|
11
|
+
.list {
|
|
12
|
+
display: inline-flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
flex-wrap: wrap;
|
|
15
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
16
|
+
list-style: none;
|
|
17
|
+
margin: 0;
|
|
18
|
+
padding: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Individual breadcrumb item (li)
|
|
22
|
+
.item {
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
26
|
+
min-width: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Link styling
|
|
30
|
+
.link {
|
|
31
|
+
display: inline-flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
34
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
35
|
+
text-decoration: none;
|
|
36
|
+
white-space: nowrap;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
text-overflow: ellipsis;
|
|
39
|
+
max-width: 200px;
|
|
40
|
+
border-radius: var(--fui-radius-sm, $fui-radius-sm);
|
|
41
|
+
padding: var(--fui-space-0-5, $fui-space-0-5) var(--fui-space-1, $fui-space-1);
|
|
42
|
+
margin: calc(-1 * var(--fui-space-0-5, $fui-space-0-5)) calc(-1 * var(--fui-space-1, $fui-space-1));
|
|
43
|
+
transition: color var(--fui-transition-fast, $fui-transition-fast),
|
|
44
|
+
background-color var(--fui-transition-fast, $fui-transition-fast);
|
|
45
|
+
|
|
46
|
+
&:hover {
|
|
47
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
48
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&:focus-visible {
|
|
52
|
+
outline: var(--fui-focus-ring-width, $fui-focus-ring-width) solid var(--fui-focus-ring-color, $fui-focus-ring-color);
|
|
53
|
+
outline-offset: var(--fui-focus-ring-offset, $fui-focus-ring-offset);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Current page (not a link)
|
|
58
|
+
.current {
|
|
59
|
+
display: inline-flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
62
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
63
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
64
|
+
white-space: nowrap;
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
text-overflow: ellipsis;
|
|
67
|
+
max-width: 200px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Separator between items
|
|
71
|
+
.separator {
|
|
72
|
+
display: inline-flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
75
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
76
|
+
user-select: none;
|
|
77
|
+
flex-shrink: 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Ellipsis button for collapsed items
|
|
81
|
+
.ellipsis {
|
|
82
|
+
display: inline-flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
justify-content: center;
|
|
85
|
+
background: none;
|
|
86
|
+
border: 1px solid var(--fui-border, $fui-border);
|
|
87
|
+
border-radius: var(--fui-radius-sm, $fui-radius-sm);
|
|
88
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
padding: var(--fui-space-0-5, $fui-space-0-5) var(--fui-space-2, $fui-space-2);
|
|
91
|
+
font-family: inherit;
|
|
92
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
93
|
+
line-height: 1;
|
|
94
|
+
transition: color var(--fui-transition-fast, $fui-transition-fast),
|
|
95
|
+
background-color var(--fui-transition-fast, $fui-transition-fast);
|
|
96
|
+
|
|
97
|
+
&:hover {
|
|
98
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
99
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
&:focus-visible {
|
|
103
|
+
outline: var(--fui-focus-ring-width, $fui-focus-ring-width) solid var(--fui-focus-ring-color, $fui-focus-ring-color);
|
|
104
|
+
outline-offset: var(--fui-focus-ring-offset, $fui-focus-ring-offset);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Icon within an item
|
|
109
|
+
.icon {
|
|
110
|
+
display: inline-flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
flex-shrink: 0;
|
|
113
|
+
width: var(--fui-icon-md, $fui-icon-md);
|
|
114
|
+
height: var(--fui-icon-md, $fui-icon-md);
|
|
115
|
+
|
|
116
|
+
svg {
|
|
117
|
+
width: 100%;
|
|
118
|
+
height: 100%;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import styles from './Breadcrumbs.module.scss';
|
|
5
|
+
import '../../styles/globals.scss';
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================
|
|
10
|
+
|
|
11
|
+
export interface BreadcrumbsProps extends React.HTMLAttributes<HTMLElement> {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
/** Custom separator between items (default: '/') */
|
|
14
|
+
separator?: React.ReactNode;
|
|
15
|
+
/** Maximum visible items before collapsing middle items with ellipsis */
|
|
16
|
+
maxItems?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BreadcrumbsItemProps extends React.HTMLAttributes<HTMLLIElement> {
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
/** URL to navigate to (renders <a> if provided) */
|
|
22
|
+
href?: string;
|
|
23
|
+
/** Icon element to display before the label */
|
|
24
|
+
icon?: React.ReactNode;
|
|
25
|
+
/** Marks this item as the current page */
|
|
26
|
+
current?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BreadcrumbsSeparatorProps {
|
|
30
|
+
children?: React.ReactNode;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================
|
|
35
|
+
// Context for separator
|
|
36
|
+
// ============================================
|
|
37
|
+
|
|
38
|
+
const BreadcrumbsSeparatorContext = React.createContext<React.ReactNode>('/');
|
|
39
|
+
|
|
40
|
+
// ============================================
|
|
41
|
+
// Components
|
|
42
|
+
// ============================================
|
|
43
|
+
|
|
44
|
+
function BreadcrumbsRoot({
|
|
45
|
+
children,
|
|
46
|
+
separator = '/',
|
|
47
|
+
maxItems,
|
|
48
|
+
className,
|
|
49
|
+
...htmlProps
|
|
50
|
+
}: BreadcrumbsProps) {
|
|
51
|
+
const classes = [styles.root, className].filter(Boolean).join(' ');
|
|
52
|
+
|
|
53
|
+
let items = React.Children.toArray(children).filter(React.isValidElement);
|
|
54
|
+
|
|
55
|
+
// Collapse middle items if maxItems is set
|
|
56
|
+
let collapsed = false;
|
|
57
|
+
let collapsedItems: React.ReactElement[] = [];
|
|
58
|
+
if (maxItems != null && maxItems > 1 && items.length > maxItems) {
|
|
59
|
+
collapsed = true;
|
|
60
|
+
const first = items.slice(0, 1);
|
|
61
|
+
const last = items.slice(-(maxItems - 1));
|
|
62
|
+
collapsedItems = items.slice(1, items.length - (maxItems - 1)) as React.ReactElement[];
|
|
63
|
+
items = [...first, ...last];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<BreadcrumbsSeparatorContext.Provider value={separator}>
|
|
68
|
+
<nav aria-label="Breadcrumb" className={classes} {...htmlProps}>
|
|
69
|
+
<ol className={styles.list}>
|
|
70
|
+
{items.map((item, index) => {
|
|
71
|
+
const isFirst = index === 0;
|
|
72
|
+
const showEllipsis = collapsed && index === 1;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<React.Fragment key={(item as React.ReactElement).key ?? index}>
|
|
76
|
+
{!isFirst && (
|
|
77
|
+
<li role="presentation" aria-hidden="true" className={styles.separator}>
|
|
78
|
+
{separator}
|
|
79
|
+
</li>
|
|
80
|
+
)}
|
|
81
|
+
{showEllipsis && (
|
|
82
|
+
<EllipsisItem items={collapsedItems} separator={separator} />
|
|
83
|
+
)}
|
|
84
|
+
{item}
|
|
85
|
+
</React.Fragment>
|
|
86
|
+
);
|
|
87
|
+
})}
|
|
88
|
+
</ol>
|
|
89
|
+
</nav>
|
|
90
|
+
</BreadcrumbsSeparatorContext.Provider>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function EllipsisItem({
|
|
95
|
+
items,
|
|
96
|
+
separator,
|
|
97
|
+
}: {
|
|
98
|
+
items: React.ReactElement[];
|
|
99
|
+
separator: React.ReactNode;
|
|
100
|
+
}) {
|
|
101
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
102
|
+
|
|
103
|
+
if (expanded) {
|
|
104
|
+
return (
|
|
105
|
+
<>
|
|
106
|
+
{items.map((item, i) => (
|
|
107
|
+
<React.Fragment key={item.key ?? `collapsed-${i}`}>
|
|
108
|
+
{item}
|
|
109
|
+
<li role="presentation" aria-hidden="true" className={styles.separator}>
|
|
110
|
+
{separator}
|
|
111
|
+
</li>
|
|
112
|
+
</React.Fragment>
|
|
113
|
+
))}
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<>
|
|
120
|
+
<li className={styles.item}>
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
className={styles.ellipsis}
|
|
124
|
+
aria-label="Show collapsed breadcrumbs"
|
|
125
|
+
onClick={() => setExpanded(true)}
|
|
126
|
+
>
|
|
127
|
+
…
|
|
128
|
+
</button>
|
|
129
|
+
</li>
|
|
130
|
+
<li role="presentation" aria-hidden="true" className={styles.separator}>
|
|
131
|
+
{separator}
|
|
132
|
+
</li>
|
|
133
|
+
</>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function BreadcrumbsItem({
|
|
138
|
+
children,
|
|
139
|
+
href,
|
|
140
|
+
icon,
|
|
141
|
+
current = false,
|
|
142
|
+
className,
|
|
143
|
+
...htmlProps
|
|
144
|
+
}: BreadcrumbsItemProps) {
|
|
145
|
+
const classes = [styles.item, className].filter(Boolean).join(' ');
|
|
146
|
+
|
|
147
|
+
const iconEl = icon ? <span className={styles.icon}>{icon}</span> : null;
|
|
148
|
+
|
|
149
|
+
if (current) {
|
|
150
|
+
return (
|
|
151
|
+
<li className={classes} {...htmlProps}>
|
|
152
|
+
<span className={styles.current} aria-current="page">
|
|
153
|
+
{iconEl}
|
|
154
|
+
{children}
|
|
155
|
+
</span>
|
|
156
|
+
</li>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (href) {
|
|
161
|
+
return (
|
|
162
|
+
<li className={classes} {...htmlProps}>
|
|
163
|
+
<a href={href} className={styles.link}>
|
|
164
|
+
{iconEl}
|
|
165
|
+
{children}
|
|
166
|
+
</a>
|
|
167
|
+
</li>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<li className={classes} {...htmlProps}>
|
|
173
|
+
<span className={styles.link}>
|
|
174
|
+
{iconEl}
|
|
175
|
+
{children}
|
|
176
|
+
</span>
|
|
177
|
+
</li>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function BreadcrumbsSeparator({ children, className }: BreadcrumbsSeparatorProps) {
|
|
182
|
+
const defaultSeparator = React.useContext(BreadcrumbsSeparatorContext);
|
|
183
|
+
const classes = [styles.separator, className].filter(Boolean).join(' ');
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<span className={classes} role="presentation" aria-hidden="true">
|
|
187
|
+
{children ?? defaultSeparator}
|
|
188
|
+
</span>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ============================================
|
|
193
|
+
// Export compound component
|
|
194
|
+
// ============================================
|
|
195
|
+
|
|
196
|
+
export const Breadcrumbs = Object.assign(BreadcrumbsRoot, {
|
|
197
|
+
Item: BreadcrumbsItem,
|
|
198
|
+
Separator: BreadcrumbsSeparator,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Re-export individual components
|
|
202
|
+
export { BreadcrumbsRoot, BreadcrumbsItem, BreadcrumbsSeparator };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { Chip } from '.';
|
|
4
|
+
|
|
5
|
+
export default defineSegment({
|
|
6
|
+
component: Chip,
|
|
7
|
+
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'Chip',
|
|
10
|
+
description: 'Interactive pill-shaped element for filtering, selecting, and tagging. Supports single and multi-select via Chip.Group.',
|
|
11
|
+
category: 'forms',
|
|
12
|
+
status: 'stable',
|
|
13
|
+
tags: ['chip', 'tag', 'filter', 'selection', 'multi-select', 'action'],
|
|
14
|
+
since: '0.7.0',
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
usage: {
|
|
18
|
+
when: [
|
|
19
|
+
'Filtering content by categories or tags',
|
|
20
|
+
'Multi-select scenarios like choosing interests or skills',
|
|
21
|
+
'Toggling options in a compact pill-shaped control',
|
|
22
|
+
'Displaying removable user-applied filters',
|
|
23
|
+
],
|
|
24
|
+
whenNot: [
|
|
25
|
+
'Display-only status labels (use Badge)',
|
|
26
|
+
'Navigation between views (use Tabs)',
|
|
27
|
+
'Binary on/off state (use Toggle)',
|
|
28
|
+
'Primary call-to-action (use Button)',
|
|
29
|
+
],
|
|
30
|
+
guidelines: [
|
|
31
|
+
'Keep chip labels short (1-3 words)',
|
|
32
|
+
'Use Chip.Group for multi-select sets with shared state',
|
|
33
|
+
'Use onRemove only when users should be able to dismiss the chip',
|
|
34
|
+
'Pair avatar chips with user-related selections (assignees, reviewers)',
|
|
35
|
+
],
|
|
36
|
+
accessibility: [
|
|
37
|
+
'Chips use role="option" with aria-selected for selection state',
|
|
38
|
+
'Chip.Group uses role="listbox" with aria-multiselectable',
|
|
39
|
+
'Remove buttons include descriptive aria-label with chip text',
|
|
40
|
+
'Disabled chips are properly excluded from interaction',
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
props: {
|
|
45
|
+
children: {
|
|
46
|
+
type: 'node',
|
|
47
|
+
description: 'Chip label text',
|
|
48
|
+
required: true,
|
|
49
|
+
},
|
|
50
|
+
variant: {
|
|
51
|
+
type: 'enum',
|
|
52
|
+
description: 'Visual style variant',
|
|
53
|
+
values: ['filled', 'outlined', 'soft'],
|
|
54
|
+
default: 'filled',
|
|
55
|
+
},
|
|
56
|
+
size: {
|
|
57
|
+
type: 'enum',
|
|
58
|
+
description: 'Chip size',
|
|
59
|
+
values: ['sm', 'md'],
|
|
60
|
+
default: 'md',
|
|
61
|
+
},
|
|
62
|
+
selected: {
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
description: 'Whether the chip is in a selected state',
|
|
65
|
+
default: 'false',
|
|
66
|
+
},
|
|
67
|
+
disabled: {
|
|
68
|
+
type: 'boolean',
|
|
69
|
+
description: 'Disables the chip',
|
|
70
|
+
default: 'false',
|
|
71
|
+
},
|
|
72
|
+
icon: {
|
|
73
|
+
type: 'node',
|
|
74
|
+
description: 'Icon element rendered before the label',
|
|
75
|
+
},
|
|
76
|
+
avatar: {
|
|
77
|
+
type: 'node',
|
|
78
|
+
description: 'Avatar element rendered before the label',
|
|
79
|
+
},
|
|
80
|
+
onRemove: {
|
|
81
|
+
type: 'function',
|
|
82
|
+
description: 'Makes chip removable. Called when X is clicked.',
|
|
83
|
+
},
|
|
84
|
+
value: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
description: 'Value identifier used by Chip.Group for selection tracking',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
relations: [
|
|
91
|
+
{ component: 'Badge', relationship: 'sibling', note: 'Badge is display-only; Chip is interactive' },
|
|
92
|
+
{ component: 'ToggleGroup', relationship: 'alternative', note: 'Use ToggleGroup for mutually exclusive options' },
|
|
93
|
+
{ component: 'Button', relationship: 'alternative', note: 'Use Button for primary actions, Chip for selection/filtering' },
|
|
94
|
+
],
|
|
95
|
+
|
|
96
|
+
contract: {
|
|
97
|
+
propsSummary: [
|
|
98
|
+
'children: ReactNode - chip label (required)',
|
|
99
|
+
'variant: filled|outlined|soft - visual style',
|
|
100
|
+
'size: sm|md - chip size',
|
|
101
|
+
'selected: boolean - selection state',
|
|
102
|
+
'icon/avatar: ReactNode - leading visual',
|
|
103
|
+
'onRemove: () => void - makes chip removable',
|
|
104
|
+
'value: string - identifier for Chip.Group',
|
|
105
|
+
],
|
|
106
|
+
scenarioTags: [
|
|
107
|
+
'actions.filter',
|
|
108
|
+
'actions.select',
|
|
109
|
+
'actions.tag',
|
|
110
|
+
'input.multi-select',
|
|
111
|
+
],
|
|
112
|
+
a11yRules: ['A11Y_CHIP_SELECTION', 'A11Y_CHIP_DISMISS'],
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
variants: [
|
|
116
|
+
{
|
|
117
|
+
name: 'Default',
|
|
118
|
+
description: 'Basic filled chip',
|
|
119
|
+
render: () => <Chip>Default</Chip>,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'Selected',
|
|
123
|
+
description: 'Chip in selected state across variants',
|
|
124
|
+
render: () => (
|
|
125
|
+
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
|
126
|
+
<Chip selected>Filled</Chip>
|
|
127
|
+
<Chip variant="outlined" selected>Outlined</Chip>
|
|
128
|
+
<Chip variant="soft" selected>Soft</Chip>
|
|
129
|
+
</div>
|
|
130
|
+
),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'With Avatar',
|
|
134
|
+
description: 'Chip with a leading avatar image',
|
|
135
|
+
render: () => (
|
|
136
|
+
<Chip avatar={<img src="https://i.pravatar.cc/32?u=chip" alt="" />}>
|
|
137
|
+
Jane Doe
|
|
138
|
+
</Chip>
|
|
139
|
+
),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'With Remove',
|
|
143
|
+
description: 'Removable chip with dismiss button',
|
|
144
|
+
render: () => (
|
|
145
|
+
<div style={{ display: 'flex', gap: '8px' }}>
|
|
146
|
+
<Chip onRemove={() => {}}>React</Chip>
|
|
147
|
+
<Chip onRemove={() => {}}>TypeScript</Chip>
|
|
148
|
+
<Chip onRemove={() => {}}>SCSS</Chip>
|
|
149
|
+
</div>
|
|
150
|
+
),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'Chip Group',
|
|
154
|
+
description: 'Multi-select chip set with shared state',
|
|
155
|
+
render: () => (
|
|
156
|
+
<Chip.Group defaultValue={['react']}>
|
|
157
|
+
<Chip value="react">React</Chip>
|
|
158
|
+
<Chip value="vue">Vue</Chip>
|
|
159
|
+
<Chip value="angular">Angular</Chip>
|
|
160
|
+
<Chip value="svelte">Svelte</Chip>
|
|
161
|
+
</Chip.Group>
|
|
162
|
+
),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'Disabled',
|
|
166
|
+
description: 'Chip in disabled state',
|
|
167
|
+
render: () => (
|
|
168
|
+
<div style={{ display: 'flex', gap: '8px' }}>
|
|
169
|
+
<Chip disabled>Disabled</Chip>
|
|
170
|
+
<Chip disabled selected>Disabled Selected</Chip>
|
|
171
|
+
</div>
|
|
172
|
+
),
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
});
|