@fragments-sdk/ui 0.3.0 → 0.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.
Files changed (133) hide show
  1. package/fragments.json +1 -1
  2. package/package.json +9 -4
  3. package/src/components/Accordion/Accordion.fragment.tsx +186 -0
  4. package/src/components/Accordion/Accordion.module.scss +111 -0
  5. package/src/components/Accordion/index.tsx +271 -0
  6. package/src/components/Alert/Alert.fragment.tsx +66 -41
  7. package/src/components/Alert/Alert.module.scss +31 -21
  8. package/src/components/Alert/index.tsx +202 -73
  9. package/src/components/AppShell/AppShell.fragment.tsx +315 -0
  10. package/src/components/AppShell/AppShell.module.scss +213 -0
  11. package/src/components/AppShell/index.tsx +398 -0
  12. package/src/components/Avatar/index.tsx +8 -9
  13. package/src/components/Badge/Badge.module.scss +16 -10
  14. package/src/components/Badge/index.tsx +20 -6
  15. package/src/components/Box/Box.fragment.tsx +168 -0
  16. package/src/components/Box/Box.module.scss +84 -0
  17. package/src/components/Box/index.tsx +78 -0
  18. package/src/components/Button/Button.module.scss +42 -0
  19. package/src/components/Button/index.tsx +67 -33
  20. package/src/components/ButtonGroup/ButtonGroup.module.scss +37 -0
  21. package/src/components/ButtonGroup/index.tsx +40 -0
  22. package/src/components/Card/Card.fragment.tsx +51 -25
  23. package/src/components/Card/Card.module.scss +52 -5
  24. package/src/components/Card/index.tsx +154 -53
  25. package/src/components/Checkbox/Checkbox.module.scss +4 -4
  26. package/src/components/Checkbox/index.tsx +3 -4
  27. package/src/components/CodeBlock/CodeBlock.fragment.tsx +201 -0
  28. package/src/components/CodeBlock/CodeBlock.module.scss +224 -0
  29. package/src/components/CodeBlock/index.tsx +385 -0
  30. package/src/components/ColorChip/ColorChip.module.scss +165 -0
  31. package/src/components/ColorChip/index.tsx +157 -0
  32. package/src/components/ColorPicker/ColorPicker.module.scss +109 -0
  33. package/src/components/ColorPicker/index.tsx +107 -0
  34. package/src/components/Dialog/Dialog.fragment.tsx +9 -0
  35. package/src/components/Dialog/Dialog.module.scss +26 -7
  36. package/src/components/Dialog/index.tsx +12 -15
  37. package/src/components/EmptyState/EmptyState.fragment.tsx +54 -71
  38. package/src/components/EmptyState/EmptyState.module.scss +9 -9
  39. package/src/components/EmptyState/index.tsx +104 -69
  40. package/src/components/Field/Field.fragment.tsx +165 -0
  41. package/src/components/Field/Field.module.scss +31 -0
  42. package/src/components/Field/index.tsx +143 -0
  43. package/src/components/Fieldset/Fieldset.fragment.tsx +166 -0
  44. package/src/components/Fieldset/Fieldset.module.scss +22 -0
  45. package/src/components/Fieldset/index.tsx +47 -0
  46. package/src/components/Form/Form.fragment.tsx +286 -0
  47. package/src/components/Form/Form.module.scss +8 -0
  48. package/src/components/Form/index.tsx +53 -0
  49. package/src/components/Grid/Grid.fragment.tsx +17 -17
  50. package/src/components/Grid/index.tsx +6 -1
  51. package/src/components/Header/Header.fragment.tsx +192 -0
  52. package/src/components/Header/Header.module.scss +209 -0
  53. package/src/components/Header/index.tsx +363 -0
  54. package/src/components/Icon/Icon.fragment.tsx +138 -0
  55. package/src/components/Icon/Icon.module.scss +38 -0
  56. package/src/components/Icon/index.tsx +58 -0
  57. package/src/components/Image/Image.fragment.tsx +195 -0
  58. package/src/components/Image/Image.module.scss +77 -0
  59. package/src/components/Image/index.tsx +95 -0
  60. package/src/components/Input/Input.module.scss +75 -2
  61. package/src/components/Input/index.tsx +60 -21
  62. package/src/components/Link/Link.fragment.tsx +132 -0
  63. package/src/components/Link/Link.module.scss +67 -0
  64. package/src/components/Link/index.tsx +57 -0
  65. package/src/components/List/List.fragment.tsx +152 -0
  66. package/src/components/List/List.module.scss +71 -0
  67. package/src/components/List/index.tsx +106 -0
  68. package/src/components/Listbox/Listbox.fragment.tsx +191 -0
  69. package/src/components/Listbox/Listbox.module.scss +97 -0
  70. package/src/components/Listbox/index.tsx +121 -0
  71. package/src/components/Menu/Menu.fragment.tsx +9 -0
  72. package/src/components/Menu/Menu.module.scss +17 -1
  73. package/src/components/Menu/index.tsx +3 -3
  74. package/src/components/Popover/Popover.fragment.tsx +9 -0
  75. package/src/components/Popover/Popover.module.scss +33 -10
  76. package/src/components/Popover/index.tsx +9 -11
  77. package/src/components/Progress/Progress.module.scss +11 -11
  78. package/src/components/Progress/index.tsx +34 -7
  79. package/src/components/Prompt/Prompt.fragment.tsx +231 -0
  80. package/src/components/Prompt/Prompt.module.scss +243 -0
  81. package/src/components/Prompt/index.tsx +439 -0
  82. package/src/components/RadioGroup/RadioGroup.module.scss +3 -3
  83. package/src/components/RadioGroup/index.tsx +3 -4
  84. package/src/components/Select/Select.fragment.tsx +9 -0
  85. package/src/components/Select/index.tsx +6 -7
  86. package/src/components/Separator/index.tsx +7 -3
  87. package/src/components/Sidebar/Sidebar.fragment.tsx +9 -0
  88. package/src/components/Sidebar/Sidebar.module.scss +72 -47
  89. package/src/components/Sidebar/index.tsx +5 -3
  90. package/src/components/Skeleton/Skeleton.fragment.tsx +5 -5
  91. package/src/components/Skeleton/Skeleton.module.scss +11 -0
  92. package/src/components/Slider/Slider.module.scss +87 -0
  93. package/src/components/Slider/index.tsx +88 -0
  94. package/src/components/Stack/Stack.module.scss +120 -0
  95. package/src/components/Stack/index.tsx +148 -0
  96. package/src/components/Table/Table.fragment.tsx +7 -0
  97. package/src/components/Table/Table.module.scss +57 -0
  98. package/src/components/Table/index.tsx +44 -6
  99. package/src/components/Tabs/Tabs.fragment.tsx +9 -0
  100. package/src/components/Tabs/Tabs.module.scss +25 -10
  101. package/src/components/Tabs/index.tsx +11 -8
  102. package/src/components/Text/Text.module.scss +82 -0
  103. package/src/components/Text/index.tsx +58 -0
  104. package/src/components/Textarea/index.tsx +3 -7
  105. package/src/components/Theme/Theme.fragment.tsx +128 -0
  106. package/src/components/Theme/ThemeToggle.module.scss +82 -0
  107. package/src/components/Theme/index.tsx +343 -0
  108. package/src/components/Toast/Toast.fragment.tsx +5 -5
  109. package/src/components/Toast/Toast.module.scss +16 -1
  110. package/src/components/Toast/index.tsx +27 -11
  111. package/src/components/Toggle/Toggle.module.scss +25 -10
  112. package/src/components/Toggle/index.tsx +12 -0
  113. package/src/components/ToggleGroup/ToggleGroup.module.scss +134 -0
  114. package/src/components/ToggleGroup/index.tsx +144 -0
  115. package/src/components/Tooltip/Tooltip.module.scss +4 -4
  116. package/src/components/Tooltip/index.tsx +4 -2
  117. package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +134 -0
  118. package/src/components/VisuallyHidden/VisuallyHidden.module.scss +13 -0
  119. package/src/components/VisuallyHidden/index.tsx +29 -0
  120. package/src/index.ts +195 -3
  121. package/src/recipes/AppShell.recipe.ts +175 -0
  122. package/src/recipes/CardGrid.recipe.ts +6 -2
  123. package/src/recipes/ChatInterface.recipe.ts +87 -0
  124. package/src/recipes/CodeExamples.recipe.ts +66 -0
  125. package/src/recipes/DashboardLayout.recipe.ts +46 -12
  126. package/src/recipes/DashboardNav.recipe.ts +183 -0
  127. package/src/recipes/LoginForm.recipe.ts +8 -1
  128. package/src/recipes/SettingsPage.recipe.ts +37 -20
  129. package/src/styles/globals.scss +31 -0
  130. package/src/tokens/_index.scss +3 -0
  131. package/src/tokens/_mixins.scss +54 -1
  132. package/src/tokens/_variables.scss +429 -64
  133. package/src/utils/a11y.tsx +439 -0
