@horon-ai/react 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +168 -2
  2. package/package.json +50 -5
  3. package/src/assets/sidebar-photo.jpg +0 -0
  4. package/src/components/agent-activity.tsx +204 -0
  5. package/src/components/agent-lane.tsx +80 -0
  6. package/src/components/agent-lanes-timeline.tsx +312 -0
  7. package/src/components/agent-panel.tsx +125 -0
  8. package/src/components/agent-steering-panel.tsx +276 -0
  9. package/src/components/approval-card.tsx +89 -0
  10. package/src/components/avatar.tsx +104 -0
  11. package/src/components/badge.tsx +63 -0
  12. package/src/components/banner.tsx +103 -0
  13. package/src/components/bot-instructions-panel.tsx +369 -0
  14. package/src/components/bot-list-row.tsx +42 -0
  15. package/src/components/bot-memory-panel.tsx +229 -0
  16. package/src/components/bottom-sheet.tsx +140 -0
  17. package/src/components/breadcrumb-header.tsx +87 -0
  18. package/src/components/breadcrumb.tsx +86 -0
  19. package/src/components/button.tsx +146 -0
  20. package/src/components/calendar.tsx +410 -0
  21. package/src/components/card.tsx +79 -0
  22. package/src/components/chat-bubble.tsx +36 -0
  23. package/src/components/chat-controls.tsx +118 -0
  24. package/src/components/checkbox.tsx +73 -0
  25. package/src/components/chip.tsx +81 -0
  26. package/src/components/choice-card.tsx +55 -0
  27. package/src/components/code-panel.tsx +27 -0
  28. package/src/components/combobox.tsx +312 -0
  29. package/src/components/command-palette.tsx +206 -0
  30. package/src/components/composer.tsx +124 -0
  31. package/src/components/data-table.tsx +429 -0
  32. package/src/components/date-field.tsx +100 -0
  33. package/src/components/dialog.tsx +93 -0
  34. package/src/components/directory-card.tsx +59 -0
  35. package/src/components/document-extract-panel.tsx +398 -0
  36. package/src/components/empty-state.tsx +29 -0
  37. package/src/components/field.tsx +78 -0
  38. package/src/components/file-drop.tsx +89 -0
  39. package/src/components/filter-bar.tsx +83 -0
  40. package/src/components/flue-background.tsx +388 -0
  41. package/src/components/horon-icon.tsx +280 -0
  42. package/src/components/horon-wordmark.tsx +26 -0
  43. package/src/components/icon-button.tsx +61 -0
  44. package/src/components/input-group.tsx +112 -0
  45. package/src/components/input.tsx +72 -0
  46. package/src/components/integration-row.tsx +103 -0
  47. package/src/components/kbd.tsx +42 -0
  48. package/src/components/knowledge-base-panel.tsx +185 -0
  49. package/src/components/liquid-glass.tsx +24 -0
  50. package/src/components/list-detail.tsx +275 -0
  51. package/src/components/marketplace-card.tsx +97 -0
  52. package/src/components/mcp-server-row.tsx +122 -0
  53. package/src/components/menu.tsx +165 -0
  54. package/src/components/message-extras.tsx +146 -0
  55. package/src/components/modal.tsx +108 -0
  56. package/src/components/orchestrator-panel.tsx +175 -0
  57. package/src/components/page-header.tsx +48 -0
  58. package/src/components/plan-card.tsx +70 -0
  59. package/src/components/plan-step.tsx +111 -0
  60. package/src/components/plugin-row.tsx +128 -0
  61. package/src/components/progress.tsx +21 -0
  62. package/src/components/radio-group.tsx +84 -0
  63. package/src/components/research-run-panel.tsx +232 -0
  64. package/src/components/schedule.tsx +332 -0
  65. package/src/components/segmented-control.tsx +136 -0
  66. package/src/components/select.tsx +154 -0
  67. package/src/components/settings-list.tsx +99 -0
  68. package/src/components/sidebar.tsx +542 -0
  69. package/src/components/skeleton.tsx +75 -0
  70. package/src/components/slider.tsx +29 -0
  71. package/src/components/source-row.tsx +112 -0
  72. package/src/components/spinner.tsx +29 -0
  73. package/src/components/split-button.tsx +72 -0
  74. package/src/components/switch.tsx +79 -0
  75. package/src/components/tab-bar.tsx +90 -0
  76. package/src/components/tabs.tsx +162 -0
  77. package/src/components/task-row.tsx +159 -0
  78. package/src/components/textarea-field.tsx +127 -0
  79. package/src/components/textarea.tsx +47 -0
  80. package/src/components/toast.tsx +128 -0
  81. package/src/components/tool-call.tsx +215 -0
  82. package/src/components/tool-calls-header.tsx +65 -0
  83. package/src/components/tool-permissions.tsx +105 -0
  84. package/src/components/tooltip.tsx +33 -0
  85. package/src/components/top-bar.tsx +65 -0
  86. package/src/components/usage-meter.tsx +47 -0
  87. package/src/components/workspace-menu.tsx +77 -0
  88. package/src/hooks/index.ts +8 -0
  89. package/src/hooks/use-active-indicator.ts +60 -0
  90. package/src/hooks/use-color-scheme.ts +27 -0
  91. package/src/hooks/use-copy-to-clipboard.ts +32 -0
  92. package/src/hooks/use-device.ts +41 -0
  93. package/src/hooks/use-hotkey.ts +35 -0
  94. package/src/hooks/use-media-query.ts +25 -0
  95. package/src/hooks/use-platform.ts +39 -0
  96. package/src/hooks/use-reduced-motion.ts +8 -0
  97. package/src/index.ts +99 -0
  98. package/src/lib/cn.ts +25 -0
  99. package/src/lib/cursor-ghost.ts +244 -0
  100. package/src/lib/platform.ts +169 -0
  101. package/src/lib/size.ts +10 -0
  102. package/src/styles/globals.css +564 -0
