@commonpub/layer 0.8.2 → 0.8.4

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.
Files changed (76) hide show
  1. package/components/ContentCard.vue +1 -1
  2. package/components/ImageUpload.vue +1 -1
  3. package/components/ShareToHubModal.vue +1 -1
  4. package/components/blocks/BlockCodeView.vue +26 -25
  5. package/components/contest/ContestEntries.vue +112 -0
  6. package/components/contest/ContestHero.vue +204 -0
  7. package/components/contest/ContestJudges.vue +51 -0
  8. package/components/contest/ContestPrizes.vue +82 -0
  9. package/components/contest/ContestRules.vue +34 -0
  10. package/components/contest/ContestSidebar.vue +83 -0
  11. package/components/editors/ArticleEditor.vue +19 -1
  12. package/components/editors/BlogEditor.vue +1 -1
  13. package/components/editors/DocsPageTree.vue +10 -0
  14. package/components/hub/HubHero.vue +1 -1
  15. package/composables/useSanitize.ts +112 -9
  16. package/layouts/default.vue +7 -7
  17. package/middleware/feature-gate.global.ts +24 -0
  18. package/package.json +8 -8
  19. package/pages/[type]/index.vue +4 -3
  20. package/pages/admin/audit.vue +3 -2
  21. package/pages/admin/federation.vue +9 -1
  22. package/pages/admin/index.vue +7 -1
  23. package/pages/admin/reports.vue +152 -36
  24. package/pages/admin/settings.vue +17 -5
  25. package/pages/admin/theme.vue +5 -3
  26. package/pages/auth/forgot-password.vue +35 -35
  27. package/pages/auth/login.vue +6 -5
  28. package/pages/auth/reset-password.vue +44 -32
  29. package/pages/contests/[slug]/edit.vue +238 -56
  30. package/pages/contests/[slug]/index.vue +54 -450
  31. package/pages/contests/[slug]/judge.vue +141 -53
  32. package/pages/contests/[slug]/results.vue +182 -0
  33. package/pages/contests/create.vue +64 -64
  34. package/pages/contests/index.vue +2 -1
  35. package/pages/docs/[siteSlug]/[...pagePath].vue +6 -5
  36. package/pages/docs/[siteSlug]/edit.vue +58 -2
  37. package/pages/docs/[siteSlug]/index.vue +6 -5
  38. package/pages/federated-hubs/[id]/posts/[postId].vue +2 -2
  39. package/pages/hubs/index.vue +3 -2
  40. package/pages/index.vue +25 -7
  41. package/pages/learn/index.vue +1 -1
  42. package/pages/mirror/[id].vue +3 -3
  43. package/pages/notifications.vue +15 -1
  44. package/pages/settings/notifications.vue +7 -1
  45. package/pages/tags/[slug].vue +3 -2
  46. package/pages/tags/index.vue +3 -2
  47. package/pages/videos/[id].vue +18 -0
  48. package/server/api/admin/content/[id].patch.ts +1 -1
  49. package/server/api/admin/federation/mirrors/[id]/backfill.post.ts +1 -1
  50. package/server/api/admin/federation/refederate.post.ts +7 -3
  51. package/server/api/admin/federation/repair-types.post.ts +2 -45
  52. package/server/api/admin/federation/retry.post.ts +7 -4
  53. package/server/api/admin/reports.get.ts +1 -0
  54. package/server/api/auth/sign-in-username.post.ts +42 -0
  55. package/server/api/content/[id]/products-sync.post.ts +7 -6
  56. package/server/api/contests/[slug]/entries/[entryId].delete.ts +14 -0
  57. package/server/api/contests/[slug]/entries.get.ts +6 -1
  58. package/server/api/contests/[slug]/judge.post.ts +8 -2
  59. package/server/api/docs/[siteSlug]/nav.get.ts +1 -1
  60. package/server/api/docs/[siteSlug]/pages/[pageId]/duplicate.post.ts +16 -0
  61. package/server/api/docs/[siteSlug]/pages/reorder.post.ts +4 -1
  62. package/server/api/docs/migrate-content.post.ts +1 -7
  63. package/server/api/federation/hub-follow-status.get.ts +2 -18
  64. package/server/api/federation/hub-follow.post.ts +9 -27
  65. package/server/api/federation/hub-post-like.post.ts +9 -98
  66. package/server/api/federation/hub-post-likes.get.ts +3 -13
  67. package/server/api/notifications/read.post.ts +6 -1
  68. package/server/api/search/index.get.ts +2 -2
  69. package/server/api/search/trending.get.ts +3 -3
  70. package/server/api/users/index.get.ts +9 -2
  71. package/server/middleware/content-ap.ts +2 -2
  72. package/server/routes/.well-known/webfinger.ts +2 -2
  73. package/theme/base.css +23 -0
  74. package/components/EditorPropertiesPanel.vue +0 -393
  75. package/components/views/BlogView.vue +0 -735
  76. package/server/api/resolve-identity.post.ts +0 -34
