@usefragments/ui 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefragments/ui",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
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.7.1"
244
+ "@usefragments/core": "1.9.0"
245
245
  },
246
246
  "files": [
247
247
  "src",
@@ -77,6 +77,11 @@
77
77
  ]
78
78
  },
79
79
  "examples": [
80
+ {
81
+ "name": "Nested Heading",
82
+ "description": "Select the heading level required by the surrounding document outline",
83
+ "code": "<Card variant=\"panel\" padding=\"none\">\n <Card.Header divided>\n <Card.Title as=\"h4\">Nested panel</Card.Title>\n </Card.Header>\n <Card.Body padding=\"md\">Panel content</Card.Body>\n</Card>"
84
+ },
80
85
  {
81
86
  "name": "Default",
82
87
  "description": "Standard quiet card surface",
@@ -42,6 +42,9 @@ export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
42
42
 
43
43
  export interface CardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
44
44
  children: React.ReactNode;
45
+ /** Semantic heading level.
46
+ * @default "h3" */
47
+ as?: "h2" | "h3" | "h4" | "h5" | "h6";
45
48
  }
46
49
 
47
50
  export interface CardDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
@@ -179,12 +182,12 @@ function CardHeader({ children, divided, className, ...htmlProps }: CardHeaderPr
179
182
  );
180
183
  }
181
184
 
182
- function CardTitle({ children, className, ...htmlProps }: CardTitleProps) {
185
+ function CardTitle({ children, as: Component = "h3", className, ...htmlProps }: CardTitleProps) {
183
186
  const classes = [styles.title, className].filter(Boolean).join(" ");
184
187
  return (
185
- <h3 {...htmlProps} className={classes}>
188
+ <Component {...htmlProps} className={classes}>
186
189
  {children}
187
- </h3>
190
+ </Component>
188
191
  );
189
192
  }
190
193
 
@@ -3,13 +3,7 @@
3
3
  "name": "Progress",
4
4
  "description": "Visual indicator of task completion or loading state. Available in linear and circular variants.",
5
5
  "category": "feedback",
6
- "tags": [
7
- "progress",
8
- "loading",
9
- "indicator",
10
- "percentage",
11
- "status"
12
- ],
6
+ "tags": ["progress", "loading", "indicator", "percentage", "status"],
13
7
  "status": "stable",
14
8
  "sourcePath": "src/components/Progress/index.tsx",
15
9
  "exportName": "Progress",
@@ -18,7 +12,8 @@
18
12
  "min: number (default: 0)",
19
13
  "max: number (default: 100)",
20
14
  "size: sm|md|lg (default: md)",
21
- "variant: default|success|warning|danger (default: default)",
15
+ "variant: default|neutral|success|warning|danger (default: default)",
16
+ "role: progressbar|meter (default: progressbar)",
22
17
  "label: string",
23
18
  "showValue: boolean (default: false)",
24
19
  "formatValue: function"
@@ -47,23 +42,14 @@
47
42
  "description": "Size of the progress bar",
48
43
  "default": "md",
49
44
  "required": false,
50
- "values": [
51
- "sm",
52
- "md",
53
- "lg"
54
- ]
45
+ "values": ["sm", "md", "lg"]
55
46
  },
56
47
  "variant": {
57
48
  "type": "enum",
58
49
  "description": "Color variant",
59
50
  "default": "default",
60
51
  "required": false,
61
- "values": [
62
- "default",
63
- "success",
64
- "warning",
65
- "danger"
66
- ]
52
+ "values": ["default", "neutral", "success", "warning", "danger"]
67
53
  },
68
54
  "label": {
69
55
  "type": "string",
@@ -99,10 +85,12 @@
99
85
  "Use indeterminate for unknown durations",
100
86
  "Include a label for context when the purpose isnt obvious",
101
87
  "Use appropriate color variants for success/warning/danger states",
88
+ "Use variant=\"neutral\" with role=\"meter\" for magnitude or distribution values where reaching the maximum is not a success state",
102
89
  "Determinate values are clamped to the min/max range for display"
103
90
  ],
