@stackonward/cli 1.3.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +248 -0
  2. package/dist/index.js +14124 -635
  3. package/dist/index.js.map +1 -1
  4. package/dist/resources/components/nuxt/site-source/app/app.vue +4 -2
  5. package/dist/resources/components/nuxt/site-source/app/assets/css/site-shell.css +50 -50
  6. package/dist/resources/components/nuxt/site-source/app/components/{site/ApplicationHeader.vue → app/Header.vue} +3 -3
  7. package/dist/resources/components/nuxt/site-source/app/components/{site/ApplicationShell.vue → app/Shell.vue} +2 -2
  8. package/dist/resources/components/nuxt/site-source/app/components/{site/ApplicationSidebar.vue → app/Sidebar.vue} +2 -2
  9. package/dist/resources/components/nuxt/site-source/app/components/{ui/CheckoutStatus.vue → checkout/Status.vue} +8 -3
  10. package/dist/resources/components/nuxt/site-source/app/components/{ui/CheckoutSummary.vue → checkout/Summary.vue} +11 -11
  11. package/dist/resources/components/nuxt/site-source/app/components/{site/CookieConsent.vue → consent/Banner.vue} +3 -3
  12. package/dist/resources/components/nuxt/site-source/app/components/{site/ContentArticlePage.vue → content/Article.vue} +3 -3
  13. package/dist/resources/components/nuxt/site-source/app/components/{site/ContentListPage.vue → content/List.vue} +2 -2
  14. package/dist/resources/components/nuxt/site-source/app/components/{ui/GoogleSignInButton.vue → google/SignInButton.vue} +3 -3
  15. package/dist/resources/components/nuxt/site-source/app/components/{site/LocaleSwitcher.vue → locale/Menu.vue} +2 -2
  16. package/dist/resources/components/nuxt/site-source/app/components/site/{MarketingFooter.vue → Footer.vue} +8 -8
  17. package/dist/resources/components/nuxt/site-source/app/components/site/{MarketingHeader.vue → Header.vue} +27 -27
  18. package/dist/resources/components/nuxt/site-source/app/components/site/Shell.vue +29 -0
  19. package/dist/resources/components/nuxt/site-source/app/components/{site/ThemeSwitcher.vue → theme/Menu.vue} +2 -2
  20. package/dist/resources/components/nuxt/site-source/app/components/ui/{BaseButton.vue → Button.vue} +1 -8
  21. package/dist/resources/components/nuxt/site-source/app/components/ui/InfiniteScroll.vue +2 -2
  22. package/dist/resources/components/nuxt/site-source/app/components/{ui/YoutubeEmbed.vue → youtube/Embed.vue} +10 -2
  23. package/dist/resources/components/nuxt/site-source/app/composables/useConsent.ts +8 -10
  24. package/dist/resources/components/nuxt/site-source/app/composables/useNavigation.ts +9 -14
  25. package/dist/resources/components/nuxt/site-source/app/composables/useSeo.ts +3 -1
  26. package/dist/resources/components/nuxt/site-source/app/composables/useSiteShell.ts +19 -22
  27. package/dist/resources/components/nuxt/site-source/app/consent/types.ts +59 -0
  28. package/dist/resources/components/nuxt/site-source/app/content/types.ts +17 -0
  29. package/dist/resources/components/nuxt/site-source/app/layouts/app.vue +2 -2
  30. package/dist/resources/components/nuxt/site-source/app/layouts/default.vue +2 -2
  31. package/dist/resources/components/nuxt/site-source/app/navigation/types.ts +68 -0
  32. package/dist/resources/components/nuxt/site-source/app/plugins/consent.ts +2 -2
  33. package/dist/resources/components/nuxt/site-source/app/site/config.ts +35 -0
  34. package/dist/resources/components/registry.json +63 -51
  35. package/dist/resources/deploy/ci/github/caller.yml +38 -0
  36. package/dist/resources/deploy/ci/onedev/import.yml +14 -0
  37. package/dist/resources/deploy/nginx/cache.conf +17 -0
  38. package/dist/resources/deploy/nginx/compression.conf +23 -0
  39. package/dist/resources/deploy/nginx/proxy.conf +15 -0
  40. package/dist/resources/deploy/profiles/go-service/compose.yaml +19 -0
  41. package/dist/resources/deploy/profiles/nuxt-node/Dockerfile +17 -0
  42. package/dist/resources/deploy/profiles/static-files/nginx/site.conf.template +55 -0
  43. package/dist/resources/deploy/profiles/static-nginx/Dockerfile +18 -0
  44. package/dist/resources/deploy/profiles/static-nginx/nginx/default.conf.template +28 -0
  45. package/dist/resources/deploy/targets/compose-host/deploy.config.example.json +114 -0
  46. package/dist/resources/deploy/traffic/nginx/stackonward-nginx-traffic +96 -0
  47. package/dist/resources/registry.json +30 -30
  48. package/dist/resources/templates/base/.claude/docs/architecture.md +4 -0
  49. package/dist/resources/templates/base/AGENTS.md +15 -0
  50. package/dist/resources/templates/base/CLAUDE.md +15 -0
  51. package/dist/resources/templates/go-backend/.dockerignore +25 -3
  52. package/dist/resources/templates/go-backend/deploy.config.json +26 -0
  53. package/dist/resources/templates/go-backend/docs/runtime-provenance.json +9 -6
  54. package/dist/resources/templates/go-backend/docs/source-provenance.json +33 -22
  55. package/dist/resources/templates/go-backend/internal/common/enums/register_locale.go +7 -0
  56. package/dist/resources/templates/go-backend/internal/common/enums/register_locale_test.go +30 -0
  57. package/dist/resources/templates/go-backend/internal/common/locale/locale.go +102 -76
  58. package/dist/resources/templates/go-backend/internal/common/locale/locale_test.go +168 -45
  59. package/dist/resources/templates/go-backend/internal/core/commandreplay/model.go +5 -5
  60. package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence.go +20 -7
  61. package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence_test.go +51 -22
  62. package/dist/resources/templates/go-backend/internal/core/commandreplay/repository.go +2 -2
  63. package/dist/resources/templates/go-backend/internal/core/commandreplay/service.go +13 -24
  64. package/dist/resources/templates/go-backend/internal/core/commandreplay/service_persistence_test.go +98 -13
  65. package/dist/resources/templates/go-backend/internal/core/commandreplay/service_test.go +61 -36
  66. package/dist/resources/templates/go-backend/internal/core/commandreplay/types.go +6 -6
  67. package/dist/resources/templates/go-backend/internal/core/integrationevent/config.go +24 -0
  68. package/dist/resources/templates/go-backend/internal/core/integrationevent/errors.go +53 -0
  69. package/dist/resources/templates/go-backend/internal/core/integrationevent/model.go +71 -0
  70. package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence.go +193 -0
  71. package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_postgres_test.go +178 -0
  72. package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_test.go +168 -0
  73. package/dist/resources/templates/go-backend/internal/core/integrationevent/provider.go +16 -0
  74. package/dist/resources/templates/go-backend/internal/core/integrationevent/registry.go +69 -0
  75. package/dist/resources/templates/go-backend/internal/core/integrationevent/repository.go +17 -0
  76. package/dist/resources/templates/go-backend/internal/core/integrationevent/service.go +283 -0
  77. package/dist/resources/templates/go-backend/internal/core/integrationevent/service_test.go +433 -0
  78. package/dist/resources/templates/go-backend/internal/core/integrationevent/types.go +67 -0
  79. package/dist/resources/templates/go-backend/migrations/000001_infrastructure.down.sql +6 -0
  80. package/dist/resources/templates/go-backend/migrations/000001_infrastructure.up.sql +176 -16
  81. package/dist/resources/templates/go-backend/tests/infrastructure.sql +109 -6
  82. package/dist/resources/templates/nuxt/.claude/docs/stack.md +5 -1
  83. package/dist/resources/templates/nuxt/.dockerignore +19 -0
  84. package/dist/resources/templates/nuxt/README.md +3 -3
  85. package/dist/resources/templates/nuxt/app/app.config.ts +1 -1
  86. package/dist/resources/templates/nuxt/app/i18n/locales/en.json +1 -1
  87. package/dist/resources/templates/nuxt/app/pages/app/index.vue +1 -1
  88. package/dist/resources/templates/nuxt/app/pages/authors/[slug].vue +2 -2
  89. package/dist/resources/templates/nuxt/app/pages/blog/[slug].vue +4 -3
  90. package/dist/resources/templates/nuxt/app/pages/blog/category/[category].vue +2 -2
  91. package/dist/resources/templates/nuxt/app/pages/blog/index.vue +2 -2
  92. package/dist/resources/templates/nuxt/app/pages/legal/cookies.vue +3 -3
  93. package/dist/resources/templates/nuxt/app/pages/legal/privacy.vue +2 -2
  94. package/dist/resources/templates/nuxt/app/pages/legal/terms.vue +2 -2
  95. package/dist/resources/templates/nuxt/app/pages/ui/index.vue +45 -45
  96. package/dist/resources/templates/nuxt/deploy.config.json +27 -0
  97. package/dist/resources/templates/nuxt/nuxt.config.ts +4 -10
  98. package/dist/resources/templates/nuxt/package.json +8 -8
  99. package/dist/resources/templates/nuxt/test/e2e/rendering.test.ts +1 -0
  100. package/dist/resources/templates/nuxt/test/unit/consent.test.ts +4 -7
  101. package/package.json +5 -3
  102. package/dist/resources/components/nuxt/site-source/app/components/site/MarketingShell.vue +0 -33
  103. package/dist/resources/components/nuxt/site-source/app/types/site-shell.ts +0 -172
  104. /package/dist/resources/components/nuxt/site-source/app/components/{site/ConsentSettingsButton.vue → consent/Settings.vue} +0 -0
  105. /package/dist/resources/components/nuxt/site-source/app/components/{site/StaticPage.vue → content/Document.vue} +0 -0
  106. /package/dist/resources/components/nuxt/site-source/app/components/{site/ToastViewport.vue → feedback/Toast.vue} +0 -0
  107. /package/dist/resources/components/nuxt/site-source/app/components/site/{SiteBrand.vue → Brand.vue} +0 -0
  108. /package/dist/resources/components/nuxt/site-source/app/components/ui/{BaseGradientBorder.vue → GradientBorder.vue} +0 -0
  109. /package/dist/resources/components/nuxt/site-source/app/components/ui/{BaseModal.vue → Modal.vue} +0 -0
  110. /package/dist/resources/components/nuxt/site-source/app/components/ui/{BasePopover.vue → Popover.vue} +0 -0
  111. /package/dist/resources/components/nuxt/site-source/app/components/ui/{BaseText.vue → Text.vue} +0 -0
