@fragments-sdk/ui 0.1.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.
Files changed (73) hide show
  1. package/package.json +44 -0
  2. package/src/brand.ts +15 -0
  3. package/src/components/Alert/Alert.fragment.tsx +163 -0
  4. package/src/components/Alert/Alert.module.scss +116 -0
  5. package/src/components/Alert/index.tsx +95 -0
  6. package/src/components/Avatar/Avatar.fragment.tsx +147 -0
  7. package/src/components/Avatar/Avatar.module.scss +136 -0
  8. package/src/components/Avatar/index.tsx +177 -0
  9. package/src/components/Badge/Badge.fragment.tsx +151 -0
  10. package/src/components/Badge/Badge.module.scss +87 -0
  11. package/src/components/Badge/index.tsx +55 -0
  12. package/src/components/Button/Button.fragment.tsx +159 -0
  13. package/src/components/Button/Button.module.scss +97 -0
  14. package/src/components/Button/index.tsx +51 -0
  15. package/src/components/Card/Card.fragment.tsx +156 -0
  16. package/src/components/Card/Card.module.scss +86 -0
  17. package/src/components/Card/index.tsx +79 -0
  18. package/src/components/Checkbox/Checkbox.fragment.tsx +166 -0
  19. package/src/components/Checkbox/Checkbox.module.scss +144 -0
  20. package/src/components/Checkbox/index.tsx +166 -0
  21. package/src/components/Dialog/Dialog.fragment.tsx +179 -0
  22. package/src/components/Dialog/Dialog.module.scss +158 -0
  23. package/src/components/Dialog/index.tsx +230 -0
  24. package/src/components/EmptyState/EmptyState.fragment.tsx +222 -0
  25. package/src/components/EmptyState/EmptyState.module.scss +120 -0
  26. package/src/components/EmptyState/index.tsx +80 -0
  27. package/src/components/Input/Input.fragment.tsx +174 -0
  28. package/src/components/Input/Input.module.scss +64 -0
  29. package/src/components/Input/index.tsx +76 -0
  30. package/src/components/Menu/Menu.fragment.tsx +168 -0
  31. package/src/components/Menu/Menu.module.scss +190 -0
  32. package/src/components/Menu/index.tsx +318 -0
  33. package/src/components/Popover/Popover.fragment.tsx +178 -0
  34. package/src/components/Popover/Popover.module.scss +165 -0
  35. package/src/components/Popover/index.tsx +229 -0
  36. package/src/components/Progress/Progress.fragment.tsx +142 -0
  37. package/src/components/Progress/Progress.module.scss +185 -0
  38. package/src/components/Progress/index.tsx +196 -0
  39. package/src/components/RadioGroup/RadioGroup.fragment.tsx +188 -0
  40. package/src/components/RadioGroup/RadioGroup.module.scss +155 -0
  41. package/src/components/RadioGroup/index.tsx +166 -0
  42. package/src/components/Select/Select.fragment.tsx +173 -0
  43. package/src/components/Select/Select.module.scss +187 -0
  44. package/src/components/Select/index.tsx +233 -0
  45. package/src/components/Separator/Separator.fragment.tsx +148 -0
  46. package/src/components/Separator/Separator.module.scss +92 -0
  47. package/src/components/Separator/index.tsx +89 -0
  48. package/src/components/Skeleton/Skeleton.fragment.tsx +147 -0
  49. package/src/components/Skeleton/Skeleton.module.scss +166 -0
  50. package/src/components/Skeleton/index.tsx +185 -0
  51. package/src/components/Table/Table.fragment.tsx +193 -0
  52. package/src/components/Table/Table.module.scss +152 -0
  53. package/src/components/Table/index.tsx +266 -0
  54. package/src/components/Tabs/Tabs.fragment.tsx +155 -0
  55. package/src/components/Tabs/Tabs.module.scss +142 -0
  56. package/src/components/Tabs/index.tsx +142 -0
  57. package/src/components/Textarea/Textarea.fragment.tsx +171 -0
  58. package/src/components/Textarea/Textarea.module.scss +89 -0
  59. package/src/components/Textarea/index.tsx +128 -0
  60. package/src/components/Toast/Toast.fragment.tsx +210 -0
  61. package/src/components/Toast/Toast.module.scss +227 -0
  62. package/src/components/Toast/index.tsx +315 -0
  63. package/src/components/Toggle/Toggle.fragment.tsx +174 -0
  64. package/src/components/Toggle/Toggle.module.scss +103 -0
  65. package/src/components/Toggle/index.tsx +80 -0
  66. package/src/components/Tooltip/Tooltip.fragment.tsx +158 -0
  67. package/src/components/Tooltip/Tooltip.module.scss +82 -0
  68. package/src/components/Tooltip/index.tsx +135 -0
  69. package/src/index.ts +151 -0
  70. package/src/scss.d.ts +4 -0
  71. package/src/styles/globals.scss +17 -0
  72. package/src/tokens/_mixins.scss +93 -0
  73. package/src/tokens/_variables.scss +276 -0
