@fragments-sdk/ui 0.4.0 → 0.5.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 +98 -2
- package/fragments.json +1 -1
- package/package.json +3 -2
- package/src/components/Accordion/Accordion.fragment.tsx +1 -1
- package/src/components/Alert/Alert.fragment.tsx +1 -1
- package/src/components/AppShell/AppShell.fragment.tsx +4 -4
- package/src/components/Avatar/Avatar.fragment.tsx +2 -2
- package/src/components/Badge/Badge.fragment.tsx +2 -2
- package/src/components/Badge/Badge.module.scss +1 -1
- package/src/components/Box/Box.fragment.tsx +1 -1
- package/src/components/Button/Button.fragment.tsx +2 -2
- package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +153 -0
- package/src/components/Card/Card.fragment.tsx +1 -1
- package/src/components/Chart/Chart.fragment.tsx +213 -0
- package/src/components/Chart/Chart.module.scss +123 -0
- package/src/components/Chart/index.tsx +267 -0
- package/src/components/Checkbox/Checkbox.fragment.tsx +1 -1
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +265 -6
- package/src/components/CodeBlock/CodeBlock.module.scss +141 -3
- package/src/components/CodeBlock/index.tsx +250 -36
- package/src/components/Collapsible/Collapsible.fragment.tsx +199 -0
- package/src/components/Collapsible/Collapsible.module.scss +117 -0
- package/src/components/Collapsible/index.tsx +219 -0
- package/src/components/ColorPicker/ColorPicker.fragment.tsx +196 -0
- package/src/components/ColorPicker/ColorPicker.module.scss +33 -23
- package/src/components/ColorPicker/index.tsx +34 -12
- package/src/components/ConversationList/ConversationList.fragment.tsx +202 -0
- package/src/components/ConversationList/ConversationList.module.scss +160 -0
- package/src/components/ConversationList/index.tsx +254 -0
- package/src/components/Dialog/Dialog.fragment.tsx +3 -3
- package/src/components/EmptyState/EmptyState.fragment.tsx +2 -2
- package/src/components/Field/Field.fragment.tsx +3 -3
- package/src/components/Fieldset/Fieldset.fragment.tsx +7 -7
- package/src/components/Form/Form.fragment.tsx +11 -11
- package/src/components/Grid/Grid.fragment.tsx +1 -1
- package/src/components/Header/Header.fragment.tsx +4 -4
- package/src/components/Header/Header.module.scss +9 -10
- package/src/components/Icon/Icon.fragment.tsx +2 -2
- package/src/components/Image/Image.fragment.tsx +2 -2
- package/src/components/Input/Input.fragment.tsx +1 -1
- package/src/components/Input/Input.module.scss +2 -2
- package/src/components/Link/Link.fragment.tsx +1 -1
- package/src/components/List/List.fragment.tsx +2 -2
- package/src/components/Listbox/Listbox.fragment.tsx +1 -1
- package/src/components/Loading/Loading.fragment.tsx +153 -0
- package/src/components/Loading/Loading.module.scss +256 -0
- package/src/components/Loading/index.tsx +236 -0
- package/src/components/Menu/Menu.fragment.tsx +3 -3
- package/src/components/Message/Message.fragment.tsx +200 -0
- package/src/components/Message/Message.module.scss +224 -0
- package/src/components/Message/index.tsx +278 -0
- package/src/components/Popover/Popover.fragment.tsx +4 -4
- package/src/components/Progress/Progress.fragment.tsx +1 -1
- package/src/components/Prompt/Prompt.fragment.tsx +2 -2
- package/src/components/RadioGroup/RadioGroup.fragment.tsx +1 -1
- package/src/components/RadioGroup/RadioGroup.module.scss +7 -4
- package/src/components/Select/Select.fragment.tsx +1 -1
- package/src/components/Select/Select.module.scss +8 -0
- package/src/components/Select/index.tsx +85 -5
- package/src/components/Separator/Separator.fragment.tsx +1 -1
- package/src/components/Sidebar/Sidebar.fragment.tsx +2 -2
- package/src/components/Sidebar/Sidebar.module.scss +19 -0
- package/src/components/Sidebar/index.tsx +52 -11
- package/src/components/Skeleton/Skeleton.fragment.tsx +1 -1
- package/src/components/Slider/Slider.fragment.tsx +201 -0
- package/src/components/Stack/Stack.fragment.tsx +194 -0
- package/src/components/Table/Table.fragment.tsx +3 -3
- package/src/components/Tabs/Tabs.fragment.tsx +1 -1
- package/src/components/Tabs/Tabs.module.scss +2 -2
- package/src/components/Text/Text.fragment.tsx +188 -0
- package/src/components/Textarea/Textarea.fragment.tsx +1 -1
- package/src/components/Theme/Theme.fragment.tsx +2 -2
- package/src/components/Theme/ThemeToggle.module.scss +13 -13
- package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +182 -0
- package/src/components/ThinkingIndicator/ThinkingIndicator.module.scss +226 -0
- package/src/components/ThinkingIndicator/index.tsx +258 -0
- package/src/components/Toast/Toast.fragment.tsx +1 -1
- package/src/components/Toggle/Toggle.fragment.tsx +1 -1
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +207 -0
- package/src/components/Tooltip/Tooltip.fragment.tsx +3 -3
- package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +2 -2
- package/src/index.ts +86 -3
- package/src/recipes/AIChat.recipe.ts +266 -0
- package/src/tokens/_computed.scss +212 -0
- package/src/tokens/_density.scss +171 -0
- package/src/tokens/_derive.scss +287 -0
- package/src/tokens/_index.scss +39 -1
- package/src/tokens/_mixins.scss +41 -0
- package/src/tokens/_palettes.scss +185 -0
- package/src/tokens/_radius.scss +107 -0
- package/src/tokens/_seeds.scss +59 -0
- package/src/tokens/_variables.scss +171 -130
- package/src/components/ColorChip/ColorChip.module.scss +0 -165
- package/src/components/ColorChip/index.tsx +0 -157
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { defineSegment } from '@fragments/core';
|
|
3
|
-
import { Prompt } from '
|
|
3
|
+
import { Prompt } from '.';
|
|
4
4
|
|
|
5
5
|
export default defineSegment({
|
|
6
6
|
component: Prompt,
|
|
@@ -8,7 +8,7 @@ export default defineSegment({
|
|
|
8
8
|
meta: {
|
|
9
9
|
name: 'Prompt',
|
|
10
10
|
description: 'Multi-line input with toolbar for AI/chat interfaces',
|
|
11
|
-
category: '
|
|
11
|
+
category: 'ai',
|
|
12
12
|
status: 'stable',
|
|
13
13
|
tags: ['prompt', 'chat', 'ai', 'input', 'textarea', 'form'],
|
|
14
14
|
},
|
|
@@ -89,23 +89,26 @@
|
|
|
89
89
|
margin-top: 0;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
// The indicator dot
|
|
92
|
+
// The indicator dot - use absolute positioning for perfect centering
|
|
93
93
|
.indicator {
|
|
94
|
+
position: absolute;
|
|
95
|
+
top: 50%;
|
|
96
|
+
left: 50%;
|
|
94
97
|
width: 0.5rem;
|
|
95
98
|
height: 0.5rem;
|
|
96
99
|
background-color: var(--fui-color-accent, $fui-color-accent);
|
|
97
100
|
border-radius: var(--fui-radius-full, $fui-radius-full);
|
|
98
101
|
|
|
99
|
-
// Animation
|
|
102
|
+
// Animation - use translate for centering combined with scale
|
|
100
103
|
opacity: 0;
|
|
101
|
-
transform: scale(0);
|
|
104
|
+
transform: translate(-50%, -50%) scale(0);
|
|
102
105
|
transition:
|
|
103
106
|
opacity var(--fui-transition-fast, $fui-transition-fast),
|
|
104
107
|
transform var(--fui-transition-fast, $fui-transition-fast);
|
|
105
108
|
|
|
106
109
|
[data-checked] > & {
|
|
107
110
|
opacity: 1;
|
|
108
|
-
transform: scale(1);
|
|
111
|
+
transform: translate(-50%, -50%) scale(1);
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { defineSegment } from '@fragments/core';
|
|
3
|
-
import { Select } from '
|
|
3
|
+
import { Select } from '.';
|
|
4
4
|
|
|
5
5
|
// Stateful wrapper for interactive demos
|
|
6
6
|
function StatefulSelect(props: React.ComponentProps<typeof Select> & {
|
|
@@ -37,15 +37,23 @@
|
|
|
37
37
|
// Value display
|
|
38
38
|
.value {
|
|
39
39
|
flex: 1;
|
|
40
|
+
min-width: 0;
|
|
40
41
|
overflow: hidden;
|
|
41
42
|
text-overflow: ellipsis;
|
|
42
43
|
white-space: nowrap;
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
43
46
|
|
|
44
47
|
&[data-placeholder] {
|
|
45
48
|
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
// Placeholder text
|
|
53
|
+
.placeholder {
|
|
54
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
// Chevron icon
|
|
50
58
|
.icon {
|
|
51
59
|
display: flex;
|
|
@@ -101,10 +101,23 @@ function CheckIcon() {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// ============================================
|
|
104
|
-
// Context for
|
|
104
|
+
// Context for Select state
|
|
105
105
|
// ============================================
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
interface SelectContextValue {
|
|
108
|
+
placeholder?: string;
|
|
109
|
+
value?: SelectValue | null;
|
|
110
|
+
itemsRef: React.MutableRefObject<Map<SelectValue, React.ReactNode>>;
|
|
111
|
+
// Version counter to trigger re-renders when items register
|
|
112
|
+
itemsVersion: number;
|
|
113
|
+
incrementItemsVersion: () => void;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const SelectContext = React.createContext<SelectContextValue>({
|
|
117
|
+
itemsRef: { current: new Map() },
|
|
118
|
+
itemsVersion: 0,
|
|
119
|
+
incrementItemsVersion: () => {},
|
|
120
|
+
});
|
|
108
121
|
|
|
109
122
|
// ============================================
|
|
110
123
|
// Components
|
|
@@ -123,12 +136,55 @@ function SelectRoot({
|
|
|
123
136
|
name,
|
|
124
137
|
placeholder,
|
|
125
138
|
}: SelectProps) {
|
|
139
|
+
// Track current value for controlled and uncontrolled modes
|
|
140
|
+
const [internalValue, setInternalValue] = React.useState<SelectValue | null | undefined>(
|
|
141
|
+
value ?? defaultValue ?? null
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Registry for item children - allows trigger to render selected item's content
|
|
145
|
+
const itemsRef = React.useRef<Map<SelectValue, React.ReactNode>>(new Map());
|
|
146
|
+
|
|
147
|
+
// Version counter to trigger trigger re-render when items register
|
|
148
|
+
const [itemsVersion, setItemsVersion] = React.useState(0);
|
|
149
|
+
const incrementItemsVersion = React.useCallback(() => {
|
|
150
|
+
setItemsVersion((v) => v + 1);
|
|
151
|
+
}, []);
|
|
152
|
+
|
|
153
|
+
// Sync internal value with controlled value
|
|
154
|
+
React.useEffect(() => {
|
|
155
|
+
if (value !== undefined) {
|
|
156
|
+
setInternalValue(value);
|
|
157
|
+
}
|
|
158
|
+
}, [value]);
|
|
159
|
+
|
|
160
|
+
const handleValueChange = React.useCallback(
|
|
161
|
+
(newValue: SelectValue | null) => {
|
|
162
|
+
if (value === undefined) {
|
|
163
|
+
// Uncontrolled mode
|
|
164
|
+
setInternalValue(newValue);
|
|
165
|
+
}
|
|
166
|
+
onValueChange?.(newValue);
|
|
167
|
+
},
|
|
168
|
+
[value, onValueChange]
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const contextValue = React.useMemo(
|
|
172
|
+
() => ({
|
|
173
|
+
placeholder,
|
|
174
|
+
value: value !== undefined ? value : internalValue,
|
|
175
|
+
itemsRef,
|
|
176
|
+
itemsVersion,
|
|
177
|
+
incrementItemsVersion,
|
|
178
|
+
}),
|
|
179
|
+
[placeholder, value, internalValue, itemsVersion, incrementItemsVersion]
|
|
180
|
+
);
|
|
181
|
+
|
|
126
182
|
return (
|
|
127
|
-
<SelectContext.Provider value={
|
|
183
|
+
<SelectContext.Provider value={contextValue}>
|
|
128
184
|
<BaseSelect.Root
|
|
129
185
|
value={value}
|
|
130
186
|
defaultValue={defaultValue}
|
|
131
|
-
onValueChange={
|
|
187
|
+
onValueChange={handleValueChange}
|
|
132
188
|
open={open}
|
|
133
189
|
defaultOpen={defaultOpen}
|
|
134
190
|
onOpenChange={onOpenChange}
|
|
@@ -148,11 +204,24 @@ function SelectTrigger({ children, placeholder, className, ...htmlProps }: Selec
|
|
|
148
204
|
|
|
149
205
|
const classes = [styles.trigger, className].filter(Boolean).join(' ');
|
|
150
206
|
|
|
207
|
+
// Get the selected item's children from the registry
|
|
208
|
+
// Note: itemsVersion in context ensures we re-render when items register
|
|
209
|
+
const selectedContent = context.value != null
|
|
210
|
+
? context.itemsRef.current.get(context.value)
|
|
211
|
+
: null;
|
|
212
|
+
|
|
213
|
+
// Determine what to show in the value area
|
|
214
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
215
|
+
const _version = context.itemsVersion; // Force dependency on itemsVersion for re-render
|
|
216
|
+
const displayContent = selectedContent ?? (
|
|
217
|
+
placeholderText ? <span className={styles.placeholder}>{placeholderText}</span> : null
|
|
218
|
+
);
|
|
219
|
+
|
|
151
220
|
return (
|
|
152
221
|
<BaseSelect.Trigger {...htmlProps} className={classes}>
|
|
153
222
|
{children ?? (
|
|
154
223
|
<>
|
|
155
|
-
<
|
|
224
|
+
<span className={styles.value}>{displayContent}</span>
|
|
156
225
|
<BaseSelect.Icon className={styles.icon}>
|
|
157
226
|
<ChevronDownIcon />
|
|
158
227
|
</BaseSelect.Icon>
|
|
@@ -187,8 +256,19 @@ function SelectContent({
|
|
|
187
256
|
}
|
|
188
257
|
|
|
189
258
|
function SelectItem({ children, value, disabled, className }: SelectItemProps) {
|
|
259
|
+
const context = React.useContext(SelectContext);
|
|
190
260
|
const classes = [styles.item, className].filter(Boolean).join(' ');
|
|
191
261
|
|
|
262
|
+
// Register this item's children in the registry so the trigger can display them
|
|
263
|
+
React.useEffect(() => {
|
|
264
|
+
context.itemsRef.current.set(value, children);
|
|
265
|
+
// Trigger re-render of trigger to show the registered content
|
|
266
|
+
context.incrementItemsVersion();
|
|
267
|
+
return () => {
|
|
268
|
+
context.itemsRef.current.delete(value);
|
|
269
|
+
};
|
|
270
|
+
}, [context, value, children]);
|
|
271
|
+
|
|
192
272
|
return (
|
|
193
273
|
<BaseSelect.Item value={value} disabled={disabled} className={classes}>
|
|
194
274
|
<BaseSelect.ItemText>{children}</BaseSelect.ItemText>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { defineSegment } from '@fragments/core';
|
|
3
|
-
import { Sidebar, SidebarProvider, useSidebar } from '
|
|
4
|
-
import { Button } from '../Button
|
|
3
|
+
import { Sidebar, SidebarProvider, useSidebar } from '.';
|
|
4
|
+
import { Button } from '../Button';
|
|
5
5
|
|
|
6
6
|
// Example icons (inline SVGs for demo)
|
|
7
7
|
const HomeIcon = () => (
|
|
@@ -159,6 +159,25 @@
|
|
|
159
159
|
padding: 0;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// Collapsible section styles (using Collapsible component)
|
|
163
|
+
.sectionCollapsible {
|
|
164
|
+
// Override Collapsible trigger styles for sidebar context
|
|
165
|
+
:global(.collapsible-trigger),
|
|
166
|
+
button[aria-expanded] {
|
|
167
|
+
background: transparent;
|
|
168
|
+
justify-content: space-between;
|
|
169
|
+
padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
|
|
170
|
+
|
|
171
|
+
&:hover {
|
|
172
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.sectionContent {
|
|
178
|
+
// Content container - no extra padding needed
|
|
179
|
+
}
|
|
180
|
+
|
|
162
181
|
// ============================================
|
|
163
182
|
// Item
|
|
164
183
|
// ============================================
|
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
import styles from './Sidebar.module.scss';
|
|
3
3
|
import { Tooltip } from '../Tooltip';
|
|
4
4
|
import { Skeleton } from '../Skeleton';
|
|
5
|
+
import { Collapsible } from '../Collapsible';
|
|
5
6
|
// Import globals to ensure CSS variables are defined
|
|
6
7
|
import '../../styles/globals.scss';
|
|
7
8
|
|
|
@@ -82,6 +83,10 @@ export interface SidebarSectionProps {
|
|
|
82
83
|
label?: string;
|
|
83
84
|
/** Action element to display in the section header (e.g., "Add" button) */
|
|
84
85
|
action?: React.ReactNode;
|
|
86
|
+
/** Enable collapsible behavior */
|
|
87
|
+
collapsible?: boolean;
|
|
88
|
+
/** Default expanded state (only applies when collapsible is true) */
|
|
89
|
+
defaultOpen?: boolean;
|
|
85
90
|
className?: string;
|
|
86
91
|
}
|
|
87
92
|
|
|
@@ -618,23 +623,59 @@ function SidebarNav({ children, 'aria-label': ariaLabel = 'Main navigation', cla
|
|
|
618
623
|
);
|
|
619
624
|
}
|
|
620
625
|
|
|
621
|
-
function SidebarSection({
|
|
626
|
+
function SidebarSection({
|
|
627
|
+
children,
|
|
628
|
+
label,
|
|
629
|
+
action,
|
|
630
|
+
collapsible: isCollapsibleProp = false,
|
|
631
|
+
defaultOpen = true,
|
|
632
|
+
className
|
|
633
|
+
}: SidebarSectionProps) {
|
|
622
634
|
const { collapsed, isMobile } = useSidebarContext();
|
|
623
|
-
|
|
635
|
+
|
|
636
|
+
const classes = [
|
|
637
|
+
styles.section,
|
|
638
|
+
className
|
|
639
|
+
].filter(Boolean).join(' ');
|
|
640
|
+
|
|
624
641
|
const showLabel = label && (!collapsed || isMobile);
|
|
625
642
|
const showAction = action && (!collapsed || isMobile);
|
|
643
|
+
const isCollapsible = isCollapsibleProp && showLabel;
|
|
644
|
+
|
|
645
|
+
// Non-collapsible section
|
|
646
|
+
if (!isCollapsible) {
|
|
647
|
+
return (
|
|
648
|
+
<div className={classes} role="group" aria-label={label}>
|
|
649
|
+
{(showLabel || showAction) && (
|
|
650
|
+
<div className={styles.sectionHeader}>
|
|
651
|
+
{showLabel && <div className={styles.sectionLabel}>{label}</div>}
|
|
652
|
+
{showAction && <div className={styles.sectionActionWrapper}>{action}</div>}
|
|
653
|
+
</div>
|
|
654
|
+
)}
|
|
655
|
+
<ul className={styles.sectionList}>
|
|
656
|
+
{children}
|
|
657
|
+
</ul>
|
|
658
|
+
</div>
|
|
659
|
+
);
|
|
660
|
+
}
|
|
626
661
|
|
|
662
|
+
// Collapsible section using Collapsible component
|
|
627
663
|
return (
|
|
628
664
|
<div className={classes} role="group" aria-label={label}>
|
|
629
|
-
{
|
|
630
|
-
<
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
665
|
+
<Collapsible defaultOpen={defaultOpen} className={styles.sectionCollapsible}>
|
|
666
|
+
<Collapsible.Trigger
|
|
667
|
+
className={styles.sectionHeader}
|
|
668
|
+
chevronPosition="end"
|
|
669
|
+
>
|
|
670
|
+
<span className={styles.sectionLabel}>{label}</span>
|
|
671
|
+
{showAction && <span className={styles.sectionActionWrapper} onClick={(e) => e.stopPropagation()}>{action}</span>}
|
|
672
|
+
</Collapsible.Trigger>
|
|
673
|
+
<Collapsible.Content className={styles.sectionContent}>
|
|
674
|
+
<ul className={styles.sectionList}>
|
|
675
|
+
{children}
|
|
676
|
+
</ul>
|
|
677
|
+
</Collapsible.Content>
|
|
678
|
+
</Collapsible>
|
|
638
679
|
</div>
|
|
639
680
|
);
|
|
640
681
|
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { Slider } from '.';
|
|
4
|
+
|
|
5
|
+
export default defineSegment({
|
|
6
|
+
component: Slider,
|
|
7
|
+
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'Slider',
|
|
10
|
+
description: 'Range input control for selecting a numeric value within a defined range. Supports labels, value display, and custom step intervals.',
|
|
11
|
+
category: 'forms',
|
|
12
|
+
status: 'stable',
|
|
13
|
+
tags: ['slider', 'range', 'input', 'number', 'control'],
|
|
14
|
+
since: '0.2.0',
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
usage: {
|
|
18
|
+
when: [
|
|
19
|
+
'Selecting a value from a continuous range',
|
|
20
|
+
'Volume or brightness controls',
|
|
21
|
+
'Price range filters',
|
|
22
|
+
'Settings that benefit from visual feedback',
|
|
23
|
+
],
|
|
24
|
+
whenNot: [
|
|
25
|
+
'Precise numeric input (use Input type="number")',
|
|
26
|
+
'Discrete options (use RadioGroup or Select)',
|
|
27
|
+
'Yes/no choices (use Toggle)',
|
|
28
|
+
],
|
|
29
|
+
guidelines: [
|
|
30
|
+
'Always provide a label describing what the slider controls',
|
|
31
|
+
'Show the current value when precision matters',
|
|
32
|
+
'Use appropriate min/max values for the context',
|
|
33
|
+
'Consider step size for usability',
|
|
34
|
+
],
|
|
35
|
+
accessibility: [
|
|
36
|
+
'Label is associated with the slider',
|
|
37
|
+
'Keyboard accessible with arrow keys',
|
|
38
|
+
'Current value is announced to screen readers',
|
|
39
|
+
'Uses native slider semantics',
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
props: {
|
|
44
|
+
value: {
|
|
45
|
+
type: 'number',
|
|
46
|
+
description: 'Controlled value',
|
|
47
|
+
},
|
|
48
|
+
defaultValue: {
|
|
49
|
+
type: 'number',
|
|
50
|
+
description: 'Default value for uncontrolled usage',
|
|
51
|
+
},
|
|
52
|
+
onChange: {
|
|
53
|
+
type: 'function',
|
|
54
|
+
description: 'Called with new value when changed',
|
|
55
|
+
},
|
|
56
|
+
min: {
|
|
57
|
+
type: 'number',
|
|
58
|
+
description: 'Minimum value',
|
|
59
|
+
default: '0',
|
|
60
|
+
},
|
|
61
|
+
max: {
|
|
62
|
+
type: 'number',
|
|
63
|
+
description: 'Maximum value',
|
|
64
|
+
default: '100',
|
|
65
|
+
},
|
|
66
|
+
step: {
|
|
67
|
+
type: 'number',
|
|
68
|
+
description: 'Step interval',
|
|
69
|
+
default: '1',
|
|
70
|
+
},
|
|
71
|
+
label: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'Label text',
|
|
74
|
+
},
|
|
75
|
+
showValue: {
|
|
76
|
+
type: 'boolean',
|
|
77
|
+
description: 'Display current value',
|
|
78
|
+
default: 'false',
|
|
79
|
+
},
|
|
80
|
+
valueSuffix: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: 'Suffix after value (e.g., "%", "px")',
|
|
83
|
+
},
|
|
84
|
+
disabled: {
|
|
85
|
+
type: 'boolean',
|
|
86
|
+
description: 'Disable the slider',
|
|
87
|
+
default: 'false',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
relations: [
|
|
92
|
+
{ component: 'Input', relationship: 'alternative', note: 'Use Input for precise numeric entry' },
|
|
93
|
+
{ component: 'Progress', relationship: 'sibling', note: 'Similar visual, but Progress is read-only' },
|
|
94
|
+
],
|
|
95
|
+
|
|
96
|
+
contract: {
|
|
97
|
+
propsSummary: [
|
|
98
|
+
'value: number - controlled value',
|
|
99
|
+
'defaultValue: number - initial value',
|
|
100
|
+
'onChange: (value: number) => void - change handler',
|
|
101
|
+
'min/max: number - range bounds',
|
|
102
|
+
'step: number - increment size',
|
|
103
|
+
'label: string - field label',
|
|
104
|
+
'showValue: boolean - display value',
|
|
105
|
+
'valueSuffix: string - unit suffix',
|
|
106
|
+
],
|
|
107
|
+
scenarioTags: [
|
|
108
|
+
'forms.range',
|
|
109
|
+
'input.numeric',
|
|
110
|
+
'control.slider',
|
|
111
|
+
],
|
|
112
|
+
a11yRules: ['A11Y_LABEL_REQUIRED', 'A11Y_KEYBOARD_ACCESSIBLE'],
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
variants: [
|
|
116
|
+
{
|
|
117
|
+
name: 'Default',
|
|
118
|
+
description: 'Basic slider with label',
|
|
119
|
+
render: () => (
|
|
120
|
+
<div style={{ width: '300px' }}>
|
|
121
|
+
<Slider label="Volume" defaultValue={50} />
|
|
122
|
+
</div>
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'With Value Display',
|
|
127
|
+
description: 'Shows current value alongside the slider',
|
|
128
|
+
render: () => (
|
|
129
|
+
<div style={{ width: '300px' }}>
|
|
130
|
+
<Slider
|
|
131
|
+
label="Brightness"
|
|
132
|
+
defaultValue={75}
|
|
133
|
+
showValue
|
|
134
|
+
valueSuffix="%"
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
),
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'Custom Range',
|
|
141
|
+
description: 'Custom min, max, and step values',
|
|
142
|
+
render: () => (
|
|
143
|
+
<div style={{ width: '300px', display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
|
144
|
+
<Slider
|
|
145
|
+
label="Temperature"
|
|
146
|
+
min={60}
|
|
147
|
+
max={80}
|
|
148
|
+
step={1}
|
|
149
|
+
defaultValue={72}
|
|
150
|
+
showValue
|
|
151
|
+
valueSuffix="°F"
|
|
152
|
+
/>
|
|
153
|
+
<Slider
|
|
154
|
+
label="Font Size"
|
|
155
|
+
min={12}
|
|
156
|
+
max={32}
|
|
157
|
+
step={2}
|
|
158
|
+
defaultValue={16}
|
|
159
|
+
showValue
|
|
160
|
+
valueSuffix="px"
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'Controlled',
|
|
167
|
+
description: 'Controlled slider with external state',
|
|
168
|
+
render: () => {
|
|
169
|
+
const [value, setValue] = React.useState(50);
|
|
170
|
+
return (
|
|
171
|
+
<div style={{ width: '300px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
172
|
+
<Slider
|
|
173
|
+
label="Opacity"
|
|
174
|
+
value={value}
|
|
175
|
+
onChange={setValue}
|
|
176
|
+
showValue
|
|
177
|
+
valueSuffix="%"
|
|
178
|
+
/>
|
|
179
|
+
<div style={{ fontSize: '14px', color: 'var(--fui-text-secondary)' }}>
|
|
180
|
+
Current value: {value}%
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'Disabled',
|
|
188
|
+
description: 'Disabled slider',
|
|
189
|
+
render: () => (
|
|
190
|
+
<div style={{ width: '300px' }}>
|
|
191
|
+
<Slider
|
|
192
|
+
label="Locked Setting"
|
|
193
|
+
defaultValue={30}
|
|
194
|
+
showValue
|
|
195
|
+
disabled
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
),
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
});
|