@@ -0,0 +1,564 @@
1
+ @import "tailwindcss";
2
+ @import "pretendard/dist/web/variable/pretendardvariable-dynamic-subset.css";
3
+ @import "@fontsource-variable/geist-mono";
4
+ @import "@horon-ai/tokens/theme.css";
5
+
6
+ @custom-variant dark (&:is(.dark *));
7
+
8
+ @source "../../../../apps/**/*.{ts,tsx}";
9
+ @source "../**/*.{ts,tsx}";
10
+
11
+ @layer base {
12
+ html {
13
+ font-family: var(--font-sans);
14
+ -webkit-font-smoothing: antialiased;
15
+ }
16
+
17
+ body {
18
+ background-color: var(--color-bg-default);
19
+ color: var(--color-text-primary);
20
+ }
21
+ }
22
+
23
+ /* iOS Safari zooms the whole page in when a focused form control's font size is
24
+ * under 16px, and it does not zoom back out on blur, so one tap on a 14px field
25
+ * leaves the layout cropped. A 16px floor on the control removes the trigger and
26
+ * the page stays at 1x. `maximum-scale` and `user-scalable=no` would also stop
27
+ * the zoom but take the user's own pinch zoom away, so this rule is the only
28
+ * remedy used. Coarse pointers only: a mouse-driven screen never zooms on focus,
29
+ * so it keeps the designed 13/14px field text, and coarse is the closest signal
30
+ * for the devices that do. max(16px, 1em) leaves text on a larger scale alone
31
+ * instead of shrinking it. Unlayered on purpose: field utilities such as
32
+ * `text-body-sm` sit in Tailwind's `utilities` layer and would otherwise win,
33
+ * leaving the control at 14px and the zoom in place. */
34
+ @media (pointer: coarse) {
35
+ input,
36
+ textarea,
37
+ select {
38
+ font-size: max(16px, 1em);
39
+ }
40
+ }
41
+
42
+ /* Glass, the material in @horon-ai/tokens glass.ts that Flutter's
43
+ * HoronLiquidGlass draws with the same values: the backdrop blurred and
44
+ * saturated, under the host's own fill, and a rim ring on the edge. Only
45
+ * backdrop-filter, gradients and masks, so Chromium, Safari and Firefox match. */
46
+ @layer components {
47
+ .liquid-layers {
48
+ position: absolute;
49
+ inset: 0;
50
+ border-radius: inherit;
51
+ pointer-events: none;
52
+ -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
53
+ backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
54
+ }
55
+
56
+ /* The rim: a vertical gradient cut down to a ring by masking the border-box
57
+ * against the content-box. A host tints it for a state (selected, pending,
58
+ * danger) by setting --glass-edge to a colour; a field in error does it
59
+ * through data-invalid. Longhands, not the `mask` shorthand: the site's CSS
60
+ * minifier drops the `exclude` operator from the shorthand and the ring would
61
+ * fill the whole surface. */
62
+ .liquid-layers::after {
63
+ content: "";
64
+ position: absolute;
65
+ inset: 0;
66
+ border-radius: inherit;
67
+ padding: var(--glass-rim-width);
68
+ background: var(
69
+ --glass-edge,
70
+ linear-gradient(
71
+ to bottom,
72
+ var(--glass-rim-top),
73
+ var(--glass-rim-mid) var(--glass-rim-mid-stop),
74
+ var(--glass-rim-bottom)
75
+ )
76
+ );
77
+ mask-image: linear-gradient(#000 0 0), linear-gradient(#000 0 0);
78
+ mask-clip: content-box, border-box;
79
+ mask-composite: exclude;
80
+ -webkit-mask-image: linear-gradient(#000 0 0), linear-gradient(#000 0 0);
81
+ -webkit-mask-clip: content-box, border-box;
82
+ -webkit-mask-composite: xor;
83
+ }
84
+
85
+ /* The tint is the host's own: every glass host resets it, so a glass button
86
+ * inside a pending card keeps its plain rim. Zero specificity, so the host's
87
+ * `[--glass-edge:…]` utility still wins. */
88
+ :where(:has(> .liquid-layers)) {
89
+ --glass-edge: initial;
90
+ }
91
+
92
+ :is([data-invalid], :has(> [data-invalid])) > .liquid-layers {
93
+ --glass-edge: var(--color-border-danger);
94
+ }
95
+
96
+ /* No translucency by request, or no backdrop filter in the engine: a frosted
97
+ * fill that keeps the text readable. The rim stays. */
98
+ @media (prefers-reduced-transparency: reduce) {
99
+ .liquid-layers {
100
+ background: var(--color-glass-fill-frost);
101
+ -webkit-backdrop-filter: none;
102
+ backdrop-filter: none;
103
+ }
104
+ }
105
+
106
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
107
+ .liquid-layers {
108
+ background: var(--color-glass-fill-frost);
109
+ }
110
+ }
111
+
112
+ /* The veil, glass.ts `veil`: the light glass of fields (`veil-field`) and
113
+ * overlays (`veil-overlay`). An 8px blur with 160% saturation, a fill, a 1px
114
+ * ring drawn as a shadow and a soft drop shadow; no rim, no liquid layers.
115
+ * A field in error swaps the ring for the danger colour. */
116
+ .veil-field,
117
+ .veil-overlay {
118
+ -webkit-backdrop-filter: blur(var(--veil-blur)) saturate(var(--veil-saturate));
119
+ backdrop-filter: blur(var(--veil-blur)) saturate(var(--veil-saturate));
120
+ border-color: transparent;
121
+ }
122
+
123
+ .veil-field {
124
+ background-color: var(--veil-field);
125
+ box-shadow:
126
+ 0 0 0 var(--veil-ring-width) var(--veil-ring),
127
+ 0 2px 10px var(--veil-field-shadow);
128
+ }
129
+
130
+ .veil-field:focus-within {
131
+ background-color: var(--veil-field-focus);
132
+ }
133
+
134
+ .veil-field:is([data-invalid], :has([data-invalid])) {
135
+ box-shadow: 0 0 0 var(--veil-ring-width) var(--color-border-danger);
136
+ }
137
+
138
+ .veil-overlay {
139
+ background-color: var(--veil-overlay);
140
+ box-shadow:
141
+ 0 0 0 var(--veil-ring-width) var(--veil-ring),
142
+ 0 6px 24px var(--veil-overlay-shadow);
143
+ }
144
+
145
+ /* File drop: the veil's blur, fill and soft shadow without the ring, because
146
+ * the target's 1.5px dashed border is the only outline it should show. */
147
+ .veil-drop {
148
+ -webkit-backdrop-filter: blur(var(--veil-blur)) saturate(var(--veil-saturate));
149
+ backdrop-filter: blur(var(--veil-blur)) saturate(var(--veil-saturate));
150
+ background-color: var(--veil-field);
151
+ box-shadow: 0 2px 10px var(--veil-field-shadow);
152
+ }
153
+
154
+ @media (prefers-reduced-transparency: reduce) {
155
+ .veil-field,
156
+ .veil-overlay,
157
+ .veil-drop {
158
+ background-color: var(--color-glass-fill-frost);
159
+ -webkit-backdrop-filter: none;
160
+ backdrop-filter: none;
161
+ }
162
+ }
163
+
164
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
165
+ .veil-field,
166
+ .veil-overlay,
167
+ .veil-drop {
168
+ background-color: var(--color-glass-fill-frost);
169
+ }
170
+ }
171
+ }
172
+
173
+ /* Motion. The Figma file defines no motion variables, so the library's motion
174
+ * lives here: three easings, a few keyframes, and utilities components compose.
175
+ * Durations: 120-150ms for color and press feedback, 180-240ms for popups and
176
+ * panels, ~280ms for indicators that travel between items. */
177
+ @theme {
178
+ --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
179
+ --ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
180
+ --ease-spring: cubic-bezier(0.34, 1.3, 0.64, 1);
181
+
182
+ --animate-shimmer: shimmer 1.6s linear infinite;
183
+ --animate-live-pulse: live-pulse 2s var(--ease-out-quint) infinite;
184
+ --animate-caret: caret-blink 1s steps(1, end) infinite;
185
+ --animate-thinking: thinking-dot 1.2s ease-in-out infinite;
186
+ --animate-pop-in: pop-in 220ms var(--ease-spring) both;
187
+ --animate-pop-out: pop-out 130ms ease-in both;
188
+ --animate-wave: wave-level 900ms ease-in-out infinite;
189
+ --animate-slide-next: slide-next 200ms var(--ease-out-quint) both;
190
+ --animate-slide-prev: slide-prev 200ms var(--ease-out-quint) both;
191
+ --animate-slide-shift-next: slide-shift-next 200ms var(--ease-out-quint) both;
192
+ --animate-slide-shift-prev: slide-shift-prev 200ms var(--ease-out-quint) both;
193
+ --animate-swap-move: swap-move 200ms var(--ease-out-quint) both;
194
+ --animate-fade-in: fade-in 160ms var(--ease-out-quint) both;
195
+ --animate-scale-out: scale-out 120ms ease-in both;
196
+ --animate-sweep: sweep 1.4s var(--ease-out-quint) infinite;
197
+
198
+ @keyframes shimmer {
199
+ from {
200
+ background-position: 200% 0;
201
+ }
202
+ to {
203
+ background-position: -200% 0;
204
+ }
205
+ }
206
+
207
+ @keyframes live-pulse {
208
+ 0% {
209
+ box-shadow: 0 0 0 0 color-mix(in oklab, currentColor 45%, transparent);
210
+ }
211
+ 70%,
212
+ 100% {
213
+ box-shadow: 0 0 0 6px transparent;
214
+ }
215
+ }
216
+
217
+ @keyframes caret-blink {
218
+ 0%,
219
+ 100% {
220
+ opacity: 1;
221
+ }
222
+ 50% {
223
+ opacity: 0;
224
+ }
225
+ }
226
+
227
+ @keyframes thinking-dot {
228
+ 0%,
229
+ 80%,
230
+ 100% {
231
+ opacity: 0.3;
232
+ translate: 0 0;
233
+ }
234
+ 40% {
235
+ opacity: 1;
236
+ translate: 0 -2px;
237
+ }
238
+ }
239
+
240
+ @keyframes tab-panel-in {
241
+ from {
242
+ opacity: 0;
243
+ translate: 0 4px;
244
+ }
245
+ to {
246
+ opacity: 1;
247
+ translate: 0 0;
248
+ }
249
+ }
250
+
251
+ @keyframes pop-in {
252
+ from {
253
+ opacity: 0;
254
+ scale: 0.6;
255
+ }
256
+ to {
257
+ opacity: 1;
258
+ scale: 1;
259
+ }
260
+ }
261
+
262
+ @keyframes pop-out {
263
+ from {
264
+ opacity: 1;
265
+ scale: 1;
266
+ }
267
+ to {
268
+ opacity: 0;
269
+ scale: 0.97;
270
+ }
271
+ }
272
+
273
+ @keyframes wave-level {
274
+ 0%,
275
+ 100% {
276
+ scale: 1 0.35;
277
+ }
278
+ 50% {
279
+ scale: 1 1;
280
+ }
281
+ }
282
+
283
+ @keyframes slide-next {
284
+ from {
285
+ opacity: 0;
286
+ translate: 12px 0;
287
+ }
288
+ to {
289
+ opacity: 1;
290
+ translate: 0 0;
291
+ }
292
+ }
293
+
294
+ @keyframes slide-prev {
295
+ from {
296
+ opacity: 0;
297
+ translate: -12px 0;
298
+ }
299
+ to {
300
+ opacity: 1;
301
+ translate: 0 0;
302
+ }
303
+ }
304
+
305
+ /* Opacity-free variants of slide-next/prev: a panel that is replaced in place moves in without a
306
+ * blank first frame. */
307
+ @keyframes slide-shift-next {
308
+ from {
309
+ translate: 12px 0;
310
+ }
311
+ to {
312
+ translate: 0 0;
313
+ }
314
+ }
315
+
316
+ @keyframes slide-shift-prev {
317
+ from {
318
+ translate: -12px 0;
319
+ }
320
+ to {
321
+ translate: 0 0;
322
+ }
323
+ }
324
+
325
+ /* The same, for content that is swapped rather than slid: a short rise with no fade. */
326
+ @keyframes swap-move {
327
+ from {
328
+ translate: 0 4px;
329
+ }
330
+ to {
331
+ translate: 0 0;
332
+ }
333
+ }
334
+
335
+ /* In-place text and value changes: a label, caret or number that is replaced, not moved. */
336
+ @keyframes fade-in {
337
+ from {
338
+ opacity: 0;
339
+ }
340
+ to {
341
+ opacity: 1;
342
+ }
343
+ }
344
+
345
+ /* Deeper than pop-out, for small glyphs (checkmarks, radio dots) leaving a fixed slot. */
346
+ @keyframes scale-out {
347
+ from {
348
+ opacity: 1;
349
+ scale: 1;
350
+ }
351
+ to {
352
+ opacity: 0;
353
+ scale: 0.6;
354
+ }
355
+ }
356
+
357
+ /* Indeterminate progress: a bar one third of the track wide that crosses it and re-enters. */
358
+ @keyframes sweep {
359
+ from {
360
+ translate: -100% 0;
361
+ }
362
+ to {
363
+ translate: 400% 0;
364
+ }
365
+ }
366
+ }
367
+
368
+ /* Optical centering for one-line labels in fixed-height pills and controls: the label box is trimmed to the
369
+ * cap height and the alphabetic baseline, so `items-center` centres the letters instead of the font's
370
+ * ascent/descent box, whatever the font. Put it on the label element (a flex item), not the container: an
371
+ * anonymous text run is not trimmed. Only inside a fixed height, since it shrinks the label box. Browsers
372
+ * without `text-box` keep the plain line box. */
373
+ @utility text-trim {
374
+ text-box: trim-both cap alphabetic;
375
+ }
376
+
377
+ /* Panel, lane and row entrance; stagger with animation-delay: calc(var(--i) * 30ms). */
378
+ @utility motion-rise {
379
+ animation: tab-panel-in 220ms var(--ease-out-quint) both;
380
+ }
381
+
382
+ /* Popups (Select, Menu, Tooltip, Popover, Dialog): fade and grow from the
383
+ * anchor (Base UI sets --transform-origin), leave a little faster. Uses the
384
+ * `scale` property so it never fights a positioner's transform. */
385
+ @utility motion-popup {
386
+ transform-origin: var(--transform-origin, center);
387
+ transition-property: opacity, scale, translate;
388
+ transition-duration: 200ms;
389
+ transition-timing-function: var(--ease-out-quint);
390
+
391
+ &[data-starting-style] {
392
+ opacity: 0;
393
+ scale: 0.95;
394
+ }
395
+
396
+ &[data-ending-style] {
397
+ opacity: 0;
398
+ scale: 0.97;
399
+ transition-duration: 130ms;
400
+ transition-timing-function: ease-in;
401
+ }
402
+ }
403
+
404
+ /* Backdrops and other plain fades. */
405
+ @utility motion-fade {
406
+ transition-property: opacity;
407
+ transition-duration: 200ms;
408
+ transition-timing-function: ease-out;
409
+
410
+ &[data-starting-style],
411
+ &[data-ending-style] {
412
+ opacity: 0;
413
+ }
414
+ }
415
+
416
+ /* Base UI Collapsible / Accordion panels: animate to and from the measured
417
+ * --collapsible-panel-height. */
418
+ @utility motion-collapse {
419
+ overflow: hidden;
420
+ height: var(--collapsible-panel-height);
421
+ transition-property: height, opacity;
422
+ transition-duration: 240ms;
423
+ transition-timing-function: var(--ease-out-quint);
424
+
425
+ &[data-starting-style],
426
+ &[data-ending-style] {
427
+ height: 0;
428
+ opacity: 0;
429
+ }
430
+ }
431
+
432
+ /* A <details> whose body opens and closes by height instead of snapping. Chromium 131+ and
433
+ * Safari 18.4+ animate ::details-content; other browsers keep the instant toggle. */
434
+ @utility motion-details {
435
+ interpolate-size: allow-keywords;
436
+
437
+ &::details-content {
438
+ block-size: 0;
439
+ overflow: hidden;
440
+ transition-property: block-size, content-visibility;
441
+ transition-duration: 240ms;
442
+ transition-timing-function: var(--ease-out-quint);
443
+ transition-behavior: allow-discrete;
444
+ }
445
+
446
+ &[open]::details-content {
447
+ block-size: auto;
448
+ }
449
+ }
450
+
451
+ /* An inline block that opens and closes in place where Base UI's Collapsible is not used, so
452
+ * --collapsible-panel-height is not available: one grid row animates between 1fr (open) and 0fr.
453
+ * Wrap exactly one child; the child clips its own overflow. The wrapper is open by default — add
454
+ * data-collapsed to close it, and give focusable descendants `inert` while it is collapsed. */
455
+ @utility motion-reveal {
456
+ display: grid;
457
+ grid-template-rows: 1fr;
458
+ opacity: 1;
459
+ transition-property: grid-template-rows, opacity;
460
+ transition-duration: 200ms, 150ms;
461
+ transition-timing-function: var(--ease-out-quint), ease-out;
462
+
463
+ > * {
464
+ min-height: 0;
465
+ overflow: hidden;
466
+ }
467
+
468
+ &[data-collapsed] {
469
+ grid-template-rows: 0fr;
470
+ opacity: 0;
471
+ }
472
+ }
473
+
474
+ /* The horizontal counterpart of motion-reveal, for a slot that appears beside other content
475
+ * (an action button, a badge). Same contract: open by default, data-collapsed to close. */
476
+ @utility motion-reveal-x {
477
+ display: grid;
478
+ grid-template-columns: minmax(0, 1fr);
479
+ opacity: 1;
480
+ transition-property: grid-template-columns, opacity;
481
+ transition-duration: 200ms, 150ms;
482
+ transition-timing-function: var(--ease-out-quint), ease-out;
483
+
484
+ > * {
485
+ min-width: 0;
486
+ overflow: hidden;
487
+ }
488
+
489
+ &[data-collapsed] {
490
+ grid-template-columns: minmax(0, 0fr);
491
+ opacity: 0;
492
+ }
493
+ }
494
+
495
+ /* One slot, two states: stack both children in the same grid cell and put data-hidden on the
496
+ * inactive one, so a control's icon, label or action crossfades instead of swapping. The slot is
497
+ * as wide as the larger child. Also give the inactive child aria-hidden and tabIndex={-1}. */
498
+ @utility motion-swap {
499
+ display: grid;
500
+ grid-template-areas: "swap";
501
+
502
+ > * {
503
+ grid-area: swap;
504
+ transition-property: opacity, scale;
505
+ transition-duration: 150ms;
506
+ transition-timing-function: var(--ease-out-quint);
507
+ }
508
+
509
+ > [data-hidden] {
510
+ opacity: 0;
511
+ scale: 0.95;
512
+ pointer-events: none;
513
+ }
514
+ }
515
+
516
+ /* An indicator that travels between items (tabs, segmented control, tab bar). */
517
+ @utility motion-indicator {
518
+ transition-property: translate, left, top, width, height, opacity;
519
+ transition-duration: 280ms;
520
+ transition-timing-function: var(--ease-emphasized);
521
+ }
522
+
523
+ /* Color feedback for hover, focus, selection and validation. */
524
+ @utility motion-colors {
525
+ transition-property: color, background-color, border-color, outline-color, box-shadow, opacity, fill, stroke;
526
+ transition-duration: 150ms;
527
+ transition-timing-function: ease-out;
528
+ }
529
+
530
+ /* Pressable controls: color feedback plus a small press-in. */
531
+ @utility motion-press {
532
+ transition-property: color, background-color, border-color, box-shadow, opacity, scale;
533
+ transition-duration: 150ms;
534
+ transition-timing-function: var(--ease-out-quint);
535
+
536
+ &:active:not(:disabled, [data-disabled], [aria-disabled="true"]) {
537
+ scale: 0.97;
538
+ }
539
+ }
540
+
541
+ /* Users who ask for less motion get instant state changes; looping animations stop after one frame. */
542
+ @layer base {
543
+ @media (prefers-reduced-motion: reduce) {
544
+ *,
545
+ ::before,
546
+ ::after {
547
+ animation-duration: 0.01ms !important;
548
+ animation-iteration-count: 1 !important;
549
+ transition-duration: 0.01ms !important;
550
+ scroll-behavior: auto !important;
551
+ }
552
+ }
553
+ }
554
+
555
+ /* Paper sidebar ground: #EEF3F8 under the Horon photo (bottom-anchored cover) and a 55% white veil. */
556
+ @utility bg-sidebar-photo {
557
+ background-color: #eef3f8;
558
+ background-image: linear-gradient(#ffffff8c, #ffffff8c), url("../assets/sidebar-photo.jpg");
559
+ background-size: auto, cover;
560
+ background-position:
561
+ center,
562
+ 50% 100%;
563
+ background-repeat: no-repeat;
564
+ }