@cavulsqa/create 2.9.3 → 2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/bin/create.mjs +18 -25
- package/lib/templates.mjs +56 -0
- package/package.json +13 -5
- package/templates/f7-app/auto-imports.d.ts +9 -0
- package/templates/f7-app/package.json +6 -3
- package/templates/f7-app/src/env.d.ts +1 -0
- package/templates/f7-app/src/main.ts +3 -0
- package/templates/f7-app/src/plugins/bootstrapError.ts +2 -0
- package/templates/f7-app/src/plugins/recorder.plugin.ts +155 -0
- package/templates/f7-app/src/shared/recorder/capacitorSink.ts +124 -0
- package/templates/f7-app/vite.config.ts +1 -0
- package/templates/m3e-app/.claude/rules/data-fetching.md +68 -0
- package/templates/m3e-app/.claude/rules/database.md +106 -0
- package/templates/m3e-app/.claude/rules/m3e-ui.md +48 -0
- package/templates/m3e-app/.claude/rules/modules.md +43 -0
- package/templates/m3e-app/.claude/rules/native.md +60 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/SKILL.md +68 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/color.md +44 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/components.md +325 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/layout.md +42 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/motion.md +51 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/shapes-type.md +48 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/sources.md +53 -0
- package/templates/m3e-app/.claude/skills/module-architecture/SKILL.md +65 -0
- package/templates/m3e-app/.claude/skills/module-architecture/file-templates.md +178 -0
- package/templates/m3e-app/.claude/skills/reactive-data/SKILL.md +90 -0
- package/templates/m3e-app/.claude/skills/reactive-data/testing.md +55 -0
- package/templates/m3e-app/.env.example +32 -0
- package/templates/m3e-app/CLAUDE.md +93 -0
- package/templates/m3e-app/auto-imports.d.ts +851 -0
- package/templates/m3e-app/capacitor.config.ts +44 -0
- package/templates/m3e-app/components.d.ts +252 -0
- package/templates/m3e-app/index.html +16 -0
- package/templates/m3e-app/package.json +108 -0
- package/templates/m3e-app/src/App.vue +129 -0
- package/templates/m3e-app/src/app/pragmas.config.ts +35 -0
- package/templates/m3e-app/src/app/scroll.config.ts +20 -0
- package/templates/m3e-app/src/app/storage.config.ts +65 -0
- package/templates/m3e-app/src/app/tabs.ts +33 -0
- package/templates/m3e-app/src/app/theme.config.ts +51 -0
- package/templates/m3e-app/src/assets/css/app.css +18 -0
- package/templates/m3e-app/src/assets/css/base.css +53 -0
- package/templates/m3e-app/src/assets/css/layout/container-transform.css +113 -0
- package/templates/m3e-app/src/assets/css/layout/shell.css +69 -0
- package/templates/m3e-app/src/assets/css/layout/transitions.css +121 -0
- package/templates/m3e-app/src/assets/css/m3e.css +6 -0
- package/templates/m3e-app/src/assets/css/theme/framework7.css +18 -0
- package/templates/m3e-app/src/assets/css/theme/tailwind.css +365 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.dataset.ts +209 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.suite.ts +662 -0
- package/templates/m3e-app/src/domains/sales/sales.repository.ts +458 -0
- package/templates/m3e-app/src/env.d.ts +40 -0
- package/templates/m3e-app/src/locales/ar.json +1260 -0
- package/templates/m3e-app/src/locales/en.json +1260 -0
- package/templates/m3e-app/src/locales/fr.json +1260 -0
- package/templates/m3e-app/src/main.ts +41 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBenchmark.vue +119 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBusLog.vue +63 -0
- package/templates/m3e-app/src/modules/demo/components/DemoCreateOrderSheet.vue +203 -0
- package/templates/m3e-app/src/modules/demo/components/DemoMetricsSheet.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoOrderList.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoPipelineBenchmark.vue +65 -0
- package/templates/m3e-app/src/modules/demo/components/DemoStatCards.vue +71 -0
- package/templates/m3e-app/src/modules/demo/composables/useBenchmark.ts +139 -0
- package/templates/m3e-app/src/modules/demo/composables/useOrderStatus.ts +64 -0
- package/templates/m3e-app/src/modules/demo/composables/useReactiveDemo.ts +171 -0
- package/templates/m3e-app/src/modules/demo/router/routes/demo.routes.ts +22 -0
- package/templates/m3e-app/src/modules/demo/views/DemoView.vue +210 -0
- package/templates/m3e-app/src/modules/demo/views/OrderDetailView.vue +136 -0
- package/templates/m3e-app/src/modules/demo/views/OrderSearchView.vue +74 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryBlock.vue +16 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCarouselTile.vue +71 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCustomerForm.vue +76 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryProofOfDelivery.vue +69 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaDay.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaMonth.vue +40 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaVisitList.vue +45 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatAttachSheet.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatContactPicker.vue +85 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatInviteComposer.vue +131 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatPollComposer.vue +148 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatRecentPhotos.vue +62 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsAccount.vue +79 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsTags.vue +53 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsVerification.vue +73 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/PasswordStrength.vue +37 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryButtons.vue +128 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCarousels.vue +236 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCharts.vue +84 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryFabs.vue +70 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryInputs.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryNavigation.vue +141 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryOverlays.vue +368 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryPickers.vue +175 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryProgress.vue +118 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryScale.vue +103 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySelection.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryShapes.vue +48 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySurfaces.vue +213 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryTables.vue +185 -0
- package/templates/m3e-app/src/modules/gallery/components/surfaces/SurfacesContainerTransform.vue +46 -0
- package/templates/m3e-app/src/modules/gallery/composables/chatTeam.ts +18 -0
- package/templates/m3e-app/src/modules/gallery/composables/composeKind.ts +2 -0
- package/templates/m3e-app/src/modules/gallery/composables/currentPosition.ts +35 -0
- package/templates/m3e-app/src/modules/gallery/composables/demoOrders.ts +29 -0
- package/templates/m3e-app/src/modules/gallery/composables/featuredStories.ts +73 -0
- package/templates/m3e-app/src/modules/gallery/composables/fieldFormats.ts +36 -0
- package/templates/m3e-app/src/modules/gallery/composables/openExternal.ts +11 -0
- package/templates/m3e-app/src/modules/gallery/composables/routePlan.ts +90 -0
- package/templates/m3e-app/src/modules/gallery/composables/useAgenda.ts +61 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatCustomers.ts +31 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatDemo.ts +299 -0
- package/templates/m3e-app/src/modules/gallery/composables/useGallerySections.ts +230 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocalAttachments.ts +71 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocationShare.ts +38 -0
- package/templates/m3e-app/src/modules/gallery/composables/usePhotoScenes.ts +82 -0
- package/templates/m3e-app/src/modules/gallery/composables/useTreeDemo.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/composables/wilayas.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/router/routes/gallery.routes.ts +52 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryAgendaView.vue +117 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryChatView.vue +292 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryContactsView.vue +77 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryFeaturedView.vue +60 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryLoginView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryOnboardingView.vue +116 -0
- package/templates/m3e-app/src/modules/gallery/views/GallerySectionView.vue +27 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryTabsView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryView.vue +50 -0
- package/templates/m3e-app/src/modules/home/components/HomeHero.vue +57 -0
- package/templates/m3e-app/src/modules/home/composables/useHomeFeatures.ts +128 -0
- package/templates/m3e-app/src/modules/home/composables/useOpenFeature.ts +22 -0
- package/templates/m3e-app/src/modules/home/router/routes/home.routes.ts +21 -0
- package/templates/m3e-app/src/modules/home/views/FeatureDetailView.vue +66 -0
- package/templates/m3e-app/src/modules/home/views/HomeView.vue +47 -0
- package/templates/m3e-app/src/modules/settings/components/RolePalette.vue +38 -0
- package/templates/m3e-app/src/modules/settings/components/SeedSwatches.vue +41 -0
- package/templates/m3e-app/src/modules/settings/components/SettingsChoice.vue +43 -0
- package/templates/m3e-app/src/modules/settings/components/StudioPreview.vue +52 -0
- package/templates/m3e-app/src/modules/settings/router/routes/settings.routes.ts +17 -0
- package/templates/m3e-app/src/modules/settings/types.ts +7 -0
- package/templates/m3e-app/src/modules/settings/views/ColorStudioView.vue +166 -0
- package/templates/m3e-app/src/modules/settings/views/SettingsView.vue +157 -0
- package/templates/m3e-app/src/plugins/bootstrapError.ts +62 -0
- package/templates/m3e-app/src/plugins/capacitor/index.ts +14 -0
- package/templates/m3e-app/src/plugins/capacitor/useAndroidBackButton.ts +36 -0
- package/templates/m3e-app/src/plugins/capacitor/useKeyboard.ts +48 -0
- package/templates/m3e-app/src/plugins/capacitor/useSplashScreen.ts +43 -0
- package/templates/m3e-app/src/plugins/capacitor/useStatusBar.ts +16 -0
- package/templates/m3e-app/src/plugins/framework7.plugin.ts +36 -0
- package/templates/m3e-app/src/plugins/i18n.plugin.ts +87 -0
- package/templates/m3e-app/src/plugins/m3e.plugin.ts +22 -0
- package/templates/m3e-app/src/plugins/seed.plugin.ts +7 -0
- package/templates/m3e-app/src/plugins/sqlite.plugin.ts +32 -0
- package/templates/m3e-app/src/router/global/global.routes.ts +12 -0
- package/templates/m3e-app/src/router/index.ts +20 -0
- package/templates/m3e-app/src/shared/components/error/404.vue +13 -0
- package/templates/m3e-app/src/shared/components/layout/EmptyState.vue +30 -0
- package/templates/m3e-app/src/shared/components/layout/SectionHeader.vue +10 -0
- package/templates/m3e-app/src/shared/components/page/AppPage.vue +104 -0
- package/templates/m3e-app/src/shared/composables/navigation/useActiveTab.ts +39 -0
- package/templates/m3e-app/src/shared/composables/navigation/useContainerTransform.ts +117 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationGuard.ts +48 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationVisibility.ts +64 -0
- package/templates/m3e-app/src/shared/composables/navigation/useViewRouter.ts +25 -0
- package/templates/m3e-app/src/shared/composables/navigation/useWindowClass.ts +22 -0
- package/templates/m3e-app/src/shared/composables/theme/useThemeSettings.ts +155 -0
- package/templates/m3e-app/src/shared/database/candidates/capacitorSqlite.ts +36 -0
- package/templates/m3e-app/src/shared/database/candidates/index.ts +4 -0
- package/templates/m3e-app/src/shared/database/candidates/opfsSahPool.ts +25 -0
- package/templates/m3e-app/src/shared/database/candidates/types.ts +56 -0
- package/templates/m3e-app/src/shared/database/candidates/waSqlite.ts +56 -0
- package/templates/m3e-app/src/shared/database/database.ts +219 -0
- package/templates/m3e-app/src/shared/database/index.ts +3 -0
- package/templates/m3e-app/src/shared/database/migrations.ts +223 -0
- package/templates/m3e-app/src/shared/database/opfs.worker.ts +5 -0
- package/templates/m3e-app/src/shared/database/queries.ts +26 -0
- package/templates/m3e-app/src/shared/database/schema.ts +153 -0
- package/templates/m3e-app/src/shared/database/storage.ts +64 -0
- package/templates/m3e-app/src/shared/database/wa.worker.ts +5 -0
- package/templates/m3e-app/src/shared/utils/lazyRoute.ts +41 -0
- package/templates/m3e-app/src/shared/utils/resolvers/resolvers.ts +42 -0
- package/templates/m3e-app/src/shared/utils/textDirection.ts +22 -0
- package/templates/m3e-app/src/shared/utils/theme/themeSettings.ts +59 -0
- package/templates/m3e-app/src/shared/utils/tone.ts +8 -0
- package/templates/m3e-app/tests/benchmark.suite.test.ts +104 -0
- package/templates/m3e-app/tests/containerTransform.test.ts +58 -0
- package/templates/m3e-app/tests/fieldFormats.test.ts +25 -0
- package/templates/m3e-app/tests/lazyRoute.test.ts +30 -0
- package/templates/m3e-app/tests/locales.test.ts +73 -0
- package/templates/m3e-app/tests/migrations.test.ts +74 -0
- package/templates/m3e-app/tests/navigationGuard.test.ts +82 -0
- package/templates/m3e-app/tests/openDatabase.test.ts +54 -0
- package/templates/m3e-app/tests/sales.repository.test.ts +329 -0
- package/templates/m3e-app/tests/storage.test.ts +117 -0
- package/templates/m3e-app/tests/textDirection.test.ts +11 -0
- package/templates/m3e-app/tooling/linkedPackages.ts +112 -0
- package/templates/m3e-app/tsconfig.json +21 -0
- package/templates/m3e-app/vite.config.ts +144 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { MaterialShapeName } from "@cavulsqa/m3e";
|
|
2
|
+
import type { Component } from "vue";
|
|
3
|
+
import BackIcon from "~icons/material-symbols/arrow-back-rounded";
|
|
4
|
+
import BoltIcon from "~icons/material-symbols/bolt-rounded";
|
|
5
|
+
import DatabaseIcon from "~icons/material-symbols/database-outline-rounded";
|
|
6
|
+
import KeyboardIcon from "~icons/material-symbols/keyboard-outline-rounded";
|
|
7
|
+
import MonitoringIcon from "~icons/material-symbols/monitoring-rounded";
|
|
8
|
+
import PaletteIcon from "~icons/material-symbols/palette-outline-rounded";
|
|
9
|
+
import ShapesIcon from "~icons/material-symbols/shapes-outline-rounded";
|
|
10
|
+
import WidgetsIcon from "~icons/material-symbols/widgets-outline-rounded";
|
|
11
|
+
import type { Tone } from "@/shared/utils/tone";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Framework7's own page transitions, one per feature so each push looks different. They ship in
|
|
15
|
+
* Framework7's core stylesheet, and Framework7 replays the one a page opened with when it goes back.
|
|
16
|
+
*/
|
|
17
|
+
export type PageTransition =
|
|
18
|
+
| "f7-circle"
|
|
19
|
+
| "f7-cover"
|
|
20
|
+
| "f7-cover-v"
|
|
21
|
+
| "f7-dive"
|
|
22
|
+
| "f7-fade"
|
|
23
|
+
| "f7-flip"
|
|
24
|
+
| "f7-parallax"
|
|
25
|
+
| "f7-push";
|
|
26
|
+
|
|
27
|
+
export interface HomeFeature {
|
|
28
|
+
id: string;
|
|
29
|
+
icon: Component;
|
|
30
|
+
shape: MaterialShapeName;
|
|
31
|
+
tone: Tone;
|
|
32
|
+
titleKey: string;
|
|
33
|
+
subtitleKey: string;
|
|
34
|
+
textKey: string;
|
|
35
|
+
transition: PageTransition;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The template's capabilities as data, so removing a layer means removing an entry and the screen
|
|
40
|
+
* never advertises something the generator left out. Each one opens inside the Home tab's own
|
|
41
|
+
* view, which is the nesting being demonstrated: the push stays in this tab's history.
|
|
42
|
+
*/
|
|
43
|
+
export const features: readonly HomeFeature[] = [
|
|
44
|
+
{
|
|
45
|
+
id: "expressive",
|
|
46
|
+
icon: ShapesIcon,
|
|
47
|
+
shape: "cookie9Sided",
|
|
48
|
+
tone: "primary",
|
|
49
|
+
titleKey: "features.expressive.title",
|
|
50
|
+
subtitleKey: "features.expressive.subtitle",
|
|
51
|
+
textKey: "features.expressive.text",
|
|
52
|
+
transition: "f7-cover",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "color",
|
|
56
|
+
icon: PaletteIcon,
|
|
57
|
+
shape: "clover4Leaf",
|
|
58
|
+
tone: "tertiary",
|
|
59
|
+
titleKey: "features.color.title",
|
|
60
|
+
subtitleKey: "features.color.subtitle",
|
|
61
|
+
textKey: "features.color.text",
|
|
62
|
+
transition: "f7-circle",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "shell",
|
|
66
|
+
icon: WidgetsIcon,
|
|
67
|
+
shape: "pentagon",
|
|
68
|
+
tone: "secondary",
|
|
69
|
+
titleKey: "features.shell.title",
|
|
70
|
+
subtitleKey: "features.shell.subtitle",
|
|
71
|
+
textKey: "features.shell.text",
|
|
72
|
+
transition: "f7-parallax",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "sqlite",
|
|
76
|
+
icon: DatabaseIcon,
|
|
77
|
+
shape: "cookie4Sided",
|
|
78
|
+
tone: "primary",
|
|
79
|
+
titleKey: "features.sqlite.title",
|
|
80
|
+
subtitleKey: "features.sqlite.subtitle",
|
|
81
|
+
textKey: "features.sqlite.text",
|
|
82
|
+
transition: "f7-push",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "reactive",
|
|
86
|
+
icon: BoltIcon,
|
|
87
|
+
shape: "sunny",
|
|
88
|
+
tone: "tertiary",
|
|
89
|
+
titleKey: "features.reactive.title",
|
|
90
|
+
subtitleKey: "features.reactive.subtitle",
|
|
91
|
+
textKey: "features.reactive.text",
|
|
92
|
+
transition: "f7-dive",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "back",
|
|
96
|
+
icon: BackIcon,
|
|
97
|
+
shape: "pill",
|
|
98
|
+
tone: "secondary",
|
|
99
|
+
titleKey: "features.back.title",
|
|
100
|
+
subtitleKey: "features.back.subtitle",
|
|
101
|
+
textKey: "features.back.text",
|
|
102
|
+
transition: "f7-flip",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "keyboard",
|
|
106
|
+
icon: KeyboardIcon,
|
|
107
|
+
shape: "gem",
|
|
108
|
+
tone: "primary",
|
|
109
|
+
titleKey: "features.keyboard.title",
|
|
110
|
+
subtitleKey: "features.keyboard.subtitle",
|
|
111
|
+
textKey: "features.keyboard.text",
|
|
112
|
+
transition: "f7-fade",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "metrics",
|
|
116
|
+
icon: MonitoringIcon,
|
|
117
|
+
shape: "flower",
|
|
118
|
+
tone: "tertiary",
|
|
119
|
+
titleKey: "features.metrics.title",
|
|
120
|
+
subtitleKey: "features.metrics.subtitle",
|
|
121
|
+
textKey: "features.metrics.text",
|
|
122
|
+
transition: "f7-cover-v",
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
export function findFeature(id: string): HomeFeature | undefined {
|
|
127
|
+
return features.find((feature) => feature.id === id);
|
|
128
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Router } from "framework7/types";
|
|
2
|
+
import type { HomeFeature } from "@/modules/home/composables/useHomeFeatures";
|
|
3
|
+
|
|
4
|
+
/** The transition's name without Framework7's prefix, as the rows and the detail page show it. */
|
|
5
|
+
export function transitionName(feature: HomeFeature): string {
|
|
6
|
+
return feature.transition.replace(/^f7-/, "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Opens a feature's page inside the Home tab with that feature's own transition. Under reduced
|
|
11
|
+
* motion it falls back to the app's default, which is a cross-fade there: a flip or a circle reveal
|
|
12
|
+
* is exactly the movement reduced motion asks to remove.
|
|
13
|
+
*/
|
|
14
|
+
export function useOpenFeature() {
|
|
15
|
+
const reduced = useReducedMotion();
|
|
16
|
+
|
|
17
|
+
return (router: Router.Router, feature: HomeFeature) =>
|
|
18
|
+
router.navigate(
|
|
19
|
+
`/home/feature/${feature.id}/`,
|
|
20
|
+
reduced.value ? {} : { transition: feature.transition },
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Router } from "framework7/types";
|
|
2
|
+
import { lazyRoute } from "@/shared/utils/lazyRoute";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The detail route is a child of the tab's own view, which is what keeps the tab bar in place and
|
|
6
|
+
* the back gesture scoped to this tab's history.
|
|
7
|
+
*/
|
|
8
|
+
const homeRoutes: Router.RouteParameters[] = [
|
|
9
|
+
{
|
|
10
|
+
name: "home",
|
|
11
|
+
path: "/home/",
|
|
12
|
+
async: lazyRoute(() => import("@/modules/home/views/HomeView.vue")),
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "home-feature",
|
|
16
|
+
path: "/home/feature/:id/",
|
|
17
|
+
async: lazyRoute(() => import("@/modules/home/views/FeatureDetailView.vue")),
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export default homeRoutes;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<AppPage :title="feature ? t(feature.titleKey) : t('errors.notFoundTitle')" variant="medium" back>
|
|
3
|
+
<template v-if="feature">
|
|
4
|
+
<section class="flex flex-col items-center gap-4 px-6 pt-4 pb-2 text-center">
|
|
5
|
+
<M3Shape :shape="feature.shape" class="size-32" :class="TONE_CLASSES[feature.tone]">
|
|
6
|
+
<component :is="feature.icon" class="size-12" />
|
|
7
|
+
</M3Shape>
|
|
8
|
+
<p class="type-title-medium m-0 text-on-surface-variant">{{ t(feature.subtitleKey) }}</p>
|
|
9
|
+
<p class="type-body-large m-0 max-w-[36rem] text-on-surface">{{ t(feature.textKey) }}</p>
|
|
10
|
+
</section>
|
|
11
|
+
|
|
12
|
+
<SectionHeader :title="t('detail.thisScreen')" />
|
|
13
|
+
<M3List variant="segmented" inset>
|
|
14
|
+
<M3ListItem
|
|
15
|
+
:headline="t('detail.transition')"
|
|
16
|
+
:supporting="t('detail.transitionNote', { name: transitionName(feature) })"
|
|
17
|
+
:trailing-text="transitionName(feature)"
|
|
18
|
+
multiline
|
|
19
|
+
>
|
|
20
|
+
<template #leading><i-ms-swipe-rounded /></template>
|
|
21
|
+
</M3ListItem>
|
|
22
|
+
<M3ListItem :headline="t('detail.appBar')" :supporting="t('detail.appBarNote')" multiline>
|
|
23
|
+
<template #leading><i-ms-web-rounded /></template>
|
|
24
|
+
</M3ListItem>
|
|
25
|
+
<M3ListItem :headline="t('detail.nesting')" :supporting="t('detail.nestingNote')" multiline>
|
|
26
|
+
<template #leading><i-ms-widgets-outline-rounded /></template>
|
|
27
|
+
</M3ListItem>
|
|
28
|
+
</M3List>
|
|
29
|
+
|
|
30
|
+
<div class="flex justify-end px-4 pt-6">
|
|
31
|
+
<M3Button variant="tonal" size="m" @click="openNext">
|
|
32
|
+
{{ t("detail.next") }}
|
|
33
|
+
<template #trailing><i-ms-chevron-right-rounded class="rtl:-scale-x-100" /></template>
|
|
34
|
+
</M3Button>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<EmptyState
|
|
39
|
+
v-else
|
|
40
|
+
shape="ghostish"
|
|
41
|
+
:headline="t('errors.notFoundTitle')"
|
|
42
|
+
:text="t('errors.notFound')"
|
|
43
|
+
/>
|
|
44
|
+
</AppPage>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<script setup lang="ts">
|
|
48
|
+
import type { Router } from "framework7/types";
|
|
49
|
+
import { features, findFeature } from "@/modules/home/composables/useHomeFeatures";
|
|
50
|
+
import { transitionName, useOpenFeature } from "@/modules/home/composables/useOpenFeature";
|
|
51
|
+
import { TONE_CLASSES } from "@/shared/utils/tone";
|
|
52
|
+
|
|
53
|
+
const { t } = useI18n();
|
|
54
|
+
const props = defineProps<{ f7route: Router.Route; f7router: Router.Router }>();
|
|
55
|
+
|
|
56
|
+
const openFeature = useOpenFeature();
|
|
57
|
+
|
|
58
|
+
const feature = computed(() => findFeature(String(props.f7route.params.id ?? "")));
|
|
59
|
+
|
|
60
|
+
/** Pushes the next feature onto this tab's own history, so the stack grows inside the tab. */
|
|
61
|
+
function openNext() {
|
|
62
|
+
const index = features.findIndex((entry) => entry.id === feature.value?.id);
|
|
63
|
+
const next = features[(index + 1) % features.length];
|
|
64
|
+
if (next) openFeature(props.f7router, next);
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<AppPage :title="t('home.title')" name="home">
|
|
3
|
+
<template #actions>
|
|
4
|
+
<M3IconButton :label="t('home.studio')" @click="openStudio">
|
|
5
|
+
<i-ms-palette-outline-rounded />
|
|
6
|
+
</M3IconButton>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<HomeHero @explore="tabs.show('gallery')" @personalise="openStudio" />
|
|
10
|
+
|
|
11
|
+
<SectionHeader :title="t('home.included')" />
|
|
12
|
+
<M3List variant="segmented" inset :label="t('home.included')">
|
|
13
|
+
<M3ListItem
|
|
14
|
+
v-for="feature in features"
|
|
15
|
+
:key="feature.id"
|
|
16
|
+
clickable
|
|
17
|
+
:headline="t(feature.titleKey)"
|
|
18
|
+
:supporting="t(feature.subtitleKey)"
|
|
19
|
+
:trailing-text="transitionName(feature)"
|
|
20
|
+
@click="openFeature(f7router, feature)"
|
|
21
|
+
>
|
|
22
|
+
<template #leading>
|
|
23
|
+
<M3Shape :shape="feature.shape" class="size-10" :class="TONE_CLASSES[feature.tone]">
|
|
24
|
+
<component :is="feature.icon" class="size-5" />
|
|
25
|
+
</M3Shape>
|
|
26
|
+
</template>
|
|
27
|
+
<template #trailing><i-ms-chevron-right-rounded class="rtl:-scale-x-100" /></template>
|
|
28
|
+
</M3ListItem>
|
|
29
|
+
</M3List>
|
|
30
|
+
</AppPage>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import type { Router } from "framework7/types";
|
|
35
|
+
import HomeHero from "@/modules/home/components/HomeHero.vue";
|
|
36
|
+
import { features } from "@/modules/home/composables/useHomeFeatures";
|
|
37
|
+
import { transitionName, useOpenFeature } from "@/modules/home/composables/useOpenFeature";
|
|
38
|
+
import { TONE_CLASSES } from "@/shared/utils/tone";
|
|
39
|
+
|
|
40
|
+
const { t } = useI18n();
|
|
41
|
+
defineProps<{ f7router: Router.Router }>();
|
|
42
|
+
const tabs = useActiveTab();
|
|
43
|
+
const openFeature = useOpenFeature();
|
|
44
|
+
function openStudio() {
|
|
45
|
+
tabs.open("settings", "/settings/studio/");
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-2 px-4">
|
|
3
|
+
<div
|
|
4
|
+
v-for="role in ROLES"
|
|
5
|
+
:key="role.name"
|
|
6
|
+
class="flex min-h-18 flex-col justify-between rounded-md p-3"
|
|
7
|
+
:style="{
|
|
8
|
+
background: `var(--md-sys-color-${role.name})`,
|
|
9
|
+
color: `var(--md-sys-color-${role.on})`,
|
|
10
|
+
}"
|
|
11
|
+
>
|
|
12
|
+
<span class="type-label-large">{{ role.name }}</span>
|
|
13
|
+
<span class="type-label-small opacity-80">{{ role.on }}</span>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
/**
|
|
20
|
+
* The generated roles in pairs - each container with the content colour that must stay legible on
|
|
21
|
+
* it. A pair that reads poorly here reads poorly everywhere; raise the contrast level, not the
|
|
22
|
+
* individual colour.
|
|
23
|
+
*/
|
|
24
|
+
const ROLES = [
|
|
25
|
+
{ name: "primary", on: "on-primary" },
|
|
26
|
+
{ name: "primary-container", on: "on-primary-container" },
|
|
27
|
+
{ name: "secondary", on: "on-secondary" },
|
|
28
|
+
{ name: "secondary-container", on: "on-secondary-container" },
|
|
29
|
+
{ name: "tertiary", on: "on-tertiary" },
|
|
30
|
+
{ name: "tertiary-container", on: "on-tertiary-container" },
|
|
31
|
+
{ name: "error-container", on: "on-error-container" },
|
|
32
|
+
{ name: "success-container", on: "on-success-container" },
|
|
33
|
+
{ name: "surface-container-low", on: "on-surface" },
|
|
34
|
+
{ name: "surface-container-highest", on: "on-surface-variant" },
|
|
35
|
+
{ name: "inverse-surface", on: "inverse-on-surface" },
|
|
36
|
+
{ name: "primary-fixed", on: "on-primary-fixed" },
|
|
37
|
+
] as const;
|
|
38
|
+
</script>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="grid grid-cols-5 gap-3 px-4" role="radiogroup" :aria-label="t('studio.seed')">
|
|
3
|
+
<button
|
|
4
|
+
v-for="preset in SEED_PRESETS"
|
|
5
|
+
:key="preset.id"
|
|
6
|
+
type="button"
|
|
7
|
+
role="radio"
|
|
8
|
+
class="m3-focus-ring grid aspect-square place-items-center rounded-full"
|
|
9
|
+
:aria-checked="isSelected(preset.seed)"
|
|
10
|
+
:aria-label="t(preset.labelKey)"
|
|
11
|
+
:style="{ color: preset.seed }"
|
|
12
|
+
@click="emit('select', preset.seed)"
|
|
13
|
+
>
|
|
14
|
+
<M3ShapeMorph
|
|
15
|
+
:shape="isSelected(preset.seed) ? 'cookie9Sided' : 'circle'"
|
|
16
|
+
:rotate="isSelected(preset.seed) ? 30 : 0"
|
|
17
|
+
class="size-full max-w-14"
|
|
18
|
+
>
|
|
19
|
+
<i-ms-check-rounded
|
|
20
|
+
class="size-6 text-white transition-opacity duration-spring-fast ease-fast-effects"
|
|
21
|
+
:class="isSelected(preset.seed) ? 'opacity-100' : 'opacity-0'"
|
|
22
|
+
/>
|
|
23
|
+
</M3ShapeMorph>
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup lang="ts">
|
|
29
|
+
import { SEED_PRESETS } from "@/app/theme.config";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The preset seeds as shapes that morph from a circle into a cookie when chosen. The swatch is the
|
|
33
|
+
* raw seed, the one place a hex value reaches the screen, because the point is to show the input
|
|
34
|
+
* before Material turns it into a scheme.
|
|
35
|
+
*/
|
|
36
|
+
const props = defineProps<{ selected: string }>();
|
|
37
|
+
const emit = defineEmits<{ select: [seed: string] }>();
|
|
38
|
+
const { t } = useI18n();
|
|
39
|
+
|
|
40
|
+
const isSelected = (seed: string) => seed.toLowerCase() === props.selected.toLowerCase();
|
|
41
|
+
</script>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="flex flex-col gap-3 rounded-xs bg-surface-container-low px-4 py-4 first:rounded-t-lg last:rounded-b-lg"
|
|
4
|
+
>
|
|
5
|
+
<div class="flex items-center gap-3">
|
|
6
|
+
<span class="text-on-surface-variant [&>svg]:size-6"><slot name="icon" /></span>
|
|
7
|
+
<div class="flex min-w-0 flex-col">
|
|
8
|
+
<span class="type-body-large text-on-surface">{{ props.label }}</span>
|
|
9
|
+
<span v-if="props.supporting" class="type-body-medium text-on-surface-variant">{{
|
|
10
|
+
props.supporting
|
|
11
|
+
}}</span>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<M3ButtonGroup variant="connected" size="s" :label="props.label">
|
|
15
|
+
<M3Button
|
|
16
|
+
v-for="option in props.options"
|
|
17
|
+
:key="option.value"
|
|
18
|
+
variant="tonal"
|
|
19
|
+
toggle
|
|
20
|
+
:selected="model === option.value"
|
|
21
|
+
@update:selected="model = option.value"
|
|
22
|
+
>
|
|
23
|
+
<template v-if="option.icon" #icon><component :is="option.icon" /></template>
|
|
24
|
+
{{ option.label }}
|
|
25
|
+
</M3Button>
|
|
26
|
+
</M3ButtonGroup>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts" generic="T extends string">
|
|
31
|
+
import type { ChoiceOption } from "@/modules/settings/types";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One setting with a few exclusive choices: a label and a connected button group, the M3
|
|
35
|
+
* Expressive replacement for segmented buttons. Sits in a segmented stack with its siblings.
|
|
36
|
+
*/
|
|
37
|
+
const props = defineProps<{
|
|
38
|
+
label: string;
|
|
39
|
+
supporting?: string;
|
|
40
|
+
options: readonly ChoiceOption<T>[];
|
|
41
|
+
}>();
|
|
42
|
+
const model = defineModel<T>({ required: true });
|
|
43
|
+
</script>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section
|
|
3
|
+
class="mx-4 flex flex-col gap-4 rounded-xl bg-surface-container-low p-5 text-on-surface shadow-1"
|
|
4
|
+
:aria-label="t('studio.preview')"
|
|
5
|
+
>
|
|
6
|
+
<header class="flex items-center gap-3">
|
|
7
|
+
<M3Shape shape="clover4Leaf" class="size-12 bg-tertiary-container text-on-tertiary-container">
|
|
8
|
+
<i-ms-auto-awesome-outline-rounded class="size-6" />
|
|
9
|
+
</M3Shape>
|
|
10
|
+
<div class="flex min-w-0 flex-1 flex-col">
|
|
11
|
+
<span class="type-title-medium-emphasized">{{ t("studio.previewTitle") }}</span>
|
|
12
|
+
<span class="type-body-medium text-on-surface-variant">{{ t("studio.previewText") }}</span>
|
|
13
|
+
</div>
|
|
14
|
+
<M3Switch v-model="on" :label="t('studio.previewSwitch')" />
|
|
15
|
+
</header>
|
|
16
|
+
|
|
17
|
+
<M3LinearProgress :value="0.62" :label="t('studio.previewProgress')" />
|
|
18
|
+
|
|
19
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
20
|
+
<M3Chip
|
|
21
|
+
kind="filter"
|
|
22
|
+
:label="t('studio.previewChip')"
|
|
23
|
+
:selected="on"
|
|
24
|
+
@update:selected="on = $event"
|
|
25
|
+
/>
|
|
26
|
+
<M3Chip :label="t('studio.previewAssist')">
|
|
27
|
+
<template #icon><i-ms-bolt-rounded /></template>
|
|
28
|
+
</M3Chip>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="flex items-center gap-2">
|
|
32
|
+
<M3ButtonGroup size="s" :label="t('studio.preview')">
|
|
33
|
+
<M3Button>{{ t("studio.previewPrimary") }}</M3Button>
|
|
34
|
+
<M3Button variant="tonal">{{ t("studio.previewTonal") }}</M3Button>
|
|
35
|
+
</M3ButtonGroup>
|
|
36
|
+
<span class="flex-1" />
|
|
37
|
+
<M3LoadingIndicator contained :size="40" :label="t('studio.previewLoading')" />
|
|
38
|
+
<M3Fab size="small" :label="t('studio.previewFab')" color="tertiary-container">
|
|
39
|
+
<i-ms-add-rounded />
|
|
40
|
+
</M3Fab>
|
|
41
|
+
</div>
|
|
42
|
+
</section>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script setup lang="ts">
|
|
46
|
+
/**
|
|
47
|
+
* A handful of live components in the generated scheme, so a seed or variant change is judged on
|
|
48
|
+
* real surfaces - containers, an active wave, a selected chip - rather than on swatches alone.
|
|
49
|
+
*/
|
|
50
|
+
const { t } = useI18n();
|
|
51
|
+
const on = ref(true);
|
|
52
|
+
</script>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Router } from "framework7/types";
|
|
2
|
+
import { lazyRoute } from "@/shared/utils/lazyRoute";
|
|
3
|
+
|
|
4
|
+
const settingsRoutes: Router.RouteParameters[] = [
|
|
5
|
+
{
|
|
6
|
+
name: "settings",
|
|
7
|
+
path: "/settings/",
|
|
8
|
+
async: lazyRoute(() => import("@/modules/settings/views/SettingsView.vue")),
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: "settings-studio",
|
|
12
|
+
path: "/settings/studio/",
|
|
13
|
+
async: lazyRoute(() => import("@/modules/settings/views/ColorStudioView.vue")),
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export default settingsRoutes;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<AppPage :title="t('studio.title')" variant="medium" back>
|
|
3
|
+
<template #actions>
|
|
4
|
+
<M3IconButton :label="t('studio.reset')" @click="reset">
|
|
5
|
+
<i-ms-restart-alt-rounded />
|
|
6
|
+
</M3IconButton>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<StudioPreview />
|
|
10
|
+
|
|
11
|
+
<SectionHeader :title="t('studio.seed')" />
|
|
12
|
+
<SeedSwatches :selected="settings.seed" @select="(seed) => theme.update({ seed })" />
|
|
13
|
+
|
|
14
|
+
<div class="flex items-start gap-2 px-4 pt-4">
|
|
15
|
+
<M3TextField
|
|
16
|
+
v-model="hex"
|
|
17
|
+
class="flex-1"
|
|
18
|
+
:label="t('studio.custom')"
|
|
19
|
+
:error="hexError"
|
|
20
|
+
:supporting="t('studio.customHint')"
|
|
21
|
+
autocapitalize="off"
|
|
22
|
+
autocomplete="off"
|
|
23
|
+
spellcheck="false"
|
|
24
|
+
>
|
|
25
|
+
<template #leading>
|
|
26
|
+
<span
|
|
27
|
+
class="block size-6 rounded-full"
|
|
28
|
+
:style="{ background: hexValid ? normalizedHex : 'transparent' }"
|
|
29
|
+
/>
|
|
30
|
+
</template>
|
|
31
|
+
</M3TextField>
|
|
32
|
+
<M3IconButton
|
|
33
|
+
variant="tonal"
|
|
34
|
+
size="m"
|
|
35
|
+
class="mt-2"
|
|
36
|
+
:label="t('studio.pick')"
|
|
37
|
+
@click="openPicker"
|
|
38
|
+
>
|
|
39
|
+
<i-ms-tune-rounded />
|
|
40
|
+
</M3IconButton>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<template #fixed>
|
|
44
|
+
<M3BottomSheet v-model:open="pickerOpen" :title="t('studio.pick')">
|
|
45
|
+
<div class="flex flex-col gap-6 px-6 pb-6">
|
|
46
|
+
<M3ColorPicker
|
|
47
|
+
v-model="draftSeed"
|
|
48
|
+
:label="t('studio.pick')"
|
|
49
|
+
:hue-label="t('studio.hue')"
|
|
50
|
+
:chroma-label="t('studio.chroma')"
|
|
51
|
+
:tone-label="t('studio.tone')"
|
|
52
|
+
:hex-label="t('studio.hex')"
|
|
53
|
+
:invalid-text="t('studio.customInvalid')"
|
|
54
|
+
/>
|
|
55
|
+
<p class="type-body-small m-0 text-on-surface-variant">{{ t("studio.pickNote") }}</p>
|
|
56
|
+
<div class="flex justify-end gap-2">
|
|
57
|
+
<M3Button variant="text" @click="pickerOpen = false">{{ t("studio.cancel") }}</M3Button>
|
|
58
|
+
<M3Button @click="applyPicked">{{ t("studio.apply") }}</M3Button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</M3BottomSheet>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<SectionHeader :title="t('studio.variant')" />
|
|
65
|
+
<div class="flex flex-wrap gap-2 px-4" role="radiogroup" :aria-label="t('studio.variant')">
|
|
66
|
+
<M3Chip
|
|
67
|
+
v-for="variant in STUDIO_VARIANTS"
|
|
68
|
+
:key="variant"
|
|
69
|
+
kind="filter"
|
|
70
|
+
:label="t(`studio.variants.${variant}`)"
|
|
71
|
+
:selected="settings.variant === variant"
|
|
72
|
+
@update:selected="theme.update({ variant })"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
<p class="type-body-small m-0 px-6 pt-2 text-on-surface-variant">
|
|
76
|
+
{{ effectiveSpec(settings.variant) === "2025" ? t("studio.spec2025") : t("studio.spec2021") }}
|
|
77
|
+
</p>
|
|
78
|
+
|
|
79
|
+
<div class="mx-4 mt-6">
|
|
80
|
+
<SettingsChoice
|
|
81
|
+
:model-value="settings.contrast"
|
|
82
|
+
:label="t('studio.contrast')"
|
|
83
|
+
:supporting="t('studio.contrastNote')"
|
|
84
|
+
:options="contrastOptions"
|
|
85
|
+
@update:model-value="(contrast) => theme.update({ contrast })"
|
|
86
|
+
>
|
|
87
|
+
<template #icon><i-ms-contrast-rounded /></template>
|
|
88
|
+
</SettingsChoice>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<SectionHeader :title="t('studio.roles')" />
|
|
92
|
+
<RolePalette />
|
|
93
|
+
</AppPage>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<script setup lang="ts">
|
|
97
|
+
import { effectiveSpec, isHexColor } from "@cavulsqa/m3e";
|
|
98
|
+
import { STUDIO_VARIANTS } from "@/app/theme.config";
|
|
99
|
+
import RolePalette from "@/modules/settings/components/RolePalette.vue";
|
|
100
|
+
import SeedSwatches from "@/modules/settings/components/SeedSwatches.vue";
|
|
101
|
+
import SettingsChoice from "@/modules/settings/components/SettingsChoice.vue";
|
|
102
|
+
import StudioPreview from "@/modules/settings/components/StudioPreview.vue";
|
|
103
|
+
import type { ChoiceOption } from "@/modules/settings/types";
|
|
104
|
+
import type { ContrastChoice } from "@/shared/utils/theme/themeSettings";
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The colour studio: one seed, a scheme variant and a contrast level, and Material's 2025 colour
|
|
108
|
+
* system generates every role from them in both modes. Changes apply live and persist.
|
|
109
|
+
*
|
|
110
|
+
* @see https://m3.material.io/styles/color/choosing-a-scheme
|
|
111
|
+
*/
|
|
112
|
+
const { t } = useI18n();
|
|
113
|
+
const theme = useThemeSettings();
|
|
114
|
+
const settings = computed(() => theme.settings.value);
|
|
115
|
+
const snackbar = useSnackbar();
|
|
116
|
+
|
|
117
|
+
const hex = ref(settings.value.seed);
|
|
118
|
+
const normalizedHex = computed(() => (hex.value.startsWith("#") ? hex.value : `#${hex.value}`));
|
|
119
|
+
const hexValid = computed(() => isHexColor(hex.value));
|
|
120
|
+
const hexError = computed(() =>
|
|
121
|
+
hex.value.length >= 4 && !hexValid.value ? t("studio.customInvalid") : undefined,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
watch(normalizedHex, (value) => {
|
|
125
|
+
if (hexValid.value && value.toLowerCase() !== settings.value.seed.toLowerCase()) {
|
|
126
|
+
theme.update({ seed: value });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
watch(
|
|
131
|
+
() => settings.value.seed,
|
|
132
|
+
(seed) => {
|
|
133
|
+
if (seed.toLowerCase() !== normalizedHex.value.toLowerCase()) hex.value = seed;
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const contrastOptions = computed<ChoiceOption<ContrastChoice>[]>(() => [
|
|
138
|
+
{ value: "standard", label: t("studio.contrastStandard") },
|
|
139
|
+
{ value: "medium", label: t("studio.contrastMedium") },
|
|
140
|
+
{ value: "high", label: t("studio.contrastHigh") },
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
const pickerOpen = ref(false);
|
|
144
|
+
const draftSeed = ref(settings.value.seed);
|
|
145
|
+
|
|
146
|
+
function openPicker() {
|
|
147
|
+
draftSeed.value = settings.value.seed;
|
|
148
|
+
pickerOpen.value = true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function applyPicked() {
|
|
152
|
+
hex.value = draftSeed.value;
|
|
153
|
+
pickerOpen.value = false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function reset() {
|
|
157
|
+
const previous = { ...settings.value };
|
|
158
|
+
theme.reset();
|
|
159
|
+
const result = await snackbar.show({
|
|
160
|
+
message: t("studio.resetDone"),
|
|
161
|
+
action: t("studio.undo"),
|
|
162
|
+
duration: "long",
|
|
163
|
+
});
|
|
164
|
+
if (result === "action") theme.update(previous);
|
|
165
|
+
}
|
|
166
|
+
</script>
|