@justin_evo/evo-ui 1.1.0 → 1.2.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/LICENSE +21 -21
- package/README.md +70 -70
- package/dist/TopNav/TopNav.d.ts +19 -0
- package/dist/evo-ui.css +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +3301 -3197
- package/package.json +1 -1
- package/src/RichTextArea/RichTextArea.tsx +20 -3
- package/src/TopNav/TopNav.tsx +169 -0
- package/src/css/checkbox.module.scss +8 -5
- package/src/css/topnav.module.scss +172 -0
package/package.json
CHANGED
|
@@ -502,9 +502,26 @@ export const EvoRichTextArea = forwardRef<EvoRichTextHandle, EvoRichTextAreaProp
|
|
|
502
502
|
|
|
503
503
|
// ---- Insert image (used by paste, drop, button) ----
|
|
504
504
|
const insertImageAtCaret = useCallback((src: string, alt = '') => {
|
|
505
|
-
editorRef.current
|
|
506
|
-
|
|
507
|
-
|
|
505
|
+
const el = editorRef.current;
|
|
506
|
+
if (!el) return;
|
|
507
|
+
el.focus();
|
|
508
|
+
const safeSrc = src.replace(/"/g, '"');
|
|
509
|
+
const safeAlt = alt.replace(/"/g, '"');
|
|
510
|
+
// Images render display:block; inserting a bare <img> leaves the caret
|
|
511
|
+
// beside it, which paints at the image's top-right edge. Drop a trailing
|
|
512
|
+
// empty paragraph and move the caret into it, so the user lands on a clean
|
|
513
|
+
// new line *below* the image. (Marker idiom matches unwrapBlocks above.)
|
|
514
|
+
execCommand('insertHTML', `<img src="${safeSrc}" alt="${safeAlt}" /><p data-evo-caret><br></p>`);
|
|
515
|
+
const landing = el.querySelector<HTMLParagraphElement>('p[data-evo-caret]');
|
|
516
|
+
if (landing) {
|
|
517
|
+
landing.removeAttribute('data-evo-caret');
|
|
518
|
+
const sel = window.getSelection();
|
|
519
|
+
const r = document.createRange();
|
|
520
|
+
r.setStart(landing, 0);
|
|
521
|
+
r.collapse(true);
|
|
522
|
+
sel?.removeAllRanges();
|
|
523
|
+
sel?.addRange(r);
|
|
524
|
+
}
|
|
508
525
|
emitChange();
|
|
509
526
|
}, [emitChange]);
|
|
510
527
|
|
package/src/TopNav/TopNav.tsx
CHANGED
|
@@ -32,6 +32,14 @@ export interface EvoTopNavProps
|
|
|
32
32
|
onOpenChange?: (open: boolean) => void;
|
|
33
33
|
/** Width in px below which Menu collapses into the drawer. @default 768 */
|
|
34
34
|
collapseBelow?: number;
|
|
35
|
+
/** Staggered mount animation for the bar's contents. @default 'none' */
|
|
36
|
+
entrance?: 'none' | 'rise' | 'fade';
|
|
37
|
+
/** Pin the bar with position: sticky; top: 0. @default false */
|
|
38
|
+
sticky?: boolean;
|
|
39
|
+
/** On-scroll treatment of a sticky bar. @default 'none' */
|
|
40
|
+
scrollBehavior?: 'none' | 'elevate' | 'shrink' | 'hide';
|
|
41
|
+
/** Render a thin scroll-progress accent line along the bottom edge. @default false */
|
|
42
|
+
showProgress?: boolean;
|
|
35
43
|
className?: string;
|
|
36
44
|
}
|
|
37
45
|
|
|
@@ -74,6 +82,17 @@ export interface EvoTopNavToggleProps
|
|
|
74
82
|
className?: string;
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
export interface EvoTopNavSearchProps
|
|
86
|
+
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
|
87
|
+
/** Placeholder text shown inside the trigger. @default 'Search…' */
|
|
88
|
+
placeholder?: string;
|
|
89
|
+
/** Opt-in global hotkey, e.g. 'mod+k' (mod = ⌘ on macOS, Ctrl elsewhere). Default: none. */
|
|
90
|
+
shortcut?: string;
|
|
91
|
+
/** Override the kbd hint. @default platform-aware ⌘K / Ctrl K */
|
|
92
|
+
shortcutHint?: React.ReactNode;
|
|
93
|
+
className?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
77
96
|
export interface EvoTopNavDropdownProps {
|
|
78
97
|
label: React.ReactNode;
|
|
79
98
|
icon?: React.ReactNode;
|
|
@@ -159,6 +178,53 @@ const useHoverCapable = () =>
|
|
|
159
178
|
const usePrefersReducedMotion = () =>
|
|
160
179
|
useMediaQuery('(prefers-reduced-motion: reduce)');
|
|
161
180
|
|
|
181
|
+
function useScrollState(
|
|
182
|
+
enabled: boolean,
|
|
183
|
+
behavior: 'none' | 'elevate' | 'shrink' | 'hide',
|
|
184
|
+
wantProgress: boolean,
|
|
185
|
+
) {
|
|
186
|
+
const [scrolled, setScrolled] = useState(false);
|
|
187
|
+
const [hidden, setHidden] = useState(false);
|
|
188
|
+
const [progress, setProgress] = useState(0);
|
|
189
|
+
const lastY = useRef(0);
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (typeof window === 'undefined' || !enabled) {
|
|
193
|
+
setScrolled(false);
|
|
194
|
+
setHidden(false);
|
|
195
|
+
setProgress(0);
|
|
196
|
+
lastY.current = 0;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
let raf = 0;
|
|
200
|
+
const read = () => {
|
|
201
|
+
raf = 0;
|
|
202
|
+
const doc = document.documentElement;
|
|
203
|
+
const y = window.scrollY || doc.scrollTop || 0;
|
|
204
|
+
setScrolled(y > 8);
|
|
205
|
+
setHidden(behavior === 'hide' ? y > lastY.current && y > 64 : false);
|
|
206
|
+
if (wantProgress) {
|
|
207
|
+
const max = doc.scrollHeight - doc.clientHeight || 1;
|
|
208
|
+
setProgress(Math.min(1, Math.max(0, y / max)));
|
|
209
|
+
}
|
|
210
|
+
lastY.current = y;
|
|
211
|
+
};
|
|
212
|
+
const onScroll = () => {
|
|
213
|
+
if (!raf) raf = requestAnimationFrame(read);
|
|
214
|
+
};
|
|
215
|
+
read();
|
|
216
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
217
|
+
window.addEventListener('resize', onScroll, { passive: true });
|
|
218
|
+
return () => {
|
|
219
|
+
if (raf) cancelAnimationFrame(raf);
|
|
220
|
+
window.removeEventListener('scroll', onScroll);
|
|
221
|
+
window.removeEventListener('resize', onScroll);
|
|
222
|
+
};
|
|
223
|
+
}, [enabled, behavior, wantProgress]);
|
|
224
|
+
|
|
225
|
+
return { scrolled, hidden, progress };
|
|
226
|
+
}
|
|
227
|
+
|
|
162
228
|
function getFocusable(root: HTMLElement | null) {
|
|
163
229
|
if (!root) return [] as HTMLElement[];
|
|
164
230
|
return Array.from(
|
|
@@ -318,6 +384,23 @@ const ChevronIcon = ({ open }: { open: boolean }) => (
|
|
|
318
384
|
</svg>
|
|
319
385
|
);
|
|
320
386
|
|
|
387
|
+
const SearchGlyph = () => (
|
|
388
|
+
<svg
|
|
389
|
+
width="16"
|
|
390
|
+
height="16"
|
|
391
|
+
viewBox="0 0 24 24"
|
|
392
|
+
fill="none"
|
|
393
|
+
stroke="currentColor"
|
|
394
|
+
strokeWidth="2"
|
|
395
|
+
strokeLinecap="round"
|
|
396
|
+
strokeLinejoin="round"
|
|
397
|
+
aria-hidden="true"
|
|
398
|
+
>
|
|
399
|
+
<circle cx="11" cy="11" r="7" />
|
|
400
|
+
<path d="m20 20-3.2-3.2" />
|
|
401
|
+
</svg>
|
|
402
|
+
);
|
|
403
|
+
|
|
321
404
|
// ============================================================================
|
|
322
405
|
// EvoTopNav.Brand
|
|
323
406
|
// ============================================================================
|
|
@@ -522,6 +605,69 @@ const EvoTopNavToggle = forwardRef<HTMLButtonElement, EvoTopNavToggleProps>(
|
|
|
522
605
|
},
|
|
523
606
|
);
|
|
524
607
|
|
|
608
|
+
// ============================================================================
|
|
609
|
+
// EvoTopNav.Search — presentational ⌘K quick-search trigger
|
|
610
|
+
// ============================================================================
|
|
611
|
+
|
|
612
|
+
const EvoTopNavSearch = forwardRef<HTMLButtonElement, EvoTopNavSearchProps>(
|
|
613
|
+
function EvoTopNavSearch(
|
|
614
|
+
{ placeholder = 'Search…', shortcut, shortcutHint, className, onClick, ...rest },
|
|
615
|
+
forwardedRef,
|
|
616
|
+
) {
|
|
617
|
+
const localRef = useRef<HTMLButtonElement | null>(null);
|
|
618
|
+
const setRef = (node: HTMLButtonElement | null) => {
|
|
619
|
+
localRef.current = node;
|
|
620
|
+
if (typeof forwardedRef === 'function') forwardedRef(node);
|
|
621
|
+
else if (forwardedRef)
|
|
622
|
+
(forwardedRef as React.RefObject<HTMLButtonElement | null>).current = node;
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
// Platform-aware hint resolved after mount to avoid SSR hydration mismatch.
|
|
626
|
+
const [autoHint, setAutoHint] = useState<React.ReactNode>(null);
|
|
627
|
+
useEffect(() => {
|
|
628
|
+
if (shortcutHint !== undefined) return;
|
|
629
|
+
const platform =
|
|
630
|
+
(typeof navigator !== 'undefined' &&
|
|
631
|
+
(navigator.platform || navigator.userAgent)) || '';
|
|
632
|
+
setAutoHint(/Mac|iPhone|iPad|iPod/.test(platform) ? '⌘K' : 'Ctrl K');
|
|
633
|
+
}, [shortcutHint]);
|
|
634
|
+
const hint = shortcutHint !== undefined ? shortcutHint : autoHint;
|
|
635
|
+
|
|
636
|
+
// Opt-in global hotkey → dispatch a real click so onClick fires naturally.
|
|
637
|
+
useEffect(() => {
|
|
638
|
+
if (!shortcut) return;
|
|
639
|
+
const parts = shortcut.toLowerCase().split('+').map((p) => p.trim());
|
|
640
|
+
const wantMod = parts.some((p) => ['mod', 'cmd', 'meta', 'ctrl', 'control'].includes(p));
|
|
641
|
+
const key = parts[parts.length - 1];
|
|
642
|
+
const handler = (e: KeyboardEvent) => {
|
|
643
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
644
|
+
if ((!wantMod || mod) && e.key.toLowerCase() === key) {
|
|
645
|
+
e.preventDefault();
|
|
646
|
+
localRef.current?.click();
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
document.addEventListener('keydown', handler);
|
|
650
|
+
return () => document.removeEventListener('keydown', handler);
|
|
651
|
+
}, [shortcut]);
|
|
652
|
+
|
|
653
|
+
return (
|
|
654
|
+
<button
|
|
655
|
+
ref={setRef}
|
|
656
|
+
className={cn(styles.topNavSearch, className)}
|
|
657
|
+
onClick={onClick}
|
|
658
|
+
{...rest}
|
|
659
|
+
type="button"
|
|
660
|
+
>
|
|
661
|
+
<span className={styles.topNavSearchIcon} aria-hidden="true">
|
|
662
|
+
<SearchGlyph />
|
|
663
|
+
</span>
|
|
664
|
+
<span className={styles.topNavSearchText}>{placeholder}</span>
|
|
665
|
+
{hint != null && <kbd className={styles.topNavSearchKbd}>{hint}</kbd>}
|
|
666
|
+
</button>
|
|
667
|
+
);
|
|
668
|
+
},
|
|
669
|
+
);
|
|
670
|
+
|
|
525
671
|
// ============================================================================
|
|
526
672
|
// EvoTopNav.Dropdown
|
|
527
673
|
// ============================================================================
|
|
@@ -812,7 +958,12 @@ export const EvoTopNav = forwardRef<HTMLElement, EvoTopNavProps>(
|
|
|
812
958
|
defaultOpen = false,
|
|
813
959
|
onOpenChange,
|
|
814
960
|
collapseBelow = 768,
|
|
961
|
+
entrance = 'none',
|
|
962
|
+
sticky = false,
|
|
963
|
+
scrollBehavior = 'none',
|
|
964
|
+
showProgress = false,
|
|
815
965
|
className,
|
|
966
|
+
style,
|
|
816
967
|
...rest
|
|
817
968
|
},
|
|
818
969
|
ref,
|
|
@@ -825,6 +976,12 @@ export const EvoTopNav = forwardRef<HTMLElement, EvoTopNavProps>(
|
|
|
825
976
|
|
|
826
977
|
const isCollapsed = useIsCollapsed(collapseBelow);
|
|
827
978
|
const reducedMotion = usePrefersReducedMotion();
|
|
979
|
+
const scrollEnabled = scrollBehavior !== 'none' || showProgress;
|
|
980
|
+
const { scrolled, hidden, progress } = useScrollState(scrollEnabled, scrollBehavior, showProgress);
|
|
981
|
+
const animateEntrance = entrance !== 'none' && !reducedMotion;
|
|
982
|
+
const mergedStyle: React.CSSProperties = showProgress
|
|
983
|
+
? ({ ...style, ['--evo-topnav-progress' as string]: progress } as React.CSSProperties)
|
|
984
|
+
: (style as React.CSSProperties);
|
|
828
985
|
|
|
829
986
|
const [toggleCount, setToggleCount] = useState(0);
|
|
830
987
|
const registerToggle = useCallback(() => {
|
|
@@ -944,15 +1101,24 @@ export const EvoTopNav = forwardRef<HTMLElement, EvoTopNavProps>(
|
|
|
944
1101
|
ref={ref}
|
|
945
1102
|
className={cn(
|
|
946
1103
|
styles.topNav,
|
|
1104
|
+
sticky && styles.topNavSticky,
|
|
947
1105
|
drawerActive && styles.topNavDrawerOpen,
|
|
948
1106
|
reducedMotion && styles.topNavReducedMotion,
|
|
949
1107
|
className,
|
|
950
1108
|
)}
|
|
1109
|
+
style={mergedStyle}
|
|
951
1110
|
data-collapsed={isCollapsed || undefined}
|
|
952
1111
|
data-drawer-open={drawerActive || undefined}
|
|
1112
|
+
data-entrance={animateEntrance ? entrance : undefined}
|
|
1113
|
+
data-scroll={scrollBehavior !== 'none' ? scrollBehavior : undefined}
|
|
1114
|
+
data-scrolled={scrolled || undefined}
|
|
1115
|
+
data-hidden={hidden || undefined}
|
|
953
1116
|
{...rest}
|
|
954
1117
|
>
|
|
955
1118
|
<div className={styles.topNavInner}>{children}</div>
|
|
1119
|
+
{showProgress && (
|
|
1120
|
+
<span className={styles.topNavProgress} aria-hidden="true" />
|
|
1121
|
+
)}
|
|
956
1122
|
{drawerActive && (
|
|
957
1123
|
<div
|
|
958
1124
|
className={styles.topNavBackdrop}
|
|
@@ -972,6 +1138,7 @@ export const EvoTopNav = forwardRef<HTMLElement, EvoTopNavProps>(
|
|
|
972
1138
|
Item: typeof EvoTopNavItem;
|
|
973
1139
|
Actions: typeof EvoTopNavActions;
|
|
974
1140
|
Toggle: typeof EvoTopNavToggle;
|
|
1141
|
+
Search: typeof EvoTopNavSearch;
|
|
975
1142
|
Dropdown: typeof EvoTopNavDropdown;
|
|
976
1143
|
DropdownItem: typeof EvoTopNavDropdownItem;
|
|
977
1144
|
};
|
|
@@ -981,6 +1148,7 @@ EvoTopNavMenu.displayName = 'EvoTopNav.Menu';
|
|
|
981
1148
|
EvoTopNavItem.displayName = 'EvoTopNav.Item';
|
|
982
1149
|
EvoTopNavActions.displayName = 'EvoTopNav.Actions';
|
|
983
1150
|
EvoTopNavToggle.displayName = 'EvoTopNav.Toggle';
|
|
1151
|
+
EvoTopNavSearch.displayName = 'EvoTopNav.Search';
|
|
984
1152
|
EvoTopNavDropdown.displayName = 'EvoTopNav.Dropdown';
|
|
985
1153
|
EvoTopNavDropdownItem.displayName = 'EvoTopNav.DropdownItem';
|
|
986
1154
|
(EvoTopNav as { displayName?: string }).displayName = 'EvoTopNav';
|
|
@@ -990,5 +1158,6 @@ EvoTopNav.Menu = EvoTopNavMenu;
|
|
|
990
1158
|
EvoTopNav.Item = EvoTopNavItem;
|
|
991
1159
|
EvoTopNav.Actions = EvoTopNavActions;
|
|
992
1160
|
EvoTopNav.Toggle = EvoTopNavToggle;
|
|
1161
|
+
EvoTopNav.Search = EvoTopNavSearch;
|
|
993
1162
|
EvoTopNav.Dropdown = EvoTopNavDropdown;
|
|
994
1163
|
EvoTopNav.DropdownItem = EvoTopNavDropdownItem;
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
|
|
43
43
|
&::after {
|
|
44
44
|
opacity: 1;
|
|
45
|
-
transform: rotate(45deg) scale(1);
|
|
45
|
+
transform: translate(-50%, -60%) rotate(45deg) scale(1);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -94,15 +94,18 @@
|
|
|
94
94
|
&::after {
|
|
95
95
|
content: '';
|
|
96
96
|
position: absolute;
|
|
97
|
-
top:
|
|
98
|
-
left:
|
|
97
|
+
top: 50%;
|
|
98
|
+
left: 50%;
|
|
99
99
|
width: 5px;
|
|
100
|
-
height:
|
|
100
|
+
height: 9px;
|
|
101
101
|
border: 2px solid $evo-primary-fg;
|
|
102
102
|
border-top: none;
|
|
103
103
|
border-left: none;
|
|
104
104
|
opacity: 0;
|
|
105
|
-
|
|
105
|
+
// Centered via translate (parent's flex-centering can't reach an absolute
|
|
106
|
+
// child). -60% Y nudges the rotated "L" optically into the middle. The
|
|
107
|
+
// centering translate is folded into both states so the scale stays put.
|
|
108
|
+
transform: translate(-50%, -60%) rotate(45deg) scale(0.5);
|
|
106
109
|
transition: opacity $transition-fast, transform $transition-fast;
|
|
107
110
|
}
|
|
108
111
|
}
|
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
to { opacity: 1; transform: translateY(0); }
|
|
21
21
|
}
|
|
22
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
|
+
|
|
23
33
|
// ---------------------------------------------------------------------------
|
|
24
34
|
// Root
|
|
25
35
|
// ---------------------------------------------------------------------------
|
|
@@ -159,6 +169,61 @@
|
|
|
159
169
|
flex-shrink: 0;
|
|
160
170
|
}
|
|
161
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
|
+
|
|
162
227
|
// ---------------------------------------------------------------------------
|
|
163
228
|
// Toggle (hamburger) — hidden above the collapse breakpoint
|
|
164
229
|
// ---------------------------------------------------------------------------
|
|
@@ -360,6 +425,17 @@
|
|
|
360
425
|
z-index: 55;
|
|
361
426
|
animation: topNavOverlayFadeIn 180ms ease;
|
|
362
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
|
+
}
|
|
363
439
|
}
|
|
364
440
|
|
|
365
441
|
// Above the breakpoint, drawer-specific bits never render visually.
|
|
@@ -371,6 +447,85 @@
|
|
|
371
447
|
}
|
|
372
448
|
}
|
|
373
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
|
+
|
|
374
529
|
// ---------------------------------------------------------------------------
|
|
375
530
|
// Reduced motion — kill animations
|
|
376
531
|
// ---------------------------------------------------------------------------
|
|
@@ -385,6 +540,16 @@
|
|
|
385
540
|
.topNavDropdownChevron {
|
|
386
541
|
transition: none;
|
|
387
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; }
|
|
388
553
|
}
|
|
389
554
|
|
|
390
555
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -393,4 +558,11 @@
|
|
|
393
558
|
.topNavBackdrop {
|
|
394
559
|
animation: none;
|
|
395
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
|
+
}
|
|
396
568
|
}
|