@@ -1,35 +1,35 @@
1
1
  <script setup lang="ts">
2
- import type { MarketingNavigationItem } from "~/types/site-shell";
2
+ import type { NavItem } from "~/navigation/types";
3
3
 
4
4
  const site = useSiteShell();
5
- const { marketingNav, primaryActionLabel, secondaryActionLabel } = useNavigation();
5
+ const { items, primaryActionLabel, secondaryActionLabel } = useNavigation();
6
6
  const { t } = useI18n();
7
- const menuOpen = ref(false);
7
+ const isMenuOpen = ref(false);
8
8
 
9
9
  function closeMenu(): void {
10
- menuOpen.value = false;
10
+ isMenuOpen.value = false;
11
11
  }
12
12
 
13
- function hasChildren(item: MarketingNavigationItem): boolean {
13
+ function hasChildren(item: NavItem): boolean {
14
14
  return Boolean(item.children?.length);
15
15
  }
16
16
  </script>
17
17
 
18
18
  <template>
19
- <header class="marketing-header">
20
- <div class="marketing-header__inner">
19
+ <header class="site-header">
20
+ <div class="site-header__inner">
21
21
  <SiteBrand />
22
22
 
23
- <nav class="marketing-header__nav" :aria-label="t('navigation.primary')">
24
- <template v-for="item in marketingNav" :key="item.label">
25
- <details v-if="hasChildren(item)" class="marketing-nav-menu">
23
+ <nav class="site-header__nav" :aria-label="t('navigation.primary')">
24
+ <template v-for="item in items" :key="item.label">
25
+ <details v-if="hasChildren(item)" class="site-nav-menu">
26
26
  <summary>{{ item.label }}</summary>
27
- <div class="marketing-nav-menu__panel">
27
+ <div class="site-nav-menu__panel">
28
28
  <NuxtLinkLocale
29
29
  v-for="child in item.children"
30
30
  :key="child.label"
31
31
  :to="child.to || site.homePath"
32
- class="marketing-nav-menu__item"
32
+ class="site-nav-menu__item"
33
33
  >
34
34
  <span>{{ child.label }}</span>
35
35
  <small v-if="child.description">{{ child.description }}</small>
@@ -38,23 +38,23 @@ function hasChildren(item: MarketingNavigationItem): boolean {
38
38
  </details>
39
39
  <a
40
40
  v-else-if="item.external"
41
- class="marketing-nav-link"
41
+ class="site-nav-link"
42
42
  :href="item.to"
43
43
  target="_blank"
44
44
  rel="noopener noreferrer"
45
45
  >
46
46
  {{ item.label }}
47
47
  </a>
48
- <NuxtLinkLocale v-else class="marketing-nav-link" :to="item.to || site.homePath">
48
+ <NuxtLinkLocale v-else class="site-nav-link" :to="item.to || site.homePath">
49
49
  {{ item.label }}
50
50
  </NuxtLinkLocale>
51
51
  </template>
52
52
  </nav>
53
53
 
54
- <div class="marketing-header__actions">
55
- <div class="marketing-header__preferences">
56
- <ThemeSwitcher />
57
- <LocaleSwitcher />
54
+ <div class="site-header__actions">
55
+ <div class="site-header__preferences">
56
+ <ThemeMenu />
57
+ <LocaleMenu />
58
58
  </div>
59
59
  <slot name="actions">
60
60
  <NuxtLinkLocale :to="site.secondaryActionPath" class="base-button base-button--secondary">
@@ -67,15 +67,15 @@ function hasChildren(item: MarketingNavigationItem): boolean {
67
67
  </slot>
68
68
  <button
69
69
  type="button"
70
- class="icon-button marketing-header__menu-button"
71
- :aria-expanded="menuOpen"
70
+ class="icon-button site-header__menu-button"
71
+ :aria-expanded="isMenuOpen"
72
72
  aria-controls="mobile-nav"
73
73
  :aria-label="t('navigation.toggle')"
74
- @click="menuOpen = !menuOpen"
74
+ @click="isMenuOpen = !isMenuOpen"
75
75
  >
76
76
  <UIcon
77
- :name="menuOpen ? 'i-lucide-x' : 'i-lucide-menu'"
78
- class="marketing-header__menu-icon"
77
+ :name="isMenuOpen ? 'i-lucide-x' : 'i-lucide-menu'"
78
+ class="site-header__menu-icon"
79
79
  />
80
80
  </button>
81
81
  </div>
@@ -84,10 +84,10 @@ function hasChildren(item: MarketingNavigationItem): boolean {
84
84
  <nav
85
85
  id="mobile-nav"
86
86
  class="mobile-nav"
87
- :class="{ 'mobile-nav--open': menuOpen }"
87
+ :class="{ 'mobile-nav--open': isMenuOpen }"
88
88
  :aria-label="t('navigation.mobilePrimary')"
89
89
  >
90
- <template v-for="item in marketingNav" :key="item.label">
90
+ <template v-for="item in items" :key="item.label">
91
91
  <NuxtLinkLocale
92
92
  v-if="!hasChildren(item)"
93
93
  :to="item.to || site.homePath"
@@ -110,8 +110,8 @@ function hasChildren(item: MarketingNavigationItem): boolean {
110
110
  </details>
111
111
  </template>
112
112
  <div class="mobile-nav__preferences">
113
- <ThemeSwitcher @changed="closeMenu" />
114
- <LocaleSwitcher @changed="closeMenu" />
113
+ <ThemeMenu @select="closeMenu" />
114
+ <LocaleMenu @select="closeMenu" />
115
115
  </div>
116
116
  </nav>
117
117
  </header>
@@ -0,0 +1,29 @@
1
+ <script setup lang="ts">
2
+ import BackToTop from "./BackToTop.vue";
3
+
4
+ const LazyHeader = defineLazyHydrationComponent("idle", () => import("./Header.vue"));
5
+ const LazyFooter = defineLazyHydrationComponent("visible", () => import("./Footer.vue"));
6
+ </script>
7
+
8
+ <template>
9
+ <div class="site-shell">
10
+ <slot name="header">
11
+ <LazyHeader :hydrate-on-idle="800" />
12
+ </slot>
13
+
14
+ <main id="site-content" class="site-shell__main">
15
+ <slot />
16
+ </main>
17
+
18
+ <slot name="footer">
19
+ <LazyFooter hydrate-on-visible />
20
+ </slot>
21
+
22
+ <div class="site-floating-stack">
23
+ <BackToTop />
24
+ <slot name="consent">
25
+ <ConsentBanner />
26
+ </slot>
27
+ </div>
28
+ </div>
29
+ </template>
@@ -3,7 +3,7 @@ type ThemePreference = "system" | "light" | "dark";
3
3
 
4
4
  defineOptions({ inheritAttrs: false });
5
5
 
6
- const emit = defineEmits<{ changed: [] }>();
6
+ const emit = defineEmits<{ select: [value: ThemePreference] }>();
7
7
  const colorMode = useColorMode();
8
8
  const { t } = useI18n();
9
9
  const dropdown = ref<HTMLDetailsElement | null>(null);
@@ -23,7 +23,7 @@ const current = computed(
23
23
  function selectTheme(value: ThemePreference): void {
24
24
  colorMode.preference = value;
25
25
  dropdown.value?.removeAttribute("open");
26
- emit("changed");
26
+ emit("select", value);
27
27
  }
28
28
 
29
29
  function handleKeydown(event: KeyboardEvent): void {
@@ -1,12 +1,5 @@
1
1
  <script setup lang="ts">
2
- type ButtonColor =
3
- | "primary"
4
- | "secondary"
5
- | "success"
6
- | "info"
7
- | "warning"
8
- | "error"
9
- | "neutral";
2
+ type ButtonColor = "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral";
10
3
  type ButtonVariant = "solid" | "outline" | "soft" | "subtle" | "ghost" | "link";
11
4
  type ButtonSize = "xs" | "sm" | "md" | "lg" | "xl";
12
5
 
@@ -93,12 +93,12 @@ watch(
93
93
  <div ref="sentinelRef" class="infinite-scroll__sentinel" aria-hidden="true" />
94
94
  <div v-if="loading" class="infinite-scroll__state">
95
95
  <slot name="loading">
96
- <BaseText as="p" size="sm" tone="muted">Loading more...</BaseText>
96
+ <UiText as="p" size="sm" tone="muted">Loading more...</UiText>
97
97
  </slot>
98
98
  </div>
99
99
  <div v-else-if="!hasMore" class="infinite-scroll__state">
100
100
  <slot name="complete">
101
- <BaseText as="p" size="sm" tone="muted">All items loaded.</BaseText>
101
+ <UiText as="p" size="sm" tone="muted">All items loaded.</UiText>
102
102
  </slot>
103
103
  </div>
104
104
  </div>
@@ -84,12 +84,20 @@ const embedSrc = computed(() => {
84
84
  :src="embedSrc"
85
85
  :title="title"
86
86
  loading="lazy"
87
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
87
+ allow="
88
+ accelerometer;
89
+ autoplay;
90
+ clipboard-write;
91
+ encrypted-media;
92
+ gyroscope;
93
+ picture-in-picture;
94
+ web-share;
95
+ "
88
96
  referrerpolicy="strict-origin-when-cross-origin"
89
97
  allowfullscreen
90
98
  />
91
99
  <div v-else class="youtube-embed__fallback">
92
- <BaseText tone="danger" size="sm">Invalid YouTube source.</BaseText>
100
+ <UiText tone="danger" size="sm">Invalid YouTube source.</UiText>
93
101
  </div>
94
102
  </figure>
95
103
  </template>
@@ -3,11 +3,11 @@ import type { ComputedRef, Ref } from "vue";
3
3
  import type {
4
4
  ConsentInitializationResult,
5
5
  ConsentMutationResult,
6
+ ConsentConfig,
7
+ ConsentController,
6
8
  ConsentState,
7
9
  ConsentValue,
8
- SiteConsentController,
9
- SiteShellConsentConfig,
10
- } from "~/types/site-shell";
10
+ } from "~/consent/types";
11
11
 
12
12
  const CONSENT_RECORD_VERSION = 1;
13
13
  const MAX_TIMER_DELAY = 2_147_483_647;
@@ -26,16 +26,14 @@ interface ConsentAnalyticsBridge {
26
26
  readonly reset: () => void;
27
27
  }
28
28
 
29
- interface CreateSiteConsentControllerOptions {
29
+ interface ControllerOptions {
30
30
  readonly state: Ref<ConsentState>;
31
- readonly config: Readonly<ComputedRef<SiteShellConsentConfig>>;
31
+ readonly config: Readonly<ComputedRef<ConsentConfig>>;
32
32
  readonly analytics: ConsentAnalyticsBridge;
33
33
  }
34
34
 
35
35
  /** 在应用生命周期内协调同意状态、持久化和分析开关。 */
36
- export function createSiteConsentController(
37
- options: CreateSiteConsentControllerOptions,
38
- ): SiteConsentController {
36
+ export function createConsentController(options: ControllerOptions): ConsentController {
39
37
  let initialized = false;
40
38
  let disposed = false;
41
39
  let expiryTimer: ReturnType<typeof setTimeout> | undefined;
@@ -311,9 +309,9 @@ export class ConsentLifecycleError extends Error {
311
309
  }
312
310
 
313
311
  /** 返回 Nuxt 应用提供的唯一同意控制器。 */
314
- export function useConsent(): SiteConsentController {
312
+ export function useConsent(): ConsentController {
315
313
  const nuxtApp = useNuxtApp() as ReturnType<typeof useNuxtApp> & {
316
- readonly $siteConsent: SiteConsentController;
314
+ readonly $siteConsent: ConsentController;
317
315
  };
318
316
  return nuxtApp.$siteConsent;
319
317
  }
@@ -1,32 +1,27 @@
1
- import type {
2
- AppNavigationItem,
3
- LocalizedNavigationItemConfig,
4
- MarketingNavigationItem,
5
- ResolvedSiteNavigation,
6
- } from "~/types/site-shell";
1
+ import type { AppNavItem, NavItem, NavItemConfig, Navigation } from "~/navigation/types";
7
2
 
8
3
  /** 将产品配置中的资源键投影为当前语言可渲染的导航。 */
9
- export function useNavigation(): ResolvedSiteNavigation {
4
+ export function useNavigation(): Navigation {
10
5
  const site = useSiteShell();
11
6
  const { t } = useI18n();
12
7
 
13
- function resolveMarketingItem(item: LocalizedNavigationItemConfig): MarketingNavigationItem {
8
+ function resolveItem(item: NavItemConfig): NavItem {
14
9
  return {
15
10
  label: t(item.labelKey),
16
11
  ...(item.to === undefined ? {} : { to: item.to }),
17
12
  ...(item.external === undefined ? {} : { external: item.external }),
18
13
  ...(item.descriptionKey === undefined ? {} : { description: t(item.descriptionKey) }),
19
- ...(item.children === undefined ? {} : { children: item.children.map(resolveMarketingItem) }),
14
+ ...(item.children === undefined ? {} : { children: item.children.map(resolveItem) }),
20
15
  };
21
16
  }
22
17
 
23
18
  return {
24
- marketingNav: computed(() => site.value.navigation.marketing.map(resolveMarketingItem)),
25
- appNavGroups: computed(() =>
19
+ items: computed(() => site.value.navigation.main.map(resolveItem)),
20
+ appGroups: computed(() =>
26
21
  site.value.navigation.app.map((group) => ({
27
22
  ...(group.labelKey === undefined ? {} : { label: t(group.labelKey) }),
28
23
  items: group.items.map(
29
- (item): AppNavigationItem => ({
24
+ (item): AppNavItem => ({
30
25
  label: t(item.labelKey),
31
26
  to: item.to,
32
27
  ...(item.icon === undefined ? {} : { icon: item.icon }),
@@ -38,10 +33,10 @@ export function useNavigation(): ResolvedSiteNavigation {
38
33
  footerGroups: computed(() =>
39
34
  site.value.navigation.footer.map((group) => ({
40
35
  title: t(group.titleKey),
41
- links: group.links.map(resolveMarketingItem),
36
+ links: group.links.map(resolveItem),
42
37
  })),
43
38
  ),
44
- legalLinks: computed(() => site.value.navigation.legal.map(resolveMarketingItem)),
39
+ legalLinks: computed(() => site.value.navigation.legal.map(resolveItem)),
45
40
  socialLinks: computed(() =>
46
41
  site.value.navigation.social.map((link) => ({
47
42
  label: t(link.labelKey),
@@ -1,3 +1,5 @@
1
+ import { formatTitle } from "@stackonward/nuxt-site-shell/runtime";
2
+
1
3
  export interface SeoInput {
2
4
  title?: string;
3
5
  description?: string;
@@ -23,7 +25,7 @@ export function useSeo(input: SeoInput): void {
23
25
  useSeoMeta({
24
26
  title: input.title,
25
27
  description,
26
- ogTitle: input.title,
28
+ ogTitle: computed(() => formatTitle(input.title, site.value.name)),
27
29
  ogDescription: description,
28
30
  ogImage: input.image,
29
31
  robots: input.robots,
@@ -1,21 +1,18 @@
1
1
  import type { ComputedRef } from "vue";
2
- import type {
3
- SiteShellConfig,
4
- SiteShellConsentConfig,
5
- StorageDisclosureConfig,
6
- } from "~/types/site-shell";
2
+ import type { ConsentConfig, StorageDisclosure } from "~/consent/types";
3
+ import type { Config } from "~/site/config";
7
4
 
8
- type SiteShellConfigInput = Omit<
9
- Partial<SiteShellConfig>,
5
+ type ConfigInput = Omit<
6
+ Partial<Config>,
10
7
  "consent" | "storageDisclosures" | "navigation" | "content"
11
8
  > & {
12
- readonly consent?: Partial<SiteShellConsentConfig>;
13
- readonly storageDisclosures?: readonly StorageDisclosureConfig[];
14
- readonly navigation?: Partial<SiteShellConfig["navigation"]>;
15
- readonly content?: Partial<SiteShellConfig["content"]>;
9
+ readonly consent?: Partial<ConsentConfig>;
10
+ readonly storageDisclosures?: readonly StorageDisclosure[];
11
+ readonly navigation?: Partial<Config["navigation"]>;
12
+ readonly content?: Partial<Config["content"]>;
16
13
  };
17
14
 
18
- const DEFAULT_SITE_SHELL_CONFIG: SiteShellConfig = {
15
+ const DEFAULT_CONFIG: Config = {
19
16
  name: "Product Studio",
20
17
  descriptionKey: "site.description",
21
18
  url: "",
@@ -35,7 +32,7 @@ const DEFAULT_SITE_SHELL_CONFIG: SiteShellConfig = {
35
32
  },
36
33
  storageDisclosures: [],
37
34
  navigation: {
38
- marketing: [],
35
+ main: [],
39
36
  app: [],
40
37
  footer: [],
41
38
  legal: [],
@@ -49,28 +46,28 @@ const DEFAULT_SITE_SHELL_CONFIG: SiteShellConfig = {
49
46
  },
50
47
  };
51
48
 
52
- function mergeSiteShellConfig(input: SiteShellConfigInput | undefined): SiteShellConfig {
49
+ function mergeConfig(input: ConfigInput | undefined): Config {
53
50
  return {
54
- ...DEFAULT_SITE_SHELL_CONFIG,
51
+ ...DEFAULT_CONFIG,
55
52
  ...input,
56
53
  consent: {
57
- ...DEFAULT_SITE_SHELL_CONFIG.consent,
54
+ ...DEFAULT_CONFIG.consent,
58
55
  ...input?.consent,
59
56
  },
60
- storageDisclosures: input?.storageDisclosures ?? DEFAULT_SITE_SHELL_CONFIG.storageDisclosures,
57
+ storageDisclosures: input?.storageDisclosures ?? DEFAULT_CONFIG.storageDisclosures,
61
58
  navigation: {
62
- ...DEFAULT_SITE_SHELL_CONFIG.navigation,
59
+ ...DEFAULT_CONFIG.navigation,
63
60
  ...input?.navigation,
64
61
  },
65
62
  content: {
66
- ...DEFAULT_SITE_SHELL_CONFIG.content,
63
+ ...DEFAULT_CONFIG.content,
67
64
  ...input?.content,
68
65
  },
69
66
  };
70
67
  }
71
68
 
72
69
  /** 返回当前 Nuxt 应用拥有的站点配置只读视图。 */
73
- export function useSiteShell(): Readonly<ComputedRef<SiteShellConfig>> {
74
- const appConfig = useAppConfig() as { readonly siteShell?: SiteShellConfigInput };
75
- return computed(() => mergeSiteShellConfig(appConfig.siteShell));
70
+ export function useSiteShell(): Readonly<ComputedRef<Config>> {
71
+ const appConfig = useAppConfig() as { readonly siteShell?: ConfigInput };
72
+ return computed(() => mergeConfig(appConfig.siteShell));
76
73
  }
@@ -0,0 +1,59 @@
1
+ import type { ComputedRef, DeepReadonly, Ref } from "vue";
2
+
3
+ export interface StorageDisclosure {
4
+ readonly name: string;
5
+ readonly category: "necessary" | "preferences" | "analytics";
6
+ readonly purposeKey: string;
7
+ readonly retentionKey: string;
8
+ readonly providerKey: string;
9
+ }
10
+
11
+ export interface ConsentConfig {
12
+ readonly enabled: boolean;
13
+ readonly storageKey: string;
14
+ readonly policyRevision: string;
15
+ readonly retentionDays: number;
16
+ readonly policyPath: string;
17
+ }
18
+
19
+ export type ConsentValue = "accepted" | "rejected";
20
+
21
+ export type ConsentPersistenceError =
22
+ | "read-failed"
23
+ | "write-failed"
24
+ | "clear-failed"
25
+ | "write-and-clear-failed";
26
+
27
+ export interface ConsentState {
28
+ decision: ConsentValue | null;
29
+ ready: boolean;
30
+ settingsOpen: boolean;
31
+ persistence: "none" | "persisted" | "cleared" | "cleanup-failed" | "session-only";
32
+ error: ConsentPersistenceError | null;
33
+ }
34
+
35
+ export type ConsentInitializationResult =
36
+ | { status: "restored"; value: ConsentValue }
37
+ | { status: "undecided"; cleanup: "not-required" | "cleared" }
38
+ | { status: "undecided"; cleanup: "failed"; error: "clear-failed" }
39
+ | { status: "storage-unavailable"; error: "read-failed" };
40
+
41
+ export type ConsentMutationResult =
42
+ | { status: "persisted"; value: ConsentValue }
43
+ | { status: "cleared"; value: "rejected"; error: "write-failed" }
44
+ | {
45
+ status: "session-only";
46
+ value: ConsentValue;
47
+ error: "write-failed" | "write-and-clear-failed";
48
+ };
49
+
50
+ export interface ConsentController {
51
+ readonly state: DeepReadonly<Ref<ConsentState>>;
52
+ readonly visible: Readonly<ComputedRef<boolean>>;
53
+ initialize(): ConsentInitializationResult;
54
+ accept(): ConsentMutationResult;
55
+ reject(): ConsentMutationResult;
56
+ openSettings(): void;
57
+ closeSettings(): void;
58
+ dispose(): void;
59
+ }
@@ -0,0 +1,17 @@
1
+ export interface ListItem {
2
+ readonly title: string;
3
+ readonly description?: string;
4
+ readonly to: string;
5
+ readonly category?: string;
6
+ readonly date?: string;
7
+ readonly author?: string;
8
+ }
9
+
10
+ export interface Article {
11
+ readonly title: string;
12
+ readonly description?: string;
13
+ readonly category?: string;
14
+ readonly author?: string;
15
+ readonly date?: string;
16
+ readonly updatedAt?: string;
17
+ }
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <ApplicationShell>
2
+ <AppShell>
3
3
  <slot />
4
- </ApplicationShell>
4
+ </AppShell>
5
5
  </template>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <MarketingShell>
2
+ <SiteShell>
3
3
  <slot />
4
- </MarketingShell>
4
+ </SiteShell>
5
5
  </template>
@@ -0,0 +1,68 @@
1
+ import type { ComputedRef } from "vue";
2
+
3
+ export interface NavItemConfig {
4
+ readonly labelKey: string;
5
+ readonly to?: string;
6
+ readonly external?: boolean;
7
+ readonly descriptionKey?: string;
8
+ readonly children?: readonly NavItemConfig[];
9
+ }
10
+
11
+ export interface NavItem {
12
+ readonly label: string;
13
+ readonly to?: string;
14
+ readonly external?: boolean;
15
+ readonly description?: string;
16
+ readonly children?: readonly NavItem[];
17
+ }
18
+
19
+ export interface AppNavItemConfig {
20
+ readonly labelKey: string;
21
+ readonly to: string;
22
+ readonly icon?: string;
23
+ readonly badge?: string;
24
+ }
25
+
26
+ export interface AppNavItem extends Omit<AppNavItemConfig, "labelKey"> {
27
+ readonly label: string;
28
+ }
29
+
30
+ export interface NavGroupConfig {
31
+ readonly labelKey?: string;
32
+ readonly items: readonly AppNavItemConfig[];
33
+ }
34
+
35
+ export interface NavGroup {
36
+ readonly label?: string;
37
+ readonly items: readonly AppNavItem[];
38
+ }
39
+
40
+ export interface FooterGroupConfig {
41
+ readonly titleKey: string;
42
+ readonly links: readonly NavItemConfig[];
43
+ }
44
+
45
+ export interface FooterGroup {
46
+ readonly title: string;
47
+ readonly links: readonly NavItem[];
48
+ }
49
+
50
+ export interface SocialLinkConfig {
51
+ readonly labelKey: string;
52
+ readonly href: string;
53
+ }
54
+
55
+ export interface SocialLink {
56
+ readonly label: string;
57
+ readonly href: string;
58
+ }
59
+
60
+ export interface Navigation {
61
+ readonly items: Readonly<ComputedRef<readonly NavItem[]>>;
62
+ readonly appGroups: Readonly<ComputedRef<readonly NavGroup[]>>;
63
+ readonly footerGroups: Readonly<ComputedRef<readonly FooterGroup[]>>;
64
+ readonly legalLinks: Readonly<ComputedRef<readonly NavItem[]>>;
65
+ readonly socialLinks: Readonly<ComputedRef<readonly SocialLink[]>>;
66
+ readonly primaryActionLabel: Readonly<ComputedRef<string>>;
67
+ readonly secondaryActionLabel: Readonly<ComputedRef<string>>;
68
+ }
@@ -1,4 +1,4 @@
1
- import type { ConsentState } from "~/types/site-shell";
1
+ import type { ConsentState } from "~/consent/types";
2
2
 
3
3
  export default defineNuxtPlugin({
4
4
  name: "site:consent",
@@ -14,7 +14,7 @@ export default defineNuxtPlugin({
14
14
  persistence: "none",
15
15
  error: null,
16
16
  }));
17
- const controller = createSiteConsentController({
17
+ const controller = createConsentController({
18
18
  state,
19
19
  config: computed(() => site.value.consent),
20
20
  analytics: {
@@ -0,0 +1,35 @@
1
+ import type { ConsentConfig, StorageDisclosure } from "~/consent/types";
2
+ import type {
3
+ FooterGroupConfig,
4
+ NavGroupConfig,
5
+ NavItemConfig,
6
+ SocialLinkConfig,
7
+ } from "~/navigation/types";
8
+
9
+ export interface Config {
10
+ readonly name: string;
11
+ readonly descriptionKey: string;
12
+ readonly url: string;
13
+ readonly defaultLocale: string;
14
+ readonly homePath: string;
15
+ readonly appPath: string;
16
+ readonly logoText: string;
17
+ readonly primaryActionLabelKey: string;
18
+ readonly secondaryActionLabelKey: string;
19
+ readonly secondaryActionPath: string;
20
+ readonly consent: ConsentConfig;
21
+ readonly storageDisclosures: readonly StorageDisclosure[];
22
+ readonly navigation: {
23
+ readonly main: readonly NavItemConfig[];
24
+ readonly app: readonly NavGroupConfig[];
25
+ readonly footer: readonly FooterGroupConfig[];
26
+ readonly legal: readonly NavItemConfig[];
27
+ readonly social: readonly SocialLinkConfig[];
28
+ };
29
+ readonly content: {
30
+ readonly blogPath: string;
31
+ readonly authorPath: string;
32
+ readonly previewPath: string;
33
+ readonly sitemapSourcePath: string;
34
+ };
35
+ }