@dxlbnl/ui 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/README.md +94 -0
- package/dist/components/cards/Card.stories.svelte +82 -0
- package/dist/components/cards/Card.stories.svelte.d.ts +19 -0
- package/dist/components/cards/Card.svelte +28 -0
- package/dist/components/cards/Card.svelte.d.ts +12 -0
- package/dist/components/cards/NoteCard.stories.svelte +94 -0
- package/dist/components/cards/NoteCard.stories.svelte.d.ts +19 -0
- package/dist/components/cards/NoteCard.svelte +89 -0
- package/dist/components/cards/NoteCard.svelte.d.ts +18 -0
- package/dist/components/cards/ProductCard.stories.svelte +98 -0
- package/dist/components/cards/ProductCard.stories.svelte.d.ts +19 -0
- package/dist/components/cards/ProductCard.svelte +150 -0
- package/dist/components/cards/ProductCard.svelte.d.ts +22 -0
- package/dist/components/cards/ProjectCard.stories.svelte +88 -0
- package/dist/components/cards/ProjectCard.stories.svelte.d.ts +19 -0
- package/dist/components/cards/ProjectCard.svelte +109 -0
- package/dist/components/cards/ProjectCard.svelte.d.ts +20 -0
- package/dist/components/cards/index.d.ts +4 -0
- package/dist/components/cards/index.js +4 -0
- package/dist/components/data/Accordion.stories.svelte +316 -0
- package/dist/components/data/Accordion.stories.svelte.d.ts +19 -0
- package/dist/components/data/Accordion.svelte +23 -0
- package/dist/components/data/Accordion.svelte.d.ts +9 -0
- package/dist/components/data/AccordionItem.svelte +112 -0
- package/dist/components/data/AccordionItem.svelte.d.ts +11 -0
- package/dist/components/data/Table.composition.stories.svelte +67 -0
- package/dist/components/data/Table.composition.stories.svelte.d.ts +19 -0
- package/dist/components/data/Table.stories.svelte +137 -0
- package/dist/components/data/Table.stories.svelte.d.ts +19 -0
- package/dist/components/data/Table.svelte +83 -0
- package/dist/components/data/Table.svelte.d.ts +14 -0
- package/dist/components/data/Tabs.stories.svelte +386 -0
- package/dist/components/data/Tabs.stories.svelte.d.ts +19 -0
- package/dist/components/data/Tabs.svelte +142 -0
- package/dist/components/data/Tabs.svelte.d.ts +19 -0
- package/dist/components/data/index.d.ts +4 -0
- package/dist/components/data/index.js +4 -0
- package/dist/components/feedback/Modal.stories.svelte +192 -0
- package/dist/components/feedback/Modal.stories.svelte.d.ts +4 -0
- package/dist/components/feedback/Modal.svelte +185 -0
- package/dist/components/feedback/Modal.svelte.d.ts +19 -0
- package/dist/components/feedback/Toast.stories.svelte +203 -0
- package/dist/components/feedback/Toast.stories.svelte.d.ts +19 -0
- package/dist/components/feedback/Toast.svelte +109 -0
- package/dist/components/feedback/Toast.svelte.d.ts +15 -0
- package/dist/components/feedback/ToastRegion.stories.svelte +193 -0
- package/dist/components/feedback/ToastRegion.stories.svelte.d.ts +19 -0
- package/dist/components/feedback/ToastRegion.svelte +102 -0
- package/dist/components/feedback/ToastRegion.svelte.d.ts +9 -0
- package/dist/components/feedback/index.d.ts +3 -0
- package/dist/components/feedback/index.js +3 -0
- package/dist/components/forms/Checkbox.stories.svelte +103 -0
- package/dist/components/forms/Checkbox.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Checkbox.svelte +150 -0
- package/dist/components/forms/Checkbox.svelte.d.ts +11 -0
- package/dist/components/forms/Field.stories.svelte +113 -0
- package/dist/components/forms/Field.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Field.svelte +77 -0
- package/dist/components/forms/Field.svelte.d.ts +17 -0
- package/dist/components/forms/Input.stories.svelte +58 -0
- package/dist/components/forms/Input.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Input.svelte +64 -0
- package/dist/components/forms/Input.svelte.d.ts +9 -0
- package/dist/components/forms/InputWrap.composition.stories.svelte +32 -0
- package/dist/components/forms/InputWrap.composition.stories.svelte.d.ts +19 -0
- package/dist/components/forms/InputWrap.stories.svelte +53 -0
- package/dist/components/forms/InputWrap.stories.svelte.d.ts +19 -0
- package/dist/components/forms/InputWrap.svelte +128 -0
- package/dist/components/forms/InputWrap.svelte.d.ts +21 -0
- package/dist/components/forms/Radio.stories.svelte +70 -0
- package/dist/components/forms/Radio.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Radio.svelte +109 -0
- package/dist/components/forms/Radio.svelte.d.ts +9 -0
- package/dist/components/forms/RadioGroup.stories.svelte +115 -0
- package/dist/components/forms/RadioGroup.stories.svelte.d.ts +19 -0
- package/dist/components/forms/RadioGroup.svelte +116 -0
- package/dist/components/forms/RadioGroup.svelte.d.ts +24 -0
- package/dist/components/forms/Select.stories.svelte +168 -0
- package/dist/components/forms/Select.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Select.svelte +262 -0
- package/dist/components/forms/Select.svelte.d.ts +23 -0
- package/dist/components/forms/Switch.stories.svelte +86 -0
- package/dist/components/forms/Switch.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Switch.svelte +113 -0
- package/dist/components/forms/Switch.svelte.d.ts +11 -0
- package/dist/components/forms/Textarea.stories.svelte +40 -0
- package/dist/components/forms/Textarea.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Textarea.svelte +66 -0
- package/dist/components/forms/Textarea.svelte.d.ts +9 -0
- package/dist/components/forms/field-context.d.ts +7 -0
- package/dist/components/forms/field-context.js +1 -0
- package/dist/components/forms/index.d.ts +9 -0
- package/dist/components/forms/index.js +9 -0
- package/dist/components/layout/Container.stories.svelte +67 -0
- package/dist/components/layout/Container.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Container.svelte +52 -0
- package/dist/components/layout/Container.svelte.d.ts +14 -0
- package/dist/components/layout/Grid.stories.svelte +109 -0
- package/dist/components/layout/Grid.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Grid.svelte +54 -0
- package/dist/components/layout/Grid.svelte.d.ts +19 -0
- package/dist/components/layout/Inline.stories.svelte +136 -0
- package/dist/components/layout/Inline.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Inline.svelte +46 -0
- package/dist/components/layout/Inline.svelte.d.ts +19 -0
- package/dist/components/layout/Prose.stories.svelte +423 -0
- package/dist/components/layout/Prose.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Prose.svelte +176 -0
- package/dist/components/layout/Prose.svelte.d.ts +12 -0
- package/dist/components/layout/Rule.stories.svelte +80 -0
- package/dist/components/layout/Rule.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Rule.svelte +33 -0
- package/dist/components/layout/Rule.svelte.d.ts +9 -0
- package/dist/components/layout/Spread.stories.svelte +118 -0
- package/dist/components/layout/Spread.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Spread.svelte +38 -0
- package/dist/components/layout/Spread.svelte.d.ts +16 -0
- package/dist/components/layout/Stack.stories.svelte +90 -0
- package/dist/components/layout/Stack.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Stack.svelte +37 -0
- package/dist/components/layout/Stack.svelte.d.ts +16 -0
- package/dist/components/layout/index.d.ts +7 -0
- package/dist/components/layout/index.js +7 -0
- package/dist/components/navigation/Breadcrumb.stories.svelte +122 -0
- package/dist/components/navigation/Breadcrumb.stories.svelte.d.ts +19 -0
- package/dist/components/navigation/Breadcrumb.svelte +70 -0
- package/dist/components/navigation/Breadcrumb.svelte.d.ts +13 -0
- package/dist/components/navigation/Nav.stories.svelte +323 -0
- package/dist/components/navigation/Nav.stories.svelte.d.ts +19 -0
- package/dist/components/navigation/Nav.svelte +257 -0
- package/dist/components/navigation/Nav.svelte.d.ts +21 -0
- package/dist/components/navigation/index.d.ts +2 -0
- package/dist/components/navigation/index.js +2 -0
- package/dist/components/patterns/ActivityRow.stories.svelte +45 -0
- package/dist/components/patterns/ActivityRow.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/ActivityRow.svelte +69 -0
- package/dist/components/patterns/ActivityRow.svelte.d.ts +16 -0
- package/dist/components/patterns/Alert.stories.svelte +63 -0
- package/dist/components/patterns/Alert.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/Alert.svelte +91 -0
- package/dist/components/patterns/Alert.svelte.d.ts +16 -0
- package/dist/components/patterns/CtaBlock.stories.svelte +62 -0
- package/dist/components/patterns/CtaBlock.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/CtaBlock.svelte +80 -0
- package/dist/components/patterns/CtaBlock.svelte.d.ts +16 -0
- package/dist/components/patterns/KvList.stories.svelte +48 -0
- package/dist/components/patterns/KvList.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/KvList.svelte +65 -0
- package/dist/components/patterns/KvList.svelte.d.ts +15 -0
- package/dist/components/patterns/PageHero.stories.svelte +62 -0
- package/dist/components/patterns/PageHero.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/PageHero.svelte +62 -0
- package/dist/components/patterns/PageHero.svelte.d.ts +14 -0
- package/dist/components/patterns/ProgressBar.stories.svelte +83 -0
- package/dist/components/patterns/ProgressBar.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/ProgressBar.svelte +71 -0
- package/dist/components/patterns/ProgressBar.svelte.d.ts +13 -0
- package/dist/components/patterns/SectionFoot.stories.svelte +37 -0
- package/dist/components/patterns/SectionFoot.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/SectionFoot.svelte +70 -0
- package/dist/components/patterns/SectionFoot.svelte.d.ts +15 -0
- package/dist/components/patterns/SectionHead.stories.svelte +67 -0
- package/dist/components/patterns/SectionHead.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/SectionHead.svelte +54 -0
- package/dist/components/patterns/SectionHead.svelte.d.ts +14 -0
- package/dist/components/patterns/StatCard.stories.svelte +59 -0
- package/dist/components/patterns/StatCard.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/StatCard.svelte +57 -0
- package/dist/components/patterns/StatCard.svelte.d.ts +15 -0
- package/dist/components/patterns/index.d.ts +9 -0
- package/dist/components/patterns/index.js +9 -0
- package/dist/components/primitives/Button.stories.svelte +132 -0
- package/dist/components/primitives/Button.stories.svelte.d.ts +19 -0
- package/dist/components/primitives/Button.svelte +142 -0
- package/dist/components/primitives/Button.svelte.d.ts +16 -0
- package/dist/components/primitives/Heading.stories.svelte +137 -0
- package/dist/components/primitives/Heading.stories.svelte.d.ts +19 -0
- package/dist/components/primitives/Heading.svelte +107 -0
- package/dist/components/primitives/Heading.svelte.d.ts +23 -0
- package/dist/components/primitives/Led.stories.svelte +63 -0
- package/dist/components/primitives/Led.stories.svelte.d.ts +19 -0
- package/dist/components/primitives/Led.svelte +65 -0
- package/dist/components/primitives/Led.svelte.d.ts +11 -0
- package/dist/components/primitives/TagPill.stories.svelte +90 -0
- package/dist/components/primitives/TagPill.stories.svelte.d.ts +19 -0
- package/dist/components/primitives/TagPill.svelte +44 -0
- package/dist/components/primitives/TagPill.svelte.d.ts +9 -0
- package/dist/components/primitives/Text.stories.svelte +252 -0
- package/dist/components/primitives/Text.stories.svelte.d.ts +19 -0
- package/dist/components/primitives/Text.svelte +101 -0
- package/dist/components/primitives/Text.svelte.d.ts +25 -0
- package/dist/components/primitives/index.d.ts +5 -0
- package/dist/components/primitives/index.js +5 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/stores/toast.d.ts +19 -0
- package/dist/stores/toast.js +22 -0
- package/dist/storybook-utils.d.ts +11 -0
- package/dist/storybook-utils.js +29 -0
- package/dist/tokens/ColorSwatch.svelte +73 -0
- package/dist/tokens/ColorSwatch.svelte.d.ts +10 -0
- package/dist/tokens/layout.css +144 -0
- package/dist/tokens/patterns.css +281 -0
- package/dist/tokens/tokens.css +96 -0
- package/dist/tokens/tokens.stories.svelte +107 -0
- package/dist/tokens/tokens.stories.svelte.d.ts +18 -0
- package/dist/tokens/typography.css +159 -0
- package/package.json +62 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Crumb {
|
|
3
|
+
label: string
|
|
4
|
+
href: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface BreadcrumbProps {
|
|
8
|
+
/** Array of `{ label, href }` crumbs — last item gets `aria-current="page"`. */
|
|
9
|
+
crumbs: Crumb[]
|
|
10
|
+
/** Root element tag. Use `"div"` when embedding inside a `<nav>` to avoid nested landmark. @default 'nav' */
|
|
11
|
+
as?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { crumbs, as = 'nav' }: BreadcrumbProps = $props()
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<svelte:element this={as} class="breadcrumb" aria-label="breadcrumb">
|
|
18
|
+
<ol class="bc-list">
|
|
19
|
+
{#each crumbs as crumb, i}
|
|
20
|
+
<li class="bc-item">
|
|
21
|
+
{#if i > 0}<span class="bc-sep" aria-hidden="true">/</span>{/if}
|
|
22
|
+
<a
|
|
23
|
+
href={crumb.href}
|
|
24
|
+
class="bc-link"
|
|
25
|
+
aria-current={i === crumbs.length - 1 ? 'page' : undefined}
|
|
26
|
+
>{crumb.label}</a>
|
|
27
|
+
</li>
|
|
28
|
+
{/each}
|
|
29
|
+
</ol>
|
|
30
|
+
</svelte:element>
|
|
31
|
+
|
|
32
|
+
<style>
|
|
33
|
+
.breadcrumb {
|
|
34
|
+
display: block;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.bc-list {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
flex-wrap: wrap;
|
|
41
|
+
gap: 6px;
|
|
42
|
+
list-style: none;
|
|
43
|
+
padding: 0;
|
|
44
|
+
margin: 0;
|
|
45
|
+
font-family: var(--mono);
|
|
46
|
+
font-size: 11px;
|
|
47
|
+
letter-spacing: 0.08em;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.bc-item {
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: 6px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.bc-link {
|
|
57
|
+
color: var(--amber);
|
|
58
|
+
text-decoration: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.bc-link:hover {
|
|
62
|
+
color: var(--amber);
|
|
63
|
+
opacity: 0.75;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.bc-sep {
|
|
67
|
+
color: var(--ink-faint);
|
|
68
|
+
user-select: none;
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface Crumb {
|
|
2
|
+
label: string;
|
|
3
|
+
href: string;
|
|
4
|
+
}
|
|
5
|
+
interface BreadcrumbProps {
|
|
6
|
+
/** Array of `{ label, href }` crumbs — last item gets `aria-current="page"`. */
|
|
7
|
+
crumbs: Crumb[];
|
|
8
|
+
/** Root element tag. Use `"div"` when embedding inside a `<nav>` to avoid nested landmark. @default 'nav' */
|
|
9
|
+
as?: string;
|
|
10
|
+
}
|
|
11
|
+
declare const Breadcrumb: import("svelte").Component<BreadcrumbProps, {}, "">;
|
|
12
|
+
type Breadcrumb = ReturnType<typeof Breadcrumb>;
|
|
13
|
+
export default Breadcrumb;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
3
|
+
import { expect, within } from "storybook/test";
|
|
4
|
+
import Nav from "./Nav.svelte";
|
|
5
|
+
import { resolveTokenColor, resolveTokenFgColor } from "../../storybook-utils.js";
|
|
6
|
+
|
|
7
|
+
const { Story } = defineMeta({
|
|
8
|
+
title: "Navigation/Nav",
|
|
9
|
+
component: Nav,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
});
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<!-- AC-B28-9 (44,45,46,47,48,49), AC-B28-3 (17), AC-B28-5 (27,28,29,30,31), B28-3 (15,18) -->
|
|
15
|
+
<Story name="Default" args={{
|
|
16
|
+
links: [
|
|
17
|
+
{ href: '/feed', label: 'FEED' },
|
|
18
|
+
{ href: '/catalogue', label: 'CATALOGUE', active: true },
|
|
19
|
+
{ href: '/projects', label: 'PROJECTS' },
|
|
20
|
+
{ href: '/notes', label: 'NOTES' },
|
|
21
|
+
{ href: '/about', label: 'ABOUT' },
|
|
22
|
+
{ href: '/contact', label: 'CONTACT' },
|
|
23
|
+
],
|
|
24
|
+
'data-testid': 'main-nav',
|
|
25
|
+
}}
|
|
26
|
+
play={async ({ canvasElement }) => {
|
|
27
|
+
const canvas = within(canvasElement);
|
|
28
|
+
|
|
29
|
+
// AC-44: <nav> element is present
|
|
30
|
+
const nav = canvas.getByRole('navigation');
|
|
31
|
+
await expect(nav).toBeVisible();
|
|
32
|
+
|
|
33
|
+
// AC-45: <nav> has position: fixed and z-index >= 100
|
|
34
|
+
await expect(getComputedStyle(nav).position).toBe('fixed');
|
|
35
|
+
await expect(parseInt(getComputedStyle(nav).zIndex)).toBeGreaterThanOrEqual(100);
|
|
36
|
+
|
|
37
|
+
// AC-46: <nav> has background-color matching --bg and border-bottom-color matching --rule
|
|
38
|
+
await expect(getComputedStyle(nav).backgroundColor).toBe(resolveTokenColor('--bg'));
|
|
39
|
+
await expect(getComputedStyle(nav).borderBottomColor).toBe(resolveTokenColor('--rule'));
|
|
40
|
+
|
|
41
|
+
// AC-47: .nav-inner has display: flex and height: 48px
|
|
42
|
+
const inner = canvasElement.querySelector('.nav-inner') as HTMLElement | null;
|
|
43
|
+
await expect(inner).not.toBeNull();
|
|
44
|
+
await expect(getComputedStyle(inner!).display).toBe('flex');
|
|
45
|
+
await expect(getComputedStyle(inner!).height).toBe('48px');
|
|
46
|
+
|
|
47
|
+
// AC-48: ...rest spread — data-testid forwarded onto <nav>
|
|
48
|
+
await expect(nav.getAttribute('data-testid')).toBe('main-nav');
|
|
49
|
+
|
|
50
|
+
// AC-49: brand link contains 'DEXTERLABS' (default siteName)
|
|
51
|
+
await expect(canvas.getByRole('link', { name: /DEXTERLABS/ })).toBeVisible();
|
|
52
|
+
|
|
53
|
+
// active link has aria-current="page" (AC-44 / AC-51)
|
|
54
|
+
const activeLink = canvas.getByRole('link', { name: 'CATALOGUE' });
|
|
55
|
+
await expect(activeLink.getAttribute('aria-current')).toBe('page');
|
|
56
|
+
|
|
57
|
+
// AC-31: palette toggle button present with correct accessible name
|
|
58
|
+
const toggleBtn = canvas.getByRole('button', { name: /toggle colour palette/i });
|
|
59
|
+
await expect(toggleBtn).toBeVisible();
|
|
60
|
+
|
|
61
|
+
// AC-27: palette toggle has both 'btn' and 'btn-nav' classes
|
|
62
|
+
await expect(toggleBtn.classList.contains('btn')).toBe(true);
|
|
63
|
+
await expect(toggleBtn.classList.contains('btn-nav')).toBe(true);
|
|
64
|
+
|
|
65
|
+
// AC-28: palette toggle does NOT have 'btn-ghost' class
|
|
66
|
+
await expect(toggleBtn.classList.contains('btn-ghost')).toBe(false);
|
|
67
|
+
|
|
68
|
+
// AC-29: palette toggle color matches --ink-faint at rest
|
|
69
|
+
await expect(getComputedStyle(toggleBtn).color).toBe(resolveTokenFgColor('--ink-faint'));
|
|
70
|
+
|
|
71
|
+
// AC-30: palette toggle background-color is transparent
|
|
72
|
+
await expect(getComputedStyle(toggleBtn).backgroundColor).toBe('rgba(0, 0, 0, 0)');
|
|
73
|
+
|
|
74
|
+
// AC-18: no .nav-hamburger in DOM
|
|
75
|
+
const hamburger = canvasElement.querySelector('.nav-hamburger');
|
|
76
|
+
await expect(hamburger).toBeNull();
|
|
77
|
+
|
|
78
|
+
// AC-18: no #nav-drawer in DOM
|
|
79
|
+
const drawer = canvasElement.querySelector('#nav-drawer');
|
|
80
|
+
await expect(drawer).toBeNull();
|
|
81
|
+
}} />
|
|
82
|
+
|
|
83
|
+
<!-- AC-B28-3 (15,17), AC-B28-9 (44) -->
|
|
84
|
+
<Story name="No Links" args={{ links: [] }}
|
|
85
|
+
play={async ({ canvasElement }) => {
|
|
86
|
+
const canvas = within(canvasElement);
|
|
87
|
+
|
|
88
|
+
// AC-44: <nav> is present
|
|
89
|
+
const nav = canvas.getByRole('navigation');
|
|
90
|
+
await expect(nav).toBeVisible();
|
|
91
|
+
|
|
92
|
+
// AC-15: no <details> element in DOM when links is empty
|
|
93
|
+
const details = canvasElement.querySelector('details');
|
|
94
|
+
await expect(details).toBeNull();
|
|
95
|
+
|
|
96
|
+
// no <ul class="nav-links">
|
|
97
|
+
const navLinks = canvasElement.querySelector('ul.nav-links');
|
|
98
|
+
await expect(navLinks).toBeNull();
|
|
99
|
+
|
|
100
|
+
// AC-17: palette toggle is still present
|
|
101
|
+
const toggleBtn = canvas.getByRole('button', { name: 'Toggle colour palette' });
|
|
102
|
+
await expect(toggleBtn).toBeVisible();
|
|
103
|
+
}} />
|
|
104
|
+
|
|
105
|
+
<!-- AC-B28-2 (7,8,9,10,12) -->
|
|
106
|
+
<Story name="With Breadcrumbs" args={{
|
|
107
|
+
links: [
|
|
108
|
+
{ href: '/feed', label: 'FEED' },
|
|
109
|
+
{ href: '/catalogue', label: 'CATALOGUE', active: true },
|
|
110
|
+
{ href: '/projects', label: 'PROJECTS' },
|
|
111
|
+
{ href: '/notes', label: 'NOTES', active: false },
|
|
112
|
+
{ href: '/about', label: 'ABOUT' },
|
|
113
|
+
{ href: '/contact', label: 'CONTACT' },
|
|
114
|
+
],
|
|
115
|
+
breadcrumbs: [
|
|
116
|
+
{ label: '~', href: '/' },
|
|
117
|
+
{ label: 'NOTES', href: '/notes/' },
|
|
118
|
+
]
|
|
119
|
+
}}
|
|
120
|
+
play={async ({ canvasElement }) => {
|
|
121
|
+
// AC-8/9: .nav-path element is present
|
|
122
|
+
const navPath = canvasElement.querySelector('.nav-path');
|
|
123
|
+
await expect(navPath).not.toBeNull();
|
|
124
|
+
|
|
125
|
+
// crumb anchors: ~ links to / and NOTES links to /notes/
|
|
126
|
+
const crumbLinks = navPath!.querySelectorAll('a');
|
|
127
|
+
const hrefs = Array.from(crumbLinks).map(a => a.getAttribute('href'));
|
|
128
|
+
await expect(hrefs).toContain('/');
|
|
129
|
+
await expect(hrefs).toContain('/notes/');
|
|
130
|
+
|
|
131
|
+
// AC-11: each crumb anchor color matches --amber and is clickable (no pointer-events: none)
|
|
132
|
+
const amberColor = resolveTokenFgColor('--amber');
|
|
133
|
+
for (const crumb of Array.from(crumbLinks) as HTMLElement[]) {
|
|
134
|
+
await expect(getComputedStyle(crumb).color).toBe(amberColor);
|
|
135
|
+
await expect(getComputedStyle(crumb).pointerEvents).not.toBe('none');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// AC-10: .nav-sep span contains '//'
|
|
139
|
+
const navSep = navPath!.querySelector('.nav-sep');
|
|
140
|
+
await expect(navSep).not.toBeNull();
|
|
141
|
+
await expect(navSep!.textContent?.trim()).toBe('//');
|
|
142
|
+
|
|
143
|
+
// AC-12: .nav-sep color matches --ink-faint
|
|
144
|
+
const inkFaint = resolveTokenFgColor('--ink-faint');
|
|
145
|
+
await expect(getComputedStyle(navSep!).color).toBe(inkFaint);
|
|
146
|
+
}} />
|
|
147
|
+
|
|
148
|
+
<!-- AC-B28-2 (7), AC-B28-3 (15) — breadcrumbs present but links empty -->
|
|
149
|
+
<Story name="Breadcrumbs Only No Links" args={{
|
|
150
|
+
links: [],
|
|
151
|
+
breadcrumbs: [
|
|
152
|
+
{ label: '~', href: '/' },
|
|
153
|
+
{ label: 'NOTES', href: '/notes/' },
|
|
154
|
+
]
|
|
155
|
+
}}
|
|
156
|
+
play={async ({ canvasElement }) => {
|
|
157
|
+
// .nav-path present because breadcrumbs is non-empty
|
|
158
|
+
const navPath = canvasElement.querySelector('.nav-path');
|
|
159
|
+
await expect(navPath).not.toBeNull();
|
|
160
|
+
|
|
161
|
+
// no <details> because links is empty (AC-15)
|
|
162
|
+
const details = canvasElement.querySelector('details');
|
|
163
|
+
await expect(details).toBeNull();
|
|
164
|
+
}} />
|
|
165
|
+
|
|
166
|
+
<!-- AC-B28-9 (50) -->
|
|
167
|
+
<Story name="Custom Site Name" args={{ links: [], siteName: 'LAB' }}
|
|
168
|
+
play={async ({ canvasElement }) => {
|
|
169
|
+
// brand link contains the custom siteName
|
|
170
|
+
await expect(within(canvasElement).getByRole('link', { name: /LAB/ })).toBeVisible();
|
|
171
|
+
// must not contain the default
|
|
172
|
+
const text = canvasElement.textContent ?? '';
|
|
173
|
+
await expect(text).toContain('LAB');
|
|
174
|
+
}} />
|
|
175
|
+
|
|
176
|
+
<!-- AC-B28-9 (53) -->
|
|
177
|
+
<Story name="Palette Toggle" args={{
|
|
178
|
+
links: [
|
|
179
|
+
{ href: '/feed', label: 'FEED' },
|
|
180
|
+
{ href: '/catalogue', label: 'CATALOGUE', active: true },
|
|
181
|
+
{ href: '/projects', label: 'PROJECTS' },
|
|
182
|
+
{ href: '/notes', label: 'NOTES' },
|
|
183
|
+
{ href: '/about', label: 'ABOUT' },
|
|
184
|
+
{ href: '/contact', label: 'CONTACT' },
|
|
185
|
+
]
|
|
186
|
+
}}
|
|
187
|
+
play={async ({ canvasElement, userEvent }) => {
|
|
188
|
+
const canvas = within(canvasElement);
|
|
189
|
+
const btn = canvas.getByRole('button', { name: 'Toggle colour palette' });
|
|
190
|
+
|
|
191
|
+
// record the initial data-palette value
|
|
192
|
+
const before = document.documentElement.getAttribute('data-palette');
|
|
193
|
+
|
|
194
|
+
// click once — palette should change
|
|
195
|
+
await userEvent.click(btn);
|
|
196
|
+
const after = document.documentElement.getAttribute('data-palette');
|
|
197
|
+
await expect(after).not.toBe(before);
|
|
198
|
+
await expect(localStorage.getItem('dxlb-palette')).toBe(after);
|
|
199
|
+
|
|
200
|
+
// click again — palette should be restored
|
|
201
|
+
await userEvent.click(btn);
|
|
202
|
+
const restored = document.documentElement.getAttribute('data-palette');
|
|
203
|
+
await expect(restored).toBe(before);
|
|
204
|
+
await expect(localStorage.getItem('dxlb-palette')).toBe(before);
|
|
205
|
+
}} />
|
|
206
|
+
|
|
207
|
+
<!-- AC-B28-7 (34,35) -->
|
|
208
|
+
<Story name="Active Link Styling" args={{
|
|
209
|
+
links: [
|
|
210
|
+
{ href: '/feed', label: 'FEED' },
|
|
211
|
+
{ href: '/catalogue', label: 'CATALOGUE' },
|
|
212
|
+
{ href: '/projects', label: 'PROJECTS' },
|
|
213
|
+
{ href: '/notes', label: 'NOTES', active: true },
|
|
214
|
+
{ href: '/about', label: 'ABOUT' },
|
|
215
|
+
{ href: '/contact', label: 'CONTACT' },
|
|
216
|
+
]
|
|
217
|
+
}}
|
|
218
|
+
play={async ({ canvasElement }) => {
|
|
219
|
+
// get the NOTES anchor via aria-current
|
|
220
|
+
const activeLinks = canvasElement.querySelectorAll('.nav-link.active');
|
|
221
|
+
await expect(activeLinks.length).toBeGreaterThan(0);
|
|
222
|
+
const anchor = activeLinks[0] as HTMLElement;
|
|
223
|
+
|
|
224
|
+
// AC-34: aria-current="page"
|
|
225
|
+
await expect(anchor.getAttribute('aria-current')).toBe('page');
|
|
226
|
+
|
|
227
|
+
// AC-35: border-bottom-color matches --amber
|
|
228
|
+
const amberColor = resolveTokenFgColor('--amber');
|
|
229
|
+
await expect(getComputedStyle(anchor).borderBottomColor).toBe(amberColor);
|
|
230
|
+
|
|
231
|
+
// AC-34: color matches --ink
|
|
232
|
+
const inkColor = resolveTokenFgColor('--ink');
|
|
233
|
+
await expect(getComputedStyle(anchor).color).toBe(inkColor);
|
|
234
|
+
|
|
235
|
+
// AC-36: a non-active .nav-link has color matching --ink-dim
|
|
236
|
+
const inactiveLinks = canvasElement.querySelectorAll('.nav-link:not(.active)');
|
|
237
|
+
await expect(inactiveLinks.length).toBeGreaterThan(0);
|
|
238
|
+
const inactiveLink = inactiveLinks[0] as HTMLElement;
|
|
239
|
+
const inkDimColor = resolveTokenFgColor('--ink-dim');
|
|
240
|
+
await expect(getComputedStyle(inactiveLink).color).toBe(inkDimColor);
|
|
241
|
+
|
|
242
|
+
// AC-37: a non-active .nav-link has transparent border-bottom-color
|
|
243
|
+
await expect(getComputedStyle(inactiveLink).borderBottomColor).toBe('rgba(0, 0, 0, 0)');
|
|
244
|
+
}} />
|
|
245
|
+
|
|
246
|
+
<!-- AC-B28-4 (20,21,22,23,24) -->
|
|
247
|
+
<Story name="Mobile Menu Details" args={{
|
|
248
|
+
links: [
|
|
249
|
+
{ href: '/feed', label: 'FEED' },
|
|
250
|
+
{ href: '/catalogue', label: 'CATALOGUE', active: true },
|
|
251
|
+
{ href: '/projects', label: 'PROJECTS' },
|
|
252
|
+
{ href: '/notes', label: 'NOTES' },
|
|
253
|
+
{ href: '/about', label: 'ABOUT' },
|
|
254
|
+
{ href: '/contact', label: 'CONTACT' },
|
|
255
|
+
]
|
|
256
|
+
}}
|
|
257
|
+
play={async ({ canvasElement }) => {
|
|
258
|
+
// AC-20: <details class="nav-menu"> is present
|
|
259
|
+
const details = canvasElement.querySelector('details.nav-menu') as HTMLDetailsElement | null;
|
|
260
|
+
await expect(details).not.toBeNull();
|
|
261
|
+
|
|
262
|
+
// AC-20: contains <summary class="nav-summary">
|
|
263
|
+
const summary = details!.querySelector('summary.nav-summary');
|
|
264
|
+
await expect(summary).not.toBeNull();
|
|
265
|
+
|
|
266
|
+
// AC-21: summary contains nav-icon-open (≡) and nav-icon-close (×)
|
|
267
|
+
const iconOpen = details!.querySelector('.nav-icon-open') as HTMLElement | null;
|
|
268
|
+
const iconClose = details!.querySelector('.nav-icon-close') as HTMLElement | null;
|
|
269
|
+
await expect(iconOpen).not.toBeNull();
|
|
270
|
+
await expect(iconClose).not.toBeNull();
|
|
271
|
+
await expect(iconOpen!.textContent?.trim()).toBe('≡');
|
|
272
|
+
await expect(iconClose!.textContent?.trim()).toBe('×');
|
|
273
|
+
|
|
274
|
+
// AC-22: .nav-icon-close is display:none when <details> is closed
|
|
275
|
+
await expect(getComputedStyle(iconClose!).display).toBe('none');
|
|
276
|
+
|
|
277
|
+
// AC-23: open the details programmatically
|
|
278
|
+
details!.setAttribute('open', '');
|
|
279
|
+
// Allow browser to re-apply styles
|
|
280
|
+
await new Promise(r => setTimeout(r, 0));
|
|
281
|
+
await expect(getComputedStyle(iconOpen!).display).toBe('none');
|
|
282
|
+
await expect(getComputedStyle(iconClose!).display).not.toBe('none');
|
|
283
|
+
|
|
284
|
+
// Close it again for cleanliness
|
|
285
|
+
details!.removeAttribute('open');
|
|
286
|
+
|
|
287
|
+
// AC-24: .nav-dropdown-link anchors exist with correct hrefs
|
|
288
|
+
const dropdownLinks = details!.querySelectorAll('.nav-dropdown-link');
|
|
289
|
+
await expect(dropdownLinks.length).toBe(6);
|
|
290
|
+
const hrefs = Array.from(dropdownLinks).map(a => (a as HTMLAnchorElement).getAttribute('href'));
|
|
291
|
+
await expect(hrefs).toContain('/feed');
|
|
292
|
+
await expect(hrefs).toContain('/catalogue');
|
|
293
|
+
await expect(hrefs).toContain('/notes');
|
|
294
|
+
|
|
295
|
+
// AC-25: active dropdown link has aria-current="page"
|
|
296
|
+
const activeDropdownLink = Array.from(dropdownLinks).find(
|
|
297
|
+
a => (a as HTMLAnchorElement).getAttribute('href') === '/catalogue'
|
|
298
|
+
) as HTMLElement | undefined;
|
|
299
|
+
await expect(activeDropdownLink).toBeDefined();
|
|
300
|
+
await expect(activeDropdownLink!.getAttribute('aria-current')).toBe('page');
|
|
301
|
+
|
|
302
|
+
// AC-39: .nav-dropdown has position: absolute
|
|
303
|
+
const dropdown = details!.querySelector('.nav-dropdown') as HTMLElement | null;
|
|
304
|
+
await expect(dropdown).not.toBeNull();
|
|
305
|
+
await expect(getComputedStyle(dropdown!).position).toBe('absolute');
|
|
306
|
+
|
|
307
|
+
// AC-40: .nav-dropdown background-color matches --bg
|
|
308
|
+
await expect(getComputedStyle(dropdown!).backgroundColor).toBe(resolveTokenColor('--bg'));
|
|
309
|
+
|
|
310
|
+
// AC-41: .nav-dropdown border-bottom-color matches --rule
|
|
311
|
+
await expect(getComputedStyle(dropdown!).borderBottomColor).toBe(resolveTokenColor('--rule'));
|
|
312
|
+
|
|
313
|
+
// AC-42: first dropdown link has padding-top and padding-bottom of 10px
|
|
314
|
+
await expect(getComputedStyle(dropdownLinks[0] as HTMLElement).paddingTop).toBe('10px');
|
|
315
|
+
await expect(getComputedStyle(dropdownLinks[0] as HTMLElement).paddingBottom).toBe('10px');
|
|
316
|
+
|
|
317
|
+
// AC-43 (non-last): first dropdown link has border-bottom-color matching --rule
|
|
318
|
+
await expect(getComputedStyle(dropdownLinks[0] as HTMLElement).borderBottomColor).toBe(resolveTokenColor('--rule'));
|
|
319
|
+
|
|
320
|
+
// AC-43 (last-child): last dropdown link has border-bottom-width of 0px
|
|
321
|
+
const lastLink = dropdownLinks[dropdownLinks.length - 1] as HTMLElement;
|
|
322
|
+
await expect(getComputedStyle(lastLink).borderBottomWidth).toBe('0px');
|
|
323
|
+
}} />
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Nav from "./Nav.svelte";
|
|
2
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
3
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
4
|
+
$$bindings?: Bindings;
|
|
5
|
+
} & Exports;
|
|
6
|
+
(internal: unknown, props: {
|
|
7
|
+
$$events?: Events;
|
|
8
|
+
$$slots?: Slots;
|
|
9
|
+
}): Exports & {
|
|
10
|
+
$set?: any;
|
|
11
|
+
$on?: any;
|
|
12
|
+
};
|
|
13
|
+
z_$$bindings?: Bindings;
|
|
14
|
+
}
|
|
15
|
+
declare const Nav: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
16
|
+
[evt: string]: CustomEvent<any>;
|
|
17
|
+
}, {}, {}, string>;
|
|
18
|
+
type Nav = InstanceType<typeof Nav>;
|
|
19
|
+
export default Nav;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Led from '../primitives/Led.svelte'
|
|
3
|
+
import Button from '../primitives/Button.svelte'
|
|
4
|
+
import Breadcrumb from './Breadcrumb.svelte'
|
|
5
|
+
|
|
6
|
+
interface BreadcrumbItem {
|
|
7
|
+
label: string
|
|
8
|
+
href: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface NavLink {
|
|
12
|
+
href: string
|
|
13
|
+
label: string
|
|
14
|
+
active?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
/** Navigation links to render in the top bar. @default [] */
|
|
19
|
+
links?: NavLink[]
|
|
20
|
+
/** Brand name shown next to the status LED. @default 'DEXTERLABS' */
|
|
21
|
+
siteName?: string
|
|
22
|
+
/** Optional breadcrumb trail. @default [] */
|
|
23
|
+
breadcrumbs?: BreadcrumbItem[]
|
|
24
|
+
[key: string]: unknown
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let { links = [], siteName = 'DEXTERLABS', breadcrumbs = [], ...rest }: Props = $props()
|
|
28
|
+
|
|
29
|
+
type Palette = 'phosphor' | 'paper'
|
|
30
|
+
let palette = $state<Palette>('phosphor')
|
|
31
|
+
|
|
32
|
+
const PALETTE_KEY = 'dxlb-palette'
|
|
33
|
+
|
|
34
|
+
$effect(() => {
|
|
35
|
+
const stored = localStorage.getItem(PALETTE_KEY) as Palette | null
|
|
36
|
+
if (stored === 'paper' || stored === 'phosphor') palette = stored
|
|
37
|
+
document.documentElement.setAttribute('data-palette', palette)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
function handlePaletteToggle() {
|
|
41
|
+
palette = palette === 'paper' ? 'phosphor' : 'paper'
|
|
42
|
+
document.documentElement.setAttribute('data-palette', palette)
|
|
43
|
+
localStorage.setItem(PALETTE_KEY, palette)
|
|
44
|
+
}
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<nav class="nav" {...rest}>
|
|
48
|
+
<div class="nav-inner">
|
|
49
|
+
<a class="nav-brand" href="/">
|
|
50
|
+
<Led color="ok" aria-hidden="true" />
|
|
51
|
+
<span class="nav-wordmark">{siteName}</span>
|
|
52
|
+
</a>
|
|
53
|
+
|
|
54
|
+
{#if breadcrumbs?.length}
|
|
55
|
+
<div class="nav-path">
|
|
56
|
+
<span class="nav-sep">//</span>
|
|
57
|
+
<Breadcrumb crumbs={breadcrumbs} as="div" />
|
|
58
|
+
</div>
|
|
59
|
+
{/if}
|
|
60
|
+
|
|
61
|
+
<div class="nav-right">
|
|
62
|
+
{#if links?.length}
|
|
63
|
+
<ul class="nav-links">
|
|
64
|
+
{#each links as link}
|
|
65
|
+
<li>
|
|
66
|
+
<a href={link.href} class="nav-link" class:active={link.active}
|
|
67
|
+
aria-current={link.active ? 'page' : undefined}>
|
|
68
|
+
{link.label}
|
|
69
|
+
</a>
|
|
70
|
+
</li>
|
|
71
|
+
{/each}
|
|
72
|
+
</ul>
|
|
73
|
+
{/if}
|
|
74
|
+
|
|
75
|
+
<Button variant="nav" aria-label="Toggle colour palette" onclick={handlePaletteToggle}>
|
|
76
|
+
{palette === 'paper' ? '◑' : '◐'}
|
|
77
|
+
</Button>
|
|
78
|
+
|
|
79
|
+
{#if links?.length}
|
|
80
|
+
<details class="nav-menu">
|
|
81
|
+
<summary class="nav-summary">
|
|
82
|
+
<span class="nav-icon-open">≡</span>
|
|
83
|
+
<span class="nav-icon-close">×</span>
|
|
84
|
+
</summary>
|
|
85
|
+
<div class="nav-dropdown">
|
|
86
|
+
{#each links as link}
|
|
87
|
+
<a href={link.href} class="nav-dropdown-link" class:active={link.active}
|
|
88
|
+
aria-current={link.active ? 'page' : undefined}>{link.label}</a>
|
|
89
|
+
{/each}
|
|
90
|
+
</div>
|
|
91
|
+
</details>
|
|
92
|
+
{/if}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</nav>
|
|
96
|
+
|
|
97
|
+
<style>
|
|
98
|
+
.nav {
|
|
99
|
+
position: fixed;
|
|
100
|
+
top: 0;
|
|
101
|
+
left: 0;
|
|
102
|
+
right: 0;
|
|
103
|
+
z-index: 100;
|
|
104
|
+
background: var(--bg);
|
|
105
|
+
border-bottom: 1px solid var(--rule);
|
|
106
|
+
font-family: var(--mono);
|
|
107
|
+
font-size: var(--t-mono);
|
|
108
|
+
letter-spacing: 0.08em;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.nav-inner {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
flex-wrap: nowrap;
|
|
115
|
+
padding: 0 24px;
|
|
116
|
+
height: 48px;
|
|
117
|
+
max-width: 1200px;
|
|
118
|
+
margin: 0 auto;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.nav-brand {
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: var(--u);
|
|
125
|
+
flex-shrink: 0;
|
|
126
|
+
color: var(--ink);
|
|
127
|
+
text-decoration: none;
|
|
128
|
+
font-weight: 700;
|
|
129
|
+
letter-spacing: 0.12em;
|
|
130
|
+
text-transform: uppercase;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.nav-path {
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 6px;
|
|
137
|
+
overflow: hidden;
|
|
138
|
+
min-width: 0;
|
|
139
|
+
margin-left: 8px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.nav-sep {
|
|
143
|
+
color: var(--ink-faint);
|
|
144
|
+
flex-shrink: 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.nav-right {
|
|
148
|
+
display: flex;
|
|
149
|
+
align-items: center;
|
|
150
|
+
gap: var(--u2);
|
|
151
|
+
margin-left: auto;
|
|
152
|
+
flex-shrink: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.nav-links {
|
|
156
|
+
display: flex;
|
|
157
|
+
align-items: center;
|
|
158
|
+
flex-wrap: wrap;
|
|
159
|
+
gap: var(--u2);
|
|
160
|
+
padding: 0;
|
|
161
|
+
margin: 0;
|
|
162
|
+
flex-shrink: 0;
|
|
163
|
+
text-transform: uppercase;
|
|
164
|
+
font-size: 12px;
|
|
165
|
+
list-style: none;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.nav-link {
|
|
169
|
+
color: var(--ink-dim);
|
|
170
|
+
text-decoration: none;
|
|
171
|
+
padding-bottom: 2px;
|
|
172
|
+
border-bottom: 1px solid transparent;
|
|
173
|
+
transition: color var(--transition), border-color var(--transition);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.nav-link:hover {
|
|
177
|
+
color: var(--ink);
|
|
178
|
+
border-bottom-color: var(--amber);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.nav-link.active {
|
|
182
|
+
color: var(--ink);
|
|
183
|
+
border-bottom-color: var(--amber);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.nav-menu {
|
|
187
|
+
display: none;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.nav-icon-close {
|
|
191
|
+
display: none;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
details[open] .nav-icon-open {
|
|
195
|
+
display: none;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
details[open] .nav-icon-close {
|
|
199
|
+
display: inline;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.nav-summary {
|
|
203
|
+
list-style: none;
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
color: var(--ink-dim);
|
|
206
|
+
user-select: none;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.nav-summary:hover {
|
|
210
|
+
color: var(--ink);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.nav-summary::-webkit-details-marker {
|
|
214
|
+
display: none;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.nav-dropdown {
|
|
218
|
+
position: absolute;
|
|
219
|
+
top: 100%;
|
|
220
|
+
left: 0;
|
|
221
|
+
right: 0;
|
|
222
|
+
z-index: 100;
|
|
223
|
+
background: var(--bg);
|
|
224
|
+
border-bottom: 1px solid var(--rule);
|
|
225
|
+
display: flex;
|
|
226
|
+
flex-direction: column;
|
|
227
|
+
padding: 4px 16px 8px;
|
|
228
|
+
text-transform: uppercase;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.nav-dropdown-link {
|
|
232
|
+
padding: 10px 0;
|
|
233
|
+
border-bottom: 1px solid var(--rule);
|
|
234
|
+
color: var(--ink-dim);
|
|
235
|
+
text-decoration: none;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.nav-dropdown-link.active {
|
|
239
|
+
color: var(--amber);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.nav-dropdown > :last-child {
|
|
243
|
+
border-bottom: none;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@media (max-width: 720px) {
|
|
247
|
+
.nav-links {
|
|
248
|
+
display: none;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.nav-menu {
|
|
252
|
+
display: block;
|
|
253
|
+
margin-left: auto;
|
|
254
|
+
flex-shrink: 0;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
</style>
|