@commonpub/layer 0.24.0 → 0.25.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 (82) hide show
  1. package/README.md +41 -12
  2. package/components/LayoutRow.vue +944 -0
  3. package/components/LayoutSection.vue +1028 -0
  4. package/components/LayoutSlot.vue +104 -162
  5. package/components/PageFrame.vue +116 -0
  6. package/components/admin/layouts/AdminLayoutsAnnouncer.vue +53 -0
  7. package/components/admin/layouts/AdminLayoutsAutoForm.vue +419 -0
  8. package/components/admin/layouts/AdminLayoutsCanvas.vue +332 -0
  9. package/components/admin/layouts/AdminLayoutsConflictModal.vue +266 -0
  10. package/components/admin/layouts/AdminLayoutsHelpOverlay.vue +346 -0
  11. package/components/admin/layouts/AdminLayoutsInspector.vue +157 -0
  12. package/components/admin/layouts/AdminLayoutsInspectorPage.vue +266 -0
  13. package/components/admin/layouts/AdminLayoutsInspectorRow.vue +80 -0
  14. package/components/admin/layouts/AdminLayoutsInspectorSection.vue +175 -0
  15. package/components/admin/layouts/AdminLayoutsPalette.vue +117 -0
  16. package/components/admin/layouts/AdminLayoutsPaletteTile.vue +149 -0
  17. package/components/admin/layouts/AdminLayoutsToolbar.vue +483 -0
  18. package/components/blocks/BlockDividerView.vue +52 -2
  19. package/components/homepage/ContentGridSection.vue +23 -1
  20. package/components/homepage/HeroSection.vue +69 -8
  21. package/components/sections/SectionCta.vue +175 -0
  22. package/composables/autoFormSchema.ts +319 -0
  23. package/composables/useAdminSidebar.ts +116 -0
  24. package/composables/useEditorChrome.ts +56 -0
  25. package/composables/useLayout.ts +34 -41
  26. package/composables/useLayoutAnnouncer.ts +332 -0
  27. package/composables/useLayoutAutoSave.ts +117 -0
  28. package/composables/useLayoutDrag.ts +290 -0
  29. package/composables/useLayoutEditor.ts +593 -0
  30. package/composables/useLayoutHistory.ts +583 -0
  31. package/composables/useLayoutHotkeys.ts +366 -0
  32. package/composables/useLayoutResize.ts +783 -0
  33. package/layouts/admin.vue +137 -24
  34. package/middleware/admin-layouts.ts +29 -0
  35. package/package.json +10 -7
  36. package/pages/[...customPath].vue +154 -0
  37. package/pages/admin/homepage.vue +46 -0
  38. package/pages/admin/index.vue +16 -0
  39. package/pages/admin/layouts/[id].vue +1110 -0
  40. package/pages/admin/layouts/index.vue +356 -0
  41. package/pages/explore.vue +16 -6
  42. package/sections/builtin/content-feed.ts +18 -29
  43. package/sections/builtin/contests.ts +11 -19
  44. package/sections/builtin/cta.ts +46 -0
  45. package/sections/builtin/custom-html.ts +16 -30
  46. package/sections/builtin/divider.ts +15 -17
  47. package/sections/builtin/editorial.ts +11 -21
  48. package/sections/builtin/embed.ts +31 -0
  49. package/sections/builtin/gallery.ts +29 -0
  50. package/sections/builtin/heading.ts +14 -19
  51. package/sections/builtin/hero.ts +16 -51
  52. package/sections/builtin/hubs.ts +11 -26
  53. package/sections/builtin/image.ts +12 -49
  54. package/sections/builtin/learning.ts +5 -13
  55. package/sections/builtin/markdown.ts +29 -0
  56. package/sections/builtin/paragraph.ts +14 -17
  57. package/sections/builtin/stats.ts +17 -18
  58. package/sections/builtin/video.ts +30 -0
  59. package/sections/registry.ts +11 -0
  60. package/server/api/admin/homepage/sections.put.ts +52 -1
  61. package/server/api/admin/layouts/[id]/publish.post.ts +12 -0
  62. package/server/api/admin/layouts/[id]/versions/[versionId]/revert.post.ts +11 -0
  63. package/server/api/admin/layouts/[id].delete.ts +33 -1
  64. package/server/api/admin/layouts/[id].put.ts +78 -0
  65. package/server/api/admin/layouts/index.post.ts +60 -4
  66. package/server/api/admin/layouts/migrate-homepage.post.ts +12 -0
  67. package/server/api/admin/layouts/seed-homepage.post.ts +9 -0
  68. package/server/api/layouts/by-route.get.ts +64 -12
  69. package/server/utils/layoutCache.ts +37 -1
  70. package/server/utils/validateSectionConfigs.ts +123 -0
  71. package/theme/base.css +1 -0
  72. package/components/sections/SectionContentFeed.vue +0 -160
  73. package/components/sections/SectionContests.vue +0 -193
  74. package/components/sections/SectionCustomHtml.vue +0 -70
  75. package/components/sections/SectionDivider.vue +0 -55
  76. package/components/sections/SectionEditorial.vue +0 -138
  77. package/components/sections/SectionHeading.vue +0 -78
  78. package/components/sections/SectionHero.vue +0 -164
  79. package/components/sections/SectionHubs.vue +0 -247
  80. package/components/sections/SectionImage.vue +0 -104
  81. package/components/sections/SectionParagraph.vue +0 -55
  82. package/components/sections/SectionStats.vue +0 -151
