@fragments-sdk/ui 0.6.2 → 0.6.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fragments-sdk/ui",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Customizable UI components built on Base UI headless primitives",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -36,7 +36,7 @@
36
36
  "react-dom": "^19.0.0",
37
37
  "sass": "^1.83.0",
38
38
  "typescript": "^5.7.0",
39
- "@fragments-sdk/cli": "0.4.1"
39
+ "@fragments-sdk/cli": "0.4.4"
40
40
  },
41
41
  "files": [
42
42
  "src",
@@ -95,6 +95,11 @@ export default defineSegment({
95
95
  description: 'Auto-highlight first matching item while filtering',
96
96
  default: 'true',
97
97
  },
98
+ maxVisibleItems: {
99
+ type: 'number',
100
+ description: 'Maximum visible options before scrolling. Shows half of the next item as a scroll hint.',
101
+ default: '4',
102
+ },
98
103
  },
99
104
 
100
105
  relations: [
@@ -111,6 +116,7 @@ export default defineSegment({
111
116
  'placeholder: string - input placeholder text',
112
117
  'disabled: boolean - disable combobox',
113
118
  'autoHighlight: boolean - auto-highlight first match',
119
+ 'maxVisibleItems: number - max visible options before scrolling (default 4)',
114
120
  ],
115
121
  scenarioTags: [
116
122
  'form.combobox',
@@ -204,6 +210,50 @@ export default defineSegment({
204
210
  </StatefulCombobox>
205
211
  ),
206
212
  },
213
+ {
214
+ name: 'Scrollable List',
215
+ description: 'Long list with scroll hint — shows 4 items with half-peek of the 5th',
216
+ render: () => (
217
+ <StatefulCombobox placeholder="Search languages...">
218
+ <Combobox.Input />
219
+ <Combobox.Content>
220
+ <Combobox.Empty>No results found</Combobox.Empty>
221
+ <Combobox.Item value="js">JavaScript</Combobox.Item>
222
+ <Combobox.Item value="ts">TypeScript</Combobox.Item>
223
+ <Combobox.Item value="py">Python</Combobox.Item>
224
+ <Combobox.Item value="rs">Rust</Combobox.Item>
225
+ <Combobox.Item value="go">Go</Combobox.Item>
226
+ <Combobox.Item value="rb">Ruby</Combobox.Item>
227
+ <Combobox.Item value="java">Java</Combobox.Item>
228
+ <Combobox.Item value="swift">Swift</Combobox.Item>
229
+ <Combobox.Item value="kt">Kotlin</Combobox.Item>
230
+ <Combobox.Item value="cpp">C++</Combobox.Item>
231
+ </Combobox.Content>
232
+ </StatefulCombobox>
233
+ ),
234
+ },
235
+ {
236
+ name: 'Custom Max Visible Items',
237
+ description: 'Show 6 items before scrolling with half-peek scroll hint',
238
+ render: () => (
239
+ <StatefulCombobox placeholder="Search cities...">
240
+ <Combobox.Input />
241
+ <Combobox.Content maxVisibleItems={6}>
242
+ <Combobox.Empty>No results found</Combobox.Empty>
243
+ <Combobox.Item value="nyc">New York</Combobox.Item>
244
+ <Combobox.Item value="lon">London</Combobox.Item>
245
+ <Combobox.Item value="tok">Tokyo</Combobox.Item>
246
+ <Combobox.Item value="par">Paris</Combobox.Item>
247
+ <Combobox.Item value="syd">Sydney</Combobox.Item>
248
+ <Combobox.Item value="ber">Berlin</Combobox.Item>
249
+ <Combobox.Item value="tor">Toronto</Combobox.Item>
250
+ <Combobox.Item value="sin">Singapore</Combobox.Item>
251
+ <Combobox.Item value="dub">Dubai</Combobox.Item>
252
+ <Combobox.Item value="sao">São Paulo</Combobox.Item>
253
+ </Combobox.Content>
254
+ </StatefulCombobox>
255
+ ),
256
+ },
207
257
  {
208
258
  name: 'Disabled',
209
259
  description: 'Disabled combobox',
@@ -152,9 +152,9 @@
152
152
  }
153
153
  }
154
154
 
155
- // Positioner
155
+ // Positioner — z-index must exceed Dialog positioner (51)
156
156
  .positioner {
157
- z-index: 50;
157
+ z-index: 52;
158
158
  outline: none;
159
159
  }
160
160
 
@@ -162,9 +162,20 @@
162
162
  .popup {
163
163
  @include surface-elevated;
164
164
 
165
+ // Item height derived from text-base font + vertical padding
166
+ --_item-h: calc(
167
+ var(--fui-font-size-sm, #{$fui-font-size-sm}) * var(--fui-line-height-normal, #{$fui-line-height-normal}) +
168
+ var(--fui-space-2, #{$fui-space-2}) * 2
169
+ );
170
+
165
171
  min-width: var(--anchor-width);
166
- max-height: 20rem;
167
- overflow-y: auto;
172
+ // Show N items + half-peek scroll hint (default 4.5 items)
173
+ // !important needed to override Base UI's inline max-height: 100%
174
+ max-height: calc(
175
+ var(--_item-h) * var(--fui-select-max-items, 4.5) +
176
+ var(--fui-space-1, #{$fui-space-1}) * 2
177
+ ) !important;
178
+ overflow-y: auto !important;
168
179
  padding: var(--fui-space-1, $fui-space-1);
169
180
  box-shadow: var(--fui-shadow-md, $fui-shadow-md);
170
181
 
@@ -42,6 +42,8 @@ export interface ComboboxContentProps extends React.HTMLAttributes<HTMLDivElemen
42
42
  children: React.ReactNode;
43
43
  sideOffset?: number;
44
44
  align?: 'start' | 'center' | 'end';
45
+ /** Maximum number of visible options before scrolling. Shows half of the next item as a scroll hint. @default 4 */
46
+ maxVisibleItems?: number;
45
47
  }
46
48
 
47
49
  export interface ComboboxItemProps {
@@ -310,10 +312,15 @@ function ComboboxContent({
310
312
  className,
311
313
  sideOffset = 4,
312
314
  align = 'start',
315
+ maxVisibleItems,
313
316
  ...htmlProps
314
317
  }: ComboboxContentProps) {
315
318
  const popupClasses = [styles.popup, className].filter(Boolean).join(' ');
316
319
 
320
+ const popupStyle = maxVisibleItems != null
321
+ ? { '--fui-select-max-items': maxVisibleItems + 0.5, ...htmlProps.style } as React.CSSProperties
322
+ : htmlProps.style;
323
+
317
324
  return (
318
325
  <BaseCombobox.Portal>
319
326
  <BaseCombobox.Positioner
@@ -322,7 +329,7 @@ function ComboboxContent({
322
329
  align={align}
323
330
  className={styles.positioner}
324
331
  >
325
- <BaseCombobox.Popup {...htmlProps} className={popupClasses}>
332
+ <BaseCombobox.Popup {...htmlProps} className={popupClasses} style={popupStyle}>
326
333
  {children}
327
334
  </BaseCombobox.Popup>
328
335
  </BaseCombobox.Positioner>
@@ -1,9 +1,9 @@
1
1
  @use '../../tokens/variables' as *;
2
2
  @use '../../tokens/mixins' as *;
3
3
 
4
- // Positioner
4
+ // Positioner — z-index must exceed Dialog positioner (51)
5
5
  .positioner {
6
- z-index: 50;
6
+ z-index: 52;
7
7
  outline: none;
8
8
  }
9
9
 
@@ -1,9 +1,9 @@
1
1
  @use '../../tokens/variables' as *;
2
2
  @use '../../tokens/mixins' as *;
3
3
 
4
- // Positioner
4
+ // Positioner — z-index must exceed Dialog positioner (51)
5
5
  .positioner {
6
- z-index: 50;
6
+ z-index: 52;
7
7
  outline: none;
8
8
  }
9
9
 
@@ -81,6 +81,11 @@ export default defineSegment({
81
81
  description: 'Disable the select',
82
82
  default: 'false',
83
83
  },
84
+ maxVisibleItems: {
85
+ type: 'number',
86
+ description: 'Maximum visible options before scrolling. Shows half of the next item as a scroll hint.',
87
+ default: '4',
88
+ },
84
89
  },
85
90
 
86
91
  relations: [
@@ -95,6 +100,7 @@ export default defineSegment({
95
100
  'onValueChange: (value) => void - selection handler',
96
101
  'placeholder: string - placeholder text',
97
102
  'disabled: boolean - disable select',
103
+ 'maxVisibleItems: number - max visible options before scrolling (default 4)',
98
104
  ],
99
105
  scenarioTags: [
100
106
  'form.select',
@@ -166,6 +172,48 @@ export default defineSegment({
166
172
  </StatefulSelect>
167
173
  ),
168
174
  },
175
+ {
176
+ name: 'Scrollable List',
177
+ description: 'Long list with scroll hint — shows 4 items with half-peek of the 5th to indicate more',
178
+ render: () => (
179
+ <StatefulSelect placeholder="Select a timezone">
180
+ <Select.Trigger />
181
+ <Select.Content>
182
+ <Select.Item value="utc-8">Pacific Time (UTC-8)</Select.Item>
183
+ <Select.Item value="utc-7">Mountain Time (UTC-7)</Select.Item>
184
+ <Select.Item value="utc-6">Central Time (UTC-6)</Select.Item>
185
+ <Select.Item value="utc-5">Eastern Time (UTC-5)</Select.Item>
186
+ <Select.Item value="utc-4">Atlantic Time (UTC-4)</Select.Item>
187
+ <Select.Item value="utc+0">GMT (UTC+0)</Select.Item>
188
+ <Select.Item value="utc+1">Central European (UTC+1)</Select.Item>
189
+ <Select.Item value="utc+5.5">India Standard (UTC+5:30)</Select.Item>
190
+ <Select.Item value="utc+8">China Standard (UTC+8)</Select.Item>
191
+ <Select.Item value="utc+9">Japan Standard (UTC+9)</Select.Item>
192
+ </Select.Content>
193
+ </StatefulSelect>
194
+ ),
195
+ },
196
+ {
197
+ name: 'Custom Max Visible Items',
198
+ description: 'Show 6 items before scrolling with half-peek scroll hint',
199
+ render: () => (
200
+ <StatefulSelect placeholder="Select a color">
201
+ <Select.Trigger />
202
+ <Select.Content maxVisibleItems={6}>
203
+ <Select.Item value="red">Red</Select.Item>
204
+ <Select.Item value="orange">Orange</Select.Item>
205
+ <Select.Item value="yellow">Yellow</Select.Item>
206
+ <Select.Item value="green">Green</Select.Item>
207
+ <Select.Item value="blue">Blue</Select.Item>
208
+ <Select.Item value="indigo">Indigo</Select.Item>
209
+ <Select.Item value="violet">Violet</Select.Item>
210
+ <Select.Item value="pink">Pink</Select.Item>
211
+ <Select.Item value="teal">Teal</Select.Item>
212
+ <Select.Item value="cyan">Cyan</Select.Item>
213
+ </Select.Content>
214
+ </StatefulSelect>
215
+ ),
216
+ },
169
217
  {
170
218
  name: 'Disabled',
171
219
  description: 'Disabled select',
@@ -72,9 +72,9 @@
72
72
  }
73
73
  }
74
74
 
75
- // Positioner
75
+ // Positioner — z-index must exceed Dialog positioner (51)
76
76
  .positioner {
77
- z-index: 50;
77
+ z-index: 52;
78
78
  outline: none;
79
79
  }
80
80
 
@@ -82,9 +82,20 @@
82
82
  .popup {
83
83
  @include surface-elevated;
84
84
 
85
+ // Item height derived from text-base font + vertical padding
86
+ --_item-h: calc(
87
+ var(--fui-font-size-sm, #{$fui-font-size-sm}) * var(--fui-line-height-normal, #{$fui-line-height-normal}) +
88
+ var(--fui-space-2, #{$fui-space-2}) * 2
89
+ );
90
+
85
91
  min-width: var(--anchor-width);
86
- max-height: 20rem;
87
- overflow-y: auto;
92
+ // Show N items + half-peek scroll hint (default 4.5 items)
93
+ // !important needed to override Base UI's inline max-height: 100%
94
+ max-height: calc(
95
+ var(--_item-h) * var(--fui-select-max-items, 4.5) +
96
+ var(--fui-space-1, #{$fui-space-1}) * 2
97
+ ) !important;
98
+ overflow-y: auto !important;
88
99
  padding: var(--fui-space-1, $fui-space-1);
89
100
  box-shadow: var(--fui-shadow-md, $fui-shadow-md);
90
101
 
@@ -39,6 +39,8 @@ export interface SelectContentProps extends React.HTMLAttributes<HTMLDivElement>
39
39
  children: React.ReactNode;
40
40
  sideOffset?: number;
41
41
  align?: 'start' | 'center' | 'end';
42
+ /** Maximum number of visible options before scrolling. Shows half of the next item as a scroll hint. @default 4 */
43
+ maxVisibleItems?: number;
42
44
  }
43
45
 
44
46
  export interface SelectItemProps {
@@ -236,10 +238,15 @@ function SelectContent({
236
238
  className,
237
239
  sideOffset = 4,
238
240
  align = 'start',
241
+ maxVisibleItems,
239
242
  ...htmlProps
240
243
  }: SelectContentProps) {
241
244
  const popupClasses = [styles.popup, className].filter(Boolean).join(' ');
242
245
 
246
+ const popupStyle = maxVisibleItems != null
247
+ ? { '--fui-select-max-items': maxVisibleItems + 0.5, ...htmlProps.style } as React.CSSProperties
248
+ : htmlProps.style;
249
+
243
250
  return (
244
251
  <BaseSelect.Portal>
245
252
  <BaseSelect.Positioner
@@ -247,7 +254,7 @@ function SelectContent({
247
254
  align={align}
248
255
  className={styles.positioner}
249
256
  >
250
- <BaseSelect.Popup {...htmlProps} className={popupClasses}>
257
+ <BaseSelect.Popup {...htmlProps} className={popupClasses} style={popupStyle}>
251
258
  {children}
252
259
  </BaseSelect.Popup>
253
260
  </BaseSelect.Positioner>
@@ -256,18 +263,18 @@ function SelectContent({
256
263
  }
257
264
 
258
265
  function SelectItem({ children, value, disabled, className }: SelectItemProps) {
259
- const context = React.useContext(SelectContext);
266
+ const { itemsRef, incrementItemsVersion } = React.useContext(SelectContext);
260
267
  const classes = [styles.item, className].filter(Boolean).join(' ');
261
268
 
262
269
  // Register this item's children in the registry so the trigger can display them
263
270
  React.useEffect(() => {
264
- context.itemsRef.current.set(value, children);
271
+ itemsRef.current.set(value, children);
265
272
  // Trigger re-render of trigger to show the registered content
266
- context.incrementItemsVersion();
273
+ incrementItemsVersion();
267
274
  return () => {
268
- context.itemsRef.current.delete(value);
275
+ itemsRef.current.delete(value);
269
276
  };
270
- }, [context, value, children]);
277
+ }, [itemsRef, incrementItemsVersion, value, children]);
271
278
 
272
279
  return (
273
280
  <BaseSelect.Item value={value} disabled={disabled} className={classes}>
@@ -2,6 +2,7 @@
2
2
 
3
3
  .text {
4
4
  font-family: var(--fui-font-sans, $fui-font-sans);
5
+ color: var(--fui-text-primary, $fui-text-primary);
5
6
  margin: 0;
6
7
  }
7
8