@fragments-sdk/ui 0.8.0 → 0.8.2

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 (128) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/fragments.json +1 -1
  4. package/package.json +21 -2
  5. package/src/assets/fragments-logo.tsx +37 -0
  6. package/src/assets/fragments_logo.svg +1 -0
  7. package/src/assets/fragments_logo_text.svg +1 -0
  8. package/src/blocks/AccountSettings.block.ts +1 -1
  9. package/src/blocks/ActivityFeed.block.ts +7 -7
  10. package/src/blocks/ChatInterface.block.ts +36 -80
  11. package/src/blocks/DashboardLayout.block.ts +85 -66
  12. package/src/blocks/DashboardPage.block.ts +297 -0
  13. package/src/blocks/EmptyState.block.ts +5 -3
  14. package/src/blocks/FeatureGrid.block.ts +1 -1
  15. package/src/blocks/LoginForm.block.ts +21 -26
  16. package/src/blocks/PricingComparison.block.ts +1 -1
  17. package/src/blocks/ShoppingCart.block.ts +2 -2
  18. package/src/components/Accordion/Accordion.fragment.tsx +2 -2
  19. package/src/components/Alert/Alert.fragment.tsx +2 -2
  20. package/src/components/AppShell/AppShell.fragment.tsx +2 -2
  21. package/src/components/Avatar/Avatar.fragment.tsx +2 -2
  22. package/src/components/Badge/Badge.fragment.tsx +2 -2
  23. package/src/components/Box/Box.fragment.tsx +2 -2
  24. package/src/components/Breadcrumbs/Breadcrumbs.fragment.tsx +2 -2
  25. package/src/components/Button/Button.fragment.tsx +2 -2
  26. package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +2 -2
  27. package/src/components/Card/Card.fragment.tsx +2 -2
  28. package/src/components/Chart/Chart.fragment.tsx +2 -2
  29. package/src/components/Checkbox/Checkbox.fragment.tsx +2 -2
  30. package/src/components/Chip/Chip.fragment.tsx +2 -2
  31. package/src/components/CodeBlock/CodeBlock.fragment.tsx +2 -2
  32. package/src/components/CodeBlock/CodeBlock.module.scss +10 -52
  33. package/src/components/CodeBlock/index.tsx +13 -24
  34. package/src/components/Collapsible/Collapsible.fragment.tsx +2 -2
  35. package/src/components/ColorPicker/ColorPicker.fragment.tsx +2 -2
  36. package/src/components/Combobox/Combobox.fragment.tsx +2 -2
  37. package/src/components/ConversationList/ConversationList.fragment.tsx +2 -2
  38. package/src/components/DatePicker/DatePicker.fragment.tsx +2 -2
  39. package/src/components/Dialog/Dialog.fragment.tsx +2 -2
  40. package/src/components/EmptyState/EmptyState.fragment.tsx +2 -2
  41. package/src/components/Field/Field.fragment.tsx +2 -2
  42. package/src/components/Fieldset/Fieldset.fragment.tsx +2 -2
  43. package/src/components/Form/Form.fragment.tsx +2 -2
  44. package/src/components/Grid/Grid.fragment.tsx +2 -2
  45. package/src/components/Header/Header.fragment.tsx +2 -2
  46. package/src/components/Icon/Icon.fragment.tsx +2 -2
  47. package/src/components/Image/Image.fragment.tsx +2 -2
  48. package/src/components/Input/Input.fragment.tsx +2 -2
  49. package/src/components/Input/Input.test.tsx +35 -0
  50. package/src/components/Input/index.tsx +47 -2
  51. package/src/components/Link/Link.fragment.tsx +2 -2
  52. package/src/components/List/List.fragment.tsx +2 -2
  53. package/src/components/Listbox/Listbox.fragment.tsx +2 -2
  54. package/src/components/Loading/Loading.fragment.tsx +2 -2
  55. package/src/components/Markdown/Markdown.fragment.tsx +2 -2
  56. package/src/components/Markdown/Markdown.module.scss +5 -0
  57. package/src/components/Menu/Menu.fragment.tsx +2 -2
  58. package/src/components/Menu/Menu.module.scss +2 -0
  59. package/src/components/Message/Message.fragment.tsx +2 -2
  60. package/src/components/Popover/Popover.fragment.tsx +2 -2
  61. package/src/components/Progress/Progress.fragment.tsx +2 -2
  62. package/src/components/Prompt/Prompt.fragment.tsx +2 -2
  63. package/src/components/RadioGroup/RadioGroup.fragment.tsx +2 -2
  64. package/src/components/ScrollArea/ScrollArea.fragment.tsx +2 -2
  65. package/src/components/Select/Select.fragment.tsx +2 -2
  66. package/src/components/Separator/Separator.fragment.tsx +2 -2
  67. package/src/components/Sidebar/Sidebar.fragment.tsx +2 -2
  68. package/src/components/Sidebar/Sidebar.module.scss +1 -5
  69. package/src/components/Skeleton/Skeleton.fragment.tsx +2 -2
  70. package/src/components/Slider/Slider.fragment.tsx +2 -2
  71. package/src/components/Stack/Stack.fragment.tsx +2 -2
  72. package/src/components/Table/Table.fragment.tsx +3 -3
  73. package/src/components/Table/index.tsx +43 -5
  74. package/src/components/TableOfContents/TableOfContents.fragment.tsx +2 -2
  75. package/src/components/TableOfContents/TableOfContents.module.scss +0 -5
  76. package/src/components/TableOfContents/index.tsx +6 -1
  77. package/src/components/Tabs/Tabs.fragment.tsx +2 -2
  78. package/src/components/Text/Text.fragment.tsx +2 -2
  79. package/src/components/Text/Text.module.scss +6 -0
  80. package/src/components/Text/Text.test.tsx +5 -0
  81. package/src/components/Text/index.tsx +3 -0
  82. package/src/components/Textarea/Textarea.fragment.tsx +2 -2
  83. package/src/components/Theme/Theme.fragment.tsx +2 -2
  84. package/src/components/Theme/ThemeToggle.module.scss +1 -1
  85. package/src/components/Theme/index.tsx +1 -1
  86. package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +2 -2
  87. package/src/components/Toast/Toast.fragment.tsx +2 -2
  88. package/src/components/Toggle/Toggle.fragment.tsx +2 -2
  89. package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +37 -5
  90. package/src/components/Tooltip/Tooltip.fragment.tsx +2 -2
  91. package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +2 -2
  92. package/src/index.ts +3 -0
  93. package/src/tokens/_derive.scss +32 -8
  94. package/src/tokens/_mixins.scss +9 -0
  95. package/src/tokens/_variables.scss +4 -4
  96. package/src/blocks/AIChat.block.ts +0 -266
  97. package/src/blocks/AppShell.block.ts +0 -175
  98. package/src/blocks/CTABanner.block.ts +0 -24
  99. package/src/blocks/CardGrid.block.ts +0 -22
  100. package/src/blocks/CodeExamples.block.ts +0 -66
  101. package/src/blocks/ConfirmDialog.block.ts +0 -19
  102. package/src/blocks/ConversationWithHistory.block.ts +0 -45
  103. package/src/blocks/DashboardNav.block.ts +0 -183
  104. package/src/blocks/ForgotPassword.block.ts +0 -26
  105. package/src/blocks/FormLayout.block.ts +0 -31
  106. package/src/blocks/InsetDashboardLayout.block.ts +0 -79
  107. package/src/blocks/MetricDashboard.block.ts +0 -38
  108. package/src/blocks/NewsletterSignup.block.ts +0 -26
  109. package/src/blocks/NotificationList.block.ts +0 -39
  110. package/src/blocks/NotificationPreferences.block.ts +0 -40
  111. package/src/blocks/OrderSummary.block.ts +0 -52
  112. package/src/blocks/ProfileEditForm.block.ts +0 -51
  113. package/src/blocks/SearchResults.block.ts +0 -39
  114. package/src/blocks/SettingsPage.block.ts +0 -58
  115. package/src/blocks/StreamingMessage.block.ts +0 -24
  116. package/src/blocks/TestimonialCard.block.ts +0 -27
  117. package/src/blocks/UserProfileCard.block.ts +0 -29
  118. package/src/recipes/AIChat.recipe.ts +0 -266
  119. package/src/recipes/AppShell.recipe.ts +0 -175
  120. package/src/recipes/CardGrid.recipe.ts +0 -22
  121. package/src/recipes/ChatInterface.recipe.ts +0 -87
  122. package/src/recipes/CodeExamples.recipe.ts +0 -66
  123. package/src/recipes/ConfirmDialog.recipe.ts +0 -19
  124. package/src/recipes/DashboardLayout.recipe.ts +0 -73
  125. package/src/recipes/DashboardNav.recipe.ts +0 -183
  126. package/src/recipes/FormLayout.recipe.ts +0 -31
  127. package/src/recipes/LoginForm.recipe.ts +0 -33
  128. package/src/recipes/SettingsPage.recipe.ts +0 -58
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Button } from '.';
4
4
 
