@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.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/fragments.json +1 -1
- package/package.json +21 -2
- package/src/assets/fragments-logo.tsx +37 -0
- package/src/assets/fragments_logo.svg +1 -0
- package/src/assets/fragments_logo_text.svg +1 -0
- package/src/blocks/AccountSettings.block.ts +1 -1
- package/src/blocks/ActivityFeed.block.ts +7 -7
- package/src/blocks/ChatInterface.block.ts +36 -80
- package/src/blocks/DashboardLayout.block.ts +85 -66
- package/src/blocks/DashboardPage.block.ts +297 -0
- package/src/blocks/EmptyState.block.ts +5 -3
- package/src/blocks/FeatureGrid.block.ts +1 -1
- package/src/blocks/LoginForm.block.ts +21 -26
- package/src/blocks/PricingComparison.block.ts +1 -1
- package/src/blocks/ShoppingCart.block.ts +2 -2
- package/src/components/Accordion/Accordion.fragment.tsx +2 -2
- package/src/components/Alert/Alert.fragment.tsx +2 -2
- package/src/components/AppShell/AppShell.fragment.tsx +2 -2
- package/src/components/Avatar/Avatar.fragment.tsx +2 -2
- package/src/components/Badge/Badge.fragment.tsx +2 -2
- package/src/components/Box/Box.fragment.tsx +2 -2
- package/src/components/Breadcrumbs/Breadcrumbs.fragment.tsx +2 -2
- package/src/components/Button/Button.fragment.tsx +2 -2
- package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +2 -2
- package/src/components/Card/Card.fragment.tsx +2 -2
- package/src/components/Chart/Chart.fragment.tsx +2 -2
- package/src/components/Checkbox/Checkbox.fragment.tsx +2 -2
- package/src/components/Chip/Chip.fragment.tsx +2 -2
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +2 -2
- package/src/components/CodeBlock/CodeBlock.module.scss +10 -52
- package/src/components/CodeBlock/index.tsx +13 -24
- package/src/components/Collapsible/Collapsible.fragment.tsx +2 -2
- package/src/components/ColorPicker/ColorPicker.fragment.tsx +2 -2
- package/src/components/Combobox/Combobox.fragment.tsx +2 -2
- package/src/components/ConversationList/ConversationList.fragment.tsx +2 -2
- package/src/components/DatePicker/DatePicker.fragment.tsx +2 -2
- package/src/components/Dialog/Dialog.fragment.tsx +2 -2
- package/src/components/EmptyState/EmptyState.fragment.tsx +2 -2
- package/src/components/Field/Field.fragment.tsx +2 -2
- package/src/components/Fieldset/Fieldset.fragment.tsx +2 -2
- package/src/components/Form/Form.fragment.tsx +2 -2
- package/src/components/Grid/Grid.fragment.tsx +2 -2
- package/src/components/Header/Header.fragment.tsx +2 -2
- 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 +2 -2
- package/src/components/Input/Input.test.tsx +35 -0
- package/src/components/Input/index.tsx +47 -2
- package/src/components/Link/Link.fragment.tsx +2 -2
- package/src/components/List/List.fragment.tsx +2 -2
- package/src/components/Listbox/Listbox.fragment.tsx +2 -2
- package/src/components/Loading/Loading.fragment.tsx +2 -2
- package/src/components/Markdown/Markdown.fragment.tsx +2 -2
- package/src/components/Markdown/Markdown.module.scss +5 -0
- package/src/components/Menu/Menu.fragment.tsx +2 -2
- package/src/components/Menu/Menu.module.scss +2 -0
- package/src/components/Message/Message.fragment.tsx +2 -2
- package/src/components/Popover/Popover.fragment.tsx +2 -2
- package/src/components/Progress/Progress.fragment.tsx +2 -2
- package/src/components/Prompt/Prompt.fragment.tsx +2 -2
- package/src/components/RadioGroup/RadioGroup.fragment.tsx +2 -2
- package/src/components/ScrollArea/ScrollArea.fragment.tsx +2 -2
- package/src/components/Select/Select.fragment.tsx +2 -2
- package/src/components/Separator/Separator.fragment.tsx +2 -2
- package/src/components/Sidebar/Sidebar.fragment.tsx +2 -2
- package/src/components/Sidebar/Sidebar.module.scss +1 -5
- package/src/components/Skeleton/Skeleton.fragment.tsx +2 -2
- package/src/components/Slider/Slider.fragment.tsx +2 -2
- package/src/components/Stack/Stack.fragment.tsx +2 -2
- package/src/components/Table/Table.fragment.tsx +3 -3
- package/src/components/Table/index.tsx +43 -5
- package/src/components/TableOfContents/TableOfContents.fragment.tsx +2 -2
- package/src/components/TableOfContents/TableOfContents.module.scss +0 -5
- package/src/components/TableOfContents/index.tsx +6 -1
- package/src/components/Tabs/Tabs.fragment.tsx +2 -2
- package/src/components/Text/Text.fragment.tsx +2 -2
- package/src/components/Text/Text.module.scss +6 -0
- package/src/components/Text/Text.test.tsx +5 -0
- package/src/components/Text/index.tsx +3 -0
- package/src/components/Textarea/Textarea.fragment.tsx +2 -2
- package/src/components/Theme/Theme.fragment.tsx +2 -2
- package/src/components/Theme/ThemeToggle.module.scss +1 -1
- package/src/components/Theme/index.tsx +1 -1
- package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +2 -2
- package/src/components/Toast/Toast.fragment.tsx +2 -2
- package/src/components/Toggle/Toggle.fragment.tsx +2 -2
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +37 -5
- package/src/components/Tooltip/Tooltip.fragment.tsx +2 -2
- package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +2 -2
- package/src/index.ts +3 -0
- package/src/tokens/_derive.scss +32 -8
- package/src/tokens/_mixins.scss +9 -0
- package/src/tokens/_variables.scss +4 -4
- package/src/blocks/AIChat.block.ts +0 -266
- package/src/blocks/AppShell.block.ts +0 -175
- package/src/blocks/CTABanner.block.ts +0 -24
- package/src/blocks/CardGrid.block.ts +0 -22
- package/src/blocks/CodeExamples.block.ts +0 -66
- package/src/blocks/ConfirmDialog.block.ts +0 -19
- package/src/blocks/ConversationWithHistory.block.ts +0 -45
- package/src/blocks/DashboardNav.block.ts +0 -183
- package/src/blocks/ForgotPassword.block.ts +0 -26
- package/src/blocks/FormLayout.block.ts +0 -31
- package/src/blocks/InsetDashboardLayout.block.ts +0 -79
- package/src/blocks/MetricDashboard.block.ts +0 -38
- package/src/blocks/NewsletterSignup.block.ts +0 -26
- package/src/blocks/NotificationList.block.ts +0 -39
- package/src/blocks/NotificationPreferences.block.ts +0 -40
- package/src/blocks/OrderSummary.block.ts +0 -52
- package/src/blocks/ProfileEditForm.block.ts +0 -51
- package/src/blocks/SearchResults.block.ts +0 -39
- package/src/blocks/SettingsPage.block.ts +0 -58
- package/src/blocks/StreamingMessage.block.ts +0 -24
- package/src/blocks/TestimonialCard.block.ts +0 -27
- package/src/blocks/UserProfileCard.block.ts +0 -29
- package/src/recipes/AIChat.recipe.ts +0 -266
- package/src/recipes/AppShell.recipe.ts +0 -175
- package/src/recipes/CardGrid.recipe.ts +0 -22
- package/src/recipes/ChatInterface.recipe.ts +0 -87
- package/src/recipes/CodeExamples.recipe.ts +0 -66
- package/src/recipes/ConfirmDialog.recipe.ts +0 -19
- package/src/recipes/DashboardLayout.recipe.ts +0 -73
- package/src/recipes/DashboardNav.recipe.ts +0 -183
- package/src/recipes/FormLayout.recipe.ts +0 -31
- package/src/recipes/LoginForm.recipe.ts +0 -33
- package/src/recipes/SettingsPage.recipe.ts +0 -58
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { ButtonGroup } from '.';
|
|
4
4
|
import { Button } from '../Button';
|
|
5
5
|
|
|
6
|
-
export default
|
|
6
|
+
export default defineFragment({
|
|
7
7
|
component: ButtonGroup,
|
|
8
8
|
|
|
9
9
|
meta: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { CodeBlock } from '.';
|
|
4
4
|
|
|
5
|
-
export default
|
|
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
|
|
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
|
|
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
|
-
|
|
58
|
-
padding: var(--fui-
|
|
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-
|
|
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-
|
|
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
|
|
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 = '
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
</
|
|
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 {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { Collapsible } from '.';
|
|
4
4
|
|
|
5
|
-
export default
|
|
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 {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { ColorPicker } from '.';
|
|
4
4
|
|
|
5
|
-
export default
|
|
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 {
|
|
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
|
|
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 {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { ConversationList } from '.';
|
|
4
4
|
import { Message } from '../Message';
|
|
5
5
|
|
|
6
|
-
export default
|
|
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 {
|
|
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
|
|
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 {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { Dialog } from '.';
|
|
4
4
|
import { Button } from '../Button';
|
|
5
5
|
|
|
6
|
-
export default
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
15
|
+
export default defineFragment({
|
|
16
16
|
component: Form,
|
|
17
17
|
|
|
18
18
|
meta: {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
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
|
|
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 {
|
|
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
|
|
6
|
+
export default defineFragment({
|
|
7
7
|
component: Icon,
|
|
8
8
|
|
|
9
9
|
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={
|
|
133
|
+
ref={mergedRef}
|
|
89
134
|
type={type}
|
|
90
135
|
value={value}
|
|
91
136
|
defaultValue={defaultValue}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { List } from '.';
|
|
4
4
|
import { Check } from '@phosphor-icons/react';
|
|
5
5
|
|
|
6
|
-
export default
|
|
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 {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { Listbox } from '.';
|
|
4
4
|
|
|
5
|
-
export default
|
|
5
|
+
export default defineFragment({
|
|
6
6
|
component: Listbox,
|
|
7
7
|
|
|
8
8
|
meta: {
|