rizzo-css 0.0.40 → 0.0.42
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 +6 -4
- package/bin/rizzo-css.js +142 -124
- package/dist/fonts/DMMono/DMMono-Italic.ttf +0 -0
- package/dist/fonts/DMMono/DMMono-Regular.ttf +0 -0
- package/dist/fonts/DMMono/OFL.txt +93 -0
- package/dist/fonts/DMSans/DMSans-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/dist/fonts/DMSans/DMSans-VariableFont_opsz,wght.ttf +0 -0
- package/dist/fonts/DMSans/OFL.txt +93 -0
- package/dist/fonts/DMSans/README.txt +136 -0
- package/dist/fonts/IBMPlexMono/IBMPlexMono-Italic.ttf +0 -0
- package/dist/fonts/IBMPlexMono/IBMPlexMono-Regular.ttf +0 -0
- package/dist/fonts/IBMPlexMono/OFL.txt +93 -0
- package/dist/fonts/IBMPlexSans/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/dist/fonts/IBMPlexSans/IBMPlexSans-VariableFont_wdth,wght.ttf +0 -0
- package/dist/fonts/IBMPlexSans/OFL.txt +93 -0
- package/dist/fonts/IBMPlexSans/README.txt +106 -0
- package/dist/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/dist/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/dist/fonts/Inter/OFL.txt +93 -0
- package/dist/fonts/JetBrainsMono/AUTHORS.txt +10 -0
- package/dist/fonts/JetBrainsMono/JetBrainsMono-Italic[wght].ttf +0 -0
- package/dist/fonts/JetBrainsMono/JetBrainsMono[wght].ttf +0 -0
- package/dist/fonts/JetBrainsMono/OFL.txt +93 -0
- package/dist/fonts/Outfit/OFL.txt +93 -0
- package/dist/fonts/Outfit/Outfit-VariableFont_wght.ttf +0 -0
- package/dist/fonts/Outfit/README.txt +71 -0
- package/dist/fonts/SourceCodePro/OFL.txt +93 -0
- package/dist/fonts/SourceCodePro/README.txt +79 -0
- package/dist/fonts/SourceCodePro/SourceCodePro-Italic-VariableFont_wght.ttf +0 -0
- package/dist/fonts/SourceCodePro/SourceCodePro-VariableFont_wght.ttf +0 -0
- package/dist/fonts/SourceSans3/OFL.txt +93 -0
- package/dist/fonts/SourceSans3/README.txt +79 -0
- package/dist/fonts/SourceSans3/SourceSans3-Italic-VariableFont_wght.ttf +0 -0
- package/dist/fonts/SourceSans3/SourceSans3-VariableFont_wght.ttf +0 -0
- package/dist/rizzo.min.css +6 -3
- package/package.json +10 -9
- package/scaffold/astro/FontSwitcher.astro +221 -0
- package/scaffold/astro/Modal.astro +14 -8
- package/scaffold/astro/Settings.astro +19 -0
- package/scaffold/astro/SoundEffects.astro +49 -0
- package/scaffold/{astro-minimal → astro-core}/README-RIZZO.md +1 -1
- package/scaffold/astro-core/src/layouts/Layout.astro +33 -0
- package/scaffold/config/fonts.ts +59 -0
- package/scaffold/svelte/DocsSidebar.svelte +47 -0
- package/scaffold/svelte/FontSwitcher.svelte +180 -0
- package/scaffold/svelte/Modal.svelte +2 -0
- package/scaffold/svelte/Settings.svelte +173 -1
- package/scaffold/svelte/SoundEffects.svelte +43 -0
- package/scaffold/{svelte-minimal → svelte-core}/README-RIZZO.md +1 -1
- package/scaffold/vanilla/README-RIZZO.md +3 -3
- package/scaffold/vanilla/components/accordion.html +75 -0
- package/scaffold/vanilla/components/alert.html +75 -0
- package/scaffold/vanilla/components/avatar.html +75 -0
- package/scaffold/vanilla/components/badge.html +75 -0
- package/scaffold/vanilla/components/breadcrumb.html +75 -0
- package/scaffold/vanilla/components/button.html +75 -0
- package/scaffold/vanilla/components/cards.html +75 -0
- package/scaffold/vanilla/components/copy-to-clipboard.html +75 -0
- package/scaffold/vanilla/components/divider.html +75 -0
- package/scaffold/vanilla/components/dropdown.html +75 -0
- package/scaffold/vanilla/components/forms.html +75 -0
- package/scaffold/vanilla/components/icons.html +75 -0
- package/scaffold/vanilla/components/index.html +75 -0
- package/scaffold/vanilla/components/modal.html +75 -0
- package/scaffold/vanilla/components/navbar.html +75 -0
- package/scaffold/vanilla/components/pagination.html +75 -0
- package/scaffold/vanilla/components/progress-bar.html +75 -0
- package/scaffold/vanilla/components/search.html +75 -0
- package/scaffold/vanilla/components/settings.html +75 -0
- package/scaffold/vanilla/components/spinner.html +75 -0
- package/scaffold/vanilla/components/table.html +75 -0
- package/scaffold/vanilla/components/tabs.html +75 -0
- package/scaffold/vanilla/components/theme-switcher.html +75 -0
- package/scaffold/vanilla/components/toast.html +75 -0
- package/scaffold/vanilla/components/tooltip.html +75 -0
- package/scaffold/vanilla/index.html +75 -0
- package/scaffold/vanilla/js/main.js +20 -0
- package/scaffold/astro-minimal/src/layouts/Layout.astro +0 -29
- /package/scaffold/{astro-minimal → astro-core}/.env.example +0 -0
- /package/scaffold/{astro-minimal → astro-core}/astro.config.mjs +0 -0
- /package/scaffold/{astro-minimal → astro-core}/gitignore +0 -0
- /package/scaffold/{astro-minimal → astro-core}/package.json +0 -0
- /package/scaffold/{astro-minimal → astro-core}/public/.gitkeep +0 -0
- /package/scaffold/{astro-minimal → astro-core}/public/favicon.svg +0 -0
- /package/scaffold/{astro-minimal → astro-core}/src/pages/index.astro +0 -0
- /package/scaffold/{astro-minimal → astro-core}/tsconfig.json +0 -0
- /package/scaffold/{svelte-minimal → svelte-core}/.env.example +0 -0
- /package/scaffold/{svelte-minimal → svelte-core}/gitignore +0 -0
- /package/scaffold/{svelte-minimal → svelte-core}/package.json +0 -0
- /package/scaffold/{svelte-minimal → svelte-core}/src/app.d.ts +0 -0
- /package/scaffold/{svelte-minimal → svelte-core}/src/app.html +0 -0
- /package/scaffold/{svelte-minimal → svelte-core}/src/routes/+layout.svelte +0 -0
- /package/scaffold/{svelte-minimal → svelte-core}/src/routes/+page.svelte +0 -0
- /package/scaffold/{svelte-minimal → svelte-core}/static/.gitkeep +0 -0
- /package/scaffold/{svelte-minimal → svelte-core}/svelte.config.js +0 -0
- /package/scaffold/{svelte-minimal → svelte-core}/tsconfig.json +0 -0
|
@@ -30,7 +30,7 @@ const classes = `modal ${sizeClass} ${className}`.trim();
|
|
|
30
30
|
class="modal__overlay"
|
|
31
31
|
data-modal-overlay
|
|
32
32
|
aria-hidden={open ? 'false' : 'true'}
|
|
33
|
-
{...(!open && { inert: true })}
|
|
33
|
+
{...(!open && { inert: true, hidden: true })}
|
|
34
34
|
id={`${modalId}-overlay`}
|
|
35
35
|
></div>
|
|
36
36
|
|
|
@@ -40,7 +40,7 @@ const classes = `modal ${sizeClass} ${className}`.trim();
|
|
|
40
40
|
aria-modal="true"
|
|
41
41
|
aria-labelledby={`${modalId}-title`}
|
|
42
42
|
aria-hidden={open ? 'false' : 'true'}
|
|
43
|
-
{...(!open && { inert: true })}
|
|
43
|
+
{...(!open && { inert: true, hidden: true })}
|
|
44
44
|
id={modalId}
|
|
45
45
|
data-modal
|
|
46
46
|
>
|
|
@@ -87,10 +87,12 @@ const classes = `modal ${sizeClass} ${className}`.trim();
|
|
|
87
87
|
|
|
88
88
|
if (!overlay || !closeBtn) return;
|
|
89
89
|
|
|
90
|
-
// Ensure modal starts closed unless explicitly opened
|
|
90
|
+
// Ensure modal starts closed unless explicitly opened (hidden removes from a11y tree for axe aria-hidden-focus)
|
|
91
91
|
if (!open) {
|
|
92
|
-
modal.setAttribute('aria-hidden', 'true');
|
|
93
92
|
overlay.setAttribute('aria-hidden', 'true');
|
|
93
|
+
overlay.setAttribute('hidden', '');
|
|
94
|
+
modal.setAttribute('aria-hidden', 'true');
|
|
95
|
+
modal.setAttribute('hidden', '');
|
|
94
96
|
modal.removeAttribute('data-open');
|
|
95
97
|
}
|
|
96
98
|
|
|
@@ -134,10 +136,12 @@ const classes = `modal ${sizeClass} ${className}`.trim();
|
|
|
134
136
|
// Open modal
|
|
135
137
|
const openModal = () => {
|
|
136
138
|
previousActiveElement = document.activeElement;
|
|
137
|
-
modal.removeAttribute('inert');
|
|
138
139
|
overlay.removeAttribute('inert');
|
|
139
|
-
|
|
140
|
+
overlay.removeAttribute('hidden');
|
|
140
141
|
overlay.setAttribute('aria-hidden', 'false');
|
|
142
|
+
modal.removeAttribute('inert');
|
|
143
|
+
modal.removeAttribute('hidden');
|
|
144
|
+
modal.setAttribute('aria-hidden', 'false');
|
|
141
145
|
modal.setAttribute('data-open', 'true');
|
|
142
146
|
restoreFocusables(modal);
|
|
143
147
|
|
|
@@ -189,10 +193,12 @@ const classes = `modal ${sizeClass} ${className}`.trim();
|
|
|
189
193
|
// Close modal
|
|
190
194
|
const closeModal = () => {
|
|
191
195
|
setFocusablesInert(modal);
|
|
192
|
-
modal.setAttribute('inert', '');
|
|
193
196
|
overlay.setAttribute('inert', '');
|
|
194
|
-
|
|
197
|
+
overlay.setAttribute('hidden', '');
|
|
195
198
|
overlay.setAttribute('aria-hidden', 'true');
|
|
199
|
+
modal.setAttribute('inert', '');
|
|
200
|
+
modal.setAttribute('hidden', '');
|
|
201
|
+
modal.setAttribute('aria-hidden', 'true');
|
|
196
202
|
modal.removeAttribute('data-open');
|
|
197
203
|
|
|
198
204
|
// Remove focus trap
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import ThemeSwitcher from './ThemeSwitcher.astro';
|
|
3
|
+
import FontSwitcher from './FontSwitcher.astro';
|
|
4
|
+
import SoundEffects from './SoundEffects.astro';
|
|
3
5
|
import Close from './icons/Close.astro';
|
|
4
6
|
|
|
5
7
|
interface Props {
|
|
@@ -62,6 +64,23 @@ const { open = false } = Astro.props;
|
|
|
62
64
|
</div>
|
|
63
65
|
</section>
|
|
64
66
|
|
|
67
|
+
<!-- Font (pair: sans + mono) Section — dropdown like Theme -->
|
|
68
|
+
<section class="settings__section">
|
|
69
|
+
<h3 class="settings__section-title">Font</h3>
|
|
70
|
+
<div class="settings__control">
|
|
71
|
+
<FontSwitcher idPrefix="settings" />
|
|
72
|
+
<p class="settings__help-text">Body text and code blocks use the selected pair.</p>
|
|
73
|
+
</div>
|
|
74
|
+
</section>
|
|
75
|
+
|
|
76
|
+
<!-- Sound Section -->
|
|
77
|
+
<section class="settings__section">
|
|
78
|
+
<h3 class="settings__section-title">Sound</h3>
|
|
79
|
+
<div class="settings__control">
|
|
80
|
+
<SoundEffects />
|
|
81
|
+
</div>
|
|
82
|
+
</section>
|
|
83
|
+
|
|
65
84
|
<!-- Accessibility Section -->
|
|
66
85
|
<section class="settings__section">
|
|
67
86
|
<h3 class="settings__section-title">Accessibility</h3>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Standalone toggle for "Play sound on click". Persists in localStorage (key: soundEffects).
|
|
4
|
+
* The actual click sound is played by a global script in Layout.astro when soundEffects === 'true'.
|
|
5
|
+
* Can be used standalone or inside Settings.
|
|
6
|
+
*/
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Optional class for the wrapper (e.g. settings__control). */
|
|
9
|
+
class?: string;
|
|
10
|
+
/** If true, show the help text below the checkbox. */
|
|
11
|
+
showHelp?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { class: className = '', showHelp = true } = Astro.props;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<div class:list={[className]} data-sound-effects-wrapper>
|
|
18
|
+
<label class="settings__checkbox-label">
|
|
19
|
+
<input
|
|
20
|
+
type="checkbox"
|
|
21
|
+
class="settings__checkbox"
|
|
22
|
+
data-sound-effects
|
|
23
|
+
aria-label="Play sound on click"
|
|
24
|
+
/>
|
|
25
|
+
<span>Play sound on click</span>
|
|
26
|
+
</label>
|
|
27
|
+
{showHelp && (
|
|
28
|
+
<p class="settings__help-text">Short click sound when you interact with buttons and links. Off by default.</p>
|
|
29
|
+
)}
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<script>
|
|
33
|
+
function initSoundEffects() {
|
|
34
|
+
document.querySelectorAll<HTMLInputElement>('[data-sound-effects]').forEach((el) => {
|
|
35
|
+
if (el.hasAttribute('data-sound-effects-inited')) return;
|
|
36
|
+
el.setAttribute('data-sound-effects-inited', 'true');
|
|
37
|
+
el.checked = localStorage.getItem('soundEffects') === 'true';
|
|
38
|
+
el.addEventListener('change', () => {
|
|
39
|
+
localStorage.setItem('soundEffects', el.checked ? 'true' : 'false');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (document.readyState === 'loading') {
|
|
44
|
+
document.addEventListener('DOMContentLoaded', initSoundEffects);
|
|
45
|
+
} else {
|
|
46
|
+
initSoundEffects();
|
|
47
|
+
}
|
|
48
|
+
document.addEventListener('astro:page-load', initSoundEffects);
|
|
49
|
+
</script>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
Design system · Vanilla · Astro · Svelte
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Astro project with Rizzo CSS. Scaffolded with `npx rizzo-css init --framework astro --template full
|
|
17
|
+
Astro project with Rizzo CSS. Scaffolded with `npx rizzo-css init --framework astro --template full` or **manual**. When you choose **Full**, the CLI copies all 31 Rizzo components (Button, Modal, Navbar, Search, Settings, ThemeSwitcher, etc.) into this project so they work together; **Manual** lets you pick which of those to include (all are pre-selected by default).
|
|
18
18
|
|
|
19
19
|
## Setup
|
|
20
20
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
/* Placeholders replaced by rizzo-css CLI when scaffolding */
|
|
3
|
+
/* {{THEME_LIST_COMMENT}} */
|
|
4
|
+
const DATA_THEME = '{{DATA_THEME}}';
|
|
5
|
+
/** @type {{ title?: string }} */
|
|
6
|
+
const { title = '{{TITLE}}' } = Astro.props;
|
|
7
|
+
{{RIZZO_LAYOUT_IMPORTS}}
|
|
8
|
+
---
|
|
9
|
+
<!doctype html>
|
|
10
|
+
<html lang="en" data-theme={DATA_THEME}>
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="UTF-8" />
|
|
13
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
14
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
15
|
+
<script is:inline>
|
|
16
|
+
(function(){try{
|
|
17
|
+
var s=localStorage.getItem('theme');var d='{{DEFAULT_DARK}}';var l='{{DEFAULT_LIGHT}}';var initial=document.documentElement.getAttribute('data-theme');var r=!s?((initial&&initial!=='system')?initial:(matchMedia('(prefers-color-scheme: dark)').matches?d:l)):s==='system'?(matchMedia('(prefers-color-scheme: dark)').matches?d:l):s;document.documentElement.setAttribute('data-theme',r);
|
|
18
|
+
var fs=localStorage.getItem('fontSizeScale');if(fs)document.documentElement.style.setProperty('--font-size-scale',fs);
|
|
19
|
+
var fp=localStorage.getItem('fontPair')||'geist';if(fp==='geist'){document.documentElement.style.setProperty('--font-family','var(--font-family-geist-sans)');document.documentElement.style.setProperty('--font-family-mono','var(--font-family-geist-mono)');}else if(fp==='inter-jetbrains'){document.documentElement.style.setProperty('--font-family','var(--font-family-inter)');document.documentElement.style.setProperty('--font-family-mono','var(--font-family-jetbrains-mono)');}else if(fp==='ibm-plex'){document.documentElement.style.setProperty('--font-family','var(--font-family-ibm-plex-sans)');document.documentElement.style.setProperty('--font-family-mono','var(--font-family-ibm-plex-mono)');}else if(fp==='source'){document.documentElement.style.setProperty('--font-family','var(--font-family-source-sans-3)');document.documentElement.style.setProperty('--font-family-mono','var(--font-family-source-code-pro)');}else if(fp==='dm'){document.documentElement.style.setProperty('--font-family','var(--font-family-dm-sans)');document.documentElement.style.setProperty('--font-family-mono','var(--font-family-dm-mono)');}else if(fp==='outfit-jetbrains'){document.documentElement.style.setProperty('--font-family','var(--font-family-outfit)');document.documentElement.style.setProperty('--font-family-mono','var(--font-family-jetbrains-mono)');}
|
|
20
|
+
}catch(e){}})();
|
|
21
|
+
</script>
|
|
22
|
+
<link rel="stylesheet" href="/css/rizzo.min.css" />
|
|
23
|
+
<title>{title}</title>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
27
|
+
{{RIZZO_LAYOUT_BODY_TOP}}
|
|
28
|
+
<main id="main-content">
|
|
29
|
+
<slot />
|
|
30
|
+
</main>
|
|
31
|
+
{{RIZZO_LAYOUT_BODY_BOTTOM}}
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font pairs (sans + mono) for the Settings font changer.
|
|
3
|
+
* Each option sets both --font-family (body/UI) and --font-family-mono (code blocks, pre, kbd).
|
|
4
|
+
* Used by Settings components and layout flash scripts to apply and persist the chosen pair.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface FontPairEntry {
|
|
8
|
+
value: string;
|
|
9
|
+
label: string;
|
|
10
|
+
/** CSS value for --font-family (sans stack) */
|
|
11
|
+
sans: string;
|
|
12
|
+
/** CSS value for --font-family-mono */
|
|
13
|
+
mono: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const FONT_PAIR_DEFAULT = 'geist' as const;
|
|
17
|
+
|
|
18
|
+
export const FONT_PAIRS: FontPairEntry[] = [
|
|
19
|
+
{
|
|
20
|
+
value: 'geist',
|
|
21
|
+
label: 'Geist (Sans + Mono)',
|
|
22
|
+
sans: 'var(--font-family-geist-sans)',
|
|
23
|
+
mono: 'var(--font-family-geist-mono)',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
value: 'inter-jetbrains',
|
|
27
|
+
label: 'Inter + JetBrains Mono',
|
|
28
|
+
sans: 'var(--font-family-inter)',
|
|
29
|
+
mono: 'var(--font-family-jetbrains-mono)',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
value: 'ibm-plex',
|
|
33
|
+
label: 'IBM Plex Sans + Mono',
|
|
34
|
+
sans: 'var(--font-family-ibm-plex-sans)',
|
|
35
|
+
mono: 'var(--font-family-ibm-plex-mono)',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
value: 'source',
|
|
39
|
+
label: 'Source Sans 3 + Source Code Pro',
|
|
40
|
+
sans: 'var(--font-family-source-sans-3)',
|
|
41
|
+
mono: 'var(--font-family-source-code-pro)',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
value: 'dm',
|
|
45
|
+
label: 'DM Sans + DM Mono',
|
|
46
|
+
sans: 'var(--font-family-dm-sans)',
|
|
47
|
+
mono: 'var(--font-family-dm-mono)',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: 'outfit-jetbrains',
|
|
51
|
+
label: 'Outfit + JetBrains Mono',
|
|
52
|
+
sans: 'var(--font-family-outfit)',
|
|
53
|
+
mono: 'var(--font-family-jetbrains-mono)',
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
export function getFontPairById(id: string): FontPairEntry | undefined {
|
|
58
|
+
return FONT_PAIRS.find((p) => p.value === id);
|
|
59
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { DOCS_NAV } from '../../config/docsNav';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Current URL pathname (e.g. $page.url.pathname or window.location.pathname). */
|
|
6
|
+
currentPath: string;
|
|
7
|
+
/** Path prefix for framework-specific links (e.g. /docs, /docs/svelte, /docs/vanilla). */
|
|
8
|
+
pathPrefix?: string;
|
|
9
|
+
}
|
|
10
|
+
let { currentPath, pathPrefix = '/docs' }: Props = $props();
|
|
11
|
+
|
|
12
|
+
function fullHref(link: { href: string; frameworkOnly?: boolean }): string {
|
|
13
|
+
const base = link.frameworkOnly ? pathPrefix : '/docs';
|
|
14
|
+
return `${base}/${link.href}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isActive(link: { href: string; frameworkOnly?: boolean }): boolean {
|
|
18
|
+
const path = currentPath.replace(/\/$/, '');
|
|
19
|
+
const href = fullHref(link);
|
|
20
|
+
return path === href;
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<aside id="docs-sidebar" class="docs-sidebar" aria-label="Documentation navigation" tabindex="0">
|
|
25
|
+
<nav class="docs-sidebar__nav">
|
|
26
|
+
{#each DOCS_NAV as group}
|
|
27
|
+
<div class="docs-sidebar__group">
|
|
28
|
+
<h2 class="docs-sidebar__group-label">{group.label}</h2>
|
|
29
|
+
<ul class="docs-sidebar__list">
|
|
30
|
+
{#each group.links as link}
|
|
31
|
+
{@const href = fullHref(link)}
|
|
32
|
+
{@const active = isActive(link)}
|
|
33
|
+
<li class="docs-sidebar__item">
|
|
34
|
+
<a
|
|
35
|
+
href={href}
|
|
36
|
+
class="docs-sidebar__link"
|
|
37
|
+
class:docs-sidebar__link--active={active}
|
|
38
|
+
>
|
|
39
|
+
{link.label}
|
|
40
|
+
</a>
|
|
41
|
+
</li>
|
|
42
|
+
{/each}
|
|
43
|
+
</ul>
|
|
44
|
+
</div>
|
|
45
|
+
{/each}
|
|
46
|
+
</nav>
|
|
47
|
+
</aside>
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import ChevronDown from './icons/ChevronDown.svelte';
|
|
4
|
+
import { FONT_PAIRS, FONT_PAIR_DEFAULT } from '../../config/fonts';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Optional prefix for trigger/menu IDs when multiple FontSwitchers exist. */
|
|
8
|
+
idPrefix?: string;
|
|
9
|
+
}
|
|
10
|
+
let { idPrefix = '' }: Props = $props();
|
|
11
|
+
|
|
12
|
+
const triggerId = idPrefix ? `font-pair-trigger-${idPrefix}` : 'font-pair-trigger';
|
|
13
|
+
const menuId = idPrefix ? `font-pair-menu-${idPrefix}` : 'font-pair-menu';
|
|
14
|
+
|
|
15
|
+
let open = $state(false);
|
|
16
|
+
let selectedValue = $state(FONT_PAIR_DEFAULT);
|
|
17
|
+
let previewOption = $state<{ sans: string; mono: string } | null>(null);
|
|
18
|
+
let menuEl: HTMLElement | null = $state(null);
|
|
19
|
+
let triggerEl: HTMLElement | null = $state(null);
|
|
20
|
+
|
|
21
|
+
const selectedPair = $derived(FONT_PAIRS.find((p) => p.value === selectedValue) ?? FONT_PAIRS[0]);
|
|
22
|
+
const label = $derived(selectedPair?.label ?? 'Font');
|
|
23
|
+
|
|
24
|
+
function applyPair(sans: string, mono: string) {
|
|
25
|
+
if (typeof document === 'undefined') return;
|
|
26
|
+
document.documentElement.style.setProperty('--font-family', sans);
|
|
27
|
+
document.documentElement.style.setProperty('--font-family-mono', mono);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function selectPair(value: string) {
|
|
31
|
+
const pair = FONT_PAIRS.find((p) => p.value === value);
|
|
32
|
+
if (!pair) return;
|
|
33
|
+
selectedValue = value;
|
|
34
|
+
applyPair(pair.sans, pair.mono);
|
|
35
|
+
localStorage?.setItem('fontPair', value);
|
|
36
|
+
open = false;
|
|
37
|
+
previewOption = null;
|
|
38
|
+
triggerEl?.focus();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setPreview(option: { sans: string; mono: string } | null) {
|
|
42
|
+
previewOption = option;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function addClickOutsideListener() {
|
|
46
|
+
const handler = (e: MouseEvent) => {
|
|
47
|
+
const target = e.target as Node;
|
|
48
|
+
if (menuEl && !menuEl.contains(target) && triggerEl && !triggerEl.contains(target)) {
|
|
49
|
+
open = false;
|
|
50
|
+
setPreview(null);
|
|
51
|
+
document.removeEventListener('click', handler);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
setTimeout(() => document.addEventListener('click', handler), 0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onMount(() => {
|
|
58
|
+
try {
|
|
59
|
+
const saved = typeof localStorage !== 'undefined' && localStorage.getItem ? localStorage.getItem('fontPair') || FONT_PAIR_DEFAULT : FONT_PAIR_DEFAULT;
|
|
60
|
+
const pair = FONT_PAIRS.find((p) => p.value === saved);
|
|
61
|
+
if (pair) {
|
|
62
|
+
selectedValue = saved;
|
|
63
|
+
applyPair(pair.sans, pair.mono);
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
selectedValue = FONT_PAIR_DEFAULT;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function openMenu() {
|
|
71
|
+
open = true;
|
|
72
|
+
setPreview(FONT_PAIRS.find((p) => p.value === selectedValue) ?? null);
|
|
73
|
+
addClickOutsideListener();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
77
|
+
if (!open) {
|
|
78
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
openMenu();
|
|
81
|
+
} else if (e.key === 'ArrowDown') {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
openMenu();
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (e.key === 'Escape') {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
open = false;
|
|
90
|
+
setPreview(null);
|
|
91
|
+
triggerEl?.focus();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<div class="font-switcher" data-font-switcher>
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
class="font-switcher__trigger"
|
|
100
|
+
aria-expanded={open}
|
|
101
|
+
aria-haspopup="true"
|
|
102
|
+
aria-controls={menuId}
|
|
103
|
+
aria-label="Select font pair"
|
|
104
|
+
id={triggerId}
|
|
105
|
+
bind:this={triggerEl}
|
|
106
|
+
onclick={() => {
|
|
107
|
+
if (open) {
|
|
108
|
+
open = false;
|
|
109
|
+
setPreview(null);
|
|
110
|
+
} else {
|
|
111
|
+
openMenu();
|
|
112
|
+
}
|
|
113
|
+
}}
|
|
114
|
+
onkeydown={handleKeydown}
|
|
115
|
+
>
|
|
116
|
+
<span class="font-switcher__label" data-font-pair-label>{label}</span>
|
|
117
|
+
<ChevronDown class="font-switcher__icon" width={16} height={16} />
|
|
118
|
+
</button>
|
|
119
|
+
<div
|
|
120
|
+
class="font-switcher__menu"
|
|
121
|
+
class:font-switcher__menu--open={open}
|
|
122
|
+
id={menuId}
|
|
123
|
+
role="menu"
|
|
124
|
+
aria-labelledby={triggerId}
|
|
125
|
+
aria-label="Font pair selection"
|
|
126
|
+
aria-orientation="vertical"
|
|
127
|
+
aria-hidden={!open}
|
|
128
|
+
tabindex="-1"
|
|
129
|
+
bind:this={menuEl}
|
|
130
|
+
onmouseleave={() => setPreview(null)}
|
|
131
|
+
>
|
|
132
|
+
<div class="font-switcher__menu-options">
|
|
133
|
+
{#each FONT_PAIRS as pair (pair.value)}
|
|
134
|
+
<div
|
|
135
|
+
class="font-switcher__option"
|
|
136
|
+
class:font-switcher__option--active={selectedValue === pair.value}
|
|
137
|
+
role="menuitemradio"
|
|
138
|
+
aria-checked={selectedValue === pair.value}
|
|
139
|
+
tabindex={open ? 0 : -1}
|
|
140
|
+
data-font-pair-value={pair.value}
|
|
141
|
+
data-font-pair-sans={pair.sans}
|
|
142
|
+
data-font-pair-mono={pair.mono}
|
|
143
|
+
data-font-pair-label={pair.label}
|
|
144
|
+
onmouseenter={() => setPreview({ sans: pair.sans, mono: pair.mono })}
|
|
145
|
+
onfocus={() => setPreview({ sans: pair.sans, mono: pair.mono })}
|
|
146
|
+
onclick={() => selectPair(pair.value)}
|
|
147
|
+
onkeydown={(e) => {
|
|
148
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
selectPair(pair.value);
|
|
151
|
+
}
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
{pair.label}
|
|
155
|
+
</div>
|
|
156
|
+
{/each}
|
|
157
|
+
</div>
|
|
158
|
+
<div
|
|
159
|
+
class="font-switcher__preview"
|
|
160
|
+
data-font-preview
|
|
161
|
+
aria-hidden={!previewOption}
|
|
162
|
+
>
|
|
163
|
+
<div class="font-switcher__preview-title">Preview</div>
|
|
164
|
+
<div
|
|
165
|
+
class="font-switcher__preview-sample"
|
|
166
|
+
data-font-preview-sample
|
|
167
|
+
style={previewOption ? `font-family: ${previewOption.sans}` : ''}
|
|
168
|
+
>
|
|
169
|
+
Aa Bb Cc 012
|
|
170
|
+
</div>
|
|
171
|
+
<div
|
|
172
|
+
class="font-switcher__preview-mono"
|
|
173
|
+
data-font-preview-mono
|
|
174
|
+
style={previewOption ? `font-family: ${previewOption.mono}` : ''}
|
|
175
|
+
>
|
|
176
|
+
code
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
@@ -129,6 +129,7 @@
|
|
|
129
129
|
data-modal-overlay
|
|
130
130
|
aria-hidden={!open}
|
|
131
131
|
inert={!open ? true : undefined}
|
|
132
|
+
hidden={open ? undefined : true}
|
|
132
133
|
id="{modalId}-overlay"
|
|
133
134
|
onclick={handleOverlayClick}
|
|
134
135
|
role="presentation"
|
|
@@ -142,6 +143,7 @@
|
|
|
142
143
|
aria-labelledby="{modalId}-title"
|
|
143
144
|
aria-hidden={!open}
|
|
144
145
|
inert={!open ? true : undefined}
|
|
146
|
+
hidden={open ? undefined : true}
|
|
145
147
|
id={modalId}
|
|
146
148
|
data-modal
|
|
147
149
|
data-open={open || undefined}
|