@commonpub/layer 0.75.0 → 0.76.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.
@@ -0,0 +1,57 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Compact Markdown / HTML segmented toggle for a single rich-text field.
4
+ * Used inline beside a field label so each field's render mode is set
5
+ * independently. `html` mode renders the author's raw HTML (script-free).
6
+ */
7
+ const model = defineModel<'markdown' | 'html'>({ default: 'markdown' });
8
+ </script>
9
+
10
+ <template>
11
+ <div class="cpub-fmt-toggle" role="radiogroup" aria-label="Field format">
12
+ <button
13
+ type="button"
14
+ class="cpub-fmt-opt"
15
+ :class="{ 'cpub-fmt-active': model === 'markdown' }"
16
+ :aria-pressed="model === 'markdown'"
17
+ @click="model = 'markdown'"
18
+ >Markdown</button>
19
+ <button
20
+ type="button"
21
+ class="cpub-fmt-opt"
22
+ :class="{ 'cpub-fmt-active': model === 'html' }"
23
+ :aria-pressed="model === 'html'"
24
+ @click="model = 'html'"
25
+ >HTML</button>
26
+ </div>
27
+ </template>
28
+
29
+ <style scoped>
30
+ .cpub-fmt-toggle {
31
+ display: inline-flex;
32
+ border: var(--border-width-default) solid var(--border);
33
+ background: var(--surface2);
34
+ }
35
+ .cpub-fmt-opt {
36
+ font-family: var(--font-mono);
37
+ font-size: 10px;
38
+ text-transform: uppercase;
39
+ letter-spacing: 0.06em;
40
+ padding: 3px 10px;
41
+ background: transparent;
42
+ color: var(--text-faint);
43
+ border: none;
44
+ cursor: pointer;
45
+ }
46
+ .cpub-fmt-opt + .cpub-fmt-opt {
47
+ border-left: var(--border-width-default) solid var(--border);
48
+ }
49
+ .cpub-fmt-active {
50
+ background: var(--accent);
51
+ color: var(--accent-contrast, #fff);
52
+ }
53
+ .cpub-fmt-opt:focus-visible {
54
+ outline: 2px solid var(--accent);
55
+ outline-offset: -2px;
56
+ }
57
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonpub/layer",
3
- "version": "0.75.0",
3
+ "version": "0.76.0",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "files": [
@@ -54,16 +54,16 @@
54
54
  "vue-router": "^4.3.0",
55
55
  "zod": "^4.3.6",
56
56
  "@commonpub/auth": "0.8.0",
57
+ "@commonpub/config": "0.22.1",
57
58
  "@commonpub/docs": "0.6.3",
59
+ "@commonpub/editor": "0.7.12",
58
60
  "@commonpub/learning": "0.5.2",
59
- "@commonpub/server": "2.85.0",
60
- "@commonpub/theme-studio": "0.6.1",
61
- "@commonpub/config": "0.22.1",
62
- "@commonpub/explainer": "0.7.15",
63
- "@commonpub/schema": "0.42.0",
61
+ "@commonpub/schema": "0.43.0",
62
+ "@commonpub/protocol": "0.13.0",
63
+ "@commonpub/server": "2.86.0",
64
64
  "@commonpub/ui": "0.13.1",
65
- "@commonpub/editor": "0.7.12",
66
- "@commonpub/protocol": "0.13.0"
65
+ "@commonpub/explainer": "0.7.15",
66
+ "@commonpub/theme-studio": "0.6.1"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@testing-library/jest-dom": "^6.9.1",
@@ -29,8 +29,10 @@ function slugify(s: string): string {
29
29
  const subheading = ref('');
30
30
  const description = ref('');
31
31
  const rules = ref('');
32
- // Render mode for description/rules/prizes overview: Markdown (default) or raw HTML.
33
- const contentFormat = ref<'markdown' | 'html'>('markdown');
32
+ // Per-field render mode: Markdown (default) or raw HTML, independent per field.
33
+ const descriptionFormat = ref<'markdown' | 'html'>('markdown');
34
+ const rulesFormat = ref<'markdown' | 'html'>('markdown');
35
+ const prizesDescriptionFormat = ref<'markdown' | 'html'>('markdown');
34
36
  const bannerUrl = ref('');
35
37
  const coverImageUrl = ref('');
36
38
  const startDate = ref('');
@@ -88,7 +90,9 @@ watch(contest, (c) => {
88
90
  subheading.value = c.subheading ?? '';
89
91
  description.value = c.description ?? '';
90
92
  rules.value = c.rules ?? '';
91
- contentFormat.value = (c.contentFormat as 'markdown' | 'html') ?? 'markdown';
93
+ descriptionFormat.value = (c.descriptionFormat as 'markdown' | 'html') ?? 'markdown';
94
+ rulesFormat.value = (c.rulesFormat as 'markdown' | 'html') ?? 'markdown';
95
+ prizesDescriptionFormat.value = (c.prizesDescriptionFormat as 'markdown' | 'html') ?? 'markdown';
92
96
  bannerUrl.value = c.bannerUrl ?? '';
93
97
  coverImageUrl.value = c.coverImageUrl ?? '';
94
98
  startDate.value = c.startDate ? new Date(c.startDate).toISOString().slice(0, 16) : '';
@@ -127,7 +131,7 @@ watch(contest, (c) => {
127
131
  // Mark the form dirty on any post-hydration edit (gives the save bar its
128
132
  // "unsaved changes" cue). Worst case (timing) is a harmless early "dirty".
129
133
  watch(
130
- [title, slugInput, subheading, description, rules, contentFormat, bannerUrl, coverImageUrl, startDate, endDate, judgingEndDate,
134
+ [title, slugInput, subheading, description, rules, descriptionFormat, rulesFormat, prizesDescriptionFormat, bannerUrl, coverImageUrl, startDate, endDate, judgingEndDate,
131
135
  communityVotingEnabled, judgingVisibility, eligibleContentTypes, maxEntriesPerUser, visibility, visibleToRoles,
132
136
  showPrizes, stages, currentStageIdRef, prizesDescription, prizes, criteria],
133
137
  () => { if (!hydratingForm) formDirty.value = true; },
@@ -198,7 +202,9 @@ async function handleSave(): Promise<void> {
198
202
  subheading: subheading.value || undefined,
199
203
  description: description.value || undefined,
200
204
  rules: rules.value || undefined,
201
- contentFormat: contentFormat.value,
205
+ descriptionFormat: descriptionFormat.value,
206
+ rulesFormat: rulesFormat.value,
207
+ prizesDescriptionFormat: prizesDescriptionFormat.value,
202
208
  bannerUrl: bannerUrl.value || undefined,
203
209
  coverImageUrl: coverImageUrl.value || undefined,
204
210
  startDate: startDate.value ? new Date(startDate.value).toISOString() : undefined,
@@ -352,25 +358,20 @@ async function transitionStatus(newStatus: string): Promise<void> {
352
358
  <p class="cpub-form-hint">Short plain-text tagline shown under the title in the hero. The Description below is the full body.</p>
353
359
  </div>
354
360
  <div class="cpub-form-field">
355
- <label class="cpub-form-label">Content format</label>
356
- <div class="cpub-type-options" role="radiogroup" aria-label="Content format">
357
- <label class="cpub-form-check"><input v-model="contentFormat" type="radio" value="markdown" /> <span>Markdown</span></label>
358
- <label class="cpub-form-check"><input v-model="contentFormat" type="radio" value="html" /> <span>Full HTML</span></label>
361
+ <div class="cpub-field-head">
362
+ <label class="cpub-form-label">Description</label>
363
+ <FormatToggle v-model="descriptionFormat" />
359
364
  </div>
360
- <p class="cpub-form-hint">
361
- Applies to Description, Rules, and the Prizes overview. <strong>Markdown</strong> supports headings, lists, links, and safe inline HTML.
362
- <strong>Full HTML</strong> renders your raw HTML, CSS, and SVG as-is (scripts and event handlers are removed for safety).
363
- </p>
364
- </div>
365
- <div class="cpub-form-field">
366
- <label class="cpub-form-label">Description</label>
367
365
  <textarea v-model="description" class="cpub-form-textarea" rows="4" maxlength="50000" />
368
- <p class="cpub-form-hint">{{ contentFormat === 'html' ? 'Rendered as full HTML/CSS/SVG.' : 'Supports Markdown (headings, lists, bold, links) and inline HTML.' }} Shown formatted on the contest page.</p>
366
+ <p class="cpub-form-hint">{{ descriptionFormat === 'html' ? 'Rendered as your raw HTML, CSS, and SVG (scripts removed for safety).' : 'Supports Markdown (headings, lists, bold, links) and inline HTML.' }} Shown formatted on the contest page.</p>
369
367
  </div>
370
368
  <div class="cpub-form-field">
371
- <label class="cpub-form-label">Rules</label>
369
+ <div class="cpub-field-head">
370
+ <label class="cpub-form-label">Rules</label>
371
+ <FormatToggle v-model="rulesFormat" />
372
+ </div>
372
373
  <textarea v-model="rules" class="cpub-form-textarea" rows="6" maxlength="50000" placeholder="One rule per line, or full Markdown" />
373
- <p class="cpub-form-hint">Supports Markdown. Plain one-rule-per-line text is rendered as a numbered list.</p>
374
+ <p class="cpub-form-hint">{{ rulesFormat === 'html' ? 'Rendered as your raw HTML, CSS, and SVG (scripts removed for safety).' : 'Supports Markdown. Plain one-rule-per-line text renders as a list.' }}</p>
374
375
  </div>
375
376
  <div class="cpub-form-field">
376
377
  <ImageUpload v-model="bannerUrl" purpose="banner" label="Banner Image" hint="Wide hero image across the top of the contest page (~4:1)." />
@@ -457,9 +458,12 @@ async function transitionStatus(newStatus: string): Promise<void> {
457
458
  <p v-if="!showPrizes" class="cpub-form-hint">The Prizes tab is hidden, any prizes below are saved but not shown to visitors.</p>
458
459
  <p class="cpub-form-hint">Every field is optional. Use <strong>place</strong> for ranked prizes, a <strong>category</strong> for themed awards, or just a <strong>description</strong>, whatever fits. Cash value is optional.</p>
459
460
  <div class="cpub-form-field">
460
- <label class="cpub-form-label">Prizes overview (optional)</label>
461
- <textarea v-model="prizesDescription" class="cpub-form-textarea" rows="3" maxlength="50000" placeholder="Intro shown above the prize cards. Supports Markdown." />
462
- <p class="cpub-form-hint">Markdown intro displayed on the Prizes tab, above the individual prizes.</p>
461
+ <div class="cpub-field-head">
462
+ <label class="cpub-form-label">Prizes overview (optional)</label>
463
+ <FormatToggle v-model="prizesDescriptionFormat" />
464
+ </div>
465
+ <textarea v-model="prizesDescription" class="cpub-form-textarea" rows="3" maxlength="50000" placeholder="Intro shown above the prize cards." />
466
+ <p class="cpub-form-hint">{{ prizesDescriptionFormat === 'html' ? 'Rendered as your raw HTML, CSS, and SVG (scripts removed for safety).' : 'Markdown intro' }} displayed on the Prizes tab, above the individual prizes.</p>
463
467
  </div>
464
468
  <div v-for="(prize, i) in prizes" :key="i" class="cpub-prize-row">
465
469
  <div class="cpub-prize-header">
@@ -675,6 +679,7 @@ async function transitionStatus(newStatus: string): Promise<void> {
675
679
  .cpub-form-section { border: var(--border-width-default) solid var(--border); background: var(--surface); padding: 20px; box-shadow: var(--shadow-md); }
676
680
  .cpub-form-section-title { font-size: 14px; font-weight: 700; margin-bottom: 14px; }
677
681
  .cpub-form-field { display: flex; flex-direction: column; gap: var(--space-1); margin-bottom: var(--space-3); }
682
+ .cpub-field-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
678
683
  .cpub-form-field:last-child { margin-bottom: 0; }
679
684
  .cpub-form-input, .cpub-form-textarea { width: 100%; padding: var(--space-2) var(--space-3); border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text); font-size: var(--text-sm); font-family: var(--font-sans); }
680
685
  .cpub-form-input:focus, .cpub-form-textarea:focus { border-color: var(--accent); outline: none; box-shadow: var(--shadow-accent); }
@@ -312,7 +312,7 @@ async function withdrawEntry(entryId: string): Promise<void> {
312
312
  <div class="cpub-about-section">
313
313
  <div class="cpub-sec-head"><h2><i class="fa fa-circle-info" style="color: var(--accent);"></i> About This Contest</h2></div>
314
314
  <div class="cpub-about-card">
315
- <CpubMarkdown v-if="c?.description" :source="c.description" :format="c?.contentFormat" />
315
+ <CpubMarkdown v-if="c?.description" :source="c.description" :format="c?.descriptionFormat" />
316
316
  <p v-else>No description available for this contest.</p>
317
317
  </div>
318
318
  </div>
@@ -321,12 +321,12 @@ async function withdrawEntry(entryId: string): Promise<void> {
321
321
 
322
322
  <!-- RULES -->
323
323
  <div v-show="activeTab === 'rules'" id="cpub-panel-rules" role="tabpanel" aria-labelledby="cpub-tab-rules" tabindex="0">
324
- <ContestRules v-if="c?.rules" :rules="c.rules" :format="c?.contentFormat" />
324
+ <ContestRules v-if="c?.rules" :rules="c.rules" :format="c?.rulesFormat" />
325
325
  </div>
326
326
 
327
327
  <!-- PRIZES -->
328
328
  <div v-show="activeTab === 'prizes'" id="cpub-panel-prizes" role="tabpanel" aria-labelledby="cpub-tab-prizes" tabindex="0">
329
- <ContestPrizes v-if="c?.showPrizes !== false && (c?.prizes?.length || c?.prizesDescription)" :prizes="c?.prizes ?? []" :description="c?.prizesDescription" :format="c?.contentFormat" />
329
+ <ContestPrizes v-if="c?.showPrizes !== false && (c?.prizes?.length || c?.prizesDescription)" :prizes="c?.prizes ?? []" :description="c?.prizesDescription" :format="c?.prizesDescriptionFormat" />
330
330
  </div>
331
331
 
332
332
  <!-- ENTRIES -->
@@ -20,7 +20,9 @@ watch(title, (t) => { if (!slugTouched.value) slug.value = slugify(t); });
20
20
  const subheading = ref('');
21
21
  const description = ref('');
22
22
  const rules = ref('');
23
- const contentFormat = ref<'markdown' | 'html'>('markdown');
23
+ const descriptionFormat = ref<'markdown' | 'html'>('markdown');
24
+ const rulesFormat = ref<'markdown' | 'html'>('markdown');
25
+ const prizesDescriptionFormat = ref<'markdown' | 'html'>('markdown');
24
26
  const bannerUrl = ref('');
25
27
  const coverImageUrl = ref('');
26
28
  const startDate = ref('');
@@ -111,7 +113,9 @@ async function handleCreate(): Promise<void> {
111
113
  subheading: subheading.value || undefined,
112
114
  description: description.value || undefined,
113
115
  rules: rules.value || undefined,
114
- contentFormat: contentFormat.value,
116
+ descriptionFormat: descriptionFormat.value,
117
+ rulesFormat: rulesFormat.value,
118
+ prizesDescriptionFormat: prizesDescriptionFormat.value,
115
119
  bannerUrl: bannerUrl.value || undefined,
116
120
  coverImageUrl: coverImageUrl.value || undefined,
117
121
  startDate: new Date(startDate.value).toISOString(),
@@ -188,25 +192,20 @@ function prizeLabel(prize: Prize): string {
188
192
  <p class="cpub-form-hint">Short plain-text tagline shown under the title in the hero. The Description below is the full body.</p>
189
193
  </div>
190
194
  <div class="cpub-form-field">
191
- <label class="cpub-form-label">Content format</label>
192
- <div class="cpub-type-options" role="radiogroup" aria-label="Content format">
193
- <label class="cpub-form-check"><input v-model="contentFormat" type="radio" value="markdown" /> <span>Markdown</span></label>
194
- <label class="cpub-form-check"><input v-model="contentFormat" type="radio" value="html" /> <span>Full HTML</span></label>
195
+ <div class="cpub-field-head">
196
+ <label for="contest-desc" class="cpub-form-label">Description</label>
197
+ <FormatToggle v-model="descriptionFormat" />
195
198
  </div>
196
- <p class="cpub-form-hint">
197
- Applies to Description, Rules, and the Prizes overview. <strong>Markdown</strong> supports headings, lists, links, and safe inline HTML.
198
- <strong>Full HTML</strong> renders your raw HTML, CSS, and SVG as-is (scripts and event handlers are removed for safety).
199
- </p>
200
- </div>
201
- <div class="cpub-form-field">
202
- <label for="contest-desc" class="cpub-form-label">Description</label>
203
199
  <textarea id="contest-desc" v-model="description" class="cpub-form-textarea" rows="4" maxlength="50000" placeholder="Describe your contest. Supports Markdown, # headings, - lists, **bold**, [links](url)…" />
204
- <p class="cpub-form-hint">{{ contentFormat === 'html' ? 'Rendered as full HTML/CSS/SVG.' : 'Supports Markdown (headings, lists, bold, links) and inline HTML.' }} Shown formatted on the contest page.</p>
200
+ <p class="cpub-form-hint">{{ descriptionFormat === 'html' ? 'Rendered as your raw HTML, CSS, and SVG (scripts removed for safety).' : 'Supports Markdown (headings, lists, bold, links) and inline HTML.' }} Shown formatted on the contest page.</p>
205
201
  </div>
206
202
  <div class="cpub-form-field">
207
- <label for="contest-rules" class="cpub-form-label">Rules</label>
203
+ <div class="cpub-field-head">
204
+ <label for="contest-rules" class="cpub-form-label">Rules</label>
205
+ <FormatToggle v-model="rulesFormat" />
206
+ </div>
208
207
  <textarea id="contest-rules" v-model="rules" class="cpub-form-textarea" rows="6" maxlength="50000" placeholder="Contest rules and requirements. Supports Markdown, one rule per line, or full Markdown." />
209
- <p class="cpub-form-hint">Supports Markdown. Plain one-rule-per-line text is rendered as a numbered list.</p>
208
+ <p class="cpub-form-hint">{{ rulesFormat === 'html' ? 'Rendered as your raw HTML, CSS, and SVG (scripts removed for safety).' : 'Supports Markdown. Plain one-rule-per-line text renders as a list.' }}</p>
210
209
  </div>
211
210
  <div class="cpub-form-field">
212
211
  <ImageUpload v-model="bannerUrl" purpose="banner" label="Banner Image" hint="Wide hero image across the top of the contest page (~4:1)." />
@@ -348,9 +347,12 @@ function prizeLabel(prize: Prize): string {
348
347
 
349
348
  <p class="cpub-form-hint">Contests don't need prizes, leave this empty to skip them entirely. If you do add prizes, every field is optional: use <strong>place</strong> for ranked prizes (1st/2nd/3rd), a <strong>category</strong> for themed awards (e.g. "Best in Show"), or just a <strong>description</strong>. Cash value is optional.</p>
350
349
  <div class="cpub-form-field">
351
- <label for="prizes-desc" class="cpub-form-label">Prizes overview (optional)</label>
352
- <textarea id="prizes-desc" v-model="prizesDescription" class="cpub-form-textarea" rows="3" maxlength="50000" placeholder="Intro shown above the prize cards. Supports Markdown." />
353
- <p class="cpub-form-hint">Markdown intro displayed on the Prizes tab, above the individual prizes.</p>
350
+ <div class="cpub-field-head">
351
+ <label for="prizes-desc" class="cpub-form-label">Prizes overview (optional)</label>
352
+ <FormatToggle v-model="prizesDescriptionFormat" />
353
+ </div>
354
+ <textarea id="prizes-desc" v-model="prizesDescription" class="cpub-form-textarea" rows="3" maxlength="50000" placeholder="Intro shown above the prize cards." />
355
+ <p class="cpub-form-hint">{{ prizesDescriptionFormat === 'html' ? 'Rendered as your raw HTML, CSS, and SVG (scripts removed for safety).' : 'Markdown intro' }} displayed on the Prizes tab, above the individual prizes.</p>
354
356
  </div>
355
357
  <div v-for="(prize, idx) in prizes" :key="idx" class="cpub-prize-card">
356
358
  <div class="cpub-prize-header">
@@ -417,6 +419,7 @@ function prizeLabel(prize: Prize): string {
417
419
  .cpub-form-section-header .cpub-form-section-title { margin-bottom: 0; }
418
420
 
419
421
  .cpub-form-field { display: flex; flex-direction: column; gap: var(--space-1); margin-bottom: var(--space-3); }
422
+ .cpub-field-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
420
423
  .cpub-form-field:last-child { margin-bottom: 0; }
421
424
  .cpub-form-input, .cpub-form-textarea { width: 100%; padding: var(--space-2) var(--space-3); border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text); font-size: var(--text-sm); font-family: var(--font-sans); }
422
425
  .cpub-form-input:focus, .cpub-form-textarea:focus { border-color: var(--accent); outline: none; box-shadow: var(--shadow-accent); }