@cueplusplus/ui 0.8.0 → 0.10.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/CHANGELOG.md +457 -0
- package/README.md +131 -0
- package/dist/chat/message-list.js +2 -1
- package/dist/configurator/_export.d.ts +1 -1
- package/dist/configurator/_export.js +53 -13
- package/dist/configurator/_overrides.d.ts +25 -6
- package/dist/configurator/_overrides.js +30 -17
- package/dist/configurator/configurator.js +8 -3
- package/dist/configurator/panel-sections.js +43 -13
- package/dist/elements/command-palette.js +1 -1
- package/dist/elements/flow-graph.js +2 -2
- package/dist/elements/markdown.js +1 -1
- package/dist/elements/surfaces.js +4 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +5 -2
- package/dist/instruments/meter.d.ts +5 -1
- package/dist/layout/bento.d.ts +144 -0
- package/dist/layout/bento.js +266 -0
- package/dist/layout/index.d.ts +3 -2
- package/dist/layout/index.js +3 -2
- package/dist/midi/piano-keyboard.js +5 -1
- package/dist/primitives/button.d.ts +12 -0
- package/dist/styles.css +23 -1
- package/dist/system/density.d.ts +17 -8
- package/dist/system/density.js +39 -16
- package/dist/system/index.d.ts +5 -2
- package/dist/system/index.js +3 -1
- package/dist/system/overrides.d.ts +80 -0
- package/dist/system/overrides.js +277 -0
- package/dist/system/portal.d.ts +4 -2
- package/dist/system/portal.js +34 -3
- package/dist/system/prepaint.d.ts +58 -7
- package/dist/system/prepaint.js +72 -20
- package/dist/system/theme-provider.d.ts +133 -8
- package/dist/system/theme-provider.js +203 -72
- package/dist/system/theme-registry.d.ts +53 -0
- package/dist/system/theme-registry.js +66 -0
- package/dist/system/use-density.d.ts +13 -5
- package/dist/system/use-density.js +142 -13
- package/dist/system/use-theme.d.ts +5 -3
- package/dist/system/use-theme.js +5 -3
- package/dist/system/vocabulary.d.ts +15 -0
- package/dist/system/vocabulary.js +111 -0
- package/dist/theming/contrast.d.ts +2 -122
- package/dist/theming/contrast.js +2 -194
- package/dist/theming/create-theme.d.ts +37 -11
- package/dist/theming/create-theme.js +54 -17
- package/dist/theming/index.d.ts +3 -4
- package/dist/theming/index.js +3 -4
- package/dist/theming/serialize.d.ts +24 -11
- package/dist/theming/serialize.js +16 -18
- package/manifest/components/accordion.json +24 -6
- package/manifest/components/activity-graph.json +20 -5
- package/manifest/components/agent-card.json +37 -9
- package/manifest/components/agent-handoff.json +21 -5
- package/manifest/components/agent-mode-badge.json +22 -4
- package/manifest/components/agent-pile.json +38 -8
- package/manifest/components/agent-plan.json +9 -2
- package/manifest/components/agent-status.json +17 -3
- package/manifest/components/agent-surface.json +26 -11
- package/manifest/components/alert-dialog.json +29 -5
- package/manifest/components/animated-number.json +14 -6
- package/manifest/components/app-bar.json +66 -13
- package/manifest/components/app-shell.json +133 -31
- package/manifest/components/app-window-frame.json +28 -6
- package/manifest/components/approval-card.json +37 -7
- package/manifest/components/artifact-card.json +16 -4
- package/manifest/components/ask-box.json +99 -21
- package/manifest/components/audience-icon.json +1 -7
- package/manifest/components/autocomplete.json +80 -18
- package/manifest/components/avatar-group.json +13 -2
- package/manifest/components/avatar.json +22 -4
- package/manifest/components/background-inbox.json +9 -2
- package/manifest/components/bento.json +306 -0
- package/manifest/components/branch-picker.json +29 -7
- package/manifest/components/breadcrumb.json +13 -3
- package/manifest/components/button-group.json +21 -4
- package/manifest/components/button.json +41 -4
- package/manifest/components/canvas-split-body.json +4 -1
- package/manifest/components/canvas-split-header.json +17 -4
- package/manifest/components/canvas-split-line.json +4 -1
- package/manifest/components/canvas-split-message.json +8 -1
- package/manifest/components/card.json +14 -3
- package/manifest/components/carousel.json +54 -12
- package/manifest/components/catalogue-icon.json +1 -7
- package/manifest/components/channel-beta-icon.json +1 -7
- package/manifest/components/channel-matrix.json +80 -15
- package/manifest/components/channel-released-icon.json +1 -7
- package/manifest/components/chart-container.json +26 -10
- package/manifest/components/chart-ramp.json +13 -3
- package/manifest/components/chart-swatch.json +9 -2
- package/manifest/components/chart-tooltip-content.json +31 -7
- package/manifest/components/chart.json +30 -6
- package/manifest/components/chat-panel-composer.json +9 -2
- package/manifest/components/checkbox-group.json +4 -2
- package/manifest/components/checkbox.json +17 -4
- package/manifest/components/checkpoint-history.json +13 -3
- package/manifest/components/chip.json +35 -5
- package/manifest/components/clamp.json +30 -7
- package/manifest/components/cli-tool-icon.json +1 -7
- package/manifest/components/code-diff.json +20 -5
- package/manifest/components/code-runner.json +32 -6
- package/manifest/components/collapsible.json +16 -4
- package/manifest/components/color-area.json +4 -1
- package/manifest/components/color-field.json +13 -3
- package/manifest/components/color-picker.json +75 -17
- package/manifest/components/color-slider.json +12 -3
- package/manifest/components/color-swatch.json +13 -2
- package/manifest/components/colors-section.json +10 -7
- package/manifest/components/combobox.json +81 -18
- package/manifest/components/command-palette.json +57 -13
- package/manifest/components/compaction-row.json +35 -7
- package/manifest/components/comparison-card.json +17 -4
- package/manifest/components/composer-attachment-chip.json +9 -2
- package/manifest/components/composer-bar.json +4 -1
- package/manifest/components/composer-command-item.json +8 -2
- package/manifest/components/composer-context.json +4 -1
- package/manifest/components/composer-input.json +5 -1
- package/manifest/components/composer-menu-item.json +4 -1
- package/manifest/components/composer-menu.json +12 -2
- package/manifest/components/composer-model-item.json +8 -2
- package/manifest/components/composer-model-trigger.json +8 -2
- package/manifest/components/composer-person-item.json +8 -2
- package/manifest/components/composer-send.json +8 -2
- package/manifest/components/composer-voice-button.json +4 -1
- package/manifest/components/composer-voice.json +8 -2
- package/manifest/components/composer.json +76 -17
- package/manifest/components/computer-use.json +17 -4
- package/manifest/components/confidence-marker.json +13 -3
- package/manifest/components/connection-state.json +23 -4
- package/manifest/components/container.json +14 -2
- package/manifest/components/context-breakdown.json +8 -2
- package/manifest/components/context-menu.json +16 -4
- package/manifest/components/context-usage.json +16 -4
- package/manifest/components/conversation-search.json +22 -5
- package/manifest/components/copy-button.json +32 -7
- package/manifest/components/cost-meter.json +12 -3
- package/manifest/components/cue-logotype.json +17 -3
- package/manifest/components/cue-mark.json +17 -3
- package/manifest/components/cue-portal-frame.json +14 -4
- package/manifest/components/data-row.json +21 -7
- package/manifest/components/data-table-pagination.json +18 -4
- package/manifest/components/data-table-toolbar.json +30 -7
- package/manifest/components/data-tree.json +42 -9
- package/manifest/components/date-field.json +13 -2
- package/manifest/components/date-picker.json +81 -19
- package/manifest/components/date-range-picker.json +81 -19
- package/manifest/components/day-separator.json +4 -1
- package/manifest/components/delegation-card.json +64 -12
- package/manifest/components/density.json +25 -5
- package/manifest/components/description-list.json +8 -1
- package/manifest/components/diagram.json +33 -7
- package/manifest/components/dialog.json +29 -6
- package/manifest/components/disclosure.json +31 -7
- package/manifest/components/dmx-bar.json +16 -4
- package/manifest/components/dmx-strip.json +20 -5
- package/manifest/components/document-reference.json +21 -5
- package/manifest/components/draft-restore.json +18 -4
- package/manifest/components/drawer.json +30 -6
- package/manifest/components/dropdown-menu.json +55 -10
- package/manifest/components/edit-message.json +32 -7
- package/manifest/components/elements-command-palette.json +27 -6
- package/manifest/components/elements-data-table.json +9 -2
- package/manifest/components/elements-timeline.json +8 -2
- package/manifest/components/elicitation-form.json +31 -6
- package/manifest/components/empty-state-composer.json +9 -2
- package/manifest/components/empty-state-suggestion.json +4 -1
- package/manifest/components/empty-state.json +25 -5
- package/manifest/components/end-of-turn-summary.json +16 -4
- package/manifest/components/env-var-input.json +54 -12
- package/manifest/components/error-state.json +17 -4
- package/manifest/components/export-dialog.json +27 -11
- package/manifest/components/eyebrow.json +4 -1
- package/manifest/components/feedback-dialog.json +33 -7
- package/manifest/components/field-description.json +4 -1
- package/manifest/components/field-error.json +13 -3
- package/manifest/components/field-label.json +4 -1
- package/manifest/components/field.json +4 -1
- package/manifest/components/file-tree.json +16 -4
- package/manifest/components/file-upload.json +58 -13
- package/manifest/components/flow-graph.json +12 -3
- package/manifest/components/folder-icon.json +1 -7
- package/manifest/components/footer.json +24 -4
- package/manifest/components/frac.json +10 -2
- package/manifest/components/generation-loader.json +17 -3
- package/manifest/components/generative-ui.json +39 -7
- package/manifest/components/grid.json +56 -5
- package/manifest/components/group-bar.json +25 -6
- package/manifest/components/guardrail-notice.json +22 -5
- package/manifest/components/hover-card.json +45 -8
- package/manifest/components/icon-button.json +22 -4
- package/manifest/components/image-generation.json +8 -2
- package/manifest/components/info-tip.json +42 -7
- package/manifest/components/inline-citation.json +14 -3
- package/manifest/components/input-group.json +24 -5
- package/manifest/components/input.json +13 -3
- package/manifest/components/item.json +42 -8
- package/manifest/components/job-progress.json +25 -6
- package/manifest/components/launcher-bubble.json +32 -7
- package/manifest/components/ledger.json +87 -19
- package/manifest/components/link.json +16 -3
- package/manifest/components/live-region-announcer.json +21 -5
- package/manifest/components/log-viewer.json +42 -10
- package/manifest/components/map-answer.json +17 -4
- package/manifest/components/markdown-text.json +36 -5
- package/manifest/components/math-block.json +12 -3
- package/manifest/components/mcp-server-icon.json +1 -7
- package/manifest/components/mcp-server-panel.json +18 -4
- package/manifest/components/memory-chips.json +9 -2
- package/manifest/components/menubar.json +33 -7
- package/manifest/components/message-actions.json +32 -7
- package/manifest/components/message-attachments.json +9 -2
- package/manifest/components/message-branches.json +14 -3
- package/manifest/components/message-list.json +27 -6
- package/manifest/components/message-pair.json +25 -5
- package/manifest/components/message-queue.json +13 -3
- package/manifest/components/message-timing.json +8 -2
- package/manifest/components/message.json +41 -9
- package/manifest/components/meter.json +43 -7
- package/manifest/components/mobile-composer.json +47 -10
- package/manifest/components/model-picker.json +13 -3
- package/manifest/components/multi-select.json +78 -17
- package/manifest/components/musical-time-input.json +34 -8
- package/manifest/components/navigation-menu.json +54 -12
- package/manifest/components/node-card.json +42 -7
- package/manifest/components/node-handle.json +17 -2
- package/manifest/components/number-field.json +28 -6
- package/manifest/components/number-ticker.json +8 -2
- package/manifest/components/onboarding.json +18 -4
- package/manifest/components/otp-field.json +43 -10
- package/manifest/components/page-shell.json +35 -6
- package/manifest/components/pagination.json +21 -5
- package/manifest/components/panel-header.json +15 -3
- package/manifest/components/password-input.json +38 -8
- package/manifest/components/permission-grant.json +28 -5
- package/manifest/components/permission-scopes.json +57 -12
- package/manifest/components/piano-keyboard.json +38 -9
- package/manifest/components/plussie.json +34 -5
- package/manifest/components/popover.json +45 -8
- package/manifest/components/preset-section.json +14 -8
- package/manifest/components/progress.json +25 -6
- package/manifest/components/prompt-library.json +27 -6
- package/manifest/components/queue-dock.json +36 -8
- package/manifest/components/quota-banner.json +25 -6
- package/manifest/components/quote-reply.json +29 -7
- package/manifest/components/radio-group.json +12 -2
- package/manifest/components/radio.json +17 -3
- package/manifest/components/rating.json +60 -13
- package/manifest/components/read-aloud.json +35 -8
- package/manifest/components/reasoning-effort.json +17 -4
- package/manifest/components/reasoning-panel.json +33 -8
- package/manifest/components/recommendation-card.json +35 -10
- package/manifest/components/regenerate-menu.json +22 -5
- package/manifest/components/replay-player.json +29 -7
- package/manifest/components/research-report.json +12 -3
- package/manifest/components/resizable.json +4 -1
- package/manifest/components/retrieval-chunks.json +16 -4
- package/manifest/components/revert-dock.json +61 -14
- package/manifest/components/reviewable-diff.json +23 -5
- package/manifest/components/risk-badge.json +18 -3
- package/manifest/components/row.json +17 -4
- package/manifest/components/schedule-card.json +25 -6
- package/manifest/components/score-breakdown.json +20 -5
- package/manifest/components/scroll-anchor.json +13 -3
- package/manifest/components/scroll-area.json +31 -8
- package/manifest/components/scrollable-tabs-list.json +12 -4
- package/manifest/components/scrub-input.json +84 -18
- package/manifest/components/seam-cell.json +19 -3
- package/manifest/components/seam-grid.json +12 -3
- package/manifest/components/search-input.json +49 -10
- package/manifest/components/section-header.json +36 -8
- package/manifest/components/segmented-control.json +50 -11
- package/manifest/components/select.json +74 -16
- package/manifest/components/separator.json +4 -1
- package/manifest/components/settings-panel.json +41 -9
- package/manifest/components/shape-section.json +10 -7
- package/manifest/components/shared-conversation.json +21 -5
- package/manifest/components/sheet.json +29 -6
- package/manifest/components/shimmer-label.json +4 -2
- package/manifest/components/sidebar.json +60 -13
- package/manifest/components/skill-icon.json +1 -7
- package/manifest/components/slider.json +22 -5
- package/manifest/components/sources.json +17 -4
- package/manifest/components/sparkline.json +17 -4
- package/manifest/components/speaker-identity.json +4 -1
- package/manifest/components/spec-sheet.json +16 -4
- package/manifest/components/spectrum-visualizer.json +41 -10
- package/manifest/components/spinner.json +12 -2
- package/manifest/components/stack-icon.json +1 -7
- package/manifest/components/stack.json +54 -7
- package/manifest/components/stacks-matrix-icon.json +1 -7
- package/manifest/components/stat.json +28 -4
- package/manifest/components/status-bar.json +50 -9
- package/manifest/components/status-dot.json +23 -4
- package/manifest/components/stepper.json +22 -5
- package/manifest/components/stopped-run.json +19 -4
- package/manifest/components/streaming-text.json +12 -3
- package/manifest/components/subagent-list.json +21 -5
- package/manifest/components/suggestions.json +27 -5
- package/manifest/components/swap-label.json +18 -4
- package/manifest/components/switch.json +18 -3
- package/manifest/components/table-scroll-region.json +4 -1
- package/manifest/components/table.json +54 -11
- package/manifest/components/tabs.json +16 -5
- package/manifest/components/tags-input.json +79 -17
- package/manifest/components/tail-status.json +37 -8
- package/manifest/components/terminal-block.json +25 -5
- package/manifest/components/terminal-frame.json +34 -8
- package/manifest/components/textarea.json +8 -3
- package/manifest/components/theme-configurator.json +47 -14
- package/manifest/components/theme-provider.json +97 -18
- package/manifest/components/thinking-indicator.json +8 -2
- package/manifest/components/thread-list.json +13 -3
- package/manifest/components/thread-search.json +22 -5
- package/manifest/components/threshold-rail.json +77 -14
- package/manifest/components/time-boundary.json +8 -2
- package/manifest/components/time-field.json +13 -2
- package/manifest/components/timeline-ruler.json +51 -12
- package/manifest/components/timeline.json +19 -3
- package/manifest/components/title-bar.json +19 -4
- package/manifest/components/toast.json +12 -3
- package/manifest/components/todo-list.json +8 -2
- package/manifest/components/toggle-group.json +33 -7
- package/manifest/components/toggle.json +23 -4
- package/manifest/components/token-editor.json +26 -10
- package/manifest/components/tool-call-card.json +83 -16
- package/manifest/components/tool-call.json +37 -9
- package/manifest/components/tool-error.json +34 -8
- package/manifest/components/tool-group.json +17 -4
- package/manifest/components/tool-timeline.json +37 -9
- package/manifest/components/toolbar.json +37 -8
- package/manifest/components/tooltip.json +41 -7
- package/manifest/components/trace-waterfall.json +12 -3
- package/manifest/components/tree-visibility-toggle.json +18 -3
- package/manifest/components/tree.json +52 -11
- package/manifest/components/turn-footer.json +36 -8
- package/manifest/components/two-step-button.json +46 -9
- package/manifest/components/typing-indicator.json +8 -1
- package/manifest/components/universe-grid.json +37 -10
- package/manifest/components/unread-divider.json +4 -1
- package/manifest/components/usage-chart.json +46 -9
- package/manifest/components/verdict-row.json +20 -5
- package/manifest/components/voice-conversation.json +37 -7
- package/manifest/components/web-preview.json +23 -5
- package/manifest/components/web-search.json +20 -5
- package/manifest/components/work-collapse.json +30 -7
- package/manifest/fixtures.json +106 -0
- package/manifest/manifest.json +449 -310
- package/manifest/tokens.json +122 -12
- package/package.json +16 -6
- package/dist/theming/_presets.d.ts +0 -11
- package/dist/theming/_presets.js +0 -678
|
@@ -1,19 +1,42 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
3
|
import { useIsomorphicInsertionEffect, useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect.js";
|
|
4
|
-
import {
|
|
4
|
+
import { AmbientThemeContext, DensityContext } from "./density.js";
|
|
5
|
+
import { buildRegistry, vocabulary, warnOnce } from "./vocabulary.js";
|
|
6
|
+
import { OverridesContext, inlineOverrides, overridesCss, reportOverrides } from "./overrides.js";
|
|
5
7
|
import { DEFAULT_STORAGE_KEY } from "./prepaint.js";
|
|
8
|
+
import { ThemeRegistryContext } from "./theme-registry.js";
|
|
6
9
|
import * as React from "react";
|
|
7
|
-
import { jsx } from "react/jsx-runtime";
|
|
8
|
-
import {
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
import { MODES, THEME_NAME_PATTERN, validateManifest } from "@cueplusplus/theme-base";
|
|
9
12
|
//#region src/system/theme-provider.tsx
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The empty registry, at module scope so the default is referentially stable.
|
|
15
|
+
*
|
|
16
|
+
* Every memo below is keyed on `themes`, and a fresh `[]` per render would
|
|
17
|
+
* rebuild the registry, the vocabulary and the context value on every commit of
|
|
18
|
+
* every app that has not registered anything yet — which, for one release, is
|
|
19
|
+
* all of them.
|
|
20
|
+
*/
|
|
21
|
+
const EMPTY = [];
|
|
22
|
+
/**
|
|
23
|
+
* Dark-first defaults, and the one thing to know about the first of them: it is
|
|
24
|
+
* a *name*, and since this release nothing in the library paints it.
|
|
25
|
+
*
|
|
26
|
+
* A bare `:root` used to carry `cue`'s palette, because `@cueplusplus/tokens`
|
|
27
|
+
* shipped ten presets and put the default one there. `styles.css` now imports
|
|
28
|
+
* `@cueplusplus/theme-base/base.css` instead, whose `:root` is the blank base —
|
|
29
|
+
* greys and one desaturated accent. So an app that registers nothing and stamps
|
|
30
|
+
* nothing gets `data-theme="cue"` on its provider element and paints the blank
|
|
31
|
+
* base, because no stylesheet on the page declares a `[data-theme="cue"]` block.
|
|
32
|
+
* Colour is decided by CSS, not by this constant.
|
|
33
|
+
*
|
|
34
|
+
* That is deliberate and it is why the name stays `cue`: the alternative — a
|
|
35
|
+
* default of `"base"` or `""` — would change what every app that *does* import
|
|
36
|
+
* `@cueplusplus/theme-cue/theme.css` renders when it passes no `theme` prop, to
|
|
37
|
+
* buy nothing, since a name that matches no imported block already paints the
|
|
38
|
+
* base.
|
|
39
|
+
*/
|
|
17
40
|
const DEFAULT_THEME = "cue";
|
|
18
41
|
const DEFAULT_MODE = "dark";
|
|
19
42
|
const DEFAULT_RESOLVED_MODE = "dark";
|
|
@@ -51,28 +74,48 @@ function readPreferences(storageKey) {
|
|
|
51
74
|
return {};
|
|
52
75
|
}
|
|
53
76
|
}
|
|
54
|
-
/** Resolve `"system"` the way {@link prepaintScript} resolves it: against the media query. */
|
|
55
|
-
function resolveMode(mode) {
|
|
56
|
-
if (mode !== "system") return mode;
|
|
57
|
-
if (typeof matchMedia !== "function") return DEFAULT_RESOLVED_MODE;
|
|
58
|
-
return matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
|
|
59
|
-
}
|
|
60
77
|
/**
|
|
61
|
-
* The
|
|
62
|
-
*
|
|
78
|
+
* The stored quadruple, accepted the way the pre-paint script accepts it:
|
|
79
|
+
* **theme first**, then each axis under the theme resolution actually chose.
|
|
80
|
+
*
|
|
81
|
+
* A rung a theme adds is scoped by the cascade to that theme's `data-theme`, so
|
|
82
|
+
* checking it against the theme that is about to be replaced rejects exactly
|
|
83
|
+
* the pairing the registry exists to allow — and on every commit that reads
|
|
84
|
+
* storage the component's `theme` is still the *prop*, never the theme the
|
|
85
|
+
* visitor stored. The script does `var t=…; var a=A[t]`; so does this, and it
|
|
86
|
+
* is the only place either rule is applied.
|
|
87
|
+
*
|
|
88
|
+
* `mode` is returned as stored, including the literal `"system"`. Resolving it
|
|
89
|
+
* here is what used to put `data-mode="dark"` on `<html>` for one commit under
|
|
90
|
+
* a stored `system`, which the stamp's own comment says must not happen.
|
|
63
91
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
92
|
+
* **The fallback is judged too.** It is the app's own default quadruple, which
|
|
93
|
+
* was chosen under the app's own default *theme* — and the theme resolution
|
|
94
|
+
* just chose need not be that one. Handing back `fallback.density` unchecked
|
|
95
|
+
* puts a rung the resolved theme does not declare onto `<html>`: an attribute
|
|
96
|
+
* no block matches, which is the precise failure the registry exists to
|
|
97
|
+
* prevent, arrived at by the code that was meant to prevent it. So each axis
|
|
98
|
+
* falls through the stored value, then the app default, then the base default,
|
|
99
|
+
* which every theme offers by construction.
|
|
100
|
+
*
|
|
101
|
+
* A *refused theme* is the one case here worth a word to the developer. The
|
|
102
|
+
* other three axes have a visible fallback the reader can see and change back;
|
|
103
|
+
* a theme that was registered when the visitor chose it and is not registered
|
|
104
|
+
* now is almost always a registration that was dropped by accident, and it
|
|
105
|
+
* shows up as "my users keep losing their theme" long after the commit that
|
|
106
|
+
* caused it. With an empty registry nothing is refused for being unregistered
|
|
107
|
+
* — theme validation is off — so the only refusal left there is a name that
|
|
108
|
+
* could not be a `data-theme` value at all, and it gets the other sentence:
|
|
109
|
+
* "register it" is not the fix for `"9bad"`.
|
|
68
110
|
*/
|
|
69
|
-
function
|
|
70
|
-
const
|
|
111
|
+
function acceptedPreferences(vocab, stored, fallback) {
|
|
112
|
+
const theme = vocab.acceptsTheme(stored.theme) ? stored.theme : fallback.theme;
|
|
113
|
+
if (stored.theme !== void 0 && stored.theme !== theme) warnOnce(vocab.themes === null ? `stored theme "${stored.theme}" is not a valid theme name; falling back to "${theme}". A theme name must match ${String(THEME_NAME_PATTERN)}.` : `stored theme "${stored.theme}" is not registered; falling back to "${theme}". A visitor who chose it keeps losing it until its manifest is back in ThemeProvider's themes prop.`);
|
|
71
114
|
return {
|
|
72
|
-
theme
|
|
73
|
-
density:
|
|
74
|
-
font:
|
|
75
|
-
mode:
|
|
115
|
+
theme,
|
|
116
|
+
density: vocab.acceptsDensity(theme, stored.density) ? stored.density : vocab.acceptsDensity(theme, fallback.density) ? fallback.density : vocab.preferredDensity(theme),
|
|
117
|
+
font: vocab.acceptsFont(theme, stored.font) ? stored.font : vocab.acceptsFont(theme, fallback.font) ? fallback.font : vocab.preferredFont(theme),
|
|
118
|
+
mode: isOneOf(MODES, stored.mode) ? stored.mode : fallback.mode
|
|
76
119
|
};
|
|
77
120
|
}
|
|
78
121
|
function writePreferences(storageKey, preferences) {
|
|
@@ -84,7 +127,8 @@ function writePreferences(storageKey, preferences) {
|
|
|
84
127
|
/**
|
|
85
128
|
* Root of the theme system: stamps `data-theme`, `data-density` and `data-mode`
|
|
86
129
|
* so the token layer resolves, puts `data-font` on `<html>`, publishes
|
|
87
|
-
* `--cue-font-scale`,
|
|
130
|
+
* `--cue-font-scale`, owns the persisted user preference, and publishes the
|
|
131
|
+
* registry the three axis hooks read.
|
|
88
132
|
*
|
|
89
133
|
* Rendering: `<div data-cue-root data-theme data-density data-mode style={{ colorScheme, --cue-font-scale }}>`
|
|
90
134
|
* (or the single child when `asChild`). The `theme`/`density`/`font`/`mode` props are
|
|
@@ -106,26 +150,72 @@ function writePreferences(storageKey, preferences) {
|
|
|
106
150
|
* font pairing — `useTheme().font` and `setFont` inside one are the root's.
|
|
107
151
|
*
|
|
108
152
|
* @example
|
|
109
|
-
*
|
|
153
|
+
* // `cue` and `terminal` are the `manifest.json` each theme package ships, read
|
|
154
|
+
* // from the `…/manifest.json` subpath of `@cueplusplus/theme-cue` and
|
|
155
|
+
* // `@cueplusplus/theme-terminal`. Written that way round on purpose: a literal
|
|
156
|
+
* // import statement in this comment reads, to every import-graph gate in this
|
|
157
|
+
* // repository, as `ui` depending on a theme package — which is the one thing
|
|
158
|
+
* // it may not do.
|
|
159
|
+
* <ThemeProvider themes={[cue, terminal]} theme="terminal" density="ultra-compact" font="plex" mode="system">
|
|
110
160
|
* <App />
|
|
111
161
|
* </ThemeProvider>
|
|
112
162
|
*/
|
|
113
|
-
function ThemeProvider({ theme: themeProp
|
|
163
|
+
function ThemeProvider({ themes: themesProp, theme: themeProp, density: densityProp, mode: modeProp = DEFAULT_MODE, font: fontProp, fontScale = 1, fontFamilies, overrides, storageKey = DEFAULT_STORAGE_KEY, persistPreferences = true, asChild = false, className, style: styleProp, children }) {
|
|
114
164
|
const ambient = React.useContext(ThemeContext);
|
|
115
165
|
const isRoot = ambient === null;
|
|
116
|
-
const
|
|
117
|
-
const
|
|
166
|
+
const ambientRegistry = React.useContext(ThemeRegistryContext);
|
|
167
|
+
const themes = themesProp ?? ambientRegistry?.themes ?? EMPTY;
|
|
168
|
+
const vocab = React.useMemo(() => vocabulary(themes), [themes]);
|
|
169
|
+
React.useMemo(() => {
|
|
170
|
+
if (process.env.NODE_ENV === "production") return;
|
|
171
|
+
for (const manifest of themes) {
|
|
172
|
+
const problems = validateManifest(manifest);
|
|
173
|
+
if (problems.length > 0) throw new TypeError(`ThemeProvider: manifest ${JSON.stringify(manifest.name)} is invalid\n${problems.map((p) => ` ${p.path}: ${p.message}`).join("\n")}`);
|
|
174
|
+
}
|
|
175
|
+
for (const manifest of themes) {
|
|
176
|
+
const rung = manifest.densities.default;
|
|
177
|
+
if (rung !== void 0 && !vocab.acceptsDensity(manifest.name, rung)) warnOnce(`manifest "${manifest.name}": densities.default "${rung}" is neither a base rung nor one this theme adds; the base default is used`);
|
|
178
|
+
const pairing = manifest.fontPairings.default;
|
|
179
|
+
if (pairing !== void 0 && !vocab.acceptsFont(manifest.name, pairing)) warnOnce(`manifest "${manifest.name}": fontPairings.default "${pairing}" is neither a base pairing nor one this theme adds; the base default is used`);
|
|
180
|
+
}
|
|
181
|
+
if (isRoot && themes.length === 0) warnOnce("ThemeProvider: no `themes` were registered, so there is no registry: useThemes() is empty, useTheme().manifest is null, the pre-paint script accepts any well-formed theme name out of storage, and no theme's added density rungs or font pairings are offered. Whatever [data-theme] blocks the page imported still paint. Pass themes={[…]} — see @cueplusplus/theme-base.");
|
|
182
|
+
}, [
|
|
183
|
+
themes,
|
|
184
|
+
vocab,
|
|
185
|
+
isRoot
|
|
186
|
+
]);
|
|
187
|
+
const initialTheme = themeProp ?? (isRoot ? themes[0]?.name : ambient?.theme) ?? DEFAULT_THEME;
|
|
188
|
+
const initialDensity = densityProp ?? vocab.preferredDensity(initialTheme);
|
|
189
|
+
const initialFont = fontProp ?? vocab.preferredFont(initialTheme);
|
|
190
|
+
const [theme, setThemeState] = React.useState(initialTheme);
|
|
191
|
+
const [densityState, setDensityState] = React.useState(initialDensity);
|
|
118
192
|
const [mode, setModeState] = React.useState(modeProp);
|
|
119
|
-
const [
|
|
120
|
-
if (!isRoot || !persistPreferences) return
|
|
121
|
-
|
|
122
|
-
|
|
193
|
+
const [storedSeed] = React.useState(() => {
|
|
194
|
+
if (!isRoot || !persistPreferences) return {
|
|
195
|
+
theme: initialTheme,
|
|
196
|
+
font: initialFont
|
|
197
|
+
};
|
|
198
|
+
const stored = readPreferences(storageKey);
|
|
199
|
+
const theme = vocab.acceptsTheme(stored.theme) ? stored.theme : initialTheme;
|
|
200
|
+
const fallback = fontProp ?? (theme === initialTheme ? initialFont : vocab.preferredFont(theme));
|
|
201
|
+
return {
|
|
202
|
+
theme,
|
|
203
|
+
font: vocab.acceptsFont(theme, stored.font) ? stored.font : fallback
|
|
204
|
+
};
|
|
123
205
|
});
|
|
124
|
-
const
|
|
206
|
+
const [rootFont, setRootFontState] = React.useState(storedSeed.font);
|
|
207
|
+
const rawFont = isRoot ? rootFont : ambient.font;
|
|
125
208
|
const [systemMode, setSystemMode] = React.useState(DEFAULT_RESOLVED_MODE);
|
|
126
|
-
const resolvedMode = mode === "system" ? systemMode : mode;
|
|
127
209
|
const restoredKey = React.useRef(null);
|
|
128
210
|
const [restored, setRestored] = React.useState(false);
|
|
211
|
+
const manifest = React.useMemo(() => themes.find((m) => m.name === theme) ?? null, [themes, theme]);
|
|
212
|
+
if (vocab.themes !== null && manifest === null) warnOnce(`theme "${theme}" is not registered; useTheme().manifest is null and only the base axes are offered. Add its manifest to ThemeProvider's themes prop.`);
|
|
213
|
+
const density = vocab.acceptsDensity(theme, densityState) ? densityState : vocab.preferredDensity(theme);
|
|
214
|
+
if (density !== densityState) warnOnce(`density "${densityState}" is not offered by theme "${theme}"; using "${density}"`);
|
|
215
|
+
const fontTheme = isRoot && persistPreferences && !restored ? storedSeed.theme : theme;
|
|
216
|
+
const font = vocab.acceptsFont(fontTheme, rawFont) ? rawFont : vocab.preferredFont(fontTheme);
|
|
217
|
+
if (font !== rawFont) warnOnce(`font "${rawFont}" is not offered by theme "${fontTheme}"; using "${font}"`);
|
|
218
|
+
const resolvedMode = manifest?.supportsLight === false ? "dark" : mode === "system" ? systemMode : mode;
|
|
129
219
|
useIsomorphicLayoutEffect(() => {
|
|
130
220
|
if (!isRoot) return;
|
|
131
221
|
if (!persistPreferences) {
|
|
@@ -136,10 +226,17 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
|
|
|
136
226
|
restoredKey.current = storageKey;
|
|
137
227
|
setRestored(true);
|
|
138
228
|
const stored = readPreferences(storageKey);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
229
|
+
const next = acceptedPreferences(vocab, stored, {
|
|
230
|
+
theme,
|
|
231
|
+
density,
|
|
232
|
+
font,
|
|
233
|
+
mode
|
|
234
|
+
});
|
|
235
|
+
setThemeState(next.theme);
|
|
236
|
+
setDensityState(next.density);
|
|
237
|
+
setRootFontState(next.font);
|
|
238
|
+
setModeState(next.mode);
|
|
239
|
+
if (stored.theme !== void 0 && stored.theme !== next.theme || stored.density !== void 0 && stored.density !== next.density || stored.font !== void 0 && stored.font !== next.font || stored.mode !== void 0 && stored.mode !== next.mode) writePreferences(storageKey, next);
|
|
143
240
|
}, [
|
|
144
241
|
isRoot,
|
|
145
242
|
persistPreferences,
|
|
@@ -159,10 +256,10 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
|
|
|
159
256
|
mode: modeProp,
|
|
160
257
|
font: fontProp
|
|
161
258
|
};
|
|
162
|
-
if (previous.theme !== themeProp) setThemeState(themeProp);
|
|
163
|
-
if (previous.density !== densityProp) setDensityState(densityProp);
|
|
259
|
+
if (previous.theme !== themeProp && themeProp !== void 0) setThemeState(themeProp);
|
|
260
|
+
if (previous.density !== densityProp && densityProp !== void 0) setDensityState(densityProp);
|
|
164
261
|
if (previous.mode !== modeProp) setModeState(modeProp);
|
|
165
|
-
if (previous.font !== fontProp) setRootFontState(fontProp);
|
|
262
|
+
if (previous.font !== fontProp && fontProp !== void 0) setRootFontState(fontProp);
|
|
166
263
|
}, [
|
|
167
264
|
themeProp,
|
|
168
265
|
densityProp,
|
|
@@ -179,16 +276,16 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
|
|
|
179
276
|
}, []);
|
|
180
277
|
const stateRef = React.useRef({
|
|
181
278
|
theme,
|
|
182
|
-
density,
|
|
279
|
+
density: densityState,
|
|
183
280
|
mode,
|
|
184
|
-
font
|
|
281
|
+
font: rawFont
|
|
185
282
|
});
|
|
186
283
|
useIsomorphicLayoutEffect(() => {
|
|
187
284
|
stateRef.current = {
|
|
188
285
|
theme,
|
|
189
|
-
density,
|
|
286
|
+
density: densityState,
|
|
190
287
|
mode,
|
|
191
|
-
font
|
|
288
|
+
font: rawFont
|
|
192
289
|
};
|
|
193
290
|
});
|
|
194
291
|
const persist = React.useCallback((patch) => {
|
|
@@ -226,17 +323,13 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
|
|
|
226
323
|
useIsomorphicInsertionEffect(() => {
|
|
227
324
|
if (!isRoot || typeof document === "undefined") return;
|
|
228
325
|
const root = document.documentElement;
|
|
229
|
-
const
|
|
326
|
+
const current = {
|
|
230
327
|
theme,
|
|
231
328
|
density,
|
|
232
329
|
font,
|
|
233
330
|
mode
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
density: densityProp,
|
|
237
|
-
font: fontProp,
|
|
238
|
-
mode: modeProp
|
|
239
|
-
});
|
|
331
|
+
};
|
|
332
|
+
const next = restored || !persistPreferences ? current : acceptedPreferences(vocab, readPreferences(storageKey), current);
|
|
240
333
|
originalStamp.current ??= {
|
|
241
334
|
theme: root.getAttribute("data-theme"),
|
|
242
335
|
density: root.getAttribute("data-density"),
|
|
@@ -251,7 +344,7 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
|
|
|
251
344
|
write("data-density", next.density);
|
|
252
345
|
write("data-font", next.font);
|
|
253
346
|
write("data-mode", next.mode);
|
|
254
|
-
const scheme = next.mode === "system" ? "light dark" : next.mode;
|
|
347
|
+
const scheme = (themes.find((m) => m.name === next.theme) ?? null)?.supportsLight === false ? "dark" : next.mode === "system" ? "light dark" : next.mode;
|
|
255
348
|
if (root.style.colorScheme !== scheme) root.style.colorScheme = scheme;
|
|
256
349
|
}, [
|
|
257
350
|
isRoot,
|
|
@@ -262,10 +355,8 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
|
|
|
262
355
|
density,
|
|
263
356
|
font,
|
|
264
357
|
mode,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
fontProp,
|
|
268
|
-
modeProp
|
|
358
|
+
themes,
|
|
359
|
+
vocab
|
|
269
360
|
]);
|
|
270
361
|
React.useEffect(() => () => {
|
|
271
362
|
const original = originalStamp.current;
|
|
@@ -284,6 +375,7 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
|
|
|
284
375
|
}, []);
|
|
285
376
|
const contextValue = React.useMemo(() => ({
|
|
286
377
|
theme,
|
|
378
|
+
manifest,
|
|
287
379
|
density,
|
|
288
380
|
mode,
|
|
289
381
|
font,
|
|
@@ -294,6 +386,7 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
|
|
|
294
386
|
setFont
|
|
295
387
|
}), [
|
|
296
388
|
theme,
|
|
389
|
+
manifest,
|
|
297
390
|
density,
|
|
298
391
|
mode,
|
|
299
392
|
font,
|
|
@@ -303,6 +396,8 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
|
|
|
303
396
|
setMode,
|
|
304
397
|
setFont
|
|
305
398
|
]);
|
|
399
|
+
/** The axes on offer under the theme that is actually active. */
|
|
400
|
+
const registry = React.useMemo(() => buildRegistry(themes, theme), [themes, theme]);
|
|
306
401
|
const parentFontFamilies = React.useContext(FontFamiliesContext);
|
|
307
402
|
const effectiveFontFamilies = React.useMemo(() => {
|
|
308
403
|
const sans = fontFamilies?.sans ?? parentFontFamilies?.sans;
|
|
@@ -317,17 +412,40 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
|
|
|
317
412
|
parentFontFamilies?.sans,
|
|
318
413
|
parentFontFamilies?.mono
|
|
319
414
|
]);
|
|
415
|
+
const overridesKey = JSON.stringify(overrides ?? null);
|
|
416
|
+
const inline = React.useMemo(() => inlineOverrides(overrides, resolvedMode), [overridesKey, resolvedMode]);
|
|
417
|
+
const overridesStyle = React.useMemo(() => overridesCss(overrides), [overridesKey]);
|
|
418
|
+
const parentOverrides = React.useContext(OverridesContext);
|
|
419
|
+
const effectiveOverrides = React.useMemo(() => {
|
|
420
|
+
const merged = {
|
|
421
|
+
...parentOverrides,
|
|
422
|
+
...inline
|
|
423
|
+
};
|
|
424
|
+
return Object.keys(merged).length === 0 ? null : merged;
|
|
425
|
+
}, [parentOverrides, inline]);
|
|
426
|
+
React.useEffect(() => {
|
|
427
|
+
if (process.env.NODE_ENV === "production") return;
|
|
428
|
+
reportOverrides(manifest, resolvedMode, overrides, theme);
|
|
429
|
+
}, [
|
|
430
|
+
manifest,
|
|
431
|
+
resolvedMode,
|
|
432
|
+
overridesKey,
|
|
433
|
+
theme
|
|
434
|
+
]);
|
|
320
435
|
const style = React.useMemo(() => ({
|
|
321
|
-
colorScheme: mode === "system" ? "light dark" : mode,
|
|
436
|
+
colorScheme: manifest?.supportsLight === false ? "dark" : mode === "system" ? "light dark" : mode,
|
|
322
437
|
"--cue-font-scale": fontScale,
|
|
323
438
|
...fontFamilies?.sans === void 0 ? null : { "--cue-font-sans": fontFamilies.sans },
|
|
324
439
|
...fontFamilies?.mono === void 0 ? null : { "--cue-font-mono": fontFamilies.mono },
|
|
440
|
+
...inline,
|
|
325
441
|
...styleProp
|
|
326
442
|
}), [
|
|
443
|
+
manifest?.supportsLight,
|
|
327
444
|
mode,
|
|
328
445
|
fontScale,
|
|
329
446
|
fontFamilies?.sans,
|
|
330
447
|
fontFamilies?.mono,
|
|
448
|
+
inline,
|
|
331
449
|
styleProp
|
|
332
450
|
]);
|
|
333
451
|
const stamp = {
|
|
@@ -353,15 +471,28 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
|
|
|
353
471
|
style,
|
|
354
472
|
children
|
|
355
473
|
});
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
children:
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
474
|
+
const densityLayer = overridesStyle === "" ? null : /* @__PURE__ */ jsx("style", {
|
|
475
|
+
"data-cue-overrides": "",
|
|
476
|
+
children: overridesStyle
|
|
477
|
+
});
|
|
478
|
+
return /* @__PURE__ */ jsx(ThemeRegistryContext.Provider, {
|
|
479
|
+
value: registry,
|
|
480
|
+
children: /* @__PURE__ */ jsx(ThemeContext.Provider, {
|
|
481
|
+
value: contextValue,
|
|
482
|
+
children: /* @__PURE__ */ jsx(AmbientThemeContext.Provider, {
|
|
483
|
+
value: theme,
|
|
484
|
+
children: /* @__PURE__ */ jsx(FontScaleContext.Provider, {
|
|
485
|
+
value: fontScale,
|
|
486
|
+
children: /* @__PURE__ */ jsx(FontFamiliesContext.Provider, {
|
|
487
|
+
value: effectiveFontFamilies,
|
|
488
|
+
children: /* @__PURE__ */ jsx(OverridesContext.Provider, {
|
|
489
|
+
value: effectiveOverrides,
|
|
490
|
+
children: /* @__PURE__ */ jsxs(DensityContext.Provider, {
|
|
491
|
+
value: density,
|
|
492
|
+
children: [root, densityLayer]
|
|
493
|
+
})
|
|
494
|
+
})
|
|
495
|
+
})
|
|
365
496
|
})
|
|
366
497
|
})
|
|
367
498
|
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ThemeRegistry } from "./vocabulary.js";
|
|
2
|
+
import "react";
|
|
3
|
+
import { DensityEntry, FontEntry, ThemeManifest } from "@cueplusplus/theme-base";
|
|
4
|
+
//#region src/system/theme-registry.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Every registered manifest, in registration order.
|
|
7
|
+
*
|
|
8
|
+
* The array a picker enumerates. It is the `themes` prop the nearest provider
|
|
9
|
+
* was handed, not a copy: registration is an application decision, and this
|
|
10
|
+
* library never discovers a theme by itself.
|
|
11
|
+
*
|
|
12
|
+
* @throws If called outside a `<ThemeProvider>`, like `useTheme()`.
|
|
13
|
+
* @example
|
|
14
|
+
* const themes = useThemes();
|
|
15
|
+
* const { theme, setTheme } = useTheme();
|
|
16
|
+
* return themes.map((m) => (
|
|
17
|
+
* <Chip key={m.name} selected={m.name === theme} onClick={() => setTheme(m.name)}>
|
|
18
|
+
* {m.name}
|
|
19
|
+
* </Chip>
|
|
20
|
+
* ));
|
|
21
|
+
*/
|
|
22
|
+
declare function useThemes(): readonly ThemeManifest[];
|
|
23
|
+
/**
|
|
24
|
+
* The rungs an app may offer right now: the base five, then the ACTIVE theme's
|
|
25
|
+
* additions.
|
|
26
|
+
*
|
|
27
|
+
* Active, not registered: a rung a theme adds is emitted under that theme's
|
|
28
|
+
* `[data-theme]`, so offering another theme's rung would offer a control that
|
|
29
|
+
* changes nothing. Switch theme and this list changes with it.
|
|
30
|
+
*
|
|
31
|
+
* @throws If called outside a `<ThemeProvider>`.
|
|
32
|
+
* @example
|
|
33
|
+
* const rungs = useDensities();
|
|
34
|
+
* const { density, setDensity } = useTheme();
|
|
35
|
+
* <Select
|
|
36
|
+
* items={rungs.map((d) => ({ value: d.name, label: d.name }))}
|
|
37
|
+
* value={density}
|
|
38
|
+
* onValueChange={setDensity}
|
|
39
|
+
* />
|
|
40
|
+
*/
|
|
41
|
+
declare function useDensities(): readonly DensityEntry[];
|
|
42
|
+
/**
|
|
43
|
+
* The font pairings, by the same rule: the base eight, then the active theme's.
|
|
44
|
+
*
|
|
45
|
+
* @throws If called outside a `<ThemeProvider>`.
|
|
46
|
+
* @example
|
|
47
|
+
* // The stacks a pairing resolves to, for a specimen that renders its own row.
|
|
48
|
+
* const { font } = useTheme();
|
|
49
|
+
* const sans = useFonts().find((f) => f.name === font)?.stacks["font-sans"];
|
|
50
|
+
*/
|
|
51
|
+
declare function useFonts(): readonly FontEntry[];
|
|
52
|
+
//#endregion
|
|
53
|
+
export { type ThemeRegistry, useDensities, useFonts, useThemes };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import "./vocabulary.js";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
//#region src/system/theme-registry.tsx
|
|
5
|
+
/** Internal: consumers read it through the three hooks. `null` outside a provider. */
|
|
6
|
+
const ThemeRegistryContext = React.createContext(null);
|
|
7
|
+
function useRegistry(hook) {
|
|
8
|
+
const registry = React.useContext(ThemeRegistryContext);
|
|
9
|
+
if (registry === null) throw new Error(`${hook}() must be called inside a <ThemeProvider>.`);
|
|
10
|
+
return registry;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Every registered manifest, in registration order.
|
|
14
|
+
*
|
|
15
|
+
* The array a picker enumerates. It is the `themes` prop the nearest provider
|
|
16
|
+
* was handed, not a copy: registration is an application decision, and this
|
|
17
|
+
* library never discovers a theme by itself.
|
|
18
|
+
*
|
|
19
|
+
* @throws If called outside a `<ThemeProvider>`, like `useTheme()`.
|
|
20
|
+
* @example
|
|
21
|
+
* const themes = useThemes();
|
|
22
|
+
* const { theme, setTheme } = useTheme();
|
|
23
|
+
* return themes.map((m) => (
|
|
24
|
+
* <Chip key={m.name} selected={m.name === theme} onClick={() => setTheme(m.name)}>
|
|
25
|
+
* {m.name}
|
|
26
|
+
* </Chip>
|
|
27
|
+
* ));
|
|
28
|
+
*/
|
|
29
|
+
function useThemes() {
|
|
30
|
+
return useRegistry("useThemes").themes;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The rungs an app may offer right now: the base five, then the ACTIVE theme's
|
|
34
|
+
* additions.
|
|
35
|
+
*
|
|
36
|
+
* Active, not registered: a rung a theme adds is emitted under that theme's
|
|
37
|
+
* `[data-theme]`, so offering another theme's rung would offer a control that
|
|
38
|
+
* changes nothing. Switch theme and this list changes with it.
|
|
39
|
+
*
|
|
40
|
+
* @throws If called outside a `<ThemeProvider>`.
|
|
41
|
+
* @example
|
|
42
|
+
* const rungs = useDensities();
|
|
43
|
+
* const { density, setDensity } = useTheme();
|
|
44
|
+
* <Select
|
|
45
|
+
* items={rungs.map((d) => ({ value: d.name, label: d.name }))}
|
|
46
|
+
* value={density}
|
|
47
|
+
* onValueChange={setDensity}
|
|
48
|
+
* />
|
|
49
|
+
*/
|
|
50
|
+
function useDensities() {
|
|
51
|
+
return useRegistry("useDensities").densities;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The font pairings, by the same rule: the base eight, then the active theme's.
|
|
55
|
+
*
|
|
56
|
+
* @throws If called outside a `<ThemeProvider>`.
|
|
57
|
+
* @example
|
|
58
|
+
* // The stacks a pairing resolves to, for a specimen that renders its own row.
|
|
59
|
+
* const { font } = useTheme();
|
|
60
|
+
* const sans = useFonts().find((f) => f.name === font)?.stacks["font-sans"];
|
|
61
|
+
*/
|
|
62
|
+
function useFonts() {
|
|
63
|
+
return useRegistry("useFonts").fonts;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { ThemeRegistryContext, useDensities, useFonts, useThemes };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Density } from "@cueplusplus/
|
|
1
|
+
import { Density } from "@cueplusplus/theme-base";
|
|
2
2
|
//#region src/system/use-density.d.ts
|
|
3
3
|
/** Rung of the control ladder to measure. */
|
|
4
4
|
type ControlSize = "sm" | "md" | "lg";
|
|
@@ -27,10 +27,18 @@ declare function useDensity(): Density;
|
|
|
27
27
|
* canvas renderer needs when it cannot ask CSS.
|
|
28
28
|
*
|
|
29
29
|
* Measured from a throwaway `document.body` probe stamped with the ambient
|
|
30
|
-
* density
|
|
31
|
-
* re-raise of `ultra-compact
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* density *and the ambient theme*, so app-wide token overrides, the `@media
|
|
31
|
+
* (pointer: coarse)` re-raise of `ultra-compact`, and a rung a theme adds are
|
|
32
|
+
* all honoured. Overrides scoped to a subtree the probe is not in are still
|
|
33
|
+
* not: the probe hangs off `document.body`, outside every provider root.
|
|
34
|
+
*
|
|
35
|
+
* Falls back to the authored ladder × 16px during SSR and before the first
|
|
36
|
+
* layout effect, which keeps hydration stable. A rung the ambient theme does
|
|
37
|
+
* not declare falls back to the default ladder with a development warning
|
|
38
|
+
* rather than throwing — the probe corrects it on the next tick either way.
|
|
39
|
+
*
|
|
40
|
+
* Safe outside a `<ThemeProvider>`: it reads the theme and the registry from
|
|
41
|
+
* context directly instead of through `useTheme()`, which throws.
|
|
34
42
|
*
|
|
35
43
|
* @param size - Which rung to measure. Defaults to `"md"`.
|
|
36
44
|
* @returns Height in CSS pixels.
|