@commonpub/layer 0.54.0 → 0.56.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.
|
@@ -45,6 +45,25 @@ function onDate(i: number, field: 'startsAt' | 'endsAt', e: Event): void {
|
|
|
45
45
|
setField(i, { [field]: v } as Partial<ContestStage>);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Per-round rubric (review stages). Immutable updates via setField.
|
|
49
|
+
type StageCriterion = { label: string; weight?: number; description?: string };
|
|
50
|
+
function addCriterion(i: number): void {
|
|
51
|
+
const cur = (stages.value[i]?.criteria ?? []) as StageCriterion[];
|
|
52
|
+
setField(i, { criteria: [...cur, { label: '' }] });
|
|
53
|
+
}
|
|
54
|
+
function setCriterion(i: number, ci: number, patch: Partial<StageCriterion>): void {
|
|
55
|
+
const cur = (stages.value[i]?.criteria ?? []).map((c, idx) => (idx === ci ? { ...c, ...patch } : c));
|
|
56
|
+
setField(i, { criteria: cur });
|
|
57
|
+
}
|
|
58
|
+
function removeCriterion(i: number, ci: number): void {
|
|
59
|
+
const cur = (stages.value[i]?.criteria ?? []).filter((_, idx) => idx !== ci);
|
|
60
|
+
setField(i, { criteria: cur.length ? cur : undefined });
|
|
61
|
+
}
|
|
62
|
+
function critWeightInput(i: number, ci: number, e: Event): void {
|
|
63
|
+
const v = (e.target as HTMLInputElement).value;
|
|
64
|
+
setCriterion(i, ci, { weight: v === '' ? undefined : Math.max(0, Math.min(100, Math.round(Number(v)))) });
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
// Array operations live as pure functions in utils/contestStages.ts (unit-tested).
|
|
49
68
|
function addStage(): void {
|
|
50
69
|
commit(withStageAdded(stages.value));
|
|
@@ -147,6 +166,8 @@ const missingSubmission = computed(() => stages.value.length > 0 && !stages.valu
|
|
|
147
166
|
</div>
|
|
148
167
|
</div>
|
|
149
168
|
|
|
169
|
+
<p class="cpub-stage-kind-help"><i class="fa-solid fa-circle-info"></i> {{ STAGE_KIND_HELP[stage.kind] }}</p>
|
|
170
|
+
|
|
150
171
|
<div class="cpub-form-row">
|
|
151
172
|
<div class="cpub-form-field">
|
|
152
173
|
<label class="cpub-form-label">Starts</label>
|
|
@@ -169,6 +190,20 @@ const missingSubmission = computed(() => stages.value.length > 0 && !stages.valu
|
|
|
169
190
|
/>
|
|
170
191
|
</div>
|
|
171
192
|
|
|
193
|
+
<!-- Per-round judging rubric (review stages) -->
|
|
194
|
+
<div v-if="stage.kind === 'review'" class="cpub-stage-criteria">
|
|
195
|
+
<div class="cpub-stage-criteria-head">
|
|
196
|
+
<span class="cpub-form-label" style="margin: 0;">Judging criteria — this round</span>
|
|
197
|
+
<button type="button" class="cpub-btn cpub-btn-sm" @click="addCriterion(i)"><i class="fa-solid fa-plus"></i> Add</button>
|
|
198
|
+
</div>
|
|
199
|
+
<p class="cpub-form-hint" style="margin: 4px 0;">Optional — leave empty to use the contest’s default criteria. Set per-round criteria for multi-round contests (e.g. judge proposals on Feasibility, prototypes on Deployment readiness).</p>
|
|
200
|
+
<div v-for="(crit, ci) in (stage.criteria ?? [])" :key="ci" class="cpub-stage-crit-row">
|
|
201
|
+
<input :value="crit.label" type="text" class="cpub-form-input" placeholder="Criterion (e.g. Community impact)" @input="setCriterion(i, ci, { label: ($event.target as HTMLInputElement).value })" />
|
|
202
|
+
<input :value="crit.weight ?? ''" type="number" min="0" max="100" class="cpub-form-input cpub-stage-crit-pts" placeholder="pts" @input="critWeightInput(i, ci, $event)" />
|
|
203
|
+
<button type="button" class="cpub-stage-iconbtn cpub-stage-del" aria-label="Remove criterion" @click="removeCriterion(i, ci)"><i class="fa-solid fa-xmark"></i></button>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
172
207
|
<div v-if="stage.kind === 'event'" class="cpub-form-row">
|
|
173
208
|
<div class="cpub-form-field">
|
|
174
209
|
<label class="cpub-form-label">Location</label>
|
|
@@ -190,6 +225,16 @@ const missingSubmission = computed(() => stages.value.length > 0 && !stages.valu
|
|
|
190
225
|
</template>
|
|
191
226
|
|
|
192
227
|
<style scoped>
|
|
228
|
+
/* Self-contained form-control styles (tokenised) — Vue scoped CSS doesn't cross
|
|
229
|
+
component boundaries, so this extracted editor styles its own inputs rather than
|
|
230
|
+
relying on the parent page. Mirrors the contest pages' controls. */
|
|
231
|
+
.cpub-form-field { display: flex; flex-direction: column; gap: var(--space-1); margin-bottom: var(--space-3); }
|
|
232
|
+
.cpub-form-field:last-child { margin-bottom: 0; }
|
|
233
|
+
.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); }
|
|
234
|
+
.cpub-form-input:focus, .cpub-form-textarea:focus { border-color: var(--accent); outline: none; box-shadow: var(--shadow-accent); }
|
|
235
|
+
.cpub-form-textarea { resize: vertical; }
|
|
236
|
+
.cpub-form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--space-3); }
|
|
237
|
+
|
|
193
238
|
.cpub-stages-standard { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
|
|
194
239
|
.cpub-stage-tophead { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
195
240
|
.cpub-stage-count { font-size: 11px; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: .06em; color: var(--text-faint); }
|
|
@@ -206,4 +251,11 @@ const missingSubmission = computed(() => stages.value.length > 0 && !stages.valu
|
|
|
206
251
|
.cpub-stage-del:hover { border-color: var(--red-border); color: var(--red); }
|
|
207
252
|
.cpub-stage-toolbar { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
|
|
208
253
|
.cpub-stage-reset { color: var(--text-faint); }
|
|
254
|
+
.cpub-stage-kind-help { font-size: 11px; color: var(--text-faint); line-height: 1.5; margin: 0 0 4px; display: flex; gap: 6px; }
|
|
255
|
+
.cpub-stage-kind-help i { color: var(--accent); margin-top: 2px; flex-shrink: 0; }
|
|
256
|
+
.cpub-stage-criteria { border: var(--border-width-default) dashed var(--border2); padding: 10px; margin-top: 4px; background: var(--surface); }
|
|
257
|
+
.cpub-stage-criteria-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
258
|
+
.cpub-stage-crit-row { display: flex; align-items: center; gap: 6px; margin-top: 6px; }
|
|
259
|
+
.cpub-stage-crit-row .cpub-form-input { margin: 0; }
|
|
260
|
+
.cpub-stage-crit-pts { max-width: 70px; flex-shrink: 0; }
|
|
209
261
|
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -55,14 +55,14 @@
|
|
|
55
55
|
"zod": "^4.3.6",
|
|
56
56
|
"@commonpub/auth": "0.8.0",
|
|
57
57
|
"@commonpub/config": "0.18.0",
|
|
58
|
-
"@commonpub/editor": "0.7.11",
|
|
59
58
|
"@commonpub/docs": "0.6.3",
|
|
60
|
-
"@commonpub/
|
|
59
|
+
"@commonpub/editor": "0.7.11",
|
|
61
60
|
"@commonpub/explainer": "0.7.15",
|
|
62
|
-
"@commonpub/
|
|
63
|
-
"@commonpub/server": "2.
|
|
61
|
+
"@commonpub/schema": "0.31.0",
|
|
62
|
+
"@commonpub/server": "2.78.0",
|
|
63
|
+
"@commonpub/protocol": "0.13.0",
|
|
64
64
|
"@commonpub/ui": "0.9.2",
|
|
65
|
-
"@commonpub/
|
|
65
|
+
"@commonpub/learning": "0.5.2"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -574,9 +574,12 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
574
574
|
.cpub-edit-side { display: flex; flex-direction: column; gap: 16px; position: sticky; top: 76px; }
|
|
575
575
|
.cpub-form-section { border: var(--border-width-default) solid var(--border); background: var(--surface); padding: 20px; box-shadow: var(--shadow-md); }
|
|
576
576
|
.cpub-form-section-title { font-size: 14px; font-weight: 700; margin-bottom: 14px; }
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
577
|
+
.cpub-form-field { display: flex; flex-direction: column; gap: var(--space-1); margin-bottom: var(--space-3); }
|
|
578
|
+
.cpub-form-field:last-child { margin-bottom: 0; }
|
|
579
|
+
.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); }
|
|
580
|
+
.cpub-form-input:focus, .cpub-form-textarea:focus { border-color: var(--accent); outline: none; box-shadow: var(--shadow-accent); }
|
|
581
|
+
.cpub-form-textarea { resize: vertical; }
|
|
582
|
+
.cpub-form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--space-3); }
|
|
580
583
|
|
|
581
584
|
.cpub-form-error { font-size: 12px; color: var(--red); margin-top: 8px; }
|
|
582
585
|
.cpub-form-check { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-dim); cursor: pointer; }
|
|
@@ -15,9 +15,23 @@ const { data: entriesData, refresh: refreshEntries } = useLazyFetch<{ items: (Se
|
|
|
15
15
|
{ query: { includeJudgeScores: true } },
|
|
16
16
|
);
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
const
|
|
18
|
+
// The current review stage (multi-round contests). Drives the round label + the
|
|
19
|
+
// per-round rubric; falls back to the contest-level rubric when the stage has none.
|
|
20
|
+
const currentReviewStage = computed(() => {
|
|
21
|
+
const c = contest.value;
|
|
22
|
+
if (!c || !c.stages?.length) return null;
|
|
23
|
+
const cid = currentStageId(c);
|
|
24
|
+
const st = c.stages.find((s) => s.id === cid);
|
|
25
|
+
return st && st.kind === 'review' ? st : null;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Judging rubric: per-round criteria if the current review stage defines them,
|
|
29
|
+
// else the contest-level rubric. Judges score each criterion (0..max); the overall
|
|
30
|
+
// is the normalized weighted sum (computed server-side).
|
|
31
|
+
const criteria = computed(() => {
|
|
32
|
+
const stageCrit = currentReviewStage.value?.criteria;
|
|
33
|
+
return (stageCrit && stageCrit.length ? stageCrit : contest.value?.judgingCriteria) ?? [];
|
|
34
|
+
});
|
|
21
35
|
const hasCriteria = computed(() => criteria.value.length > 0);
|
|
22
36
|
function critMax(i: number): number {
|
|
23
37
|
const w = criteria.value[i]?.weight;
|
|
@@ -48,7 +62,9 @@ async function acceptInvite(): Promise<void> {
|
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
const entryList = computed(() => {
|
|
51
|
-
|
|
65
|
+
// Cohort scope: once a review stage has culled the field, judges only score the
|
|
66
|
+
// surviving cohort (eliminated entries drop out of later rounds).
|
|
67
|
+
const items = (entriesData.value?.items ?? []).filter((e) => !e.eliminated);
|
|
52
68
|
return items.map((entry) => {
|
|
53
69
|
const myScore = entry.judgeScores?.find((s) => s.judgeId === user.value?.id);
|
|
54
70
|
return {
|
|
@@ -161,8 +177,12 @@ async function submitScore(entryId: string): Promise<void> {
|
|
|
161
177
|
<h1 class="cpub-judge-title">
|
|
162
178
|
<i class="fa-solid fa-gavel cpub-judge-icon"></i>
|
|
163
179
|
Judge: {{ contest?.title || 'Contest' }}
|
|
180
|
+
<span v-if="currentReviewStage" class="cpub-judge-round">{{ currentReviewStage.name }}</span>
|
|
164
181
|
</h1>
|
|
165
|
-
<p class="cpub-judge-desc">
|
|
182
|
+
<p class="cpub-judge-desc">
|
|
183
|
+
Score each entry from 1 to 100. Add optional feedback. Scores are saved immediately.
|
|
184
|
+
<template v-if="currentReviewStage"> You're judging the <strong>{{ entryList.length }}</strong> {{ entryList.length === 1 ? 'entry' : 'entries' }} still in this round.</template>
|
|
185
|
+
</p>
|
|
166
186
|
</header>
|
|
167
187
|
|
|
168
188
|
<!-- Loading -->
|
|
@@ -200,9 +220,9 @@ async function submitScore(entryId: string): Promise<void> {
|
|
|
200
220
|
Scoring opens when the contest enters the judging phase (currently <strong>{{ contest.status }}</strong>).
|
|
201
221
|
</div>
|
|
202
222
|
|
|
203
|
-
<!-- Rubric guidance -->
|
|
204
|
-
<div v-if="
|
|
205
|
-
<ContestJudgingCriteria :criteria="
|
|
223
|
+
<!-- Rubric guidance (per-round criteria when the current review stage defines them) -->
|
|
224
|
+
<div v-if="criteria.length" class="cpub-judge-rubric">
|
|
225
|
+
<ContestJudgingCriteria :criteria="criteria" compact />
|
|
206
226
|
</div>
|
|
207
227
|
|
|
208
228
|
<!-- Progress bar -->
|
|
@@ -298,6 +318,7 @@ async function submitScore(entryId: string): Promise<void> {
|
|
|
298
318
|
.cpub-judge-back:hover { color: var(--accent); }
|
|
299
319
|
.cpub-judge-title { font-size: 20px; font-weight: 700; display: flex; align-items: center; gap: 10px; }
|
|
300
320
|
.cpub-judge-icon { color: var(--accent); font-size: 18px; }
|
|
321
|
+
.cpub-judge-round { font-size: 10px; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: .08em; color: var(--accent); border: var(--border-width-default) solid var(--accent-border); background: var(--accent-bg); padding: 3px 9px; border-radius: var(--radius); }
|
|
301
322
|
.cpub-judge-desc { font-size: 13px; color: var(--text-dim); margin-top: 6px; }
|
|
302
323
|
|
|
303
324
|
.cpub-judge-unauthorized { text-align: center; padding: 48px 0; color: var(--text-faint); font-size: 13px; display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
|
@@ -403,8 +403,12 @@ function prizeLabel(prize: Prize): string {
|
|
|
403
403
|
.cpub-form-section-title { font-size: 14px; font-weight: 700; margin-bottom: 16px; }
|
|
404
404
|
.cpub-form-section-header .cpub-form-section-title { margin-bottom: 0; }
|
|
405
405
|
|
|
406
|
-
|
|
407
|
-
|
|
406
|
+
.cpub-form-field { display: flex; flex-direction: column; gap: var(--space-1); margin-bottom: var(--space-3); }
|
|
407
|
+
.cpub-form-field:last-child { margin-bottom: 0; }
|
|
408
|
+
.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); }
|
|
409
|
+
.cpub-form-input:focus, .cpub-form-textarea:focus { border-color: var(--accent); outline: none; box-shadow: var(--shadow-accent); }
|
|
410
|
+
.cpub-form-textarea { resize: vertical; }
|
|
411
|
+
.cpub-form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--space-3); }
|
|
408
412
|
|
|
409
413
|
.cpub-prize-card { border: var(--border-width-default) solid var(--border); padding: 14px; margin-bottom: 10px; background: var(--surface2); }
|
|
410
414
|
.cpub-prize-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
package/utils/contestStages.ts
CHANGED
|
@@ -112,3 +112,14 @@ export const STAGE_KIND_LABEL: Record<ContestStage['kind'], string> = {
|
|
|
112
112
|
event: 'Event / Showcase',
|
|
113
113
|
custom: 'Custom milestone',
|
|
114
114
|
};
|
|
115
|
+
|
|
116
|
+
/** What each stage kind actually DOES — shown under the editor's type picker so
|
|
117
|
+
* organisers understand the behaviour they're choosing. */
|
|
118
|
+
export const STAGE_KIND_HELP: Record<ContestStage['kind'], string> = {
|
|
119
|
+
submission: 'Entrants submit (or, in a later round, refine) entries. The hero countdown targets this stage’s end date.',
|
|
120
|
+
review: 'Judges score entries on a rubric. End a review stage with an Advancement cut (Top-N) to pick who continues. Add per-round criteria below for multi-round contests.',
|
|
121
|
+
interim: 'A working period — e.g. a build sprint. The surviving cohort refines their existing entries; no new entrants.',
|
|
122
|
+
results: 'Final standings are published (ranks calculated from the latest judging round).',
|
|
123
|
+
event: 'A real-world milestone or showcase (date + location). Informational — no entry/judging behaviour.',
|
|
124
|
+
custom: 'An arbitrary dated milestone. No behaviour — just appears on the timeline.',
|
|
125
|
+
};
|