@@ -5,8 +5,10 @@ const route = useRoute();
5
5
  const slug = route.params.slug as string;
6
6
  const toast = useToast();
7
7
  const { extract: extractError } = useApiError();
8
+ const { user, isAdmin } = useAuth();
8
9
 
9
10
  const { data: contest, refresh } = useLazyFetch(`/api/contests/${slug}`);
11
+ const isOwner = computed(() => isAdmin.value || !!(user.value?.id && contest.value?.createdById === user.value.id));
10
12
  useSeoMeta({ title: () => `Edit: ${contest.value?.title ?? 'Contest'} — ${useSiteName()}` });
11
13
 
12
14
  const saving = ref(false);
@@ -15,20 +17,95 @@ const description = ref('');
15
17
  const rules = ref('');
16
18
  const startDate = ref('');
17
19
  const endDate = ref('');
20
+ const judgingEndDate = ref('');
21
+ const prizes = ref<Array<{ place: number; title: string; description: string; value: string }>>([]);
22
+ const judgeIds = ref<string[]>([]);
23
+ const judgeSearch = ref('');
24
+ const judgeSearchResults = ref<Array<{ id: string; username: string; displayName: string | null }>>([]);
25
+ const searchingJudges = ref(false);
26
+
27
+ interface JudgeDisplay { id: string; username: string; displayName: string | null }
28
+ const resolvedJudges = ref<JudgeDisplay[]>([]);
18
29
 
19
30
  // Load current data
