@justin_evo/evo-ui 1.2.0 → 1.2.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 (77) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +70 -70
  3. package/dist/declarations.d.ts +6 -6
  4. package/package.json +52 -52
  5. package/src/Alert/Alert.tsx +49 -49
  6. package/src/AutoComplete/AutoComplete.tsx +810 -810
  7. package/src/Badge/Badge.tsx +53 -53
  8. package/src/Breadcrumb/Breadcrumb.tsx +53 -53
  9. package/src/Button/Button.tsx +125 -125
  10. package/src/Card/Card.tsx +257 -257
  11. package/src/Checkbox/Checkbox.tsx +59 -59
  12. package/src/CommandPalette/CommandPalette.tsx +185 -185
  13. package/src/Container/Container.tsx +31 -31
  14. package/src/Divider/Divider.tsx +31 -31
  15. package/src/Form/Form.tsx +185 -185
  16. package/src/Grid/Grid.tsx +66 -66
  17. package/src/ImageCropper/ImageCropper.tsx +911 -911
  18. package/src/Input/Input.tsx +74 -74
  19. package/src/Modal/Modal.tsx +77 -77
  20. package/src/Nav/Nav.tsx +708 -708
  21. package/src/Notification/Notification.tsx +1503 -1503
  22. package/src/Pagination/Pagination.tsx +76 -76
  23. package/src/Radio/Radio.tsx +69 -69
  24. package/src/RichTextArea/RichTextArea.tsx +886 -886
  25. package/src/Select/Select.tsx +515 -515
  26. package/src/Skeleton/Skeleton.tsx +70 -70
  27. package/src/Stack/Stack.tsx +52 -52
  28. package/src/Table/Table.tsx +335 -335
  29. package/src/Tabs/Tabs.tsx +90 -90
  30. package/src/Theme/ThemeProvider.tsx +253 -253
  31. package/src/Theme/ThemeToggle.tsx +79 -79
  32. package/src/Toggle/Toggle.tsx +48 -48
  33. package/src/Tooltip/Tooltip.tsx +38 -38
  34. package/src/TopNav/TopNav.tsx +1163 -1163
  35. package/src/TreeSelect/TreeSelect.tsx +825 -825
  36. package/src/css/alert.module.scss +93 -93
  37. package/src/css/autocomplete.module.scss +416 -416
  38. package/src/css/badge.module.scss +82 -82
  39. package/src/css/base/_color.scss +159 -159
  40. package/src/css/base/_theme.scss +237 -237
  41. package/src/css/base/_variables.scss +161 -161
  42. package/src/css/breadcrumb.module.scss +50 -50
  43. package/src/css/button.module.scss +385 -385
  44. package/src/css/card.module.scss +217 -217
  45. package/src/css/checkbox.module.scss +123 -123
  46. package/src/css/commandpalette.module.scss +211 -211
  47. package/src/css/container.module.scss +18 -18
  48. package/src/css/divider.module.scss +41 -41
  49. package/src/css/form.module.scss +245 -245
  50. package/src/css/imagecropper.module.scss +397 -397
  51. package/src/css/input.module.scss +89 -89
  52. package/src/css/modal.module.scss +105 -105
  53. package/src/css/nav.module.scss +494 -494
  54. package/src/css/notification.module.scss +691 -691
  55. package/src/css/pagination.module.scss +63 -63
  56. package/src/css/radio.module.scss +89 -89
  57. package/src/css/richtextarea.module.scss +307 -307
  58. package/src/css/select.module.scss +525 -525
  59. package/src/css/skeleton.module.scss +30 -30
  60. package/src/css/table.module.scss +386 -386
  61. package/src/css/tabs.module.scss +63 -63
  62. package/src/css/theme-toggle.module.scss +83 -83
  63. package/src/css/toggle.module.scss +54 -54
  64. package/src/css/tooltip.module.scss +97 -97
  65. package/src/css/topnav.module.scss +568 -568
  66. package/src/css/treeselect.module.scss +558 -558
  67. package/src/css/utilities/_borders.scss +111 -111
  68. package/src/css/utilities/_colors.scss +66 -66
  69. package/src/css/utilities/_effects.scss +216 -216
  70. package/src/css/utilities/_layout.scss +181 -181
  71. package/src/css/utilities/_position.scss +75 -75
  72. package/src/css/utilities/_sizing.scss +138 -138
  73. package/src/css/utilities/_spacing.scss +99 -99
  74. package/src/css/utilities/_typography.scss +121 -121
  75. package/src/css/utilities/index.scss +24 -24
  76. package/src/declarations.d.ts +6 -6
  77. package/src/index.ts +60 -60
