@ojiepermana/angular 22.1.11 → 22.1.13
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 +32 -26
- package/chart/README.md +272 -0
- package/component/README.md +89 -0
- package/component/accordion/README.md +195 -0
- package/component/alert/README.md +184 -0
- package/component/alert-dialog/README.md +255 -0
- package/component/aspect-ratio/README.md +116 -0
- package/component/avatar/README.md +182 -0
- package/component/badge/README.md +143 -0
- package/component/breadcrumb/README.md +223 -0
- package/component/button/README.md +141 -0
- package/component/button-group/README.md +214 -0
- package/component/calendar/README.md +137 -0
- package/component/card/README.md +232 -0
- package/component/carousel/README.md +276 -0
- package/component/checkbox/README.md +153 -0
- package/component/collapsible/README.md +216 -0
- package/component/combobox/README.md +202 -0
- package/component/command/README.md +287 -0
- package/component/composer/README.md +241 -0
- package/component/context-menu/README.md +268 -0
- package/component/date-picker/README.md +202 -0
- package/component/dialog/README.md +276 -0
- package/component/drawer/README.md +147 -0
- package/component/dropdown-menu/README.md +336 -0
- package/component/editor/README.md +152 -0
- package/component/empty/README.md +193 -0
- package/component/form/README.md +216 -0
- package/component/hover-card/README.md +150 -0
- package/component/icon/README.md +25 -0
- package/component/input/README.md +160 -0
- package/component/input-group/README.md +245 -0
- package/component/input-otp/README.md +331 -0
- package/component/item/README.md +254 -0
- package/component/kanban/README.md +81 -0
- package/component/kbd/README.md +141 -0
- package/component/label/README.md +137 -0
- package/component/menubar/README.md +270 -0
- package/component/native-select/README.md +176 -0
- package/component/navigation-menu/README.md +165 -0
- package/component/pagination/README.md +151 -0
- package/component/pillbox/README.md +68 -0
- package/component/popover/README.md +54 -0
- package/component/progress/README.md +160 -0
- package/component/radio/README.md +212 -0
- package/component/resizable/README.md +178 -0
- package/component/scroll-area/README.md +150 -0
- package/component/select/README.md +228 -0
- package/component/separator/README.md +175 -0
- package/component/sheet/README.md +190 -0
- package/component/skeleton/README.md +158 -0
- package/component/slider/README.md +207 -0
- package/component/spinner/README.md +160 -0
- package/component/switch/README.md +170 -0
- package/component/table/README.md +292 -0
- package/component/tabs/README.md +220 -0
- package/component/textarea/README.md +169 -0
- package/component/timeline/README.md +94 -0
- package/component/toast/README.md +173 -0
- package/component/toggle/README.md +133 -0
- package/component/toggle-group/README.md +234 -0
- package/component/tooltip/README.md +230 -0
- package/fesm2022/ojiepermana-angular-chart-area.mjs +289 -3
- package/fesm2022/ojiepermana-angular-chart-bar.mjs +722 -3
- package/fesm2022/ojiepermana-angular-chart-core.mjs +1482 -3
- package/fesm2022/ojiepermana-angular-chart-line.mjs +294 -3
- package/fesm2022/ojiepermana-angular-chart-pie.mjs +291 -3
- package/fesm2022/ojiepermana-angular-chart-primitives.mjs +1301 -3
- package/fesm2022/ojiepermana-angular-chart-radar.mjs +421 -3
- package/fesm2022/ojiepermana-angular-chart-radial.mjs +295 -3
- package/fesm2022/ojiepermana-angular-chart-scatter.mjs +306 -3
- package/fesm2022/ojiepermana-angular-chart.mjs +12 -3
- package/fesm2022/ojiepermana-angular-component-accordion.mjs +267 -3
- package/fesm2022/ojiepermana-angular-component-alert-dialog.mjs +359 -3
- package/fesm2022/ojiepermana-angular-component-alert.mjs +101 -3
- package/fesm2022/ojiepermana-angular-component-aspect-ratio.mjs +31 -3
- package/fesm2022/ojiepermana-angular-component-avatar.mjs +139 -3
- package/fesm2022/ojiepermana-angular-component-badge.mjs +57 -3
- package/fesm2022/ojiepermana-angular-component-breadcrumb.mjs +192 -3
- package/fesm2022/ojiepermana-angular-component-button-group.mjs +95 -3
- package/fesm2022/ojiepermana-angular-component-button.mjs +73 -3
- package/fesm2022/ojiepermana-angular-component-calendar.mjs +718 -3
- package/fesm2022/ojiepermana-angular-component-card.mjs +167 -3
- package/fesm2022/ojiepermana-angular-component-carousel.mjs +326 -3
- package/fesm2022/ojiepermana-angular-component-checkbox.mjs +190 -3
- package/fesm2022/ojiepermana-angular-component-collapsible.mjs +117 -3
- package/fesm2022/ojiepermana-angular-component-combobox.mjs +327 -3
- package/fesm2022/ojiepermana-angular-component-command.mjs +351 -3
- package/fesm2022/ojiepermana-angular-component-composer.mjs +359 -3
- package/fesm2022/ojiepermana-angular-component-context-menu.mjs +158 -3
- package/fesm2022/ojiepermana-angular-component-date-picker.mjs +482 -3
- package/fesm2022/ojiepermana-angular-component-dialog.mjs +400 -3
- package/fesm2022/ojiepermana-angular-component-drawer.mjs +1 -3
- package/fesm2022/ojiepermana-angular-component-dropdown-menu.mjs +689 -3
- package/fesm2022/ojiepermana-angular-component-editor.mjs +511 -3
- package/fesm2022/ojiepermana-angular-component-empty.mjs +134 -3
- package/fesm2022/ojiepermana-angular-component-form.mjs +438 -3
- package/fesm2022/ojiepermana-angular-component-hover-card.mjs +273 -3
- package/fesm2022/ojiepermana-angular-component-icon.mjs +139 -3
- package/fesm2022/ojiepermana-angular-component-input-group.mjs +176 -3
- package/fesm2022/ojiepermana-angular-component-input-otp.mjs +550 -3
- package/fesm2022/ojiepermana-angular-component-input.mjs +52 -3
- package/fesm2022/ojiepermana-angular-component-item.mjs +264 -3
- package/fesm2022/ojiepermana-angular-component-kanban.mjs +310 -3
- package/fesm2022/ojiepermana-angular-component-kbd.mjs +48 -3
- package/fesm2022/ojiepermana-angular-component-label.mjs +27 -3
- package/fesm2022/ojiepermana-angular-component-menubar.mjs +327 -3
- package/fesm2022/ojiepermana-angular-component-native-select.mjs +62 -3
- package/fesm2022/ojiepermana-angular-component-navigation-menu.mjs +378 -3
- package/fesm2022/ojiepermana-angular-component-pagination.mjs +238 -3
- package/fesm2022/ojiepermana-angular-component-pillbox.mjs +999 -3
- package/fesm2022/ojiepermana-angular-component-popover.mjs +201 -3
- package/fesm2022/ojiepermana-angular-component-progress.mjs +57 -3
- package/fesm2022/ojiepermana-angular-component-radio.mjs +180 -3
- package/fesm2022/ojiepermana-angular-component-resizable.mjs +493 -3
- package/fesm2022/ojiepermana-angular-component-scroll-area.mjs +51 -3
- package/fesm2022/ojiepermana-angular-component-segmented-control.mjs +227 -3
- package/fesm2022/ojiepermana-angular-component-select.mjs +609 -3
- package/fesm2022/ojiepermana-angular-component-separator.mjs +31 -3
- package/fesm2022/ojiepermana-angular-component-sheet.mjs +407 -3
- package/fesm2022/ojiepermana-angular-component-skeleton.mjs +25 -3
- package/fesm2022/ojiepermana-angular-component-slider.mjs +456 -3
- package/fesm2022/ojiepermana-angular-component-spinner.mjs +56 -3
- package/fesm2022/ojiepermana-angular-component-switch.mjs +145 -3
- package/fesm2022/ojiepermana-angular-component-table.mjs +155 -3
- package/fesm2022/ojiepermana-angular-component-tabs.mjs +315 -3
- package/fesm2022/ojiepermana-angular-component-textarea.mjs +46 -3
- package/fesm2022/ojiepermana-angular-component-timeline.mjs +237 -3
- package/fesm2022/ojiepermana-angular-component-toast.mjs +241 -3
- package/fesm2022/ojiepermana-angular-component-toggle-group.mjs +290 -3
- package/fesm2022/ojiepermana-angular-component-toggle.mjs +76 -3
- package/fesm2022/ojiepermana-angular-component-tooltip.mjs +519 -3
- package/fesm2022/ojiepermana-angular-component-utils-core.mjs +310 -3
- package/fesm2022/ojiepermana-angular-component-utils-overlay.mjs +62 -3
- package/fesm2022/ojiepermana-angular-component-utils-scale.mjs +34 -3
- package/fesm2022/ojiepermana-angular-component-utils.mjs +3 -2
- package/fesm2022/ojiepermana-angular-component.mjs +4 -4
- package/fesm2022/ojiepermana-angular-navigation-core.mjs +21 -3
- package/fesm2022/ojiepermana-angular-navigation-icon.mjs +20 -3
- package/fesm2022/ojiepermana-angular-navigation-navigation-badge-count.component-CDBxQ8rk.mjs +91 -0
- package/fesm2022/ojiepermana-angular-navigation-navigation-dockbar-menu.component-DBQHdvGa.mjs +512 -0
- package/fesm2022/ojiepermana-angular-navigation-navigation-entry-grid.component-Cd-jh6tg.mjs +623 -0
- package/fesm2022/ojiepermana-angular-navigation-navigation-flyout-menu.component-BCGvhoYj.mjs +404 -0
- package/fesm2022/ojiepermana-angular-navigation-navigation-horizontal.component-Dk_03e-6.mjs +466 -0
- package/fesm2022/ojiepermana-angular-navigation-ojiepermana-angular-navigation-DNKU6viC.mjs +2642 -0
- package/fesm2022/ojiepermana-angular-navigation-service.mjs +725 -3
- package/fesm2022/ojiepermana-angular-navigation-types.mjs +65 -4
- package/fesm2022/ojiepermana-angular-navigation.mjs +4 -10
- package/fesm2022/ojiepermana-angular-sdk.mjs +204 -4
- package/fesm2022/ojiepermana-angular-theme-component-settings.mjs +1387 -3
- package/fesm2022/ojiepermana-angular-theme-layout-services.mjs +331 -3
- package/fesm2022/ojiepermana-angular-theme-layout-types.mjs +45 -4
- package/fesm2022/ojiepermana-angular-theme-layout-wrapper.mjs +1044 -3
- package/fesm2022/ojiepermana-angular-theme-layout.mjs +729 -3
- package/fesm2022/ojiepermana-angular-theme-page-core.mjs +381 -3
- package/fesm2022/ojiepermana-angular-theme-page-filter.mjs +267 -3
- package/fesm2022/ojiepermana-angular-theme-page-root.mjs +361 -3
- package/fesm2022/ojiepermana-angular-theme-page-side.mjs +198 -3
- package/fesm2022/ojiepermana-angular-theme-page-slots.mjs +110 -3
- package/fesm2022/ojiepermana-angular-theme-page.mjs +7 -2
- package/fesm2022/ojiepermana-angular-theme-shell-services.mjs +338 -3
- package/fesm2022/ojiepermana-angular-theme-shell-types.mjs +160 -4
- package/fesm2022/ojiepermana-angular-theme-shell.mjs +811 -3
- package/fesm2022/ojiepermana-angular-theme-styles.mjs +783 -3
- package/fesm2022/ojiepermana-angular-theme.mjs +4 -4
- package/fesm2022/ojiepermana-angular.mjs +6 -11
- package/navigation/README.md +374 -0
- package/package.json +81 -20
- package/sdk/README.md +212 -0
- package/sdk/schematics/collection.json +20 -0
- package/sdk/schematics/init/index.js +83 -0
- package/sdk/schematics/init/schema.json +19 -0
- package/sdk/schematics/ng-add/index.js +147 -0
- package/sdk/schematics/ng-add/schema.json +14 -0
- package/sdk/schematics/package.json +3 -0
- package/sdk/schematics/sdk/index.js +175 -0
- package/sdk/schematics/sdk/schema.json +19 -0
- package/sdk/sdk.config.example.json +24 -0
- package/sdk/src/config/loader.js +43 -0
- package/sdk/src/config/schema.js +59 -0
- package/sdk/src/emit/client.js +251 -0
- package/sdk/src/emit/metadata.js +310 -0
- package/sdk/src/emit/models.js +104 -0
- package/sdk/src/emit/navigation.js +56 -0
- package/sdk/src/emit/operations.js +118 -0
- package/sdk/src/emit/public-api.js +54 -0
- package/sdk/src/emit/services.js +110 -0
- package/sdk/src/engine.js +67 -0
- package/sdk/src/layout/per-domain.js +360 -0
- package/sdk/src/package.json +3 -0
- package/sdk/src/parser/bundle.js +25 -0
- package/sdk/src/parser/ir.js +429 -0
- package/sdk/src/parser/types.js +7 -0
- package/sdk/src/render/template.js +152 -0
- package/sdk/src/writer/index.js +286 -0
- package/theme/README.md +126 -0
- package/theme/component/settings/README.md +135 -0
- package/theme/layout/README.md +539 -0
- package/theme/page/README.md +453 -0
- package/theme/shell/README.md +340 -0
- package/theme/styles/README.md +178 -0
- package/theme/styles/css/base/components.css +553 -0
- package/theme/styles/css/base/index.css +14 -0
- package/theme/styles/css/base/package.css +9 -0
- package/theme/styles/css/base/prefers-dark.css +86 -0
- package/theme/styles/css/base/print.css +62 -0
- package/theme/styles/css/base/status.css +38 -0
- package/theme/styles/css/base/tailwind.css +165 -0
- package/theme/styles/css/base/theme.css +374 -0
- package/theme/styles/css/base/tokens.css +361 -0
- package/theme/styles/css/color/amber.css +72 -0
- package/theme/styles/css/color/blue.css +72 -0
- package/theme/styles/css/color/brand.css +38 -0
- package/theme/styles/css/color/cyan.css +72 -0
- package/theme/styles/css/color/emerald.css +72 -0
- package/theme/styles/css/color/fuchsia.css +72 -0
- package/theme/styles/css/color/green.css +72 -0
- package/theme/styles/css/color/index.css +19 -0
- package/theme/styles/css/color/indigo.css +72 -0
- package/theme/styles/css/color/lime.css +72 -0
- package/theme/styles/css/color/orange.css +72 -0
- package/theme/styles/css/color/pink.css +72 -0
- package/theme/styles/css/color/purple.css +72 -0
- package/theme/styles/css/color/red.css +72 -0
- package/theme/styles/css/color/rose.css +72 -0
- package/theme/styles/css/color/sky.css +72 -0
- package/theme/styles/css/color/teal.css +72 -0
- package/theme/styles/css/color/violet.css +72 -0
- package/theme/styles/css/color/yellow.css +72 -0
- package/theme/styles/css/index.css +18 -0
- package/theme/styles/css/neutral/gray.css +63 -0
- package/theme/styles/css/neutral/index.css +11 -0
- package/theme/styles/css/neutral/mauve.css +63 -0
- package/theme/styles/css/neutral/mist.css +63 -0
- package/theme/styles/css/neutral/neutral.css +63 -0
- package/theme/styles/css/neutral/olive.css +63 -0
- package/theme/styles/css/neutral/slate.css +63 -0
- package/theme/styles/css/neutral/stone.css +63 -0
- package/theme/styles/css/neutral/taupe.css +63 -0
- package/theme/styles/css/neutral/zinc.css +63 -0
- package/theme/styles/css/radius/index.css +30 -0
- package/theme/styles/css/space/index.css +20 -0
- package/types/ojiepermana-angular-chart-area.d.ts +76 -1
- package/types/ojiepermana-angular-chart-bar.d.ts +201 -1
- package/types/ojiepermana-angular-chart-core.d.ts +811 -1
- package/types/ojiepermana-angular-chart-line.d.ts +75 -1
- package/types/ojiepermana-angular-chart-pie.d.ts +131 -1
- package/types/ojiepermana-angular-chart-primitives.d.ts +291 -1
- package/types/ojiepermana-angular-chart-radar.d.ts +116 -1
- package/types/ojiepermana-angular-chart-radial.d.ts +116 -1
- package/types/ojiepermana-angular-chart-scatter.d.ts +125 -1
- package/types/ojiepermana-angular-chart.d.ts +2 -1
- package/types/ojiepermana-angular-component-accordion.d.ts +85 -1
- package/types/ojiepermana-angular-component-alert-dialog.d.ts +140 -1
- package/types/ojiepermana-angular-component-alert.d.ts +42 -1
- package/types/ojiepermana-angular-component-aspect-ratio.d.ts +12 -1
- package/types/ojiepermana-angular-component-avatar.d.ts +56 -1
- package/types/ojiepermana-angular-component-badge.d.ts +24 -1
- package/types/ojiepermana-angular-component-breadcrumb.d.ts +46 -1
- package/types/ojiepermana-angular-component-button-group.d.ts +26 -1
- package/types/ojiepermana-angular-component-button.d.ts +27 -1
- package/types/ojiepermana-angular-component-calendar.d.ts +111 -1
- package/types/ojiepermana-angular-component-card.d.ts +69 -1
- package/types/ojiepermana-angular-component-carousel.d.ts +86 -1
- package/types/ojiepermana-angular-component-checkbox.d.ts +42 -1
- package/types/ojiepermana-angular-component-collapsible.d.ts +42 -1
- package/types/ojiepermana-angular-component-combobox.d.ts +60 -1
- package/types/ojiepermana-angular-component-command.d.ts +130 -1
- package/types/ojiepermana-angular-component-composer.d.ts +90 -1
- package/types/ojiepermana-angular-component-context-menu.d.ts +54 -1
- package/types/ojiepermana-angular-component-date-picker.d.ts +109 -1
- package/types/ojiepermana-angular-component-dialog.d.ts +136 -1
- package/types/ojiepermana-angular-component-drawer.d.ts +1 -1
- package/types/ojiepermana-angular-component-dropdown-menu.d.ts +248 -1
- package/types/ojiepermana-angular-component-editor.d.ts +135 -1
- package/types/ojiepermana-angular-component-empty.d.ts +50 -1
- package/types/ojiepermana-angular-component-form.d.ts +159 -1
- package/types/ojiepermana-angular-component-hover-card.d.ts +75 -1
- package/types/ojiepermana-angular-component-icon.d.ts +89 -1
- package/types/ojiepermana-angular-component-input-group.d.ts +51 -1
- package/types/ojiepermana-angular-component-input-otp.d.ts +155 -1
- package/types/ojiepermana-angular-component-input.d.ts +21 -1
- package/types/ojiepermana-angular-component-item.d.ts +93 -1
- package/types/ojiepermana-angular-component-kanban.d.ts +70 -1
- package/types/ojiepermana-angular-component-kbd.d.ts +16 -1
- package/types/ojiepermana-angular-component-label.d.ts +11 -1
- package/types/ojiepermana-angular-component-menubar.d.ts +93 -1
- package/types/ojiepermana-angular-component-native-select.d.ts +26 -1
- package/types/ojiepermana-angular-component-navigation-menu.d.ts +104 -1
- package/types/ojiepermana-angular-component-pagination.d.ts +33 -1
- package/types/ojiepermana-angular-component-pillbox.d.ts +222 -1
- package/types/ojiepermana-angular-component-popover.d.ts +66 -1
- package/types/ojiepermana-angular-component-progress.d.ts +17 -1
- package/types/ojiepermana-angular-component-radio.d.ts +60 -1
- package/types/ojiepermana-angular-component-resizable.d.ts +117 -1
- package/types/ojiepermana-angular-component-scroll-area.d.ts +19 -1
- package/types/ojiepermana-angular-component-segmented-control.d.ts +58 -1
- package/types/ojiepermana-angular-component-select.d.ts +122 -1
- package/types/ojiepermana-angular-component-separator.d.ts +14 -1
- package/types/ojiepermana-angular-component-sheet.d.ts +130 -1
- package/types/ojiepermana-angular-component-skeleton.d.ts +10 -1
- package/types/ojiepermana-angular-component-slider.d.ts +83 -1
- package/types/ojiepermana-angular-component-spinner.d.ts +13 -1
- package/types/ojiepermana-angular-component-switch.d.ts +44 -1
- package/types/ojiepermana-angular-component-table.d.ts +57 -1
- package/types/ojiepermana-angular-component-tabs.d.ts +124 -1
- package/types/ojiepermana-angular-component-textarea.d.ts +17 -1
- package/types/ojiepermana-angular-component-timeline.d.ts +64 -1
- package/types/ojiepermana-angular-component-toast.d.ts +64 -1
- package/types/ojiepermana-angular-component-toggle-group.d.ts +90 -1
- package/types/ojiepermana-angular-component-toggle.d.ts +25 -1
- package/types/ojiepermana-angular-component-tooltip.d.ts +143 -1
- package/types/ojiepermana-angular-component-utils-core.d.ts +184 -1
- package/types/ojiepermana-angular-component-utils-overlay.d.ts +21 -1
- package/types/ojiepermana-angular-component-utils-scale.d.ts +19 -1
- package/types/ojiepermana-angular-component-utils.d.ts +2 -1
- package/types/ojiepermana-angular-component.d.ts +2 -1
- package/types/ojiepermana-angular-navigation-core.d.ts +59 -1
- package/types/ojiepermana-angular-navigation-icon.d.ts +12 -1
- package/types/ojiepermana-angular-navigation-service.d.ts +116 -1
- package/types/ojiepermana-angular-navigation-types.d.ts +144 -1
- package/types/ojiepermana-angular-navigation.d.ts +507 -1
- package/types/ojiepermana-angular-sdk.d.ts +89 -1
- package/types/ojiepermana-angular-theme-component-settings.d.ts +154 -1
- package/types/ojiepermana-angular-theme-layout-services.d.ts +147 -1
- package/types/ojiepermana-angular-theme-layout-types.d.ts +40 -1
- package/types/ojiepermana-angular-theme-layout-wrapper.d.ts +299 -1
- package/types/ojiepermana-angular-theme-layout.d.ts +129 -1
- package/types/ojiepermana-angular-theme-page-core.d.ts +157 -1
- package/types/ojiepermana-angular-theme-page-filter.d.ts +77 -1
- package/types/ojiepermana-angular-theme-page-root.d.ts +88 -1
- package/types/ojiepermana-angular-theme-page-side.d.ts +50 -1
- package/types/ojiepermana-angular-theme-page-slots.d.ts +40 -1
- package/types/ojiepermana-angular-theme-page.d.ts +5 -1
- package/types/ojiepermana-angular-theme-shell-services.d.ts +193 -1
- package/types/ojiepermana-angular-theme-shell-types.d.ts +105 -1
- package/types/ojiepermana-angular-theme-shell.d.ts +100 -1
- package/types/ojiepermana-angular-theme-styles.d.ts +319 -1
- package/types/ojiepermana-angular-theme.d.ts +2 -1
- package/types/ojiepermana-angular.d.ts +5 -5
|
@@ -1,7 +1,787 @@
|
|
|
1
|
-
|
|
1
|
+
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { inject, signal, effect, computed, DestroyRef, InjectionToken, Service, makeEnvironmentProviders, provideEnvironmentInitializer, PLATFORM_ID } from '@angular/core';
|
|
4
|
+
import { documentLocalStorage } from '@ojiepermana/angular/component/utils/core';
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
/**
|
|
7
|
+
* One place for the "a preference that lives in localStorage and shows up as an
|
|
8
|
+
* attribute on `<html>`" pattern that every theme axis needs.
|
|
9
|
+
*
|
|
10
|
+
* Each axis service used to hand-roll the same four private methods (`storage`,
|
|
11
|
+
* `readStorage`, `persist`, plus an effect writing the attribute) around a
|
|
12
|
+
* `signal`. That is the whole body of the simpler services, so a fix to any of
|
|
13
|
+
* it had to be made five times.
|
|
14
|
+
*
|
|
15
|
+
* The storage half itself lives in `@ojiepermana/angular/component/utils/core`,
|
|
16
|
+
* because `navigation` needs it too and navigation must not depend on this
|
|
17
|
+
* package.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* A persisted value, without the DOM half.
|
|
21
|
+
*
|
|
22
|
+
* Useful on its own for a preference that is not an attribute: the brand axis
|
|
23
|
+
* stores an object and paints custom properties instead.
|
|
24
|
+
*
|
|
25
|
+
* Two timing details the theme services depend on:
|
|
26
|
+
*
|
|
27
|
+
* - the stored value is read SYNCHRONOUSLY here, not in an effect, because
|
|
28
|
+
* every service exposes its state to consumers that read it during the same
|
|
29
|
+
* change detection pass;
|
|
30
|
+
* - the effect writes on its FIRST run, so storage holds the resolved default
|
|
31
|
+
* immediately after construction even when nothing was stored.
|
|
32
|
+
*/
|
|
33
|
+
function persistedSignal(options) {
|
|
34
|
+
const documentRef = options.documentRef !== undefined ? options.documentRef : inject(DOCUMENT, { optional: true });
|
|
35
|
+
const storage = documentLocalStorage(documentRef);
|
|
36
|
+
const state = signal(options.parse(storage.read(options.key)), /* @ts-ignore */
|
|
37
|
+
...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
38
|
+
effect(() => {
|
|
39
|
+
const serialized = options.serialize(state());
|
|
40
|
+
if (serialized === null) {
|
|
41
|
+
storage.remove(options.key);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
storage.write(options.key, serialized);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return state;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Mirror a reactive string onto one or more `<html>` attributes. A `null` value
|
|
51
|
+
* removes them.
|
|
52
|
+
*/
|
|
53
|
+
function htmlAttributeEffect(options) {
|
|
54
|
+
const documentRef = options.documentRef !== undefined ? options.documentRef : inject(DOCUMENT, { optional: true });
|
|
55
|
+
const attributes = typeof options.attribute === 'string' ? [options.attribute] : options.attribute;
|
|
56
|
+
effect(() => {
|
|
57
|
+
const value = options.source();
|
|
58
|
+
const element = documentRef?.documentElement;
|
|
59
|
+
if (!element)
|
|
60
|
+
return;
|
|
61
|
+
for (const attribute of attributes) {
|
|
62
|
+
if (value === null) {
|
|
63
|
+
element.removeAttribute(attribute);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
element.setAttribute(attribute, value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Mirror reactive inline custom properties onto `<html>`. A `null` value removes one. */
|
|
72
|
+
function htmlStylePropertyEffect(options) {
|
|
73
|
+
const documentRef = options.documentRef !== undefined ? options.documentRef : inject(DOCUMENT, { optional: true });
|
|
74
|
+
effect(() => {
|
|
75
|
+
const properties = options.source();
|
|
76
|
+
const element = documentRef?.documentElement;
|
|
77
|
+
if (!element)
|
|
78
|
+
return;
|
|
79
|
+
for (const [property, value] of Object.entries(properties)) {
|
|
80
|
+
if (value === null) {
|
|
81
|
+
element.style.removeProperty(property);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
element.style.setProperty(property, value);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A persisted preference that also shows up as an attribute on `<html>`.
|
|
91
|
+
*
|
|
92
|
+
* The stored value wins over the configured default, which is the rule every
|
|
93
|
+
* axis already followed.
|
|
94
|
+
*/
|
|
95
|
+
function persistedHtmlAttributeSignal(options) {
|
|
96
|
+
const documentRef = options.documentRef !== undefined ? options.documentRef : inject(DOCUMENT, { optional: true });
|
|
97
|
+
const state = persistedSignal({
|
|
98
|
+
key: options.key,
|
|
99
|
+
documentRef,
|
|
100
|
+
// A stored value wins; otherwise fall back to the configured default. Both
|
|
101
|
+
// go through `normalize`, so neither can put a junk value into the signal.
|
|
102
|
+
//
|
|
103
|
+
// An empty stored string counts as ABSENT rather than as a value: no axis
|
|
104
|
+
// has a legal `''`, so a key cleared to it (a storage the browser or another
|
|
105
|
+
// tab emptied, a hand-edited entry) is a key with nothing in it. Reading it
|
|
106
|
+
// as present would send it through `normalize` to the LIBRARY default and
|
|
107
|
+
// silently discard the default the consumer configured.
|
|
108
|
+
parse: (raw) => {
|
|
109
|
+
const stored = raw !== null && raw !== '' ? raw : null;
|
|
110
|
+
return options.normalize(stored ?? options.normalize(options.initial));
|
|
111
|
+
},
|
|
112
|
+
serialize: (value) => value,
|
|
113
|
+
});
|
|
114
|
+
// A computed, so whatever the resolver reads is tracked alongside the stored
|
|
115
|
+
// value. That is what keeps a `'system'` mode following the operating system
|
|
116
|
+
// while still persisting `'system'` rather than the resolved light or dark.
|
|
117
|
+
const resolveAttributeValue = options.resolveAttributeValue;
|
|
118
|
+
const attributeValue = resolveAttributeValue
|
|
119
|
+
? computed(() => resolveAttributeValue(state()))
|
|
120
|
+
: computed(() => state());
|
|
121
|
+
htmlAttributeEffect({
|
|
122
|
+
attribute: options.attribute,
|
|
123
|
+
source: () => attributeValue(),
|
|
124
|
+
documentRef,
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
value: state.asReadonly(),
|
|
128
|
+
attributeValue,
|
|
129
|
+
set: (next) => state.set(next),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* `(prefers-color-scheme: dark)` as a signal, torn down with the injection
|
|
134
|
+
* context that created it.
|
|
135
|
+
*
|
|
136
|
+
* Lives here because it is the other half of the same story: an axis whose
|
|
137
|
+
* PERSISTED value is `'system'` still has to write a concrete `light` or `dark`
|
|
138
|
+
* to the DOM, and this is what makes that reactive.
|
|
139
|
+
*/
|
|
140
|
+
function prefersDarkSignal(documentRef) {
|
|
141
|
+
const resolved = documentRef !== undefined ? documentRef : inject(DOCUMENT, { optional: true });
|
|
142
|
+
const mediaQueryList = resolved?.defaultView?.matchMedia?.('(prefers-color-scheme: dark)') ?? null;
|
|
143
|
+
const state = signal(mediaQueryList?.matches ?? false, /* @ts-ignore */
|
|
144
|
+
...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
145
|
+
if (mediaQueryList) {
|
|
146
|
+
const onChange = (event) => state.set(event.matches);
|
|
147
|
+
mediaQueryList.addEventListener('change', onChange);
|
|
148
|
+
inject(DestroyRef).onDestroy(() => mediaQueryList.removeEventListener('change', onChange));
|
|
149
|
+
}
|
|
150
|
+
return state.asReadonly();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* WCAG contrast math for tokens decided at runtime.
|
|
155
|
+
*
|
|
156
|
+
* A TypeScript port of scripts/theme-contrast.mjs, which is where the built-in
|
|
157
|
+
* palettes are measured at generation time. The two halves have to agree: a
|
|
158
|
+
* brand a consumer sets through `ThemeBrandService` lands on `--primary` and
|
|
159
|
+
* `--ring` exactly like a generated accent does, so it is held to the same
|
|
160
|
+
* thresholds by the same formula rather than a second approximation of it.
|
|
161
|
+
*
|
|
162
|
+
* Everything works on the bare HSL triplet (`H S% L%`) the CSS consumes through
|
|
163
|
+
* `hsl(var(--token))`, so the numbers asserted are the numbers shipped. Parsing
|
|
164
|
+
* returns `null` instead of throwing, because the caller here is a setter on a
|
|
165
|
+
* running app and not a build step.
|
|
166
|
+
*/
|
|
167
|
+
/** WCAG 1.4.3 minimum for normal-size text. */
|
|
168
|
+
const TEXT_CONTRAST_MIN = 4.5;
|
|
169
|
+
/** WCAG 1.4.11 minimum for non-text contrast (focus rings, borders, icons). */
|
|
170
|
+
const NON_TEXT_CONTRAST_MIN = 3;
|
|
171
|
+
/**
|
|
172
|
+
* Extra headroom used when solving for a lightness, so a value that only just
|
|
173
|
+
* clears the threshold cannot fail an assertion on a rounding difference.
|
|
174
|
+
*/
|
|
175
|
+
const SOLVE_MARGIN = 0.02;
|
|
176
|
+
const NUMBER = String.raw `[+-]?(?:\d+(?:\.\d+)?|\.\d+)`;
|
|
177
|
+
// CSS accepts the components separated by commas or by whitespace, and the hue
|
|
178
|
+
// as any <angle>. Every one of those forms paints, so every one of them has to
|
|
179
|
+
// be measurable — a syntax this rejected used to walk straight past the brand
|
|
180
|
+
// contrast guarantee.
|
|
181
|
+
const SEPARATOR = String.raw `(?:\s*,\s*|\s+)`;
|
|
182
|
+
const HSL_TRIPLET = new RegExp(`^\\s*(${NUMBER})(deg|grad|rad|turn)?${SEPARATOR}(${NUMBER})%${SEPARATOR}(${NUMBER})%\\s*$`, 'i');
|
|
183
|
+
const HEX_COLOR = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
184
|
+
/** `<angle>` units CSS allows for a hue, as a multiplier onto degrees. */
|
|
185
|
+
const DEGREES_PER_UNIT = {
|
|
186
|
+
deg: 1,
|
|
187
|
+
grad: 0.9,
|
|
188
|
+
rad: 180 / Math.PI,
|
|
189
|
+
turn: 360,
|
|
190
|
+
};
|
|
191
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
192
|
+
/**
|
|
193
|
+
* Wrap a hue into 0..360 the way CSS does, so `-240` and `120` are the same
|
|
194
|
+
* colour here as well and cannot measure differently.
|
|
195
|
+
*/
|
|
196
|
+
const normalizeHue = (hue) => ((hue % 360) + 360) % 360;
|
|
197
|
+
/** Read an HSL triplet string (`'221 83% 48%'`). `null` when it is not one. */
|
|
198
|
+
function parseHslTriplet(value) {
|
|
199
|
+
const match = HSL_TRIPLET.exec(value);
|
|
200
|
+
if (!match) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const [h, s, l] = [Number(match[1]), Number(match[3]), Number(match[4])];
|
|
204
|
+
if (![h, s, l].every(Number.isFinite)) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
return { h: normalizeHue(h * DEGREES_PER_UNIT[match[2]?.toLowerCase() ?? 'deg']), s, l };
|
|
208
|
+
}
|
|
209
|
+
/** Format numbers back into the triplet form the CSS consumes. */
|
|
210
|
+
function formatHslTriplet({ h, s, l }) {
|
|
211
|
+
return `${h} ${clamp(Math.round(s), 0, 100)}% ${clamp(Math.round(l), 0, 100)}%`;
|
|
212
|
+
}
|
|
213
|
+
/** `#rgb` / `#rrggbb` as an HSL triplet. `null` when the input is not a hex color. */
|
|
214
|
+
function hexToHslTriplet(value) {
|
|
215
|
+
const match = HEX_COLOR.exec(value.trim());
|
|
216
|
+
if (!match) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const digits = match[1];
|
|
220
|
+
const expanded = digits.length === 3
|
|
221
|
+
? digits
|
|
222
|
+
.split('')
|
|
223
|
+
.map((digit) => digit + digit)
|
|
224
|
+
.join('')
|
|
225
|
+
: digits;
|
|
226
|
+
const [red, green, blue] = [0, 2, 4].map((offset) => Number.parseInt(expanded.slice(offset, offset + 2), 16) / 255);
|
|
227
|
+
const largest = Math.max(red, green, blue);
|
|
228
|
+
const smallest = Math.min(red, green, blue);
|
|
229
|
+
const delta = largest - smallest;
|
|
230
|
+
const lightness = (largest + smallest) / 2;
|
|
231
|
+
let hue = 0;
|
|
232
|
+
if (delta !== 0) {
|
|
233
|
+
if (largest === red)
|
|
234
|
+
hue = 60 * (((green - blue) / delta + 6) % 6);
|
|
235
|
+
else if (largest === green)
|
|
236
|
+
hue = 60 * ((blue - red) / delta + 2);
|
|
237
|
+
else
|
|
238
|
+
hue = 60 * ((red - green) / delta + 4);
|
|
239
|
+
}
|
|
240
|
+
const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1));
|
|
241
|
+
return formatHslTriplet({ h: Math.round(hue), s: saturation * 100, l: lightness * 100 });
|
|
242
|
+
}
|
|
243
|
+
/** HSL to sRGB, each channel in the 0..1 range (CSS Color 4 algorithm). */
|
|
244
|
+
function hslToRgb({ h, s, l }) {
|
|
245
|
+
const saturation = clamp(s, 0, 100) / 100;
|
|
246
|
+
const lightness = clamp(l, 0, 100) / 100;
|
|
247
|
+
const k = (n) => (n + h / 30) % 12;
|
|
248
|
+
const a = saturation * Math.min(lightness, 1 - lightness);
|
|
249
|
+
const channel = (n) => lightness - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
250
|
+
return [channel(0), channel(8), channel(4)];
|
|
251
|
+
}
|
|
252
|
+
/** sRGB channel to linear light, per the WCAG relative luminance definition. */
|
|
253
|
+
const toLinear = (channel) => channel <= 0.03928 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
|
|
254
|
+
/** WCAG relative luminance of an HSL triplet. */
|
|
255
|
+
function relativeLuminance(triplet) {
|
|
256
|
+
const parsed = parseHslTriplet(triplet);
|
|
257
|
+
if (!parsed) {
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
const [r, g, b] = hslToRgb(parsed);
|
|
261
|
+
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
262
|
+
}
|
|
263
|
+
/** WCAG contrast ratio between two HSL triplets. Order does not matter. */
|
|
264
|
+
function contrastRatio(a, b) {
|
|
265
|
+
const ya = relativeLuminance(a);
|
|
266
|
+
const yb = relativeLuminance(b);
|
|
267
|
+
return (Math.max(ya, yb) + 0.05) / (Math.min(ya, yb) + 0.05);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Walk outward from `from` in 1% lightness steps and return the nearest
|
|
271
|
+
* lightness whose color clears `min` against every entry in `against`. Keeping
|
|
272
|
+
* hue and saturation fixed means the brand's identity survives; only how light
|
|
273
|
+
* it is moves, and it moves as little as the threshold allows.
|
|
274
|
+
*
|
|
275
|
+
* `against` is a list so a value that has to hold over more than one surface can
|
|
276
|
+
* be solved in one pass; the brand ring solves against one `--background` per
|
|
277
|
+
* mode, because it is painted per mode.
|
|
278
|
+
*
|
|
279
|
+
* `null` when no lightness in 0..100 satisfies every background.
|
|
280
|
+
*/
|
|
281
|
+
function solveLightnessNearest(options) {
|
|
282
|
+
const target = options.min + (options.margin ?? SOLVE_MARGIN);
|
|
283
|
+
const start = Math.round(clamp(options.from, 0, 100));
|
|
284
|
+
for (let delta = 0; delta <= 100; delta += 1) {
|
|
285
|
+
for (const candidate of delta === 0 ? [start] : [start - delta, start + delta]) {
|
|
286
|
+
if (candidate < 0 || candidate > 100)
|
|
287
|
+
continue;
|
|
288
|
+
const triplet = formatHslTriplet({ h: options.h, s: options.s, l: candidate });
|
|
289
|
+
if (options.against.every((background) => contrastRatio(triplet, background) >= target)) {
|
|
290
|
+
return triplet;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Keep a designed foreground when it already clears the threshold, otherwise
|
|
298
|
+
* nudge its lightness to the nearest value that does. Anything unparsable is
|
|
299
|
+
* returned untouched — there is nothing to measure.
|
|
300
|
+
*/
|
|
301
|
+
function fitForeground(options) {
|
|
302
|
+
const min = options.min ?? TEXT_CONTRAST_MIN;
|
|
303
|
+
if (contrastRatio(options.foreground, options.background) >= min) {
|
|
304
|
+
return options.foreground;
|
|
305
|
+
}
|
|
306
|
+
const parsed = parseHslTriplet(options.foreground);
|
|
307
|
+
if (!parsed) {
|
|
308
|
+
return options.foreground;
|
|
309
|
+
}
|
|
310
|
+
return (solveLightnessNearest({
|
|
311
|
+
h: parsed.h,
|
|
312
|
+
s: parsed.s,
|
|
313
|
+
against: [options.background],
|
|
314
|
+
min,
|
|
315
|
+
from: parsed.l,
|
|
316
|
+
}) ?? options.foreground);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const THEME_OPTIONS = new InjectionToken('THEME_OPTIONS');
|
|
320
|
+
|
|
321
|
+
const THEME_MODES = ['light', 'dark', 'system'];
|
|
322
|
+
const RESOLVED_THEME_MODES = ['light', 'dark'];
|
|
323
|
+
/**
|
|
324
|
+
* FluxUI-style accent palettes (axis `theme-color`). `base` = core theme (no
|
|
325
|
+
* override); `brand` = the consumer's `--brand` color. Others re-tint the full
|
|
326
|
+
* palette. Order mirrors the FluxUI accent picker (base…rose) + `brand`.
|
|
327
|
+
*/
|
|
328
|
+
const THEME_COLORS = [
|
|
329
|
+
'base',
|
|
330
|
+
'red',
|
|
331
|
+
'orange',
|
|
332
|
+
'amber',
|
|
333
|
+
'yellow',
|
|
334
|
+
'lime',
|
|
335
|
+
'green',
|
|
336
|
+
'emerald',
|
|
337
|
+
'teal',
|
|
338
|
+
'cyan',
|
|
339
|
+
'sky',
|
|
340
|
+
'blue',
|
|
341
|
+
'indigo',
|
|
342
|
+
'violet',
|
|
343
|
+
'purple',
|
|
344
|
+
'fuchsia',
|
|
345
|
+
'pink',
|
|
346
|
+
'rose',
|
|
347
|
+
'brand',
|
|
348
|
+
];
|
|
349
|
+
/**
|
|
350
|
+
* Neutral families (axis `theme-neutral`). `base` = core theme neutrals and lets
|
|
351
|
+
* Layout patterns follow the accent. Non-base values override both the gray
|
|
352
|
+
* family and Layout's decorative pattern hue, while composing with any accent.
|
|
353
|
+
*/
|
|
354
|
+
const THEME_NEUTRALS = [
|
|
355
|
+
'base',
|
|
356
|
+
'slate',
|
|
357
|
+
'gray',
|
|
358
|
+
'zinc',
|
|
359
|
+
'neutral',
|
|
360
|
+
'stone',
|
|
361
|
+
'mauve',
|
|
362
|
+
'olive',
|
|
363
|
+
'mist',
|
|
364
|
+
'taupe',
|
|
365
|
+
];
|
|
366
|
+
/**
|
|
367
|
+
* Corner radius presets (axis `theme-radius`). Each drives the single
|
|
368
|
+
* `--radius-base` knob; the whole `--radius-*` scale + `rounded-*` utilities
|
|
369
|
+
* follow. Every preset takes Tailwind's stock value for that name — `none` 0,
|
|
370
|
+
* `xs` 0.125rem, `sm` 0.25rem, `md` 0.375rem (the default), `xl` 0.75rem — and
|
|
371
|
+
* `full` is the pill extreme.
|
|
372
|
+
*/
|
|
373
|
+
const THEME_RADII = ['none', 'xs', 'sm', 'md', 'xl', 'full'];
|
|
374
|
+
/**
|
|
375
|
+
* Spacing density presets (axis `theme-space`). Each drives the single
|
|
376
|
+
* `--spacing-base` knob; every `p-*`/`m-*`/`gap-*`/`w-*`/`h-*` utility follows.
|
|
377
|
+
* `normal` = the 0.25rem default (no-op baseline).
|
|
378
|
+
*/
|
|
379
|
+
const THEME_SPACES = ['compact', 'normal'];
|
|
380
|
+
const DEFAULT_THEME_MODE = 'system';
|
|
381
|
+
const DEFAULT_THEME_COLOR = 'base';
|
|
382
|
+
const DEFAULT_THEME_NEUTRAL = 'base';
|
|
383
|
+
const DEFAULT_THEME_RADIUS = 'md';
|
|
384
|
+
const DEFAULT_THEME_SPACE = 'normal';
|
|
385
|
+
const THEME_MODE_STORAGE_KEY = 'theme-mode';
|
|
386
|
+
const THEME_COLOR_STORAGE_KEY = 'theme-color';
|
|
387
|
+
const THEME_NEUTRAL_STORAGE_KEY = 'theme-neutral';
|
|
388
|
+
const THEME_BRAND_STORAGE_KEY = 'theme-brand';
|
|
389
|
+
const THEME_RADIUS_STORAGE_KEY = 'theme-radius';
|
|
390
|
+
const THEME_SPACE_STORAGE_KEY = 'theme-space';
|
|
391
|
+
function isThemeMode(value) {
|
|
392
|
+
return typeof value === 'string' && THEME_MODES.includes(value);
|
|
393
|
+
}
|
|
394
|
+
function isResolvedThemeMode(value) {
|
|
395
|
+
return typeof value === 'string' && RESOLVED_THEME_MODES.includes(value);
|
|
396
|
+
}
|
|
397
|
+
function isThemeColor(value) {
|
|
398
|
+
return typeof value === 'string' && THEME_COLORS.includes(value);
|
|
399
|
+
}
|
|
400
|
+
function isThemeNeutral(value) {
|
|
401
|
+
return typeof value === 'string' && THEME_NEUTRALS.includes(value);
|
|
402
|
+
}
|
|
403
|
+
function isThemeRadius(value) {
|
|
404
|
+
return typeof value === 'string' && THEME_RADII.includes(value);
|
|
405
|
+
}
|
|
406
|
+
function isThemeSpace(value) {
|
|
407
|
+
return typeof value === 'string' && THEME_SPACES.includes(value);
|
|
408
|
+
}
|
|
409
|
+
function normalizeThemeMode(value) {
|
|
410
|
+
return isThemeMode(value) ? value : DEFAULT_THEME_MODE;
|
|
411
|
+
}
|
|
412
|
+
function normalizeThemeColor(value) {
|
|
413
|
+
return isThemeColor(value) ? value : DEFAULT_THEME_COLOR;
|
|
414
|
+
}
|
|
415
|
+
function normalizeThemeNeutral(value) {
|
|
416
|
+
return isThemeNeutral(value) ? value : DEFAULT_THEME_NEUTRAL;
|
|
417
|
+
}
|
|
418
|
+
function normalizeThemeRadius(value) {
|
|
419
|
+
return isThemeRadius(value) ? value : DEFAULT_THEME_RADIUS;
|
|
420
|
+
}
|
|
421
|
+
function normalizeThemeSpace(value) {
|
|
422
|
+
return isThemeSpace(value) ? value : DEFAULT_THEME_SPACE;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const DEFAULT_BRAND_FOREGROUND = '0 0% 100%';
|
|
426
|
+
/**
|
|
427
|
+
* `--background` per mode, from css/base/tokens.css.
|
|
428
|
+
*
|
|
429
|
+
* The brand fill is one inline custom property that both modes read, but the
|
|
430
|
+
* focus ring is not: `--brand-ring-light` and `--brand-ring-dark` are solved
|
|
431
|
+
* separately and css/color/brand.css picks the one the current mode needs, so
|
|
432
|
+
* the ring clears the non-text threshold without dragging the fill with it.
|
|
433
|
+
*/
|
|
434
|
+
const RING_BACKGROUNDS = { light: '0 0% 100%', dark: '0 0% 5%' };
|
|
435
|
+
/**
|
|
436
|
+
* The brand as the canonical HSL triplet the CSS consumes: hex is converted, and
|
|
437
|
+
* every HSL form CSS accepts (comma separated, an angle-suffixed hue) is
|
|
438
|
+
* normalised, so none of them can paint while skipping the measurement.
|
|
439
|
+
*
|
|
440
|
+
* `null` for anything else — a `var()` reference is a documented escape hatch,
|
|
441
|
+
* and there is nothing in it to measure or convert.
|
|
442
|
+
*/
|
|
443
|
+
function toBrandTriplet(color) {
|
|
444
|
+
const trimmed = color.trim();
|
|
445
|
+
const parsed = parseHslTriplet(trimmed);
|
|
446
|
+
return parsed ? formatHslTriplet(parsed) : hexToHslTriplet(trimmed);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Hold a brand to the text half of the standard the generator holds every
|
|
450
|
+
* built-in palette to: text on the brand fill at least 4.5 to 1. Only the
|
|
451
|
+
* FOREGROUND moves, so the fill the consumer chose is the fill that ships —
|
|
452
|
+
* exactly as the generator keeps `--primary` at its design lightness and
|
|
453
|
+
* darkens a separate `--ring` instead.
|
|
454
|
+
*
|
|
455
|
+
* The non-text half is the ring's job; see {@link brandRing}.
|
|
456
|
+
*/
|
|
457
|
+
function hardenBrand(brand) {
|
|
458
|
+
const color = toBrandTriplet(brand.color);
|
|
459
|
+
if (!color) {
|
|
460
|
+
return brand;
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
color,
|
|
464
|
+
foreground: fitForeground({
|
|
465
|
+
foreground: brand.foreground,
|
|
466
|
+
background: color,
|
|
467
|
+
min: TEXT_CONTRAST_MIN,
|
|
468
|
+
}),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* The focus ring for one mode: the brand itself when it already clears the
|
|
473
|
+
* non-text threshold against that mode's `--background`, otherwise the nearest
|
|
474
|
+
* lightness that does. Hue and saturation never move, so the ring still reads
|
|
475
|
+
* as the brand.
|
|
476
|
+
*/
|
|
477
|
+
function brandRing(color, background) {
|
|
478
|
+
const parsed = parseHslTriplet(color);
|
|
479
|
+
if (!parsed) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
if (contrastRatio(color, background) >= NON_TEXT_CONTRAST_MIN) {
|
|
483
|
+
return color;
|
|
484
|
+
}
|
|
485
|
+
return (solveLightnessNearest({
|
|
486
|
+
h: parsed.h,
|
|
487
|
+
s: parsed.s,
|
|
488
|
+
against: [background],
|
|
489
|
+
min: NON_TEXT_CONTRAST_MIN,
|
|
490
|
+
from: parsed.l,
|
|
491
|
+
}) ?? color);
|
|
492
|
+
}
|
|
493
|
+
/** Extract the hue/saturation pair used by Layout's soft pattern tiers. */
|
|
494
|
+
function brandPatternHueSaturation(color) {
|
|
495
|
+
const parsed = parseHslTriplet(color);
|
|
496
|
+
return parsed ? `${parsed.h} ${parsed.s}%` : null;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Consumer-configurable brand color. Writes `--brand`, `--brand-foreground`, the
|
|
500
|
+
* derived `--brand-pattern-hs` hue/saturation source, and a `--brand-ring-light`
|
|
501
|
+
* / `--brand-ring-dark` pair as inline custom properties on `<html>` so solid
|
|
502
|
+
* brand utilities, the focus ring, and the soft Layout pattern tiers can resolve
|
|
503
|
+
* without CSS relative-color syntax.
|
|
504
|
+
*
|
|
505
|
+
* Default comes from `provideUiTheme({ brand })`; `setBrand()` overrides at
|
|
506
|
+
* runtime and persists. When unset, `--brand` falls back to `var(--primary)`.
|
|
507
|
+
*/
|
|
508
|
+
class ThemeBrandService {
|
|
509
|
+
options = inject(THEME_OPTIONS, { optional: true });
|
|
510
|
+
/**
|
|
511
|
+
* Storage half shared with the other axes; the DOM half is custom, because the
|
|
512
|
+
* brand paints a set of inline custom properties rather than setting an
|
|
513
|
+
* attribute. `serialize` returning `null` is what clears the stored key.
|
|
514
|
+
*/
|
|
515
|
+
brandState = persistedSignal({
|
|
516
|
+
key: THEME_BRAND_STORAGE_KEY,
|
|
517
|
+
parse: (raw) => this.parseStored(raw) ?? this.normalizeOption(this.options?.brand),
|
|
518
|
+
serialize: (brand) => (brand ? JSON.stringify(brand) : null),
|
|
519
|
+
});
|
|
520
|
+
brand = this.brandState.asReadonly();
|
|
521
|
+
constructor() {
|
|
522
|
+
htmlStylePropertyEffect({ source: () => this.brandProperties(this.brandState()) });
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Set the brand color. `color`/`foreground` are HSL triplets (e.g.
|
|
526
|
+
* `'221 83% 53%'`); a hex color is converted, since that is what a native
|
|
527
|
+
* picker hands back. The fill is kept exactly as chosen — `--brand-foreground`
|
|
528
|
+
* is moved until text on it clears 4.5 to 1, and the focus ring is solved
|
|
529
|
+
* separately per mode, so a pale brand can never leave the ring invisible.
|
|
530
|
+
*/
|
|
531
|
+
setBrand(color, foreground = DEFAULT_BRAND_FOREGROUND) {
|
|
532
|
+
this.brandState.set(hardenBrand({ color, foreground }));
|
|
533
|
+
}
|
|
534
|
+
/** Clear the consumer brand → `--brand` falls back to `var(--primary)`. */
|
|
535
|
+
clearBrand() {
|
|
536
|
+
this.brandState.set(null);
|
|
537
|
+
}
|
|
538
|
+
normalizeOption(brand) {
|
|
539
|
+
if (!brand) {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
if (typeof brand === 'string') {
|
|
543
|
+
return hardenBrand({ color: brand, foreground: DEFAULT_BRAND_FOREGROUND });
|
|
544
|
+
}
|
|
545
|
+
return hardenBrand({
|
|
546
|
+
color: brand.color,
|
|
547
|
+
foreground: brand.foreground ?? DEFAULT_BRAND_FOREGROUND,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
/** The custom properties the brand paints. `null` removes one. */
|
|
551
|
+
brandProperties(brand) {
|
|
552
|
+
if (!brand) {
|
|
553
|
+
return {
|
|
554
|
+
'--brand': null,
|
|
555
|
+
'--brand-foreground': null,
|
|
556
|
+
'--brand-pattern-hs': null,
|
|
557
|
+
'--brand-ring-light': null,
|
|
558
|
+
'--brand-ring-dark': null,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
'--brand': brand.color,
|
|
563
|
+
'--brand-foreground': brand.foreground,
|
|
564
|
+
'--brand-pattern-hs': brandPatternHueSaturation(brand.color),
|
|
565
|
+
'--brand-ring-light': brandRing(brand.color, RING_BACKGROUNDS.light),
|
|
566
|
+
'--brand-ring-dark': brandRing(brand.color, RING_BACKGROUNDS.dark),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
/** Stored brands are JSON. Anything unparsable reads as "no stored brand". */
|
|
570
|
+
parseStored(raw) {
|
|
571
|
+
if (!raw) {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
try {
|
|
575
|
+
const parsed = JSON.parse(raw);
|
|
576
|
+
if (typeof parsed?.color !== 'string') {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
// Storage is editable by hand and predates the guarantee, so a restored
|
|
580
|
+
// brand is measured exactly like a freshly set one.
|
|
581
|
+
return hardenBrand({
|
|
582
|
+
color: parsed.color,
|
|
583
|
+
foreground: typeof parsed.foreground === 'string' ? parsed.foreground : DEFAULT_BRAND_FOREGROUND,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeBrandService, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
591
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.4", ngImport: i0, type: ThemeBrandService });
|
|
592
|
+
}
|
|
593
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeBrandService, decorators: [{
|
|
594
|
+
type: Service
|
|
595
|
+
}], ctorParameters: () => [] });
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Drives the two FluxUI-style color axes:
|
|
599
|
+
* - accent → `<html theme-color>` (full tinted palette per color)
|
|
600
|
+
* - neutral → `<html theme-neutral>` (gray family + Layout pattern hue)
|
|
601
|
+
*
|
|
602
|
+
* Both persist to localStorage and seed from `provideUiTheme({ color, neutral })`
|
|
603
|
+
* (a persisted choice wins over the configured default). `base` on either axis
|
|
604
|
+
* means "no override" — the core base theme applies.
|
|
605
|
+
*/
|
|
606
|
+
class ThemeColorService {
|
|
607
|
+
options = inject(THEME_OPTIONS, { optional: true });
|
|
608
|
+
colorAxis = persistedHtmlAttributeSignal({
|
|
609
|
+
key: THEME_COLOR_STORAGE_KEY,
|
|
610
|
+
attribute: 'theme-color',
|
|
611
|
+
normalize: normalizeThemeColor,
|
|
612
|
+
// A configured brand with no explicit accent defaults the accent to 'brand'
|
|
613
|
+
// so the consumer's brand color becomes the primary (FluxUI-style). The
|
|
614
|
+
// helper knows nothing about brand, so this stays here.
|
|
615
|
+
initial: this.options?.color ?? (this.options?.brand ? 'brand' : undefined),
|
|
616
|
+
});
|
|
617
|
+
neutralAxis = persistedHtmlAttributeSignal({
|
|
618
|
+
key: THEME_NEUTRAL_STORAGE_KEY,
|
|
619
|
+
attribute: 'theme-neutral',
|
|
620
|
+
normalize: normalizeThemeNeutral,
|
|
621
|
+
initial: this.options?.neutral,
|
|
622
|
+
});
|
|
623
|
+
color = this.colorAxis.value;
|
|
624
|
+
neutral = this.neutralAxis.value;
|
|
625
|
+
setColor(color) {
|
|
626
|
+
this.colorAxis.set(color);
|
|
627
|
+
}
|
|
628
|
+
setNeutral(neutral) {
|
|
629
|
+
this.neutralAxis.set(neutral);
|
|
630
|
+
}
|
|
631
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeColorService, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
632
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.4", ngImport: i0, type: ThemeColorService });
|
|
633
|
+
}
|
|
634
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeColorService, decorators: [{
|
|
635
|
+
type: Service
|
|
636
|
+
}] });
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Color mode axis. Writes both `<html theme-mode>` and `<html data-mode>`.
|
|
640
|
+
*
|
|
641
|
+
* The persisted value and the written value differ here, which is the whole
|
|
642
|
+
* point of the axis: `'system'` is what gets stored, while `light` or `dark` is
|
|
643
|
+
* what reaches the DOM, and it keeps following the operating system live.
|
|
644
|
+
*/
|
|
645
|
+
class ThemeModeService {
|
|
646
|
+
options = inject(THEME_OPTIONS, { optional: true });
|
|
647
|
+
systemPrefersDark = prefersDarkSignal();
|
|
648
|
+
axis = persistedHtmlAttributeSignal({
|
|
649
|
+
key: THEME_MODE_STORAGE_KEY,
|
|
650
|
+
// One value, two attributes: `theme-mode` for the theme CSS and `data-mode`
|
|
651
|
+
// for the Tailwind dark variant.
|
|
652
|
+
attribute: ['theme-mode', 'data-mode'],
|
|
653
|
+
normalize: normalizeThemeMode,
|
|
654
|
+
initial: this.options?.mode,
|
|
655
|
+
// Read inside the effect, so a system preference change repaints without
|
|
656
|
+
// touching what is persisted.
|
|
657
|
+
resolveAttributeValue: (mode) => mode === 'system' ? (this.systemPrefersDark() ? 'dark' : 'light') : mode,
|
|
658
|
+
});
|
|
659
|
+
mode = this.axis.value;
|
|
660
|
+
resolvedMode = computed(() => this.axis.attributeValue(), /* @ts-ignore */
|
|
661
|
+
...(ngDevMode ? [{ debugName: "resolvedMode" }] : /* istanbul ignore next */ []));
|
|
662
|
+
isDark = computed(() => this.resolvedMode() === 'dark', /* @ts-ignore */
|
|
663
|
+
...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
|
|
664
|
+
setMode(mode) {
|
|
665
|
+
this.axis.set(mode);
|
|
666
|
+
}
|
|
667
|
+
toggle() {
|
|
668
|
+
this.setMode(this.isDark() ? 'light' : 'dark');
|
|
669
|
+
}
|
|
670
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeModeService, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
671
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.4", ngImport: i0, type: ThemeModeService });
|
|
672
|
+
}
|
|
673
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeModeService, decorators: [{
|
|
674
|
+
type: Service
|
|
675
|
+
}] });
|
|
676
|
+
|
|
677
|
+
/** Radius in px @ 16px root; `full` uses a finite SVG-friendly sentinel. */
|
|
678
|
+
const RADIUS_PX = {
|
|
679
|
+
none: 0,
|
|
680
|
+
xs: 2,
|
|
681
|
+
sm: 4,
|
|
682
|
+
md: 6,
|
|
683
|
+
xl: 12,
|
|
684
|
+
full: 9999,
|
|
685
|
+
};
|
|
686
|
+
/**
|
|
687
|
+
* Corner radius axis. Writes `<html theme-radius>`; each preset CSS sets the
|
|
688
|
+
* single `--radius-base` knob so the whole `--radius-*` scale + `rounded-*`
|
|
689
|
+
* utilities rescale. Persists to localStorage and seeds from
|
|
690
|
+
* `provideUiTheme({ radius })` (a persisted choice wins over the default).
|
|
691
|
+
*/
|
|
692
|
+
class ThemeRadiusService {
|
|
693
|
+
options = inject(THEME_OPTIONS, { optional: true });
|
|
694
|
+
axis = persistedHtmlAttributeSignal({
|
|
695
|
+
key: THEME_RADIUS_STORAGE_KEY,
|
|
696
|
+
attribute: 'theme-radius',
|
|
697
|
+
normalize: normalizeThemeRadius,
|
|
698
|
+
initial: this.options?.radius,
|
|
699
|
+
});
|
|
700
|
+
radius = this.axis.value;
|
|
701
|
+
/** Active preset in px @ 16px root. */
|
|
702
|
+
radiusPx = computed(() => RADIUS_PX[this.radius()] ?? RADIUS_PX.md, /* @ts-ignore */
|
|
703
|
+
...(ngDevMode ? [{ debugName: "radiusPx" }] : /* istanbul ignore next */ []));
|
|
704
|
+
setRadius(radius) {
|
|
705
|
+
this.axis.set(radius);
|
|
706
|
+
}
|
|
707
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeRadiusService, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
708
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.4", ngImport: i0, type: ThemeRadiusService });
|
|
709
|
+
}
|
|
710
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeRadiusService, decorators: [{
|
|
711
|
+
type: Service
|
|
712
|
+
}] });
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Spacing density axis. Writes `<html theme-space>`; each preset CSS sets the
|
|
716
|
+
* single `--spacing-base` knob, which Tailwind's `--spacing` derives from, so
|
|
717
|
+
* every `p-*`/`m-*`/`gap-*`/`w-*`/`h-*` utility rescales. Persists to
|
|
718
|
+
* localStorage and seeds from `provideUiTheme({ space })` (a persisted choice
|
|
719
|
+
* wins over the default).
|
|
720
|
+
*/
|
|
721
|
+
class ThemeSpaceService {
|
|
722
|
+
options = inject(THEME_OPTIONS, { optional: true });
|
|
723
|
+
axis = persistedHtmlAttributeSignal({
|
|
724
|
+
key: THEME_SPACE_STORAGE_KEY,
|
|
725
|
+
attribute: 'theme-space',
|
|
726
|
+
normalize: normalizeThemeSpace,
|
|
727
|
+
initial: this.options?.space,
|
|
728
|
+
});
|
|
729
|
+
space = this.axis.value;
|
|
730
|
+
setSpace(space) {
|
|
731
|
+
this.axis.set(space);
|
|
732
|
+
}
|
|
733
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeSpaceService, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
734
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.4", ngImport: i0, type: ThemeSpaceService });
|
|
735
|
+
}
|
|
736
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeSpaceService, decorators: [{
|
|
737
|
+
type: Service
|
|
738
|
+
}] });
|
|
739
|
+
|
|
740
|
+
const MATERIAL_SYMBOLS_FONT_ATTR = 'data-icon-font';
|
|
741
|
+
const MATERIAL_SYMBOLS_FONT_ID = 'material-symbols';
|
|
742
|
+
const MATERIAL_SYMBOLS_FONT_HREF = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&family=Material+Symbols+Sharp:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=block';
|
|
743
|
+
function provideUiTheme(options = {}) {
|
|
744
|
+
return makeEnvironmentProviders([
|
|
745
|
+
{
|
|
746
|
+
provide: THEME_OPTIONS,
|
|
747
|
+
useValue: options,
|
|
748
|
+
},
|
|
749
|
+
provideEnvironmentInitializer(() => {
|
|
750
|
+
// Opt-in: the Material Symbols font is only fetched from Google Fonts when
|
|
751
|
+
// explicitly enabled, so the default install makes no external network
|
|
752
|
+
// request (privacy / offline / CSP friendly). Set `icons.materialSymbols: true`
|
|
753
|
+
// or self-host the font and import it yourself.
|
|
754
|
+
if (options.icons?.materialSymbols === true) {
|
|
755
|
+
loadMaterialSymbolsStylesheet();
|
|
756
|
+
}
|
|
757
|
+
inject(ThemeModeService);
|
|
758
|
+
inject(ThemeColorService);
|
|
759
|
+
inject(ThemeBrandService);
|
|
760
|
+
inject(ThemeRadiusService);
|
|
761
|
+
inject(ThemeSpaceService);
|
|
762
|
+
}),
|
|
763
|
+
]);
|
|
764
|
+
}
|
|
765
|
+
function loadMaterialSymbolsStylesheet() {
|
|
766
|
+
const documentRef = inject(DOCUMENT, { optional: true });
|
|
767
|
+
const platformId = inject(PLATFORM_ID);
|
|
768
|
+
const head = documentRef?.head;
|
|
769
|
+
if (!isPlatformBrowser(platformId) || !head) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const existing = head.querySelector(`link[${MATERIAL_SYMBOLS_FONT_ATTR}="${MATERIAL_SYMBOLS_FONT_ID}"]`);
|
|
773
|
+
if (existing) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
const link = head.ownerDocument.createElement('link');
|
|
777
|
+
link.rel = 'stylesheet';
|
|
778
|
+
link.href = MATERIAL_SYMBOLS_FONT_HREF;
|
|
779
|
+
link.setAttribute(MATERIAL_SYMBOLS_FONT_ATTR, MATERIAL_SYMBOLS_FONT_ID);
|
|
780
|
+
head.appendChild(link);
|
|
781
|
+
}
|
|
4
782
|
|
|
5
783
|
/**
|
|
6
784
|
* Generated bundle index. Do not edit.
|
|
7
|
-
*/
|
|
785
|
+
*/
|
|
786
|
+
|
|
787
|
+
export { DEFAULT_THEME_COLOR, DEFAULT_THEME_MODE, DEFAULT_THEME_NEUTRAL, DEFAULT_THEME_RADIUS, DEFAULT_THEME_SPACE, RESOLVED_THEME_MODES, THEME_BRAND_STORAGE_KEY, THEME_COLORS, THEME_COLOR_STORAGE_KEY, THEME_MODES, THEME_MODE_STORAGE_KEY, THEME_NEUTRALS, THEME_NEUTRAL_STORAGE_KEY, THEME_OPTIONS, THEME_RADII, THEME_RADIUS_STORAGE_KEY, THEME_SPACES, THEME_SPACE_STORAGE_KEY, ThemeBrandService, ThemeColorService, ThemeModeService, ThemeRadiusService, ThemeSpaceService, htmlAttributeEffect, htmlStylePropertyEffect, isResolvedThemeMode, isThemeColor, isThemeMode, isThemeNeutral, isThemeRadius, isThemeSpace, normalizeThemeColor, normalizeThemeMode, normalizeThemeNeutral, normalizeThemeRadius, normalizeThemeSpace, persistedHtmlAttributeSignal, persistedSignal, prefersDarkSignal, provideUiTheme };
|