104
91
  "accessibility": [
105
92
  "Uses role=\"progressbar\" with aria-valuenow",
93
+ "Accepts role=\"meter\" for scalar measurements within a known range",
106
94
  "Label is associated with the progress bar",
107
95
  "State changes are announced to screen readers"
108
96
  ]
@@ -155,17 +143,11 @@
155
143
  "showValue: boolean - display percentage",
156
144
  "Determinate display values are clamped (and invalid min/max ranges render as 0%)"
157
145
  ],
158
- "a11yRules": [
159
- "A11Y_PROGRESS_ROLE",
160
- "A11Y_PROGRESS_VALUE"
161
- ]
146
+ "a11yRules": ["A11Y_PROGRESS_ROLE", "A11Y_PROGRESS_VALUE"]
162
147
  },
163
148
  "ai": {
164
149
  "compositionPattern": "compound",
165
- "subComponents": [
166
- "Root",
167
- "Circular"
168
- ]
150
+ "subComponents": ["Root", "Circular"]
169
151
  },
170
152
  "provenance": {
171
153
  "source": "migrated",
@@ -63,6 +63,10 @@
63
63
  }
64
64
 
65
65
  // Color variants
66
+ .indicatorNeutral {
67
+ background-color: var(--fui-text-tertiary, $fui-text-tertiary);
68
+ }
69
+
66
70
  .indicatorSuccess {
67
71
  background-color: var(--fui-color-success-text, $fui-color-success-text);
68
72
  }
@@ -94,7 +98,7 @@
94
98
  }
95
99
 
96
100
  // Complete state
