@usefragments/ui 1.3.2 → 1.4.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 +3 -1
- package/dist/assets/ui.css +1261 -849
- package/dist/chart.cjs +60 -24
- package/dist/chart.js +38 -24
- package/dist/codeblock.cjs +14 -6
- package/dist/codeblock.js +14 -6
- package/dist/colorpicker.cjs +56 -9
- package/dist/colorpicker.js +34 -9
- package/dist/components/Badge/Badge.module.scss.cjs +23 -16
- package/dist/components/Badge/Badge.module.scss.js +23 -16
- package/dist/components/Badge/index.cjs +8 -1
- package/dist/components/Badge/index.d.ts +3 -0
- package/dist/components/Badge/index.d.ts.map +1 -1
- package/dist/components/Badge/index.js +8 -1
- package/dist/components/Button/Button.module.scss.cjs +16 -13
- package/dist/components/Button/Button.module.scss.js +16 -13
- package/dist/components/Button/index.d.ts +2 -1
- package/dist/components/Button/index.d.ts.map +1 -1
- package/dist/components/Chart/index.d.ts +3 -0
- package/dist/components/Chart/index.d.ts.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.module.scss.cjs +21 -21
- package/dist/components/CodeBlock/CodeBlock.module.scss.js +21 -21
- package/dist/components/CodeBlock/index.d.ts.map +1 -1
- package/dist/components/ColorPicker/index.d.ts +4 -0
- package/dist/components/ColorPicker/index.d.ts.map +1 -1
- package/dist/components/Combobox/Combobox.module.scss.cjs +23 -23
- package/dist/components/Combobox/Combobox.module.scss.js +23 -23
- package/dist/components/Command/Command.module.scss.cjs +11 -11
- package/dist/components/Command/Command.module.scss.js +11 -11
- package/dist/components/ConversationList/ConversationList.module.scss.cjs +13 -10
- package/dist/components/ConversationList/ConversationList.module.scss.js +14 -11
- package/dist/components/ConversationList/index.cjs +26 -22
- package/dist/components/ConversationList/index.d.ts +5 -1
- package/dist/components/ConversationList/index.d.ts.map +1 -1
- package/dist/components/ConversationList/index.js +27 -23
- package/dist/components/DataTable/DataTable.module.scss.cjs +35 -35
- package/dist/components/DataTable/DataTable.module.scss.js +35 -35
- package/dist/components/DataTable/index.d.ts +4 -1
- package/dist/components/DataTable/index.d.ts.map +1 -1
- package/dist/components/DatePicker/index.d.ts +3 -0
- package/dist/components/DatePicker/index.d.ts.map +1 -1
- package/dist/components/Editor/index.d.ts +4 -1
- package/dist/components/Editor/index.d.ts.map +1 -1
- package/dist/components/Listbox/Listbox.module.scss.cjs +8 -8
- package/dist/components/Listbox/Listbox.module.scss.js +8 -8
- package/dist/components/Markdown/Markdown.module.scss.cjs +1 -1
- package/dist/components/Markdown/Markdown.module.scss.js +1 -1
- package/dist/components/Markdown/index.d.ts +16 -0
- package/dist/components/Markdown/index.d.ts.map +1 -1
- package/dist/components/Menu/Menu.module.scss.cjs +14 -14
- package/dist/components/Menu/Menu.module.scss.js +14 -14
- package/dist/components/Message/Message.module.scss.cjs +23 -20
- package/dist/components/Message/Message.module.scss.js +24 -21
- package/dist/components/Message/index.cjs +5 -1
- package/dist/components/Message/index.d.ts.map +1 -1
- package/dist/components/Message/index.js +5 -1
- package/dist/components/NavigationMenu/NavigationMenu.module.scss.cjs +28 -28
- package/dist/components/NavigationMenu/NavigationMenu.module.scss.js +28 -28
- package/dist/components/Pagination/Pagination.module.scss.cjs +7 -7
- package/dist/components/Pagination/Pagination.module.scss.js +7 -7
- package/dist/components/Prompt/Prompt.module.scss.cjs +52 -22
- package/dist/components/Prompt/Prompt.module.scss.js +52 -22
- package/dist/components/Prompt/index.cjs +197 -2
- package/dist/components/Prompt/index.d.ts +98 -3
- package/dist/components/Prompt/index.d.ts.map +1 -1
- package/dist/components/Prompt/index.js +198 -3
- package/dist/components/Select/Select.module.scss.cjs +29 -17
- package/dist/components/Select/Select.module.scss.js +30 -18
- package/dist/components/Select/index.cjs +45 -8
- package/dist/components/Select/index.d.ts +17 -2
- package/dist/components/Select/index.d.ts.map +1 -1
- package/dist/components/Select/index.js +45 -8
- package/dist/components/Sidebar/Sidebar.module.scss.cjs +45 -42
- package/dist/components/Sidebar/Sidebar.module.scss.js +45 -42
- package/dist/components/Sidebar/index.cjs +2 -0
- package/dist/components/Sidebar/index.d.ts +6 -1
- package/dist/components/Sidebar/index.d.ts.map +1 -1
- package/dist/components/Sidebar/index.js +2 -0
- package/dist/components/Text/Text.module.scss.cjs +63 -51
- package/dist/components/Text/Text.module.scss.js +63 -51
- package/dist/components/Text/index.d.ts +4 -2
- package/dist/components/Text/index.d.ts.map +1 -1
- package/dist/data-table.cjs +210 -28
- package/dist/data-table.js +189 -29
- package/dist/datepicker.cjs +63 -14
- package/dist/datepicker.js +41 -14
- package/dist/editor.cjs +67 -16
- package/dist/editor.js +45 -16
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/markdown.cjs +63 -19
- package/dist/markdown.js +41 -19
- package/fragments.json +1 -1
- package/package.json +2 -2
- package/src/components/Badge/Badge.contract.json +28 -5
- package/src/components/Badge/Badge.module.scss +21 -0
- package/src/components/Badge/index.tsx +10 -1
- package/src/components/Button/Button.contract.json +9 -3
- package/src/components/Button/Button.module.scss +34 -0
- package/src/components/Button/index.tsx +11 -1
- package/src/components/Chart/Chart.contract.json +7 -0
- package/src/components/Chart/index.tsx +44 -27
- package/src/components/CodeBlock/CodeBlock.contract.json +7 -0
- package/src/components/CodeBlock/CodeBlock.module.scss +7 -2
- package/src/components/CodeBlock/index.tsx +38 -6
- package/src/components/ColorPicker/ColorPicker.contract.json +9 -1
- package/src/components/ColorPicker/index.tsx +42 -12
- package/src/components/Combobox/Combobox.module.scss +4 -13
- package/src/components/ConversationList/ConversationList.contract.json +16 -1
- package/src/components/ConversationList/ConversationList.module.scss +12 -0
- package/src/components/ConversationList/index.tsx +40 -26
- package/src/components/DataTable/DataTable.contract.json +8 -1
- package/src/components/DataTable/DataTable.module.scss +5 -0
- package/src/components/DataTable/index.tsx +282 -51
- package/src/components/DatePicker/DatePicker.contract.json +8 -1
- package/src/components/DatePicker/index.tsx +51 -22
- package/src/components/Editor/Editor.contract.json +19 -1
- package/src/components/Editor/index.tsx +67 -23
- package/src/components/Listbox/Listbox.module.scss +3 -11
- package/src/components/Markdown/Markdown.contract.json +15 -1
- package/src/components/Markdown/Markdown.module.scss +18 -1
- package/src/components/Markdown/index.tsx +58 -24
- package/src/components/Menu/Menu.module.scss +12 -2
- package/src/components/Message/Message.contract.json +3 -0
- package/src/components/Message/Message.module.scss +33 -4
- package/src/components/Message/index.tsx +7 -1
- package/src/components/Pagination/Pagination.contract.json +1 -0
- package/src/components/Pagination/Pagination.module.scss +5 -4
- package/src/components/Prompt/Prompt.contract.json +40 -1
- package/src/components/Prompt/Prompt.module.scss +351 -4
- package/src/components/Prompt/index.tsx +324 -0
- package/src/components/Select/Select.contract.json +11 -1
- package/src/components/Select/Select.module.scss +80 -13
- package/src/components/Select/index.tsx +73 -7
- package/src/components/Sidebar/Sidebar.contract.json +1 -0
- package/src/components/Sidebar/Sidebar.module.scss +10 -0
- package/src/components/Sidebar/index.tsx +7 -0
- package/src/components/Text/Text.contract.json +7 -4
- package/src/components/Text/Text.module.scss +18 -0
- package/src/components/Text/index.tsx +4 -2
- package/src/index.ts +1 -0
- package/src/tokens/_mixins.scss +39 -4
- package/src/tokens/_variables.scss +30 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usefragments/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Customizable UI components built on Base UI headless primitives",
|
|
6
6
|
"author": "Conan McNicholl",
|
|
@@ -241,7 +241,7 @@
|
|
|
241
241
|
"vite": "^6.0.0",
|
|
242
242
|
"vitest": "^2.1.8",
|
|
243
243
|
"vitest-axe": "^0.1.0",
|
|
244
|
-
"@usefragments/core": "1.
|
|
244
|
+
"@usefragments/core": "1.7.1"
|
|
245
245
|
},
|
|
246
246
|
"files": [
|
|
247
247
|
"src",
|
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
"exportName": "Badge",
|
|
10
10
|
"propsSummary": [
|
|
11
11
|
"children: node (required)",
|
|
12
|
-
"variant: default|success|warning|error|info|outline (default: default)",
|
|
12
|
+
"variant: default|success|warning|error|info|outline|label|dim (default: default)",
|
|
13
13
|
"size: sm|md|lg (default: md)",
|
|
14
14
|
"dot: boolean (default: false)",
|
|
15
|
+
"dotPulse: boolean (default: false)",
|
|
16
|
+
"dotColor: string",
|
|
15
17
|
"icon: node",
|
|
18
|
+
"active: boolean (default: false)",
|
|
16
19
|
"onRemove: function",
|
|
17
20
|
"announce: boolean (default: false)",
|
|
18
21
|
"role: union"
|
|
@@ -25,10 +28,10 @@
|
|
|
25
28
|
},
|
|
26
29
|
"variant": {
|
|
27
30
|
"type": "enum",
|
|
28
|
-
"description": "Visual style indicating severity or category",
|
|
31
|
+
"description": "Visual style indicating severity or category. `label` is a hairline-bordered metadata pill (pair with dotColor); `dim` is transparent filter-pill chrome (pair with active).",
|
|
29
32
|
"default": "default",
|
|
30
33
|
"required": false,
|
|
31
|
-
"values": ["default", "success", "warning", "error", "info", "outline"]
|
|
34
|
+
"values": ["default", "success", "warning", "error", "info", "outline", "label", "dim"]
|
|
32
35
|
},
|
|
33
36
|
"size": {
|
|
34
37
|
"type": "enum",
|
|
@@ -43,11 +46,28 @@
|
|
|
43
46
|
"default": "false",
|
|
44
47
|
"required": false
|
|
45
48
|
},
|
|
49
|
+
"dotPulse": {
|
|
50
|
+
"type": "boolean",
|
|
51
|
+
"description": "Breathe the status dot, for a state that is still happening rather than one that has settled. Opacity only, and honours prefers-reduced-motion",
|
|
52
|
+
"default": "false",
|
|
53
|
+
"required": false
|
|
54
|
+
},
|
|
55
|
+
"dotColor": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "Color for the leading status dot. Applies to variant=\"label\" (and the generic dot prop). Accepts any CSS color including CSS custom properties.",
|
|
58
|
+
"required": false
|
|
59
|
+
},
|
|
46
60
|
"icon": {
|
|
47
61
|
"type": "node",
|
|
48
62
|
"description": "Optional icon element before the text",
|
|
49
63
|
"required": false
|
|
50
64
|
},
|
|
65
|
+
"active": {
|
|
66
|
+
"type": "boolean",
|
|
67
|
+
"description": "Marks variant=\"dim\" as the currently selected filter. Has no effect on other variants.",
|
|
68
|
+
"default": "false",
|
|
69
|
+
"required": false
|
|
70
|
+
},
|
|
51
71
|
"onRemove": {
|
|
52
72
|
"type": "function",
|
|
53
73
|
"description": "Makes the badge removable. Called when X is clicked.",
|
|
@@ -149,9 +169,12 @@
|
|
|
149
169
|
"contract": {
|
|
150
170
|
"propsSummary": [
|
|
151
171
|
"children: ReactNode - badge label (required)",
|
|
152
|
-
"variant: default|success|warning|error|info|outline - visual style",
|
|
153
|
-
"size: sm|md - badge size",
|
|
172
|
+
"variant: default|success|warning|error|info|outline|label|dim - visual style",
|
|
173
|
+
"size: sm|md|lg - badge size",
|
|
154
174
|
"dot: boolean - show status dot indicator",
|
|
175
|
+
"dotPulse: boolean - breathe the dot for an in-progress state (respects prefers-reduced-motion)",
|
|
176
|
+
"dotColor: string - color for the leading status dot",
|
|
177
|
+
"active: boolean - marks variant=\"dim\" as the selected filter",
|
|
155
178
|
"onRemove: () => void - makes badge removable",
|
|
156
179
|
"announce: boolean - opt into role=\"status\" + auto status aria-label"
|
|
157
180
|
],
|
|
@@ -116,6 +116,27 @@ $fui-badge-dot-md: 6px !default;
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// "Still happening", as distinct from "is in this state". A live badge and a
|
|
120
|
+
// settled one are otherwise the same object, and the difference between them is
|
|
121
|
+
// the one thing a status board is scanned for — so it is carried by the only
|
|
122
|
+
// motion the component has. Opacity only: nothing reflows, and a reader who has
|
|
123
|
+
// asked for less motion loses the breathing and keeps the badge.
|
|
124
|
+
.dotPulse {
|
|
125
|
+
animation: fui-badge-dot-pulse 1.6s ease-in-out infinite;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@keyframes fui-badge-dot-pulse {
|
|
129
|
+
50% {
|
|
130
|
+
opacity: 0.35;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@media (prefers-reduced-motion: reduce) {
|
|
135
|
+
.dotPulse {
|
|
136
|
+
animation: none;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
119
140
|
.icon {
|
|
120
141
|
display: flex;
|
|
121
142
|
align-items: center;
|
|
@@ -26,6 +26,9 @@ export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
|
26
26
|
size?: "sm" | "md" | "lg";
|
|
27
27
|
/** Show a status dot before the label */
|
|
28
28
|
dot?: boolean;
|
|
29
|
+
/** Breathe the status dot, for a state that is still happening rather than
|
|
30
|
+
* one that has settled. Respects `prefers-reduced-motion`. */
|
|
31
|
+
dotPulse?: boolean;
|
|
29
32
|
/** Color for the leading status dot. Applies to `variant="label"` (and the
|
|
30
33
|
* generic `dot` prop). Accepts any CSS color including CSS custom properties. */
|
|
31
34
|
dotColor?: string;
|
|
@@ -47,6 +50,7 @@ const BadgeRoot = React.forwardRef<HTMLSpanElement, BadgeProps>(function Badge(
|
|
|
47
50
|
variant = "default",
|
|
48
51
|
size = "md",
|
|
49
52
|
dot = false,
|
|
53
|
+
dotPulse = false,
|
|
50
54
|
dotColor,
|
|
51
55
|
icon,
|
|
52
56
|
active = false,
|
|
@@ -90,7 +94,12 @@ const BadgeRoot = React.forwardRef<HTMLSpanElement, BadgeProps>(function Badge(
|
|
|
90
94
|
role={role ?? (announce ? "status" : undefined)}
|
|
91
95
|
aria-label={effectiveAriaLabel}
|
|
92
96
|
>
|
|
93
|
-
{dot &&
|
|
97
|
+
{dot && (
|
|
98
|
+
<span
|
|
99
|
+
className={[styles.dot, dotPulse && styles.dotPulse].filter(Boolean).join(" ")}
|
|
100
|
+
aria-hidden="true"
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
94
103
|
{icon && (
|
|
95
104
|
<span className={styles.icon} aria-hidden="true">
|
|
96
105
|
{icon}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"propsSummary": [
|
|
16
16
|
"as: button|a",
|
|
17
17
|
"children: node (required)",
|
|
18
|
-
"variant: primary|secondary|ghost|link|danger|outlined|outline|icon (default: \"primary\")",
|
|
18
|
+
"variant: primary|secondary|ghost|link|quiet|danger|outlined|outline|icon (default: \"primary\")",
|
|
19
19
|
"size: xs|sm|md|lg (default: \"md\")",
|
|
20
20
|
"icon: boolean",
|
|
21
21
|
"fullWidth: boolean",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"variant": {
|
|
41
41
|
"type": "enum",
|
|
42
|
-
"description": "Visual style variant (\"icon\" is a convenience alias for outlined + icon-only layout, \"link\" is accent-coloured + transparent for tertiary CTAs)",
|
|
42
|
+
"description": "Visual style variant (\"icon\" is a convenience alias for outlined + icon-only layout, \"link\" is accent-coloured + transparent for tertiary CTAs, \"quiet\" is a neutral text button with no box and no height floor for meta lines and footnotes)",
|
|
43
43
|
"default": "\"primary\"",
|
|
44
44
|
"required": false,
|
|
45
45
|
"values": [
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"secondary",
|
|
48
48
|
"ghost",
|
|
49
49
|
"link",
|
|
50
|
+
"quiet",
|
|
50
51
|
"danger",
|
|
51
52
|
"outlined",
|
|
52
53
|
"outline",
|
|
@@ -134,6 +135,11 @@
|
|
|
134
135
|
"description": "Accent-coloured transparent button for tertiary CTAs",
|
|
135
136
|
"code": "<Button variant=\"link\">View all →</Button>"
|
|
136
137
|
},
|
|
138
|
+
{
|
|
139
|
+
"name": "Quiet",
|
|
140
|
+
"description": "Neutral text button with no box and no height floor, for meta lines and footnotes",
|
|
141
|
+
"code": "<Button variant=\"quiet\" size=\"xs\">main</Button>"
|
|
142
|
+
},
|
|
137
143
|
{
|
|
138
144
|
"name": "Danger",
|
|
139
145
|
"description": "Destructive action requiring attention",
|
|
@@ -184,7 +190,7 @@
|
|
|
184
190
|
],
|
|
185
191
|
"contract": {
|
|
186
192
|
"propsSummary": [
|
|
187
|
-
"variant: primary|secondary|ghost|link|danger|outlined|outline|icon (default: primary, icon = outlined + icon-only, link = accent transparent)",
|
|
193
|
+
"variant: primary|secondary|ghost|link|quiet|danger|outlined|outline|icon (default: primary, icon = outlined + icon-only, link = accent transparent, quiet = neutral text button, no box)",
|
|
188
194
|
"size: xs|sm|md|lg (default: md, xs for inline row-action controls)",
|
|
189
195
|
"disabled: boolean - disables interaction",
|
|
190
196
|
"type: button|submit|reset (default: button)",
|
|
@@ -180,6 +180,40 @@
|
|
|
180
180
|
--_button-shadow-active: none;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// Quiet: a button that reads as text. The neutral sibling of `link` — nothing
|
|
184
|
+
// but the foreground changes on hover, because in a meta line, a breadcrumb or
|
|
185
|
+
// a footnote a box would be the loudest thing on the row for the least
|
|
186
|
+
// important one. It is also the one variant that drops the interactive height
|
|
187
|
+
// floor: there is no box to hold open, so it sits on the line it is in and
|
|
188
|
+
// takes its hit target from the text and the row's own padding. That makes it
|
|
189
|
+
// the wrong choice for a standalone action — reach for `ghost` there.
|
|
190
|
+
.quiet {
|
|
191
|
+
--_button-bg: transparent;
|
|
192
|
+
--_button-bg-hover: transparent;
|
|
193
|
+
--_button-bg-active: transparent;
|
|
194
|
+
--_button-border: transparent;
|
|
195
|
+
--_button-border-hover: transparent;
|
|
196
|
+
--_button-color: var(--fui-text-tertiary, #{$fui-text-tertiary});
|
|
197
|
+
--_button-shadow: none;
|
|
198
|
+
--_button-shadow-hover: none;
|
|
199
|
+
--_button-shadow-active: none;
|
|
200
|
+
|
|
201
|
+
min-height: 0;
|
|
202
|
+
padding: 0;
|
|
203
|
+
font-weight: var(--fui-font-weight-normal, $fui-font-weight-normal);
|
|
204
|
+
|
|
205
|
+
// Only the custom property is restated: `color` reads it on .button, so the
|
|
206
|
+
// foreground follows without a second `color` declaration to keep in step.
|
|
207
|
+
&:hover:not(:disabled):not([data-disabled]) {
|
|
208
|
+
--_button-color: var(--fui-text-secondary, #{$fui-text-secondary});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
&:active:not(:disabled):not([data-disabled]),
|
|
212
|
+
&[data-pressed] {
|
|
213
|
+
--_button-color: var(--fui-text-primary, #{$fui-text-primary});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
183
217
|
.danger {
|
|
184
218
|
// Flat danger fill — no gradient. Hover/active darken the solid colour.
|
|
185
219
|
--_button-bg: var(--fui-color-danger, #{$fui-color-danger});
|
|
@@ -29,9 +29,19 @@ type ButtonBaseProps = {
|
|
|
29
29
|
* @see https://usefragments.com/components/button#variants
|
|
30
30
|
* @remarks
|
|
31
31
|
* - `"link"` — accent-coloured, transparent; for tertiary CTAs like "View all →"
|
|
32
|
+
* - `"quiet"` — neutral text button with no box and no height floor; for meta lines and footnotes
|
|
32
33
|
* - `"icon"` — convenience alias for outlined + icon-only layout
|
|
33
34
|
* - `"outline"` — alias for `"outlined"` */
|
|
34
|
-
variant?:
|
|
35
|
+
variant?:
|
|
36
|
+
| "primary"
|
|
37
|
+
| "secondary"
|
|
38
|
+
| "ghost"
|
|
39
|
+
| "link"
|
|
40
|
+
| "quiet"
|
|
41
|
+
| "danger"
|
|
42
|
+
| "outlined"
|
|
43
|
+
| "outline"
|
|
44
|
+
| "icon";
|
|
35
45
|
/** Button size. `"xs"` is for inline row-action controls (accept/dismiss, table-row icons) where `"sm"` is too tall.
|
|
36
46
|
* @default "md"
|
|
37
47
|
* @see https://usefragments.com/components/button#sizes */
|
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
"recharts"
|
|
11
11
|
],
|
|
12
12
|
"status": "stable",
|
|
13
|
+
"dependencies": [
|
|
14
|
+
{
|
|
15
|
+
"name": "recharts",
|
|
16
|
+
"version": ">=2.0.0 || >=3.0.0",
|
|
17
|
+
"reason": "Optional — powers Chart.Tooltip and Chart.Legend; both render nothing until it is installed."
|
|
18
|
+
}
|
|
19
|
+
],
|
|
13
20
|
"sourcePath": "src/components/Chart/index.tsx",
|
|
14
21
|
"exportName": "ChartContainer",
|
|
15
22
|
"propsSummary": [
|
|
@@ -60,20 +60,46 @@ type RechartsLegendProps = Record<string, unknown>;
|
|
|
60
60
|
|
|
61
61
|
let _RechartsTooltip: React.ComponentType<Record<string, unknown>> | null = null;
|
|
62
62
|
let _RechartsLegend: React.ComponentType<Record<string, unknown>> | null = null;
|
|
63
|
-
let
|
|
63
|
+
let _chartLoadPromise: Promise<void> | null = null;
|
|
64
64
|
let _chartFailed = false;
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
// Resolved with import() rather than require(): browser ESM bundles have no
|
|
67
|
+
// `require`, so the synchronous shape disabled recharts even when installed.
|
|
68
|
+
function loadChartDeps(): Promise<void> {
|
|
69
|
+
if (!_chartLoadPromise) {
|
|
70
|
+
_chartLoadPromise = (async () => {
|
|
71
|
+
try {
|
|
72
|
+
const rc = await import('recharts');
|
|
73
|
+
_RechartsTooltip = rc.Tooltip as unknown as React.ComponentType<Record<string, unknown>>;
|
|
74
|
+
_RechartsLegend = rc.Legend as unknown as React.ComponentType<Record<string, unknown>>;
|
|
75
|
+
} catch {
|
|
76
|
+
_chartFailed = true;
|
|
77
|
+
if (process.env.NODE_ENV === 'development') {
|
|
78
|
+
console.warn(
|
|
79
|
+
'[@usefragments/ui] Chart: recharts is not installed. ' +
|
|
80
|
+
'Install it with: npm install recharts'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
76
85
|
}
|
|
86
|
+
return _chartLoadPromise;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Kick off the lazy recharts load on mount and re-render once it settles. */
|
|
90
|
+
function useChartDeps(): boolean {
|
|
91
|
+
const [, rerender] = React.useReducer((n: number) => n + 1, 0);
|
|
92
|
+
React.useEffect(() => {
|
|
93
|
+
if (_RechartsTooltip || _chartFailed) return;
|
|
94
|
+
let active = true;
|
|
95
|
+
void loadChartDeps().then(() => {
|
|
96
|
+
if (active) rerender();
|
|
97
|
+
});
|
|
98
|
+
return () => {
|
|
99
|
+
active = false;
|
|
100
|
+
};
|
|
101
|
+
}, []);
|
|
102
|
+
return _RechartsTooltip !== null;
|
|
77
103
|
}
|
|
78
104
|
|
|
79
105
|
|
|
@@ -252,7 +278,7 @@ export function ChartTooltip({
|
|
|
252
278
|
content,
|
|
253
279
|
...props
|
|
254
280
|
}: ChartTooltipProps) {
|
|
255
|
-
|
|
281
|
+
const chartReady = useChartDeps();
|
|
256
282
|
|
|
257
283
|
const defaultContent = React.useCallback(
|
|
258
284
|
|
|
@@ -269,13 +295,8 @@ export function ChartTooltip({
|
|
|
269
295
|
[indicator, hideLabel, hideIndicator, labelFormatter, valueFormatter],
|
|
270
296
|
);
|
|
271
297
|
|
|
272
|
-
if
|
|
273
|
-
|
|
274
|
-
console.warn(
|
|
275
|
-
'[@usefragments/ui] Chart: recharts is not installed. ' +
|
|
276
|
-
'Install it with: npm install recharts'
|
|
277
|
-
);
|
|
278
|
-
}
|
|
298
|
+
// Nothing until recharts resolves; nothing at all if it is not installed.
|
|
299
|
+
if (!chartReady || !_RechartsTooltip) {
|
|
279
300
|
return null;
|
|
280
301
|
}
|
|
281
302
|
|
|
@@ -330,19 +351,13 @@ type ChartLegendProps = RechartsLegendProps & {
|
|
|
330
351
|
};
|
|
331
352
|
|
|
332
353
|
export function ChartLegend({ content, ...props }: ChartLegendProps) {
|
|
333
|
-
|
|
354
|
+
const chartReady = useChartDeps();
|
|
334
355
|
|
|
335
356
|
const defaultContent = (legendProps: Record<string, unknown>) => (
|
|
336
357
|
<ChartLegendContent {...(legendProps as ChartLegendContentProps)} />
|
|
337
358
|
);
|
|
338
359
|
|
|
339
|
-
if (
|
|
340
|
-
if (_chartFailed && process.env.NODE_ENV === 'development') {
|
|
341
|
-
console.warn(
|
|
342
|
-
'[@usefragments/ui] Chart: recharts is not installed. ' +
|
|
343
|
-
'Install it with: npm install recharts'
|
|
344
|
-
);
|
|
345
|
-
}
|
|
360
|
+
if (!chartReady || !_RechartsLegend) {
|
|
346
361
|
return null;
|
|
347
362
|
}
|
|
348
363
|
|
|
@@ -362,4 +377,6 @@ export const Chart = Object.assign(ChartContainer, {
|
|
|
362
377
|
TooltipContent: ChartTooltipContent,
|
|
363
378
|
Legend: ChartLegend,
|
|
364
379
|
LegendContent: ChartLegendContent,
|
|
380
|
+
/** Start resolving recharts before first render (optional). */
|
|
381
|
+
preload: loadChartDeps,
|
|
365
382
|
});
|
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
"diff"
|
|
13
13
|
],
|
|
14
14
|
"status": "stable",
|
|
15
|
+
"dependencies": [
|
|
16
|
+
{
|
|
17
|
+
"name": "shiki",
|
|
18
|
+
"version": ">=1.0.0",
|
|
19
|
+
"reason": "Optional — provides syntax highlighting; without it code renders unhighlighted."
|
|
20
|
+
}
|
|
21
|
+
],
|
|
15
22
|
"sourcePath": "src/components/CodeBlock/index.tsx",
|
|
16
23
|
"exportName": "CodeBlock",
|
|
17
24
|
"propsSummary": [
|
|
@@ -318,9 +318,14 @@ $fui-code-scrollbar-thumb-hover: rgba(255, 255, 255, 0.2) !default;
|
|
|
318
318
|
word-break: break-word;
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
// `display: block` here would double-space every wrapped block: shiki emits a
|
|
322
|
+
// newline between each `.line`, and under `pre-wrap` that newline becomes a
|
|
323
|
+
// line box of its own the moment the lines stop being inline. The base rule's
|
|
324
|
+
// `inline-block` keeps it in the inline flow, and a full-width inline-block
|
|
325
|
+
// still wraps and still grows taller, which is all this variant needs.
|
|
321
326
|
:global(.line) {
|
|
322
|
-
display: block;
|
|
323
|
-
width:
|
|
327
|
+
display: inline-block;
|
|
328
|
+
width: 100%;
|
|
324
329
|
}
|
|
325
330
|
}
|
|
326
331
|
|
|
@@ -249,24 +249,56 @@ function dedent(str: string): string {
|
|
|
249
249
|
return lines.map((line) => line.slice(minIndent)).join("\n");
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Languages where leading whitespace carries meaning, so the first line has to
|
|
254
|
+
* take part in the common-indent calculation like any other.
|
|
255
|
+
*/
|
|
256
|
+
const INDENTATION_SIGNIFICANT_LANGUAGES = new Set<CodeBlockLanguage>([
|
|
257
|
+
"yaml",
|
|
258
|
+
"yml",
|
|
259
|
+
"json",
|
|
260
|
+
"python",
|
|
261
|
+
"py",
|
|
262
|
+
]);
|
|
263
|
+
|
|
252
264
|
/**
|
|
253
265
|
* Normalize indentation while handling JSX where first line is already at column 0.
|
|
266
|
+
*
|
|
267
|
+
* The JSX case this exists for is a snippet authored inline, where the body
|
|
268
|
+
* carries the source file's indentation but the opening tag does not:
|
|
269
|
+
*
|
|
270
|
+
* code={`<Card>
|
|
271
|
+
* <Card.Header>Title</Card.Header>
|
|
272
|
+
* </Card>`}
|
|
273
|
+
*
|
|
274
|
+
* Excluding line 0 from the minimum is what renders that correctly. But
|
|
275
|
+
* `normalizeCode` trims first, so line 0 has always had its indentation removed
|
|
276
|
+
* by the time we get here — which means the same rule strips one real level off
|
|
277
|
+
* any snippet with a single root and an indented body:
|
|
278
|
+
*
|
|
279
|
+
* job: job:
|
|
280
|
+
* image: node → image: node (now a sibling key, not a child)
|
|
281
|
+
*
|
|
282
|
+
* For YAML, JSON and Python that is not cosmetic: the rendered snippet is
|
|
283
|
+
* invalid, and copying it gives you a broken file. Those languages count line 0,
|
|
284
|
+
* which is plain dedent. Everything else keeps the JSX-friendly heuristic.
|
|
254
285
|
*/
|
|
255
|
-
function normalizeIndentation(str: string): string {
|
|
286
|
+
function normalizeIndentation(str: string, language: CodeBlockLanguage): string {
|
|
256
287
|
const lines = str.split("\n");
|
|
257
288
|
if (lines.length <= 1) return str;
|
|
258
289
|
|
|
290
|
+
const firstLineCounts = INDENTATION_SIGNIFICANT_LANGUAGES.has(language);
|
|
259
291
|
let minIndent = Infinity;
|
|
260
292
|
const firstLineIndent = lines[0].match(/^(\s*)/)?.[1].length ?? 0;
|
|
261
293
|
|
|
262
|
-
for (let i = 1; i < lines.length; i += 1) {
|
|
294
|
+
for (let i = firstLineCounts ? 0 : 1; i < lines.length; i += 1) {
|
|
263
295
|
const line = lines[i];
|
|
264
296
|
if (line.trim().length === 0) continue;
|
|
265
297
|
const indent = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
266
298
|
minIndent = Math.min(minIndent, indent);
|
|
267
299
|
}
|
|
268
300
|
|
|
269
|
-
if (firstLineIndent > 0) {
|
|
301
|
+
if (!firstLineCounts && firstLineIndent > 0) {
|
|
270
302
|
minIndent = Math.min(minIndent, firstLineIndent);
|
|
271
303
|
}
|
|
272
304
|
|
|
@@ -431,11 +463,11 @@ function formatLongJsxTags(code: string): string {
|
|
|
431
463
|
.join("\n");
|
|
432
464
|
}
|
|
433
465
|
|
|
434
|
-
function normalizeCode(code: string): string {
|
|
466
|
+
function normalizeCode(code: string, language: CodeBlockLanguage): string {
|
|
435
467
|
const trimmed = code.trim();
|
|
436
468
|
if (trimmed.length === 0) return "";
|
|
437
469
|
|
|
438
|
-
const normalized = normalizeIndentation(trimmed);
|
|
470
|
+
const normalized = normalizeIndentation(trimmed, language);
|
|
439
471
|
const dedented = dedent(normalized);
|
|
440
472
|
const withoutTrailingWhitespace = trimTrailingWhitespace(dedented);
|
|
441
473
|
return formatLongJsxTags(withoutTrailingWhitespace);
|
|
@@ -562,7 +594,7 @@ const CodeBlockBase = React.forwardRef<HTMLDivElement, CodeBlockProps>(function
|
|
|
562
594
|
const [highlight, setHighlight] = useState<{ html: string; loading: boolean }>({ html: '', loading: true });
|
|
563
595
|
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
|
564
596
|
|
|
565
|
-
const trimmedCode = useMemo(() => normalizeCode(code), [code]);
|
|
597
|
+
const trimmedCode = useMemo(() => normalizeCode(code, language), [code, language]);
|
|
566
598
|
const codeLines = trimmedCode.split("\n");
|
|
567
599
|
const totalLines = codeLines.length;
|
|
568
600
|
const shouldShowCollapse = collapsible && totalLines > collapsedLines;
|
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
"theme"
|
|
13
13
|
],
|
|
14
14
|
"status": "stable",
|
|
15
|
+
"dependencies": [
|
|
16
|
+
{
|
|
17
|
+
"name": "react-colorful",
|
|
18
|
+
"version": ">=5.0.0",
|
|
19
|
+
"reason": "Optional — renders the visual picker surface; the swatch and hex input work without it."
|
|
20
|
+
}
|
|
21
|
+
],
|
|
15
22
|
"sourcePath": "src/components/ColorPicker/index.tsx",
|
|
16
23
|
"exportName": "ColorPicker",
|
|
17
24
|
"propsSummary": [
|
|
@@ -200,7 +207,8 @@
|
|
|
200
207
|
"ai": {
|
|
201
208
|
"compositionPattern": "compound",
|
|
202
209
|
"subComponents": [
|
|
203
|
-
"Root"
|
|
210
|
+
"Root",
|
|
211
|
+
"preload"
|
|
204
212
|
]
|
|
205
213
|
},
|
|
206
214
|
"provenance": {
|
|
@@ -7,20 +7,48 @@ import { Popover as BasePopover } from "@base-ui/react/popover";
|
|
|
7
7
|
// Lazy-loaded dependency (react-colorful)
|
|
8
8
|
// ============================================
|
|
9
9
|
|
|
10
|
-
let _HexColorPicker:
|
|
11
|
-
|
|
10
|
+
let _HexColorPicker: React.ComponentType<{
|
|
11
|
+
color: string;
|
|
12
|
+
onChange: (color: string) => void;
|
|
13
|
+
}> | null = null;
|
|
14
|
+
let _colorfulLoadPromise: Promise<void> | null = null;
|
|
12
15
|
let _colorfulFailed = false;
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
// Resolved with import() rather than require(): browser ESM bundles have no
|
|
18
|
+
// `require`, so the synchronous shape hid the picker even when installed.
|
|
19
|
+
function loadColorfulDeps(): Promise<void> {
|
|
20
|
+
if (!_colorfulLoadPromise) {
|
|
21
|
+
_colorfulLoadPromise = (async () => {
|
|
22
|
+
try {
|
|
23
|
+
const rc = await import("react-colorful");
|
|
24
|
+
_HexColorPicker = rc.HexColorPicker;
|
|
25
|
+
} catch {
|
|
26
|
+
_colorfulFailed = true;
|
|
27
|
+
if (process.env.NODE_ENV === "development") {
|
|
28
|
+
console.warn(
|
|
29
|
+
"[@usefragments/ui] ColorPicker: react-colorful is not installed. " +
|
|
30
|
+
"Install it with: npm install react-colorful"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
})();
|
|
23
35
|
}
|
|
36
|
+
return _colorfulLoadPromise;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Kick off the lazy react-colorful load on mount and re-render once it settles. */
|
|
40
|
+
function useColorfulDeps(): void {
|
|
41
|
+
const [, rerender] = React.useReducer((n: number) => n + 1, 0);
|
|
42
|
+
React.useEffect(() => {
|
|
43
|
+
if (_HexColorPicker || _colorfulFailed) return;
|
|
44
|
+
let active = true;
|
|
45
|
+
void loadColorfulDeps().then(() => {
|
|
46
|
+
if (active) rerender();
|
|
47
|
+
});
|
|
48
|
+
return () => {
|
|
49
|
+
active = false;
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
24
52
|
}
|
|
25
53
|
import { Field } from "@base-ui/react/field";
|
|
26
54
|
import { useResolvedControlSize } from "../ComponentDefaults";
|
|
@@ -75,7 +103,7 @@ const ColorPickerRoot = React.forwardRef<HTMLDivElement, ColorPickerProps>(funct
|
|
|
75
103
|
) {
|
|
76
104
|
const size = useResolvedControlSize(sizeProp);
|
|
77
105
|
const resolvedHelperText = helperText ?? description;
|
|
78
|
-
|
|
106
|
+
useColorfulDeps();
|
|
79
107
|
const [internalValue, setInternalValue] = React.useState(defaultValue);
|
|
80
108
|
const [inputValue, setInputValue] = React.useState(value ?? defaultValue);
|
|
81
109
|
const displayValue = value !== undefined ? value : internalValue;
|
|
@@ -179,4 +207,6 @@ const ColorPickerRoot = React.forwardRef<HTMLDivElement, ColorPickerProps>(funct
|
|
|
179
207
|
|
|
180
208
|
export const ColorPicker = Object.assign(ColorPickerRoot, {
|
|
181
209
|
Root: ColorPickerRoot,
|
|
210
|
+
/** Start resolving react-colorful before first render (optional). */
|
|
211
|
+
preload: loadColorfulDeps,
|
|
182
212
|
});
|
|
@@ -194,22 +194,13 @@
|
|
|
194
194
|
@include surface-elevated;
|
|
195
195
|
@include popup-container;
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
// Item height derived from text-base font + vertical padding
|
|
200
|
-
--_combobox-item-h: calc(
|
|
201
|
-
var(--fui-font-size-sm, #{$fui-font-size-sm}) *
|
|
202
|
-
var(--fui-line-height-normal, #{$fui-line-height-normal}) +
|
|
203
|
-
var(--fui-space-2, #{$fui-space-2}) * 2
|
|
204
|
-
);
|
|
197
|
+
@include popup-scroll-viewport;
|
|
205
198
|
|
|
199
|
+
border-color: var(--fui-form-group-border, $fui-border);
|
|
206
200
|
min-width: var(--anchor-width);
|
|
207
|
-
// Show N items + half-peek scroll hint (default 4.5 items)
|
|
201
|
+
// Show N items + half-peek scroll hint (default 4.5 items).
|
|
208
202
|
// !important needed to override Base UI's inline max-height: 100%
|
|
209
|
-
max-height:
|
|
210
|
-
var(--_combobox-item-h) * var(--fui-select-max-items, 4.5) +
|
|
211
|
-
var(--fui-space-1, #{$fui-space-1}) * 2
|
|
212
|
-
) !important;
|
|
203
|
+
max-height: var(--_popup-scroll-max-h) !important;
|
|
213
204
|
overflow-y: auto !important;
|
|
214
205
|
|
|
215
206
|
// Animation
|