@ojiepermana/angular 22.1.10 → 22.1.12
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 +373 -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,1391 @@
|
|
|
1
|
-
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, PLATFORM_ID, signal, effect, Service, computed, Component, model, input } from '@angular/core';
|
|
3
|
+
import { DrawerCloseDirective, DrawerComponent, DrawerContentComponent, DrawerHeaderComponent, DrawerTitleComponent } from '@ojiepermana/angular/component/drawer';
|
|
4
|
+
import { IconComponent } from '@ojiepermana/angular/component/icon';
|
|
5
|
+
import { SegmentedControlComponent } from '@ojiepermana/angular/component/segmented-control';
|
|
6
|
+
import { globalLocalStorage, injectUniqueId, cn } from '@ojiepermana/angular/component/utils/core';
|
|
7
|
+
import { LAYOUT_TYPE_STORAGE_KEY, LAYOUT_LOCKED_WIDTH } from '@ojiepermana/angular/theme/layout/types';
|
|
8
|
+
import { ShellService } from '@ojiepermana/angular/theme/shell/services';
|
|
9
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
10
|
+
import { LayoutService } from '@ojiepermana/angular/theme/layout/services';
|
|
11
|
+
import { resolveShellDeviceForMode, shellDevicesForMode, SHELL_MODES, isShellDeviceForMode, SHELL_FRAMES, SHELL_COLORS } from '@ojiepermana/angular/theme/shell/types';
|
|
12
|
+
import { ThemeModeService, ThemeColorService, ThemeBrandService, ThemeRadiusService, ThemeSpaceService, THEME_COLORS, THEME_NEUTRALS, isThemeMode, isThemeRadius, isThemeSpace } from '@ojiepermana/angular/theme/styles';
|
|
2
13
|
|
|
3
|
-
|
|
14
|
+
/** Provide this from an application's persisted layout-settings facade. */
|
|
15
|
+
const THEME_SETTINGS_ADAPTER = new InjectionToken('THEME_SETTINGS_ADAPTER');
|
|
16
|
+
|
|
17
|
+
/** Storage key for the persisted navigation type. Owned by the package. */
|
|
18
|
+
const NAV_TYPE_STORAGE_KEY = 'nav-type';
|
|
19
|
+
/** Storage key for the persisted navigation presentation mode. Owned by the package. */
|
|
20
|
+
const NAV_TYPE_MODE_STORAGE_KEY = 'nav-type-mode';
|
|
21
|
+
const NAV_TYPES = [
|
|
22
|
+
'sidebar',
|
|
23
|
+
'dockbar',
|
|
24
|
+
'navbar',
|
|
25
|
+
'flyout',
|
|
26
|
+
'desktop',
|
|
27
|
+
];
|
|
28
|
+
const NAV_TYPE_MODES = ['default', 'collapsed', 'drawer'];
|
|
29
|
+
/**
|
|
30
|
+
* Nav types valid for a layout/shell pair — mirrors `LayoutWrapperDefault`
|
|
31
|
+
* (`navbar`/`flyout` → web horizontal, `sidebar`/`dockbar` → vertical).
|
|
32
|
+
* A desktop-mode horizontal shell owns exactly one navigation shape: `desktop`.
|
|
33
|
+
* `empty` and `fluid` shells are brand-only, so no nav type applies.
|
|
34
|
+
*/
|
|
35
|
+
function allowedNavTypes$1(layoutType, shellMode) {
|
|
36
|
+
switch (layoutType) {
|
|
37
|
+
case 'vertical':
|
|
38
|
+
return ['sidebar', 'dockbar'];
|
|
39
|
+
case 'horizontal':
|
|
40
|
+
return shellMode === 'desktop' ? ['desktop'] : ['navbar', 'flyout'];
|
|
41
|
+
default:
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Presentation modes meaningful to a navigation type: sidebar collapses, dockbar
|
|
47
|
+
* turns into a drawer, and every other type has one presentation only.
|
|
48
|
+
*/
|
|
49
|
+
function allowedNavTypeModes(navType) {
|
|
50
|
+
switch (navType) {
|
|
51
|
+
case 'sidebar':
|
|
52
|
+
return ['default', 'collapsed'];
|
|
53
|
+
case 'dockbar':
|
|
54
|
+
return ['default', 'drawer'];
|
|
55
|
+
default:
|
|
56
|
+
return ['default'];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** `fluid` width only has an effect when the layout type is also `fluid`. */
|
|
60
|
+
function isWidthEnabled(layoutType, width) {
|
|
61
|
+
return width !== 'fluid' || layoutType === 'fluid';
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Default `ThemeSettingsAdapter`: the axis coherence rules that make runtime
|
|
65
|
+
* layout and navigation switching safe, published so every consumer shares one
|
|
66
|
+
* tested implementation instead of copying it.
|
|
67
|
+
*
|
|
68
|
+
* - `surface` / `appearance` / `width` delegate to the library `LayoutService`
|
|
69
|
+
* (already reactive and persisted, with no nav-derived override to drift from).
|
|
70
|
+
* - `layoutType` / `navType` / `navTypeMode` are owned here: `<Layout>` writes
|
|
71
|
+
* `type` back to the service (`persist: false`) to follow nav orientation, so
|
|
72
|
+
* reading `layout.type()` would drift from the operator's choice. Nav type is
|
|
73
|
+
* not `LayoutService` territory at all.
|
|
74
|
+
*
|
|
75
|
+
* Incoherent axis pairs auto-correct, so the shell never renders an impossible
|
|
76
|
+
* state. Only presentation choices are stored, never identity or session data,
|
|
77
|
+
* and storage that is unavailable or holds a foreign value falls back to the
|
|
78
|
+
* default without throwing.
|
|
79
|
+
*
|
|
80
|
+
* Install with {@link provideThemeSettingsAdapter}.
|
|
81
|
+
*/
|
|
82
|
+
class ThemeSettingsDefaultAdapter {
|
|
83
|
+
platformId = inject(PLATFORM_ID);
|
|
84
|
+
storage = globalLocalStorage(isPlatformBrowser(this.platformId));
|
|
85
|
+
layout = inject(LayoutService);
|
|
86
|
+
shell = inject(ShellService);
|
|
87
|
+
surface = this.layout.surface;
|
|
88
|
+
appearance = this.layout.appearance;
|
|
89
|
+
width = this.layout.width;
|
|
90
|
+
layoutTypeState = signal(this.layout.getStoredType(), /* @ts-ignore */
|
|
91
|
+
...(ngDevMode ? [{ debugName: "layoutTypeState" }] : /* istanbul ignore next */ []));
|
|
92
|
+
navTypeState = signal(this.readStoredNavType(), /* @ts-ignore */
|
|
93
|
+
...(ngDevMode ? [{ debugName: "navTypeState" }] : /* istanbul ignore next */ []));
|
|
94
|
+
navTypeModeState = signal(this.readStoredNavTypeMode(), /* @ts-ignore */
|
|
95
|
+
...(ngDevMode ? [{ debugName: "navTypeModeState" }] : /* istanbul ignore next */ []));
|
|
96
|
+
layoutType = this.layoutTypeState.asReadonly();
|
|
97
|
+
navType = this.navTypeState.asReadonly();
|
|
98
|
+
navTypeMode = this.navTypeModeState.asReadonly();
|
|
99
|
+
/** Shell mode the nav-type constraints are resolved against. */
|
|
100
|
+
shellMode = this.shell.mode;
|
|
101
|
+
constructor() {
|
|
102
|
+
// Keep external shell-mode changes coherent too: desktop+horizontal owns
|
|
103
|
+
// exactly `desktop`, while that nav type cannot survive another shell mode.
|
|
104
|
+
effect(() => this.enforceConstraints(this.layoutTypeState(), this.shell.mode()));
|
|
105
|
+
}
|
|
106
|
+
setSurface(surface) {
|
|
107
|
+
this.layout.setSurface(surface);
|
|
108
|
+
}
|
|
109
|
+
setAppearance(appearance) {
|
|
110
|
+
this.layout.setAppearance(appearance);
|
|
111
|
+
}
|
|
112
|
+
setWidth(width) {
|
|
113
|
+
this.layout.setWidth(width);
|
|
114
|
+
}
|
|
115
|
+
setLayoutType(type) {
|
|
116
|
+
this.layoutTypeState.set(type);
|
|
117
|
+
this.storage.write(LAYOUT_TYPE_STORAGE_KEY, type);
|
|
118
|
+
this.enforceConstraints(type, this.shell.mode());
|
|
119
|
+
}
|
|
120
|
+
setNavType(type) {
|
|
121
|
+
if (!allowedNavTypes$1(this.layoutTypeState(), this.shell.mode()).includes(type)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
this.navTypeState.set(type);
|
|
125
|
+
this.storage.write(NAV_TYPE_STORAGE_KEY, type);
|
|
126
|
+
this.enforceNavTypeMode();
|
|
127
|
+
}
|
|
128
|
+
setNavTypeMode(mode) {
|
|
129
|
+
if (!allowedNavTypeModes(this.navTypeState()).includes(mode)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.navTypeModeState.set(mode);
|
|
133
|
+
this.storage.write(NAV_TYPE_MODE_STORAGE_KEY, mode);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Auto-correct the lower-priority axes a layout type invalidates, so the
|
|
137
|
+
* rendered shell never lands in an inconsistent state (e.g. `vertical` +
|
|
138
|
+
* `navbar`). Idempotent — only writes when a value is actually out of range.
|
|
139
|
+
*/
|
|
140
|
+
enforceConstraints(type, shellMode) {
|
|
141
|
+
const navs = allowedNavTypes$1(type, shellMode);
|
|
142
|
+
if (navs.length && !navs.includes(this.navTypeState())) {
|
|
143
|
+
this.setNavType(navs[0]);
|
|
144
|
+
}
|
|
145
|
+
this.enforceNavTypeMode();
|
|
146
|
+
if (type === 'fluid' && this.width() !== 'fluid') {
|
|
147
|
+
this.setWidth('fluid');
|
|
148
|
+
}
|
|
149
|
+
else if (type !== 'fluid' && this.width() === 'fluid') {
|
|
150
|
+
this.setWidth('wide');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
enforceNavTypeMode() {
|
|
154
|
+
const available = allowedNavTypeModes(this.navTypeState());
|
|
155
|
+
if (!available.includes(this.navTypeModeState())) {
|
|
156
|
+
this.setNavTypeMode(available[0]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
readStoredNavType() {
|
|
160
|
+
const stored = this.storage.read(NAV_TYPE_STORAGE_KEY);
|
|
161
|
+
return stored && NAV_TYPES.includes(stored)
|
|
162
|
+
? stored
|
|
163
|
+
: 'dockbar';
|
|
164
|
+
}
|
|
165
|
+
readStoredNavTypeMode() {
|
|
166
|
+
const stored = this.storage.read(NAV_TYPE_MODE_STORAGE_KEY);
|
|
167
|
+
return stored && NAV_TYPE_MODES.includes(stored)
|
|
168
|
+
? stored
|
|
169
|
+
: 'default';
|
|
170
|
+
}
|
|
171
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeSettingsDefaultAdapter, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
172
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.4", ngImport: i0, type: ThemeSettingsDefaultAdapter });
|
|
173
|
+
}
|
|
174
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeSettingsDefaultAdapter, decorators: [{
|
|
175
|
+
type: Service
|
|
176
|
+
}], ctorParameters: () => [] });
|
|
177
|
+
/**
|
|
178
|
+
* Install the published {@link ThemeSettingsDefaultAdapter} on
|
|
179
|
+
* `THEME_SETTINGS_ADAPTER`, which turns on the layout and navigation sections of
|
|
180
|
+
* `<ThemeSettings>`:
|
|
181
|
+
*
|
|
182
|
+
* ```ts
|
|
183
|
+
* providers: [provideThemeSettingsAdapter()];
|
|
184
|
+
* ```
|
|
185
|
+
*
|
|
186
|
+
* Additive: consumers that provide their own adapter, or none at all, are
|
|
187
|
+
* unaffected.
|
|
188
|
+
*/
|
|
189
|
+
function provideThemeSettingsAdapter() {
|
|
190
|
+
return [{ provide: THEME_SETTINGS_ADAPTER, useExisting: ThemeSettingsDefaultAdapter }];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The one rule the settings sections share.
|
|
195
|
+
*
|
|
196
|
+
* Two of the four sections change something that can invalidate the navigation
|
|
197
|
+
* type: the layout section (layout type) and the shell section (shell mode). Both
|
|
198
|
+
* have to run the same correction, and before the split they called one private
|
|
199
|
+
* method. Pure functions here, so neither section owns it and neither can drift.
|
|
200
|
+
*/
|
|
201
|
+
/** Nav types the active layout and shell pair allows. Empty without an adapter. */
|
|
202
|
+
function allowedNavTypes(adapter, shellMode) {
|
|
203
|
+
const layoutType = adapter?.layoutType();
|
|
204
|
+
return layoutType ? allowedNavTypes$1(layoutType, shellMode) : [];
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Correct a navigation type the new layout or shell pair no longer allows.
|
|
208
|
+
* Idempotent, and a no-op without an adapter.
|
|
209
|
+
*/
|
|
210
|
+
function normalizeNavType(adapter, shellMode) {
|
|
211
|
+
const current = adapter?.navType();
|
|
212
|
+
const allowed = allowedNavTypes(adapter, shellMode);
|
|
213
|
+
if (current && allowed.length && !allowed.includes(current)) {
|
|
214
|
+
adapter?.setNavType(allowed[0]);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/*
|
|
219
|
+
* Option tables and swatch palettes for `ThemeSettings`.
|
|
220
|
+
*
|
|
221
|
+
* Pure data, extracted from the component so the drawer stays inside the
|
|
222
|
+
* 700-line source-unit budget (scripts/check-library-boundaries.mjs) as axes are
|
|
223
|
+
* added. Each option list is the full domain of one axis; the component decides
|
|
224
|
+
* which entries are `disabled` in the current combination.
|
|
225
|
+
*/
|
|
226
|
+
const DEFAULT_BRAND_COLOR = '262 83% 58%';
|
|
227
|
+
const DEFAULT_BRAND_HSL = [262, 83, 58];
|
|
228
|
+
const MODE_OPTIONS = [
|
|
229
|
+
{ value: 'light', label: 'Light', icon: 'light_mode' },
|
|
230
|
+
{ value: 'dark', label: 'Dark', icon: 'dark_mode' },
|
|
231
|
+
{ value: 'system', label: 'System', icon: 'desktop_windows' },
|
|
232
|
+
];
|
|
233
|
+
/**
|
|
234
|
+
* Shell delivery targets (`@ojiepermana/angular/theme/shell`). Everything but
|
|
235
|
+
* `web` owns its whole surface, which is what locks the layout width to `full`.
|
|
236
|
+
*/
|
|
237
|
+
const SHELL_MODE_OPTIONS = [
|
|
238
|
+
{ value: 'web', label: 'Web', icon: 'public' },
|
|
239
|
+
{ value: 'desktop', label: 'Desktop', icon: 'desktop_windows' },
|
|
240
|
+
{ value: 'tablet', label: 'Tablet', icon: 'tablet' },
|
|
241
|
+
{ value: 'mobile', label: 'Mobile', icon: 'smartphone' },
|
|
242
|
+
];
|
|
243
|
+
/**
|
|
244
|
+
* Where the shell's own chrome takes its colors from. `sync` follows the theme
|
|
245
|
+
* axes above; `default` paints the active device's native titlebar instead.
|
|
246
|
+
*/
|
|
247
|
+
const SHELL_COLOR_OPTIONS = [
|
|
248
|
+
{ value: 'sync', label: 'Sync', icon: 'palette' },
|
|
249
|
+
{ value: 'default', label: 'Native', icon: 'desktop_mac' },
|
|
250
|
+
];
|
|
251
|
+
/**
|
|
252
|
+
* How the app sits in its window. `modern` insets it as a panel and fuses the
|
|
253
|
+
* titlebar with the surround; only a desktop window has a frame to shape.
|
|
254
|
+
*/
|
|
255
|
+
const SHELL_FRAME_OPTIONS = [
|
|
256
|
+
{ value: 'classic', label: 'Classic', icon: 'crop_din' },
|
|
257
|
+
{ value: 'modern', label: 'Modern', icon: 'fullscreen_exit' },
|
|
258
|
+
];
|
|
259
|
+
/**
|
|
260
|
+
* Host devices, keyed so the drawer can offer only the ones the active shell mode
|
|
261
|
+
* can actually run on (`SHELL_MODE_DEVICES`) — with `web` that is a single entry,
|
|
262
|
+
* so a flat 7-option control would be mostly disabled noise.
|
|
263
|
+
*/
|
|
264
|
+
const SHELL_DEVICE_OPTIONS = {
|
|
265
|
+
browser: { value: 'browser', label: 'Browser', icon: 'language' },
|
|
266
|
+
macos: { value: 'macos', label: 'macOS', icon: 'laptop_mac' },
|
|
267
|
+
windows: { value: 'windows', label: 'Windows', icon: 'laptop_windows' },
|
|
268
|
+
linux: { value: 'linux', label: 'Linux', icon: 'terminal' },
|
|
269
|
+
ipad: { value: 'ipad', label: 'iPad', icon: 'tablet_mac' },
|
|
270
|
+
android: { value: 'android', label: 'Android', icon: 'android' },
|
|
271
|
+
ios: { value: 'ios', label: 'iOS', icon: 'phone_iphone' },
|
|
272
|
+
};
|
|
273
|
+
/** Corner radius axis; each preset carries Tailwind's stock value for its name. */
|
|
274
|
+
const RADIUS_OPTIONS = [
|
|
275
|
+
{ value: 'none', label: 'None', icon: 'crop_square' },
|
|
276
|
+
{ value: 'xs', label: 'Xs', icon: 'rounded_corner' },
|
|
277
|
+
{ value: 'sm', label: 'Sm', icon: 'rounded_corner' },
|
|
278
|
+
{ value: 'md', label: 'Md', icon: 'rounded_corner' },
|
|
279
|
+
{ value: 'xl', label: 'Xl', icon: 'rounded_corner' },
|
|
280
|
+
{ value: 'full', label: 'Full', icon: 'circle' },
|
|
281
|
+
];
|
|
282
|
+
const SPACE_OPTIONS = [
|
|
283
|
+
{ value: 'compact', label: 'Compact', icon: 'density_small' },
|
|
284
|
+
{ value: 'normal', label: 'Normal', icon: 'density_medium' },
|
|
285
|
+
];
|
|
286
|
+
const APPEARANCE_OPTIONS = [
|
|
287
|
+
{ value: 'flat', label: 'Flat', icon: 'web_asset' },
|
|
288
|
+
{ value: 'border-rail', label: 'Border rail', icon: 'border_outer' },
|
|
289
|
+
];
|
|
290
|
+
const SURFACE_OPTIONS = [
|
|
291
|
+
{ value: 'flat', label: 'Flat', icon: 'format_color_reset' },
|
|
292
|
+
{ value: 'grid', label: 'Grid', icon: 'grid_4x4' },
|
|
293
|
+
{ value: 'grid-line', label: 'Grid line', icon: 'grid_view' },
|
|
294
|
+
{ value: 'honeycomb', label: 'Honeycomb', icon: 'hexagon' },
|
|
295
|
+
{ value: 'matrix', label: 'Matrix', icon: 'data_object' },
|
|
296
|
+
{ value: 'circuit', label: 'Circuit', icon: 'memory' },
|
|
297
|
+
{ value: 'line-vertical', label: 'Vertical lines', icon: 'view_column' },
|
|
298
|
+
{ value: 'line-horizontal', label: 'Horizontal lines', icon: 'view_agenda' },
|
|
299
|
+
];
|
|
300
|
+
const LAYOUT_TYPE_OPTIONS = [
|
|
301
|
+
{ value: 'vertical', label: 'Vertical', icon: 'view_sidebar' },
|
|
302
|
+
{ value: 'horizontal', label: 'Horizontal', icon: 'view_stream' },
|
|
303
|
+
{ value: 'empty', label: 'Empty', icon: 'crop_free' },
|
|
304
|
+
{ value: 'fluid', label: 'Fluid', icon: 'fit_screen' },
|
|
305
|
+
];
|
|
306
|
+
const WIDTH_OPTIONS = [
|
|
307
|
+
{ value: 'full', label: 'Full', icon: 'width_full' },
|
|
308
|
+
{ value: 'wide', label: 'Wide', icon: 'width_wide' },
|
|
309
|
+
{ value: 'container', label: 'Container', icon: 'width_normal' },
|
|
310
|
+
{ value: 'fluid', label: 'Fluid', icon: 'aspect_ratio' },
|
|
311
|
+
];
|
|
312
|
+
const NAV_TYPE_OPTIONS = [
|
|
313
|
+
{ value: 'sidebar', label: 'Sidebar', icon: 'left_panel_open' },
|
|
314
|
+
{ value: 'dockbar', label: 'Dockbar', icon: 'dock_to_left' },
|
|
315
|
+
{ value: 'navbar', label: 'Navbar', icon: 'menu' },
|
|
316
|
+
{ value: 'flyout', label: 'Flyout', icon: 'open_in_new' },
|
|
317
|
+
{ value: 'desktop', label: 'Desktop', icon: 'desktop_windows' },
|
|
318
|
+
];
|
|
319
|
+
const ACCENT_SWATCH = {
|
|
320
|
+
base: 'hsl(240 6% 10%)',
|
|
321
|
+
red: 'hsl(0 72% 48%)',
|
|
322
|
+
orange: 'hsl(24 95% 48%)',
|
|
323
|
+
amber: 'hsl(38 92% 48%)',
|
|
324
|
+
yellow: 'hsl(48 96% 48%)',
|
|
325
|
+
lime: 'hsl(84 78% 44%)',
|
|
326
|
+
green: 'hsl(142 69% 40%)',
|
|
327
|
+
emerald: 'hsl(160 78% 38%)',
|
|
328
|
+
teal: 'hsl(173 75% 38%)',
|
|
329
|
+
cyan: 'hsl(191 85% 40%)',
|
|
330
|
+
sky: 'hsl(199 89% 46%)',
|
|
331
|
+
blue: 'hsl(221 83% 53%)',
|
|
332
|
+
indigo: 'hsl(239 78% 60%)',
|
|
333
|
+
violet: 'hsl(258 86% 62%)',
|
|
334
|
+
purple: 'hsl(271 81% 56%)',
|
|
335
|
+
fuchsia: 'hsl(292 80% 56%)',
|
|
336
|
+
pink: 'hsl(330 78% 54%)',
|
|
337
|
+
rose: 'hsl(349 84% 52%)',
|
|
338
|
+
brand: 'hsl(var(--brand))',
|
|
339
|
+
};
|
|
340
|
+
const NEUTRAL_SWATCH = {
|
|
341
|
+
base: 'hsl(220 9% 46%)',
|
|
342
|
+
slate: 'hsl(215 16% 47%)',
|
|
343
|
+
gray: 'hsl(220 9% 46%)',
|
|
344
|
+
zinc: 'hsl(240 5% 46%)',
|
|
345
|
+
neutral: 'hsl(0 0% 46%)',
|
|
346
|
+
stone: 'hsl(30 6% 46%)',
|
|
347
|
+
mauve: 'hsl(280 6% 47%)',
|
|
348
|
+
olive: 'hsl(80 8% 42%)',
|
|
349
|
+
mist: 'hsl(200 14% 50%)',
|
|
350
|
+
taupe: 'hsl(28 10% 46%)',
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* The Layout section of `ThemeSettings`: appearance, surface, layout type, width.
|
|
355
|
+
* Internal, see the Theme section for why.
|
|
356
|
+
*
|
|
357
|
+
* The adapter is injected NON-optionally: the parent only renders this section
|
|
358
|
+
* when an adapter is present, which is what lets a theme-only consumer use
|
|
359
|
+
* `ThemeSettings` with just the Theme and Shell sections.
|
|
360
|
+
*/
|
|
361
|
+
class ThemeSettingsLayoutSectionComponent {
|
|
362
|
+
shell = inject(ShellService);
|
|
363
|
+
settings = inject(THEME_SETTINGS_ADAPTER);
|
|
364
|
+
shellMode = this.shell.mode;
|
|
365
|
+
/** Every mode but `web` owns its surface, which is what locks the width axis. */
|
|
366
|
+
widthLocked = computed(() => this.shellMode() !== 'web', /* @ts-ignore */
|
|
367
|
+
...(ngDevMode ? [{ debugName: "widthLocked" }] : /* istanbul ignore next */ []));
|
|
368
|
+
appearanceOptions = APPEARANCE_OPTIONS;
|
|
369
|
+
surfaceOptions = SURFACE_OPTIONS;
|
|
370
|
+
layoutTypeOptions = LAYOUT_TYPE_OPTIONS;
|
|
371
|
+
widthOptions = computed(() => WIDTH_OPTIONS.map((option) => ({
|
|
372
|
+
...option,
|
|
373
|
+
disabled:
|
|
374
|
+
// The shell mode lock leaves exactly one legal width; `fluid` additionally
|
|
375
|
+
// needs the fluid layout type to mean anything.
|
|
376
|
+
(this.widthLocked() && option.value !== LAYOUT_LOCKED_WIDTH) ||
|
|
377
|
+
(option.value === 'fluid' && this.settings.layoutType() !== 'fluid'),
|
|
378
|
+
})), /* @ts-ignore */
|
|
379
|
+
...(ngDevMode ? [{ debugName: "widthOptions" }] : /* istanbul ignore next */ []));
|
|
380
|
+
layoutGroupId = injectUniqueId('theme-settings-layout');
|
|
381
|
+
appearanceLabelId = injectUniqueId('theme-settings-appearance');
|
|
382
|
+
surfaceLabelId = injectUniqueId('theme-settings-surface');
|
|
383
|
+
layoutTypeLabelId = injectUniqueId('theme-settings-layout-type');
|
|
384
|
+
widthLabelId = injectUniqueId('theme-settings-width');
|
|
385
|
+
setAppearance(value) {
|
|
386
|
+
if (value === 'flat' || value === 'border-rail')
|
|
387
|
+
this.settings.setAppearance(value);
|
|
388
|
+
}
|
|
389
|
+
setSurface(value) {
|
|
390
|
+
if (SURFACE_OPTIONS.some((option) => option.value === value))
|
|
391
|
+
this.settings.setSurface(value);
|
|
392
|
+
}
|
|
393
|
+
setLayoutType(value) {
|
|
394
|
+
if (LAYOUT_TYPE_OPTIONS.some((option) => option.value === value)) {
|
|
395
|
+
this.settings.setLayoutType(value);
|
|
396
|
+
normalizeNavType(this.settings, this.shellMode());
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
setWidth(value) {
|
|
400
|
+
// Refuse the change rather than record a preference the lock would hide.
|
|
401
|
+
if (this.widthLocked())
|
|
402
|
+
return;
|
|
403
|
+
if (WIDTH_OPTIONS.some((option) => option.value === value))
|
|
404
|
+
this.settings.setWidth(value);
|
|
405
|
+
}
|
|
406
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeSettingsLayoutSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
407
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: ThemeSettingsLayoutSectionComponent, isStandalone: true, selector: "ThemeSettingsLayoutSection", host: { classAttribute: "contents" }, ngImport: i0, template: `
|
|
408
|
+
<section
|
|
409
|
+
class="flex flex-col gap-5 border-t border-border pt-7"
|
|
410
|
+
[attr.aria-labelledby]="layoutGroupId"
|
|
411
|
+
>
|
|
412
|
+
<h2 [id]="layoutGroupId" class="nav-heading text-foreground">Layout</h2>
|
|
413
|
+
<div class="flex flex-col gap-2">
|
|
414
|
+
<h3 [id]="appearanceLabelId" class="nav-heading text-muted-foreground">
|
|
415
|
+
Layout appearance
|
|
416
|
+
</h3>
|
|
417
|
+
<SegmentedControl
|
|
418
|
+
appearance="icon-text"
|
|
419
|
+
space="compact"
|
|
420
|
+
[value]="settings.appearance()"
|
|
421
|
+
[options]="appearanceOptions"
|
|
422
|
+
[aria-labelledby]="appearanceLabelId"
|
|
423
|
+
(valueChange)="setAppearance($event)"
|
|
424
|
+
/>
|
|
425
|
+
</div>
|
|
426
|
+
<div class="flex flex-col gap-2">
|
|
427
|
+
<h3 [id]="surfaceLabelId" class="nav-heading text-muted-foreground">Layout surface</h3>
|
|
428
|
+
<SegmentedControl
|
|
429
|
+
appearance="icon-text"
|
|
430
|
+
space="compact"
|
|
431
|
+
[value]="settings.surface()"
|
|
432
|
+
[options]="surfaceOptions"
|
|
433
|
+
[aria-labelledby]="surfaceLabelId"
|
|
434
|
+
(valueChange)="setSurface($event)"
|
|
435
|
+
/>
|
|
436
|
+
</div>
|
|
437
|
+
<div class="flex flex-col gap-2">
|
|
438
|
+
<h3 [id]="layoutTypeLabelId" class="nav-heading text-muted-foreground">Layout type</h3>
|
|
439
|
+
<SegmentedControl
|
|
440
|
+
appearance="icon-text"
|
|
441
|
+
space="compact"
|
|
442
|
+
[value]="settings.layoutType()"
|
|
443
|
+
[options]="layoutTypeOptions"
|
|
444
|
+
[aria-labelledby]="layoutTypeLabelId"
|
|
445
|
+
(valueChange)="setLayoutType($event)"
|
|
446
|
+
/>
|
|
447
|
+
</div>
|
|
448
|
+
<div class="flex flex-col gap-2">
|
|
449
|
+
<h3 [id]="widthLabelId" class="nav-heading text-muted-foreground">Layout width</h3>
|
|
450
|
+
<SegmentedControl
|
|
451
|
+
appearance="icon-text"
|
|
452
|
+
space="compact"
|
|
453
|
+
[value]="settings.width()"
|
|
454
|
+
[options]="widthOptions()"
|
|
455
|
+
[aria-labelledby]="widthLabelId"
|
|
456
|
+
(valueChange)="setWidth($event)"
|
|
457
|
+
/>
|
|
458
|
+
@if (widthLocked()) {
|
|
459
|
+
<p class="text-xs text-muted-foreground" aria-live="polite">
|
|
460
|
+
Locked to full on {{ shellMode() }}: the app is the whole surface, so there is no page
|
|
461
|
+
to inset the frame within.
|
|
462
|
+
</p>
|
|
463
|
+
}
|
|
464
|
+
</div>
|
|
465
|
+
</section>
|
|
466
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SegmentedControlComponent, selector: "SegmentedControl", inputs: ["options", "value", "appearance", "space", "disabled", "aria-label", "aria-labelledby", "aria-describedby", "class"], outputs: ["valueChange"] }] });
|
|
467
|
+
}
|
|
468
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeSettingsLayoutSectionComponent, decorators: [{
|
|
469
|
+
type: Component,
|
|
470
|
+
args: [{
|
|
471
|
+
selector: 'ThemeSettingsLayoutSection',
|
|
472
|
+
imports: [SegmentedControlComponent],
|
|
473
|
+
host: { class: 'contents' },
|
|
474
|
+
template: `
|
|
475
|
+
<section
|
|
476
|
+
class="flex flex-col gap-5 border-t border-border pt-7"
|
|
477
|
+
[attr.aria-labelledby]="layoutGroupId"
|
|
478
|
+
>
|
|
479
|
+
<h2 [id]="layoutGroupId" class="nav-heading text-foreground">Layout</h2>
|
|
480
|
+
<div class="flex flex-col gap-2">
|
|
481
|
+
<h3 [id]="appearanceLabelId" class="nav-heading text-muted-foreground">
|
|
482
|
+
Layout appearance
|
|
483
|
+
</h3>
|
|
484
|
+
<SegmentedControl
|
|
485
|
+
appearance="icon-text"
|
|
486
|
+
space="compact"
|
|
487
|
+
[value]="settings.appearance()"
|
|
488
|
+
[options]="appearanceOptions"
|
|
489
|
+
[aria-labelledby]="appearanceLabelId"
|
|
490
|
+
(valueChange)="setAppearance($event)"
|
|
491
|
+
/>
|
|
492
|
+
</div>
|
|
493
|
+
<div class="flex flex-col gap-2">
|
|
494
|
+
<h3 [id]="surfaceLabelId" class="nav-heading text-muted-foreground">Layout surface</h3>
|
|
495
|
+
<SegmentedControl
|
|
496
|
+
appearance="icon-text"
|
|
497
|
+
space="compact"
|
|
498
|
+
[value]="settings.surface()"
|
|
499
|
+
[options]="surfaceOptions"
|
|
500
|
+
[aria-labelledby]="surfaceLabelId"
|
|
501
|
+
(valueChange)="setSurface($event)"
|
|
502
|
+
/>
|
|
503
|
+
</div>
|
|
504
|
+
<div class="flex flex-col gap-2">
|
|
505
|
+
<h3 [id]="layoutTypeLabelId" class="nav-heading text-muted-foreground">Layout type</h3>
|
|
506
|
+
<SegmentedControl
|
|
507
|
+
appearance="icon-text"
|
|
508
|
+
space="compact"
|
|
509
|
+
[value]="settings.layoutType()"
|
|
510
|
+
[options]="layoutTypeOptions"
|
|
511
|
+
[aria-labelledby]="layoutTypeLabelId"
|
|
512
|
+
(valueChange)="setLayoutType($event)"
|
|
513
|
+
/>
|
|
514
|
+
</div>
|
|
515
|
+
<div class="flex flex-col gap-2">
|
|
516
|
+
<h3 [id]="widthLabelId" class="nav-heading text-muted-foreground">Layout width</h3>
|
|
517
|
+
<SegmentedControl
|
|
518
|
+
appearance="icon-text"
|
|
519
|
+
space="compact"
|
|
520
|
+
[value]="settings.width()"
|
|
521
|
+
[options]="widthOptions()"
|
|
522
|
+
[aria-labelledby]="widthLabelId"
|
|
523
|
+
(valueChange)="setWidth($event)"
|
|
524
|
+
/>
|
|
525
|
+
@if (widthLocked()) {
|
|
526
|
+
<p class="text-xs text-muted-foreground" aria-live="polite">
|
|
527
|
+
Locked to full on {{ shellMode() }}: the app is the whole surface, so there is no page
|
|
528
|
+
to inset the frame within.
|
|
529
|
+
</p>
|
|
530
|
+
}
|
|
531
|
+
</div>
|
|
532
|
+
</section>
|
|
533
|
+
`,
|
|
534
|
+
}]
|
|
535
|
+
}] });
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* The Navigation section of `ThemeSettings`: navigation type and its mode.
|
|
539
|
+
* Internal, see the Theme section for why.
|
|
540
|
+
*/
|
|
541
|
+
class ThemeSettingsNavigationSectionComponent {
|
|
542
|
+
shell = inject(ShellService);
|
|
543
|
+
settings = inject(THEME_SETTINGS_ADAPTER);
|
|
544
|
+
navTypeOptions = computed(() => NAV_TYPE_OPTIONS.map((option) => ({
|
|
545
|
+
...option,
|
|
546
|
+
disabled: !allowedNavTypes(this.settings, this.shell.mode()).includes(option.value),
|
|
547
|
+
})), /* @ts-ignore */
|
|
548
|
+
...(ngDevMode ? [{ debugName: "navTypeOptions" }] : /* istanbul ignore next */ []));
|
|
549
|
+
// Mirrors `allowedNavTypeModes` in the default adapter, which decides which of
|
|
550
|
+
// these the adapter will actually accept; here they also carry their labels,
|
|
551
|
+
// and dockbar names its default "Sticky".
|
|
552
|
+
navTypeModeOptions = computed(() => {
|
|
553
|
+
switch (this.settings.navType()) {
|
|
554
|
+
case 'sidebar':
|
|
555
|
+
return [
|
|
556
|
+
{ value: 'default', label: 'Default', icon: 'view_sidebar' },
|
|
557
|
+
{ value: 'collapsed', label: 'Collapsed', icon: 'first_page' },
|
|
558
|
+
];
|
|
559
|
+
case 'dockbar':
|
|
560
|
+
return [
|
|
561
|
+
{ value: 'default', label: 'Sticky', icon: 'dock_to_left' },
|
|
562
|
+
{ value: 'drawer', label: 'Drawer', icon: 'side_navigation' },
|
|
563
|
+
];
|
|
564
|
+
default:
|
|
565
|
+
return [{ value: 'default', label: 'Default', icon: 'tune' }];
|
|
566
|
+
}
|
|
567
|
+
}, /* @ts-ignore */
|
|
568
|
+
...(ngDevMode ? [{ debugName: "navTypeModeOptions" }] : /* istanbul ignore next */ []));
|
|
569
|
+
navigationGroupId = injectUniqueId('theme-settings-navigation');
|
|
570
|
+
navTypeLabelId = injectUniqueId('theme-settings-nav-type');
|
|
571
|
+
navTypeModeLabelId = injectUniqueId('theme-settings-nav-type-mode');
|
|
572
|
+
setNavType(value) {
|
|
573
|
+
if (value &&
|
|
574
|
+
allowedNavTypes(this.settings, this.shell.mode()).includes(value)) {
|
|
575
|
+
this.settings.setNavType(value);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
setNavTypeMode(value) {
|
|
579
|
+
if (value === 'default' || value === 'collapsed' || value === 'drawer')
|
|
580
|
+
this.settings.setNavTypeMode(value);
|
|
581
|
+
}
|
|
582
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeSettingsNavigationSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
583
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.4", type: ThemeSettingsNavigationSectionComponent, isStandalone: true, selector: "ThemeSettingsNavigationSection", host: { classAttribute: "contents" }, ngImport: i0, template: `
|
|
584
|
+
<section
|
|
585
|
+
class="flex flex-col gap-5 border-t border-border pt-7"
|
|
586
|
+
[attr.aria-labelledby]="navigationGroupId"
|
|
587
|
+
>
|
|
588
|
+
<h2 [id]="navigationGroupId" class="nav-heading text-foreground">Navigation</h2>
|
|
589
|
+
<div class="flex flex-col gap-2">
|
|
590
|
+
<h3 [id]="navTypeLabelId" class="nav-heading text-muted-foreground">Nav type</h3>
|
|
591
|
+
<SegmentedControl
|
|
592
|
+
appearance="icon-text"
|
|
593
|
+
space="compact"
|
|
594
|
+
[value]="settings.navType()"
|
|
595
|
+
[options]="navTypeOptions()"
|
|
596
|
+
[aria-labelledby]="navTypeLabelId"
|
|
597
|
+
(valueChange)="setNavType($event)"
|
|
598
|
+
/>
|
|
599
|
+
</div>
|
|
600
|
+
<div class="flex flex-col gap-2">
|
|
601
|
+
<h3 [id]="navTypeModeLabelId" class="nav-heading text-muted-foreground">Nav type mode</h3>
|
|
602
|
+
<SegmentedControl
|
|
603
|
+
appearance="icon-text"
|
|
604
|
+
space="compact"
|
|
605
|
+
[value]="settings.navTypeMode()"
|
|
606
|
+
[options]="navTypeModeOptions()"
|
|
607
|
+
[aria-labelledby]="navTypeModeLabelId"
|
|
608
|
+
(valueChange)="setNavTypeMode($event)"
|
|
609
|
+
/>
|
|
610
|
+
</div>
|
|
611
|
+
</section>
|
|
612
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SegmentedControlComponent, selector: "SegmentedControl", inputs: ["options", "value", "appearance", "space", "disabled", "aria-label", "aria-labelledby", "aria-describedby", "class"], outputs: ["valueChange"] }] });
|
|
613
|
+
}
|
|
614
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeSettingsNavigationSectionComponent, decorators: [{
|
|
615
|
+
type: Component,
|
|
616
|
+
args: [{
|
|
617
|
+
selector: 'ThemeSettingsNavigationSection',
|
|
618
|
+
imports: [SegmentedControlComponent],
|
|
619
|
+
host: { class: 'contents' },
|
|
620
|
+
template: `
|
|
621
|
+
<section
|
|
622
|
+
class="flex flex-col gap-5 border-t border-border pt-7"
|
|
623
|
+
[attr.aria-labelledby]="navigationGroupId"
|
|
624
|
+
>
|
|
625
|
+
<h2 [id]="navigationGroupId" class="nav-heading text-foreground">Navigation</h2>
|
|
626
|
+
<div class="flex flex-col gap-2">
|
|
627
|
+
<h3 [id]="navTypeLabelId" class="nav-heading text-muted-foreground">Nav type</h3>
|
|
628
|
+
<SegmentedControl
|
|
629
|
+
appearance="icon-text"
|
|
630
|
+
space="compact"
|
|
631
|
+
[value]="settings.navType()"
|
|
632
|
+
[options]="navTypeOptions()"
|
|
633
|
+
[aria-labelledby]="navTypeLabelId"
|
|
634
|
+
(valueChange)="setNavType($event)"
|
|
635
|
+
/>
|
|
636
|
+
</div>
|
|
637
|
+
<div class="flex flex-col gap-2">
|
|
638
|
+
<h3 [id]="navTypeModeLabelId" class="nav-heading text-muted-foreground">Nav type mode</h3>
|
|
639
|
+
<SegmentedControl
|
|
640
|
+
appearance="icon-text"
|
|
641
|
+
space="compact"
|
|
642
|
+
[value]="settings.navTypeMode()"
|
|
643
|
+
[options]="navTypeModeOptions()"
|
|
644
|
+
[aria-labelledby]="navTypeModeLabelId"
|
|
645
|
+
(valueChange)="setNavTypeMode($event)"
|
|
646
|
+
/>
|
|
647
|
+
</div>
|
|
648
|
+
</section>
|
|
649
|
+
`,
|
|
650
|
+
}]
|
|
651
|
+
}] });
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* The Shell section of `ThemeSettings`: delivery mode, device, frame, chrome
|
|
655
|
+
* colour. Internal, see the Theme section for why.
|
|
656
|
+
*
|
|
657
|
+
* It injects the adapter optionally because a theme-only consumer has none, and
|
|
658
|
+
* it only needs it to run the shared navigation-type correction after a mode
|
|
659
|
+
* change.
|
|
660
|
+
*/
|
|
661
|
+
class ThemeSettingsShellSectionComponent {
|
|
662
|
+
shell = inject(ShellService);
|
|
663
|
+
settings = inject(THEME_SETTINGS_ADAPTER, { optional: true });
|
|
664
|
+
shellMode = this.shell.mode;
|
|
665
|
+
shellColor = this.shell.color;
|
|
666
|
+
shellFrame = this.shell.frame;
|
|
667
|
+
/** Only a desktop window has a frame to shape, so `modern` is offered only there. */
|
|
668
|
+
shellFrameOptions = computed(() => SHELL_FRAME_OPTIONS.map((option) => ({
|
|
669
|
+
...option,
|
|
670
|
+
disabled: option.value === 'modern' && this.shellMode() !== 'desktop',
|
|
671
|
+
})), /* @ts-ignore */
|
|
672
|
+
...(ngDevMode ? [{ debugName: "shellFrameOptions" }] : /* istanbul ignore next */ []));
|
|
673
|
+
/**
|
|
674
|
+
* The device actually in effect: `ShellService.device()` is a preference that may
|
|
675
|
+
* belong to another form factor (it deliberately survives a temporary mode change),
|
|
676
|
+
* so the control shows the value `<Shell>` would render for the active mode.
|
|
677
|
+
*/
|
|
678
|
+
shellDevice = computed(() => resolveShellDeviceForMode(this.shellMode(), this.shell.device()), /* @ts-ignore */
|
|
679
|
+
...(ngDevMode ? [{ debugName: "shellDevice" }] : /* istanbul ignore next */ []));
|
|
680
|
+
shellDeviceOptions = computed(() => shellDevicesForMode(this.shellMode()).map((device) => SHELL_DEVICE_OPTIONS[device]), /* @ts-ignore */
|
|
681
|
+
...(ngDevMode ? [{ debugName: "shellDeviceOptions" }] : /* istanbul ignore next */ []));
|
|
682
|
+
shellModeOptions = SHELL_MODE_OPTIONS;
|
|
683
|
+
shellColorOptions = SHELL_COLOR_OPTIONS;
|
|
684
|
+
shellGroupId = injectUniqueId('theme-settings-shell');
|
|
685
|
+
shellModeLabelId = injectUniqueId('theme-settings-shell-mode');
|
|
686
|
+
shellDeviceLabelId = injectUniqueId('theme-settings-shell-device');
|
|
687
|
+
shellColorLabelId = injectUniqueId('theme-settings-shell-color');
|
|
688
|
+
shellFrameLabelId = injectUniqueId('theme-settings-shell-frame');
|
|
689
|
+
setShellMode(value) {
|
|
690
|
+
if (SHELL_MODES.includes(value)) {
|
|
691
|
+
this.shell.setMode(value);
|
|
692
|
+
normalizeNavType(this.settings, this.shellMode());
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
setShellDevice(value) {
|
|
696
|
+
// Guard on the active mode, not just the union: a device from another form
|
|
697
|
+
// factor would be corrected away on render anyway.
|
|
698
|
+
if (value && isShellDeviceForMode(this.shellMode(), value)) {
|
|
699
|
+
this.shell.setDevice(value);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
setShellFrame(value) {
|
|
703
|
+
// Refuse `modern` off desktop rather than store a preference the shell would
|
|
704
|
+
// coerce away on render.
|
|
705
|
+
if (value === 'modern' && this.shellMode() !== 'desktop')
|
|
706
|
+
return;
|
|
707
|
+
if (SHELL_FRAMES.includes(value))
|
|
708
|
+
this.shell.setFrame(value);
|
|
709
|
+
}
|
|
710
|
+
setShellColor(value) {
|
|
711
|
+
if (SHELL_COLORS.includes(value))
|
|
712
|
+
this.shell.setColor(value);
|
|
713
|
+
}
|
|
714
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeSettingsShellSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
715
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: ThemeSettingsShellSectionComponent, isStandalone: true, selector: "ThemeSettingsShellSection", host: { classAttribute: "contents" }, ngImport: i0, template: `
|
|
716
|
+
<section
|
|
717
|
+
class="flex flex-col gap-5 border-t border-border pt-7"
|
|
718
|
+
[attr.aria-labelledby]="shellGroupId"
|
|
719
|
+
>
|
|
720
|
+
<h2 [id]="shellGroupId" class="nav-heading text-foreground">Shell</h2>
|
|
721
|
+
<div class="flex flex-col gap-2">
|
|
722
|
+
<h3 [id]="shellModeLabelId" class="nav-heading text-muted-foreground">Shell mode</h3>
|
|
723
|
+
<SegmentedControl
|
|
724
|
+
appearance="icon-text"
|
|
725
|
+
space="compact"
|
|
726
|
+
[value]="shellMode()"
|
|
727
|
+
[options]="shellModeOptions"
|
|
728
|
+
[aria-labelledby]="shellModeLabelId"
|
|
729
|
+
(valueChange)="setShellMode($event)"
|
|
730
|
+
/>
|
|
731
|
+
<p class="text-xs text-muted-foreground">
|
|
732
|
+
Where the app is delivered. Only <code>web</code> is a page inside a browser; the others
|
|
733
|
+
own the whole surface, so the layout width locks to full.
|
|
734
|
+
</p>
|
|
735
|
+
</div>
|
|
736
|
+
<div class="flex flex-col gap-2">
|
|
737
|
+
<h3 [id]="shellDeviceLabelId" class="nav-heading text-muted-foreground">Shell device</h3>
|
|
738
|
+
<SegmentedControl
|
|
739
|
+
appearance="icon-text"
|
|
740
|
+
space="compact"
|
|
741
|
+
[value]="shellDevice()"
|
|
742
|
+
[options]="shellDeviceOptions()"
|
|
743
|
+
[aria-labelledby]="shellDeviceLabelId"
|
|
744
|
+
(valueChange)="setShellDevice($event)"
|
|
745
|
+
/>
|
|
746
|
+
<p class="text-xs text-muted-foreground">
|
|
747
|
+
Which host the shell runs on. Only the devices the active mode can host are offered —
|
|
748
|
+
<code>android</code> ships on both tablet and mobile, so only the mode tells those apart.
|
|
749
|
+
</p>
|
|
750
|
+
</div>
|
|
751
|
+
<div class="flex flex-col gap-2">
|
|
752
|
+
<h3 [id]="shellFrameLabelId" class="nav-heading text-muted-foreground">Shell frame</h3>
|
|
753
|
+
<SegmentedControl
|
|
754
|
+
appearance="icon-text"
|
|
755
|
+
space="compact"
|
|
756
|
+
[value]="shellFrame()"
|
|
757
|
+
[options]="shellFrameOptions()"
|
|
758
|
+
[aria-labelledby]="shellFrameLabelId"
|
|
759
|
+
(valueChange)="setShellFrame($event)"
|
|
760
|
+
/>
|
|
761
|
+
<p class="text-xs text-muted-foreground">
|
|
762
|
+
@if (shellMode() === 'desktop') {
|
|
763
|
+
<code>modern</code> insets the app as a panel and fuses the titlebar with the surround
|
|
764
|
+
into one frame; <code>classic</code> keeps it flush under a bordered bar.
|
|
765
|
+
} @else {
|
|
766
|
+
Only a desktop window has a frame to shape, so this stays
|
|
767
|
+
<code>classic</code> on {{ shellMode() }}.
|
|
768
|
+
}
|
|
769
|
+
</p>
|
|
770
|
+
</div>
|
|
771
|
+
<div class="flex flex-col gap-2">
|
|
772
|
+
<h3 [id]="shellColorLabelId" class="nav-heading text-muted-foreground">Shell color</h3>
|
|
773
|
+
<SegmentedControl
|
|
774
|
+
appearance="icon-text"
|
|
775
|
+
space="compact"
|
|
776
|
+
[value]="shellColor()"
|
|
777
|
+
[options]="shellColorOptions"
|
|
778
|
+
[aria-labelledby]="shellColorLabelId"
|
|
779
|
+
(valueChange)="setShellColor($event)"
|
|
780
|
+
/>
|
|
781
|
+
<p class="text-xs text-muted-foreground">
|
|
782
|
+
Whether the desktop titlebar follows the theme axes above, or paints the active device's
|
|
783
|
+
own native chrome.
|
|
784
|
+
</p>
|
|
785
|
+
</div>
|
|
786
|
+
</section>
|
|
787
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SegmentedControlComponent, selector: "SegmentedControl", inputs: ["options", "value", "appearance", "space", "disabled", "aria-label", "aria-labelledby", "aria-describedby", "class"], outputs: ["valueChange"] }] });
|
|
788
|
+
}
|
|
789
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeSettingsShellSectionComponent, decorators: [{
|
|
790
|
+
type: Component,
|
|
791
|
+
args: [{
|
|
792
|
+
selector: 'ThemeSettingsShellSection',
|
|
793
|
+
imports: [SegmentedControlComponent],
|
|
794
|
+
host: { class: 'contents' },
|
|
795
|
+
template: `
|
|
796
|
+
<section
|
|
797
|
+
class="flex flex-col gap-5 border-t border-border pt-7"
|
|
798
|
+
[attr.aria-labelledby]="shellGroupId"
|
|
799
|
+
>
|
|
800
|
+
<h2 [id]="shellGroupId" class="nav-heading text-foreground">Shell</h2>
|
|
801
|
+
<div class="flex flex-col gap-2">
|
|
802
|
+
<h3 [id]="shellModeLabelId" class="nav-heading text-muted-foreground">Shell mode</h3>
|
|
803
|
+
<SegmentedControl
|
|
804
|
+
appearance="icon-text"
|
|
805
|
+
space="compact"
|
|
806
|
+
[value]="shellMode()"
|
|
807
|
+
[options]="shellModeOptions"
|
|
808
|
+
[aria-labelledby]="shellModeLabelId"
|
|
809
|
+
(valueChange)="setShellMode($event)"
|
|
810
|
+
/>
|
|
811
|
+
<p class="text-xs text-muted-foreground">
|
|
812
|
+
Where the app is delivered. Only <code>web</code> is a page inside a browser; the others
|
|
813
|
+
own the whole surface, so the layout width locks to full.
|
|
814
|
+
</p>
|
|
815
|
+
</div>
|
|
816
|
+
<div class="flex flex-col gap-2">
|
|
817
|
+
<h3 [id]="shellDeviceLabelId" class="nav-heading text-muted-foreground">Shell device</h3>
|
|
818
|
+
<SegmentedControl
|
|
819
|
+
appearance="icon-text"
|
|
820
|
+
space="compact"
|
|
821
|
+
[value]="shellDevice()"
|
|
822
|
+
[options]="shellDeviceOptions()"
|
|
823
|
+
[aria-labelledby]="shellDeviceLabelId"
|
|
824
|
+
(valueChange)="setShellDevice($event)"
|
|
825
|
+
/>
|
|
826
|
+
<p class="text-xs text-muted-foreground">
|
|
827
|
+
Which host the shell runs on. Only the devices the active mode can host are offered —
|
|
828
|
+
<code>android</code> ships on both tablet and mobile, so only the mode tells those apart.
|
|
829
|
+
</p>
|
|
830
|
+
</div>
|
|
831
|
+
<div class="flex flex-col gap-2">
|
|
832
|
+
<h3 [id]="shellFrameLabelId" class="nav-heading text-muted-foreground">Shell frame</h3>
|
|
833
|
+
<SegmentedControl
|
|
834
|
+
appearance="icon-text"
|
|
835
|
+
space="compact"
|
|
836
|
+
[value]="shellFrame()"
|
|
837
|
+
[options]="shellFrameOptions()"
|
|
838
|
+
[aria-labelledby]="shellFrameLabelId"
|
|
839
|
+
(valueChange)="setShellFrame($event)"
|
|
840
|
+
/>
|
|
841
|
+
<p class="text-xs text-muted-foreground">
|
|
842
|
+
@if (shellMode() === 'desktop') {
|
|
843
|
+
<code>modern</code> insets the app as a panel and fuses the titlebar with the surround
|
|
844
|
+
into one frame; <code>classic</code> keeps it flush under a bordered bar.
|
|
845
|
+
} @else {
|
|
846
|
+
Only a desktop window has a frame to shape, so this stays
|
|
847
|
+
<code>classic</code> on {{ shellMode() }}.
|
|
848
|
+
}
|
|
849
|
+
</p>
|
|
850
|
+
</div>
|
|
851
|
+
<div class="flex flex-col gap-2">
|
|
852
|
+
<h3 [id]="shellColorLabelId" class="nav-heading text-muted-foreground">Shell color</h3>
|
|
853
|
+
<SegmentedControl
|
|
854
|
+
appearance="icon-text"
|
|
855
|
+
space="compact"
|
|
856
|
+
[value]="shellColor()"
|
|
857
|
+
[options]="shellColorOptions"
|
|
858
|
+
[aria-labelledby]="shellColorLabelId"
|
|
859
|
+
(valueChange)="setShellColor($event)"
|
|
860
|
+
/>
|
|
861
|
+
<p class="text-xs text-muted-foreground">
|
|
862
|
+
Whether the desktop titlebar follows the theme axes above, or paints the active device's
|
|
863
|
+
own native chrome.
|
|
864
|
+
</p>
|
|
865
|
+
</div>
|
|
866
|
+
</section>
|
|
867
|
+
`,
|
|
868
|
+
}]
|
|
869
|
+
}] });
|
|
870
|
+
|
|
871
|
+
/*
|
|
872
|
+
* Brand-color math for `ThemeSettings`.
|
|
873
|
+
*
|
|
874
|
+
* The drawer's brand picker is a native `<input type="color">`, which speaks hex,
|
|
875
|
+
* while the design system's tokens are HSL triplets (`H S% L%`). These pure
|
|
876
|
+
* conversions bridge the two, plus pick a readable foreground for a chosen brand.
|
|
877
|
+
*
|
|
878
|
+
* Extracted from the component so the drawer stays inside the 700-line source-unit
|
|
879
|
+
* budget (scripts/check-library-boundaries.mjs) as shell axes are added.
|
|
880
|
+
*/
|
|
881
|
+
function hslToHex(color) {
|
|
882
|
+
const match = /^(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%$/.exec(color);
|
|
883
|
+
const [hue, saturation, lightness] = match
|
|
884
|
+
? [Number(match[1]), Number(match[2]), Number(match[3])]
|
|
885
|
+
: DEFAULT_BRAND_HSL;
|
|
886
|
+
const chroma = (1 - Math.abs(2 * (lightness / 100) - 1)) * (saturation / 100);
|
|
887
|
+
const segment = hue / 60;
|
|
888
|
+
const x = chroma * (1 - Math.abs((segment % 2) - 1));
|
|
889
|
+
const matchLightness = lightness / 100 - chroma / 2;
|
|
890
|
+
const [red, green, blue] = segment < 1
|
|
891
|
+
? [chroma, x, 0]
|
|
892
|
+
: segment < 2
|
|
893
|
+
? [x, chroma, 0]
|
|
894
|
+
: segment < 3
|
|
895
|
+
? [0, chroma, x]
|
|
896
|
+
: segment < 4
|
|
897
|
+
? [0, x, chroma]
|
|
898
|
+
: segment < 5
|
|
899
|
+
? [x, 0, chroma]
|
|
900
|
+
: [chroma, 0, x];
|
|
901
|
+
const channel = (value) => Math.round((value + matchLightness) * 255)
|
|
902
|
+
.toString(16)
|
|
903
|
+
.padStart(2, '0');
|
|
904
|
+
return `#${channel(red)}${channel(green)}${channel(blue)}`;
|
|
905
|
+
}
|
|
906
|
+
function hexToHsl(color) {
|
|
907
|
+
const match = /^#([0-9a-f]{6})$/i.exec(color);
|
|
908
|
+
if (!match)
|
|
909
|
+
return null;
|
|
910
|
+
const hex = match[1];
|
|
911
|
+
const [red, green, blue] = [0, 2, 4].map((index) => Number.parseInt(hex.slice(index, index + 2), 16) / 255);
|
|
912
|
+
const max = Math.max(red, green, blue);
|
|
913
|
+
const min = Math.min(red, green, blue);
|
|
914
|
+
const delta = max - min;
|
|
915
|
+
const lightness = (max + min) / 2;
|
|
916
|
+
let hue = 0;
|
|
917
|
+
if (delta !== 0)
|
|
918
|
+
hue =
|
|
919
|
+
max === red
|
|
920
|
+
? 60 * (((green - blue) / delta) % 6)
|
|
921
|
+
: max === green
|
|
922
|
+
? 60 * ((blue - red) / delta + 2)
|
|
923
|
+
: 60 * ((red - green) / delta + 4);
|
|
924
|
+
if (hue < 0)
|
|
925
|
+
hue += 360;
|
|
926
|
+
const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1));
|
|
927
|
+
return `${Math.round(hue)} ${Math.round(saturation * 100)}% ${Math.round(lightness * 100)}%`;
|
|
928
|
+
}
|
|
929
|
+
function brandForeground(color) {
|
|
930
|
+
const match = /^#([0-9a-f]{6})$/i.exec(color);
|
|
931
|
+
if (!match)
|
|
932
|
+
return '0 0% 100%';
|
|
933
|
+
const channels = [0, 2, 4].map((index) => Number.parseInt(match[1].slice(index, index + 2), 16));
|
|
934
|
+
const linear = (channel) => {
|
|
935
|
+
const value = channel / 255;
|
|
936
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
937
|
+
};
|
|
938
|
+
const luminance = linear(channels[0]) * 0.2126 + linear(channels[1]) * 0.7152 + linear(channels[2]) * 0.0722;
|
|
939
|
+
return luminance > 0.179 ? '0 0% 0%' : '0 0% 100%';
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* The Theme section of `ThemeSettings`: mode, brand, accent, neutral, space,
|
|
944
|
+
* radius.
|
|
945
|
+
*
|
|
946
|
+
* Internal. Not exported from the entry point: it exists so `ThemeSettings`
|
|
947
|
+
* stays under the 700-line source guard and so each axis group can be read on
|
|
948
|
+
* its own. Every dependency arrives by injection, so the parent composes it with
|
|
949
|
+
* no inputs and no outputs.
|
|
950
|
+
*
|
|
951
|
+
* `class: 'contents'` keeps the host out of the layout: the parent's flex column
|
|
952
|
+
* spaces the `<section>` inside, and a custom element defaults to `display:
|
|
953
|
+
* inline`, which would break that spacing.
|
|
954
|
+
*/
|
|
955
|
+
class ThemeSettingsThemeSectionComponent {
|
|
956
|
+
themeMode = inject(ThemeModeService);
|
|
957
|
+
themeColor = inject(ThemeColorService);
|
|
958
|
+
themeBrand = inject(ThemeBrandService);
|
|
959
|
+
themeRadius = inject(ThemeRadiusService);
|
|
960
|
+
themeSpace = inject(ThemeSpaceService);
|
|
961
|
+
mode = this.themeMode.mode;
|
|
962
|
+
resolvedMode = this.themeMode.resolvedMode;
|
|
963
|
+
color = this.themeColor.color;
|
|
964
|
+
neutralColor = this.themeColor.neutral;
|
|
965
|
+
brand = this.themeBrand.brand;
|
|
966
|
+
brandHex = computed(() => hslToHex(this.brand()?.color ?? DEFAULT_BRAND_COLOR), /* @ts-ignore */
|
|
967
|
+
...(ngDevMode ? [{ debugName: "brandHex" }] : /* istanbul ignore next */ []));
|
|
968
|
+
radius = this.themeRadius.radius;
|
|
969
|
+
space = this.themeSpace.space;
|
|
970
|
+
accents = THEME_COLORS;
|
|
971
|
+
neutrals = THEME_NEUTRALS;
|
|
972
|
+
accentSwatch = ACCENT_SWATCH;
|
|
973
|
+
neutralSwatch = NEUTRAL_SWATCH;
|
|
974
|
+
modeOptions = MODE_OPTIONS;
|
|
975
|
+
radiusOptions = RADIUS_OPTIONS;
|
|
976
|
+
spaceOptions = SPACE_OPTIONS;
|
|
977
|
+
themeGroupId = injectUniqueId('theme-settings-theme');
|
|
978
|
+
modeLabelId = injectUniqueId('theme-settings-mode');
|
|
979
|
+
brandLabelId = injectUniqueId('theme-settings-brand');
|
|
980
|
+
accentLabelId = injectUniqueId('theme-settings-accent');
|
|
981
|
+
neutralLabelId = injectUniqueId('theme-settings-neutral');
|
|
982
|
+
spaceLabelId = injectUniqueId('theme-settings-space');
|
|
983
|
+
radiusLabelId = injectUniqueId('theme-settings-radius');
|
|
984
|
+
swatchClasses(active) {
|
|
985
|
+
return cn('inline-flex size-[18px] rounded-full ring-1 ring-black/10 transition-[transform,box-shadow] duration-200 peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-background motion-reduce:transition-none', active
|
|
986
|
+
? 'ring-2 ring-ring ring-offset-2 ring-offset-background shadow-sm'
|
|
987
|
+
: 'hover:scale-105');
|
|
988
|
+
}
|
|
989
|
+
setMode(value) {
|
|
990
|
+
if (isThemeMode(value))
|
|
991
|
+
this.themeMode.setMode(value);
|
|
992
|
+
}
|
|
993
|
+
setColor(value) {
|
|
994
|
+
this.themeColor.setColor(value);
|
|
995
|
+
}
|
|
996
|
+
setNeutral(value) {
|
|
997
|
+
this.themeColor.setNeutral(value);
|
|
998
|
+
}
|
|
999
|
+
setRadius(value) {
|
|
1000
|
+
if (isThemeRadius(value))
|
|
1001
|
+
this.themeRadius.setRadius(value);
|
|
1002
|
+
}
|
|
1003
|
+
setSpace(value) {
|
|
1004
|
+
if (isThemeSpace(value))
|
|
1005
|
+
this.themeSpace.setSpace(value);
|
|
1006
|
+
}
|
|
1007
|
+
setBrandFromPicker(event) {
|
|
1008
|
+
if (!(event.target instanceof HTMLInputElement))
|
|
1009
|
+
return;
|
|
1010
|
+
const color = event.target.value;
|
|
1011
|
+
const hsl = hexToHsl(color);
|
|
1012
|
+
if (!hsl)
|
|
1013
|
+
return;
|
|
1014
|
+
this.themeBrand.setBrand(hsl, brandForeground(color));
|
|
1015
|
+
this.themeColor.setColor('brand');
|
|
1016
|
+
}
|
|
1017
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeSettingsThemeSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1018
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: ThemeSettingsThemeSectionComponent, isStandalone: true, selector: "ThemeSettingsThemeSection", host: { classAttribute: "contents" }, ngImport: i0, template: `
|
|
1019
|
+
<section class="flex flex-col gap-5" [attr.aria-labelledby]="themeGroupId">
|
|
1020
|
+
<h2 [id]="themeGroupId" class="nav-heading text-foreground">Theme</h2>
|
|
1021
|
+
<div class="flex flex-col gap-2">
|
|
1022
|
+
<div class="flex items-baseline justify-between gap-3">
|
|
1023
|
+
<h3 [id]="modeLabelId" class="nav-heading text-muted-foreground">Theme mode</h3>
|
|
1024
|
+
<span class="text-xs text-muted-foreground">{{ resolvedMode() }}</span>
|
|
1025
|
+
</div>
|
|
1026
|
+
<SegmentedControl
|
|
1027
|
+
appearance="icon-text"
|
|
1028
|
+
space="compact"
|
|
1029
|
+
[value]="mode()"
|
|
1030
|
+
[options]="modeOptions"
|
|
1031
|
+
[aria-labelledby]="modeLabelId"
|
|
1032
|
+
(valueChange)="setMode($event)"
|
|
1033
|
+
/>
|
|
1034
|
+
</div>
|
|
1035
|
+
<div class="flex flex-col gap-2">
|
|
1036
|
+
<h3 [id]="brandLabelId" class="nav-heading text-muted-foreground">Theme brand</h3>
|
|
1037
|
+
<input
|
|
1038
|
+
type="color"
|
|
1039
|
+
class="size-10 cursor-pointer appearance-none overflow-hidden rounded-full border border-border bg-transparent p-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background [&::-moz-color-swatch]:rounded-full [&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:rounded-full"
|
|
1040
|
+
[value]="brandHex()"
|
|
1041
|
+
[attr.aria-labelledby]="brandLabelId"
|
|
1042
|
+
(input)="setBrandFromPicker($event)"
|
|
1043
|
+
/>
|
|
1044
|
+
</div>
|
|
1045
|
+
<div class="flex flex-col gap-2">
|
|
1046
|
+
<h3 [id]="accentLabelId" class="nav-heading text-muted-foreground">Theme color</h3>
|
|
1047
|
+
<fieldset class="grid grid-cols-5 gap-2 sm:grid-cols-6">
|
|
1048
|
+
<legend class="sr-only">Choose theme color</legend>
|
|
1049
|
+
@for (accent of accents; track accent) {
|
|
1050
|
+
<label class="inline-flex size-10 cursor-pointer items-center justify-center"
|
|
1051
|
+
><input
|
|
1052
|
+
class="peer sr-only"
|
|
1053
|
+
type="radio"
|
|
1054
|
+
name="theme-settings-accent"
|
|
1055
|
+
[value]="accent"
|
|
1056
|
+
[checked]="color() === accent"
|
|
1057
|
+
(change)="setColor(accent)"
|
|
1058
|
+
/><span
|
|
1059
|
+
[class]="swatchClasses(color() === accent)"
|
|
1060
|
+
[style.background]="accentSwatch[accent]"
|
|
1061
|
+
><span class="sr-only">{{ accent }}</span></span
|
|
1062
|
+
></label
|
|
1063
|
+
>
|
|
1064
|
+
}
|
|
1065
|
+
</fieldset>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div class="flex flex-col gap-2">
|
|
1068
|
+
<h3 [id]="neutralLabelId" class="nav-heading text-muted-foreground">Theme neutral</h3>
|
|
1069
|
+
<fieldset class="grid grid-cols-5 gap-2 sm:grid-cols-6">
|
|
1070
|
+
<legend class="sr-only">Choose theme neutral</legend>
|
|
1071
|
+
@for (neutral of neutrals; track neutral) {
|
|
1072
|
+
<label class="inline-flex size-10 cursor-pointer items-center justify-center"
|
|
1073
|
+
><input
|
|
1074
|
+
class="peer sr-only"
|
|
1075
|
+
type="radio"
|
|
1076
|
+
name="theme-settings-neutral"
|
|
1077
|
+
[value]="neutral"
|
|
1078
|
+
[checked]="neutralColor() === neutral"
|
|
1079
|
+
(change)="setNeutral(neutral)"
|
|
1080
|
+
/><span
|
|
1081
|
+
[class]="swatchClasses(neutralColor() === neutral)"
|
|
1082
|
+
[style.background]="neutralSwatch[neutral]"
|
|
1083
|
+
><span class="sr-only">{{ neutral }}</span></span
|
|
1084
|
+
></label
|
|
1085
|
+
>
|
|
1086
|
+
}
|
|
1087
|
+
</fieldset>
|
|
1088
|
+
</div>
|
|
1089
|
+
<div class="flex flex-col gap-2">
|
|
1090
|
+
<h3 [id]="spaceLabelId" class="nav-heading text-muted-foreground">Theme space</h3>
|
|
1091
|
+
<SegmentedControl
|
|
1092
|
+
appearance="icon-text"
|
|
1093
|
+
space="compact"
|
|
1094
|
+
[value]="space()"
|
|
1095
|
+
[options]="spaceOptions"
|
|
1096
|
+
[aria-labelledby]="spaceLabelId"
|
|
1097
|
+
(valueChange)="setSpace($event)"
|
|
1098
|
+
/>
|
|
1099
|
+
</div>
|
|
1100
|
+
<div class="flex flex-col gap-2">
|
|
1101
|
+
<h3 [id]="radiusLabelId" class="nav-heading text-muted-foreground">Theme radius</h3>
|
|
1102
|
+
<SegmentedControl
|
|
1103
|
+
appearance="icon-text"
|
|
1104
|
+
space="compact"
|
|
1105
|
+
[value]="radius()"
|
|
1106
|
+
[options]="radiusOptions"
|
|
1107
|
+
[aria-labelledby]="radiusLabelId"
|
|
1108
|
+
(valueChange)="setRadius($event)"
|
|
1109
|
+
/>
|
|
1110
|
+
</div>
|
|
1111
|
+
</section>
|
|
1112
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SegmentedControlComponent, selector: "SegmentedControl", inputs: ["options", "value", "appearance", "space", "disabled", "aria-label", "aria-labelledby", "aria-describedby", "class"], outputs: ["valueChange"] }] });
|
|
1113
|
+
}
|
|
1114
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeSettingsThemeSectionComponent, decorators: [{
|
|
1115
|
+
type: Component,
|
|
1116
|
+
args: [{
|
|
1117
|
+
selector: 'ThemeSettingsThemeSection',
|
|
1118
|
+
imports: [SegmentedControlComponent],
|
|
1119
|
+
host: { class: 'contents' },
|
|
1120
|
+
template: `
|
|
1121
|
+
<section class="flex flex-col gap-5" [attr.aria-labelledby]="themeGroupId">
|
|
1122
|
+
<h2 [id]="themeGroupId" class="nav-heading text-foreground">Theme</h2>
|
|
1123
|
+
<div class="flex flex-col gap-2">
|
|
1124
|
+
<div class="flex items-baseline justify-between gap-3">
|
|
1125
|
+
<h3 [id]="modeLabelId" class="nav-heading text-muted-foreground">Theme mode</h3>
|
|
1126
|
+
<span class="text-xs text-muted-foreground">{{ resolvedMode() }}</span>
|
|
1127
|
+
</div>
|
|
1128
|
+
<SegmentedControl
|
|
1129
|
+
appearance="icon-text"
|
|
1130
|
+
space="compact"
|
|
1131
|
+
[value]="mode()"
|
|
1132
|
+
[options]="modeOptions"
|
|
1133
|
+
[aria-labelledby]="modeLabelId"
|
|
1134
|
+
(valueChange)="setMode($event)"
|
|
1135
|
+
/>
|
|
1136
|
+
</div>
|
|
1137
|
+
<div class="flex flex-col gap-2">
|
|
1138
|
+
<h3 [id]="brandLabelId" class="nav-heading text-muted-foreground">Theme brand</h3>
|
|
1139
|
+
<input
|
|
1140
|
+
type="color"
|
|
1141
|
+
class="size-10 cursor-pointer appearance-none overflow-hidden rounded-full border border-border bg-transparent p-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background [&::-moz-color-swatch]:rounded-full [&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:rounded-full"
|
|
1142
|
+
[value]="brandHex()"
|
|
1143
|
+
[attr.aria-labelledby]="brandLabelId"
|
|
1144
|
+
(input)="setBrandFromPicker($event)"
|
|
1145
|
+
/>
|
|
1146
|
+
</div>
|
|
1147
|
+
<div class="flex flex-col gap-2">
|
|
1148
|
+
<h3 [id]="accentLabelId" class="nav-heading text-muted-foreground">Theme color</h3>
|
|
1149
|
+
<fieldset class="grid grid-cols-5 gap-2 sm:grid-cols-6">
|
|
1150
|
+
<legend class="sr-only">Choose theme color</legend>
|
|
1151
|
+
@for (accent of accents; track accent) {
|
|
1152
|
+
<label class="inline-flex size-10 cursor-pointer items-center justify-center"
|
|
1153
|
+
><input
|
|
1154
|
+
class="peer sr-only"
|
|
1155
|
+
type="radio"
|
|
1156
|
+
name="theme-settings-accent"
|
|
1157
|
+
[value]="accent"
|
|
1158
|
+
[checked]="color() === accent"
|
|
1159
|
+
(change)="setColor(accent)"
|
|
1160
|
+
/><span
|
|
1161
|
+
[class]="swatchClasses(color() === accent)"
|
|
1162
|
+
[style.background]="accentSwatch[accent]"
|
|
1163
|
+
><span class="sr-only">{{ accent }}</span></span
|
|
1164
|
+
></label
|
|
1165
|
+
>
|
|
1166
|
+
}
|
|
1167
|
+
</fieldset>
|
|
1168
|
+
</div>
|
|
1169
|
+
<div class="flex flex-col gap-2">
|
|
1170
|
+
<h3 [id]="neutralLabelId" class="nav-heading text-muted-foreground">Theme neutral</h3>
|
|
1171
|
+
<fieldset class="grid grid-cols-5 gap-2 sm:grid-cols-6">
|
|
1172
|
+
<legend class="sr-only">Choose theme neutral</legend>
|
|
1173
|
+
@for (neutral of neutrals; track neutral) {
|
|
1174
|
+
<label class="inline-flex size-10 cursor-pointer items-center justify-center"
|
|
1175
|
+
><input
|
|
1176
|
+
class="peer sr-only"
|
|
1177
|
+
type="radio"
|
|
1178
|
+
name="theme-settings-neutral"
|
|
1179
|
+
[value]="neutral"
|
|
1180
|
+
[checked]="neutralColor() === neutral"
|
|
1181
|
+
(change)="setNeutral(neutral)"
|
|
1182
|
+
/><span
|
|
1183
|
+
[class]="swatchClasses(neutralColor() === neutral)"
|
|
1184
|
+
[style.background]="neutralSwatch[neutral]"
|
|
1185
|
+
><span class="sr-only">{{ neutral }}</span></span
|
|
1186
|
+
></label
|
|
1187
|
+
>
|
|
1188
|
+
}
|
|
1189
|
+
</fieldset>
|
|
1190
|
+
</div>
|
|
1191
|
+
<div class="flex flex-col gap-2">
|
|
1192
|
+
<h3 [id]="spaceLabelId" class="nav-heading text-muted-foreground">Theme space</h3>
|
|
1193
|
+
<SegmentedControl
|
|
1194
|
+
appearance="icon-text"
|
|
1195
|
+
space="compact"
|
|
1196
|
+
[value]="space()"
|
|
1197
|
+
[options]="spaceOptions"
|
|
1198
|
+
[aria-labelledby]="spaceLabelId"
|
|
1199
|
+
(valueChange)="setSpace($event)"
|
|
1200
|
+
/>
|
|
1201
|
+
</div>
|
|
1202
|
+
<div class="flex flex-col gap-2">
|
|
1203
|
+
<h3 [id]="radiusLabelId" class="nav-heading text-muted-foreground">Theme radius</h3>
|
|
1204
|
+
<SegmentedControl
|
|
1205
|
+
appearance="icon-text"
|
|
1206
|
+
space="compact"
|
|
1207
|
+
[value]="radius()"
|
|
1208
|
+
[options]="radiusOptions"
|
|
1209
|
+
[aria-labelledby]="radiusLabelId"
|
|
1210
|
+
(valueChange)="setRadius($event)"
|
|
1211
|
+
/>
|
|
1212
|
+
</div>
|
|
1213
|
+
</section>
|
|
1214
|
+
`,
|
|
1215
|
+
}]
|
|
1216
|
+
}] });
|
|
1217
|
+
|
|
1218
|
+
/** A navigation-aligned, right-side inspector for runtime theme and shell settings. */
|
|
1219
|
+
class ThemeSettingsComponent {
|
|
1220
|
+
/**
|
|
1221
|
+
* Present only when the app provided an adapter. It is a render gate here: the
|
|
1222
|
+
* Layout and Navigation sections inject the token non-optionally, so a
|
|
1223
|
+
* theme-only consumer simply gets the Theme and Shell sections.
|
|
1224
|
+
*/
|
|
1225
|
+
settings = inject(THEME_SETTINGS_ADAPTER, { optional: true });
|
|
1226
|
+
open = model(false, /* @ts-ignore */
|
|
1227
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
1228
|
+
trigger = input('button', /* @ts-ignore */
|
|
1229
|
+
...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
|
|
1230
|
+
triggerLabel = input('Theme settings', /* @ts-ignore */
|
|
1231
|
+
...(ngDevMode ? [{ debugName: "triggerLabel" }] : /* istanbul ignore next */ []));
|
|
1232
|
+
triggerIconOnly = input(false, { ...(ngDevMode ? { debugName: "triggerIconOnly" } : /* istanbul ignore next */ {}), alias: 'trigger-icon-only' });
|
|
1233
|
+
showTrigger = input(true, { ...(ngDevMode ? { debugName: "showTrigger" } : /* istanbul ignore next */ {}), alias: 'show-trigger' });
|
|
1234
|
+
side = input('right', /* @ts-ignore */
|
|
1235
|
+
...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
|
|
1236
|
+
class = input('', { ...(ngDevMode ? { debugName: "class" } : /* istanbul ignore next */ {}), alias: 'class' });
|
|
1237
|
+
triggerClasses = computed(() => cn('inline-flex items-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none', this.trigger() === 'link'
|
|
1238
|
+
? this.triggerIconOnly()
|
|
1239
|
+
? 'size-11 p-0 text-primary underline-offset-4 hover:underline'
|
|
1240
|
+
: 'min-h-11 px-1 text-primary underline-offset-4 hover:underline'
|
|
1241
|
+
: this.triggerIconOnly()
|
|
1242
|
+
? 'size-11 justify-center p-0 text-muted-foreground hover:bg-accent hover:text-foreground'
|
|
1243
|
+
: 'min-h-11 justify-center px-3 text-muted-foreground hover:bg-accent hover:text-accent-foreground', this.class()), /* @ts-ignore */
|
|
1244
|
+
...(ngDevMode ? [{ debugName: "triggerClasses" }] : /* istanbul ignore next */ []));
|
|
1245
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1246
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: ThemeSettingsComponent, isStandalone: true, selector: "ThemeSettings", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, trigger: { classPropertyName: "trigger", publicName: "trigger", isSignal: true, isRequired: false, transformFunction: null }, triggerLabel: { classPropertyName: "triggerLabel", publicName: "triggerLabel", isSignal: true, isRequired: false, transformFunction: null }, triggerIconOnly: { classPropertyName: "triggerIconOnly", publicName: "trigger-icon-only", isSignal: true, isRequired: false, transformFunction: null }, showTrigger: { classPropertyName: "showTrigger", publicName: "show-trigger", isSignal: true, isRequired: false, transformFunction: null }, side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { classAttribute: "contents" }, ngImport: i0, template: `
|
|
1247
|
+
@if (showTrigger()) {
|
|
1248
|
+
<button
|
|
1249
|
+
type="button"
|
|
1250
|
+
[class]="triggerClasses()"
|
|
1251
|
+
[attr.aria-label]="triggerIconOnly() ? triggerLabel() : null"
|
|
1252
|
+
[attr.title]="triggerIconOnly() ? triggerLabel() : null"
|
|
1253
|
+
aria-haspopup="dialog"
|
|
1254
|
+
[attr.aria-expanded]="open()"
|
|
1255
|
+
(click)="open.set(true)"
|
|
1256
|
+
>
|
|
1257
|
+
<Icon name="palette" [size]="18" />
|
|
1258
|
+
@if (triggerIconOnly()) {
|
|
1259
|
+
<span class="sr-only">{{ triggerLabel() }}</span>
|
|
1260
|
+
} @else {
|
|
1261
|
+
<span>{{ triggerLabel() }}</span>
|
|
1262
|
+
}
|
|
1263
|
+
</button>
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
@if (open()) {
|
|
1267
|
+
<Drawer
|
|
1268
|
+
[(open)]="open"
|
|
1269
|
+
[side]="side()"
|
|
1270
|
+
[showCloseButton]="false"
|
|
1271
|
+
class="ThemeSettings w-full max-w-md gap-0! p-0! sm:w-100"
|
|
1272
|
+
>
|
|
1273
|
+
<DrawerHeader
|
|
1274
|
+
class="flex h-(--layout-topbar-height) shrink-0 flex-row items-center gap-3 border-b border-border bg-background px-3 text-left"
|
|
1275
|
+
>
|
|
1276
|
+
<span
|
|
1277
|
+
aria-hidden="true"
|
|
1278
|
+
class="grid size-8 shrink-0 place-items-center rounded-md bg-primary/10 text-primary ring-1 ring-primary/20"
|
|
1279
|
+
><Icon name="palette" [size]="18"
|
|
1280
|
+
/></span>
|
|
1281
|
+
<div class="min-w-0 flex-1">
|
|
1282
|
+
<DrawerTitle>Theme settings</DrawerTitle>
|
|
1283
|
+
</div>
|
|
1284
|
+
<button
|
|
1285
|
+
type="button"
|
|
1286
|
+
DrawerClose
|
|
1287
|
+
class="inline-flex h-8 shrink-0 items-center rounded-md bg-primary px-2 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background motion-reduce:transition-none"
|
|
1288
|
+
>
|
|
1289
|
+
Done
|
|
1290
|
+
</button>
|
|
1291
|
+
</DrawerHeader>
|
|
1292
|
+
|
|
1293
|
+
<DrawerContent class="min-h-0 px-4 py-5 sm:px-5">
|
|
1294
|
+
<div class="flex flex-col gap-8">
|
|
1295
|
+
<ThemeSettingsThemeSection />
|
|
1296
|
+
<ThemeSettingsShellSection />
|
|
1297
|
+
@if (settings) {
|
|
1298
|
+
<ThemeSettingsLayoutSection />
|
|
1299
|
+
<ThemeSettingsNavigationSection />
|
|
1300
|
+
}
|
|
1301
|
+
</div>
|
|
1302
|
+
</DrawerContent>
|
|
1303
|
+
</Drawer>
|
|
1304
|
+
}
|
|
1305
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: DrawerCloseDirective, selector: "button[SheetClose], a[SheetClose], button[DrawerClose], a[DrawerClose]" }, { kind: "component", type: DrawerComponent, selector: "Sheet, Drawer", inputs: ["open", "side", "variant", "closeOnEscape", "closeOnBackdropClick", "backdrop", "showCloseButton", "closeButtonLabel", "restoreFocus", "aria-labelledby", "aria-describedby", "role", "class"], outputs: ["openChange", "openedChange"] }, { kind: "component", type: DrawerContentComponent, selector: "SheetContent, DrawerContent", inputs: ["class"] }, { kind: "component", type: DrawerHeaderComponent, selector: "SheetHeader, DrawerHeader", inputs: ["class"] }, { kind: "component", type: DrawerTitleComponent, selector: "SheetTitle, DrawerTitle, h2[SheetTitle], h2[DrawerTitle]", inputs: ["id", "class"] }, { kind: "component", type: IconComponent, selector: "Icon", inputs: ["name", "class", "size", "fill", "weight", "grade", "opticalSize"] }, { kind: "component", type: ThemeSettingsLayoutSectionComponent, selector: "ThemeSettingsLayoutSection" }, { kind: "component", type: ThemeSettingsNavigationSectionComponent, selector: "ThemeSettingsNavigationSection" }, { kind: "component", type: ThemeSettingsShellSectionComponent, selector: "ThemeSettingsShellSection" }, { kind: "component", type: ThemeSettingsThemeSectionComponent, selector: "ThemeSettingsThemeSection" }] });
|
|
1306
|
+
}
|
|
1307
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ThemeSettingsComponent, decorators: [{
|
|
1308
|
+
type: Component,
|
|
1309
|
+
args: [{
|
|
1310
|
+
selector: 'ThemeSettings',
|
|
1311
|
+
imports: [
|
|
1312
|
+
DrawerCloseDirective,
|
|
1313
|
+
DrawerComponent,
|
|
1314
|
+
DrawerContentComponent,
|
|
1315
|
+
DrawerHeaderComponent,
|
|
1316
|
+
DrawerTitleComponent,
|
|
1317
|
+
IconComponent,
|
|
1318
|
+
ThemeSettingsLayoutSectionComponent,
|
|
1319
|
+
ThemeSettingsNavigationSectionComponent,
|
|
1320
|
+
ThemeSettingsShellSectionComponent,
|
|
1321
|
+
ThemeSettingsThemeSectionComponent,
|
|
1322
|
+
],
|
|
1323
|
+
host: { class: 'contents' },
|
|
1324
|
+
template: `
|
|
1325
|
+
@if (showTrigger()) {
|
|
1326
|
+
<button
|
|
1327
|
+
type="button"
|
|
1328
|
+
[class]="triggerClasses()"
|
|
1329
|
+
[attr.aria-label]="triggerIconOnly() ? triggerLabel() : null"
|
|
1330
|
+
[attr.title]="triggerIconOnly() ? triggerLabel() : null"
|
|
1331
|
+
aria-haspopup="dialog"
|
|
1332
|
+
[attr.aria-expanded]="open()"
|
|
1333
|
+
(click)="open.set(true)"
|
|
1334
|
+
>
|
|
1335
|
+
<Icon name="palette" [size]="18" />
|
|
1336
|
+
@if (triggerIconOnly()) {
|
|
1337
|
+
<span class="sr-only">{{ triggerLabel() }}</span>
|
|
1338
|
+
} @else {
|
|
1339
|
+
<span>{{ triggerLabel() }}</span>
|
|
1340
|
+
}
|
|
1341
|
+
</button>
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
@if (open()) {
|
|
1345
|
+
<Drawer
|
|
1346
|
+
[(open)]="open"
|
|
1347
|
+
[side]="side()"
|
|
1348
|
+
[showCloseButton]="false"
|
|
1349
|
+
class="ThemeSettings w-full max-w-md gap-0! p-0! sm:w-100"
|
|
1350
|
+
>
|
|
1351
|
+
<DrawerHeader
|
|
1352
|
+
class="flex h-(--layout-topbar-height) shrink-0 flex-row items-center gap-3 border-b border-border bg-background px-3 text-left"
|
|
1353
|
+
>
|
|
1354
|
+
<span
|
|
1355
|
+
aria-hidden="true"
|
|
1356
|
+
class="grid size-8 shrink-0 place-items-center rounded-md bg-primary/10 text-primary ring-1 ring-primary/20"
|
|
1357
|
+
><Icon name="palette" [size]="18"
|
|
1358
|
+
/></span>
|
|
1359
|
+
<div class="min-w-0 flex-1">
|
|
1360
|
+
<DrawerTitle>Theme settings</DrawerTitle>
|
|
1361
|
+
</div>
|
|
1362
|
+
<button
|
|
1363
|
+
type="button"
|
|
1364
|
+
DrawerClose
|
|
1365
|
+
class="inline-flex h-8 shrink-0 items-center rounded-md bg-primary px-2 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background motion-reduce:transition-none"
|
|
1366
|
+
>
|
|
1367
|
+
Done
|
|
1368
|
+
</button>
|
|
1369
|
+
</DrawerHeader>
|
|
1370
|
+
|
|
1371
|
+
<DrawerContent class="min-h-0 px-4 py-5 sm:px-5">
|
|
1372
|
+
<div class="flex flex-col gap-8">
|
|
1373
|
+
<ThemeSettingsThemeSection />
|
|
1374
|
+
<ThemeSettingsShellSection />
|
|
1375
|
+
@if (settings) {
|
|
1376
|
+
<ThemeSettingsLayoutSection />
|
|
1377
|
+
<ThemeSettingsNavigationSection />
|
|
1378
|
+
}
|
|
1379
|
+
</div>
|
|
1380
|
+
</DrawerContent>
|
|
1381
|
+
</Drawer>
|
|
1382
|
+
}
|
|
1383
|
+
`,
|
|
1384
|
+
}]
|
|
1385
|
+
}], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], trigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "trigger", required: false }] }], triggerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabel", required: false }] }], triggerIconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "trigger-icon-only", required: false }] }], showTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "show-trigger", required: false }] }], side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
|
|
4
1386
|
|
|
5
1387
|
/**
|
|
6
1388
|
* Generated bundle index. Do not edit.
|
|
7
|
-
*/
|
|
1389
|
+
*/
|
|
1390
|
+
|
|
1391
|
+
export { NAV_TYPE_MODE_STORAGE_KEY, NAV_TYPE_STORAGE_KEY, THEME_SETTINGS_ADAPTER, ThemeSettingsComponent, ThemeSettingsDefaultAdapter, allowedNavTypeModes, allowedNavTypes$1 as allowedNavTypes, isWidthEnabled, provideThemeSettingsAdapter };
|