@commonpub/layer 0.55.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>
@@ -216,4 +251,11 @@ const missingSubmission = computed(() => stages.value.length > 0 && !stages.valu
216
251
  .cpub-stage-del:hover { border-color: var(--red-border); color: var(--red); }
217
252
  .cpub-stage-toolbar { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
218
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; }
219
261
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonpub/layer",
3
- "version": "0.55.0",
3
+ "version": "0.56.0",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "files": [
@@ -56,13 +56,13 @@
56
56
  "@commonpub/auth": "0.8.0",
57
57
  "@commonpub/config": "0.18.0",
58
58
  "@commonpub/docs": "0.6.3",
59
+ "@commonpub/editor": "0.7.11",
59
60
  "@commonpub/explainer": "0.7.15",
61
+ "@commonpub/schema": "0.31.0",
62
+ "@commonpub/server": "2.78.0",
60
63
  "@commonpub/protocol": "0.13.0",
61
- "@commonpub/editor": "0.7.11",
62
- "@commonpub/learning": "0.5.2",
63
- "@commonpub/server": "2.77.0",
64
- "@commonpub/schema": "0.30.0",
65
- "@commonpub/ui": "0.9.2"
64
+ "@commonpub/ui": "0.9.2",
65
+ "@commonpub/learning": "0.5.2"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@testing-library/jest-dom": "^6.9.1",
@@ -15,9 +15,23 @@ const { data: entriesData, refresh: refreshEntries } = useLazyFetch<{ items: (Se
15
15
  { query: { includeJudgeScores: true } },
16
16
  );
17
17
 
18
- // Judging rubric: when defined, judges score each criterion (0..max) and the
19
- // overall is the normalized weighted sum (computed server-side).
20
- const criteria = computed(() => contest.value?.judgingCriteria ?? []);
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
- const items = entriesData.value?.items ?? [];
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">Score each entry from 1 to 100. Add optional feedback. Scores are saved immediately.</p>
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="contest.judgingCriteria?.length" class="cpub-judge-rubric">
205
- <ContestJudgingCriteria :criteria="contest.judgingCriteria" compact />
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; }
@@ -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
+ };