@chai-ui/react 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.
- package/DESIGN.md +207 -0
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/dist/ChaiProvider.d.ts +49 -0
- package/dist/ChaiProvider.d.ts.map +1 -0
- package/dist/ChaiProvider.js +54 -0
- package/dist/ChaiProvider.js.map +1 -0
- package/dist/Composer.d.ts +169 -0
- package/dist/Composer.d.ts.map +1 -0
- package/dist/Composer.js +389 -0
- package/dist/Composer.js.map +1 -0
- package/dist/MediaAnalyzer.d.ts +102 -0
- package/dist/MediaAnalyzer.d.ts.map +1 -0
- package/dist/MediaAnalyzer.js +250 -0
- package/dist/MediaAnalyzer.js.map +1 -0
- package/dist/ResultCard.d.ts +55 -0
- package/dist/ResultCard.d.ts.map +1 -0
- package/dist/ResultCard.js +409 -0
- package/dist/ResultCard.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/primitives/Pagination.d.ts +19 -0
- package/dist/primitives/Pagination.d.ts.map +1 -0
- package/dist/primitives/Pagination.js +13 -0
- package/dist/primitives/Pagination.js.map +1 -0
- package/dist/primitives/SearchMenu.d.ts +69 -0
- package/dist/primitives/SearchMenu.d.ts.map +1 -0
- package/dist/primitives/SearchMenu.js +88 -0
- package/dist/primitives/SearchMenu.js.map +1 -0
- package/dist/primitives/Toggle.d.ts +9 -0
- package/dist/primitives/Toggle.d.ts.map +1 -0
- package/dist/primitives/Toggle.js +28 -0
- package/dist/primitives/Toggle.js.map +1 -0
- package/dist/primitives/index.d.ts +4 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +4 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/style.css +1860 -0
- package/dist/use-latest.d.ts +7 -0
- package/dist/use-latest.d.ts.map +1 -0
- package/dist/use-latest.js +17 -0
- package/dist/use-latest.js.map +1 -0
- package/dist/useComposer.d.ts +75 -0
- package/dist/useComposer.d.ts.map +1 -0
- package/dist/useComposer.js +191 -0
- package/dist/useComposer.js.map +1 -0
- package/dist/useMediaAnalyzer.d.ts +37 -0
- package/dist/useMediaAnalyzer.d.ts.map +1 -0
- package/dist/useMediaAnalyzer.js +114 -0
- package/dist/useMediaAnalyzer.js.map +1 -0
- package/package.json +88 -0
- package/src/ChaiProvider.tsx +88 -0
- package/src/Composer.tsx +961 -0
- package/src/MediaAnalyzer.tsx +527 -0
- package/src/ResultCard.tsx +980 -0
- package/src/index.ts +36 -0
- package/src/primitives/Pagination.tsx +41 -0
- package/src/primitives/SearchMenu.tsx +284 -0
- package/src/primitives/Toggle.tsx +43 -0
- package/src/primitives/index.ts +3 -0
- package/src/style.css +1860 -0
- package/src/use-latest.ts +18 -0
- package/src/useComposer.ts +289 -0
- package/src/useMediaAnalyzer.ts +157 -0
package/dist/style.css
ADDED
|
@@ -0,0 +1,1860 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @chai-ui/react default styling.
|
|
3
|
+
*
|
|
4
|
+
* Requires @chai-ui/tokens/css to be loaded first (it defines every
|
|
5
|
+
* --chai-* variable referenced below) — see the package README.
|
|
6
|
+
*
|
|
7
|
+
* Class names use a `chai-` prefix with BEM-style `__element` and
|
|
8
|
+
* `--modifier` suffixes, one block per component or primitive.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
@import "@fontsource/roboto/400.css";
|
|
12
|
+
@import "@fontsource/roboto/500.css";
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* Every switch in the app, locked once here rather than per call site: the
|
|
16
|
+
* "on" track is the brand green, and the "off" state is plain gray rather
|
|
17
|
+
* than Material's green-tinted neutrals. All values are Chai tokens.
|
|
18
|
+
*/
|
|
19
|
+
.chai-switch {
|
|
20
|
+
--md-switch-selected-track-color: var(--chai-color-semantic-accent);
|
|
21
|
+
--md-switch-selected-hover-track-color: var(--chai-color-semantic-accent);
|
|
22
|
+
--md-switch-selected-focus-track-color: var(--chai-color-semantic-accent);
|
|
23
|
+
--md-switch-selected-pressed-track-color: var(--chai-color-semantic-accent);
|
|
24
|
+
--md-switch-track-color: var(--chai-color-semantic-control-track);
|
|
25
|
+
--md-switch-hover-track-color: var(--chai-color-semantic-control-track);
|
|
26
|
+
--md-switch-focus-track-color: var(--chai-color-semantic-control-track);
|
|
27
|
+
--md-switch-pressed-track-color: var(--chai-color-semantic-control-track);
|
|
28
|
+
--md-switch-track-outline-color: var(--chai-color-semantic-control-border-soft);
|
|
29
|
+
--md-switch-hover-track-outline-color: var(--chai-color-semantic-control-border-soft);
|
|
30
|
+
--md-switch-focus-track-outline-color: var(--chai-color-semantic-control-border-soft);
|
|
31
|
+
--md-switch-pressed-track-outline-color: var(--chai-color-semantic-control-border-soft);
|
|
32
|
+
--md-switch-handle-color: var(--chai-color-semantic-control-text-muted);
|
|
33
|
+
--md-switch-hover-handle-color: var(--chai-color-semantic-control-text-muted);
|
|
34
|
+
--md-switch-focus-handle-color: var(--chai-color-semantic-control-text-muted);
|
|
35
|
+
--md-switch-pressed-handle-color: var(--chai-color-semantic-control-text-muted);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* --- Buttons --- */
|
|
39
|
+
|
|
40
|
+
.chai-btn {
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
gap: 6px;
|
|
45
|
+
padding: 8px 14px;
|
|
46
|
+
/* MD3 buttons are fully-rounded (stadium) by default (m3.material.io/components/buttons). */
|
|
47
|
+
border-radius: var(--chai-radius-full);
|
|
48
|
+
border: 1px solid var(--chai-color-semantic-control-border-faint);
|
|
49
|
+
background: var(--chai-color-semantic-control-surface-raised);
|
|
50
|
+
color: var(--chai-color-semantic-control-text-strong);
|
|
51
|
+
font-family: var(--chai-type-family-material);
|
|
52
|
+
font-size: var(--chai-type-size-label-large);
|
|
53
|
+
font-weight: var(--chai-type-weight-material-medium);
|
|
54
|
+
line-height: var(--chai-type-leading-label-large);
|
|
55
|
+
letter-spacing: var(--chai-type-tracking-label-large);
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
transition: background var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.chai-btn:hover:not(:disabled) {
|
|
61
|
+
background: var(--chai-color-semantic-control-surface-hover);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.chai-btn:disabled {
|
|
65
|
+
opacity: 0.5;
|
|
66
|
+
cursor: not-allowed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
* MD3's outlined button (m3.material.io/components/buttons#outlined-button)
|
|
71
|
+
* — for actions that matter (they're real, bordered buttons, not links) but
|
|
72
|
+
* shouldn't compete with the primary action's visual weight. Reads MD3's
|
|
73
|
+
* own roles rather than CHAI's --chai-color-semantic-* ones, so it stays
|
|
74
|
+
* visually consistent with the other MD3-native pieces around it.
|
|
75
|
+
*/
|
|
76
|
+
.chai-btn--outlined {
|
|
77
|
+
background: transparent;
|
|
78
|
+
border-color: var(--chai-color-semantic-control-border);
|
|
79
|
+
color: var(--chai-color-semantic-accent-text);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.chai-btn--outlined:hover:not(:disabled) {
|
|
83
|
+
background: var(--chai-color-semantic-control-surface-hover);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/*
|
|
87
|
+
* MD3 assist chip (m3.material.io/components/chips#assist-chip) — a
|
|
88
|
+
* rounded-rectangle, not the stadium shape every `.chai-btn` uses, for
|
|
89
|
+
* "Select files" specifically (reference: Media Analyze - Default).
|
|
90
|
+
*/
|
|
91
|
+
.chai-chip-button {
|
|
92
|
+
padding: 6px 14px;
|
|
93
|
+
border-radius: var(--chai-radius-sm);
|
|
94
|
+
border: 1px solid var(--chai-color-semantic-control-border);
|
|
95
|
+
background: var(--chai-color-semantic-control-surface-raised);
|
|
96
|
+
color: var(--chai-color-semantic-control-text);
|
|
97
|
+
font-family: var(--chai-type-family-material);
|
|
98
|
+
font-size: var(--chai-type-size-label-large);
|
|
99
|
+
font-weight: var(--chai-type-weight-material-medium);
|
|
100
|
+
line-height: var(--chai-type-leading-label-large);
|
|
101
|
+
letter-spacing: var(--chai-type-tracking-label-large);
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.chai-chip-button:hover:not(:disabled) {
|
|
106
|
+
background: var(--chai-color-semantic-control-surface-hover);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.chai-chip-button:disabled {
|
|
110
|
+
opacity: 0.5;
|
|
111
|
+
cursor: not-allowed;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/*
|
|
115
|
+
* --- Search menu (reference: Media Analyze - Model Menu) — the shared
|
|
116
|
+
* searchable-menu-button pattern behind ModelMenu and every other option
|
|
117
|
+
* menu (see SearchMenu.tsx). A trigger styled like the outlined
|
|
118
|
+
* "Show options" button, opening a self-positioned panel (not `<md-menu>`'s
|
|
119
|
+
* own anchor system) with a real `<md-outlined-text-field>` search field
|
|
120
|
+
* over a hand-rolled option list, same split as SegmentedButton: real MD3
|
|
121
|
+
* web component where one exists and fits, hand-rolled where the composite
|
|
122
|
+
* doesn't map onto a single primitive.
|
|
123
|
+
*/
|
|
124
|
+
.chai-search-menu {
|
|
125
|
+
position: relative;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.chai-search-menu__trigger {
|
|
129
|
+
display: inline-flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
gap: 6px;
|
|
132
|
+
max-width: 220px;
|
|
133
|
+
min-width: 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* "text" trigger variant (reference: Composer's Model/Aspect ratio triggers
|
|
137
|
+
— outline hidden) — label + trailing chevron only, no border/fill at
|
|
138
|
+
rest. Generous padding (not the base rule's tight `max-width` alone) so
|
|
139
|
+
the label's last character has real breathing room before the chevron,
|
|
140
|
+
and a real border-radius so the hover/open state below reads as a
|
|
141
|
+
rounded rectangle, not a hard-cornered box. */
|
|
142
|
+
.chai-search-menu__trigger--text {
|
|
143
|
+
padding: 6px 10px;
|
|
144
|
+
border: none;
|
|
145
|
+
border-radius: var(--chai-radius-sm);
|
|
146
|
+
background: transparent;
|
|
147
|
+
color: var(--chai-color-semantic-control-text-strong);
|
|
148
|
+
font-family: var(--chai-type-family-material);
|
|
149
|
+
font-size: var(--chai-type-size-label-large);
|
|
150
|
+
font-weight: var(--chai-type-weight-material-medium);
|
|
151
|
+
line-height: var(--chai-type-leading-label-large);
|
|
152
|
+
letter-spacing: var(--chai-type-tracking-label-large);
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
transition: background var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.chai-search-menu__trigger--text:hover:not(:disabled) {
|
|
158
|
+
background: var(--chai-color-semantic-control-surface-hover);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.chai-search-menu__trigger--text:disabled {
|
|
162
|
+
opacity: 0.5;
|
|
163
|
+
cursor: not-allowed;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* A selected model's label (e.g. a long OpenRouter model name) is capped and
|
|
167
|
+
ellipsized here rather than left to grow the trigger — otherwise it can
|
|
168
|
+
push wide enough to overlap the card's right-aligned "Show options" /
|
|
169
|
+
"Generate" actions instead of wrapping under `chai-component-card__actions`'
|
|
170
|
+
flex-wrap. */
|
|
171
|
+
.chai-search-menu__trigger-label {
|
|
172
|
+
overflow: hidden;
|
|
173
|
+
text-overflow: ellipsis;
|
|
174
|
+
white-space: nowrap;
|
|
175
|
+
min-width: 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.chai-search-menu__trigger--open {
|
|
179
|
+
background: var(--chai-color-semantic-accent-subtle);
|
|
180
|
+
border-color: var(--chai-color-semantic-accent-subtle);
|
|
181
|
+
color: var(--chai-color-semantic-accent-text);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.chai-search-menu__chevron {
|
|
185
|
+
width: 14px;
|
|
186
|
+
height: 14px;
|
|
187
|
+
transition: transform var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.chai-search-menu__chevron--open {
|
|
191
|
+
transform: rotate(180deg);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.chai-search-menu__panel {
|
|
195
|
+
position: absolute;
|
|
196
|
+
z-index: 10;
|
|
197
|
+
top: calc(100% + 8px);
|
|
198
|
+
left: 0;
|
|
199
|
+
width: 280px;
|
|
200
|
+
padding: var(--chai-space-3);
|
|
201
|
+
border-radius: var(--chai-radius-lg);
|
|
202
|
+
/* White per Figma (reference: every dropdown menu, Model + Generator). */
|
|
203
|
+
background: var(--chai-color-semantic-control-surface-raised);
|
|
204
|
+
box-shadow: var(--chai-shadow-menu);
|
|
205
|
+
display: flex;
|
|
206
|
+
flex-direction: column;
|
|
207
|
+
gap: var(--chai-space-2);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.chai-search-menu__search {
|
|
211
|
+
width: 100%;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.chai-search-menu__search-icon {
|
|
215
|
+
display: flex;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.chai-search-menu__search svg {
|
|
219
|
+
width: 20px;
|
|
220
|
+
height: 20px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.chai-search-menu__clear {
|
|
224
|
+
padding: 0;
|
|
225
|
+
border: none;
|
|
226
|
+
background: transparent;
|
|
227
|
+
color: inherit;
|
|
228
|
+
cursor: pointer;
|
|
229
|
+
display: flex;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.chai-search-menu__list {
|
|
233
|
+
list-style: none;
|
|
234
|
+
margin: 0;
|
|
235
|
+
padding: 0;
|
|
236
|
+
max-height: 240px;
|
|
237
|
+
overflow-y: auto;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.chai-search-menu__option {
|
|
241
|
+
width: 100%;
|
|
242
|
+
display: flex;
|
|
243
|
+
align-items: center;
|
|
244
|
+
justify-content: space-between;
|
|
245
|
+
gap: 8px;
|
|
246
|
+
padding: 10px 12px;
|
|
247
|
+
border: none;
|
|
248
|
+
border-radius: var(--chai-radius-sm);
|
|
249
|
+
background: transparent;
|
|
250
|
+
color: var(--chai-color-semantic-control-text-strong);
|
|
251
|
+
font-family: var(--chai-type-family-material);
|
|
252
|
+
font-size: var(--chai-type-size-body-large);
|
|
253
|
+
text-align: left;
|
|
254
|
+
cursor: pointer;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.chai-search-menu__option:hover:not(:disabled) {
|
|
258
|
+
background: var(--chai-color-semantic-control-surface-hover);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.chai-search-menu__option--selected {
|
|
262
|
+
color: var(--chai-color-semantic-accent-text);
|
|
263
|
+
font-weight: var(--chai-type-weight-material-medium);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* Grayed + inert while a `toggleHeader` is on (reference: the account
|
|
267
|
+
holder's own "Auto-select model" mock) — the row that matches `value`
|
|
268
|
+
keeps its checkmark regardless, so the underlying manual pick stays
|
|
269
|
+
visible even though it can't be changed until the toggle goes off. */
|
|
270
|
+
.chai-search-menu__option:disabled {
|
|
271
|
+
/* MD3's disabled-content spec (on-surface at 38%) — the muted text token
|
|
272
|
+
alone read almost the same as an enabled row. */
|
|
273
|
+
color: color-mix(in srgb, var(--chai-color-semantic-control-text-strong) 38%, transparent);
|
|
274
|
+
cursor: not-allowed;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.chai-search-menu__option:disabled.chai-search-menu__option--selected {
|
|
278
|
+
color: color-mix(in srgb, var(--chai-color-semantic-control-text-strong) 38%, transparent);
|
|
279
|
+
font-weight: var(--chai-type-weight-material-regular);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* Long option labels (a full model name, at this menu's default width)
|
|
283
|
+
ellipsize instead of wrapping or pushing the checkmark out of view. */
|
|
284
|
+
.chai-search-menu__option-label {
|
|
285
|
+
flex: 1;
|
|
286
|
+
min-width: 0;
|
|
287
|
+
overflow: hidden;
|
|
288
|
+
text-overflow: ellipsis;
|
|
289
|
+
white-space: nowrap;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* Multi-select's leading checkbox: display only, so clicks land on the
|
|
293
|
+
row button that owns selection. */
|
|
294
|
+
.chai-search-menu__checkbox {
|
|
295
|
+
flex: none;
|
|
296
|
+
pointer-events: none;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.chai-search-menu__option svg {
|
|
300
|
+
width: 18px;
|
|
301
|
+
height: 18px;
|
|
302
|
+
flex: none;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/*
|
|
306
|
+
* Toggle header (reference: the design note "Auto-select model"
|
|
307
|
+
* mock) — an MD3 outlined-field look built by hand (the notched-outline
|
|
308
|
+
* trick: an absolutely-positioned label with the panel's own background
|
|
309
|
+
* masks the border behind it) rather than a real `<md-outlined-text-field>`,
|
|
310
|
+
* since this needs a switch inside it, not text input.
|
|
311
|
+
*/
|
|
312
|
+
.chai-search-menu__toggle-header {
|
|
313
|
+
position: relative;
|
|
314
|
+
margin-bottom: var(--chai-space-2);
|
|
315
|
+
padding: var(--chai-space-3);
|
|
316
|
+
border: 1px solid var(--chai-color-semantic-control-border);
|
|
317
|
+
border-radius: var(--chai-radius-sm);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.chai-search-menu__toggle-header-label {
|
|
321
|
+
position: absolute;
|
|
322
|
+
top: 0;
|
|
323
|
+
left: 10px;
|
|
324
|
+
transform: translateY(-50%);
|
|
325
|
+
padding: 0 4px;
|
|
326
|
+
background: var(--chai-color-semantic-control-surface-raised);
|
|
327
|
+
color: var(--chai-color-semantic-accent-text);
|
|
328
|
+
font-family: var(--chai-type-family-material);
|
|
329
|
+
font-size: var(--chai-type-size-label-small);
|
|
330
|
+
letter-spacing: var(--chai-type-tracking-label-small);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.chai-search-menu__toggle-header-row {
|
|
334
|
+
display: flex;
|
|
335
|
+
align-items: center;
|
|
336
|
+
justify-content: space-between;
|
|
337
|
+
gap: var(--chai-space-3);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.chai-search-menu__toggle-header-text {
|
|
341
|
+
font-family: var(--chai-type-family-material);
|
|
342
|
+
font-size: var(--chai-type-size-body-large);
|
|
343
|
+
font-weight: var(--chai-type-weight-material-medium);
|
|
344
|
+
color: var(--chai-color-semantic-control-text-strong);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.chai-search-menu__empty {
|
|
348
|
+
padding: 10px 12px;
|
|
349
|
+
color: var(--chai-color-semantic-control-text);
|
|
350
|
+
font-size: var(--chai-type-size-sm);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/* Grouped sections (reference: Media Analyzer's "Select model" menu —
|
|
354
|
+
Image/Video/Audio) — a plain heading + divider, not a new visual
|
|
355
|
+
language: `&__option`'s own rules still apply inside `&__section-list`. */
|
|
356
|
+
.chai-search-menu__section-list {
|
|
357
|
+
list-style: none;
|
|
358
|
+
margin: 0;
|
|
359
|
+
padding: 0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.chai-search-menu__section-label {
|
|
363
|
+
margin: 0;
|
|
364
|
+
padding: 8px 12px 4px;
|
|
365
|
+
color: var(--chai-color-semantic-control-text);
|
|
366
|
+
font-family: var(--chai-type-family-material);
|
|
367
|
+
font-size: var(--chai-type-size-label-small);
|
|
368
|
+
font-weight: var(--chai-type-weight-material-medium);
|
|
369
|
+
letter-spacing: var(--chai-type-tracking-label-small);
|
|
370
|
+
text-transform: uppercase;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.chai-search-menu__divider {
|
|
374
|
+
margin: var(--chai-space-2) 0;
|
|
375
|
+
border: none;
|
|
376
|
+
border-top: 1px solid var(--chai-color-semantic-control-divider);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
@keyframes chai-spin {
|
|
380
|
+
to {
|
|
381
|
+
transform: rotate(360deg);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/*
|
|
386
|
+
* --- Pagination (primitives/Pagination.tsx) ---
|
|
387
|
+
* Progress-style pager (reference: the Figma "Pagination" asset) — a pill
|
|
388
|
+
* that fills from page 1 up to whichever page is current, not independent
|
|
389
|
+
* per-page dots. Matches the mockup's own dot graphic, which is a
|
|
390
|
+
* rounded-rect fill growing under plain circles, not a row of
|
|
391
|
+
* self-contained badges.
|
|
392
|
+
*/
|
|
393
|
+
.chai-pagination {
|
|
394
|
+
position: relative;
|
|
395
|
+
display: inline-flex;
|
|
396
|
+
align-items: center;
|
|
397
|
+
align-self: center;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* The pill is 16px tall, centred on the 24px dot buttons. */
|
|
401
|
+
.chai-pagination__track {
|
|
402
|
+
position: absolute;
|
|
403
|
+
top: 4px;
|
|
404
|
+
bottom: 4px;
|
|
405
|
+
left: 0;
|
|
406
|
+
width: var(--chai-pagination-fill, 0%);
|
|
407
|
+
/* Chai UI's actual primary green — the same token `.chai-btn--primary`
|
|
408
|
+
and the selected Accept segment fill with — not `accent-strong`, which
|
|
409
|
+
is the darker shade reserved for text-on-light-bg contexts. */
|
|
410
|
+
background: var(--chai-color-semantic-accent);
|
|
411
|
+
border-radius: var(--chai-radius-full);
|
|
412
|
+
transition: width var(--chai-motion-duration-base) var(--chai-motion-easing-standard);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/* Each dot is a 24px button (WCAG 2.2 target size, 2.5.8) drawing a 6px
|
|
416
|
+
dot at its centre, so the dots sit 24px apart. */
|
|
417
|
+
.chai-pagination__dot {
|
|
418
|
+
position: relative;
|
|
419
|
+
width: 24px;
|
|
420
|
+
height: 24px;
|
|
421
|
+
padding: 0;
|
|
422
|
+
border: none;
|
|
423
|
+
border-radius: 50%;
|
|
424
|
+
background: transparent;
|
|
425
|
+
cursor: pointer;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.chai-pagination__dot::before {
|
|
429
|
+
content: "";
|
|
430
|
+
position: absolute;
|
|
431
|
+
inset: 9px;
|
|
432
|
+
border-radius: 50%;
|
|
433
|
+
background: var(--chai-color-semantic-control-border-faint);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.chai-pagination__dot--filled::before {
|
|
437
|
+
background: var(--chai-color-semantic-text-on-accent);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/*
|
|
441
|
+
* --- Composer (bar layout) ---
|
|
442
|
+
* Reference: the "Compose Component" design. Its flat cool gray comes from the control-* semantic tokens, and
|
|
443
|
+
* the --chai-composer-* properties below are the per-component hooks for
|
|
444
|
+
* restyling it.
|
|
445
|
+
*/
|
|
446
|
+
.chai-composer {
|
|
447
|
+
--chai-composer-bg: var(--chai-color-semantic-control-surface);
|
|
448
|
+
--chai-composer-border: var(--chai-color-semantic-control-border);
|
|
449
|
+
--chai-composer-placeholder: var(--chai-color-semantic-control-text-muted);
|
|
450
|
+
--chai-composer-text: var(--chai-color-semantic-control-text);
|
|
451
|
+
/* Every hover/open state in this component. */
|
|
452
|
+
--chai-composer-hover: var(--chai-color-semantic-control-surface-hover);
|
|
453
|
+
|
|
454
|
+
display: flex;
|
|
455
|
+
flex-direction: column;
|
|
456
|
+
gap: var(--chai-space-3);
|
|
457
|
+
padding: var(--chai-space-4) var(--chai-space-5);
|
|
458
|
+
/* Bespoke pill radius, doesn't match the --chai-radius-* scale (max lg
|
|
459
|
+
is 16px) — this container is meant to read rounder than any existing
|
|
460
|
+
card. */
|
|
461
|
+
border-radius: 22px;
|
|
462
|
+
border: 1px solid var(--chai-composer-border);
|
|
463
|
+
background: var(--chai-composer-bg);
|
|
464
|
+
font-family: var(--chai-type-family-material);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.chai-composer__thumbnails {
|
|
468
|
+
display: flex;
|
|
469
|
+
flex-wrap: wrap;
|
|
470
|
+
gap: var(--chai-space-3);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.chai-composer__thumb {
|
|
474
|
+
position: relative;
|
|
475
|
+
width: 88px;
|
|
476
|
+
height: 88px;
|
|
477
|
+
flex: none;
|
|
478
|
+
border-radius: var(--chai-radius-sm);
|
|
479
|
+
border: 1px solid var(--chai-color-semantic-control-divider);
|
|
480
|
+
background: var(--chai-composer-bg);
|
|
481
|
+
overflow: hidden;
|
|
482
|
+
display: flex;
|
|
483
|
+
align-items: center;
|
|
484
|
+
justify-content: center;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.chai-composer__thumb-img {
|
|
488
|
+
width: 100%;
|
|
489
|
+
height: 100%;
|
|
490
|
+
object-fit: cover;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.chai-composer__thumb-icon {
|
|
494
|
+
width: 28px;
|
|
495
|
+
height: 28px;
|
|
496
|
+
color: var(--chai-composer-placeholder);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.chai-composer__thumb-icon svg {
|
|
500
|
+
width: 100%;
|
|
501
|
+
height: 100%;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/* Hidden until the thumbnail is hovered/focused, same treatment as the
|
|
505
|
+
use-case chip's own remove button. */
|
|
506
|
+
.chai-composer__thumb-remove {
|
|
507
|
+
position: absolute;
|
|
508
|
+
top: 4px;
|
|
509
|
+
right: 4px;
|
|
510
|
+
width: 20px;
|
|
511
|
+
height: 20px;
|
|
512
|
+
display: flex;
|
|
513
|
+
align-items: center;
|
|
514
|
+
justify-content: center;
|
|
515
|
+
border: none;
|
|
516
|
+
border-radius: var(--chai-radius-full);
|
|
517
|
+
background: var(--chai-color-semantic-overlay);
|
|
518
|
+
color: var(--chai-color-semantic-on-overlay);
|
|
519
|
+
cursor: pointer;
|
|
520
|
+
opacity: 0;
|
|
521
|
+
transition: opacity var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.chai-composer__thumb:hover .chai-composer__thumb-remove,
|
|
525
|
+
.chai-composer__thumb:focus-within .chai-composer__thumb-remove {
|
|
526
|
+
opacity: 1;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.chai-composer__thumb-remove svg {
|
|
530
|
+
width: 14px;
|
|
531
|
+
height: 14px;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.chai-composer__field {
|
|
535
|
+
position: relative;
|
|
536
|
+
display: flex;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/* Shared by the real `<textarea>` and its reveal-overlay `<div>` (below) —
|
|
540
|
+
one source for the font/padding rules that must match exactly for the
|
|
541
|
+
overlay's text to sit pixel-for-pixel on top of the real thing, rather
|
|
542
|
+
than two copies that could quietly drift apart. */
|
|
543
|
+
.chai-composer__type {
|
|
544
|
+
padding: 0;
|
|
545
|
+
/* Clears the optimize button's full 40px hit box (see its own comment)
|
|
546
|
+
plus real breathing room — at the old 36px, typed text ran right up
|
|
547
|
+
against the icon before wrapping. */
|
|
548
|
+
padding-right: 56px;
|
|
549
|
+
overflow-wrap: break-word;
|
|
550
|
+
font-family: var(--chai-type-family-material);
|
|
551
|
+
/* Body Large, not the mock's own literal Title Large spec (22px) — reads
|
|
552
|
+
noticeably heavy/large once actually rendered live, a direct design correction over the original Figma value. */
|
|
553
|
+
font-size: var(--chai-type-size-body-large);
|
|
554
|
+
font-weight: var(--chai-type-weight-material-regular);
|
|
555
|
+
line-height: var(--chai-type-leading-body-large);
|
|
556
|
+
letter-spacing: var(--chai-type-tracking-body-large);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.chai-composer__input {
|
|
560
|
+
flex: 1;
|
|
561
|
+
resize: none;
|
|
562
|
+
overflow: hidden;
|
|
563
|
+
border: none;
|
|
564
|
+
background: transparent;
|
|
565
|
+
outline: none;
|
|
566
|
+
color: var(--chai-composer-text);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.chai-composer__input::placeholder {
|
|
570
|
+
color: var(--chai-composer-placeholder);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.chai-composer__input:disabled {
|
|
574
|
+
opacity: 0.6;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* The optimize/undo reveal (Composer.tsx's own doc comment on the overlay
|
|
578
|
+
for the full mechanics) — the real textarea's text goes fully
|
|
579
|
+
transparent so only the overlay's progressive, gradient-lit copy shows
|
|
580
|
+
underneath it, not both at once. */
|
|
581
|
+
.chai-composer__input--revealing {
|
|
582
|
+
color: transparent;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.chai-composer__reveal {
|
|
586
|
+
position: absolute;
|
|
587
|
+
inset: 0;
|
|
588
|
+
white-space: pre-wrap;
|
|
589
|
+
pointer-events: none;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.chai-composer__reveal-text {
|
|
593
|
+
background: var(--chai-gradient-ai);
|
|
594
|
+
background-size: 300% 100%;
|
|
595
|
+
-webkit-background-clip: text;
|
|
596
|
+
background-clip: text;
|
|
597
|
+
color: transparent;
|
|
598
|
+
animation: chai-composer-reveal-shimmer 1.4s linear infinite;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
@keyframes chai-composer-reveal-shimmer {
|
|
602
|
+
from {
|
|
603
|
+
background-position: 0% 50%;
|
|
604
|
+
}
|
|
605
|
+
to {
|
|
606
|
+
background-position: 300% 50%;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
@media (prefers-reduced-motion: reduce) {
|
|
611
|
+
.chai-composer__reveal-text {
|
|
612
|
+
animation: none;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/* Width matches `<md-fab size="small">`'s own container width (MD3 spec:
|
|
617
|
+
40px) so the icon centers over the submit FAB below it, not over this
|
|
618
|
+
button's own (much narrower) hit target — same right edge, different
|
|
619
|
+
widths, was reading visibly off-center from the FAB's icon. */
|
|
620
|
+
.chai-composer__optimize {
|
|
621
|
+
position: absolute;
|
|
622
|
+
top: 0;
|
|
623
|
+
right: 0;
|
|
624
|
+
display: flex;
|
|
625
|
+
align-items: center;
|
|
626
|
+
justify-content: center;
|
|
627
|
+
width: 40px;
|
|
628
|
+
height: 24px;
|
|
629
|
+
padding: 0;
|
|
630
|
+
border: none;
|
|
631
|
+
background: transparent;
|
|
632
|
+
color: var(--chai-composer-text);
|
|
633
|
+
cursor: pointer;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.chai-composer__optimize svg {
|
|
637
|
+
width: 24px;
|
|
638
|
+
height: 24px;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.chai-composer__optimize:disabled {
|
|
642
|
+
cursor: not-allowed;
|
|
643
|
+
opacity: 0.5;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.chai-composer__optimize:hover:not(:disabled),
|
|
647
|
+
.chai-composer__optimize--active {
|
|
648
|
+
/* The traced Optimize icon's "active" fill: the brand green. */
|
|
649
|
+
color: var(--chai-color-semantic-accent);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.chai-composer__controls {
|
|
653
|
+
display: flex;
|
|
654
|
+
align-items: center;
|
|
655
|
+
justify-content: space-between;
|
|
656
|
+
gap: var(--chai-space-3);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.chai-composer__controls-start {
|
|
660
|
+
display: flex;
|
|
661
|
+
align-items: center;
|
|
662
|
+
gap: var(--chai-space-3);
|
|
663
|
+
flex-wrap: wrap;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/* Neutral trigger text, not `--md-sys-color-primary` green (account
|
|
667
|
+
holder correction: "don't make dropdown select menu buttons green when
|
|
668
|
+
they don't need to be"). */
|
|
669
|
+
.chai-composer .chai-search-menu__trigger {
|
|
670
|
+
color: var(--chai-composer-text);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/* The shared `--trigger--open` rule fills with `--md-sys-color-secondary-
|
|
674
|
+
container` (brand green) — right for Model/Actions menus elsewhere, but
|
|
675
|
+
this component is deliberately neutral throughout (see the container
|
|
676
|
+
comment at the top of this section), so the open state here stays a
|
|
677
|
+
plain rounded-rectangle highlight instead. */
|
|
678
|
+
.chai-composer .chai-search-menu__trigger--open {
|
|
679
|
+
background: var(--chai-composer-hover);
|
|
680
|
+
border-color: transparent;
|
|
681
|
+
color: var(--chai-composer-text);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.chai-composer .chai-search-menu__trigger--text:hover:not(:disabled) {
|
|
685
|
+
background: var(--chai-composer-hover);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.chai-composer__attach {
|
|
689
|
+
position: relative;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.chai-composer__attach-toggle {
|
|
693
|
+
display: flex;
|
|
694
|
+
align-items: center;
|
|
695
|
+
justify-content: center;
|
|
696
|
+
width: 24px;
|
|
697
|
+
height: 24px;
|
|
698
|
+
padding: 0;
|
|
699
|
+
border: none;
|
|
700
|
+
border-radius: var(--chai-radius-full);
|
|
701
|
+
background: transparent;
|
|
702
|
+
color: var(--chai-composer-text);
|
|
703
|
+
cursor: pointer;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.chai-composer__attach-toggle svg {
|
|
707
|
+
width: 24px;
|
|
708
|
+
height: 24px;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.chai-composer__attach-toggle--open {
|
|
712
|
+
background: var(--chai-composer-hover);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.chai-composer__attach-toggle:disabled {
|
|
716
|
+
cursor: not-allowed;
|
|
717
|
+
opacity: 0.5;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.chai-composer__attach-menu {
|
|
721
|
+
position: absolute;
|
|
722
|
+
z-index: 10;
|
|
723
|
+
bottom: calc(100% + 8px);
|
|
724
|
+
left: 0;
|
|
725
|
+
min-width: 180px;
|
|
726
|
+
margin: 0;
|
|
727
|
+
padding: var(--chai-space-2) 0;
|
|
728
|
+
list-style: none;
|
|
729
|
+
border-radius: var(--chai-radius-sm);
|
|
730
|
+
background: var(--chai-color-semantic-control-surface-raised);
|
|
731
|
+
box-shadow: var(--chai-shadow-menu);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.chai-composer__attach-menu-item {
|
|
735
|
+
display: flex;
|
|
736
|
+
align-items: center;
|
|
737
|
+
gap: var(--chai-space-3);
|
|
738
|
+
width: 100%;
|
|
739
|
+
padding: var(--chai-space-2) var(--chai-space-4);
|
|
740
|
+
border: none;
|
|
741
|
+
background: transparent;
|
|
742
|
+
color: var(--chai-color-semantic-control-text-strong);
|
|
743
|
+
font-family: var(--chai-type-family-material);
|
|
744
|
+
font-size: var(--chai-type-size-label-large);
|
|
745
|
+
font-weight: var(--chai-type-weight-material-medium);
|
|
746
|
+
line-height: var(--chai-type-leading-label-large);
|
|
747
|
+
letter-spacing: var(--chai-type-tracking-label-large);
|
|
748
|
+
text-align: left;
|
|
749
|
+
cursor: pointer;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.chai-composer__attach-menu-item:hover {
|
|
753
|
+
background: var(--chai-composer-hover);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.chai-composer__attach-menu-icon {
|
|
757
|
+
width: 20px;
|
|
758
|
+
height: 20px;
|
|
759
|
+
flex: none;
|
|
760
|
+
color: var(--chai-color-semantic-control-text);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.chai-composer__attach-menu-icon svg {
|
|
764
|
+
width: 100%;
|
|
765
|
+
height: 100%;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/* Reference: "Add media" (`MenuListItem1`) is the only item with a divider
|
|
769
|
+
under it in the source (`showDivider: true`), separating the real
|
|
770
|
+
attach action from the three generation-mode actions below it. */
|
|
771
|
+
.chai-composer__attach-menu-divider {
|
|
772
|
+
margin: var(--chai-space-2) 0;
|
|
773
|
+
border: none;
|
|
774
|
+
border-top: 1px solid var(--chai-color-semantic-control-divider);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/* Hand-rolled use-case chip (see `UseCaseChip`'s own comment for why this
|
|
778
|
+
isn't `<md-input-chip>`) — rounded square, not MD3 input chip's stadium
|
|
779
|
+
default, no leading icon (that's the attach-menu's own affordance, not
|
|
780
|
+
the chip's — see `UseCaseChip`). No `gap` here (unlike other rows in
|
|
781
|
+
this component) — the remove button's own width/margin do that job
|
|
782
|
+
instead, collapsed to 0 at rest so the chip is genuinely narrower by
|
|
783
|
+
default, not just hiding an already-reserved slot; hovering (or
|
|
784
|
+
focusing) the chip expands it to reveal the button, rather than fading
|
|
785
|
+
an icon in place. */
|
|
786
|
+
.chai-composer__chip {
|
|
787
|
+
position: relative;
|
|
788
|
+
display: inline-flex;
|
|
789
|
+
align-items: center;
|
|
790
|
+
padding: 6px 10px;
|
|
791
|
+
border-radius: var(--chai-radius-sm);
|
|
792
|
+
border: 1px solid var(--chai-composer-border);
|
|
793
|
+
background: var(--chai-composer-bg);
|
|
794
|
+
color: var(--chai-composer-text);
|
|
795
|
+
font-family: var(--chai-type-family-material);
|
|
796
|
+
font-size: var(--chai-type-size-label-large);
|
|
797
|
+
font-weight: var(--chai-type-weight-material-medium);
|
|
798
|
+
line-height: var(--chai-type-leading-label-large);
|
|
799
|
+
letter-spacing: var(--chai-type-tracking-label-large);
|
|
800
|
+
white-space: nowrap;
|
|
801
|
+
transition: background var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.chai-composer__chip:hover {
|
|
805
|
+
background: var(--chai-composer-hover);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.chai-composer__chip-remove {
|
|
809
|
+
display: flex;
|
|
810
|
+
align-items: center;
|
|
811
|
+
justify-content: center;
|
|
812
|
+
width: 0;
|
|
813
|
+
height: 16px;
|
|
814
|
+
margin-left: 0;
|
|
815
|
+
padding: 0;
|
|
816
|
+
border: none;
|
|
817
|
+
border-radius: var(--chai-radius-full);
|
|
818
|
+
background: transparent;
|
|
819
|
+
color: var(--chai-composer-text);
|
|
820
|
+
cursor: pointer;
|
|
821
|
+
opacity: 0;
|
|
822
|
+
overflow: hidden;
|
|
823
|
+
flex: none;
|
|
824
|
+
transition:
|
|
825
|
+
width var(--chai-motion-duration-fast) var(--chai-motion-easing-standard),
|
|
826
|
+
margin-left var(--chai-motion-duration-fast) var(--chai-motion-easing-standard),
|
|
827
|
+
opacity var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
.chai-composer__chip-remove svg {
|
|
831
|
+
width: 16px;
|
|
832
|
+
height: 16px;
|
|
833
|
+
flex: none;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
.chai-composer__chip:hover .chai-composer__chip-remove,
|
|
837
|
+
.chai-composer__chip:focus-within .chai-composer__chip-remove {
|
|
838
|
+
width: 16px;
|
|
839
|
+
margin-left: 6px;
|
|
840
|
+
opacity: 1;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.chai-composer__chip-remove:disabled {
|
|
844
|
+
cursor: not-allowed;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.chai-composer__submit {
|
|
848
|
+
flex: none;
|
|
849
|
+
/* Flat always, both states — the mock has no FAB shadow at all. */
|
|
850
|
+
--md-fab-container-elevation: 0;
|
|
851
|
+
--md-fab-hover-container-elevation: 0;
|
|
852
|
+
--md-fab-pressed-container-elevation: 0;
|
|
853
|
+
--md-fab-focus-container-elevation: 0;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/* Unfilled rest state — icon only, no visible container, until there's a
|
|
857
|
+
valid prompt. Not M3's "surface" variant: a green-seeded theme's own
|
|
858
|
+
surface containers still pick up a faint tint from dynamic color, so
|
|
859
|
+
overriding the secondary-variant custom properties directly was the only
|
|
860
|
+
way to get a genuinely flat rest state. MD3's own hover state layer
|
|
861
|
+
(unaffected by these overrides) still gives the "slight color change on
|
|
862
|
+
hover" the mock wants, with no extra rule needed. Once `[data-ready]` is
|
|
863
|
+
set, neither override applies and `<md-fab>`'s real `variant="secondary"`
|
|
864
|
+
colors (the brand-green bridge) show through on their own. */
|
|
865
|
+
.chai-composer__submit:not([data-ready]) {
|
|
866
|
+
--md-fab-secondary-container-color: transparent;
|
|
867
|
+
--md-fab-secondary-icon-color: var(--chai-composer-text);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/* Retry state — real `--chai-color-semantic-danger*` tokens, not this
|
|
871
|
+
component's own literal-hex palette: unlike the neutral chrome, an
|
|
872
|
+
error should look the same "error" everywhere in the app, not just in
|
|
873
|
+
Composer. `[data-error]` can co-occur with `[data-ready]` (retry isn't
|
|
874
|
+
gated on `hasContent`, see `submitError`'s doc comment), so this rule
|
|
875
|
+
is scoped only to `[data-error]` and always wins when it's set. */
|
|
876
|
+
.chai-composer__submit[data-error] {
|
|
877
|
+
--md-fab-secondary-container-color: var(--chai-color-semantic-danger-subtle);
|
|
878
|
+
--md-fab-secondary-icon-color: var(--chai-color-semantic-danger);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/* Stop state — real `--chai-color-semantic-warning*` tokens (a light
|
|
882
|
+
orange), same reasoning as `[data-error]` above: reuses an existing
|
|
883
|
+
system color rather than inventing a one-off literal hex for this one
|
|
884
|
+
moment. Declared before `[data-error]` so error still wins if both were
|
|
885
|
+
ever true (they aren't, in practice — a request is either in flight or
|
|
886
|
+
already failed, never both), matching that rule's own documented
|
|
887
|
+
precedence. */
|
|
888
|
+
.chai-composer__submit[data-stop] {
|
|
889
|
+
--md-fab-secondary-container-color: var(--chai-color-semantic-warning-subtle);
|
|
890
|
+
--md-fab-secondary-icon-color: var(--chai-color-semantic-warning);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
.chai-composer__error {
|
|
894
|
+
display: flex;
|
|
895
|
+
align-items: center;
|
|
896
|
+
gap: var(--chai-space-2);
|
|
897
|
+
margin: 0;
|
|
898
|
+
padding: 2px 2px 0;
|
|
899
|
+
color: var(--chai-color-semantic-danger);
|
|
900
|
+
font-family: var(--chai-type-family-material);
|
|
901
|
+
font-size: var(--chai-type-size-label-large);
|
|
902
|
+
font-weight: var(--chai-type-weight-material-medium);
|
|
903
|
+
line-height: var(--chai-type-leading-label-large);
|
|
904
|
+
letter-spacing: var(--chai-type-tracking-label-large);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
.chai-composer__error svg {
|
|
908
|
+
width: 16px;
|
|
909
|
+
height: 16px;
|
|
910
|
+
flex: none;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/*
|
|
914
|
+
* --- Media Analyzer (reference: the "Composer from Media Analysis" design)
|
|
915
|
+
* — same treatment as `.chai-composer` above, with the
|
|
916
|
+
* exact same container/border/radius values, since the mock reuses them.
|
|
917
|
+
*/
|
|
918
|
+
.chai-media-analyzer {
|
|
919
|
+
--chai-media-analyzer-bg: var(--chai-color-semantic-control-surface);
|
|
920
|
+
--chai-media-analyzer-border: var(--chai-color-semantic-control-border);
|
|
921
|
+
--chai-media-analyzer-text: var(--chai-color-semantic-control-text);
|
|
922
|
+
--chai-media-analyzer-hover: var(--chai-color-semantic-control-surface-hover);
|
|
923
|
+
|
|
924
|
+
display: flex;
|
|
925
|
+
flex-direction: column;
|
|
926
|
+
gap: var(--chai-space-3);
|
|
927
|
+
padding: var(--chai-space-4) var(--chai-space-5);
|
|
928
|
+
border-radius: 22px;
|
|
929
|
+
border: 1px solid var(--chai-media-analyzer-border);
|
|
930
|
+
background: var(--chai-media-analyzer-bg);
|
|
931
|
+
font-family: var(--chai-type-family-material);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.chai-media-analyzer__dropzone {
|
|
935
|
+
display: flex;
|
|
936
|
+
flex-direction: column;
|
|
937
|
+
align-items: center;
|
|
938
|
+
justify-content: center;
|
|
939
|
+
gap: var(--chai-space-2);
|
|
940
|
+
min-height: 140px;
|
|
941
|
+
padding: var(--chai-space-4);
|
|
942
|
+
border-radius: var(--chai-radius-md);
|
|
943
|
+
border: 1.5px dashed var(--chai-color-semantic-control-text-strong);
|
|
944
|
+
/* Brand-green wash, same token `.chai-drop-zone` reads for the same
|
|
945
|
+
reason (see that rule's own comment) — this dashed box is this
|
|
946
|
+
component's equivalent surface. */
|
|
947
|
+
background: var(--chai-color-semantic-accent-subtle);
|
|
948
|
+
text-align: center;
|
|
949
|
+
cursor: pointer;
|
|
950
|
+
transition: background var(--chai-motion-duration-fast) var(--chai-motion-easing-standard),
|
|
951
|
+
border-color var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/* At cap for the attached kind (reference: "Analyze Media Composer -
|
|
955
|
+
Disabled - Maximum Media Attached") — flat gray, not a dimmed green
|
|
956
|
+
(mixing "disabled" with the brand wash read as broken, not intentional).
|
|
957
|
+
Still a real drop target for a *different* kind, which always replaces
|
|
958
|
+
what's here — see MediaAnalyzer.tsx's `nextMediaAnalyzerAttachments` for
|
|
959
|
+
why this never fully disables the zone itself, only the same-kind add. */
|
|
960
|
+
.chai-media-analyzer__dropzone--disabled {
|
|
961
|
+
background: var(--chai-color-semantic-control-surface-hover);
|
|
962
|
+
cursor: default;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/* A file is being dragged over the zone but not dropped yet — there was no
|
|
966
|
+
feedback at all for this before (the box looked identical whether or not
|
|
967
|
+
a drag was even over it). Solid border + a visibly darker green than the
|
|
968
|
+
dashed zone's own resting wash, so "this is a live drop target right
|
|
969
|
+
now" reads clearly regardless of whether the zone is also at cap (a
|
|
970
|
+
drag-over can still be a valid cross-kind replace even then, so this
|
|
971
|
+
wins over `--disabled`'s gray — see the design note on
|
|
972
|
+
the drop zone always accepting a different kind). */
|
|
973
|
+
.chai-media-analyzer__dropzone--drag-active,
|
|
974
|
+
.chai-media-analyzer__dropzone--disabled.chai-media-analyzer__dropzone--drag-active {
|
|
975
|
+
border-style: solid;
|
|
976
|
+
border-color: var(--chai-color-semantic-accent);
|
|
977
|
+
background: var(--chai-color-semantic-accent-soft);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
.chai-media-analyzer__dropzone-icon {
|
|
981
|
+
width: 32px;
|
|
982
|
+
height: 32px;
|
|
983
|
+
color: var(--chai-color-semantic-control-text);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.chai-media-analyzer__dropzone-icon svg {
|
|
987
|
+
width: 100%;
|
|
988
|
+
height: 100%;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
.chai-media-analyzer__dropzone-hint {
|
|
992
|
+
margin: 0;
|
|
993
|
+
font-size: var(--chai-type-size-body-medium);
|
|
994
|
+
font-weight: var(--chai-type-weight-material-regular);
|
|
995
|
+
color: var(--chai-media-analyzer-text);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
.chai-media-analyzer__thumbnails {
|
|
999
|
+
display: flex;
|
|
1000
|
+
flex-wrap: wrap;
|
|
1001
|
+
gap: var(--chai-space-2);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/* Same shape/behavior as `.chai-composer__thumb`, sized down (56px, not
|
|
1005
|
+
88px) so all 5 of an image analysis's maximum fit in one row under the
|
|
1006
|
+
drag box at typical card widths (reference: the design
|
|
1007
|
+
spec — "slightly smaller to fit a total of five image thumbs"). */
|
|
1008
|
+
.chai-media-analyzer__thumb {
|
|
1009
|
+
position: relative;
|
|
1010
|
+
width: 56px;
|
|
1011
|
+
height: 56px;
|
|
1012
|
+
flex: none;
|
|
1013
|
+
border-radius: var(--chai-radius-sm);
|
|
1014
|
+
border: 1px solid var(--chai-color-semantic-control-divider);
|
|
1015
|
+
background: var(--chai-media-analyzer-bg);
|
|
1016
|
+
overflow: hidden;
|
|
1017
|
+
display: flex;
|
|
1018
|
+
align-items: center;
|
|
1019
|
+
justify-content: center;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.chai-media-analyzer__thumb-img {
|
|
1023
|
+
width: 100%;
|
|
1024
|
+
height: 100%;
|
|
1025
|
+
object-fit: cover;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.chai-media-analyzer__thumb-icon {
|
|
1029
|
+
width: 20px;
|
|
1030
|
+
height: 20px;
|
|
1031
|
+
color: var(--chai-media-analyzer-text);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
.chai-media-analyzer__thumb-icon svg {
|
|
1035
|
+
width: 100%;
|
|
1036
|
+
height: 100%;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
.chai-media-analyzer__thumb-remove {
|
|
1040
|
+
position: absolute;
|
|
1041
|
+
top: 2px;
|
|
1042
|
+
right: 2px;
|
|
1043
|
+
width: 20px;
|
|
1044
|
+
height: 20px;
|
|
1045
|
+
display: flex;
|
|
1046
|
+
align-items: center;
|
|
1047
|
+
justify-content: center;
|
|
1048
|
+
border: none;
|
|
1049
|
+
border-radius: var(--chai-radius-full);
|
|
1050
|
+
/* Darker + fully opaque (was translucent black) — against a busy photo
|
|
1051
|
+
thumbnail a semi-transparent badge let the image show through and
|
|
1052
|
+
washed out the X on top of it. A solid, near-black circle is what
|
|
1053
|
+
actually gives the white X reliable contrast in every case. */
|
|
1054
|
+
background: var(--chai-color-semantic-overlay);
|
|
1055
|
+
color: var(--chai-color-semantic-on-overlay);
|
|
1056
|
+
cursor: pointer;
|
|
1057
|
+
opacity: 0;
|
|
1058
|
+
transition: opacity var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.chai-media-analyzer__thumb:hover .chai-media-analyzer__thumb-remove,
|
|
1062
|
+
.chai-media-analyzer__thumb:focus-within .chai-media-analyzer__thumb-remove {
|
|
1063
|
+
opacity: 1;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/* Still tiny in absolute terms (a corner badge on a 56px thumbnail can only
|
|
1067
|
+
be so big), but the X itself should read as a solid mark, not a hairline
|
|
1068
|
+
— reference: the design note "more legible" example, a chunky
|
|
1069
|
+
X that fills most of the circle rather than a thin one centered in a lot
|
|
1070
|
+
of empty padding. Both the icon's own size *and* its `strokeWidth`
|
|
1071
|
+
(RemoveIcon, MediaAnalyzer.tsx) went up together — bumping only one or
|
|
1072
|
+
the other still read as too delicate at this size. */
|
|
1073
|
+
.chai-media-analyzer__thumb-remove svg {
|
|
1074
|
+
width: 14px;
|
|
1075
|
+
height: 14px;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
.chai-media-analyzer__controls {
|
|
1079
|
+
display: flex;
|
|
1080
|
+
align-items: center;
|
|
1081
|
+
justify-content: space-between;
|
|
1082
|
+
gap: var(--chai-space-3);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.chai-media-analyzer__controls-start {
|
|
1086
|
+
display: flex;
|
|
1087
|
+
align-items: center;
|
|
1088
|
+
gap: var(--chai-space-2);
|
|
1089
|
+
flex-wrap: wrap;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/* Neutral trigger text/open-state, not the shared rule's brand-green fill —
|
|
1093
|
+
same correction `.chai-composer` already applies to its own SearchMenu
|
|
1094
|
+
triggers, for the same reason (see that rule's own comment). */
|
|
1095
|
+
.chai-media-analyzer .chai-search-menu__trigger {
|
|
1096
|
+
color: var(--chai-media-analyzer-text);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
.chai-media-analyzer .chai-search-menu__trigger--open {
|
|
1100
|
+
background: var(--chai-media-analyzer-hover);
|
|
1101
|
+
border-color: transparent;
|
|
1102
|
+
color: var(--chai-media-analyzer-text);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
.chai-media-analyzer .chai-search-menu__trigger--text:hover:not(:disabled) {
|
|
1106
|
+
background: var(--chai-media-analyzer-hover);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
.chai-media-analyzer__submit {
|
|
1110
|
+
flex: none;
|
|
1111
|
+
--md-fab-container-elevation: 0;
|
|
1112
|
+
--md-fab-hover-container-elevation: 0;
|
|
1113
|
+
--md-fab-pressed-container-elevation: 0;
|
|
1114
|
+
--md-fab-focus-container-elevation: 0;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/* Unfilled until there's at least one attachment to submit — same reasoning
|
|
1118
|
+
as `.chai-composer__submit:not([data-ready])`. */
|
|
1119
|
+
.chai-media-analyzer__submit:not([data-ready]) {
|
|
1120
|
+
--md-fab-secondary-container-color: transparent;
|
|
1121
|
+
--md-fab-secondary-icon-color: var(--chai-media-analyzer-text);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
.chai-media-analyzer__submit[data-error] {
|
|
1125
|
+
--md-fab-secondary-container-color: var(--chai-color-semantic-danger-subtle);
|
|
1126
|
+
--md-fab-secondary-icon-color: var(--chai-color-semantic-danger);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
.chai-media-analyzer__error {
|
|
1130
|
+
margin: 0;
|
|
1131
|
+
padding: 2px 2px 0;
|
|
1132
|
+
color: var(--chai-color-semantic-danger);
|
|
1133
|
+
font-size: var(--chai-type-size-label-large);
|
|
1134
|
+
font-weight: var(--chai-type-weight-material-medium);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/*
|
|
1138
|
+
* --- Result Card (reference: Figma "08 Result from Composer Bar", node
|
|
1139
|
+
* 194:1695) — Composer bar's compliment. Literal hex again (not
|
|
1140
|
+
* --chai- or --md-sys-color- tokens) for the card chrome itself, same
|
|
1141
|
+
* "green-seeded theme tints everything neutral" reason `.chai-composer`'s
|
|
1142
|
+
* own container comment documents; chips/menus underneath still read real
|
|
1143
|
+
* MD3 tokens since those aren't large flat fills.
|
|
1144
|
+
*/
|
|
1145
|
+
|
|
1146
|
+
/* A fixed default width (375px, matching the Figma mock), not sized off
|
|
1147
|
+
the source media's own pixel dimensions — design
|
|
1148
|
+
correction: letting the card's *width* track a real generated image's
|
|
1149
|
+
actual resolution would make the card's footprint unpredictable
|
|
1150
|
+
(anywhere from a couple hundred px to several thousand) and break the
|
|
1151
|
+
layout of whatever app it's embedded in. The *height* still isn't
|
|
1152
|
+
fixed: the media inside renders at `width: 100%; height: auto` (its own
|
|
1153
|
+
comment below), so at this one fixed width, height naturally follows
|
|
1154
|
+
whatever the real aspect ratio is — a 4:3 image lands close to the
|
|
1155
|
+
Figma mock's own ~335px total card height, 1:1 taller, 16:9 shorter,
|
|
1156
|
+
with no special-casing needed per ratio. This is the actual "thumbnail"
|
|
1157
|
+
behavior (scaled down, never cropped) — the "Expand" button is what
|
|
1158
|
+
shows the real, full-size media when someone needs it. A `width` prop
|
|
1159
|
+
overrides the 375px default for a builder who wants a different fixed
|
|
1160
|
+
footprint. */
|
|
1161
|
+
.chai-result-card {
|
|
1162
|
+
width: var(--chai-result-card-width, 375px);
|
|
1163
|
+
max-width: 100%;
|
|
1164
|
+
perspective: 1800px;
|
|
1165
|
+
font-family: var(--chai-type-family-material);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/* The *front* (the media) is what establishes the card's height — normal
|
|
1169
|
+
block flow, no special positioning, so `.inner`'s own auto height is
|
|
1170
|
+
just "whatever `.front`'s natural content needs." The *back* (info
|
|
1171
|
+
side) is `position: absolute; inset: 0` and simply conforms to
|
|
1172
|
+
whatever height that resolves to, scrolling its own prompt box
|
|
1173
|
+
internally if its content needs more room than the media provided.
|
|
1174
|
+
Deliberately not symmetric ("taller of the two, either stretches to
|
|
1175
|
+
match") — a design correction: a media-driven card
|
|
1176
|
+
flipping to a shorter or differently-proportioned info side read as
|
|
1177
|
+
visually inconsistent, and media should stay the one source of truth
|
|
1178
|
+
for the card's height in both states. `.back` being absolute doesn't
|
|
1179
|
+
reintroduce the old circular-sizing problem (`position: absolute` on
|
|
1180
|
+
*both* faces once tried and reverted) because only one side of that
|
|
1181
|
+
circle exists now — `.front` is never absolute, so it always has a
|
|
1182
|
+
real, independently-computed height for `.back` to match. */
|
|
1183
|
+
.chai-result-card__inner {
|
|
1184
|
+
position: relative;
|
|
1185
|
+
width: 100%;
|
|
1186
|
+
transition: transform var(--chai-motion-duration-slow) var(--chai-motion-easing-standard);
|
|
1187
|
+
transform-style: preserve-3d;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
.chai-result-card--flipped .chai-result-card__inner {
|
|
1191
|
+
transform: rotateY(180deg);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
.chai-result-card__face {
|
|
1195
|
+
width: 100%;
|
|
1196
|
+
display: flex;
|
|
1197
|
+
flex-direction: column;
|
|
1198
|
+
backface-visibility: hidden;
|
|
1199
|
+
border-radius: 29px;
|
|
1200
|
+
border: 1px solid var(--chai-color-semantic-control-border-soft);
|
|
1201
|
+
overflow: hidden;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
.chai-result-card__front {
|
|
1205
|
+
position: relative;
|
|
1206
|
+
background: var(--chai-color-semantic-control-surface);
|
|
1207
|
+
/* `.back` is later in DOM order and stacks above `.front` regardless of
|
|
1208
|
+
which face is currently rotated toward the viewer (sibling stacking
|
|
1209
|
+
order doesn't reorder itself just because `backface-visibility` hides
|
|
1210
|
+
paint) — so without this, `.back`'s own hidden-but-still-present box
|
|
1211
|
+
can swallow clicks meant for `.front` in some engines. Pointer-events
|
|
1212
|
+
are switched explicitly per face instead of trusting
|
|
1213
|
+
`backface-visibility: hidden` to also gate hit-testing. */
|
|
1214
|
+
pointer-events: auto;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
.chai-result-card--flipped .chai-result-card__front {
|
|
1218
|
+
pointer-events: none;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
.chai-result-card__back {
|
|
1222
|
+
position: absolute;
|
|
1223
|
+
inset: 0;
|
|
1224
|
+
background: var(--chai-color-semantic-control-surface-raised);
|
|
1225
|
+
transform: rotateY(180deg);
|
|
1226
|
+
padding: var(--chai-space-5) var(--chai-space-4) var(--chai-space-4);
|
|
1227
|
+
gap: var(--chai-space-3);
|
|
1228
|
+
pointer-events: none;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
.chai-result-card--flipped .chai-result-card__back {
|
|
1232
|
+
pointer-events: auto;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/* --- Media (image/video/audio/text, by status) ---
|
|
1236
|
+
* `flex: 1` here is only a graceful-overflow behavior, not a fixed size:
|
|
1237
|
+
* with no explicit height anywhere in this chain, the media area's own
|
|
1238
|
+
* natural size already comes from its image/video content below (`width:
|
|
1239
|
+
* 100%; height: auto`, no `object-fit` cropping/letterboxing needed) — it
|
|
1240
|
+
* only actually stretches on the rarer occasion the *other* face (the
|
|
1241
|
+
* flipped info side) is naturally taller, since both faces share one grid
|
|
1242
|
+
* track sized to the taller one (`.chai-result-card__inner`'s own
|
|
1243
|
+
* comment). When that happens the extra height lands here as breathing
|
|
1244
|
+
* room around a still-centered, still-unstretched image, not a fixed gray
|
|
1245
|
+
* box the image sits inside by default.
|
|
1246
|
+
*/
|
|
1247
|
+
.chai-result-card__media {
|
|
1248
|
+
position: relative;
|
|
1249
|
+
flex: 1;
|
|
1250
|
+
display: flex;
|
|
1251
|
+
align-items: center;
|
|
1252
|
+
justify-content: center;
|
|
1253
|
+
background: var(--chai-color-semantic-control-surface);
|
|
1254
|
+
overflow: hidden;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/* Queued/running/error/audio/text have no image of their own to derive a
|
|
1258
|
+
natural size from, so they get one fixed reasonable height instead of
|
|
1259
|
+
collapsing to near-zero. */
|
|
1260
|
+
.chai-result-card__media--fixed-height,
|
|
1261
|
+
.chai-result-card__media--pending,
|
|
1262
|
+
.chai-result-card__media--error,
|
|
1263
|
+
.chai-result-card__media--cancelled {
|
|
1264
|
+
min-height: 220px;
|
|
1265
|
+
padding: var(--chai-space-5);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/* A sibling of `.chai-result-card` itself, not a descendant — see the
|
|
1269
|
+
overlay's own comment in ResultCard.tsx (`perspective` on the card, set
|
|
1270
|
+
for the flip animation, would otherwise trap `position: fixed` inside
|
|
1271
|
+
the card's own box instead of the real viewport). */
|
|
1272
|
+
.chai-result-card__expand-overlay {
|
|
1273
|
+
position: fixed;
|
|
1274
|
+
inset: 0;
|
|
1275
|
+
z-index: 50;
|
|
1276
|
+
display: flex;
|
|
1277
|
+
align-items: center;
|
|
1278
|
+
justify-content: center;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
.chai-result-card__expand-scrim {
|
|
1282
|
+
position: absolute;
|
|
1283
|
+
inset: 0;
|
|
1284
|
+
z-index: 0;
|
|
1285
|
+
background: var(--chai-color-semantic-scrim);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.chai-result-card__expand-frame {
|
|
1289
|
+
position: relative;
|
|
1290
|
+
z-index: 1;
|
|
1291
|
+
width: min(90vw, 960px);
|
|
1292
|
+
height: min(80vh, 540px);
|
|
1293
|
+
border-radius: var(--chai-radius-lg);
|
|
1294
|
+
overflow: hidden;
|
|
1295
|
+
background: var(--chai-color-semantic-media-backdrop);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
.chai-result-card__expand-frame .chai-result-card__video,
|
|
1299
|
+
.chai-result-card__expand-frame .chai-result-card__image {
|
|
1300
|
+
/* The frame itself is a fixed box (unlike the inline card, which sizes
|
|
1301
|
+
to the media) — `object-fit: contain` here is what keeps a portrait
|
|
1302
|
+
or unusually-shaped image/video from overflowing it, height: 100%
|
|
1303
|
+
instead of auto since auto would size off the media's own aspect
|
|
1304
|
+
ratio again and could exceed the frame's fixed height. */
|
|
1305
|
+
height: 100%;
|
|
1306
|
+
object-fit: contain;
|
|
1307
|
+
aspect-ratio: auto;
|
|
1308
|
+
border-radius: 0;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/* No `object-fit`/`max-width`/`max-height` — the whole point of sizing the
|
|
1312
|
+
card itself to the media (see `.chai-result-card`'s own comment) is that
|
|
1313
|
+
the image never needs cropping or letterboxing to begin with: it just
|
|
1314
|
+
renders at its own natural aspect ratio, full width, corners rounded by
|
|
1315
|
+
`.chai-result-card__face`'s own `overflow: hidden` around it. */
|
|
1316
|
+
.chai-result-card__image {
|
|
1317
|
+
position: relative;
|
|
1318
|
+
z-index: 1;
|
|
1319
|
+
display: block;
|
|
1320
|
+
width: 100%;
|
|
1321
|
+
height: auto;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
.chai-result-card__video {
|
|
1325
|
+
position: relative;
|
|
1326
|
+
z-index: 1;
|
|
1327
|
+
display: block;
|
|
1328
|
+
width: 100%;
|
|
1329
|
+
height: auto;
|
|
1330
|
+
/* A reasonable guess before real `loadedmetadata` dimensions are known
|
|
1331
|
+
(avoids a 0-height flash) — once they load, intrinsic sizing from
|
|
1332
|
+
`width: 100%; height: auto` takes over and this stops applying. */
|
|
1333
|
+
aspect-ratio: 16 / 9;
|
|
1334
|
+
background: var(--chai-color-semantic-media-backdrop);
|
|
1335
|
+
cursor: pointer;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
.chai-result-card__audio {
|
|
1339
|
+
position: relative;
|
|
1340
|
+
z-index: 1;
|
|
1341
|
+
width: 90%;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/* Model output is often long and structured, so line breaks are kept and
|
|
1345
|
+
the text scrolls past a max height instead of growing the card. Builders
|
|
1346
|
+
can change the height with --chai-result-card-text-max-height. */
|
|
1347
|
+
.chai-result-card__text-output {
|
|
1348
|
+
position: relative;
|
|
1349
|
+
z-index: 1;
|
|
1350
|
+
/* Starts below the download/share buttons, which sit at the top right:
|
|
1351
|
+
text scrolling underneath them was hard to read. */
|
|
1352
|
+
margin: var(--chai-space-4) 0 0;
|
|
1353
|
+
padding: var(--chai-space-4);
|
|
1354
|
+
min-height: 160px;
|
|
1355
|
+
max-height: var(--chai-result-card-text-max-height, 400px);
|
|
1356
|
+
overflow-y: auto;
|
|
1357
|
+
white-space: pre-wrap;
|
|
1358
|
+
overflow-wrap: anywhere;
|
|
1359
|
+
font-size: var(--chai-type-size-body-medium);
|
|
1360
|
+
line-height: var(--chai-type-leading-body-medium);
|
|
1361
|
+
color: var(--chai-color-semantic-control-text-strong);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/* An always-visible scrollbar, so it's clear more text exists. macOS hides
|
|
1365
|
+
overlay scrollbars until you scroll, which made long answers look cut
|
|
1366
|
+
off. WebKit/Blink use the pseudo-elements; Firefox the standard props. */
|
|
1367
|
+
.chai-result-card__text-output::-webkit-scrollbar {
|
|
1368
|
+
width: 8px;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
.chai-result-card__text-output::-webkit-scrollbar-track {
|
|
1372
|
+
background: transparent;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
.chai-result-card__text-output::-webkit-scrollbar-thumb {
|
|
1376
|
+
border-radius: var(--chai-radius-full);
|
|
1377
|
+
background: var(--chai-color-semantic-control-border-soft);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
@supports not selector(::-webkit-scrollbar) {
|
|
1381
|
+
.chai-result-card__text-output {
|
|
1382
|
+
scrollbar-width: thin;
|
|
1383
|
+
scrollbar-color: var(--chai-color-semantic-control-border-soft) transparent;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/* Streaming caret — a block cursor trailing the partial text while a text
|
|
1388
|
+
result is still arriving. */
|
|
1389
|
+
.chai-result-card__text-output--streaming::after {
|
|
1390
|
+
content: "";
|
|
1391
|
+
display: inline-block;
|
|
1392
|
+
width: 0.5em;
|
|
1393
|
+
height: 1em;
|
|
1394
|
+
margin-left: 2px;
|
|
1395
|
+
vertical-align: text-bottom;
|
|
1396
|
+
background: var(--chai-color-semantic-control-text);
|
|
1397
|
+
animation: chai-caret-blink calc(var(--chai-motion-duration-steep) * 1.2) steps(1) infinite;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
@keyframes chai-caret-blink {
|
|
1401
|
+
50% {
|
|
1402
|
+
opacity: 0;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1407
|
+
.chai-result-card__text-output--streaming::after {
|
|
1408
|
+
animation: none;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
.chai-result-card__spinner {
|
|
1413
|
+
width: 32px;
|
|
1414
|
+
height: 32px;
|
|
1415
|
+
border-radius: 50%;
|
|
1416
|
+
border: 3px solid var(--chai-color-semantic-control-border-faint);
|
|
1417
|
+
border-top-color: var(--chai-color-semantic-accent);
|
|
1418
|
+
animation: chai-spin var(--chai-motion-duration-steep) linear infinite;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
.chai-result-card__cancelled-text {
|
|
1422
|
+
margin: 0;
|
|
1423
|
+
font-size: var(--chai-type-size-body-medium);
|
|
1424
|
+
color: var(--chai-color-semantic-control-text-muted);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
.chai-result-card__error-text {
|
|
1428
|
+
margin: 0;
|
|
1429
|
+
padding: 0 var(--chai-space-4);
|
|
1430
|
+
text-align: center;
|
|
1431
|
+
font-size: var(--chai-type-size-body-medium);
|
|
1432
|
+
color: var(--chai-color-semantic-danger);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
.chai-result-card__media-actions {
|
|
1436
|
+
position: absolute;
|
|
1437
|
+
z-index: 1;
|
|
1438
|
+
top: 10px;
|
|
1439
|
+
right: 10px;
|
|
1440
|
+
display: flex;
|
|
1441
|
+
gap: 2px;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/* Play/scrubber sit relative to their nearest positioned ancestor —
|
|
1445
|
+
`.chai-result-card__media` inline, `.chai-result-card__expand-frame`
|
|
1446
|
+
when expanded — not the `<video>` element itself. Both are exactly the
|
|
1447
|
+
video's own rendered box (`object-fit: contain` fills either one), so
|
|
1448
|
+
the same rule works for both without measuring the video's actual
|
|
1449
|
+
painted rect. On an aspect ratio very different from that box, the
|
|
1450
|
+
scrubber can sit in the letterboxed margin rather than snug against the
|
|
1451
|
+
visible frame — accepted trade-off against the complexity of measuring
|
|
1452
|
+
real video bounds for what's a slim progress bar, not a hit-critical
|
|
1453
|
+
control. */
|
|
1454
|
+
.chai-result-card__play {
|
|
1455
|
+
position: absolute;
|
|
1456
|
+
z-index: 1;
|
|
1457
|
+
top: 50%;
|
|
1458
|
+
left: 50%;
|
|
1459
|
+
transform: translate(-50%, -50%);
|
|
1460
|
+
width: 56px;
|
|
1461
|
+
height: 56px;
|
|
1462
|
+
border-radius: 50%;
|
|
1463
|
+
border: none;
|
|
1464
|
+
background: var(--chai-color-semantic-overlay);
|
|
1465
|
+
color: var(--chai-color-semantic-accent);
|
|
1466
|
+
display: flex;
|
|
1467
|
+
align-items: center;
|
|
1468
|
+
justify-content: center;
|
|
1469
|
+
cursor: pointer;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
.chai-result-card__play svg {
|
|
1473
|
+
width: 28px;
|
|
1474
|
+
height: 28px;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/* The scrubber is a 24px-tall slider (WCAG 2.2 target size) drawing the
|
|
1478
|
+
same thin track, centred where the track has always sat. */
|
|
1479
|
+
.chai-result-card__scrubber {
|
|
1480
|
+
--chai-scrubber-track: 6px;
|
|
1481
|
+
position: absolute;
|
|
1482
|
+
z-index: 1;
|
|
1483
|
+
left: 12px;
|
|
1484
|
+
right: 12px;
|
|
1485
|
+
bottom: calc(10px - (24px - var(--chai-scrubber-track)) / 2);
|
|
1486
|
+
height: 24px;
|
|
1487
|
+
border-radius: 6px;
|
|
1488
|
+
cursor: pointer;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
.chai-result-card__scrubber::before,
|
|
1492
|
+
.chai-result-card__scrubber-fill {
|
|
1493
|
+
position: absolute;
|
|
1494
|
+
top: calc((24px - var(--chai-scrubber-track)) / 2);
|
|
1495
|
+
height: var(--chai-scrubber-track);
|
|
1496
|
+
border-radius: var(--chai-scrubber-track);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
.chai-result-card__scrubber::before {
|
|
1500
|
+
content: "";
|
|
1501
|
+
left: 0;
|
|
1502
|
+
right: 0;
|
|
1503
|
+
background: var(--chai-color-semantic-control-track);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
.chai-result-card__scrubber--expanded {
|
|
1507
|
+
--chai-scrubber-track: 8px;
|
|
1508
|
+
left: 16px;
|
|
1509
|
+
right: 16px;
|
|
1510
|
+
bottom: calc(16px - (24px - var(--chai-scrubber-track)) / 2);
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
.chai-result-card__scrubber-fill {
|
|
1514
|
+
left: 0;
|
|
1515
|
+
background: var(--chai-color-semantic-accent);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/* Transparent, covering the video: the play/pause control. */
|
|
1519
|
+
.chai-result-card__video-toggle {
|
|
1520
|
+
position: absolute;
|
|
1521
|
+
inset: 0;
|
|
1522
|
+
padding: 0;
|
|
1523
|
+
border: none;
|
|
1524
|
+
background: transparent;
|
|
1525
|
+
cursor: pointer;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
.chai-result-card__collapse {
|
|
1529
|
+
position: fixed;
|
|
1530
|
+
z-index: 2;
|
|
1531
|
+
top: 20px;
|
|
1532
|
+
right: 20px;
|
|
1533
|
+
width: 36px;
|
|
1534
|
+
height: 36px;
|
|
1535
|
+
border-radius: 50%;
|
|
1536
|
+
border: none;
|
|
1537
|
+
background: var(--chai-color-semantic-overlay);
|
|
1538
|
+
color: var(--chai-color-semantic-on-overlay);
|
|
1539
|
+
display: flex;
|
|
1540
|
+
align-items: center;
|
|
1541
|
+
justify-content: center;
|
|
1542
|
+
cursor: pointer;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
.chai-result-card__collapse svg {
|
|
1546
|
+
width: 14px;
|
|
1547
|
+
height: 14px;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/* --- Bottom bar --- */
|
|
1551
|
+
|
|
1552
|
+
/* Reference: Figma "Result Card - Image - Paginated" — an
|
|
1553
|
+
11px inset around the icon row, and when there's more than one result the
|
|
1554
|
+
pager drops to its own centered row beneath it (8px from the card's
|
|
1555
|
+
bottom edge) rather than squeezing between the icon groups. Without a
|
|
1556
|
+
pager the icon row is simply centered in a comparably roomy bar. */
|
|
1557
|
+
.chai-result-card__bar {
|
|
1558
|
+
display: flex;
|
|
1559
|
+
flex-direction: column;
|
|
1560
|
+
align-items: center;
|
|
1561
|
+
padding: 16px 11px;
|
|
1562
|
+
background: var(--chai-color-semantic-control-surface-raised);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
.chai-result-card__bar:has(.chai-pagination) {
|
|
1566
|
+
padding: 11px 11px 8px;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
.chai-result-card__bar-row {
|
|
1570
|
+
align-self: stretch;
|
|
1571
|
+
display: flex;
|
|
1572
|
+
align-items: center;
|
|
1573
|
+
justify-content: space-between;
|
|
1574
|
+
gap: var(--chai-space-2);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
.chai-result-card__bar-start {
|
|
1578
|
+
display: flex;
|
|
1579
|
+
align-items: center;
|
|
1580
|
+
gap: 2px;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/* The shared hover-circle treatment — reference: the design
|
|
1584
|
+
note that the "like" button's hover state in the mock is how every
|
|
1585
|
+
small icon button on the card should behave, not just that one. `
|
|
1586
|
+
--active` (a persisted, not just hovered, tint) is this component's own
|
|
1587
|
+
extension for like/dislike/more, since the mock only documents hover. */
|
|
1588
|
+
.chai-result-card__icon-btn {
|
|
1589
|
+
position: relative;
|
|
1590
|
+
width: 24px;
|
|
1591
|
+
height: 24px;
|
|
1592
|
+
padding: 0;
|
|
1593
|
+
border: none;
|
|
1594
|
+
border-radius: 50%;
|
|
1595
|
+
background: transparent;
|
|
1596
|
+
color: var(--chai-color-semantic-control-text-muted);
|
|
1597
|
+
display: flex;
|
|
1598
|
+
align-items: center;
|
|
1599
|
+
justify-content: center;
|
|
1600
|
+
cursor: pointer;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
.chai-result-card__icon-btn svg {
|
|
1604
|
+
width: 18px;
|
|
1605
|
+
height: 18px;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
.chai-result-card__icon-btn:hover:not(:disabled) {
|
|
1609
|
+
background: var(--chai-color-semantic-control-surface-hover);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
.chai-result-card__icon-btn--active {
|
|
1613
|
+
color: var(--chai-color-semantic-accent-text);
|
|
1614
|
+
background: var(--chai-color-semantic-control-surface-hover);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
.chai-result-card__icon-btn:disabled {
|
|
1618
|
+
opacity: 0.4;
|
|
1619
|
+
cursor: not-allowed;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/* --- More menu (reference: Figma "Result Card - More menu expanded") --- */
|
|
1623
|
+
|
|
1624
|
+
.chai-result-card__more {
|
|
1625
|
+
position: relative;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/* Opens upward, not down like the Figma mock's own isolated rendering of
|
|
1629
|
+
it — the "more" trigger sits right at the card's bottom edge, and
|
|
1630
|
+
`.chai-result-card__face` needs `overflow: hidden` for its rounded
|
|
1631
|
+
corners and the flip's `backface-visibility`, which would clip a
|
|
1632
|
+
downward menu (confirmed via a real screenshot during this build — the
|
|
1633
|
+
trigger showed pressed/active but no menu was visible at all). Opening
|
|
1634
|
+
upward keeps it inside the still-visible media area above the bar. */
|
|
1635
|
+
.chai-result-card__more-menu {
|
|
1636
|
+
position: absolute;
|
|
1637
|
+
z-index: 10;
|
|
1638
|
+
bottom: calc(100% + 8px);
|
|
1639
|
+
left: 0;
|
|
1640
|
+
width: 180px;
|
|
1641
|
+
margin: 0;
|
|
1642
|
+
padding: var(--chai-space-2);
|
|
1643
|
+
list-style: none;
|
|
1644
|
+
border-radius: var(--chai-radius-lg);
|
|
1645
|
+
background: var(--chai-color-semantic-control-surface-raised);
|
|
1646
|
+
box-shadow: var(--chai-shadow-menu);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
.chai-result-card__more-menu-item {
|
|
1650
|
+
display: flex;
|
|
1651
|
+
align-items: center;
|
|
1652
|
+
gap: var(--chai-space-2);
|
|
1653
|
+
width: 100%;
|
|
1654
|
+
padding: 10px 12px;
|
|
1655
|
+
border: none;
|
|
1656
|
+
border-radius: var(--chai-radius-sm);
|
|
1657
|
+
background: transparent;
|
|
1658
|
+
color: var(--chai-color-semantic-control-text-strong);
|
|
1659
|
+
font-family: var(--chai-type-family-material);
|
|
1660
|
+
font-size: var(--chai-type-size-body-large);
|
|
1661
|
+
text-align: left;
|
|
1662
|
+
cursor: pointer;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
.chai-result-card__more-menu-item:hover {
|
|
1666
|
+
background: var(--chai-color-semantic-control-surface-hover);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
.chai-result-card__more-menu-icon {
|
|
1670
|
+
display: flex;
|
|
1671
|
+
width: 18px;
|
|
1672
|
+
height: 18px;
|
|
1673
|
+
flex: none;
|
|
1674
|
+
color: var(--chai-color-semantic-control-text);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/* --- Info flip (back face) --- */
|
|
1678
|
+
|
|
1679
|
+
.chai-result-card__close {
|
|
1680
|
+
position: absolute;
|
|
1681
|
+
z-index: 1;
|
|
1682
|
+
top: 12px;
|
|
1683
|
+
right: 12px;
|
|
1684
|
+
width: 24px;
|
|
1685
|
+
height: 24px;
|
|
1686
|
+
padding: 0;
|
|
1687
|
+
border: none;
|
|
1688
|
+
border-radius: 50%;
|
|
1689
|
+
background: transparent;
|
|
1690
|
+
color: var(--chai-color-semantic-control-text-muted);
|
|
1691
|
+
display: flex;
|
|
1692
|
+
align-items: center;
|
|
1693
|
+
justify-content: center;
|
|
1694
|
+
cursor: pointer;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
.chai-result-card__close:hover {
|
|
1698
|
+
color: var(--chai-color-semantic-control-text-strong);
|
|
1699
|
+
background: var(--chai-color-semantic-control-surface-hover);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
.chai-result-card__close svg {
|
|
1703
|
+
width: 14px;
|
|
1704
|
+
height: 14px;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
.chai-result-card__prompt-box {
|
|
1708
|
+
position: relative;
|
|
1709
|
+
flex: 1;
|
|
1710
|
+
min-height: 0;
|
|
1711
|
+
margin-top: var(--chai-space-4);
|
|
1712
|
+
padding: var(--chai-space-3) var(--chai-space-4);
|
|
1713
|
+
border-radius: 14px;
|
|
1714
|
+
background: var(--chai-color-semantic-control-surface);
|
|
1715
|
+
overflow-y: auto;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
.chai-result-card__prompt-text {
|
|
1719
|
+
margin: 0;
|
|
1720
|
+
padding-right: 20px;
|
|
1721
|
+
font-size: var(--chai-type-size-body-medium);
|
|
1722
|
+
line-height: var(--chai-type-leading-body-medium);
|
|
1723
|
+
color: var(--chai-color-semantic-control-text-strong);
|
|
1724
|
+
user-select: text;
|
|
1725
|
+
white-space: pre-wrap;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
.chai-result-card__copy {
|
|
1729
|
+
position: absolute;
|
|
1730
|
+
top: 9px;
|
|
1731
|
+
right: 9px;
|
|
1732
|
+
width: 22px;
|
|
1733
|
+
height: 22px;
|
|
1734
|
+
padding: 0;
|
|
1735
|
+
border: none;
|
|
1736
|
+
border-radius: 50%;
|
|
1737
|
+
background: transparent;
|
|
1738
|
+
color: var(--chai-color-semantic-control-text-muted);
|
|
1739
|
+
display: flex;
|
|
1740
|
+
align-items: center;
|
|
1741
|
+
justify-content: center;
|
|
1742
|
+
cursor: pointer;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
.chai-result-card__copy:hover {
|
|
1746
|
+
color: var(--chai-color-semantic-control-text-strong);
|
|
1747
|
+
background: var(--chai-color-semantic-control-surface-hover);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
.chai-result-card__copy svg {
|
|
1751
|
+
width: 10px;
|
|
1752
|
+
height: 13px;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
.chai-result-card__chips {
|
|
1756
|
+
display: flex;
|
|
1757
|
+
flex-wrap: wrap;
|
|
1758
|
+
gap: var(--chai-space-2);
|
|
1759
|
+
flex: none;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
.chai-result-card__chip {
|
|
1763
|
+
padding: 6px 12px;
|
|
1764
|
+
border-radius: var(--chai-radius-sm);
|
|
1765
|
+
border: 1px solid var(--chai-color-semantic-control-border);
|
|
1766
|
+
font-size: var(--chai-type-size-label-large);
|
|
1767
|
+
color: var(--chai-color-semantic-control-text);
|
|
1768
|
+
white-space: nowrap;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
.chai-result-card__chip--evaluation {
|
|
1772
|
+
display: inline-flex;
|
|
1773
|
+
align-items: center;
|
|
1774
|
+
gap: 4px;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
.chai-result-card__chip--evaluation svg {
|
|
1778
|
+
width: 14px;
|
|
1779
|
+
height: 14px;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
/* Reference: the mock's checked, filled green "Evaluation" chip. */
|
|
1783
|
+
.chai-result-card__chip--passed {
|
|
1784
|
+
border-color: var(--chai-color-semantic-accent);
|
|
1785
|
+
color: var(--chai-color-semantic-accent-text);
|
|
1786
|
+
background: var(--chai-color-semantic-accent-subtle);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
/* The failed counterpart, in the danger tokens. Outlined, not filled:
|
|
1790
|
+
danger text on danger-subtle is 2.96:1, under WCAG AA's 4.5:1, while on
|
|
1791
|
+
the card surface it's 4.62:1. The cross icon carries the distinction the
|
|
1792
|
+
fill would have. */
|
|
1793
|
+
.chai-result-card__chip--failed {
|
|
1794
|
+
border-color: var(--chai-color-semantic-danger);
|
|
1795
|
+
color: var(--chai-color-semantic-danger);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
/* The cross glyph fills its whole viewBox where the check has padding —
|
|
1799
|
+
sized down so both icons read at the same visual weight. */
|
|
1800
|
+
.chai-result-card__chip--failed svg {
|
|
1801
|
+
width: 10px;
|
|
1802
|
+
height: 10px;
|
|
1803
|
+
margin: 0 2px;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
/* Announced, not shown — carries "passed"/"failed" for screen readers. */
|
|
1807
|
+
.chai-visually-hidden {
|
|
1808
|
+
position: absolute;
|
|
1809
|
+
width: 1px;
|
|
1810
|
+
height: 1px;
|
|
1811
|
+
margin: -1px;
|
|
1812
|
+
padding: 0;
|
|
1813
|
+
overflow: hidden;
|
|
1814
|
+
clip: rect(0 0 0 0);
|
|
1815
|
+
white-space: nowrap;
|
|
1816
|
+
border: 0;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
/* --- Focus rings ------------------------------------------------------------
|
|
1820
|
+
On by default: every CHAI control shows a visible keyboard-focus ring,
|
|
1821
|
+
which WCAG 2.4.7 Focus Visible requires. Buttons and menu items
|
|
1822
|
+
keep the browser's own ring; MD3 elements (the submit FAB, the switch)
|
|
1823
|
+
draw `<md-focus-ring>`. The Composer's textarea is the one control that
|
|
1824
|
+
had none — its ring goes on the whole pill, matching MD3's ring width
|
|
1825
|
+
and color so every ring in a component reads as the same system. */
|
|
1826
|
+
.chai-composer:has(.chai-composer__input:focus-visible) {
|
|
1827
|
+
outline: 3px solid var(--chai-color-semantic-accent);
|
|
1828
|
+
outline-offset: 2px;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
/* Off switch: `data-chai-focus-ring="hidden"` on any ancestor hides every
|
|
1832
|
+
CHAI focus ring inside it — for a showcase or preview surface, not a
|
|
1833
|
+
production app. Hiding them removes the only indicator keyboard users
|
|
1834
|
+
have of where they are, failing WCAG 2.4.7 Focus Visible.
|
|
1835
|
+
Scoped to CHAI's own roots so a builder's other controls under the same
|
|
1836
|
+
ancestor keep theirs. */
|
|
1837
|
+
[data-chai-focus-ring="hidden"] :is(
|
|
1838
|
+
.chai-composer,
|
|
1839
|
+
.chai-media-analyzer,
|
|
1840
|
+
.chai-result-card,
|
|
1841
|
+
.chai-result-card__expand-overlay,
|
|
1842
|
+
.chai-search-menu,
|
|
1843
|
+
.chai-pagination,
|
|
1844
|
+
.chai-switch
|
|
1845
|
+
) {
|
|
1846
|
+
--md-focus-ring-width: 0px;
|
|
1847
|
+
--md-focus-ring-active-width: 0px;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
[data-chai-focus-ring="hidden"] .chai-composer:has(.chai-composer__input:focus-visible),
|
|
1851
|
+
[data-chai-focus-ring="hidden"] :is(
|
|
1852
|
+
.chai-composer,
|
|
1853
|
+
.chai-media-analyzer,
|
|
1854
|
+
.chai-result-card,
|
|
1855
|
+
.chai-result-card__expand-overlay,
|
|
1856
|
+
.chai-search-menu,
|
|
1857
|
+
.chai-pagination
|
|
1858
|
+
) :focus-visible {
|
|
1859
|
+
outline: none;
|
|
1860
|
+
}
|