@hraness/design-kit 0.31.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.
- package/ARTICLE_COPY.md +208 -0
- package/HERO_FIELDS.md +55 -0
- package/ICONS.md +56 -0
- package/LANTERN_MATERIAL.md +60 -0
- package/LICENSE +21 -0
- package/MARKETING_COPY.md +49 -0
- package/MARKETING_PRESET.md +108 -0
- package/PALETTES.md +90 -0
- package/PAPER_THEME.md +128 -0
- package/README.md +745 -0
- package/STATUS_PAGES.md +67 -0
- package/STYLE.md +245 -0
- package/WRITING.md +72 -0
- package/dist/browser/index.js +2345 -0
- package/dist/chunk-44djxk16.js +6872 -0
- package/dist/chunk-5gtx3pza.js +9 -0
- package/dist/chunk-77391vmq.js +454 -0
- package/dist/chunk-7b1586eb.js +1209 -0
- package/dist/chunk-8834fh4n.js +6 -0
- package/dist/chunk-9t8xyqte.js +870 -0
- package/dist/chunk-cejpzyfh.js +446 -0
- package/dist/chunk-eh71jz57.js +429 -0
- package/dist/chunk-evb02bc1.js +516 -0
- package/dist/chunk-h4k7yv6x.js +368 -0
- package/dist/chunk-he8eznb1.js +760 -0
- package/dist/chunk-mr1vdcjq.js +404 -0
- package/dist/chunk-wzvdn8ey.js +116 -0
- package/dist/fonts/nebula-sans/social-fonts.generated.js +53 -0
- package/dist/icons.js +48 -0
- package/dist/index.js +237 -0
- package/dist/launch.js +41 -0
- package/dist/mockups/client.js +422 -0
- package/dist/mockups/index.js +1850 -0
- package/dist/portfolio.js +1549 -0
- package/dist/provider-marks.js +19 -0
- package/dist/react/charts.js +11 -0
- package/dist/react/hero-backdrop.js +8 -0
- package/dist/react/index.js +7252 -0
- package/dist/react/platform-install.js +10 -0
- package/dist/react/server.js +137 -0
- package/dist/stylex-manifest.json +1 -0
- package/dist/stylex.css +5392 -0
- package/dist/syntax-highlighting.js +15 -0
- package/dist/testing.js +250 -0
- package/package.json +377 -0
- package/portfolio-inventory.json +31 -0
- package/scripts/check-lantern-material-snapshot.d.mts +26 -0
- package/scripts/check-lantern-material-snapshot.mjs +126 -0
- package/scripts/check-marketing-snapshot.d.mts +12 -0
- package/scripts/check-marketing-snapshot.mjs +58 -0
- package/scripts/lantern-material-snapshot.ts +101 -0
- package/scripts/marketing-textures.ts +48 -0
- package/scripts/paper-theme-snapshot.ts +97 -0
- package/scripts/product-marketing-snapshot.ts +149 -0
- package/src/appearance-menu.css +266 -0
- package/src/appearance.ts +32 -0
- package/src/article-html.ts +269 -0
- package/src/article.ts +686 -0
- package/src/browser/appearance-menu.ts +604 -0
- package/src/browser/artifact-share.ts +164 -0
- package/src/browser/design-palette.ts +197 -0
- package/src/browser/foil.ts +124 -0
- package/src/browser/hero-light.ts +11 -0
- package/src/browser/index.ts +29 -0
- package/src/browser/status-field.ts +432 -0
- package/src/browser/status-page.ts +100 -0
- package/src/browser/sticky-offset.ts +82 -0
- package/src/browser/theme-color-sync.ts +213 -0
- package/src/charts.css +7 -0
- package/src/compiler-components.css +34 -0
- package/src/compiler-foundation.css +18 -0
- package/src/compiler-palettes.css +7 -0
- package/src/compiler-tokens.css +342 -0
- package/src/components.css +3 -0
- package/src/design-gallery.css +431 -0
- package/src/effects.css +14 -0
- package/src/fonts/geist-mono/GeistMono[wght].woff2 +0 -0
- package/src/fonts/geist-mono/OFL.txt +93 -0
- package/src/fonts/geist-mono/PROVENANCE.md +12 -0
- package/src/fonts/instrument-serif/OFL.txt +93 -0
- package/src/fonts/instrument-serif/UPSTREAM.md +14 -0
- package/src/fonts/instrument-serif/instrument-serif-latin-400.woff2 +0 -0
- package/src/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/fonts/nebula-sans/NebulaSans-Black.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-BlackItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Bold.otf +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Bold.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-BoldItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Book.otf +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-BookItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Light.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-LightItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Medium.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-MediumItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Semibold.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-SemiboldItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/PROVENANCE.md +14 -0
- package/src/fonts/nebula-sans/social-fonts.generated.ts +3873 -0
- package/src/fonts.css +106 -0
- package/src/icons/act60/act60.svg +5 -0
- package/src/icons/act60/application.svg +9 -0
- package/src/icons/act60/chapter-compare.svg +3 -0
- package/src/icons/act60/decree-residence.svg +6 -0
- package/src/icons/act60/estimate.svg +7 -0
- package/src/icons/act60/export-services.svg +4 -0
- package/src/icons/act60/investor.svg +13 -0
- package/src/icons/act60/presence.svg +7 -0
- package/src/icons/aicharts/aicharts.svg +6 -0
- package/src/icons/aicharts/cost-compare.svg +4 -0
- package/src/icons/aicharts/task-audio.svg +7 -0
- package/src/icons/aicharts/task-coding.svg +6 -0
- package/src/icons/aicharts/task-images.svg +3 -0
- package/src/icons/aicharts/task-reasoning.svg +9 -0
- package/src/icons/aicharts/task-research.svg +5 -0
- package/src/icons/aicharts/task-video.svg +4 -0
- package/src/icons/manifest.json +1371 -0
- package/src/icons/platonik/conversation.svg +3 -0
- package/src/icons/platonik/creature.svg +7 -0
- package/src/icons/platonik/economy.svg +4 -0
- package/src/icons/platonik/keep-light.svg +6 -0
- package/src/icons/platonik/platonik.svg +7 -0
- package/src/icons/platonik/spark-route.svg +15 -0
- package/src/icons/receipts/act60-retry.receipt.json +888 -0
- package/src/icons/receipts/act60.receipt.json +1366 -0
- package/src/icons/receipts/aicharts.receipt.json +1546 -0
- package/src/icons/receipts/platonik.receipt.json +1011 -0
- package/src/icons/receipts/roughday.receipt.json +770 -0
- package/src/icons/receipts/shared.receipt.json +1296 -0
- package/src/icons/receipts/slopcamera.receipt.json +1495 -0
- package/src/icons/receipts/soundfish-retry.receipt.json +460 -0
- package/src/icons/receipts/soundfish.receipt.json +1709 -0
- package/src/icons/receipts/sponge.receipt.json +1743 -0
- package/src/icons/receipts/stripe-history-retry.receipt.json +145 -0
- package/src/icons/receipts/stripe-history.receipt.json +1202 -0
- package/src/icons/receipts/wordcell.receipt.json +1924 -0
- package/src/icons/roughday/finance.svg +7 -0
- package/src/icons/roughday/roughday.svg +6 -0
- package/src/icons/roughday/technology.svg +4 -0
- package/src/icons/roughday/world.svg +4 -0
- package/src/icons/sets/act60.json +63 -0
- package/src/icons/sets/aicharts.json +63 -0
- package/src/icons/sets/platonik.json +39 -0
- package/src/icons/sets/roughday.json +25 -0
- package/src/icons/sets/shared.json +12 -0
- package/src/icons/sets/slopcamera.json +47 -0
- package/src/icons/sets/soundfish.json +43 -0
- package/src/icons/sets/sponge.json +51 -0
- package/src/icons/sets/stripe-history.json +39 -0
- package/src/icons/sets/wordcell.json +51 -0
- package/src/icons/shared/agent-skill.svg +4 -0
- package/src/icons/shared/cli.svg +5 -0
- package/src/icons/shared/privacy.svg +3 -0
- package/src/icons/shared/research.svg +7 -0
- package/src/icons/shared/sdk.svg +3 -0
- package/src/icons/slopcamera/diagram.svg +3 -0
- package/src/icons/slopcamera/direct-scene.svg +5 -0
- package/src/icons/slopcamera/edit-video.svg +17 -0
- package/src/icons/slopcamera/mcp.svg +3 -0
- package/src/icons/slopcamera/native-world.svg +3 -0
- package/src/icons/slopcamera/slopcamera.svg +11 -0
- package/src/icons/soundfish/agent-ui.svg +7 -0
- package/src/icons/soundfish/collaborate.svg +5 -0
- package/src/icons/soundfish/compose.svg +3 -0
- package/src/icons/soundfish/hear.svg +6 -0
- package/src/icons/soundfish/human-ui.svg +6 -0
- package/src/icons/soundfish/share.svg +3 -0
- package/src/icons/soundfish/soundfish.svg +6 -0
- package/src/icons/sponge/api.svg +3 -0
- package/src/icons/sponge/documents.svg +3 -0
- package/src/icons/sponge/library.svg +3 -0
- package/src/icons/sponge/one-question.svg +5 -0
- package/src/icons/sponge/people-direct.svg +5 -0
- package/src/icons/sponge/read.svg +3 -0
- package/src/icons/sponge/save.svg +4 -0
- package/src/icons/sponge/sponge.svg +7 -0
- package/src/icons/stripe-history/company-history.svg +3 -0
- package/src/icons/stripe-history/evidence.svg +4 -0
- package/src/icons/stripe-history/independence.svg +7 -0
- package/src/icons/stripe-history/publications.svg +11 -0
- package/src/icons/stripe-history/sources.svg +6 -0
- package/src/icons/stripe-history/stripe-history.svg +3 -0
- package/src/icons/wordcell/backlinks.svg +6 -0
- package/src/icons/wordcell/capture.svg +6 -0
- package/src/icons/wordcell/git-provenance.svg +4 -0
- package/src/icons/wordcell/kb.svg +4 -0
- package/src/icons/wordcell/markdown.svg +4 -0
- package/src/icons/wordcell/scopes.svg +3 -0
- package/src/icons/wordcell/search.svg +7 -0
- package/src/icons.generated.ts +734 -0
- package/src/icons.ts +51 -0
- package/src/index.ts +275 -0
- package/src/lantern-material.css +213 -0
- package/src/launch.ts +624 -0
- package/src/marketing-assets/UPSTREAM.md +18 -0
- package/src/marketing-assets/cells.svg +1 -0
- package/src/marketing-assets/grain.svg +1 -0
- package/src/marketing-forced-colors.css +12 -0
- package/src/mockups/client.tsx +415 -0
- package/src/mockups/core.tsx +318 -0
- package/src/mockups/frames.tsx +368 -0
- package/src/mockups/index.ts +14 -0
- package/src/mockups/surfaces.tsx +681 -0
- package/src/mockups.css +2607 -0
- package/src/palette-appearance.ts +45 -0
- package/src/palette-bridge.css +124 -0
- package/src/palette-color.ts +41 -0
- package/src/palette-system.css +721 -0
- package/src/palette-themes.ts +23 -0
- package/src/palette-tokens.stylex.ts +519 -0
- package/src/palettes.css +3 -0
- package/src/palettes.ts +161 -0
- package/src/paper-theme.css +244 -0
- package/src/plain-publication.css +1241 -0
- package/src/plain-site.css +397 -0
- package/src/platforms.ts +151 -0
- package/src/portfolio.generated.json +1595 -0
- package/src/portfolio.generated.ts +1596 -0
- package/src/portfolio.ts +226 -0
- package/src/product-marketing-foundation.css +433 -0
- package/src/product-marketing-preset.css +261 -0
- package/src/product-marketing.css +2186 -0
- package/src/provider-marks.generated.ts +112 -0
- package/src/provider-marks.ts +143 -0
- package/src/react/animated-rail-stage.stylex.ts +15 -0
- package/src/react/animated-rail-stage.tsx +51 -0
- package/src/react/app-shell.stylex.ts +104 -0
- package/src/react/app-shell.tsx +121 -0
- package/src/react/article.tsx +602 -0
- package/src/react/aurora-dots-background.tsx +34 -0
- package/src/react/charts.stylex.ts +563 -0
- package/src/react/charts.tsx +610 -0
- package/src/react/chat.stylex.ts +34 -0
- package/src/react/chat.tsx +155 -0
- package/src/react/design-gallery.tsx +943 -0
- package/src/react/design-palette.stylex.ts +124 -0
- package/src/react/design-palette.tsx +196 -0
- package/src/react/design-theme-context.tsx +41 -0
- package/src/react/effects.stylex.ts +325 -0
- package/src/react/fader.stylex.ts +71 -0
- package/src/react/fader.tsx +151 -0
- package/src/react/foil-card-math.ts +100 -0
- package/src/react/foil-card-surface.stylex.ts +326 -0
- package/src/react/foil-card-surface.tsx +742 -0
- package/src/react/foil-mark.tsx +35 -0
- package/src/react/foil.stylex.ts +163 -0
- package/src/react/haptics.ts +228 -0
- package/src/react/hero-backdrop.tsx +22 -0
- package/src/react/index.ts +38 -0
- package/src/react/keyboard-shortcuts.ts +228 -0
- package/src/react/lantern-material-gallery.tsx +166 -0
- package/src/react/lantern-material.stylex.ts +39 -0
- package/src/react/launch-beats.tsx +73 -0
- package/src/react/navigation-rail.stylex.ts +130 -0
- package/src/react/navigation-rail.tsx +228 -0
- package/src/react/particle-halo.tsx +115 -0
- package/src/react/phaser-dots.tsx +501 -0
- package/src/react/platform-icons.tsx +94 -0
- package/src/react/platform-install.stylex.ts +385 -0
- package/src/react/platform-install.tsx +389 -0
- package/src/react/playback-transport.stylex.ts +14 -0
- package/src/react/playback-transport.tsx +112 -0
- package/src/react/procedural-backdrop.tsx +192 -0
- package/src/react/procedural-recipe.ts +323 -0
- package/src/react/product-marketing.stylex.ts +4469 -0
- package/src/react/product-marketing.tsx +1611 -0
- package/src/react/production-data-preview-notice.stylex.ts +40 -0
- package/src/react/production-data-preview-notice.tsx +34 -0
- package/src/react/provider-mark.stylex.ts +132 -0
- package/src/react/provider-mark.tsx +120 -0
- package/src/react/relative-time.tsx +117 -0
- package/src/react/route-state.stylex.ts +39 -0
- package/src/react/route-state.tsx +350 -0
- package/src/react/server.ts +11 -0
- package/src/react/social-kit-panel.tsx +103 -0
- package/src/react/sticky-offset.tsx +33 -0
- package/src/react/surfaces.stylex.ts +179 -0
- package/src/react/surfaces.tsx +330 -0
- package/src/react/syntax-code.tsx +33 -0
- package/src/react/theme-color-sync.ts +1 -0
- package/src/react/theme-resolution.ts +11 -0
- package/src/react/theme.stylex.ts +276 -0
- package/src/react/theme.tsx +402 -0
- package/src/reading.css +72 -0
- package/src/relative-time.ts +185 -0
- package/src/reset.css +2 -0
- package/src/site-shell.css +16 -0
- package/src/status-page-html.ts +63 -0
- package/src/status-page.css +276 -0
- package/src/status-page.ts +323 -0
- package/src/styles.css +21 -0
- package/src/syntax-code-lexers.ts +303 -0
- package/src/syntax-highlighting.css +78 -0
- package/src/syntax-highlighting.ts +477 -0
- package/src/syntax-tokens.ts +33 -0
- package/src/testing.ts +307 -0
- package/src/tokens.css +3 -0
- package/src/typography.css +86 -0
- package/vendor/evilcharts/LICENSE +21 -0
- package/vendor/evilcharts/UPSTREAM.md +14 -0
- package/vendor/platform-marks/LICENSE +30 -0
- package/vendor/platform-marks/UPSTREAM.md +15 -0
- package/vendor/platform-marks/apple.svg +1 -0
- package/vendor/platform-marks/linux.svg +1 -0
- package/vendor/provider-marks/CRUSH-LICENSE.md +134 -0
- package/vendor/provider-marks/LICENSE +21 -0
- package/vendor/provider-marks/UPSTREAM.md +27 -0
- package/vendor/provider-marks/aider.svg +3 -0
- package/vendor/provider-marks/crush-heartbit.svg +8 -0
- package/vendor/provider-marks/lobehub/alibabacloud-color.svg +1 -0
- package/vendor/provider-marks/lobehub/alibabacloud.svg +1 -0
- package/vendor/provider-marks/lobehub/amp-color.svg +1 -0
- package/vendor/provider-marks/lobehub/amp.svg +1 -0
- package/vendor/provider-marks/lobehub/anthropic.svg +1 -0
- package/vendor/provider-marks/lobehub/claude-color.svg +1 -0
- package/vendor/provider-marks/lobehub/claude.svg +1 -0
- package/vendor/provider-marks/lobehub/claudecode-color.svg +1 -0
- package/vendor/provider-marks/lobehub/claudecode.svg +1 -0
- package/vendor/provider-marks/lobehub/codex-color.svg +1 -0
- package/vendor/provider-marks/lobehub/codex.svg +1 -0
- package/vendor/provider-marks/lobehub/cursor.svg +1 -0
- package/vendor/provider-marks/lobehub/deepseek-color.svg +1 -0
- package/vendor/provider-marks/lobehub/deepseek.svg +1 -0
- package/vendor/provider-marks/lobehub/devin-color.svg +1 -0
- package/vendor/provider-marks/lobehub/devin.svg +1 -0
- package/vendor/provider-marks/lobehub/gemini-color.svg +1 -0
- package/vendor/provider-marks/lobehub/gemini.svg +1 -0
- package/vendor/provider-marks/lobehub/geminicli-color.svg +1 -0
- package/vendor/provider-marks/lobehub/geminicli.svg +1 -0
- package/vendor/provider-marks/lobehub/goose.svg +1 -0
- package/vendor/provider-marks/lobehub/meta-color.svg +1 -0
- package/vendor/provider-marks/lobehub/meta.svg +1 -0
- package/vendor/provider-marks/lobehub/mistral-color.svg +1 -0
- package/vendor/provider-marks/lobehub/mistral.svg +1 -0
- package/vendor/provider-marks/lobehub/moonshot.svg +1 -0
- package/vendor/provider-marks/lobehub/nvidia-color.svg +1 -0
- package/vendor/provider-marks/lobehub/nvidia.svg +1 -0
- package/vendor/provider-marks/lobehub/openai.svg +1 -0
- package/vendor/provider-marks/lobehub/opencode.svg +1 -0
- package/vendor/provider-marks/lobehub/perplexity-color.svg +1 -0
- package/vendor/provider-marks/lobehub/perplexity.svg +1 -0
- package/vendor/provider-marks/lobehub/qwen-color.svg +1 -0
- package/vendor/provider-marks/lobehub/qwen.svg +1 -0
- package/vendor/provider-marks/lobehub/xai.svg +1 -0
- package/vendor/provider-marks/lobehub/zai.svg +1 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ComputerIcon,
|
|
3
|
+
Moon02Icon,
|
|
4
|
+
Sun03Icon,
|
|
5
|
+
} from "@hugeicons/core-free-icons";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
defaultDesignTheme,
|
|
9
|
+
designThemeStorageKey,
|
|
10
|
+
designThemeLabel,
|
|
11
|
+
designThemes,
|
|
12
|
+
type DesignTheme,
|
|
13
|
+
isDesignTheme,
|
|
14
|
+
resolveDesignTheme,
|
|
15
|
+
} from "../appearance.js";
|
|
16
|
+
import {
|
|
17
|
+
acquireThemeColorMeta,
|
|
18
|
+
type ThemeColorMetaRegistration,
|
|
19
|
+
} from "./theme-color-sync.js";
|
|
20
|
+
|
|
21
|
+
const menuSelector = "[data-hraness-appearance-menu]";
|
|
22
|
+
const triggerSelector = ".hraness-design-theme-toggle__trigger";
|
|
23
|
+
const popoverSelector = ".hraness-design-theme-toggle__popover";
|
|
24
|
+
const menuElementSelector = ".hraness-design-theme-toggle__menu";
|
|
25
|
+
const itemSelector = ".hraness-design-theme-toggle__item[data-theme-value]";
|
|
26
|
+
const systemDarkQuery = "(prefers-color-scheme: dark)";
|
|
27
|
+
const svgNamespace = "http://www.w3.org/2000/svg";
|
|
28
|
+
|
|
29
|
+
type IconSvgElement = readonly (
|
|
30
|
+
readonly [string, Readonly<Record<string, string | number>>]
|
|
31
|
+
)[];
|
|
32
|
+
|
|
33
|
+
const appearanceIcons: Readonly<Record<DesignTheme, IconSvgElement>> = {
|
|
34
|
+
dark: Moon02Icon,
|
|
35
|
+
light: Sun03Icon,
|
|
36
|
+
system: ComputerIcon,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface AppearanceStorage {
|
|
40
|
+
readonly getItem: (key: string) => unknown;
|
|
41
|
+
readonly setItem: (key: string, value: string) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AppearanceMenuOptions {
|
|
45
|
+
readonly darkThemeColor: string;
|
|
46
|
+
readonly document?: Document;
|
|
47
|
+
readonly lightThemeColor: string;
|
|
48
|
+
readonly metaName?: string;
|
|
49
|
+
readonly storage?: AppearanceStorage | null;
|
|
50
|
+
readonly storageKey?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface AppearanceMenuInstallation {
|
|
54
|
+
readonly dispose: () => void;
|
|
55
|
+
readonly preference: () => DesignTheme;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface AppearanceMenuElements {
|
|
59
|
+
readonly items: ReadonlyMap<DesignTheme, HTMLElement>;
|
|
60
|
+
readonly menu: HTMLElement;
|
|
61
|
+
readonly popover: HTMLElement;
|
|
62
|
+
readonly root: HTMLElement;
|
|
63
|
+
readonly trigger: HTMLButtonElement;
|
|
64
|
+
typeahead: string;
|
|
65
|
+
typeaheadTimer: ReturnType<typeof setTimeout> | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface InstallationState {
|
|
69
|
+
disposed: boolean;
|
|
70
|
+
readonly publicInstallation: AppearanceMenuInstallation;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const installationByDocument = new WeakMap<Document, InstallationState>();
|
|
74
|
+
|
|
75
|
+
function requireNonBlank(value: string, name: string): string {
|
|
76
|
+
if (value.trim().length === 0) throw new Error(`${name} must not be blank.`);
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveDocument(candidate: Document | undefined): Document {
|
|
81
|
+
const ownedDocument = candidate
|
|
82
|
+
?? (typeof globalThis.document === "undefined" ? undefined : globalThis.document);
|
|
83
|
+
if (ownedDocument === undefined) {
|
|
84
|
+
throw new Error("installAppearanceMenus requires a browser Document.");
|
|
85
|
+
}
|
|
86
|
+
if (ownedDocument.documentElement === null || ownedDocument.head === null) {
|
|
87
|
+
throw new Error("installAppearanceMenus requires a complete HTML document.");
|
|
88
|
+
}
|
|
89
|
+
return ownedDocument;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function defaultStorage(document: Document): AppearanceStorage | null {
|
|
93
|
+
try {
|
|
94
|
+
return document.defaultView?.localStorage ?? null;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function storedPreference(storage: AppearanceStorage | null, storageKey: string): DesignTheme {
|
|
101
|
+
if (storage === null) return defaultDesignTheme;
|
|
102
|
+
try {
|
|
103
|
+
const value = storage.getItem(storageKey);
|
|
104
|
+
if (value === null || value === undefined) return defaultDesignTheme;
|
|
105
|
+
if (isDesignTheme(value)) return value;
|
|
106
|
+
storage.setItem(storageKey, defaultDesignTheme);
|
|
107
|
+
} catch {
|
|
108
|
+
// Appearance persistence is optional in privacy-restricted browsers.
|
|
109
|
+
}
|
|
110
|
+
return defaultDesignTheme;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function persistPreference(
|
|
114
|
+
storage: AppearanceStorage | null,
|
|
115
|
+
storageKey: string,
|
|
116
|
+
preference: DesignTheme,
|
|
117
|
+
): void {
|
|
118
|
+
if (storage === null) return;
|
|
119
|
+
try {
|
|
120
|
+
storage.setItem(storageKey, preference);
|
|
121
|
+
} catch {
|
|
122
|
+
// The current page still changes when persistence is unavailable.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isElement(value: EventTarget | null): value is Element {
|
|
127
|
+
return value !== null
|
|
128
|
+
&& typeof value === "object"
|
|
129
|
+
&& "nodeType" in value
|
|
130
|
+
&& (value as Node).nodeType === 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function attributeName(name: string): string {
|
|
134
|
+
return name.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function appearanceIcon(document: Document, theme: DesignTheme): SVGSVGElement {
|
|
138
|
+
const svg = document.createElementNS(svgNamespace, "svg");
|
|
139
|
+
svg.setAttribute("aria-hidden", "true");
|
|
140
|
+
svg.setAttribute("class", "hraness-icon");
|
|
141
|
+
svg.setAttribute("color", "currentColor");
|
|
142
|
+
svg.setAttribute("data-slot", "icon");
|
|
143
|
+
svg.setAttribute("fill", "none");
|
|
144
|
+
svg.setAttribute("height", "18");
|
|
145
|
+
svg.setAttribute("stroke", "currentColor");
|
|
146
|
+
svg.setAttribute("stroke-width", "1.5");
|
|
147
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
148
|
+
svg.setAttribute("width", "18");
|
|
149
|
+
|
|
150
|
+
for (const [tag, attributes] of appearanceIcons[theme]) {
|
|
151
|
+
const child = document.createElementNS(svgNamespace, tag);
|
|
152
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
153
|
+
if (name !== "key") child.setAttribute(attributeName(name), String(value));
|
|
154
|
+
}
|
|
155
|
+
child.setAttribute("stroke", "currentColor");
|
|
156
|
+
child.setAttribute("stroke-width", "1.5");
|
|
157
|
+
svg.append(child);
|
|
158
|
+
}
|
|
159
|
+
return svg;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function renderIconHost(
|
|
163
|
+
document: Document,
|
|
164
|
+
host: HTMLElement,
|
|
165
|
+
theme: DesignTheme,
|
|
166
|
+
): void {
|
|
167
|
+
host.classList.add("hraness-appearance-icon");
|
|
168
|
+
host.setAttribute("aria-hidden", "true");
|
|
169
|
+
host.setAttribute("data-appearance-icon", theme);
|
|
170
|
+
host.setAttribute("data-slot", "appearance-icon");
|
|
171
|
+
host.replaceChildren(appearanceIcon(document, theme));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function themeValue(element: Element): DesignTheme | null {
|
|
175
|
+
const value = element.getAttribute("data-theme-value");
|
|
176
|
+
return isDesignTheme(value) ? value : null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function requireMenuElements(root: HTMLElement): AppearanceMenuElements {
|
|
180
|
+
const triggers = root.querySelectorAll<HTMLButtonElement>(triggerSelector);
|
|
181
|
+
const popovers = root.querySelectorAll<HTMLElement>(popoverSelector);
|
|
182
|
+
const menus = root.querySelectorAll<HTMLElement>(menuElementSelector);
|
|
183
|
+
const trigger = triggers[0];
|
|
184
|
+
const popover = popovers[0];
|
|
185
|
+
const menu = menus[0];
|
|
186
|
+
if (
|
|
187
|
+
triggers.length !== 1
|
|
188
|
+
|| popovers.length !== 1
|
|
189
|
+
|| menus.length !== 1
|
|
190
|
+
|| trigger?.tagName !== "BUTTON"
|
|
191
|
+
|| popover === undefined
|
|
192
|
+
|| menu === undefined
|
|
193
|
+
) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
"Appearance menu markup requires one button trigger, popover, and menu.",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
if (
|
|
199
|
+
menu.id.trim().length === 0
|
|
200
|
+
|| trigger.getAttribute("aria-controls") !== menu.id
|
|
201
|
+
|| menu.getAttribute("role") !== "menu"
|
|
202
|
+
) {
|
|
203
|
+
throw new Error("Appearance menu trigger and menu ownership is invalid.");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const items = new Map<DesignTheme, HTMLElement>();
|
|
207
|
+
for (const element of menu.querySelectorAll<HTMLElement>(itemSelector)) {
|
|
208
|
+
const theme = themeValue(element);
|
|
209
|
+
if (
|
|
210
|
+
theme === null
|
|
211
|
+
|| items.has(theme)
|
|
212
|
+
|| element.getAttribute("role") !== "menuitemradio"
|
|
213
|
+
) {
|
|
214
|
+
throw new Error("Appearance menu items must be unique Light, Dark, and System radios.");
|
|
215
|
+
}
|
|
216
|
+
items.set(theme, element);
|
|
217
|
+
}
|
|
218
|
+
if (
|
|
219
|
+
items.size !== designThemes.length
|
|
220
|
+
|| designThemes.some((theme) => !items.has(theme))
|
|
221
|
+
) {
|
|
222
|
+
throw new Error("Appearance menu must contain Light, Dark, and System exactly once.");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
items,
|
|
227
|
+
menu,
|
|
228
|
+
popover,
|
|
229
|
+
root,
|
|
230
|
+
trigger,
|
|
231
|
+
typeahead: "",
|
|
232
|
+
typeaheadTimer: null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function menuItemFromEvent(
|
|
237
|
+
elements: AppearanceMenuElements,
|
|
238
|
+
event: Event,
|
|
239
|
+
): HTMLElement | null {
|
|
240
|
+
if (!isElement(event.target)) return null;
|
|
241
|
+
const item = event.target.closest<HTMLElement>(itemSelector);
|
|
242
|
+
return item !== null && elements.menu.contains(item) ? item : null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function focusItem(elements: AppearanceMenuElements, theme: DesignTheme): void {
|
|
246
|
+
elements.items.get(theme)?.focus();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function orderedItems(elements: AppearanceMenuElements): readonly HTMLElement[] {
|
|
250
|
+
return designThemes.flatMap((theme) => {
|
|
251
|
+
const item = elements.items.get(theme);
|
|
252
|
+
return item === undefined ? [] : [item];
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function adjacentItem(
|
|
257
|
+
elements: AppearanceMenuElements,
|
|
258
|
+
current: HTMLElement | null,
|
|
259
|
+
offset: -1 | 1,
|
|
260
|
+
): HTMLElement {
|
|
261
|
+
const items = orderedItems(elements);
|
|
262
|
+
if (items.length === 0) throw new Error("Appearance menu has no focusable items.");
|
|
263
|
+
const currentIndex = current === null ? -1 : items.indexOf(current);
|
|
264
|
+
const nextIndex = currentIndex < 0
|
|
265
|
+
? (offset === 1 ? 0 : items.length - 1)
|
|
266
|
+
: (currentIndex + offset + items.length) % items.length;
|
|
267
|
+
const next = items[nextIndex];
|
|
268
|
+
if (next === undefined) throw new Error("Appearance menu focus order is invalid.");
|
|
269
|
+
return next;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function storageEventValue(event: Event, storageKey: string): unknown {
|
|
273
|
+
const candidate = event as Event & Readonly<{ key?: unknown; newValue?: unknown }>;
|
|
274
|
+
return candidate.key === storageKey ? candidate.newValue : undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Installs the standards-only Light/Dark/System controller for static pages.
|
|
279
|
+
* Each `[data-hraness-appearance-menu]` root owns one button trigger, one
|
|
280
|
+
* popover, and one `role="menu"` with unique Light/Dark/System radio items.
|
|
281
|
+
* The call applies the stored preference synchronously; menu enhancement waits
|
|
282
|
+
* for parsed markup when the installer runs from a blocking head script.
|
|
283
|
+
*/
|
|
284
|
+
export function installAppearanceMenus(
|
|
285
|
+
options: AppearanceMenuOptions,
|
|
286
|
+
): AppearanceMenuInstallation {
|
|
287
|
+
const document = resolveDocument(options.document);
|
|
288
|
+
const active = installationByDocument.get(document);
|
|
289
|
+
if (active !== undefined && !active.disposed) {
|
|
290
|
+
throw new Error("Appearance menus are already installed for this document.");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const darkThemeColor = requireNonBlank(options.darkThemeColor, "darkThemeColor");
|
|
294
|
+
const lightThemeColor = requireNonBlank(options.lightThemeColor, "lightThemeColor");
|
|
295
|
+
const metaName = requireNonBlank(options.metaName ?? "theme-color", "metaName");
|
|
296
|
+
const storageKey = requireNonBlank(options.storageKey ?? designThemeStorageKey, "storageKey");
|
|
297
|
+
const storage = options.storage === undefined ? defaultStorage(document) : options.storage;
|
|
298
|
+
const view = document.defaultView;
|
|
299
|
+
const systemMedia = typeof view?.matchMedia === "function"
|
|
300
|
+
? view.matchMedia(systemDarkQuery)
|
|
301
|
+
: null;
|
|
302
|
+
let preference = storedPreference(storage, storageKey);
|
|
303
|
+
let disposed = false;
|
|
304
|
+
let mounted = false;
|
|
305
|
+
let openMenu: AppearanceMenuElements | null = null;
|
|
306
|
+
let metaRegistration: ThemeColorMetaRegistration | null = null;
|
|
307
|
+
const menus = new Set<AppearanceMenuElements>();
|
|
308
|
+
const menuIds = new Set<string>();
|
|
309
|
+
const cleanups: Array<() => void> = [];
|
|
310
|
+
|
|
311
|
+
const resolvedTheme = () => resolveDesignTheme(preference, systemMedia?.matches ?? false);
|
|
312
|
+
const currentThemeColor = () => resolvedTheme() === "dark"
|
|
313
|
+
? darkThemeColor
|
|
314
|
+
: lightThemeColor;
|
|
315
|
+
|
|
316
|
+
const closeMenu = (
|
|
317
|
+
elements: AppearanceMenuElements,
|
|
318
|
+
returnFocus: boolean,
|
|
319
|
+
): void => {
|
|
320
|
+
elements.popover.hidden = true;
|
|
321
|
+
elements.root.removeAttribute("data-open");
|
|
322
|
+
elements.trigger.setAttribute("aria-expanded", "false");
|
|
323
|
+
if (openMenu === elements) openMenu = null;
|
|
324
|
+
if (returnFocus) elements.trigger.focus();
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const syncMenu = (elements: AppearanceMenuElements): void => {
|
|
328
|
+
elements.root.setAttribute("data-theme-value", preference);
|
|
329
|
+
const currentLabel = designThemeLabel(preference);
|
|
330
|
+
const menuLabel = elements.menu.getAttribute("aria-label")?.trim() || "Appearance";
|
|
331
|
+
elements.trigger.setAttribute("aria-label", `${menuLabel}: ${currentLabel}`);
|
|
332
|
+
elements.trigger.setAttribute("title", `${menuLabel}: ${currentLabel}`);
|
|
333
|
+
const currentHost = elements.trigger.querySelector<HTMLElement>(
|
|
334
|
+
"[data-current-appearance-icon]",
|
|
335
|
+
);
|
|
336
|
+
if (currentHost === null) {
|
|
337
|
+
throw new Error("Appearance menu trigger is missing its current icon host.");
|
|
338
|
+
}
|
|
339
|
+
currentHost.setAttribute("data-current-appearance-icon", preference);
|
|
340
|
+
renderIconHost(document, currentHost, preference);
|
|
341
|
+
|
|
342
|
+
for (const theme of designThemes) {
|
|
343
|
+
const item = elements.items.get(theme);
|
|
344
|
+
if (item === undefined) continue;
|
|
345
|
+
const selected = theme === preference;
|
|
346
|
+
item.setAttribute("aria-checked", String(selected));
|
|
347
|
+
item.setAttribute("tabindex", "-1");
|
|
348
|
+
item.toggleAttribute("data-selected", selected);
|
|
349
|
+
const iconHost = item.querySelector<HTMLElement>("[data-appearance-icon]");
|
|
350
|
+
if (iconHost === null) {
|
|
351
|
+
throw new Error(`Appearance menu ${theme} item is missing its icon host.`);
|
|
352
|
+
}
|
|
353
|
+
renderIconHost(document, iconHost, theme);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const syncAppearance = (): void => {
|
|
358
|
+
document.documentElement.setAttribute("data-theme", resolvedTheme());
|
|
359
|
+
metaRegistration?.update(currentThemeColor());
|
|
360
|
+
for (const menu of menus) syncMenu(menu);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const setPreference = (nextPreference: DesignTheme, persist: boolean): void => {
|
|
364
|
+
preference = nextPreference;
|
|
365
|
+
if (persist) persistPreference(storage, storageKey, preference);
|
|
366
|
+
syncAppearance();
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const focusForOpen = (
|
|
370
|
+
elements: AppearanceMenuElements,
|
|
371
|
+
target: "first" | "last" | "selected",
|
|
372
|
+
): void => {
|
|
373
|
+
if (target === "selected") focusItem(elements, preference);
|
|
374
|
+
else {
|
|
375
|
+
const items = orderedItems(elements);
|
|
376
|
+
const item = target === "first" ? items[0] : items.at(-1);
|
|
377
|
+
item?.focus();
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const showMenu = (
|
|
382
|
+
elements: AppearanceMenuElements,
|
|
383
|
+
focus: "first" | "last" | "selected",
|
|
384
|
+
): void => {
|
|
385
|
+
if (openMenu !== null && openMenu !== elements) closeMenu(openMenu, false);
|
|
386
|
+
openMenu = elements;
|
|
387
|
+
elements.popover.hidden = false;
|
|
388
|
+
elements.root.setAttribute("data-open", "true");
|
|
389
|
+
elements.trigger.setAttribute("aria-expanded", "true");
|
|
390
|
+
focusForOpen(elements, focus);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const selectItem = (elements: AppearanceMenuElements, item: HTMLElement): void => {
|
|
394
|
+
const theme = themeValue(item);
|
|
395
|
+
if (theme === null) return;
|
|
396
|
+
setPreference(theme, true);
|
|
397
|
+
closeMenu(elements, true);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const clearTypeahead = (elements: AppearanceMenuElements): void => {
|
|
401
|
+
elements.typeahead = "";
|
|
402
|
+
if (elements.typeaheadTimer !== null) clearTimeout(elements.typeaheadTimer);
|
|
403
|
+
elements.typeaheadTimer = null;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const typeahead = (
|
|
407
|
+
elements: AppearanceMenuElements,
|
|
408
|
+
key: string,
|
|
409
|
+
): void => {
|
|
410
|
+
if (elements.typeaheadTimer !== null) clearTimeout(elements.typeaheadTimer);
|
|
411
|
+
elements.typeahead += key.toLocaleLowerCase();
|
|
412
|
+
const find = (query: string) => orderedItems(elements).find((item) =>
|
|
413
|
+
(item.textContent ?? "").trim().toLocaleLowerCase().startsWith(query));
|
|
414
|
+
const match = find(elements.typeahead) ?? find(key.toLocaleLowerCase());
|
|
415
|
+
match?.focus();
|
|
416
|
+
elements.typeaheadTimer = setTimeout(() => clearTypeahead(elements), 500);
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const mount = (): void => {
|
|
420
|
+
if (disposed || mounted) return;
|
|
421
|
+
mounted = true;
|
|
422
|
+
for (const root of document.querySelectorAll<HTMLElement>(menuSelector)) {
|
|
423
|
+
const elements = requireMenuElements(root);
|
|
424
|
+
if (menuIds.has(elements.menu.id)) {
|
|
425
|
+
throw new Error("Appearance menu ids must be unique within the document.");
|
|
426
|
+
}
|
|
427
|
+
menuIds.add(elements.menu.id);
|
|
428
|
+
menus.add(elements);
|
|
429
|
+
syncMenu(elements);
|
|
430
|
+
root.setAttribute("data-display", "icons");
|
|
431
|
+
root.setAttribute("data-presentation", "menu");
|
|
432
|
+
root.setAttribute("data-ready", "true");
|
|
433
|
+
root.removeAttribute("aria-busy");
|
|
434
|
+
elements.trigger.disabled = false;
|
|
435
|
+
elements.trigger.setAttribute("aria-expanded", "false");
|
|
436
|
+
elements.trigger.setAttribute("aria-haspopup", "menu");
|
|
437
|
+
elements.popover.hidden = true;
|
|
438
|
+
|
|
439
|
+
const onTriggerClick = (): void => {
|
|
440
|
+
if (openMenu === elements) closeMenu(elements, false);
|
|
441
|
+
else showMenu(elements, "selected");
|
|
442
|
+
};
|
|
443
|
+
const onTriggerKeyDown = (event: KeyboardEvent): void => {
|
|
444
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
445
|
+
event.preventDefault();
|
|
446
|
+
showMenu(elements, event.key === "ArrowDown" ? "first" : "last");
|
|
447
|
+
} else if (event.key === "Escape" && openMenu === elements) {
|
|
448
|
+
event.preventDefault();
|
|
449
|
+
closeMenu(elements, true);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
const onMenuClick = (event: MouseEvent): void => {
|
|
453
|
+
const item = menuItemFromEvent(elements, event);
|
|
454
|
+
if (item !== null) selectItem(elements, item);
|
|
455
|
+
};
|
|
456
|
+
const onMenuKeyDown = (event: KeyboardEvent): void => {
|
|
457
|
+
const item = menuItemFromEvent(elements, event);
|
|
458
|
+
if (event.key === "Escape") {
|
|
459
|
+
event.preventDefault();
|
|
460
|
+
event.stopPropagation();
|
|
461
|
+
closeMenu(elements, true);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (event.key === "Tab") {
|
|
465
|
+
closeMenu(elements, false);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
469
|
+
event.preventDefault();
|
|
470
|
+
adjacentItem(elements, item, event.key === "ArrowDown" ? 1 : -1).focus();
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (event.key === "Home" || event.key === "End") {
|
|
474
|
+
event.preventDefault();
|
|
475
|
+
const items = orderedItems(elements);
|
|
476
|
+
(event.key === "Home" ? items[0] : items.at(-1))?.focus();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if ((event.key === "Enter" || event.key === " ") && item !== null) {
|
|
480
|
+
event.preventDefault();
|
|
481
|
+
selectItem(elements, item);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (
|
|
485
|
+
event.key.length === 1
|
|
486
|
+
&& !event.altKey
|
|
487
|
+
&& !event.ctrlKey
|
|
488
|
+
&& !event.metaKey
|
|
489
|
+
) {
|
|
490
|
+
typeahead(elements, event.key);
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
elements.trigger.addEventListener("click", onTriggerClick);
|
|
494
|
+
elements.trigger.addEventListener("keydown", onTriggerKeyDown);
|
|
495
|
+
elements.menu.addEventListener("click", onMenuClick);
|
|
496
|
+
elements.menu.addEventListener("keydown", onMenuKeyDown);
|
|
497
|
+
cleanups.push(() => {
|
|
498
|
+
clearTypeahead(elements);
|
|
499
|
+
elements.trigger.removeEventListener("click", onTriggerClick);
|
|
500
|
+
elements.trigger.removeEventListener("keydown", onTriggerKeyDown);
|
|
501
|
+
elements.menu.removeEventListener("click", onMenuClick);
|
|
502
|
+
elements.menu.removeEventListener("keydown", onMenuKeyDown);
|
|
503
|
+
closeMenu(elements, false);
|
|
504
|
+
elements.root.setAttribute("aria-busy", "true");
|
|
505
|
+
elements.root.setAttribute("data-ready", "false");
|
|
506
|
+
elements.trigger.disabled = true;
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const state: InstallationState = {
|
|
512
|
+
disposed: false,
|
|
513
|
+
publicInstallation: {
|
|
514
|
+
dispose: () => {
|
|
515
|
+
if (disposed) return;
|
|
516
|
+
disposed = true;
|
|
517
|
+
state.disposed = true;
|
|
518
|
+
for (const cleanup of cleanups.splice(0).reverse()) cleanup();
|
|
519
|
+
metaRegistration?.release();
|
|
520
|
+
metaRegistration = null;
|
|
521
|
+
menus.clear();
|
|
522
|
+
if (installationByDocument.get(document) === state) {
|
|
523
|
+
installationByDocument.delete(document);
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
preference: () => preference,
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
const { publicInstallation } = state;
|
|
530
|
+
installationByDocument.set(document, state);
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
metaRegistration = acquireThemeColorMeta(
|
|
534
|
+
document,
|
|
535
|
+
metaName,
|
|
536
|
+
Symbol("hraness-static-appearance"),
|
|
537
|
+
currentThemeColor(),
|
|
538
|
+
);
|
|
539
|
+
document.documentElement.setAttribute("data-theme", resolvedTheme());
|
|
540
|
+
|
|
541
|
+
const onSystemChange = (): void => {
|
|
542
|
+
if (preference === "system") syncAppearance();
|
|
543
|
+
};
|
|
544
|
+
systemMedia?.addEventListener("change", onSystemChange);
|
|
545
|
+
if (systemMedia !== null) {
|
|
546
|
+
cleanups.push(() => systemMedia.removeEventListener("change", onSystemChange));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const onStorage = (event: Event): void => {
|
|
550
|
+
const value = storageEventValue(event, storageKey);
|
|
551
|
+
if (value === undefined) return;
|
|
552
|
+
if (isDesignTheme(value)) {
|
|
553
|
+
setPreference(value, false);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
setPreference(defaultDesignTheme, value !== null);
|
|
557
|
+
};
|
|
558
|
+
view?.addEventListener("storage", onStorage);
|
|
559
|
+
if (view !== null) {
|
|
560
|
+
cleanups.push(() => view.removeEventListener("storage", onStorage));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const closeForOutsideTarget = (target: EventTarget | null): void => {
|
|
564
|
+
if (
|
|
565
|
+
openMenu !== null
|
|
566
|
+
&& (!isElement(target) || !openMenu.root.contains(target))
|
|
567
|
+
) {
|
|
568
|
+
closeMenu(openMenu, false);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
const onDocumentPointerDown = (event: Event): void => {
|
|
572
|
+
closeForOutsideTarget(event.target);
|
|
573
|
+
};
|
|
574
|
+
const onDocumentFocusIn = (event: Event): void => {
|
|
575
|
+
closeForOutsideTarget(event.target);
|
|
576
|
+
};
|
|
577
|
+
document.addEventListener("pointerdown", onDocumentPointerDown, true);
|
|
578
|
+
document.addEventListener("focusin", onDocumentFocusIn);
|
|
579
|
+
cleanups.push(() => {
|
|
580
|
+
document.removeEventListener("pointerdown", onDocumentPointerDown, true);
|
|
581
|
+
document.removeEventListener("focusin", onDocumentFocusIn);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
if (document.readyState === "loading") {
|
|
585
|
+
const mountAfterParse = (): void => {
|
|
586
|
+
try {
|
|
587
|
+
mount();
|
|
588
|
+
} catch (error) {
|
|
589
|
+
publicInstallation.dispose();
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
document.addEventListener("DOMContentLoaded", mountAfterParse, { once: true });
|
|
594
|
+
cleanups.push(() => document.removeEventListener("DOMContentLoaded", mountAfterParse));
|
|
595
|
+
} else {
|
|
596
|
+
mount();
|
|
597
|
+
}
|
|
598
|
+
} catch (error) {
|
|
599
|
+
publicInstallation.dispose();
|
|
600
|
+
throw error;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return publicInstallation;
|
|
604
|
+
}
|