@fragments-sdk/ui 0.9.6 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/codeblock.cjs +25 -29
  2. package/dist/codeblock.cjs.map +1 -1
  3. package/dist/codeblock.js +25 -29
  4. package/dist/codeblock.js.map +1 -1
  5. package/dist/components/Chip/index.cjs +2 -1
  6. package/dist/components/Chip/index.cjs.map +1 -1
  7. package/dist/components/Chip/index.d.ts.map +1 -1
  8. package/dist/components/Chip/index.js +2 -1
  9. package/dist/components/Chip/index.js.map +1 -1
  10. package/dist/components/CodeBlock/index.d.ts.map +1 -1
  11. package/dist/components/Command/index.cjs +6 -0
  12. package/dist/components/Command/index.cjs.map +1 -1
  13. package/dist/components/Command/index.d.ts.map +1 -1
  14. package/dist/components/Command/index.js +6 -0
  15. package/dist/components/Command/index.js.map +1 -1
  16. package/dist/components/DataTable/index.cjs +26 -26
  17. package/dist/components/DataTable/index.cjs.map +1 -1
  18. package/dist/components/DataTable/index.d.ts.map +1 -1
  19. package/dist/components/DataTable/index.js +26 -26
  20. package/dist/components/DataTable/index.js.map +1 -1
  21. package/dist/components/Listbox/index.cjs +6 -0
  22. package/dist/components/Listbox/index.cjs.map +1 -1
  23. package/dist/components/Listbox/index.d.ts.map +1 -1
  24. package/dist/components/Listbox/index.js +6 -0
  25. package/dist/components/Listbox/index.js.map +1 -1
  26. package/dist/components/Loading/index.cjs +2 -12
  27. package/dist/components/Loading/index.cjs.map +1 -1
  28. package/dist/components/Loading/index.d.ts.map +1 -1
  29. package/dist/components/Loading/index.js +2 -12
  30. package/dist/components/Loading/index.js.map +1 -1
  31. package/dist/components/NavigationMenu/index.cjs +12 -1
  32. package/dist/components/NavigationMenu/index.cjs.map +1 -1
  33. package/dist/components/NavigationMenu/index.d.ts.map +1 -1
  34. package/dist/components/NavigationMenu/index.js +12 -1
  35. package/dist/components/NavigationMenu/index.js.map +1 -1
  36. package/dist/components/Skeleton/index.cjs +3 -3
  37. package/dist/components/Skeleton/index.cjs.map +1 -1
  38. package/dist/components/Skeleton/index.js +3 -3
  39. package/dist/components/Skeleton/index.js.map +1 -1
  40. package/dist/components/Stack/index.cjs +4 -3
  41. package/dist/components/Stack/index.cjs.map +1 -1
  42. package/dist/components/Stack/index.d.ts.map +1 -1
  43. package/dist/components/Stack/index.js +4 -3
  44. package/dist/components/Stack/index.js.map +1 -1
  45. package/dist/markdown.cjs +1 -1
  46. package/dist/markdown.cjs.map +1 -1
  47. package/dist/markdown.js +1 -1
  48. package/dist/markdown.js.map +1 -1
  49. package/fragments.json +1 -1
  50. package/package.json +1 -1
  51. package/src/components/Chip/index.tsx +3 -1
  52. package/src/components/CodeBlock/index.tsx +35 -41
  53. package/src/components/ColorPicker/ColorPicker.fragment.tsx +17 -15
  54. package/src/components/Command/index.tsx +1 -0
  55. package/src/components/DataTable/index.tsx +45 -45
  56. package/src/components/Listbox/index.tsx +1 -0
  57. package/src/components/Loading/index.tsx +6 -12
  58. package/src/components/Markdown/index.tsx +2 -2
  59. package/src/components/Menu/Menu.fragment.tsx +17 -15
  60. package/src/components/NavigationMenu/index.tsx +6 -1
  61. package/src/components/Skeleton/index.tsx +3 -3
  62. package/src/components/Slider/Slider.fragment.tsx +19 -17
  63. package/src/components/Stack/index.tsx +4 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fragments-sdk/ui",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "license": "MIT",