@@ -0,0 +1,165 @@
1
+ @use '../../tokens/variables' as *;
2
+ @use '../../tokens/mixins' as *;
3
+
4
+ // Positioner
5
+ .positioner {
6
+ z-index: 50;
7
+ outline: none;
8
+ }
9
+
10
+ // Popup container
11
+ .popup {
12
+ @include surface-elevated;
13
+ @include text-base;
14
+
15
+ min-width: 12rem;
16
+ max-width: 24rem;
17
+ padding: var(--fui-space-4, $fui-space-4);
18
+ box-shadow: var(--fui-shadow-md, $fui-shadow-md);
19
+
20
+ // Animation
21
+ opacity: 0;
22
+ transform: scale(0.95) translateY(-4px);
23
+ transform-origin: var(--transform-origin);
24
+ transition:
25
+ opacity var(--fui-transition-fast, $fui-transition-fast),
26
+ transform var(--fui-transition-fast, $fui-transition-fast);
27
+
28
+ &[data-open] {
29
+ opacity: 1;
30
+ transform: scale(1) translateY(0);
31
+ }
32
+
33
+ &[data-starting-style] {
34
+ opacity: 0;
35
+ transform: scale(0.95) translateY(-4px);
36
+ }
37
+
38
+ &[data-ending-style] {
39
+ opacity: 0;
40
+ transform: scale(0.95) translateY(4px);
41
+ }
42
+
43
+ // Adjust animation direction based on side
44
+ &[data-side='top'] {
45
+ &[data-starting-style] {
46
+ transform: scale(0.95) translateY(4px);
47
+ }
48
+
49
+ &[data-ending-style] {
50
+ transform: scale(0.95) translateY(-4px);
51
+ }
52
+ }
53
+
54
+ &[data-side='left'],
55
+ &[data-side='right'] {
56
+ &[data-starting-style],
57
+ &[data-ending-style] {
58
+ transform: scale(0.95) translateY(0);
59
+ }
60
+ }
61
+ }
62
+
63
+ // Size variants
64
+ .sm {
65
+ padding: var(--fui-space-3, $fui-space-3);
66
+ max-width: 16rem;
67
+ }
68
+
69
+ .md {
70
+ // Default styles
71
+ }
72
+
73
+ .lg {
74
+ padding: var(--fui-space-5, $fui-space-5);
75
+ max-width: 32rem;
76
+ }
77
+
78
+ // Title
79
+ .title {
80
+ margin: 0 0 var(--fui-space-1, $fui-space-1);
81
+ font-size: var(--fui-font-size-sm, $fui-font-size-sm);
82
+ font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold);
83
+ color: var(--fui-text-primary, $fui-text-primary);
84
+ line-height: var(--fui-line-height-tight, $fui-line-height-tight);
85
+ }
86
+
87
+ // Description
88
+ .description {
89
+ margin: 0;
90
+ font-size: var(--fui-font-size-sm, $fui-font-size-sm);
91
+ color: var(--fui-text-secondary, $fui-text-secondary);
92
+ line-height: var(--fui-line-height-normal, $fui-line-height-normal);
93
+ }
94
+
95
+ // Close button
96
+ .close {
97
+ @include button-reset;
98
+ @include interactive-base;
99
+
100
+ position: absolute;
101
+ top: var(--fui-space-2, $fui-space-2);
102
+ right: var(--fui-space-2, $fui-space-2);
103
+ display: flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ width: 1.5rem;
107
+ height: 1.5rem;
108
+ border-radius: var(--fui-radius-sm, $fui-radius-sm);
109
+ color: var(--fui-text-tertiary, $fui-text-tertiary);
110
+
111
+ &:hover {
112
+ background-color: var(--fui-bg-hover, $fui-bg-hover);
113
+ color: var(--fui-text-primary, $fui-text-primary);
114
+ }
115
+
116
+ svg {
117
+ width: 0.875rem;
118
+ height: 0.875rem;
119
+ }
120
+ }
121
+
122
+ // Arrow
123
+ .arrow {
124
+ width: 10px;
125
+ height: 10px;
126
+ transform: rotate(45deg);
127
+ background-color: var(--fui-bg-elevated, $fui-bg-elevated);
128
+ border: 1px solid var(--fui-border, $fui-border);
129
+
130
+ &[data-side='top'] {
131
+ border-top: none;
132
+ border-left: none;
133
+ }
134
+
135
+ &[data-side='bottom'] {
136
+ border-bottom: none;
137
+ border-right: none;
138
+ }
139
+
140
+ &[data-side='left'] {
141
+ border-left: none;
142
+ border-bottom: none;
143
+ }
144
+
145
+ &[data-side='right'] {
146
+ border-right: none;
147
+ border-top: none;
148
+ }
149
+ }
150
+
151
+ // Body content area
152
+ .body {
153
+ margin-top: var(--fui-space-3, $fui-space-3);
154
+ }
155
+
156
+ // Footer for actions
157
+ .footer {
158
+ display: flex;
159
+ align-items: center;
160
+ justify-content: flex-end;
161
+ gap: var(--fui-space-2, $fui-space-2);
162
+ margin-top: var(--fui-space-4, $fui-space-4);
163
+ padding-top: var(--fui-space-3, $fui-space-3);
164
+ border-top: 1px solid var(--fui-border, $fui-border);
165
+ }
@@ -0,0 +1,229 @@
1
+ import * as React from 'react';
2
+ import { Popover as BasePopover } from '@base-ui/react/popover';
3
+ import styles from './Popover.module.scss';
4
+ // Import globals to ensure CSS variables are defined
5
+ import '../../styles/globals.scss';
6
+
7
+ // ============================================
8
+ // Types
9
+ // ============================================
10
+
11
+ export interface PopoverProps {
12
+ children: React.ReactNode;
13
+ open?: boolean;
14
+ defaultOpen?: boolean;
15
+ onOpenChange?: (open: boolean) => void;
16
+ modal?: boolean;
17
+ }
18
+
19
+ export interface PopoverTriggerProps {
20
+ children: React.ReactNode;
21
+ asChild?: boolean;
22
+ className?: string;
23
+ }
24
+
25
+ export interface PopoverContentProps {
26
+ children: React.ReactNode;
27
+ size?: 'sm' | 'md' | 'lg';
28
+ side?: 'top' | 'bottom' | 'left' | 'right';
29
+ align?: 'start' | 'center' | 'end';
30
+ sideOffset?: number;
31
+ arrow?: boolean;
32
+ className?: string;
33
+ }
34
+
35
+ export interface PopoverTitleProps {
36
+ children: React.ReactNode;
37
+ className?: string;
38
+ }
39
+
40
+ export interface PopoverDescriptionProps {
41
+ children: React.ReactNode;
42
+ className?: string;
43
+ }
44
+
45
+ export interface PopoverBodyProps {
46
+ children: React.ReactNode;
47
+ className?: string;
48
+ }
49
+
50
+ export interface PopoverFooterProps {
51
+ children: React.ReactNode;
52
+ className?: string;
53
+ }
54
+
55
+ export interface PopoverCloseProps {
56
+ children?: React.ReactNode;
57
+ asChild?: boolean;
58
+ className?: string;
59
+ }
60
+
61
+ // ============================================
62
+ // Icons
63
+ // ============================================
64
+
65
+ function CloseIcon() {
66
+ return (
67
+ <svg
68
+ xmlns="http://www.w3.org/2000/svg"
69
+ width="14"
70
+ height="14"
71
+ viewBox="0 0 24 24"
72
+ fill="none"
73
+ stroke="currentColor"
74
+ strokeWidth="2"
75
+ strokeLinecap="round"
76
+ strokeLinejoin="round"
77
+ aria-hidden="true"
78
+ >
79
+ <line x1="18" y1="6" x2="6" y2="18" />
80
+ <line x1="6" y1="6" x2="18" y2="18" />
81
+ </svg>
82
+ );
83
+ }
84
+
85
+ // ============================================
86
+ // Components
87
+ // ============================================
88
+
89
+ function PopoverRoot({
90
+ children,
91
+ open,
92
+ defaultOpen,
93
+ onOpenChange,
94
+ modal = false,
95
+ }: PopoverProps) {
96
+ return (
97
+ <BasePopover.Root
98
+ open={open}
99
+ defaultOpen={defaultOpen}
100
+ onOpenChange={onOpenChange}
101
+ modal={modal}
102
+ >
103
+ {children}
104
+ </BasePopover.Root>
105
+ );
106
+ }
107
+
108
+ function PopoverTrigger({ children, asChild, className }: PopoverTriggerProps) {
109
+ if (asChild) {
110
+ return (
111
+ <BasePopover.Trigger className={className} render={children as React.ReactElement}>
112
+ {null}
113
+ </BasePopover.Trigger>
114
+ );
115
+ }
116
+
117
+ return (
118
+ <BasePopover.Trigger className={className}>
119
+ {children}
120
+ </BasePopover.Trigger>
121
+ );
122
+ }
123
+
124
+ function PopoverContent({
125
+ children,
126
+ size = 'md',
127
+ side = 'bottom',
128
+ align = 'center',
129
+ sideOffset = 8,
130
+ arrow = false,
131
+ className,
132
+ }: PopoverContentProps) {
133
+ const popupClasses = [
134
+ styles.popup,
135
+ size !== 'md' && styles[size],
136
+ className,
137
+ ].filter(Boolean).join(' ');
138
+
139
+ return (
140
+ <BasePopover.Portal>
141
+ <BasePopover.Positioner
142
+ side={side}
143
+ align={align}
144
+ sideOffset={sideOffset}
145
+ className={styles.positioner}
146
+ >
147
+ <BasePopover.Popup className={popupClasses}>
148
+ {children}
149
+ {arrow && <BasePopover.Arrow className={styles.arrow} />}
150
+ </BasePopover.Popup>
151
+ </BasePopover.Positioner>
152
+ </BasePopover.Portal>
153
+ );
154
+ }
155
+
156
+ function PopoverTitle({ children, className }: PopoverTitleProps) {
157
+ const classes = [styles.title, className].filter(Boolean).join(' ');
158
+ return <BasePopover.Title className={classes}>{children}</BasePopover.Title>;
159
+ }
160
+
161
+ function PopoverDescription({ children, className }: PopoverDescriptionProps) {
162
+ const classes = [styles.description, className].filter(Boolean).join(' ');
163
+ return (
164
+ <BasePopover.Description className={classes}>
165
+ {children}
166
+ </BasePopover.Description>
167
+ );
168
+ }
169
+
170
+ function PopoverBody({ children, className }: PopoverBodyProps) {
171
+ const classes = [styles.body, className].filter(Boolean).join(' ');
172
+ return <div className={classes}>{children}</div>;
173
+ }
174
+
175
+ function PopoverFooter({ children, className }: PopoverFooterProps) {
176
+ const classes = [styles.footer, className].filter(Boolean).join(' ');
177
+ return <div className={classes}>{children}</div>;
178
+ }
179
+
180
+ function PopoverClose({ children, asChild, className }: PopoverCloseProps) {
181
+ // Default close button (X icon)
182
+ if (!children) {
183
+ return (
184
+ <BasePopover.Close className={[styles.close, className].filter(Boolean).join(' ')}>
185
+ <CloseIcon />
186
+ </BasePopover.Close>
187
+ );
188
+ }
189
+
190
+ if (asChild) {
191
+ return (
192
+ <BasePopover.Close className={className} render={children as React.ReactElement}>
193
+ {null}
194
+ </BasePopover.Close>
195
+ );
196
+ }
197
+
198
+ return (
199
+ <BasePopover.Close className={className}>
200
+ {children}
201
+ </BasePopover.Close>
202
+ );
203
+ }
204
+
205
+ // ============================================
206
+ // Export compound component
207
+ // ============================================
208
+
209
+ export const Popover = Object.assign(PopoverRoot, {
210
+ Trigger: PopoverTrigger,
211
+ Content: PopoverContent,
212
+ Title: PopoverTitle,
213
+ Description: PopoverDescription,
214
+ Body: PopoverBody,
215
+ Footer: PopoverFooter,
216
+ Close: PopoverClose,
217
+ });
218
+
219
+ // Re-export individual components
220
+ export {
221
+ PopoverRoot,
222
+ PopoverTrigger,
223
+ PopoverContent,
224
+ PopoverTitle,
225
+ PopoverDescription,
226
+ PopoverBody,
227
+ PopoverFooter,
228
+ PopoverClose,
229
+ };
@@ -0,0 +1,142 @@
1
+ import React from 'react';
2
+ import { defineSegment } from '@fragments/core';
3
+ import { Progress, CircularProgress } from './index.js';
4
+
5
+ export default defineSegment({
6
+ component: Progress,
7
+
8
+ meta: {
9
+ name: 'Progress',
10
+ description: 'Visual indicator of task completion or loading state. Available in linear and circular variants.',
11
+ category: 'feedback',
12
+ status: 'stable',
13
+ tags: ['progress', 'loading', 'indicator', 'percentage', 'status'],
14
+ since: '0.1.0',
15
+ },
16
+
17
+ usage: {
18
+ when: [
19
+ 'Showing upload/download progress',
20
+ 'Displaying task completion percentage',
21
+ 'Form completion indicators',
22
+ 'Loading states with known duration',
23
+ ],
24
+ whenNot: [
25
+ 'Unknown loading duration (use Spinner)',
26
+ 'Step-based progress (use Stepper)',
27
+ 'Status without percentage (use Badge)',
28
+ ],
29
+ guidelines: [
30
+ 'Use determinate progress when you know the completion percentage',
31
+ 'Use indeterminate for unknown durations',
32
+ 'Include a label for context when the purpose isnt obvious',
33
+ 'Use appropriate color variants for success/warning/danger states',
34
+ ],
35
+ accessibility: [
36
+ 'Uses role="progressbar" with aria-valuenow',
37
+ 'Label is associated with the progress bar',
38
+ 'State changes are announced to screen readers',
39
+ ],
40
+ },
41
+
42
+ props: {
43
+ value: {
44
+ type: 'number',
45
+ description: 'Current progress value (0-100). Null for indeterminate.',
46
+ },
47
+ size: {
48
+ type: 'enum',
49
+ description: 'Size of the progress bar',
50
+ values: ['sm', 'md', 'lg'],
51
+ default: 'md',
52
+ },
53
+ variant: {
54
+ type: 'enum',
55
+ description: 'Color variant',
56
+ values: ['default', 'success', 'warning', 'danger'],
57
+ default: 'default',
58
+ },
59
+ label: {
60
+ type: 'string',
61
+ description: 'Label text above the progress bar',
62
+ },
63
+ showValue: {
64
+ type: 'boolean',
65
+ description: 'Show percentage value',
66
+ default: 'false',
67
+ },
68
+ },
69
+
70
+ relations: [
71
+ { component: 'Badge', relationship: 'alternative', note: 'Use Badge for status without percentage' },
72
+ { component: 'Alert', relationship: 'sibling', note: 'Use Alert for completion messages' },
73
+ ],
74
+
75
+ contract: {
76
+ propsSummary: [
77
+ 'value: number|null - progress percentage (null for indeterminate)',
78
+ 'size: sm|md|lg - bar thickness',
79
+ 'variant: default|success|warning|danger - color',
80
+ 'label: string - descriptive label',
81
+ 'showValue: boolean - display percentage',
82
+ ],
83
+ scenarioTags: [
84
+ 'feedback.progress',
85
+ 'status.loading',
86
+ 'display.percentage',
87
+ ],
88
+ a11yRules: ['A11Y_PROGRESS_ROLE', 'A11Y_PROGRESS_VALUE'],
89
+ },
90
+
91
+ variants: [
92
+ {
93
+ name: 'Default',
94
+ description: 'Basic progress bar with percentage',
95
+ render: () => (
96
+ <Progress value={60} label="Uploading..." showValue />
97
+ ),
98
+ },
99
+ {
100
+ name: 'Variants',
101
+ description: 'Different color variants for different states',
102
+ render: () => (
103
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '16px', width: '300px' }}>
104
+ <Progress value={75} variant="default" label="Processing" showValue />
105
+ <Progress value={100} variant="success" label="Complete" showValue />
106
+ <Progress value={80} variant="warning" label="Almost full" showValue />
107
+ <Progress value={95} variant="danger" label="Storage critical" showValue />
108
+ </div>
109
+ ),
110
+ },
111
+ {
112
+ name: 'Sizes',
113
+ description: 'Different progress bar sizes',
114
+ render: () => (
115
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '16px', width: '300px' }}>
116
+ <Progress value={50} size="sm" label="Small" />
117
+ <Progress value={50} size="md" label="Medium" />
118
+ <Progress value={50} size="lg" label="Large" />
119
+ </div>
120
+ ),
121
+ },
122
+ {
123
+ name: 'Indeterminate',
124
+ description: 'Loading state with unknown duration',
125
+ render: () => (
126
+ <Progress value={null} label="Loading..." />
127
+ ),
128
+ },
129
+ {
130
+ name: 'Circular',
131
+ description: 'Circular progress indicator',
132
+ render: () => (
133
+ <div style={{ display: 'flex', gap: '24px', alignItems: 'center' }}>
134
+ <CircularProgress value={25} size="sm" />
135
+ <CircularProgress value={50} size="md" showValue />
136
+ <CircularProgress value={75} size="lg" showValue variant="success" />
137
+ <CircularProgress value={null} size="md" />
138
+ </div>
139
+ ),
140
+ },
141
+ ],
142
+ });
@@ -0,0 +1,185 @@
1
+ @use '../../tokens/variables' as *;
2
+ @use '../../tokens/mixins' as *;
3
+
4
+ // Root container
5
+ .root {
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: var(--fui-space-1, $fui-space-1);
9
+ width: 100%;
10
+ font-family: var(--fui-font-sans, $fui-font-sans);
11
+ }
12
+
13
+ // Header with label and value
14
+ .header {
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: space-between;
18
+ gap: var(--fui-space-2, $fui-space-2);
19
+ }
20
+
21
+ // Label text
22
+ .label {
23
+ font-size: var(--fui-font-size-sm, $fui-font-size-sm);
24
+ font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
25
+ color: var(--fui-text-primary, $fui-text-primary);
26
+ }
27
+
28
+ // Value display
29
+ .value {
30
+ font-size: var(--fui-font-size-sm, $fui-font-size-sm);
31
+ color: var(--fui-text-secondary, $fui-text-secondary);
32
+ font-variant-numeric: tabular-nums;
33
+ }
34
+
35
+ // Track (background bar)
36
+ .track {
37
+ position: relative;
38
+ width: 100%;
39
+ overflow: hidden;
40
+ background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
41
+ border-radius: var(--fui-radius-full, $fui-radius-full);
42
+ }
43
+
44
+ // Size variants for track
45
+ .trackSm {
46
+ height: 4px;
47
+ }
48
+
49
+ .trackMd {
50
+ height: 8px;
51
+ }
52
+
53
+ .trackLg {
54
+ height: 12px;
55
+ }
56
+
57
+ // Indicator (filled portion)
58
+ .indicator {
59
+ height: 100%;
60
+ background-color: var(--fui-color-accent, $fui-color-accent);
61
+ border-radius: var(--fui-radius-full, $fui-radius-full);
62
+ transition: width var(--fui-transition-normal, $fui-transition-normal);
63
+ }
64
+
65
+ // Color variants
66
+ .indicatorSuccess {
67
+ background-color: var(--fui-color-success, $fui-color-success);
68
+ }
69
+
70
+ .indicatorWarning {
71
+ background-color: var(--fui-color-warning, $fui-color-warning);
72
+ }
73
+
74
+ .indicatorDanger {
75
+ background-color: var(--fui-color-danger, $fui-color-danger);
76
+ }
77
+
78
+ // Indeterminate animation
79
+ .indicatorIndeterminate {
80
+ width: 50% !important;
81
+ animation: indeterminate 1.5s ease-in-out infinite;
82
+ }
83
+
84
+ @keyframes indeterminate {
85
+ 0% {
86
+ transform: translateX(-100%);
87
+ }
88
+ 50% {
89
+ transform: translateX(100%);
90
+ }
91
+ 100% {
92
+ transform: translateX(-100%);
93
+ }
94
+ }
95
+
96
+ // Complete state
97
+ .root[data-complete] .indicator {
98
+ background-color: var(--fui-color-success, $fui-color-success);
99
+ }
100
+
101
+ // Circular progress variant
102
+ .circular {
103
+ position: relative;
104
+ display: inline-flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ }
108
+
109
+ .circularSvg {
110
+ transform: rotate(-90deg);
111
+ }
112
+
113
+ .circularTrack {
114
+ stroke: var(--fui-bg-tertiary, $fui-bg-tertiary);
115
+ fill: none;
116
+ }
117
+
118
+ .circularIndicator {
119
+ stroke: var(--fui-color-accent, $fui-color-accent);
120
+ fill: none;
121
+ stroke-linecap: round;
122
+ transition: stroke-dashoffset var(--fui-transition-normal, $fui-transition-normal);
123
+ }
124
+
125
+ .circularIndicatorSuccess {
126
+ stroke: var(--fui-color-success, $fui-color-success);
127
+ }
128
+
129
+ .circularIndicatorWarning {
130
+ stroke: var(--fui-color-warning, $fui-color-warning);
131
+ }
132
+
133
+ .circularIndicatorDanger {
134
+ stroke: var(--fui-color-danger, $fui-color-danger);
135
+ }
136
+
137
+ .circularIndicatorIndeterminate {
138
+ animation: circularIndeterminate 1.5s ease-in-out infinite;
139
+ }
140
+
141
+ @keyframes circularIndeterminate {
142
+ 0% {
143
+ stroke-dashoffset: 280;
144
+ transform: rotate(0deg);
145
+ }
146
+ 50% {
147
+ stroke-dashoffset: 70;
148
+ }
149
+ 100% {
150
+ stroke-dashoffset: 280;
151
+ transform: rotate(360deg);
152
+ }
153
+ }
154
+
155
+ .circularValue {
156
+ position: absolute;
157
+ font-size: var(--fui-font-size-sm, $fui-font-size-sm);
158
+ font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
159
+ color: var(--fui-text-primary, $fui-text-primary);
160
+ font-variant-numeric: tabular-nums;
161
+ }
162
+
163
+ // Size variants for circular
164
+ .circularSm {
165
+ width: 32px;
166
+ height: 32px;
167
+
168
+ .circularValue {
169
+ font-size: var(--fui-font-size-xs, $fui-font-size-xs);
170
+ }
171
+ }
172
+
173
+ .circularMd {
174
+ width: 48px;
175
+ height: 48px;
176
+ }
177
+
178
+ .circularLg {
179
+ width: 64px;
180
+ height: 64px;
181
+
182
+ .circularValue {
183
+ font-size: var(--fui-font-size-base, $fui-font-size-base);
184
+ }
185
+ }