20
- watch(contest, (c) => {
31
+ watch(contest, async (c) => {
21
32
  if (!c) return;
22
33
  title.value = c.title ?? '';
23
34
  description.value = c.description ?? '';
24
35
  rules.value = c.rules ?? '';
25
36
  startDate.value = c.startDate ? new Date(c.startDate).toISOString().slice(0, 16) : '';
26
37
  endDate.value = c.endDate ? new Date(c.endDate).toISOString().slice(0, 16) : '';
38
+ judgingEndDate.value = c.judgingEndDate ? new Date(c.judgingEndDate).toISOString().slice(0, 16) : '';
39
+ prizes.value = (c.prizes ?? []).map((p: { place: number; title: string; description?: string; value?: string }) => ({
40
+ place: p.place,
41
+ title: p.title,
42
+ description: p.description ?? '',
43
+ value: p.value ?? '',
44
+ }));
45
+ judgeIds.value = [...(c.judges ?? [])];
46
+ // Resolve judge IDs to display info
47
+ if (judgeIds.value.length > 0) {
48
+ try {
49
+ const data = await $fetch<{ items: JudgeDisplay[] }>('/api/users', { query: { ids: judgeIds.value.join(',') } });
50
+ resolvedJudges.value = data.items;
51
+ } catch { /* ignore */ }
52
+ }
27
53
  }, { immediate: true });
28
54
 
55
+ async function searchJudge(): Promise<void> {
56
+ const q = judgeSearch.value.trim();
57
+ if (!q || q.length < 2) { judgeSearchResults.value = []; return; }
58
+ searchingJudges.value = true;
59
+ try {
60
+ const data = await $fetch<{ items: Array<{ id: string; username: string; displayName: string | null }> }>('/api/users', { query: { q, limit: 5 } });
61
+ judgeSearchResults.value = data.items.filter((u) => !judgeIds.value.includes(u.id));
62
+ } catch { judgeSearchResults.value = []; }
63
+ finally { searchingJudges.value = false; }
64
+ }
65
+
66
+ function addJudge(u: { id: string; username: string; displayName: string | null }): void {
67
+ if (!judgeIds.value.includes(u.id)) {
68
+ judgeIds.value.push(u.id);
69
+ resolvedJudges.value.push(u);
70
+ }
71
+ judgeSearch.value = '';
72
+ judgeSearchResults.value = [];
73
+ }
74
+
75
+ function removeJudge(id: string): void {
76
+ judgeIds.value = judgeIds.value.filter((jid) => jid !== id);
77
+ resolvedJudges.value = resolvedJudges.value.filter((j) => j.id !== id);
78
+ }
79
+
80
+ function addPrize(): void {
81
+ const nextPlace = prizes.value.length + 1;
82
+ prizes.value.push({ place: nextPlace, title: '', description: '', value: '' });
83
+ }
84
+
85
+ function removePrize(index: number): void {
86
+ prizes.value.splice(index, 1);
87
+ prizes.value.forEach((p, i) => { p.place = i + 1; });
88
+ }
89
+
90
+ function placeLabel(place: number): string {
91
+ if (place === 1) return '1st Place';
92
+ if (place === 2) return '2nd Place';
93
+ if (place === 3) return '3rd Place';
94
+ return `${place}th Place`;
95
+ }
96
+
29
97
  async function handleSave(): Promise<void> {
30
98
  saving.value = true;
31
99
  try {
100
+ const prizeData = prizes.value
101
+ .filter((p) => p.title.trim())
102
+ .map((p) => ({
103
+ place: p.place,
104
+ title: p.title,
105
+ description: p.description || undefined,
106
+ value: p.value || undefined,
107
+ }));
108
+
32
109
  await $fetch(`/api/contests/${slug}`, {
33
110
  method: 'PUT',
34
111
  body: {
@@ -37,6 +114,9 @@ async function handleSave(): Promise<void> {
37
114
  rules: rules.value || undefined,
38
115
  startDate: startDate.value ? new Date(startDate.value).toISOString() : undefined,
39
116
  endDate: endDate.value ? new Date(endDate.value).toISOString() : undefined,
117
+ judgingEndDate: judgingEndDate.value ? new Date(judgingEndDate.value).toISOString() : undefined,
118
+ prizes: prizeData.length > 0 ? prizeData : undefined,
119
+ judges: judgeIds.value.length > 0 ? judgeIds.value : undefined,
40
120
  },
41
121
  });
42
122
  toast.success('Contest updated');
@@ -49,7 +129,10 @@ async function handleSave(): Promise<void> {
49
129
  }
50
130
 
51
131
  async function transitionStatus(newStatus: string): Promise<void> {
52
- if (!confirm(`Change contest status to "${newStatus}"?`)) return;
132
+ const msg = newStatus === 'cancelled'
133
+ ? 'Cancel this contest? This cannot be undone.'
134
+ : `Change contest status to "${newStatus}"?`;
135
+ if (!confirm(msg)) return;
53
136
  try {
54
137
  await $fetch(`/api/contests/${slug}/transition`, {
55
138
  method: 'POST',
@@ -64,56 +147,129 @@ async function transitionStatus(newStatus: string): Promise<void> {
64
147
  </script>
65
148
 
66
149
  <template>
67
- <div v-if="contest" class="contest-edit">
150
+ <div v-if="contest && !isOwner" class="cpub-not-found">
151
+ <p>You don't have permission to edit this contest.</p>
152
+ <NuxtLink :to="`/contests/${slug}`" class="cpub-btn cpub-btn-sm">Back to Contest</NuxtLink>
153
+ </div>
154
+ <div v-else-if="contest" class="cpub-contest-edit">
68
155
  <NuxtLink :to="`/contests/${slug}`" class="cpub-back-link"><i class="fa-solid fa-arrow-left"></i> Back to contest</NuxtLink>
69
- <h1 class="page-title">Edit Contest</h1>
70
- <p class="page-subtitle">
71
- Status: <span class="status-badge" :class="`status-${contest.status}`">{{ contest.status }}</span>
156
+ <h1 class="cpub-edit-title">Edit Contest</h1>
157
+ <p class="cpub-edit-subtitle">
158
+ Status: <span class="cpub-status-badge" :class="`cpub-status-${contest.status}`">{{ contest.status }}</span>
72
159
  </p>
73
160
 
74
- <form class="edit-form" @submit.prevent="handleSave">
75
- <section class="form-section">
76
- <h2 class="form-section-title">Details</h2>
77
- <div class="form-field">
78
- <label class="form-label">Title</label>
79
- <input v-model="title" type="text" class="form-input" />
161
+ <form class="cpub-edit-form" @submit.prevent="handleSave">
162
+ <section class="cpub-form-section">
163
+ <h2 class="cpub-form-section-title">Details</h2>
164
+ <div class="cpub-form-field">
165
+ <label class="cpub-form-label">Title</label>
166
+ <input v-model="title" type="text" class="cpub-form-input" />
167
+ </div>
168
+ <div class="cpub-form-field">
169
+ <label class="cpub-form-label">Description</label>
170
+ <textarea v-model="description" class="cpub-form-textarea" rows="3" />
171
+ </div>
172
+ <div class="cpub-form-field">
173
+ <label class="cpub-form-label">Rules</label>
174
+ <textarea v-model="rules" class="cpub-form-textarea" rows="4" placeholder="One rule per line" />
175
+ </div>
176
+ </section>
177
+
178
+ <section class="cpub-form-section">
179
+ <h2 class="cpub-form-section-title">Schedule</h2>
180
+ <div class="cpub-form-row">
181
+ <div class="cpub-form-field">
182
+ <label class="cpub-form-label">Start Date</label>
183
+ <input v-model="startDate" type="datetime-local" class="cpub-form-input" />
184
+ </div>
185
+ <div class="cpub-form-field">
186
+ <label class="cpub-form-label">End Date</label>
187
+ <input v-model="endDate" type="datetime-local" class="cpub-form-input" />
188
+ </div>
80
189
  </div>
81
- <div class="form-field">
82
- <label class="form-label">Description</label>
83
- <textarea v-model="description" class="form-textarea" rows="3" />
190
+ <div class="cpub-form-field">
191
+ <label class="cpub-form-label">Judging End Date</label>
192
+ <input v-model="judgingEndDate" type="datetime-local" class="cpub-form-input" />
84
193
  </div>
85
- <div class="form-field">
86
- <label class="form-label">Rules</label>
87
- <textarea v-model="rules" class="form-textarea" rows="4" />
194
+ </section>
195
+
196
+ <section class="cpub-form-section">
197
+ <h2 class="cpub-form-section-title">Prizes</h2>
198
+ <div v-for="(prize, i) in prizes" :key="i" class="cpub-prize-row">
199
+ <div class="cpub-prize-header">
200
+ <span class="cpub-prize-label">{{ placeLabel(prize.place) }}</span>
201
+ <button type="button" class="cpub-prize-remove" @click="removePrize(i)"><i class="fa-solid fa-times"></i></button>
202
+ </div>
203
+ <div class="cpub-form-row">
204
+ <div class="cpub-form-field">
205
+ <label class="cpub-form-label">Title</label>
206
+ <input v-model="prize.title" type="text" class="cpub-form-input" placeholder="e.g. Gold Prize" />
207
+ </div>
208
+ <div class="cpub-form-field">
209
+ <label class="cpub-form-label">Value</label>
210
+ <input v-model="prize.value" type="text" class="cpub-form-input" placeholder="e.g. $500" />
211
+ </div>
212
+ </div>
213
+ <div class="cpub-form-field">
214
+ <label class="cpub-form-label">Description</label>
215
+ <input v-model="prize.description" type="text" class="cpub-form-input" placeholder="Optional description" />
216
+ </div>
88
217
  </div>
218
+ <button type="button" class="cpub-btn cpub-btn-sm" @click="addPrize"><i class="fa-solid fa-plus"></i> Add Prize</button>
89
219
  </section>
90
220
 
91
- <section class="form-section">
92
- <h2 class="form-section-title">Schedule</h2>
93
- <div class="form-row">
94
- <div class="form-field">
95
- <label class="form-label">Start Date</label>
96
- <input v-model="startDate" type="datetime-local" class="form-input" />
221
+ <section class="cpub-form-section">
222
+ <h2 class="cpub-form-section-title">Judges</h2>
223
+ <div v-if="resolvedJudges.length" class="cpub-judge-list">
224
+ <div v-for="judge in resolvedJudges" :key="judge.id" class="cpub-judge-chip">
225
+ <span>{{ judge.displayName || judge.username }}</span>
226
+ <button type="button" class="cpub-judge-chip-remove" @click="removeJudge(judge.id)"><i class="fa-solid fa-times"></i></button>
97
227
  </div>
98
- <div class="form-field">
99
- <label class="form-label">End Date</label>
100
- <input v-model="endDate" type="datetime-local" class="form-input" />
228
+ </div>
229
+ <div v-else class="cpub-judge-empty">No judges assigned.</div>
230
+ <div class="cpub-judge-search">
231
+ <input
232
+ v-model="judgeSearch"
233
+ type="text"
234
+ class="cpub-form-input"
235
+ placeholder="Search users by name or username..."
236
+ @input="searchJudge"
237
+ />
238
+ <div v-if="judgeSearchResults.length" class="cpub-judge-dropdown">
239
+ <button
240
+ v-for="u in judgeSearchResults"
241
+ :key="u.id"
242
+ type="button"
243
+ class="cpub-judge-option"
244
+ @click="addJudge(u)"
245
+ >
246
+ <strong>{{ u.displayName || u.username }}</strong>
247
+ <span class="cpub-judge-option-username">@{{ u.username }}</span>
248
+ </button>
101
249
  </div>
102
250
  </div>
103
251
  </section>
104
252
 
105
- <section class="form-section">
106
- <h2 class="form-section-title">Status Transitions</h2>
107
- <div class="status-actions">
108
- <button v-if="contest.status === 'upcoming'" type="button" class="cpub-btn" style="color: var(--green); border-color: var(--green-border);" @click="transitionStatus('active')">
253
+ <section class="cpub-form-section">
254
+ <h2 class="cpub-form-section-title">Status Transitions</h2>
255
+ <div class="cpub-status-actions">
256
+ <button v-if="contest.status === 'upcoming'" type="button" class="cpub-btn cpub-transition-btn cpub-transition-activate" @click="transitionStatus('active')">
109
257
  <i class="fa-solid fa-play"></i> Start Contest
110
258
  </button>
111
- <button v-if="contest.status === 'active'" type="button" class="cpub-btn" style="color: var(--yellow); border-color: var(--yellow-border);" @click="transitionStatus('judging')">
259
+ <button v-if="contest.status === 'active'" type="button" class="cpub-btn cpub-transition-btn cpub-transition-judging" @click="transitionStatus('judging')">
112
260
  <i class="fa-solid fa-gavel"></i> Begin Judging
113
261
  </button>
114
- <button v-if="contest.status === 'judging'" type="button" class="cpub-btn" style="color: var(--accent); border-color: var(--accent-border);" @click="transitionStatus('completed')">
262
+ <button v-if="contest.status === 'judging'" type="button" class="cpub-btn cpub-transition-btn cpub-transition-complete" @click="transitionStatus('completed')">
115
263
  <i class="fa-solid fa-flag-checkered"></i> Complete
116
264
  </button>
265
+ <button
266
+ v-if="contest.status !== 'completed' && contest.status !== 'cancelled'"
267
+ type="button"
268
+ class="cpub-btn cpub-transition-btn cpub-transition-cancel"
269
+ @click="transitionStatus('cancelled')"
270
+ >
271
+ <i class="fa-solid fa-ban"></i> Cancel Contest
272
+ </button>
117
273
  </div>
118
274
  </section>
119
275
 
@@ -122,32 +278,58 @@ async function transitionStatus(newStatus: string): Promise<void> {
122
278
  </button>
123
279
  </form>
124
280
  </div>
125
- <div v-else class="not-found"><p>Contest not found</p></div>
281
+ <div v-else class="cpub-not-found"><p>Contest not found</p></div>
126
282
  </template>
127
283
 
128
284
  <style scoped>
129
- .contest-edit { max-width: 700px; margin: 0 auto; padding: 32px; }
285
+ .cpub-contest-edit { max-width: 700px; margin: 0 auto; padding: 32px; }
130
286
  .cpub-back-link { font-size: 11px; font-family: var(--font-mono); color: var(--text-faint); text-decoration: none; display: inline-flex; align-items: center; gap: 6px; margin-bottom: 16px; }
131
287
  .cpub-back-link:hover { color: var(--accent); }
132
- .page-title { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
133
- .page-subtitle { font-size: 13px; color: var(--text-dim); margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
134
- .status-badge { font-size: 10px; font-family: var(--font-mono); text-transform: uppercase; padding: 2px 8px; border: var(--border-width-default) solid; }
135
- .status-upcoming { color: var(--yellow); border-color: var(--yellow-border); background: var(--yellow-bg); }
136
- .status-active { color: var(--green); border-color: var(--green-border); background: var(--green-bg); }
137
- .status-judging { color: var(--accent); border-color: var(--accent-border); background: var(--accent-bg); }
138
- .status-completed { color: var(--text-faint); border-color: var(--border2); background: var(--surface2); }
139
-
140
- .edit-form { display: flex; flex-direction: column; gap: 16px; }
141
- .form-section { border: var(--border-width-default) solid var(--border); background: var(--surface); padding: 20px; box-shadow: var(--shadow-md); }
142
- .form-section-title { font-size: 14px; font-weight: 700; margin-bottom: 14px; }
143
- .form-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; }
144
- .form-field:last-child { margin-bottom: 0; }
145
- .form-label { font-size: 10px; font-weight: 600; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-faint); }
146
- .form-input, .form-textarea { padding: 8px 10px; border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text); font-size: 13px; font-family: inherit; }
147
- .form-input:focus, .form-textarea:focus { border-color: var(--accent); outline: none; }
148
- .form-textarea { resize: vertical; }
149
- .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
150
- .status-actions { display: flex; gap: 8px; }
151
-
152
- .not-found { text-align: center; padding: 64px; color: var(--text-dim); }
288
+ .cpub-edit-title { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
289
+ .cpub-edit-subtitle { font-size: 13px; color: var(--text-dim); margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
290
+
291
+ .cpub-status-badge { font-size: 10px; font-family: var(--font-mono); text-transform: uppercase; padding: 2px 8px; border: var(--border-width-default) solid; }
292
+ .cpub-status-upcoming { color: var(--yellow); border-color: var(--yellow-border); background: var(--yellow-bg); }
293
+ .cpub-status-active { color: var(--green); border-color: var(--green-border); background: var(--green-bg); }
294
+ .cpub-status-judging { color: var(--accent); border-color: var(--accent-border); background: var(--accent-bg); }
295
+ .cpub-status-completed { color: var(--text-faint); border-color: var(--border2); background: var(--surface2); }
296
+ .cpub-status-cancelled { color: var(--red); border-color: var(--red-border); background: var(--red-bg); }
297
+
298
+ .cpub-edit-form { display: flex; flex-direction: column; gap: 16px; }
299
+ .cpub-form-section { border: var(--border-width-default) solid var(--border); background: var(--surface); padding: 20px; box-shadow: var(--shadow-md); }
300
+ .cpub-form-section-title { font-size: 14px; font-weight: 700; margin-bottom: 14px; }
301
+ .cpub-form-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; }
302
+ .cpub-form-field:last-child { margin-bottom: 0; }
303
+ .cpub-form-label { font-size: 10px; font-weight: 600; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-faint); }
304
+ .cpub-form-input, .cpub-form-textarea { padding: 8px 10px; border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text); font-size: 13px; font-family: inherit; }
305
+ .cpub-form-input:focus, .cpub-form-textarea:focus { border-color: var(--accent); outline: none; }
306
+ .cpub-form-textarea { resize: vertical; }
307
+ .cpub-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
308
+
309
+ .cpub-prize-row { border: var(--border-width-default) solid var(--border); padding: 14px; margin-bottom: 10px; background: var(--surface2); }
310
+ .cpub-prize-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
311
+ .cpub-prize-label { font-size: 11px; font-weight: 700; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.06em; }
312
+ .cpub-prize-remove { background: none; border: none; color: var(--text-faint); cursor: pointer; font-size: 12px; }
313
+ .cpub-prize-remove:hover { color: var(--red); }
314
+
315
+ .cpub-status-actions { display: flex; gap: 8px; flex-wrap: wrap; }
316
+ .cpub-transition-btn { display: inline-flex; align-items: center; gap: 6px; }
317
+ .cpub-transition-activate { color: var(--green); border-color: var(--green-border); }
318
+ .cpub-transition-judging { color: var(--yellow); border-color: var(--yellow-border); }
319
+ .cpub-transition-complete { color: var(--accent); border-color: var(--accent-border); }
320
+ .cpub-transition-cancel { color: var(--red); border-color: var(--red-border); }
321
+
322
+ .cpub-judge-list { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
323
+ .cpub-judge-chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; background: var(--accent-bg); border: var(--border-width-default) solid var(--accent-border); font-size: 11px; font-family: var(--font-mono); color: var(--accent); }
324
+ .cpub-judge-chip-remove { background: none; border: none; color: var(--accent); cursor: pointer; font-size: 10px; padding: 0; }
325
+ .cpub-judge-chip-remove:hover { color: var(--red); }
326
+ .cpub-judge-empty { font-size: 12px; color: var(--text-faint); margin-bottom: 12px; }
327
+ .cpub-judge-search { position: relative; }
328
+ .cpub-judge-dropdown { position: absolute; z-index: 10; top: 100%; left: 0; right: 0; background: var(--surface); border: var(--border-width-default) solid var(--border); box-shadow: var(--shadow-lg); max-height: 200px; overflow-y: auto; }
329
+ .cpub-judge-option { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 12px; background: none; border: none; border-bottom: var(--border-width-default) solid var(--border); cursor: pointer; font-size: 12px; color: var(--text); text-align: left; }
330
+ .cpub-judge-option:last-child { border-bottom: none; }
331
+ .cpub-judge-option:hover { background: var(--surface2); }
332
+ .cpub-judge-option-username { color: var(--text-faint); font-family: var(--font-mono); font-size: 11px; }
333
+
334
+ .cpub-not-found { text-align: center; padding: 64px; color: var(--text-dim); display: flex; flex-direction: column; align-items: center; gap: 12px; }
153
335
  </style>