@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.
|
|
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/
|
|
60
|
-
"@commonpub/
|
|
61
|
-
"@commonpub/
|
|
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/
|
|
66
|
-
"@commonpub/
|
|
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
|
-
//
|
|
33
|
-
const
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
<
|
|
356
|
-
|
|
357
|
-
<
|
|
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">{{
|
|
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
|
-
<
|
|
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
|
|
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
|
-
<
|
|
461
|
-
|
|
462
|
-
|
|
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?.
|
|
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?.
|
|
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?.
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
192
|
-
|
|
193
|
-
<
|
|
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">{{
|
|
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
|
-
<
|
|
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
|
|
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
|
-
<
|
|
352
|
-
|
|
353
|
-
|
|
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); }
|