5
5
  "description": "Customizable UI components built on Base UI headless primitives",
6
6
  "author": "Conan McNicholl",
@@ -113,8 +113,10 @@ const ChipBase = React.forwardRef<HTMLButtonElement, ChipProps>(
113
113
  }
114
114
  );
115
115
 
116
+ const EMPTY_CHIP_GROUP: string[] = [];
117
+
116
118
  function ChipGroupInner(
117
- { children, value: controlledValue, defaultValue = [], onChange, className }: ChipGroupProps,
119
+ { children, value: controlledValue, defaultValue = EMPTY_CHIP_GROUP, onChange, className }: ChipGroupProps,
118
120
  ref: React.Ref<HTMLDivElement>
119
121
  ) {
120
122
  const [internalValue, setInternalValue] = React.useState<string[]>(defaultValue);
@@ -544,8 +544,7 @@ const CodeBlockBase = React.forwardRef<HTMLDivElement, CodeBlockProps>(function
544
544
  ref
545
545
  ) {
546
546
  const [copied, setCopied] = useState(false);
547
- const [highlightedHtml, setHighlightedHtml] = useState<string>("");
548
- const [isLoading, setIsLoading] = useState(true);
547
+ const [highlight, setHighlight] = useState<{ html: string; loading: boolean }>({ html: '', loading: true });
549
548
  const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
550
549
 
551
550
  const trimmedCode = useMemo(() => normalizeCode(code), [code]);
@@ -570,50 +569,45 @@ const CodeBlockBase = React.forwardRef<HTMLDivElement, CodeBlockProps>(function
570
569
  // Apply syntax highlighting
571
570
  useEffect(() => {
572
571
  let cancelled = false;
573
- setIsLoading(true);
572
+ setHighlight((prev) => ({ ...prev, loading: true }));
574
573
 
575
- loadShikiDeps();
574
+ const run = async () => {
575
+ loadShikiDeps();
576
576
 
577
- if (_shikiFailed || !_codeToHtml) {
578
- if (_shikiFailed && process.env.NODE_ENV === "development") {
579
- console.warn(
580
- "[@fragments-sdk/ui] CodeBlock: shiki is not installed. " +
581
- "Install it with: npm install shiki"
582
- );
583
- }
584
- // Fallback to plain text without syntax highlighting
585
- setHighlightedHtml(`<pre class="shiki"><code>${escapeHtml(visibleCode)}</code></pre>`);
586
- setIsLoading(false);
587
- return;
588
- }
577
+ const fallbackHtml = `<pre class="shiki"><code>${escapeHtml(visibleCode)}</code></pre>`;
589
578
 
590
- _codeToHtml(visibleCode, {
591
- lang: language,
592
- theme,
593
- })
594
- .then((html) => {
595
- if (!cancelled) {
596
- const processed = processShikiHtml(html, {
597
- showLineNumbers,
598
- startLineNumber,
599
- highlightLines: highlightSet,
600
- addedLines: addedSet,
601
- removedLines: removedSet,
602
- });
603
- setHighlightedHtml(processed);
604
- setIsLoading(false);
579
+ if (_shikiFailed || !_codeToHtml) {
580
+ if (_shikiFailed && process.env.NODE_ENV === "development") {
581
+ console.warn(
582
+ "[@fragments-sdk/ui] CodeBlock: shiki is not installed. " +
583
+ "Install it with: npm install shiki"
584
+ );
605
585
  }
606
- })
607
- .catch((err) => {
586
+ return fallbackHtml;
587
+ }
588
+
589
+ try {
590
+ const html = await _codeToHtml(visibleCode, { lang: language, theme });
591
+ return processShikiHtml(html, {
592
+ showLineNumbers,
593
+ startLineNumber,
594
+ highlightLines: highlightSet,
595
+ addedLines: addedSet,
596
+ removedLines: removedSet,
597
+ });
598
+ } catch (err) {
608
599
  if (process.env.NODE_ENV !== "production") {
609
600
  console.error("Syntax highlighting failed:", err);
610
601
  }
611
- if (!cancelled) {
612
- // Fallback to plain text
613
- setHighlightedHtml(`<pre class="shiki"><code>${escapeHtml(visibleCode)}</code></pre>`);
614
- setIsLoading(false);
615
- }
616
- });
602
+ return fallbackHtml;
603
+ }
604
+ };
605
+
606
+ run().then((html) => {
607
+ if (!cancelled) {
608
+ setHighlight({ html, loading: false });
609
+ }
610
+ });
617
611
 
618
612
  return () => {
619
613
  cancelled = true;
@@ -701,7 +695,7 @@ const CodeBlockBase = React.forwardRef<HTMLDivElement, CodeBlockProps>(function
701
695
  {copied ? <CheckIcon className={styles.icon} /> : <CopyIcon className={styles.icon} />}
702
696
  </button>
703
697
  )}
704
- {isLoading ? (
698
+ {highlight.loading ? (
705
699
  <div className={styles.loading} style={codeContainerStyle}>
706
700
  <pre>
707
701
  <code>{visibleCode}</code>
@@ -711,7 +705,7 @@ const CodeBlockBase = React.forwardRef<HTMLDivElement, CodeBlockProps>(function
711
705
  <div
712
706
  className={styles.codeContainer}
713
707
  style={codeContainerStyle}
714
- dangerouslySetInnerHTML={{ __html: highlightedHtml }}
708
+ dangerouslySetInnerHTML={{ __html: highlight.html }}
715
709
  />
716
710
  )}
717
711
  {persistentCopy && (
@@ -2,6 +2,22 @@ import React from 'react';
2
2
  import { defineFragment } from '@fragments-sdk/cli/core';
3
3
  import { ColorPicker } from '.';
4
4
 
5
+ function ControlledColorPickerDemo() {
6
+ const [color, setColor] = React.useState('#ef4444');
7
+ return (
8
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
9
+ <ColorPicker
10
+ label="Accent Color"
11
+ value={color}
12
+ onChange={setColor}
13
+ />
14
+ <div style={{ fontSize: '14px', color: 'var(--fui-text-secondary)' }}>
15
+ Selected: {color}
16
+ </div>
17
+ </div>
18
+ );
19
+ }
20
+
5
21
  export default defineFragment({
6
22
  component: ColorPicker,
7
23
 
@@ -133,21 +149,7 @@ export default defineFragment({
133
149
  {
134
150
  name: 'Controlled',
135
151
  description: 'Controlled color picker that logs changes',
136
- render: () => {
137
- const [color, setColor] = React.useState('#ef4444');
138
- return (
139
- <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
140
- <ColorPicker
141
- label="Accent Color"
142
- value={color}
143
- onChange={setColor}
144
- />
145
- <div style={{ fontSize: '14px', color: 'var(--fui-text-secondary)' }}>
146
- Selected: {color}
147
- </div>
148
- </div>
149
- );
150
- },
152
+ render: () => <ControlledColorPickerDemo />,
151
153
  },
152
154
  {
153
155
  name: 'Multiple Pickers',
@@ -400,6 +400,7 @@ function CommandItem({
400
400
  data-active={isActive || undefined}
401
401
  data-disabled={disabled || undefined}
402
402
  onClick={handleClick}
403
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleClick(); } }}
403
404
  onMouseEnter={handleMouseEnter}
404
405
  className={[
405
406
  styles.item,
@@ -102,6 +102,41 @@ export interface DataTableProps<T> extends Omit<React.HTMLAttributes<HTMLTableEl
102
102
  bordered?: boolean;
103
103
  }
104
104
 
105
+ function getColumnSizeStyle(
106
+ column: {
107
+ getSize: () => number;
108
+ columnDef: { size?: number; minSize?: number; maxSize?: number };
109
+ }
110
+ ): React.CSSProperties | undefined {
111
+ const { size, minSize, maxSize } = column.columnDef;
112
+ const hasExplicitSize = size !== undefined || minSize !== undefined || maxSize !== undefined;
113
+
114
+ if (!hasExplicitSize) {
115
+ return undefined;
116
+ }
117
+
118
+ const resolvedSize = column.getSize();
119
+
120
+ return {
121
+ width: resolvedSize,
122
+ minWidth: minSize ?? resolvedSize,
123
+ maxWidth: maxSize ?? resolvedSize,
124
+ };
125
+ }
126
+
127
+ function isInteractiveTarget(
128
+ target: EventTarget | null,
129
+ currentTarget: HTMLTableRowElement
130
+ ) {
131
+ if (!(target instanceof Element)) return false;
132
+
133
+ const interactiveElement = target.closest(
134
+ 'button, a, input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="switch"]'
135
+ );
136
+
137
+ return Boolean(interactiveElement && currentTarget.contains(interactiveElement));
138
+ }
139
+
105
140
  function DataTableRoot<T>({
106
141
  columns: userColumns,
107
142
  data,
@@ -178,6 +213,16 @@ function DataTableRoot<T>({
178
213
  return [checkboxColumn, ...userColumns];
179
214
  }, [userColumns, showCheckbox, selectable]);
180
215
 
216
+ const hasExplicitColumnSizing = React.useMemo(
217
+ () =>
218
+ columns.some((column) =>
219
+ column.size !== undefined ||
220
+ column.minSize !== undefined ||
221
+ column.maxSize !== undefined
222
+ ),
223
+ [columns]
224
+ );
225
+
181
226
  if (_tableFailed || !_useReactTable) {
182
227
  if (_tableFailed && process.env.NODE_ENV === 'development') {
183
228
  console.warn(
@@ -213,16 +258,6 @@ function DataTableRoot<T>({
213
258
 
214
259
  const isEmpty = data.length === 0;
215
260
 
216
- const hasExplicitColumnSizing = React.useMemo(
217
- () =>
218
- columns.some((column) =>
219
- column.size !== undefined ||
220
- column.minSize !== undefined ||
221
- column.maxSize !== undefined
222
- ),
223
- [columns]
224
- );
225
-
226
261
  const rootClasses = [
227
262
  styles.table,
228
263
  hasExplicitColumnSizing && styles.fixedLayout,
@@ -233,28 +268,6 @@ function DataTableRoot<T>({
233
268
  .filter(Boolean)
234
269
  .join(' ');
235
270
 
236
- const getColumnSizeStyle = (
237
- column: {
238
- getSize: () => number;
239
- columnDef: { size?: number; minSize?: number; maxSize?: number };
240
- }
241
- ): React.CSSProperties | undefined => {
242
- const { size, minSize, maxSize } = column.columnDef;
243
- const hasExplicitSize = size !== undefined || minSize !== undefined || maxSize !== undefined;
244
-
245
- if (!hasExplicitSize) {
246
- return undefined;
247
- }
248
-
249
- const resolvedSize = column.getSize();
250
-
251
- return {
252
- width: resolvedSize,
253
- minWidth: minSize ?? resolvedSize,
254
- maxWidth: maxSize ?? resolvedSize,
255
- };
256
- };
257
-
258
271
  if (isEmpty) {
259
272
  return (
260
273
  <div className={styles.emptyState}>
@@ -263,19 +276,6 @@ function DataTableRoot<T>({
263
276
  );
264
277
  }
265
278
 
266
- const isInteractiveTarget = (
267
- target: EventTarget | null,
268
- currentTarget: HTMLTableRowElement
269
- ) => {
270
- if (!(target instanceof Element)) return false;
271
-
272
- const interactiveElement = target.closest(
273
- 'button, a, input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="switch"]'
274
- );
275
-
276
- return Boolean(interactiveElement && currentTarget.contains(interactiveElement));
277
- };
278
-
279
279
  return (
280
280
  <div className={[styles.wrapper, bordered && styles.bordered].filter(Boolean).join(' ')}>
281
281
  <table
@@ -243,6 +243,7 @@ function ListboxItem({
243
243
  aria-disabled={disabled}
244
244
  data-active={context?.activeId === itemId || undefined}
245
245
  onClick={disabled ? undefined : onClick}
246
+ onKeyDown={disabled ? undefined : (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick?.(); } }}
246
247
  onMouseEnter={handleMouseEnter}
247
248
  className={classes}
248
249
  style={style}
@@ -118,17 +118,11 @@ const LoadingRoot = React.forwardRef<HTMLDivElement, LoadingProps>(
118
118
  .filter(Boolean)
119
119
  .join(' ');
120
120
 
121
- const renderAnimation = () => {
122
- switch (variant) {
123
- case 'dots':
124
- return <DotsAnimation className={styles.dots} />;
125
- case 'pulse':
126
- return <PulseAnimation className={styles.pulse} />;
127
- case 'spinner':
128
- default:
129
- return <SpinnerIcon className={styles.spinnerIcon} />;
130
- }
131
- };
121
+ const animation = variant === 'dots'
122
+ ? <DotsAnimation className={styles.dots} />
123
+ : variant === 'pulse'
124
+ ? <PulseAnimation className={styles.pulse} />
125
+ : <SpinnerIcon className={styles.spinnerIcon} />;
132
126
 
133
127
  const content = (
134
128
  <div
@@ -139,7 +133,7 @@ const LoadingRoot = React.forwardRef<HTMLDivElement, LoadingProps>(
139
133
  aria-live="polite"
140
134
  {...htmlProps}
141
135
  >
142
- {renderAnimation()}
136
+ {animation}
143
137
  </div>
144
138
  );
145
139
 
@@ -59,8 +59,8 @@ function FallbackRenderer({ content, className }: { content: string; className?:
59
59
  const paragraphs = content.split(/\n{2,}/);
60
60
  return (
61
61
  <div className={className}>
62
- {paragraphs.map((p, i) => (
63
- <p key={i}>{p}</p>
62
+ {paragraphs.map((p) => (
63
+ <p key={p}>{p}</p>
64
64
  ))}
65
65
  </div>
66
66
  );
@@ -3,6 +3,22 @@ import { defineFragment } from '@fragments-sdk/cli/core';
3
3
  import { Menu } from '.';
4
4
  import { Button } from '../Button';
5
5
 
6
+ function CheckedItemsMenuDemo() {
7
+ const [view, setView] = React.useState('grid');
8
+ return (
9
+ <Menu>
10
+ <Menu.Trigger asChild>
11
+ <Button variant="secondary">View</Button>
12
+ </Menu.Trigger>
13
+ <Menu.Content>
14
+ <Menu.Item checked={view === 'grid'} onSelect={() => setView('grid')}>Grid</Menu.Item>
15
+ <Menu.Item checked={view === 'list'} onSelect={() => setView('list')}>List</Menu.Item>
16
+ <Menu.Item checked={view === 'board'} onSelect={() => setView('board')}>Board</Menu.Item>
17
+ </Menu.Content>
18
+ </Menu>
19
+ );
20
+ }
21
+
6
22
  export default defineFragment({
7
23
  component: Menu,
8
24
 
@@ -181,21 +197,7 @@ export default defineFragment({
181
197
  {
182
198
  name: 'With Checked Items',
183
199
  description: 'Filter menu with check marks indicating active selections',
184
- render: () => {
185
- const [view, setView] = React.useState('grid');
186
- return (
187
- <Menu>
188
- <Menu.Trigger asChild>
189
- <Button variant="secondary">View</Button>
190
- </Menu.Trigger>
191
- <Menu.Content>
192
- <Menu.Item checked={view === 'grid'} onSelect={() => setView('grid')}>Grid</Menu.Item>
193
- <Menu.Item checked={view === 'list'} onSelect={() => setView('list')}>List</Menu.Item>
194
- <Menu.Item checked={view === 'board'} onSelect={() => setView('board')}>Board</Menu.Item>
195
- </Menu.Content>
196
- </Menu>
197
- );
198
- },
200
+ render: () => <CheckedItemsMenuDemo />,
199
201
  },
200
202
  {
201
203
  name: 'With Submenu',
@@ -840,7 +840,12 @@ function MobileCollapsibleSection({
840
840
  {label}
841
841
  </Collapsible.Trigger>
842
842
  <Collapsible.Content>
843
- <div className={styles.drawerCollapsibleContent} onClick={onLinkClick}>
843
+ <div
844
+ className={styles.drawerCollapsibleContent}
845
+ onClick={onLinkClick}
846
+ onKeyDown={(e) => { if (e.key === 'Enter') onLinkClick(); }}
847
+ role="group"
848
+ >
844
849
  {children}
845
850
  </div>
846
851
  </Collapsible.Content>
@@ -134,11 +134,11 @@ function SkeletonText({
134
134
 
135
135
  return (
136
136
  <div className={containerClasses} aria-hidden="true">
137
- {Array.from({ length: lines }, (_, i) => {
138
- const isLast = i === lines - 1;
137
+ {Array.from({ length: lines }, (_, lineIdx) => {
138
+ const isLast = lineIdx === lines - 1;
139
139
  return (
140
140
  <div
141
- key={i}
141
+ key={`line-${lineIdx}`}
142
142
  className={styles.textLine}
143
143
  style={isLast && lines > 1 ? { width: `${lastLineWidth}%` } : undefined}
144
144
  />
@@ -2,6 +2,24 @@ import React from 'react';
2
2
  import { defineFragment } from '@fragments-sdk/cli/core';
3
3
  import { Slider } from '.';
4
4
 
5
+ function ControlledSliderDemo() {
6
+ const [value, setValue] = React.useState(50);
7
+ return (
8
+ <div style={{ width: '300px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
9
+ <Slider
10
+ label="Opacity"
11
+ value={value}
12
+ onChange={setValue}
13
+ showValue
14
+ valueSuffix="%"
15
+ />
16
+ <div style={{ fontSize: '14px', color: 'var(--fui-text-secondary)' }}>
17
+ Current value: {value}%
18
+ </div>
19
+ </div>
20
+ );
21
+ }
22
+
5
23
  export default defineFragment({
6
24
  component: Slider,
7
25
 
@@ -165,23 +183,7 @@ export default defineFragment({
165
183
  {
166
184
  name: 'Controlled',
167
185
  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
- },
186
+ render: () => <ControlledSliderDemo />,
185
187
  },
186
188
  {
187
189
  name: 'Disabled',
@@ -147,11 +147,12 @@ const StackRoot = React.forwardRef<HTMLElement, StackProps>(
147
147
  ) : separator;
148
148
 
149
149
  const items: React.ReactNode[] = [];
150
- validChildren.forEach((child, i) => {
150
+ validChildren.forEach((child, idx) => {
151
151
  items.push(child);
152
- if (i < validChildren.length - 1) {
152
+ if (idx < validChildren.length - 1) {
153
+ const childKey = React.isValidElement(child) && child.key != null ? child.key : `idx-${idx}`;
153
154
  items.push(
154
- <React.Fragment key={`sep-${i}`}>{separatorEl}</React.Fragment>
155
+ <React.Fragment key={`sep-${childKey}`}>{separatorEl}</React.Fragment>
155
156
  );
156
157
  }
157
158
  });