@@ -1,104 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * Built-in section: image — single image with optional caption + link.
4
- *
5
- * Phase 1c starter. Author-provided `src` is rendered as-is; aspect-ratio
6
- * keeps cards uniform. Lazy-loaded by default for below-fold sections.
7
- *
8
- * Phase 3e inspector will swap the `src` text input for an ImageUpload
9
- * picker via `.describe('image')` Zod metadata.
10
- *
11
- * `var(--*)` only.
12
- */
13
- import type { SectionRenderProps } from '@commonpub/ui';
14
-
15
- interface ImageConfig extends Record<string, unknown> {
16
- src: string;
17
- alt: string;
18
- caption: string;
19
- href: string;
20
- fit: 'contain' | 'cover';
21
- aspectRatio: '16/9' | '4/3' | '1/1' | 'auto';
22
- }
23
-
24
- defineProps<SectionRenderProps<ImageConfig>>();
25
- </script>
26
-
27
- <template>
28
- <figure
29
- class="cpub-section-image"
30
- :data-aspect="config.aspectRatio"
31
- :data-fit="config.fit"
32
- >
33
- <!-- The `href` field is optional — link only when truthy so screen
34
- readers don't announce a non-interactive image as a link. -->
35
- <component
36
- :is="config.href ? 'a' : 'div'"
37
- :href="config.href || undefined"
38
- class="cpub-section-image-frame"
39
- >
40
- <img
41
- v-if="config.src"
42
- :src="config.src"
43
- :alt="config.alt"
44
- loading="lazy"
45
- decoding="async"
46
- class="cpub-section-image-img"
47
- />
48
- <div v-else class="cpub-section-image-placeholder" aria-hidden="true">
49
- <i class="fa-regular fa-image" />
50
- </div>
51
- </component>
52
- <figcaption v-if="config.caption" class="cpub-section-image-caption">
53
- {{ config.caption }}
54
- </figcaption>
55
- </figure>
56
- </template>
57
-
58
- <style scoped>
59
- .cpub-section-image {
60
- margin: 0;
61
- display: flex;
62
- flex-direction: column;
63
- gap: var(--space-2);
64
- }
65
- .cpub-section-image-frame {
66
- display: block;
67
- width: 100%;
68
- background: var(--surface2);
69
- border: var(--border-width-default) solid var(--border);
70
- overflow: hidden;
71
- text-decoration: none;
72
- color: inherit;
73
- }
74
- .cpub-section-image[data-aspect='16/9'] .cpub-section-image-frame { aspect-ratio: 16 / 9; }
75
- .cpub-section-image[data-aspect='4/3'] .cpub-section-image-frame { aspect-ratio: 4 / 3; }
76
- .cpub-section-image[data-aspect='1/1'] .cpub-section-image-frame { aspect-ratio: 1 / 1; }
77
-
78
- .cpub-section-image-img {
79
- width: 100%;
80
- height: 100%;
81
- display: block;
82
- }
83
- .cpub-section-image[data-fit='cover'] .cpub-section-image-img { object-fit: cover; }
84
- .cpub-section-image[data-fit='contain'] .cpub-section-image-img { object-fit: contain; }
85
- /* Aspect=auto + no explicit ratio: let the image dictate height */
86
- .cpub-section-image[data-aspect='auto'] .cpub-section-image-img {
87
- height: auto;
88
- }
89
-
90
- .cpub-section-image-placeholder {
91
- display: flex;
92
- align-items: center;
93
- justify-content: center;
94
- height: 100%;
95
- min-height: 120px;
96
- color: var(--text-faint);
97
- font-size: var(--text-xl);
98
- }
99
- .cpub-section-image-caption {
100
- font-size: var(--text-sm);
101
- color: var(--text-dim);
102
- line-height: 1.5;
103
- }
104
- </style>
@@ -1,55 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * Built-in section: paragraph — plain prose body.
4
- *
5
- * Phase 1c starter. Stores `text: string` and splits on blank lines into
6
- * paragraphs at render time. The auto-form inspector (Phase 3e) will
7
- * upgrade this to a TipTap subset via the `.describe('rich')` Zod
8
- * metadata — at which point `schemaVersion` bumps to 2 with a migration
9
- * that converts plain text → block tuples.
10
- *
11
- * `var(--*)` only.
12
- */
13
- import { computed } from 'vue';
14
- import type { SectionRenderProps } from '@commonpub/ui';
15
-
16
- interface ParagraphConfig extends Record<string, unknown> {
17
- text: string;
18
- align: 'left' | 'center';
19
- }
20
-
21
- const props = defineProps<SectionRenderProps<ParagraphConfig>>();
22
-
23
- // Blank-line split — preserves authored paragraph breaks without needing
24
- // a rich-text editor. Empty paragraphs are dropped (defensive).
25
- const paragraphs = computed<string[]>(() =>
26
- (props.config.text ?? '')
27
- .split(/\n{2,}/)
28
- .map((p) => p.trim())
29
- .filter((p) => p.length > 0),
30
- );
31
- </script>
32
-
33
- <template>
34
- <div class="cpub-section-paragraph" :data-align="config.align">
35
- <p v-for="(p, i) in paragraphs" :key="i">{{ p }}</p>
36
- </div>
37
- </template>
38
-
39
- <style scoped>
40
- .cpub-section-paragraph {
41
- margin-block: var(--space-3);
42
- color: var(--text);
43
- font-size: var(--text-md);
44
- line-height: 1.7;
45
- }
46
- .cpub-section-paragraph[data-align='center'] {
47
- text-align: center;
48
- }
49
- .cpub-section-paragraph p {
50
- margin: 0 0 var(--space-3);
51
- }
52
- .cpub-section-paragraph p:last-child {
53
- margin-bottom: 0;
54
- }
55
- </style>
@@ -1,151 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * Built-in section: stats — a numeric grid of platform totals.
4
- *
5
- * Fetches `/api/stats` (returns PlatformStats), renders 2x2 grid of
6
- * Projects / Posts / Members / Hubs. Hubs cell hides when the `hubs`
7
- * feature flag is off — same logic as the legacy `StatsSection.vue`.
8
- *
9
- * Non-await useFetch per session-158 pattern; pending state surfaced
10
- * via the template. SSR-friendly: useFetch includes the payload in the
11
- * hydration snapshot.
12
- *
13
- * `var(--*)` only.
14
- */
15
- import { computed } from 'vue';
16
- import type { SectionRenderProps } from '@commonpub/ui';
17
-
18
- // Avoid `import type { PlatformStats }` at the call site so the runtime
19
- // stub used in component tests doesn't need to satisfy the full server
20
- // type. Loose shape with optional + numeric primitives mirrors what the
21
- // endpoint actually emits.
22
- interface StatsResponse {
23
- content?: {
24
- byType?: {
25
- project?: number;
26
- blog?: number;
27
- article?: number;
28
- };
29
- };
30
- users?: { total?: number };
31
- hubs?: { total?: number };
32
- }
33
-
34
- interface StatsConfig extends Record<string, unknown> {
35
- heading: string;
36
- }
37
-
38
- const props = defineProps<SectionRenderProps<StatsConfig>>();
39
-
40
- const features = useFeatures();
41
- const hubsEnabled = computed(() => features.hubs.value);
42
-
43
- const { data: stats, pending } = useFetch<StatsResponse>(
44
- '/api/stats',
45
- {
46
- key: `section-stats:${props.meta.sectionId}`,
47
- // Lazy — sidebar metric tile, not above-the-fold content. Matches the
48
- // legacy StatsSection.vue pattern and keeps homepage SSR fast.
49
- lazy: true,
50
- },
51
- );
52
-
53
- const projectCount = computed(() => stats.value?.content?.byType?.project ?? 0);
54
- const postCount = computed(
55
- () => (stats.value?.content?.byType?.blog ?? 0)
56
- + (stats.value?.content?.byType?.article ?? 0),
57
- );
58
- const memberCount = computed(() => stats.value?.users?.total ?? 0);
59
- const hubCount = computed(() => stats.value?.hubs?.total ?? 0);
60
- </script>
61
-
62
- <template>
63
- <section
64
- class="cpub-section-stats"
65
- :aria-labelledby="config.heading ? `section-stats-${meta.sectionId}` : undefined"
66
- >
67
- <h2
68
- v-if="config.heading"
69
- :id="`section-stats-${meta.sectionId}`"
70
- class="cpub-section-stats-heading"
71
- >
72
- {{ config.heading }}
73
- </h2>
74
-
75
- <div v-if="pending" class="cpub-section-stats-loading">
76
- <i class="fa-solid fa-circle-notch fa-spin" aria-hidden="true" />
77
- </div>
78
-
79
- <dl v-else class="cpub-section-stats-grid" :data-with-hubs="hubsEnabled ? 'yes' : 'no'">
80
- <div class="cpub-section-stats-block">
81
- <dt>Projects</dt>
82
- <dd>{{ projectCount }}</dd>
83
- </div>
84
- <div class="cpub-section-stats-block">
85
- <dt>Posts</dt>
86
- <dd>{{ postCount }}</dd>
87
- </div>
88
- <div class="cpub-section-stats-block">
89
- <dt>Members</dt>
90
- <dd>{{ memberCount }}</dd>
91
- </div>
92
- <div v-if="hubsEnabled" class="cpub-section-stats-block">
93
- <dt>Hubs</dt>
94
- <dd>{{ hubCount }}</dd>
95
- </div>
96
- </dl>
97
- </section>
98
- </template>
99
-
100
- <style scoped>
101
- .cpub-section-stats {
102
- background: var(--surface);
103
- border: var(--border-width-default) solid var(--border);
104
- padding: var(--space-4);
105
- }
106
- .cpub-section-stats-heading {
107
- font-family: var(--font-mono);
108
- font-size: var(--text-xxs);
109
- font-weight: 700;
110
- text-transform: uppercase;
111
- letter-spacing: 0.08em;
112
- color: var(--text-faint);
113
- margin: 0 0 var(--space-3) 0;
114
- padding-bottom: var(--space-2);
115
- border-bottom: var(--border-width-default) solid var(--border-soft);
116
- }
117
- .cpub-section-stats-loading {
118
- display: flex;
119
- justify-content: center;
120
- padding: var(--space-4);
121
- color: var(--text-faint);
122
- }
123
- .cpub-section-stats-grid {
124
- display: grid;
125
- grid-template-columns: 1fr 1fr;
126
- gap: var(--space-2);
127
- margin: 0;
128
- }
129
- .cpub-section-stats-block {
130
- text-align: center;
131
- padding: var(--space-2) 0;
132
- }
133
- .cpub-section-stats-block dt {
134
- display: block;
135
- font-family: var(--font-mono);
136
- font-size: var(--text-xxs);
137
- text-transform: uppercase;
138
- letter-spacing: 0.06em;
139
- color: var(--text-faint);
140
- order: 2; /* number above label */
141
- }
142
- .cpub-section-stats-block dd {
143
- display: block;
144
- font-family: var(--font-mono);
145
- font-size: var(--text-lg);
146
- font-weight: 700;
147
- color: var(--text);
148
- margin: 0;
149
- order: 1;
150
- }
151
- </style>