@@ -1,568 +1,568 @@
1
- @use 'base/variables' as *;
2
- @use 'base/color' as *;
3
-
4
- // ---------------------------------------------------------------------------
5
- // Animations (token-aligned with modal.module.scss — see CLAUDE.md §0.2)
6
- // ---------------------------------------------------------------------------
7
-
8
- @keyframes topNavOverlayFadeIn {
9
- from { opacity: 0; }
10
- to { opacity: 1; }
11
- }
12
-
13
- @keyframes topNavDrawerSlideIn {
14
- from { transform: translateX(100%); }
15
- to { transform: translateX(0); }
16
- }
17
-
18
- @keyframes topNavDropdownFadeIn {
19
- from { opacity: 0; transform: translateY(-4px); }
20
- to { opacity: 1; transform: translateY(0); }
21
- }
22
-
23
- @keyframes topNavRise {
24
- from { opacity: 0; transform: translateY(0.5rem); }
25
- to { opacity: 1; transform: translateY(0); }
26
- }
27
-
28
- @keyframes topNavFade {
29
- from { opacity: 0; }
30
- to { opacity: 1; }
31
- }
32
-
33
- // ---------------------------------------------------------------------------
34
- // Root
35
- // ---------------------------------------------------------------------------
36
-
37
- .topNav {
38
- position: relative;
39
- width: 100%;
40
- font-family: $font-sans;
41
- background-color: $color-surface;
42
- border-bottom: 1px solid $color-border;
43
- }
44
-
45
- .topNavInner {
46
- display: flex;
47
- align-items: center;
48
- gap: 1.5rem;
49
- padding: 0 1rem;
50
- min-height: 3.25rem;
51
- }
52
-
53
- // ---------------------------------------------------------------------------
54
- // Brand
55
- // ---------------------------------------------------------------------------
56
-
57
- .topNavBrand {
58
- display: flex;
59
- align-items: center;
60
- flex-shrink: 0;
61
- font-size: $text-base;
62
- font-weight: 600;
63
- color: $color-text-primary;
64
- }
65
-
66
- // ---------------------------------------------------------------------------
67
- // Menu — horizontal on desktop
68
- // ---------------------------------------------------------------------------
69
-
70
- .topNavMenu {
71
- display: flex;
72
- align-items: center;
73
- list-style: none;
74
- margin: 0;
75
- padding: 0;
76
- gap: 0.125rem;
77
- flex: 1;
78
- min-width: 0;
79
- }
80
-
81
- .topNavItemRow {
82
- display: flex;
83
- align-items: stretch;
84
- }
85
-
86
- // ---------------------------------------------------------------------------
87
- // Menu — horizontal-scroll fallback (only when collapsed AND no Toggle present)
88
- // ---------------------------------------------------------------------------
89
-
90
- .topNavMenuScroll {
91
- overflow-x: auto;
92
- flex-wrap: nowrap;
93
- -webkit-overflow-scrolling: touch;
94
- scrollbar-width: none;
95
-
96
- &::-webkit-scrollbar { display: none; }
97
-
98
- > li { flex-shrink: 0; }
99
- }
100
-
101
- // ---------------------------------------------------------------------------
102
- // Item / Dropdown trigger (shared visual class)
103
- // ---------------------------------------------------------------------------
104
-
105
- .topNavItem {
106
- display: inline-flex;
107
- align-items: center;
108
- gap: 0.5rem;
109
- padding: 0.4375rem 0.75rem;
110
- min-height: 2rem;
111
- font-size: $text-sm;
112
- font-weight: 500;
113
- color: $color-text-secondary;
114
- background: transparent;
115
- border: none;
116
- border-radius: $radius-sm;
117
- cursor: pointer;
118
- white-space: nowrap;
119
- text-decoration: none;
120
- transition:
121
- background-color $transition-fast,
122
- color $transition-fast;
123
- font-family: inherit;
124
-
125
- &:hover {
126
- background-color: $color-surface-hover;
127
- color: $color-text-primary;
128
- }
129
-
130
- &:focus-visible {
131
- outline: 2px solid $evo-primary-focus;
132
- outline-offset: 2px;
133
- }
134
- }
135
-
136
- .topNavItemActive {
137
- background-color: color-mix(in srgb, $evo-primary-color 12%, transparent);
138
- color: $evo-primary-color;
139
-
140
- &:hover {
141
- background-color: color-mix(in srgb, $evo-primary-color 18%, transparent);
142
- color: $evo-primary-color;
143
- }
144
- }
145
-
146
- .topNavItemLabel {
147
- display: inline-block;
148
- }
149
-
150
- .topNavIcon {
151
- display: inline-flex;
152
- align-items: center;
153
- justify-content: center;
154
- flex-shrink: 0;
155
- width: 1.125rem;
156
- height: 1.125rem;
157
- font-size: 1rem;
158
- }
159
-
160
- // ---------------------------------------------------------------------------
161
- // Actions slot
162
- // ---------------------------------------------------------------------------
163
-
164
- .topNavActions {
165
- display: flex;
166
- align-items: center;
167
- gap: 0.5rem;
168
- margin-left: auto;
169
- flex-shrink: 0;
170
- }
171
-
172
- // ---------------------------------------------------------------------------
173
- // Search trigger (EvoTopNav.Search) — presentational ⌘K affordance
174
- // ---------------------------------------------------------------------------
175
-
176
- .topNavSearch {
177
- display: inline-flex;
178
- align-items: center;
179
- gap: 0.5rem;
180
- height: 2rem;
181
- min-width: 12rem;
182
- padding: 0 0.625rem;
183
- font-family: inherit;
184
- font-size: $text-sm;
185
- color: $color-text-muted;
186
- background-color: $color-surface-sunken;
187
- border: 1px solid $color-border;
188
- border-radius: $radius-sm;
189
- cursor: pointer;
190
- transition:
191
- background-color $transition-fast,
192
- border-color $transition-fast,
193
- color $transition-fast;
194
-
195
- &:hover {
196
- background-color: $color-surface-hover;
197
- color: $color-text-secondary;
198
- }
199
-
200
- &:focus-visible {
201
- outline: 2px solid $evo-primary-focus;
202
- outline-offset: 2px;
203
- }
204
- }
205
-
206
- .topNavSearchIcon {
207
- display: inline-flex;
208
- flex-shrink: 0;
209
- }
210
-
211
- .topNavSearchText {
212
- flex: 1;
213
- text-align: left;
214
- }
215
-
216
- .topNavSearchKbd {
217
- flex-shrink: 0;
218
- font-family: inherit;
219
- font-size: $text-xs;
220
- color: $color-text-muted;
221
- background-color: $color-surface;
222
- border: 1px solid $color-border;
223
- border-radius: 4px;
224
- padding: 0.0625rem 0.3125rem;
225
- }
226
-
227
- // ---------------------------------------------------------------------------
228
- // Toggle (hamburger) — hidden above the collapse breakpoint
229
- // ---------------------------------------------------------------------------
230
-
231
- .topNavToggle {
232
- display: none;
233
- align-items: center;
234
- justify-content: center;
235
- width: 2.75rem;
236
- height: 2.75rem;
237
- min-width: 2.75rem;
238
- min-height: 2.75rem;
239
- margin-left: 0.25rem;
240
- background: transparent;
241
- border: none;
242
- border-radius: $radius-sm;
243
- color: $color-text-primary;
244
- cursor: pointer;
245
- transition: background-color $transition-fast;
246
-
247
- &:hover {
248
- background-color: $color-surface-hover;
249
- }
250
-
251
- &:focus-visible {
252
- outline: 2px solid $evo-primary-focus;
253
- outline-offset: 2px;
254
- }
255
- }
256
-
257
- .toggleIcon {
258
- display: block;
259
- }
260
-
261
- // ---------------------------------------------------------------------------
262
- // Dropdown
263
- // ---------------------------------------------------------------------------
264
-
265
- .topNavDropdown {
266
- position: relative;
267
- display: flex;
268
- align-items: stretch;
269
- }
270
-
271
- .topNavDropdownTrigger {
272
- // inherits .topNavItem styles
273
- }
274
-
275
- .topNavDropdownChevron {
276
- transition: transform $transition-fast;
277
- margin-left: 0.125rem;
278
- opacity: 0.7;
279
- }
280
-
281
- .topNavDropdownChevronOpen {
282
- transform: rotate(180deg);
283
- }
284
-
285
- .topNavDropdownContent {
286
- position: absolute;
287
- top: calc(100% + 0.375rem);
288
- left: 0;
289
- min-width: 12rem;
290
- list-style: none;
291
- margin: 0;
292
- padding: 0.375rem;
293
- background-color: $color-surface-elevated;
294
- border: 1px solid $color-border;
295
- border-radius: $radius-md;
296
- box-shadow: $shadow-lg;
297
- z-index: 50;
298
- // Hidden by default — the .topNavDropdownContentOpen modifier flips display
299
- // to flex. Using display:none here keeps focus + tab order behaviour correct
300
- // and avoids the [hidden] / display:flex specificity conflict that left the
301
- // empty panel visible.
302
- display: none;
303
- flex-direction: column;
304
- gap: 0.125rem;
305
-
306
- > li {
307
- margin: 0;
308
- padding: 0;
309
- list-style: none;
310
- }
311
- }
312
-
313
- .topNavDropdownContentOpen {
314
- display: flex;
315
- animation: topNavDropdownFadeIn 140ms ease;
316
- }
317
-
318
- .topNavDropdownItem {
319
- display: flex;
320
- align-items: center;
321
- gap: 0.5rem;
322
- width: 100%;
323
- padding: 0.5rem 0.625rem;
324
- min-height: 2rem;
325
- font-size: $text-sm;
326
- font-weight: 500;
327
- color: $color-text-secondary;
328
- background: transparent;
329
- border: none;
330
- border-radius: $radius-sm;
331
- cursor: pointer;
332
- white-space: nowrap;
333
- text-align: left;
334
- text-decoration: none;
335
- transition: background-color $transition-fast, color $transition-fast;
336
- font-family: inherit;
337
-
338
- &:hover,
339
- &:focus-visible {
340
- background-color: $color-surface-hover;
341
- color: $color-text-primary;
342
- }
343
-
344
- &:focus-visible {
345
- outline: 2px solid $evo-primary-focus;
346
- outline-offset: 2px;
347
- }
348
- }
349
-
350
- // ---------------------------------------------------------------------------
351
- // Mobile drawer (≤ collapseBelow breakpoint)
352
- // ---------------------------------------------------------------------------
353
-
354
- @media (max-width: 767px) {
355
- .topNavInner {
356
- gap: 0.75rem;
357
- }
358
-
359
- .topNavToggle {
360
- display: inline-flex;
361
- }
362
-
363
- // Menu becomes the drawer panel (only when a Toggle is registered —
364
- // see .topNavMenuScroll for the no-Toggle fallback handled above).
365
- .topNavMenuDrawer {
366
- position: fixed;
367
- top: 0;
368
- right: 0;
369
- bottom: 0;
370
- width: min(20rem, 88vw);
371
- flex-direction: column;
372
- align-items: stretch;
373
- gap: 0.25rem;
374
- padding: 4rem 1rem 1.5rem;
375
- background-color: $color-surface-elevated;
376
- border-left: 1px solid $color-border;
377
- box-shadow: $shadow-2xl;
378
- overflow-y: auto;
379
- z-index: 60;
380
- animation: topNavDrawerSlideIn 220ms ease;
381
-
382
- // Items in the drawer take the full row and become taller for touch.
383
- .topNavItem,
384
- .topNavDropdownTrigger {
385
- width: 100%;
386
- justify-content: flex-start;
387
- min-height: 2.75rem;
388
- padding: 0.625rem 0.75rem;
389
- font-size: $text-base;
390
- }
391
- }
392
-
393
- // Drawer closed → keep it mounted (state preserved) but visually hidden
394
- // and removed from the a11y tree.
395
- .topNavMenuDrawerClosed {
396
- transform: translateX(100%);
397
- pointer-events: none;
398
- animation: none;
399
- visibility: hidden;
400
- }
401
-
402
- // Dropdowns inside the drawer flatten to an inline expandable section
403
- // (no floating panel — would clip on mobile).
404
- .topNavDropdownInDrawer {
405
- flex-direction: column;
406
- align-items: stretch;
407
-
408
- .topNavDropdownContent {
409
- position: static;
410
- box-shadow: none;
411
- border: none;
412
- background: transparent;
413
- padding: 0.125rem 0 0.125rem 1rem;
414
- min-width: 0;
415
- animation: none;
416
- }
417
- }
418
-
419
- .topNavBackdrop {
420
- position: fixed;
421
- inset: 0;
422
- background-color: $color-backdrop;
423
- -webkit-backdrop-filter: blur(2px);
424
- backdrop-filter: blur(2px);
425
- z-index: 55;
426
- animation: topNavOverlayFadeIn 180ms ease;
427
- }
428
-
429
- .topNavSearch {
430
- min-width: 0;
431
- width: 2.75rem;
432
- height: 2.75rem;
433
- justify-content: center;
434
- padding: 0;
435
-
436
- .topNavSearchText,
437
- .topNavSearchKbd { display: none; }
438
- }
439
- }
440
-
441
- // Above the breakpoint, drawer-specific bits never render visually.
442
- @media (min-width: 768px) {
443
- .topNavMenuDrawer,
444
- .topNavMenuDrawerClosed,
445
- .topNavBackdrop {
446
- // no-op: structural classes shouldn't leak desktop styles
447
- }
448
- }
449
-
450
- // ---------------------------------------------------------------------------
451
- // Entrance animation (opt-in via `entrance` prop → data-entrance attribute)
452
- // ---------------------------------------------------------------------------
453
-
454
- .topNav[data-entrance='rise'] .topNavBrand,
455
- .topNav[data-entrance='rise'] .topNavMenu > li,
456
- .topNav[data-entrance='rise'] .topNavSearch,
457
- .topNav[data-entrance='rise'] .topNavActions {
458
- animation: topNavRise 440ms cubic-bezier(0.22, 1, 0.36, 1) both;
459
- }
460
-
461
- .topNav[data-entrance='fade'] .topNavBrand,
462
- .topNav[data-entrance='fade'] .topNavMenu > li,
463
- .topNav[data-entrance='fade'] .topNavSearch,
464
- .topNav[data-entrance='fade'] .topNavActions {
465
- animation: topNavFade 440ms ease both;
466
- }
467
-
468
- // Shared left-to-right stagger (applies to both variants).
469
- .topNav[data-entrance] .topNavBrand { animation-delay: 40ms; }
470
- .topNav[data-entrance] .topNavMenu > li:nth-child(1) { animation-delay: 110ms; }
471
- .topNav[data-entrance] .topNavMenu > li:nth-child(2) { animation-delay: 160ms; }
472
- .topNav[data-entrance] .topNavMenu > li:nth-child(3) { animation-delay: 210ms; }
473
- .topNav[data-entrance] .topNavMenu > li:nth-child(4) { animation-delay: 260ms; }
474
- .topNav[data-entrance] .topNavMenu > li:nth-child(n + 5) { animation-delay: 300ms; }
475
- .topNav[data-entrance] .topNavSearch { animation-delay: 320ms; }
476
- .topNav[data-entrance] .topNavActions { animation-delay: 360ms; }
477
-
478
- // ---------------------------------------------------------------------------
479
- // Sticky + scroll-aware behavior (opt-in via `sticky` / `scrollBehavior`)
480
- // ---------------------------------------------------------------------------
481
-
482
- .topNavSticky {
483
- position: sticky;
484
- top: 0;
485
- z-index: 30;
486
- }
487
-
488
- .topNav[data-scroll] {
489
- transition:
490
- background-color $transition-fast,
491
- box-shadow $transition-fast,
492
- transform 220ms ease;
493
-
494
- .topNavInner { transition: min-height $transition-fast; }
495
- }
496
-
497
- .topNav[data-scrolled] {
498
- background-color: $color-surface; // fallback for browsers without color-mix()
499
- background-color: color-mix(in srgb, $color-surface 85%, transparent);
500
- -webkit-backdrop-filter: blur(10px);
501
- backdrop-filter: blur(10px);
502
- box-shadow: $shadow-md;
503
- }
504
-
505
- .topNav[data-scroll='shrink'][data-scrolled] .topNavInner {
506
- min-height: 2.75rem;
507
- }
508
-
509
- .topNav[data-scroll='hide'] { will-change: transform; }
510
- .topNav[data-scroll='hide'][data-hidden] { transform: translateY(-100%); }
511
-
512
- // ---------------------------------------------------------------------------
513
- // Scroll-progress line (opt-in via `showProgress`)
514
- // ---------------------------------------------------------------------------
515
-
516
- .topNavProgress {
517
- position: absolute;
518
- left: 0;
519
- bottom: 0;
520
- width: 100%;
521
- height: 2px;
522
- background: $evo-primary-color;
523
- transform: scaleX(var(--evo-topnav-progress, 0));
524
- transform-origin: left center;
525
- pointer-events: none;
526
- z-index: 31;
527
- }
528
-
529
- // ---------------------------------------------------------------------------
530
- // Reduced motion — kill animations
531
- // ---------------------------------------------------------------------------
532
-
533
- .topNavReducedMotion {
534
- .topNavMenuDrawer,
535
- .topNavDropdownContentOpen,
536
- .topNavBackdrop {
537
- animation: none;
538
- }
539
-
540
- .topNavDropdownChevron {
541
- transition: none;
542
- }
543
-
544
- .topNavBrand,
545
- .topNavMenu > li,
546
- .topNavSearch,
547
- .topNavActions {
548
- animation: none !important;
549
- }
550
-
551
- &[data-scroll='hide'][data-hidden] { transition: none; }
552
- .topNavProgress { transition: none; }
553
- }
554
-
555
- @media (prefers-reduced-motion: reduce) {
556
- .topNavMenuDrawer,
557
- .topNavDropdownContentOpen,
558
- .topNavBackdrop {
559
- animation: none;
560
- }
561
-
562
- .topNav[data-entrance] .topNavBrand,
563
- .topNav[data-entrance] .topNavMenu > li,
564
- .topNav[data-entrance] .topNavSearch,
565
- .topNav[data-entrance] .topNavActions {
566
- animation: none !important;
567
- }
568
- }
1
+ @use 'base/variables' as *;
2
+ @use 'base/color' as *;
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Animations (token-aligned with modal.module.scss — see CLAUDE.md §0.2)
6
+ // ---------------------------------------------------------------------------
7
+
8
+ @keyframes topNavOverlayFadeIn {
9
+ from { opacity: 0; }
10
+ to { opacity: 1; }
11
+ }
12
+
13
+ @keyframes topNavDrawerSlideIn {
14
+ from { transform: translateX(100%); }
15
+ to { transform: translateX(0); }
16
+ }
17
+
18
+ @keyframes topNavDropdownFadeIn {
19
+ from { opacity: 0; transform: translateY(-4px); }
20
+ to { opacity: 1; transform: translateY(0); }
21
+ }
22
+
23
+ @keyframes topNavRise {
24
+ from { opacity: 0; transform: translateY(0.5rem); }
25
+ to { opacity: 1; transform: translateY(0); }
26
+ }
27
+
28
+ @keyframes topNavFade {
29
+ from { opacity: 0; }
30
+ to { opacity: 1; }
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Root
35
+ // ---------------------------------------------------------------------------
36
+
37
+ .topNav {
38
+ position: relative;
39
+ width: 100%;
40
+ font-family: $font-sans;
41
+ background-color: $color-surface;
42
+ border-bottom: 1px solid $color-border;
43
+ }
44
+
45
+ .topNavInner {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 1.5rem;
49
+ padding: 0 1rem;
50
+ min-height: 3.25rem;
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Brand
55
+ // ---------------------------------------------------------------------------
56
+
57
+ .topNavBrand {
58
+ display: flex;
59
+ align-items: center;
60
+ flex-shrink: 0;
61
+ font-size: $text-base;
62
+ font-weight: 600;
63
+ color: $color-text-primary;
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Menu — horizontal on desktop
68
+ // ---------------------------------------------------------------------------
69
+
70
+ .topNavMenu {
71
+ display: flex;
72
+ align-items: center;
73
+ list-style: none;
74
+ margin: 0;
75
+ padding: 0;
76
+ gap: 0.125rem;
77
+ flex: 1;
78
+ min-width: 0;
79
+ }
80
+
81
+ .topNavItemRow {
82
+ display: flex;
83
+ align-items: stretch;
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Menu — horizontal-scroll fallback (only when collapsed AND no Toggle present)
88
+ // ---------------------------------------------------------------------------
89
+
90
+ .topNavMenuScroll {
91
+ overflow-x: auto;
92
+ flex-wrap: nowrap;
93
+ -webkit-overflow-scrolling: touch;
94
+ scrollbar-width: none;
95
+
96
+ &::-webkit-scrollbar { display: none; }
97
+
98
+ > li { flex-shrink: 0; }
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Item / Dropdown trigger (shared visual class)
103
+ // ---------------------------------------------------------------------------
104
+
105
+ .topNavItem {
106
+ display: inline-flex;
107
+ align-items: center;
108
+ gap: 0.5rem;
109
+ padding: 0.4375rem 0.75rem;
110
+ min-height: 2rem;
111
+ font-size: $text-sm;
112
+ font-weight: 500;
113
+ color: $color-text-secondary;
114
+ background: transparent;
115
+ border: none;
116
+ border-radius: $radius-sm;
117
+ cursor: pointer;
118
+ white-space: nowrap;
119
+ text-decoration: none;
120
+ transition:
121
+ background-color $transition-fast,
122
+ color $transition-fast;
123
+ font-family: inherit;
124
+
125
+ &:hover {
126
+ background-color: $color-surface-hover;
127
+ color: $color-text-primary;
128
+ }
129
+
130
+ &:focus-visible {
131
+ outline: 2px solid $evo-primary-focus;
132
+ outline-offset: 2px;
133
+ }
134
+ }
135
+
136
+ .topNavItemActive {
137
+ background-color: color-mix(in srgb, $evo-primary-color 12%, transparent);
138
+ color: $evo-primary-color;
139
+
140
+ &:hover {
141
+ background-color: color-mix(in srgb, $evo-primary-color 18%, transparent);
142
+ color: $evo-primary-color;
143
+ }
144
+ }
145
+
146
+ .topNavItemLabel {
147
+ display: inline-block;
148
+ }
149
+
150
+ .topNavIcon {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ flex-shrink: 0;
155
+ width: 1.125rem;
156
+ height: 1.125rem;
157
+ font-size: 1rem;
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Actions slot
162
+ // ---------------------------------------------------------------------------
163
+
164
+ .topNavActions {
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 0.5rem;
168
+ margin-left: auto;
169
+ flex-shrink: 0;
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Search trigger (EvoTopNav.Search) — presentational ⌘K affordance
174
+ // ---------------------------------------------------------------------------
175
+
176
+ .topNavSearch {
177
+ display: inline-flex;
178
+ align-items: center;
179
+ gap: 0.5rem;
180
+ height: 2rem;
181
+ min-width: 12rem;
182
+ padding: 0 0.625rem;
183
+ font-family: inherit;
184
+ font-size: $text-sm;
185
+ color: $color-text-muted;
186
+ background-color: $color-surface-sunken;
187
+ border: 1px solid $color-border;
188
+ border-radius: $radius-sm;
189
+ cursor: pointer;
190
+ transition:
191
+ background-color $transition-fast,
192
+ border-color $transition-fast,
193
+ color $transition-fast;
194
+
195
+ &:hover {
196
+ background-color: $color-surface-hover;
197
+ color: $color-text-secondary;
198
+ }
199
+
200
+ &:focus-visible {
201
+ outline: 2px solid $evo-primary-focus;
202
+ outline-offset: 2px;
203
+ }
204
+ }
205
+
206
+ .topNavSearchIcon {
207
+ display: inline-flex;
208
+ flex-shrink: 0;
209
+ }
210
+
211
+ .topNavSearchText {
212
+ flex: 1;
213
+ text-align: left;
214
+ }
215
+
216
+ .topNavSearchKbd {
217
+ flex-shrink: 0;
218
+ font-family: inherit;
219
+ font-size: $text-xs;
220
+ color: $color-text-muted;
221
+ background-color: $color-surface;
222
+ border: 1px solid $color-border;
223
+ border-radius: 4px;
224
+ padding: 0.0625rem 0.3125rem;
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Toggle (hamburger) — hidden above the collapse breakpoint
229
+ // ---------------------------------------------------------------------------
230
+
231
+ .topNavToggle {
232
+ display: none;
233
+ align-items: center;
234
+ justify-content: center;
235
+ width: 2.75rem;
236
+ height: 2.75rem;
237
+ min-width: 2.75rem;
238
+ min-height: 2.75rem;
239
+ margin-left: 0.25rem;
240
+ background: transparent;
241
+ border: none;
242
+ border-radius: $radius-sm;
243
+ color: $color-text-primary;
244
+ cursor: pointer;
245
+ transition: background-color $transition-fast;
246
+
247
+ &:hover {
248
+ background-color: $color-surface-hover;
249
+ }
250
+
251
+ &:focus-visible {
252
+ outline: 2px solid $evo-primary-focus;
253
+ outline-offset: 2px;
254
+ }
255
+ }
256
+
257
+ .toggleIcon {
258
+ display: block;
259
+ }
260
+
261
+ // ---------------------------------------------------------------------------
262
+ // Dropdown
263
+ // ---------------------------------------------------------------------------
264
+
265
+ .topNavDropdown {
266
+ position: relative;
267
+ display: flex;
268
+ align-items: stretch;
269
+ }
270
+
271
+ .topNavDropdownTrigger {
272
+ // inherits .topNavItem styles
273
+ }
274
+
275
+ .topNavDropdownChevron {
276
+ transition: transform $transition-fast;
277
+ margin-left: 0.125rem;
278
+ opacity: 0.7;
279
+ }
280
+
281
+ .topNavDropdownChevronOpen {
282
+ transform: rotate(180deg);
283
+ }
284
+
285
+ .topNavDropdownContent {
286
+ position: absolute;
287
+ top: calc(100% + 0.375rem);
288
+ left: 0;
289
+ min-width: 12rem;
290
+ list-style: none;
291
+ margin: 0;
292
+ padding: 0.375rem;
293
+ background-color: $color-surface-elevated;
294
+ border: 1px solid $color-border;
295
+ border-radius: $radius-md;
296
+ box-shadow: $shadow-lg;
297
+ z-index: 50;
298
+ // Hidden by default — the .topNavDropdownContentOpen modifier flips display
299
+ // to flex. Using display:none here keeps focus + tab order behaviour correct
300
+ // and avoids the [hidden] / display:flex specificity conflict that left the
301
+ // empty panel visible.
302
+ display: none;
303
+ flex-direction: column;
304
+ gap: 0.125rem;
305
+
306
+ > li {
307
+ margin: 0;
308
+ padding: 0;
309
+ list-style: none;
310
+ }
311
+ }
312
+
313
+ .topNavDropdownContentOpen {
314
+ display: flex;
315
+ animation: topNavDropdownFadeIn 140ms ease;
316
+ }
317
+
318
+ .topNavDropdownItem {
319
+ display: flex;
320
+ align-items: center;
321
+ gap: 0.5rem;
322
+ width: 100%;
323
+ padding: 0.5rem 0.625rem;
324
+ min-height: 2rem;
325
+ font-size: $text-sm;
326
+ font-weight: 500;
327
+ color: $color-text-secondary;
328
+ background: transparent;
329
+ border: none;
330
+ border-radius: $radius-sm;
331
+ cursor: pointer;
332
+ white-space: nowrap;
333
+ text-align: left;
334
+ text-decoration: none;
335
+ transition: background-color $transition-fast, color $transition-fast;
336
+ font-family: inherit;
337
+
338
+ &:hover,
339
+ &:focus-visible {
340
+ background-color: $color-surface-hover;
341
+ color: $color-text-primary;
342
+ }
343
+
344
+ &:focus-visible {
345
+ outline: 2px solid $evo-primary-focus;
346
+ outline-offset: 2px;
347
+ }
348
+ }
349
+
350
+ // ---------------------------------------------------------------------------
351
+ // Mobile drawer (≤ collapseBelow breakpoint)
352
+ // ---------------------------------------------------------------------------
353
+
354
+ @media (max-width: 767px) {
355
+ .topNavInner {
356
+ gap: 0.75rem;
357
+ }
358
+
359
+ .topNavToggle {
360
+ display: inline-flex;
361
+ }
362
+
363
+ // Menu becomes the drawer panel (only when a Toggle is registered —
364
+ // see .topNavMenuScroll for the no-Toggle fallback handled above).
365
+ .topNavMenuDrawer {
366
+ position: fixed;
367
+ top: 0;
368
+ right: 0;
369
+ bottom: 0;
370
+ width: min(20rem, 88vw);
371
+ flex-direction: column;
372
+ align-items: stretch;
373
+ gap: 0.25rem;
374
+ padding: 4rem 1rem 1.5rem;
375
+ background-color: $color-surface-elevated;
376
+ border-left: 1px solid $color-border;
377
+ box-shadow: $shadow-2xl;
378
+ overflow-y: auto;
379
+ z-index: 60;
380
+ animation: topNavDrawerSlideIn 220ms ease;
381
+
382
+ // Items in the drawer take the full row and become taller for touch.
383
+ .topNavItem,
384
+ .topNavDropdownTrigger {
385
+ width: 100%;
386
+ justify-content: flex-start;
387
+ min-height: 2.75rem;
388
+ padding: 0.625rem 0.75rem;
389
+ font-size: $text-base;
390
+ }
391
+ }
392
+
393
+ // Drawer closed → keep it mounted (state preserved) but visually hidden
394
+ // and removed from the a11y tree.
395
+ .topNavMenuDrawerClosed {
396
+ transform: translateX(100%);
397
+ pointer-events: none;
398
+ animation: none;
399
+ visibility: hidden;
400
+ }
401
+
402
+ // Dropdowns inside the drawer flatten to an inline expandable section
403
+ // (no floating panel — would clip on mobile).
404
+ .topNavDropdownInDrawer {
405
+ flex-direction: column;
406
+ align-items: stretch;
407
+
408
+ .topNavDropdownContent {
409
+ position: static;
410
+ box-shadow: none;
411
+ border: none;
412
+ background: transparent;
413
+ padding: 0.125rem 0 0.125rem 1rem;
414
+ min-width: 0;
415
+ animation: none;
416
+ }
417
+ }
418
+
419
+ .topNavBackdrop {
420
+ position: fixed;
421
+ inset: 0;
422
+ background-color: $color-backdrop;
423
+ -webkit-backdrop-filter: blur(2px);
424
+ backdrop-filter: blur(2px);
425
+ z-index: 55;
426
+ animation: topNavOverlayFadeIn 180ms ease;
427
+ }
428
+
429
+ .topNavSearch {
430
+ min-width: 0;
431
+ width: 2.75rem;
432
+ height: 2.75rem;
433
+ justify-content: center;
434
+ padding: 0;
435
+
436
+ .topNavSearchText,
437
+ .topNavSearchKbd { display: none; }
438
+ }
439
+ }
440
+
441
+ // Above the breakpoint, drawer-specific bits never render visually.
442
+ @media (min-width: 768px) {
443
+ .topNavMenuDrawer,
444
+ .topNavMenuDrawerClosed,
445
+ .topNavBackdrop {
446
+ // no-op: structural classes shouldn't leak desktop styles
447
+ }
448
+ }
449
+
450
+ // ---------------------------------------------------------------------------
451
+ // Entrance animation (opt-in via `entrance` prop → data-entrance attribute)
452
+ // ---------------------------------------------------------------------------
453
+
454
+ .topNav[data-entrance='rise'] .topNavBrand,
455
+ .topNav[data-entrance='rise'] .topNavMenu > li,
456
+ .topNav[data-entrance='rise'] .topNavSearch,
457
+ .topNav[data-entrance='rise'] .topNavActions {
458
+ animation: topNavRise 440ms cubic-bezier(0.22, 1, 0.36, 1) both;
459
+ }
460
+
461
+ .topNav[data-entrance='fade'] .topNavBrand,
462
+ .topNav[data-entrance='fade'] .topNavMenu > li,
463
+ .topNav[data-entrance='fade'] .topNavSearch,
464
+ .topNav[data-entrance='fade'] .topNavActions {
465
+ animation: topNavFade 440ms ease both;
466
+ }
467
+
468
+ // Shared left-to-right stagger (applies to both variants).
469
+ .topNav[data-entrance] .topNavBrand { animation-delay: 40ms; }
470
+ .topNav[data-entrance] .topNavMenu > li:nth-child(1) { animation-delay: 110ms; }
471
+ .topNav[data-entrance] .topNavMenu > li:nth-child(2) { animation-delay: 160ms; }
472
+ .topNav[data-entrance] .topNavMenu > li:nth-child(3) { animation-delay: 210ms; }
473
+ .topNav[data-entrance] .topNavMenu > li:nth-child(4) { animation-delay: 260ms; }
474
+ .topNav[data-entrance] .topNavMenu > li:nth-child(n + 5) { animation-delay: 300ms; }
475
+ .topNav[data-entrance] .topNavSearch { animation-delay: 320ms; }
476
+ .topNav[data-entrance] .topNavActions { animation-delay: 360ms; }
477
+
478
+ // ---------------------------------------------------------------------------
479
+ // Sticky + scroll-aware behavior (opt-in via `sticky` / `scrollBehavior`)
480
+ // ---------------------------------------------------------------------------
481
+
482
+ .topNavSticky {
483
+ position: sticky;
484
+ top: 0;
485
+ z-index: 30;
486
+ }
487
+
488
+ .topNav[data-scroll] {
489
+ transition:
490
+ background-color $transition-fast,
491
+ box-shadow $transition-fast,
492
+ transform 220ms ease;
493
+
494
+ .topNavInner { transition: min-height $transition-fast; }
495
+ }
496
+
497
+ .topNav[data-scrolled] {
498
+ background-color: $color-surface; // fallback for browsers without color-mix()
499
+ background-color: color-mix(in srgb, $color-surface 85%, transparent);
500
+ -webkit-backdrop-filter: blur(10px);
501
+ backdrop-filter: blur(10px);
502
+ box-shadow: $shadow-md;
503
+ }
504
+
505
+ .topNav[data-scroll='shrink'][data-scrolled] .topNavInner {
506
+ min-height: 2.75rem;
507
+ }
508
+
509
+ .topNav[data-scroll='hide'] { will-change: transform; }
510
+ .topNav[data-scroll='hide'][data-hidden] { transform: translateY(-100%); }
511
+
512
+ // ---------------------------------------------------------------------------
513
+ // Scroll-progress line (opt-in via `showProgress`)
514
+ // ---------------------------------------------------------------------------
515
+
516
+ .topNavProgress {
517
+ position: absolute;
518
+ left: 0;
519
+ bottom: 0;
520
+ width: 100%;
521
+ height: 2px;
522
+ background: $evo-primary-color;
523
+ transform: scaleX(var(--evo-topnav-progress, 0));
524
+ transform-origin: left center;
525
+ pointer-events: none;
526
+ z-index: 31;
527
+ }
528
+
529
+ // ---------------------------------------------------------------------------
530
+ // Reduced motion — kill animations
531
+ // ---------------------------------------------------------------------------
532
+
533
+ .topNavReducedMotion {
534
+ .topNavMenuDrawer,
535
+ .topNavDropdownContentOpen,
536
+ .topNavBackdrop {
537
+ animation: none;
538
+ }
539
+
540
+ .topNavDropdownChevron {
541
+ transition: none;
542
+ }
543
+
544
+ .topNavBrand,
545
+ .topNavMenu > li,
546
+ .topNavSearch,
547
+ .topNavActions {
548
+ animation: none !important;
549
+ }
550
+
551
+ &[data-scroll='hide'][data-hidden] { transition: none; }
552
+ .topNavProgress { transition: none; }
553
+ }
554
+
555
+ @media (prefers-reduced-motion: reduce) {
556
+ .topNavMenuDrawer,
557
+ .topNavDropdownContentOpen,
558
+ .topNavBackdrop {
559
+ animation: none;
560
+ }
561
+
562
+ .topNav[data-entrance] .topNavBrand,
563
+ .topNav[data-entrance] .topNavMenu > li,
564
+ .topNav[data-entrance] .topNavSearch,
565
+ .topNav[data-entrance] .topNavActions {
566
+ animation: none !important;
567
+ }
568
+ }