@commonpub/layer 0.4.5 → 0.4.7

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/pages/index.vue CHANGED
@@ -157,6 +157,40 @@ async function handleHubJoin(hubSlug: string): Promise<void> {
157
157
  <div v-if="activeContest?.endDate" class="cpub-hero-aside">
158
158
  <CountdownTimer :target-date="activeContest.endDate" />
159
159
  </div>
160
+ <!-- Large textured Town Square logo — shown for Agora theme when no contest -->
161
+ <div v-if="!activeContest" class="cpub-hero-aside cpub-hero-logo-aside">
162
+ <svg class="cpub-hero-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" aria-hidden="true">
163
+ <defs>
164
+ <filter id="cpub-hero-tex" x="-5%" y="-5%" width="110%" height="110%">
165
+ <feTurbulence type="fractalNoise" baseFrequency="0.04" numOctaves="4" result="noise"/>
166
+ <feDisplacementMap in="SourceGraphic" in2="noise" scale="2"/>
167
+ </filter>
168
+ </defs>
169
+ <g filter="url(#cpub-hero-tex)">
170
+ <rect x="18" y="18" width="72" height="72" fill="none" stroke="currentColor" stroke-width="5.5" stroke-linejoin="round"/>
171
+ <rect x="110" y="18" width="72" height="72" fill="none" stroke="currentColor" stroke-width="5.5" stroke-linejoin="round"/>
172
+ <rect x="18" y="110" width="72" height="72" fill="none" stroke="currentColor" stroke-width="5.5" stroke-linejoin="round"/>
173
+ <rect x="110" y="110" width="72" height="72" fill="none" stroke="currentColor" stroke-width="5.5" stroke-linejoin="round"/>
174
+ <rect x="90" y="90" width="20" height="20" fill="var(--accent, #3d8b5e)"/>
175
+ <rect x="84" y="84" width="6" height="6" fill="var(--accent, #3d8b5e)" opacity="0.6"/>
176
+ <rect x="110" y="84" width="6" height="6" fill="var(--accent, #3d8b5e)" opacity="0.6"/>
177
+ <rect x="84" y="110" width="6" height="6" fill="var(--accent, #3d8b5e)" opacity="0.6"/>
178
+ <rect x="110" y="110" width="6" height="6" fill="var(--accent, #3d8b5e)" opacity="0.6"/>
179
+ <line x1="32" y1="40" x2="76" y2="40" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
180
+ <line x1="32" y1="52" x2="66" y2="52" stroke="var(--accent, #3d8b5e)" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
181
+ <line x1="32" y1="64" x2="72" y2="64" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
182
+ <line x1="124" y1="40" x2="168" y2="40" stroke="var(--accent, #3d8b5e)" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
183
+ <line x1="124" y1="52" x2="158" y2="52" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
184
+ <line x1="124" y1="64" x2="164" y2="64" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
185
+ <line x1="32" y1="132" x2="76" y2="132" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
186
+ <line x1="32" y1="144" x2="62" y2="144" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
187
+ <line x1="32" y1="156" x2="70" y2="156" stroke="var(--accent, #3d8b5e)" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
188
+ <line x1="124" y1="132" x2="168" y2="132" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
189
+ <line x1="124" y1="144" x2="160" y2="144" stroke="var(--accent, #3d8b5e)" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
190
+ <line x1="124" y1="156" x2="152" y2="156" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
191
+ </g>
192
+ </svg>
193
+ </div>
160
194
  </div>
161
195
  </section>
162
196
 
@@ -464,6 +498,14 @@ async function handleHubJoin(hubSlug: string): Promise<void> {
464
498
 
465
499
  .cpub-hero-aside { flex-shrink: 0; }
466
500
 
501
+ /* Hero logo — hidden by default, shown by Agora theme CSS */
502
+ .cpub-hero-logo-aside { display: none; }
503
+ .cpub-hero-logo {
504
+ width: 160px;
505
+ height: 160px;
506
+ opacity: 0.6;
507
+ }
508
+
467
509
  /* ─── TABS BAR ─── */
468
510
  .cpub-tabs-bar {
469
511
  position: sticky;
@@ -19,13 +19,46 @@ export default defineEventHandler(async (event) => {
19
19
  const page = pages.find((p) => p.slug === pageSlug);
20
20
  if (!page) throw createError({ statusCode: 404, statusMessage: 'Page not found' });
21
21
 
22
- // Render markdown to HTML with TOC extraction
23
- const rendered = await renderMarkdown(page.content ?? '');
22
+ // Handle dual-format content: BlockTuple[] (new) or markdown string (legacy)
23
+ const content = page.content;
24
24
 
25
+ if (Array.isArray(content)) {
26
+ // New BlockTuple format — extract text for TOC generation
27
+ const headings = extractHeadingsFromBlocks(content as [string, Record<string, unknown>][]);
28
+ return {
29
+ ...page,
30
+ content,
31
+ html: null, // Client renders blocks directly
32
+ toc: headings,
33
+ frontmatter: {},
34
+ format: 'blocks',
35
+ };
36
+ }
37
+
38
+ // Legacy markdown format
39
+ const rendered = await renderMarkdown((content as string) ?? '');
25
40
  return {
26
41
  ...page,
27
42
  html: rendered.html,
28
43
  toc: rendered.toc,
29
44
  frontmatter: rendered.frontmatter,
45
+ format: 'markdown',
30
46
  };
31
47
  });
48
+
49
+ /** Extract TOC headings from BlockTuple array */
50
+ function extractHeadingsFromBlocks(
51
+ blocks: [string, Record<string, unknown>][],
52
+ ): Array<{ id: string; text: string; level: number }> {
53
+ const headings: Array<{ id: string; text: string; level: number }> = [];
54
+ for (const [type, content] of blocks) {
55
+ if (type === 'heading' && content.text) {
56
+ const text = String(content.text);
57
+ const level = (content.level as number) || 2;
58
+ // Must match BlockHeadingView.vue slugification exactly
59
+ const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
60
+ headings.push({ id, text, level });
61
+ }
62
+ }
63
+ return headings;
64
+ }
@@ -30,5 +30,18 @@ export default defineEventHandler(async (event) => {
30
30
  throw createError({ statusCode: 404, statusMessage: 'No version found for docs site' });
31
31
  }
32
32
 
33
- return listDocsPages(db, version.id);
33
+ const pages = await listDocsPages(db, version.id);
34
+
35
+ // Parse content: if stored as JSON string (BlockTuple[]), parse back to array
36
+ return pages.map((page) => {
37
+ let content: string | unknown[] = page.content ?? '';
38
+ if (typeof content === 'string' && content.startsWith('[')) {
39
+ try {
40
+ content = JSON.parse(content);
41
+ } catch {
42
+ // Not valid JSON — keep as markdown string
43
+ }
44
+ }
45
+ return { ...page, content };
46
+ });
34
47
  });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Migration endpoint: Convert docs pages from markdown (string) to BlockTuple[] (JSON).
3
+ *
4
+ * POST /api/docs/migrate-content
5
+ *
6
+ * Requires admin auth. Converts all docsPages where content is a markdown string
7
+ * into BlockTuple[] JSON format using markdownToBlockTuples.
8
+ *
9
+ * This is a one-time migration. After running, the content column can be altered to JSONB.
10
+ *
11
+ * Run this BEFORE altering the column type:
12
+ * ALTER TABLE docs_pages ALTER COLUMN content TYPE jsonb USING content::jsonb;
13
+ */
14
+ import { markdownToBlockTuples } from '@commonpub/editor';
15
+ import { docsPages } from '@commonpub/schema';
16
+ import { eq } from 'drizzle-orm';
17
+
18
+ export default defineEventHandler(async (event) => {
19
+ const user = requireAuth(event);
20
+
21
+ // Only allow admins to run migration
22
+ if (!user.role || user.role !== 'admin') {
23
+ throw createError({ statusCode: 403, statusMessage: 'Admin only' });
24
+ }
25
+
26
+ const db = useDB();
27
+
28
+ // Fetch all pages
29
+ const allPages = await db.select().from(docsPages);
30
+
31
+ let converted = 0;
32
+ let skipped = 0;
33
+ let errored = 0;
34
+ const errors: Array<{ pageId: string; title: string; error: string }> = [];
35
+
36
+ for (const page of allPages) {
37
+ const content = page.content ?? '';
38
+
39
+ // Skip if already JSON (starts with [ for BlockTuple array)
40
+ if (typeof content === 'string' && content.startsWith('[')) {
41
+ try {
42
+ JSON.parse(content);
43
+ skipped++;
44
+ continue;
45
+ } catch {
46
+ // Not valid JSON, proceed with conversion
47
+ }
48
+ }
49
+
50
+ // Skip empty content
51
+ if (!content || (typeof content === 'string' && !content.trim())) {
52
+ // Set empty content to an empty paragraph block
53
+ try {
54
+ await db
55
+ .update(docsPages)
56
+ .set({ content: JSON.stringify([['paragraph', { html: '' }]]) })
57
+ .where(eq(docsPages.id, page.id));
58
+ converted++;
59
+ } catch (err: unknown) {
60
+ errored++;
61
+ errors.push({
62
+ pageId: page.id,
63
+ title: page.title,
64
+ error: err instanceof Error ? err.message : 'Unknown error',
65
+ });
66
+ }
67
+ continue;
68
+ }
69
+
70
+ // Convert markdown to BlockTuples
71
+ try {
72
+ const blocks = markdownToBlockTuples(content as string);
73
+ const jsonContent = JSON.stringify(blocks);
74
+
75
+ await db
76
+ .update(docsPages)
77
+ .set({ content: jsonContent })
78
+ .where(eq(docsPages.id, page.id));
79
+
80
+ converted++;
81
+ } catch (err: unknown) {
82
+ errored++;
83
+ errors.push({
84
+ pageId: page.id,
85
+ title: page.title,
86
+ error: err instanceof Error ? err.message : 'Unknown error',
87
+ });
88
+ }
89
+ }
90
+
91
+ return {
92
+ total: allPages.length,
93
+ converted,
94
+ skipped,
95
+ errored,
96
+ errors,
97
+ nextStep: errored === 0
98
+ ? 'All pages converted. You can now run: ALTER TABLE docs_pages ALTER COLUMN content TYPE jsonb USING content::jsonb;'
99
+ : 'Some pages failed conversion. Fix errors and re-run.',
100
+ };
101
+ });
@@ -361,3 +361,6 @@
361
361
  /* Logo switch */
362
362
  [data-theme="agora-dark"] .cpub-logo-classic { display: none !important; }
363
363
  [data-theme="agora-dark"] .cpub-logo-agora { display: flex !important; }
364
+
365
+ /* Show Town Square logo in hero section */
366
+ [data-theme="agora-dark"] .cpub-hero-logo-aside { display: flex !important; align-items: center; justify-content: center; }
package/theme/agora.css CHANGED
@@ -470,3 +470,6 @@
470
470
 
471
471
  [data-theme="agora"] .cpub-logo-classic { display: none !important; }
472
472
  [data-theme="agora"] .cpub-logo-agora { display: flex !important; }
473
+
474
+ /* Show Town Square logo in hero section */
475
+ [data-theme="agora"] .cpub-hero-logo-aside { display: flex !important; align-items: center; justify-content: center; }