5
- export default defineSegment({
5
+ export default defineFragment({
6
6
  component: Button,
7
7
 
8
8
  meta: {
@@ -1,9 +1,9 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { ButtonGroup } from '.';
4
4
  import { Button } from '../Button';
5
5
 
6
- export default defineSegment({
6
+ export default defineFragment({
7
7
  component: ButtonGroup,
8
8
 
9
9
  meta: {
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Card } from '.';
4
4
 
5
- export default defineSegment({
5
+ export default defineFragment({
6
6
  component: Card,
7
7
 
8
8
  meta: {
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import {
4
4
  LineChart,
5
5
  Line,
@@ -40,7 +40,7 @@ const browserData = [
40
40
  { name: 'Edge', value: 10, color: 'var(--fui-color-warning)' },
41
41
  ];
42
42
 
43
- export default defineSegment({
43
+ export default defineFragment({
44
44
  component: ChartContainer,
45
45
 
46
46
  meta: {
@@ -1,5 +1,5 @@
1
1
  import React, { useState } from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Checkbox } from '.';
4
4
 
5
5
  // Stateful wrapper for interactive demos
@@ -8,7 +8,7 @@ function StatefulCheckbox(props: React.ComponentProps<typeof Checkbox>) {
8
8
  return <Checkbox {...props} checked={checked} onCheckedChange={setChecked} />;
9
9
  }
10
10
 
11
- export default defineSegment({
11
+ export default defineFragment({
12
12
  component: Checkbox,
13
13
 
14
14
  meta: {
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Chip } from '.';
4
4
 
5
- export default defineSegment({
5
+ export default defineFragment({
6
6
  component: Chip,
7
7
 
8
8
  meta: {
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { CodeBlock } from '.';
4
4
 
5
- export default defineSegment({
5
+ export default defineFragment({
6
6
  component: CodeBlock,
7
7
 
8
8
  meta: {
@@ -21,21 +21,16 @@
21
21
  }
22
22
 
23
23
  // ============================================
24
- // Header bar with filename
24
+ // Header bar (always rendered, contains filename + copy icon)
25
25
  // ============================================
26
- .hasHeader {
27
- // Adjust copy button position when header is present
28
- .copyButton {
29
- top: calc(var(--fui-space-2, $fui-space-2) + 36px); // Account for header height
30
- }
31
- }
32
-
33
26
  .header {
34
27
  display: flex;
35
28
  align-items: center;
29
+ justify-content: space-between;
36
30
  padding: var(--fui-padding-inline-sm) var(--fui-padding-container-md);
37
31
  background-color: var(--fui-code-header-bg, $fui-code-header-bg);
38
32
  border-bottom: 1px solid var(--fui-code-border, $fui-code-border);
33
+ min-height: 36px;
39
34
  }
40
35
 
41
36
  .filename {
@@ -45,44 +40,28 @@
45
40
  color: var(--fui-code-text, $fui-code-text);
46
41
  }
47
42
 
48
- // Copy button - always dark theme styling
43
+ // Copy button - icon-only, always visible in header
49
44
  .copyButton {
50
45
  @include button-reset;
51
- position: absolute;
52
- top: var(--fui-space-2, $fui-space-2);
53
- right: var(--fui-space-2, $fui-space-2);
54
- z-index: 10;
55
46
  display: flex;
56
47
  align-items: center;
57
- gap: var(--fui-space-1, $fui-space-1);
58
- padding: var(--fui-padding-item-xs) var(--fui-padding-item-sm);
59
- font-size: var(--fui-font-size-xs, $fui-font-size-xs);
60
- font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
61
- font-family: var(--fui-font-sans, $fui-font-sans);
62
- // Use code-specific tokens for consistent dark theme styling
48
+ justify-content: center;
49
+ padding: var(--fui-space-1, $fui-space-1);
63
50
  color: var(--fui-code-text-muted, $fui-code-text-muted);
64
51
  background: transparent;
65
52
  border: none;
66
53
  border-radius: var(--fui-radius-md, $fui-radius-md);
67
- opacity: 0;
68
54
  cursor: pointer;
69
55
  transition:
70
- opacity var(--fui-transition-fast, $fui-transition-fast),
71
56
  background var(--fui-transition-fast, $fui-transition-fast),
72
57
  color var(--fui-transition-fast, $fui-transition-fast);
73
58
 
74
- // Show on wrapper hover
75
- .wrapper:hover & {
76
- opacity: 1;
77
- }
78
-
79
59
  &:hover {
80
60
  color: var(--fui-code-tab-text-active, $fui-code-tab-text-active);
81
61
  background: var(--fui-code-copy-bg, $fui-code-copy-bg);
82
62
  }
83
63
 
84
64
  &:focus-visible {
85
- opacity: 1;
86
65
  outline: 2px solid rgba(255, 255, 255, 0.5);
87
66
  outline-offset: 2px;
88
67
  }
@@ -122,7 +101,7 @@
122
101
  pre {
123
102
  margin: 0;
124
103
  padding: var(--fui-padding-container-md);
125
- padding-right: var(--fui-space-10, $fui-space-10);
104
+ padding-right: var(--fui-padding-container-md);
126
105
  background-color: transparent !important;
127
106
  border: none !important;
128
107
  border-radius: 0 !important;
@@ -146,10 +125,10 @@
146
125
  .codeContainer {
147
126
  // Shiki output styling - always dark theme
148
127
  :global(.shiki) {
128
+ background-color: transparent !important;
149
129
  margin: 0;
150
130
  padding: var(--fui-padding-container-md);
151
- padding-right: var(--fui-space-10, $fui-space-10);
152
- background-color: transparent !important;
131
+ padding-right: var(--fui-padding-container-md);
153
132
  border: none !important;
154
133
  border-radius: 0 !important;
155
134
  font-family: var(--fui-font-mono, $fui-font-mono);
@@ -257,7 +236,7 @@
257
236
  }
258
237
  }
259
238
 
260
- // Compact variant — reduced padding, vertically centred copy button
239
+ // Compact variant — reduced padding
261
240
  .compact {
262
241
  .wrapper {
263
242
  border-radius: var(--fui-radius-md, $fui-radius-md);
@@ -266,23 +245,9 @@
266
245
  .codeContainer :global(.shiki),
267
246
  .loading pre {
268
247
  padding: var(--fui-padding-inline-sm) var(--fui-padding-container-sm);
269
- padding-right: var(--fui-space-8, $fui-space-8);
270
248
  font-size: var(--fui-font-size-sm, $fui-font-size-sm);
271
249
  line-height: 1.5;
272
250
  }
273
-
274
- .copyButton {
275
- top: 50%;
276
- transform: translateY(-50%);
277
- right: var(--fui-space-1, $fui-space-1);
278
- padding: var(--fui-space-1, $fui-space-1);
279
- font-size: 0;
280
- gap: 0;
281
-
282
- span {
283
- display: none;
284
- }
285
- }
286
251
  }
287
252
 
288
253
  // Word wrap
@@ -343,13 +308,6 @@
343
308
  .codeContainer :global(.shiki) {
344
309
  font-size: 0.75rem;
345
310
  padding: var(--fui-space-2, $fui-space-2);
346
- padding-right: var(--fui-space-8, $fui-space-8);
347
- }
348
-
349
- .copyButton {
350
- opacity: 0.8; // Always visible on mobile (no hover)
351
- padding: 0.25rem 0.5rem;
352
- font-size: 0.6875rem;
353
311
  }
354
312
  }
355
313
 
@@ -309,7 +309,7 @@ const CodeBlockBase = React.forwardRef<HTMLDivElement, CodeBlockProps>(
309
309
  {
310
310
  code,
311
311
  language = 'tsx',
312
- theme = 'synthwave-84',
312
+ theme = 'one-dark-pro',
313
313
  showCopy = true,
314
314
  title,
315
315
  filename,
@@ -421,7 +421,6 @@ const CodeBlockBase = React.forwardRef<HTMLDivElement, CodeBlockProps>(
421
421
 
422
422
  const wrapperClasses = [
423
423
  styles.wrapper,
424
- filename && styles.hasHeader,
425
424
  persistentCopy && styles.persistentCopyWrapper,
426
425
  ].filter(Boolean).join(' ');
427
426
 
@@ -433,30 +432,20 @@ const CodeBlockBase = React.forwardRef<HTMLDivElement, CodeBlockProps>(
433
432
  <div ref={ref} {...htmlProps} className={classNames}>
434
433
  {title && <div className={styles.title}>{title}</div>}
435
434
  <div className={wrapperClasses}>
436
- {filename && (
435
+ {(filename || (showCopy && !persistentCopy)) && (
437
436
  <div className={styles.header}>
438
- <span className={styles.filename}>{filename}</span>
439
- </div>
440
- )}
441
- {showCopy && !persistentCopy && (
442
- <button
443
- type="button"
444
- onClick={handleCopy}
445
- className={`${styles.copyButton} ${copied ? styles.copied : ''}`}
446
- aria-label={copied ? 'Copied!' : 'Copy code'}
447
- >
448
- {copied ? (
449
- <>
450
- <CheckIcon className={styles.icon} />
451
- <span>Copied!</span>
452
- </>
453
- ) : (
454
- <>
455
- <CopyIcon className={styles.icon} />
456
- <span>Copy</span>
457
- </>
437
+ <span className={styles.filename}>{filename ?? ''}</span>
438
+ {showCopy && !persistentCopy && (
439
+ <button
440
+ type="button"
441
+ onClick={handleCopy}
442
+ className={`${styles.copyButton} ${copied ? styles.copied : ''}`}
443
+ aria-label={copied ? 'Copied!' : 'Copy code'}
444
+ >
445
+ {copied ? <CheckIcon className={styles.icon} /> : <CopyIcon className={styles.icon} />}
446
+ </button>
458
447
  )}
459
- </button>
448
+ </div>
460
449
  )}
461
450
  {isLoading ? (
462
451
  <div className={styles.loading} style={codeContainerStyle}>
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Collapsible } from '.';
4
4
 
5
- export default defineSegment({
5
+ export default defineFragment({
6
6
  component: Collapsible,
7
7
 
8
8
  meta: {
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { ColorPicker } from '.';
4
4
 
5
- export default defineSegment({
5
+ export default defineFragment({
6
6
  component: ColorPicker,
7
7
 
8
8
  meta: {
@@ -1,5 +1,5 @@
1
1
  import React, { useState } from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Combobox } from '.';
4
4
 
5
5
  // Stateful wrapper for interactive demos
@@ -16,7 +16,7 @@ function StatefulCombobox(props: React.ComponentProps<typeof Combobox> & {
16
16
  );
17
17
  }
18
18
 
19
- export default defineSegment({
19
+ export default defineFragment({
20
20
  component: Combobox,
21
21
 
22
22
  meta: {
@@ -1,9 +1,9 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { ConversationList } from '.';
4
4
  import { Message } from '../Message';
5
5
 
6
- export default defineSegment({
6
+ export default defineFragment({
7
7
  component: ConversationList,
8
8
 
9
9
  meta: {
@@ -1,5 +1,5 @@
1
1
  import React, { useState } from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { DatePicker } from '.';
4
4
  import type { DateRange } from '.';
5
5
  import { subDays } from 'date-fns';
@@ -38,7 +38,7 @@ function StatefulRangePicker(props: {
38
38
 
39
39
  const today = new Date();
40
40
 
41
- export default defineSegment({
41
+ export default defineFragment({
42
42
  component: DatePicker,
43
43
 
44
44
  meta: {
@@ -1,9 +1,9 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Dialog } from '.';
4
4
  import { Button } from '../Button';
5
5
 
6
- export default defineSegment({
6
+ export default defineFragment({
7
7
  component: Dialog,
8
8
 
9
9
  meta: {
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { EmptyState } from '.';
4
4
  import { Button } from '../Button';
5
5
 
@@ -54,7 +54,7 @@ const InboxIcon = () => (
54
54
  </svg>
55
55
  );
56
56
 
57
- export default defineSegment({
57
+ export default defineFragment({
58
58
  component: EmptyState,
59
59
 
60
60
  meta: {
@@ -1,10 +1,10 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Field } from '.';
4
4
  import { Input } from '../Input';
5
5
  import { Grid } from '../Grid';
6
6
 
7
- export default defineSegment({
7
+ export default defineFragment({
8
8
  component: Field,
9
9
 
10
10
  meta: {
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Fieldset } from '.';
4
4
  import { Field } from '../Field';
5
5
  import { Input } from '../Input';
@@ -8,7 +8,7 @@ import { Select } from '../Select';
8
8
  import { Checkbox } from '../Checkbox';
9
9
  import { Grid } from '../Grid';
10
10
 
11
- export default defineSegment({
11
+ export default defineFragment({
12
12
  component: Fieldset,
13
13
 
14
14
  meta: {
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Form } from '.';
4
4
  import { Field } from '../Field';
5
5
  import { Fieldset } from '../Fieldset';
@@ -12,7 +12,7 @@ import { Toggle } from '../Toggle';
12
12
  import { Button } from '../Button';
13
13
  import { Grid } from '../Grid';
14
14
 
15
- export default defineSegment({
15
+ export default defineFragment({
16
16
  component: Form,
17
17
 
18
18
  meta: {
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Grid } from '.';
4
4
 
5
- export default defineSegment({
5
+ export default defineFragment({
6
6
  component: Grid,
7
7
 
8
8
  meta: {
@@ -1,12 +1,12 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Header } from '.';
4
4
  import { ThemeToggle, ThemeProvider } from '../Theme';
5
5
  import { Button } from '../Button';
6
6
  import { Input } from '../Input';
7
7
  import { MagnifyingGlass } from '@phosphor-icons/react';
8
8
 
9
- export default defineSegment({
9
+ export default defineFragment({
10
10
  component: Header,
11
11
 
12
12
  meta: {
@@ -1,9 +1,9 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Icon } from '.';
4
4
  import { Heart, Star, Check, Warning, Info } from '@phosphor-icons/react';
5
5
 
6
- export default defineSegment({
6
+ export default defineFragment({
7
7
  component: Icon,
8
8
 
9
9
  meta: {
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Image } from '.';
4
4
 
5
- export default defineSegment({
5
+ export default defineFragment({
6
6
  component: Image,
7
7
 
8
8
  meta: {
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Input } from '.';
4
4
 
5
- export default defineSegment({
5
+ export default defineFragment({
6
6
  component: Input,
7
7
 
8
8
  meta: {
@@ -65,6 +65,41 @@ describe('Input', () => {
65
65
  expect(handleChange).toHaveBeenCalledWith('a');
66
66
  });
67
67
 
68
+ it('focuses the input when shortcut key is pressed', () => {
69
+ render(<Input aria-label="Search" shortcut="⌘K" />);
70
+ const input = screen.getByRole('textbox');
71
+ expect(document.activeElement).not.toBe(input);
72
+
73
+ // Simulate pressing ⌘K (Meta+K)
74
+ document.dispatchEvent(
75
+ new KeyboardEvent('keydown', { key: 'k', metaKey: true, bubbles: true })
76
+ );
77
+
78
+ expect(document.activeElement).toBe(input);
79
+ });
80
+
81
+ it('focuses the input when Ctrl+K is pressed (Windows/Linux)', () => {
82
+ render(<Input aria-label="Search" shortcut="⌘K" />);
83
+ const input = screen.getByRole('textbox');
84
+
85
+ document.dispatchEvent(
86
+ new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, bubbles: true })
87
+ );
88
+
89
+ expect(document.activeElement).toBe(input);
90
+ });
91
+
92
+ it('does not focus when wrong key is pressed', () => {
93
+ render(<Input aria-label="Search" shortcut="⌘K" />);
94
+ const input = screen.getByRole('textbox');
95
+
96
+ document.dispatchEvent(
97
+ new KeyboardEvent('keydown', { key: 'j', metaKey: true, bubbles: true })
98
+ );
99
+
100
+ expect(document.activeElement).not.toBe(input);
101
+ });
102
+
68
103
  it('has no accessibility violations', async () => {
69
104
  const { container } = render(<Input label="Accessible input" />);
70
105
  await expectNoA11yViolations(container);
@@ -15,7 +15,7 @@ export interface InputProps extends Omit<React.HTMLAttributes<HTMLDivElement>, '
15
15
  error?: boolean;
16
16
  label?: string;
17
17
  helperText?: string;
18
- /** Keyboard shortcut hint displayed inside the input (e.g., "⌘K") */
18
+ /** Keyboard shortcut hint displayed inside the input (e.g., "⌘K"). Also registers a global keydown listener that focuses the input when the shortcut is pressed. */
19
19
  shortcut?: string;
20
20
  onChange?: (value: string) => void;
21
21
  onBlur?: () => void;
@@ -33,6 +33,20 @@ function mergeAriaIds(...ids: Array<string | undefined>): string | undefined {
33
33
  return merged.length > 0 ? merged : undefined;
34
34
  }
35
35
 
36
+ function parseShortcut(shortcut: string): { meta: boolean; shift: boolean; alt: boolean; key: string } | null {
37
+ let meta = false, shift = false, alt = false;
38
+ let remaining = shortcut;
39
+
40
+ if (remaining.includes('⌘')) { meta = true; remaining = remaining.replace('⌘', ''); }
41
+ if (remaining.includes('⇧')) { shift = true; remaining = remaining.replace('⇧', ''); }
42
+ if (remaining.includes('⌥')) { alt = true; remaining = remaining.replace('⌥', ''); }
43
+
44
+ remaining = remaining.trim();
45
+ if (!remaining) return null;
46
+
47
+ return { meta, shift, alt, key: remaining };
48
+ }
49
+
36
50
  const InputRoot = React.forwardRef<HTMLInputElement, InputProps>(
37
51
  function Input(
38
52
  {
@@ -66,6 +80,37 @@ const InputRoot = React.forwardRef<HTMLInputElement, InputProps>(
66
80
  const generatedId = React.useId();
67
81
  const helperId = helperText ? `input-helper-${generatedId}` : undefined;
68
82
 
83
+ const internalRef = React.useRef<HTMLInputElement>(null);
84
+ const mergedRef = React.useCallback(
85
+ (node: HTMLInputElement | null) => {
86
+ internalRef.current = node;
87
+ if (typeof ref === 'function') {
88
+ ref(node);
89
+ } else if (ref) {
90
+ (ref as React.MutableRefObject<HTMLInputElement | null>).current = node;
91
+ }
92
+ },
93
+ [ref]
94
+ );
95
+
96
+ // Register global keydown handler when shortcut is provided
97
+ React.useEffect(() => {
98
+ if (!shortcut) return;
99
+ const parsed = parseShortcut(shortcut);
100
+ if (!parsed) return;
101
+
102
+ const handler = (e: KeyboardEvent) => {
103
+ if (parsed.meta && !(e.metaKey || e.ctrlKey)) return;
104
+ if (parsed.shift && !e.shiftKey) return;
105
+ if (parsed.alt && !e.altKey) return;
106
+ if (e.key.toLowerCase() !== parsed.key.toLowerCase()) return;
107
+ e.preventDefault();
108
+ internalRef.current?.focus();
109
+ };
110
+ document.addEventListener('keydown', handler);
111
+ return () => document.removeEventListener('keydown', handler);
112
+ }, [shortcut]);
113
+
69
114
  const inputClasses = [
70
115
  styles.input,
71
116
  styles[size],
@@ -85,7 +130,7 @@ const InputRoot = React.forwardRef<HTMLInputElement, InputProps>(
85
130
 
86
131
  const inputElement = (
87
132
  <Field.Control
88
- ref={ref}
133
+ ref={mergedRef}
89
134
  type={type}
90
135
  value={value}
91
136
  defaultValue={defaultValue}
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Link } from '.';
4
4
 
5
- export default defineSegment({
5
+ export default defineFragment({
6
6
  component: Link,
7
7
 
8
8
  meta: {
@@ -1,9 +1,9 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { List } from '.';
4
4
  import { Check } from '@phosphor-icons/react';
5
5
 
6
- export default defineSegment({
6
+ export default defineFragment({
7
7
  component: List,
8
8
 
9
9
  meta: {
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
- import { defineSegment } from '@fragments/core';
2
+ import { defineFragment } from '@fragments/core';
3
3
  import { Listbox } from '.';
4
4
 
5
- export default defineSegment({
5
+ export default defineFragment({
6
6
  component: Listbox,
7
7
 
8
8
  meta: {