@@ -41,17 +41,17 @@
41
41
  border-radius: var(--fui-radius-full, $fui-radius-full);
42
42
  }
43
43
 
44
- // Size variants for track
44
+ // Size variants for track (using closest 7px grid values)
45
45
  .trackSm {
46
- height: 4px;
46
+ height: calc(var(--fui-space-1, $fui-space-1) / 2); // ~3.5px
47
47
  }
48
48
 
49
49
  .trackMd {
50
- height: 8px;
50
+ height: var(--fui-space-1, $fui-space-1); // 7px
51
51
  }
52
52
 
53
53
  .trackLg {
54
- height: 12px;
54
+ height: var(--fui-space-2, $fui-space-2); // 14px
55
55
  }
56
56
 
57
57
  // Indicator (filled portion)
@@ -160,10 +160,10 @@
160
160
  font-variant-numeric: tabular-nums;
161
161
  }
162
162
 
163
- // Size variants for circular
163
+ // Size variants for circular (using closest 7px grid values)
164
164
  .circularSm {
165
- width: 32px;
166
- height: 32px;
165
+ width: var(--fui-space-4, $fui-space-4); // 28px
166
+ height: var(--fui-space-4, $fui-space-4);
167
167
 
168
168
  .circularValue {
169
169
  font-size: var(--fui-font-size-xs, $fui-font-size-xs);
@@ -171,13 +171,13 @@
171
171
  }
172
172
 
173
173
  .circularMd {
174
- width: 48px;
175
- height: 48px;
174
+ width: var(--fui-space-6, $fui-space-6); // 42px
175
+ height: var(--fui-space-6, $fui-space-6);
176
176
  }
177
177
 
178
178
  .circularLg {
179
- width: 64px;
180
- height: 64px;
179
+ width: var(--fui-space-8, $fui-space-8); // 56px
180
+ height: var(--fui-space-8, $fui-space-8);
181
181
 
182
182
  .circularValue {
183
183
  font-size: var(--fui-font-size-base, $fui-font-size-base);
@@ -8,7 +8,7 @@ import '../../styles/globals.scss';
8
8
  // Types
9
9
  // ============================================
10
10
 
11
- export interface ProgressProps {
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 */
@@ -25,11 +25,9 @@ export interface ProgressProps {
25
25
  showValue?: boolean;
26
26
  /** Custom value formatter */
27
27
  formatValue?: (value: number) => string;
28
- /** Additional class name */
29
- className?: string;
30
28
  }
31
29
 
32
- export interface CircularProgressProps {
30
+ export interface CircularProgressProps extends React.HTMLAttributes<HTMLDivElement> {
33
31
  /** Current progress value (0-100). Null for indeterminate. */
34
32
  value?: number | null;
35
33
  /** Size of the circular progress */
@@ -40,8 +38,6 @@ export interface CircularProgressProps {
40
38
  showValue?: boolean;
41
39
  /** Stroke width */
42
40
  strokeWidth?: number;
43
- /** Additional class name */
44
- className?: string;
45
41
  }
46
42
 
47
43
  // ============================================
@@ -58,6 +54,9 @@ export function Progress({
58
54
  showValue = false,
59
55
  formatValue,
60
56
  className,
57
+ 'aria-label': ariaLabel,
58
+ 'aria-valuetext': ariaValueText,
59
+ ...htmlProps
61
60
  }: ProgressProps) {
62
61
  const isIndeterminate = value === null;
63
62
  const percentage = isIndeterminate ? 0 : Math.round(((value - min) / (max - min)) * 100);
@@ -83,12 +82,21 @@ export function Progress({
83
82
  ? formatValue(percentage)
84
83
  : `${percentage}%`;
85
84
 
85
+ // Default value text for screen readers
86
+ const effectiveValueText = ariaValueText || (
87
+ isIndeterminate ? 'Loading' : `${percentage} percent`
88
+ );
89
+
86
90
  return (
87
91
  <BaseProgress.Root
92
+ {...htmlProps}
88
93
  value={value}
89
94
  min={min}
90
95
  max={max}
91
96
  className={rootClasses}
97
+ aria-label={ariaLabel || (label ? undefined : 'Progress')}
98
+ aria-valuetext={effectiveValueText}
99
+ aria-busy={isIndeterminate}
92
100
  >
93
101
  {(label || showValue) && (
94
102
  <div className={styles.header}>
@@ -129,6 +137,10 @@ export function CircularProgress({
129
137
  showValue = false,
130
138
  strokeWidth: customStrokeWidth,
131
139
  className,
140
+ 'aria-label': ariaLabel,
141
+ 'aria-labelledby': ariaLabelledBy,
142
+ 'aria-valuetext': ariaValueText,
143
+ ...htmlProps
132
144
  }: CircularProgressProps) {
133
145
  const isIndeterminate = value === null;
134
146
  const percentage = isIndeterminate ? 0 : Math.min(100, Math.max(0, value));
@@ -156,17 +168,32 @@ export function CircularProgress({
156
168
  .filter(Boolean)
157
169
  .join(' ');
158
170
 
171
+ // Default value text for screen readers
172
+ const effectiveValueText = ariaValueText || (
173
+ isIndeterminate ? 'Loading' : `${Math.round(percentage)} percent`
174
+ );
175
+
159
176
  return (
160
177
  <BaseProgress.Root
178
+ {...htmlProps}
161
179
  value={value}
180
+ min={0}
181
+ max={100}
162
182
  className={rootClasses}
183
+ aria-label={ariaLabel}
184
+ aria-labelledby={ariaLabelledBy}
163
185
  aria-valuenow={isIndeterminate ? undefined : percentage}
186
+ aria-valuemin={0}
187
+ aria-valuemax={100}
188
+ aria-valuetext={effectiveValueText}
189
+ aria-busy={isIndeterminate}
164
190
  >
165
191
  <svg
166
192
  className={styles.circularSvg}
167
193
  width={svgSize}
168
194
  height={svgSize}
169
195
  viewBox={`0 0 ${svgSize} ${svgSize}`}
196
+ aria-hidden="true"
170
197
  >
171
198
  {/* Track circle */}
172
199
  <circle
@@ -189,7 +216,7 @@ export function CircularProgress({
189
216
  />
190
217
  </svg>
191
218
  {showValue && !isIndeterminate && (
192
- <span className={styles.circularValue}>{Math.round(percentage)}%</span>
219
+ <span className={styles.circularValue} aria-hidden="true">{Math.round(percentage)}%</span>
193
220
  )}
194
221
  </BaseProgress.Root>
195
222
  );
@@ -0,0 +1,231 @@
1
+ import React from 'react';
2
+ import { defineSegment } from '@fragments/core';
3
+ import { Prompt } from './index.js';
4
+
5
+ export default defineSegment({
6
+ component: Prompt,
7
+
8
+ meta: {
9
+ name: 'Prompt',
10
+ description: 'Multi-line input with toolbar for AI/chat interfaces',
11
+ category: 'forms',
12
+ status: 'stable',
13
+ tags: ['prompt', 'chat', 'ai', 'input', 'textarea', 'form'],
14
+ },
15
+
16
+ usage: {
17
+ when: [
18
+ 'Building chat or AI assistant interfaces',
19
+ 'Need multi-line input with submit action',
20
+ 'Require toolbar with actions like attachments or model selection',
21
+ ],
22
+ whenNot: [
23
+ 'Simple single-line text input (use Input)',
24
+ 'Basic multi-line without toolbar (use Textarea)',
25
+ 'Search-only interface (use Input with search variant)',
26
+ ],
27
+ guidelines: [
28
+ 'Always provide an onSubmit handler',
29
+ 'Use loading state during API calls',
30
+ 'Consider showing usage/token limits for AI contexts',
31
+ ],
32
+ accessibility: [
33
+ 'Enter submits, Shift+Enter for newline',
34
+ 'Submit button is keyboard accessible',
35
+ 'Loading state prevents duplicate submissions',
36
+ ],
37
+ },
38
+
39
+ props: {
40
+ value: {
41
+ type: 'string',
42
+ description: 'Controlled input value',
43
+ },
44
+ defaultValue: {
45
+ type: 'string',
46
+ description: 'Uncontrolled default value',
47
+ },
48
+ onChange: {
49
+ type: 'function',
50
+ description: 'Called when value changes',
51
+ },
52
+ onSubmit: {
53
+ type: 'function',
54
+ description: 'Called on form submission',
55
+ },
56
+ placeholder: {
57
+ type: 'string',
58
+ default: '"Ask, Search or Chat..."',
59
+ description: 'Placeholder text for the textarea',
60
+ },
61
+ disabled: {
62
+ type: 'boolean',
63
+ default: 'false',
64
+ description: 'Disable the entire prompt',
65
+ },
66
+ loading: {
67
+ type: 'boolean',
68
+ default: 'false',
69
+ description: 'Show loading state',
70
+ },
71
+ minRows: {
72
+ type: 'number',
73
+ default: '1',
74
+ description: 'Minimum number of visible rows',
75
+ },
76
+ maxRows: {
77
+ type: 'number',
78
+ default: '8',
79
+ description: 'Maximum number of visible rows',
80
+ },
81
+ autoResize: {
82
+ type: 'boolean',
83
+ default: 'true',
84
+ description: 'Enable auto-resize based on content',
85
+ },
86
+ submitOnEnter: {
87
+ type: 'boolean',
88
+ default: 'true',
89
+ description: 'Submit on Enter (Shift+Enter for newline)',
90
+ },
91
+ },
92
+
93
+ relations: [
94
+ {
95
+ component: 'Input',
96
+ relationship: 'alternative',
97
+ note: 'Use Input for simple single-line text input',
98
+ },
99
+ {
100
+ component: 'Textarea',
101
+ relationship: 'alternative',
102
+ note: 'Use Textarea for multi-line without toolbar',
103
+ },
104
+ ],
105
+
106
+ contract: {
107
+ propsSummary: [
108
+ 'value: string - controlled input value',
109
+ 'onSubmit: (value: string) => void - submission handler',
110
+ 'placeholder: string - hint text (default: "Ask, Search or Chat...")',
111
+ 'disabled: boolean - disables interaction',
112
+ 'loading: boolean - shows loading state',
113
+ 'minRows/maxRows: number - row constraints (default: 1/8)',
114
+ 'submitOnEnter: boolean - Enter key behavior (default: true)',
115
+ ],
116
+ scenarioTags: [
117
+ 'form.prompt',
118
+ 'form.chat',
119
+ 'form.ai',
120
+ 'ui.chat-input',
121
+ ],
122
+ a11yRules: [
123
+ 'A11Y_TEXTAREA_LABEL',
124
+ 'A11Y_BUTTON_LABEL',
125
+ ],
126
+ bans: [],
127
+ },
128
+
129
+ variants: [
130
+ {
131
+ name: 'Basic',
132
+ description: 'Simple prompt with submit button',
133
+ render: () => (
134
+ <Prompt onSubmit={(value) => console.log(value)}>
135
+ <Prompt.Textarea />
136
+ <Prompt.Toolbar>
137
+ <Prompt.Actions />
138
+ <Prompt.Info>
139
+ <Prompt.Submit />
140
+ </Prompt.Info>
141
+ </Prompt.Toolbar>
142
+ </Prompt>
143
+ ),
144
+ },
145
+ {
146
+ name: 'With Actions',
147
+ description: 'Prompt with attachment and mode buttons',
148
+ render: () => (
149
+ <Prompt onSubmit={(value) => console.log(value)}>
150
+ <Prompt.Textarea />
151
+ <Prompt.Toolbar>
152
+ <Prompt.Actions>
153
+ <Prompt.ActionButton aria-label="Add attachment">
154
+ +
155
+ </Prompt.ActionButton>
156
+ <Prompt.ModeButton>Auto</Prompt.ModeButton>
157
+ </Prompt.Actions>
158
+ <Prompt.Info>
159
+ <Prompt.Submit />
160
+ </Prompt.Info>
161
+ </Prompt.Toolbar>
162
+ </Prompt>
163
+ ),
164
+ },
165
+ {
166
+ name: 'With Usage',
167
+ description: 'Shows token usage indicator',
168
+ render: () => (
169
+ <Prompt onSubmit={(value) => console.log(value)}>
170
+ <Prompt.Textarea />
171
+ <Prompt.Toolbar>
172
+ <Prompt.Actions>
173
+ <Prompt.ActionButton aria-label="Add attachment">
174
+ +
175
+ </Prompt.ActionButton>
176
+ <Prompt.ModeButton active>Auto</Prompt.ModeButton>
177
+ </Prompt.Actions>
178
+ <Prompt.Info>
179
+ <Prompt.Usage>52% used</Prompt.Usage>
180
+ <Prompt.Submit />
181
+ </Prompt.Info>
182
+ </Prompt.Toolbar>
183
+ </Prompt>
184
+ ),
185
+ },
186
+ {
187
+ name: 'Loading State',
188
+ description: 'During API submission',
189
+ render: () => (
190
+ <Prompt
191
+ onSubmit={(value) => console.log(value)}
192
+ loading
193
+ defaultValue="Tell me about the weather..."
194
+ >
195
+ <Prompt.Textarea />
196
+ <Prompt.Toolbar>
197
+ <Prompt.Actions>
198
+ <Prompt.ActionButton aria-label="Add attachment">
199
+ +
200
+ </Prompt.ActionButton>
201
+ <Prompt.ModeButton>Auto</Prompt.ModeButton>
202
+ </Prompt.Actions>
203
+ <Prompt.Info>
204
+ <Prompt.Usage>52% used</Prompt.Usage>
205
+ <Prompt.Submit />
206
+ </Prompt.Info>
207
+ </Prompt.Toolbar>
208
+ </Prompt>
209
+ ),
210
+ },
211
+ {
212
+ name: 'Disabled',
213
+ description: 'Non-interactive prompt',
214
+ render: () => (
215
+ <Prompt onSubmit={(value) => console.log(value)} disabled>
216
+ <Prompt.Textarea />
217
+ <Prompt.Toolbar>
218
+ <Prompt.Actions>
219
+ <Prompt.ActionButton aria-label="Add attachment">
220
+ +
221
+ </Prompt.ActionButton>
222
+ </Prompt.Actions>
223
+ <Prompt.Info>
224
+ <Prompt.Submit />
225
+ </Prompt.Info>
226
+ </Prompt.Toolbar>
227
+ </Prompt>
228
+ ),
229
+ },
230
+ ],
231
+ });
@@ -0,0 +1,243 @@
1
+ @use '../../tokens/variables' as *;
2
+ @use '../../tokens/mixins' as *;
3
+
4
+ // ============================================
5
+ // Prompt Root
6
+ // ============================================
7
+
8
+ .prompt {
9
+ @include surface-elevated;
10
+ display: flex;
11
+ flex-direction: column;
12
+ border-radius: var(--fui-radius-xl, $fui-radius-xl);
13
+ overflow: hidden;
14
+
15
+ &[data-disabled] {
16
+ opacity: 0.5;
17
+ pointer-events: none;
18
+ }
19
+ }
20
+
21
+ // Fixed variant - for viewport-fixed prompts (centered to page)
22
+ .fixed {
23
+ position: fixed;
24
+ bottom: var(--fui-space-4, $fui-space-4);
25
+ left: 50%;
26
+ transform: translateX(-50%);
27
+ width: calc(100% - var(--fui-space-8, $fui-space-8));
28
+ max-width: 800px;
29
+ z-index: 100;
30
+ box-shadow:
31
+ 0 -4px 16px rgba(0, 0, 0, 0.08),
32
+ 0 4px 24px rgba(0, 0, 0, 0.12);
33
+ }
34
+
35
+ // Sticky variant - for content-area fixed prompts (accounts for sidebar)
36
+ // Use --fui-prompt-inset-left CSS variable to offset from sidebar
37
+ .sticky {
38
+ position: fixed;
39
+ bottom: var(--fui-space-4, $fui-space-4);
40
+ left: var(--fui-prompt-inset-left, 0);
41
+ right: 0;
42
+ margin-left: auto;
43
+ margin-right: auto;
44
+ width: calc(100% - var(--fui-prompt-inset-left, 0px) - var(--fui-space-8, $fui-space-8));
45
+ max-width: 800px;
46
+ z-index: 100;
47
+ box-shadow:
48
+ 0 -4px 16px rgba(0, 0, 0, 0.08),
49
+ 0 4px 24px rgba(0, 0, 0, 0.12);
50
+
51
+ // Center within the available space (after sidebar)
52
+ left: calc(var(--fui-prompt-inset-left, 0px) + ((100% - var(--fui-prompt-inset-left, 0px)) / 2));
53
+ transform: translateX(-50%);
54
+ }
55
+
56
+ // ============================================
57
+ // Textarea
58
+ // ============================================
59
+
60
+ .textarea {
61
+ @include button-reset;
62
+ @include text-base;
63
+
64
+ display: block;
65
+ width: 100%;
66
+ min-height: 2.857rem; // ~40px
67
+ padding: var(--fui-space-2, $fui-space-2) var(--fui-space-3, $fui-space-3);
68
+ background: transparent;
69
+ border: none;
70
+ resize: none;
71
+ overflow-y: auto;
72
+ line-height: $fui-line-height-normal;
73
+
74
+ &::placeholder {
75
+ color: var(--fui-text-tertiary, $fui-text-tertiary);
76
+ }
77
+
78
+ &:focus {
79
+ outline: none;
80
+ }
81
+
82
+ &:disabled {
83
+ cursor: not-allowed;
84
+ }
85
+ }
86
+
87
+ // ============================================
88
+ // Toolbar
89
+ // ============================================
90
+
91
+ .toolbar {
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: space-between;
95
+ gap: var(--fui-space-2, $fui-space-2);
96
+ padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
97
+ background-color: var(--fui-bg-secondary, $fui-bg-secondary);
98
+ border-top: 1px solid var(--fui-border, $fui-border);
99
+ }
100
+
101
+ // ============================================
102
+ // Actions & Info
103
+ // ============================================
104
+
105
+ .actions {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: var(--fui-space-1, $fui-space-1);
109
+ }
110
+
111
+ .info {
112
+ display: flex;
113
+ align-items: center;
114
+ gap: var(--fui-space-2, $fui-space-2);
115
+ }
116
+
117
+ // ============================================
118
+ // Action Button (circular, e.g. "+")
119
+ // ============================================
120
+
121
+ .actionButton {
122
+ @include button-reset;
123
+ @include interactive-base;
124
+
125
+ display: flex;
126
+ align-items: center;
127
+ justify-content: center;
128
+ width: 2rem;
129
+ height: 2rem;
130
+ border-radius: var(--fui-radius-full, $fui-radius-full);
131
+ background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
132
+ color: var(--fui-text-primary, $fui-text-primary);
133
+ font-size: var(--fui-font-size-sm, $fui-font-size-sm);
134
+
135
+ &:hover:not(:disabled) {
136
+ background-color: var(--fui-bg-hover, $fui-bg-hover);
137
+ }
138
+
139
+ &:active:not(:disabled) {
140
+ background-color: var(--fui-bg-active, $fui-bg-active);
141
+ }
142
+
143
+ svg {
144
+ width: 1rem;
145
+ height: 1rem;
146
+ }
147
+ }
148
+
149
+ // ============================================
150
+ // Mode Button (text button, e.g. "Auto")
151
+ // ============================================
152
+
153
+ .modeButton {
154
+ @include button-reset;
155
+ @include interactive-base;
156
+
157
+ display: flex;
158
+ align-items: center;
159
+ gap: var(--fui-space-1, $fui-space-1);
160
+ padding: var(--fui-space-1, $fui-space-1) var(--fui-space-2, $fui-space-2);
161
+ border-radius: var(--fui-radius-md, $fui-radius-md);
162
+ background-color: transparent;
163
+ color: var(--fui-text-secondary, $fui-text-secondary);
164
+ font-size: var(--fui-font-size-xs, $fui-font-size-xs);
165
+ font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
166
+
167
+ &:hover:not(:disabled) {
168
+ background-color: var(--fui-bg-hover, $fui-bg-hover);
169
+ color: var(--fui-text-primary, $fui-text-primary);
170
+ }
171
+
172
+ &:active:not(:disabled) {
173
+ background-color: var(--fui-bg-active, $fui-bg-active);
174
+ }
175
+ }
176
+
177
+ .modeButtonActive {
178
+ color: var(--fui-text-primary, $fui-text-primary);
179
+ background-color: var(--fui-bg-tertiary, $fui-bg-tertiary);
180
+ }
181
+
182
+ // ============================================
183
+ // Usage indicator
184
+ // ============================================
185
+
186
+ .usage {
187
+ @include helper-text;
188
+ white-space: nowrap;
189
+ }
190
+
191
+ // ============================================
192
+ // Submit Button
193
+ // ============================================
194
+
195
+ .submit {
196
+ @include button-reset;
197
+ @include interactive-base;
198
+
199
+ display: flex;
200
+ align-items: center;
201
+ justify-content: center;
202
+ width: 2rem;
203
+ height: 2rem;
204
+ border-radius: var(--fui-radius-full, $fui-radius-full);
205
+ background-color: var(--fui-color-accent, $fui-color-accent);
206
+ color: var(--fui-text-inverse, $fui-text-inverse);
207
+
208
+ &:hover:not(:disabled) {
209
+ background-color: var(--fui-color-accent-hover, $fui-color-accent-hover);
210
+ }
211
+
212
+ &:active:not(:disabled) {
213
+ background-color: var(--fui-color-accent-active, $fui-color-accent-active);
214
+ }
215
+
216
+ &:disabled {
217
+ opacity: 0.5;
218
+ cursor: not-allowed;
219
+ }
220
+
221
+ svg {
222
+ width: 1rem;
223
+ height: 1rem;
224
+ }
225
+ }
226
+
227
+ .submitLoading {
228
+ position: relative;
229
+
230
+ svg {
231
+ animation: pulse 1s ease-in-out infinite;
232
+ }
233
+ }
234
+
235
+ @keyframes pulse {
236
+ 0%,
237
+ 100% {
238
+ opacity: 1;
239
+ }
240
+ 50% {
241
+ opacity: 0.5;
242
+ }
243
+ }