97
- .root[data-complete] .indicator {
101
+ .root[data-complete] .indicator:not(.indicatorNeutral) {
98
102
  background-color: var(--fui-color-success-text, $fui-color-success-text);
99
103
  }
100
104
 
@@ -1,14 +1,14 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import * as React from 'react';
4
- import { Progress as BaseProgress } from '@base-ui/react/progress';
5
- import styles from './Progress.module.scss';
3
+ import * as React from "react";
4
+ import { Progress as BaseProgress } from "@base-ui/react/progress";
5
+ import styles from "./Progress.module.scss";
6
6
 
7
7
  // ============================================
8
8
  // Types
9
9
  // ============================================
10
10
 
11
- export interface ProgressProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'defaultValue'> {
11
+ export interface ProgressProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "defaultValue"> {
12
12
  /** Current progress value (0-100). Null for indeterminate. */
13
13
  value?: number | null;
14
14
  /** Minimum value */
@@ -16,9 +16,9 @@ export interface ProgressProps extends Omit<React.HTMLAttributes<HTMLDivElement>
16
16
  /** Maximum value */
17
17
  max?: number;
18
18
  /** Size of the progress bar */
19
- size?: 'sm' | 'md' | 'lg';
19
+ size?: "sm" | "md" | "lg";
20
20
  /** Color variant */
21
- variant?: 'default' | 'success' | 'warning' | 'danger';
21
+ variant?: "default" | "neutral" | "success" | "warning" | "danger";
22
22
  /** Label text */
23
23
  label?: string;
24
24
  /** Show percentage value */
@@ -31,9 +31,9 @@ export interface CircularProgressProps extends React.HTMLAttributes<HTMLDivEleme
31
31
  /** Current progress value (0-100). Null for indeterminate. */
32
32
  value?: number | null;
33
33
  /** Size of the circular progress */
34
- size?: 'sm' | 'md' | 'lg';
34
+ size?: "sm" | "md" | "lg";
35
35
  /** Color variant */
36
- variant?: 'default' | 'success' | 'warning' | 'danger';
36
+ variant?: "default" | "success" | "warning" | "danger";
37
37
  /** Show percentage in center */
38
38
  showValue?: boolean;
39
39
  /** Stroke width */
@@ -48,52 +48,51 @@ function ProgressRoot({
48
48
  value = null,
49
49
  min = 0,
50
50
  max = 100,
51
- size = 'md',
52
- variant = 'default',
51
+ size = "md",
52
+ variant = "default",
53
53
  label,
54
54
  showValue = false,
55
55
  formatValue,
56
56
  className,
57
- 'aria-label': ariaLabel,
58
- 'aria-valuetext': ariaValueText,
57
+ role,
58
+ "aria-label": ariaLabel,
59
+ "aria-valuetext": ariaValueText,
59
60
  ...htmlProps
60
61
  }: ProgressProps) {
61
62
  const isIndeterminate = value === null;
62
63
  const range = max - min;
63
- const normalizedPercentage = isIndeterminate
64
- ? 0
65
- : range <= 0
66
- ? 0
67
- : ((value - min) / range) * 100;
64
+ const normalizedPercentage = isIndeterminate ? 0 : range <= 0 ? 0 : ((value - min) / range) * 100;
68
65
  const percentage = isIndeterminate
69
66
  ? 0
70
67
  : Math.round(Math.min(100, Math.max(0, normalizedPercentage)));
71
68
 
72
69
  const trackClasses = [
73
70
  styles.track,
74
- size === 'sm' && styles.trackSm,
75
- size === 'md' && styles.trackMd,
76
- size === 'lg' && styles.trackLg,
77
- ].filter(Boolean).join(' ');
71
+ size === "sm" && styles.trackSm,
72
+ size === "md" && styles.trackMd,
73
+ size === "lg" && styles.trackLg,
74
+ ]
75
+ .filter(Boolean)
76
+ .join(" ");
78
77
 
79
78
  const indicatorClasses = [
80
79
  styles.indicator,
81
- variant === 'success' && styles.indicatorSuccess,
82
- variant === 'warning' && styles.indicatorWarning,
83
- variant === 'danger' && styles.indicatorDanger,
80
+ variant === "neutral" && styles.indicatorNeutral,
81
+ variant === "success" && styles.indicatorSuccess,
82
+ variant === "warning" && styles.indicatorWarning,
83
+ variant === "danger" && styles.indicatorDanger,
84
84
  isIndeterminate && styles.indicatorIndeterminate,
85
- ].filter(Boolean).join(' ');
85
+ ]
86
+ .filter(Boolean)
87
+ .join(" ");
86
88
 
87
- const rootClasses = [styles.root, className].filter(Boolean).join(' ');
89
+ const rootClasses = [styles.root, className].filter(Boolean).join(" ");
88
90
 
89
- const displayValue = formatValue
90
- ? formatValue(percentage)
91
- : `${percentage}%`;
91
+ const displayValue = formatValue ? formatValue(percentage) : `${percentage}%`;
92
92
 
93
93
  // Default value text for screen readers
94
- const effectiveValueText = ariaValueText || (
95
- isIndeterminate ? 'Loading' : `${percentage} percent`
96
- );
94
+ const effectiveValueText =
95
+ ariaValueText || (isIndeterminate ? "Loading" : `${percentage} percent`);
97
96
 
98
97
  return (
99
98
  <BaseProgress.Root
@@ -102,20 +101,15 @@ function ProgressRoot({
102
101
  min={min}
103
102
  max={max}
104
103
  className={rootClasses}
105
- aria-label={ariaLabel || (label ? undefined : 'Progress')}
104
+ role={role ?? "progressbar"}
105
+ aria-label={ariaLabel || (label ? undefined : "Progress")}
106
106
  aria-valuetext={effectiveValueText}
107
107
  aria-busy={isIndeterminate}
108
108
  >
109
109
  {(label || showValue) && (
110
110
  <div className={styles.header}>
111
- {label && (
112
- <BaseProgress.Label className={styles.label}>
113
- {label}
114
- </BaseProgress.Label>
115
- )}
116
- {showValue && !isIndeterminate && (
117
- <span className={styles.value}>{displayValue}</span>
118
- )}
111
+ {label && <BaseProgress.Label className={styles.label}>{label}</BaseProgress.Label>}
112
+ {showValue && !isIndeterminate && <span className={styles.value}>{displayValue}</span>}
119
113
  </div>
120
114
  )}
121
115
  <BaseProgress.Track className={trackClasses}>
@@ -140,14 +134,14 @@ const CIRCLE_SIZES = {
140
134
 
141
135
  function CircularProgressRoot({
142
136
  value = null,
143
- size = 'md',
144
- variant = 'default',
137
+ size = "md",
138
+ variant = "default",
145
139
  showValue = false,
146
140
  strokeWidth: customStrokeWidth,
147
141
  className,
148
- 'aria-label': ariaLabel,
149
- 'aria-labelledby': ariaLabelledBy,
150
- 'aria-valuetext': ariaValueText,
142
+ "aria-label": ariaLabel,
143
+ "aria-labelledby": ariaLabelledBy,
144
+ "aria-valuetext": ariaValueText,
151
145
  ...htmlProps
152
146
  }: CircularProgressProps) {
153
147
  const isIndeterminate = value === null;
@@ -160,26 +154,24 @@ function CircularProgressRoot({
160
154
  const circumference = 2 * Math.PI * radius;
161
155
  const offset = circumference - (percentage / 100) * circumference;
162
156
 
163
- const sizeClass = size === 'sm' ? styles.circularSm
164
- : size === 'lg' ? styles.circularLg
165
- : styles.circularMd;
157
+ const sizeClass =
158
+ size === "sm" ? styles.circularSm : size === "lg" ? styles.circularLg : styles.circularMd;
166
159
 
167
160
  const indicatorClasses = [
168
161
  styles.circularIndicator,
169
- variant === 'success' && styles.circularIndicatorSuccess,
170
- variant === 'warning' && styles.circularIndicatorWarning,
171
- variant === 'danger' && styles.circularIndicatorDanger,
162
+ variant === "success" && styles.circularIndicatorSuccess,
163
+ variant === "warning" && styles.circularIndicatorWarning,
164
+ variant === "danger" && styles.circularIndicatorDanger,
172
165
  isIndeterminate && styles.circularIndicatorIndeterminate,
173
- ].filter(Boolean).join(' ');
174
-
175
- const rootClasses = [styles.circular, sizeClass, className]
166
+ ]
176
167
  .filter(Boolean)
177
- .join(' ');
168
+ .join(" ");
169
+
170
+ const rootClasses = [styles.circular, sizeClass, className].filter(Boolean).join(" ");
178
171
 
179
172
  // Default value text for screen readers
180
- const effectiveValueText = ariaValueText || (
181
- isIndeterminate ? 'Loading' : `${Math.round(percentage)} percent`
182
- );
173
+ const effectiveValueText =
174
+ ariaValueText || (isIndeterminate ? "Loading" : `${Math.round(percentage)} percent`);
183
175
 
184
176
  return (
185
177
  <BaseProgress.Root
@@ -220,11 +212,13 @@ function CircularProgressRoot({
220
212
  strokeWidth={strokeWidth}
221
213
  strokeDasharray={circumference}
222
214
  strokeDashoffset={isIndeterminate ? undefined : offset}
223
- style={isIndeterminate ? { transformOrigin: 'center' } : undefined}
215
+ style={isIndeterminate ? { transformOrigin: "center" } : undefined}
224
216
  />
225
217
  </svg>
226
218
  {showValue && !isIndeterminate && (
227
- <span className={styles.circularValue} aria-hidden="true">{Math.round(percentage)}%</span>
219
+ <span className={styles.circularValue} aria-hidden="true">
220
+ {Math.round(percentage)}%
221
+ </span>
228
222
  )}
229
223
  </BaseProgress.Root>
230
224
  );