@commonpub/layer 0.46.0 → 0.48.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.
|
@@ -303,6 +303,12 @@ function formatCount(n: number | undefined): string {
|
|
|
303
303
|
.cpub-cc-av {
|
|
304
304
|
width: 18px;
|
|
305
305
|
height: 18px;
|
|
306
|
+
/* Hard-lock square so a portrait/non-square photo can't render as an oval
|
|
307
|
+
(intrinsic-ratio fallback if a dimension is dropped). See ArticleView.vue. */
|
|
308
|
+
min-width: 18px;
|
|
309
|
+
max-width: 18px;
|
|
310
|
+
min-height: 18px;
|
|
311
|
+
max-height: 18px;
|
|
306
312
|
background: var(--accent-bg);
|
|
307
313
|
border: var(--border-width-default) solid var(--accent-border);
|
|
308
314
|
color: var(--accent);
|
|
@@ -435,8 +435,18 @@ useJsonLd({
|
|
|
435
435
|
* Fix: scope display:flex centering to the div variant only.
|
|
436
436
|
*/
|
|
437
437
|
.cpub-av {
|
|
438
|
-
|
|
439
|
-
|
|
438
|
+
--cpub-av-size: 28px;
|
|
439
|
+
width: var(--cpub-av-size);
|
|
440
|
+
height: var(--cpub-av-size);
|
|
441
|
+
/* Hard-lock to a square. Without min/max clamps, a global img reset or a
|
|
442
|
+
dropped dimension lets the <img> fall back to its intrinsic aspect ratio,
|
|
443
|
+
so a portrait photo renders as a tall oval (the deveco blog-avatar bug —
|
|
444
|
+
visible even on wide viewports, so it's not flex compression). min/max on
|
|
445
|
+
BOTH axes clamp the used size regardless of what sets width/height. */
|
|
446
|
+
min-width: var(--cpub-av-size);
|
|
447
|
+
max-width: var(--cpub-av-size);
|
|
448
|
+
min-height: var(--cpub-av-size);
|
|
449
|
+
max-height: var(--cpub-av-size);
|
|
440
450
|
border-radius: 50%;
|
|
441
451
|
background: var(--surface3);
|
|
442
452
|
border: var(--border-width-default) solid var(--border);
|
|
@@ -459,8 +469,8 @@ img.cpub-av {
|
|
|
459
469
|
object-fit: cover;
|
|
460
470
|
}
|
|
461
471
|
|
|
462
|
-
.cpub-av-lg {
|
|
463
|
-
.cpub-av-xl {
|
|
472
|
+
.cpub-av-lg { --cpub-av-size: 44px; font-size: 14px; }
|
|
473
|
+
.cpub-av-xl { --cpub-av-size: 64px; font-size: 18px; }
|
|
464
474
|
|
|
465
475
|
/* ── AUTHOR ROW ── */
|
|
466
476
|
.cpub-author-row {
|
|
@@ -767,8 +767,16 @@ async function handleBuild(): Promise<void> {
|
|
|
767
767
|
* to the div-variant only — stops img-variant from squishing portrait
|
|
768
768
|
* avatars (object-fit:cover gets dropped on flex-set replaced elements). */
|
|
769
769
|
.cpub-av {
|
|
770
|
-
|
|
771
|
-
|
|
770
|
+
--cpub-av-size: 28px;
|
|
771
|
+
width: var(--cpub-av-size);
|
|
772
|
+
height: var(--cpub-av-size);
|
|
773
|
+
/* Hard-lock to a square (min/max on both axes) so a portrait photo can't
|
|
774
|
+
render as an oval if a global reset or dropped dimension lets the <img>
|
|
775
|
+
take its intrinsic aspect ratio. See ArticleView.vue. */
|
|
776
|
+
min-width: var(--cpub-av-size);
|
|
777
|
+
max-width: var(--cpub-av-size);
|
|
778
|
+
min-height: var(--cpub-av-size);
|
|
779
|
+
max-height: var(--cpub-av-size);
|
|
772
780
|
border-radius: 50%;
|
|
773
781
|
background: var(--surface3);
|
|
774
782
|
border: var(--border-width-default) solid var(--border);
|
|
@@ -789,7 +797,7 @@ img.cpub-av {
|
|
|
789
797
|
object-fit: cover;
|
|
790
798
|
}
|
|
791
799
|
|
|
792
|
-
.cpub-av-lg {
|
|
800
|
+
.cpub-av-lg { --cpub-av-size: 36px; font-size: 12px; }
|
|
793
801
|
|
|
794
802
|
.cpub-author-name {
|
|
795
803
|
font-size: 13px;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -54,12 +54,12 @@
|
|
|
54
54
|
"vue-router": "^4.3.0",
|
|
55
55
|
"zod": "^4.3.6",
|
|
56
56
|
"@commonpub/auth": "0.8.0",
|
|
57
|
-
"@commonpub/docs": "0.6.3",
|
|
58
57
|
"@commonpub/config": "0.18.0",
|
|
59
|
-
"@commonpub/
|
|
58
|
+
"@commonpub/learning": "0.5.2",
|
|
59
|
+
"@commonpub/docs": "0.6.3",
|
|
60
60
|
"@commonpub/explainer": "0.7.15",
|
|
61
|
+
"@commonpub/editor": "0.7.11",
|
|
61
62
|
"@commonpub/protocol": "0.13.0",
|
|
62
|
-
"@commonpub/learning": "0.5.2",
|
|
63
63
|
"@commonpub/schema": "0.26.0",
|
|
64
64
|
"@commonpub/server": "2.73.0",
|
|
65
65
|
"@commonpub/ui": "0.9.2"
|
package/pages/contests/index.vue
CHANGED
|
@@ -13,6 +13,20 @@ function cardBlurb(c: { subheading?: string | null; description?: string | null
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const config = useRuntimeConfig();
|
|
16
|
+
|
|
17
|
+
// Contest banner thumbnail — proxy cross-origin images through our server
|
|
18
|
+
// (same pattern as ContentCard) for caching + faster loads.
|
|
19
|
+
function coverFor(url: string | null | undefined): string | null {
|
|
20
|
+
if (!url) return null;
|
|
21
|
+
const siteDomain = (config.public?.domain as string) || '';
|
|
22
|
+
try {
|
|
23
|
+
if (siteDomain && !url.includes(siteDomain)) {
|
|
24
|
+
return `/api/image-proxy?url=${encodeURIComponent(url)}&w=600`;
|
|
25
|
+
}
|
|
26
|
+
} catch { /* invalid URL — use as-is */ }
|
|
27
|
+
return url;
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
const contestCreation = config.public.contestCreation as string || 'admin';
|
|
17
31
|
const canCreateContest = computed(() => {
|
|
18
32
|
if (!isAuthenticated.value) return false;
|
|
@@ -31,30 +45,45 @@ const canCreateContest = computed(() => {
|
|
|
31
45
|
</NuxtLink>
|
|
32
46
|
</div>
|
|
33
47
|
<div v-if="contests?.items?.length" class="cpub-grid-3">
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
<NuxtLink
|
|
49
|
+
v-for="contest in contests.items"
|
|
50
|
+
:key="contest.id"
|
|
51
|
+
:to="`/contests/${contest.slug}`"
|
|
52
|
+
class="cpub-card cpub-contest-card"
|
|
53
|
+
>
|
|
54
|
+
<!-- Banner thumbnail (contest.bannerUrl) with trophy fallback + status badge overlay -->
|
|
55
|
+
<div class="cpub-contest-thumb">
|
|
56
|
+
<img
|
|
57
|
+
v-if="coverFor(contest.bannerUrl)"
|
|
58
|
+
:src="coverFor(contest.bannerUrl)!"
|
|
59
|
+
:alt="contest.title"
|
|
60
|
+
class="cpub-contest-cover"
|
|
61
|
+
loading="lazy"
|
|
62
|
+
/>
|
|
63
|
+
<template v-else>
|
|
64
|
+
<div class="cpub-contest-thumb-grid" />
|
|
65
|
+
<i class="fa-solid fa-trophy cpub-contest-thumb-icon" />
|
|
66
|
+
</template>
|
|
67
|
+
<span class="cpub-badge cpub-contest-thumb-badge" :class="{
|
|
37
68
|
'cpub-badge-green': contest.status === 'active',
|
|
38
69
|
'cpub-badge-yellow': contest.status === 'upcoming',
|
|
39
70
|
'cpub-badge-accent': contest.status === 'judging',
|
|
40
71
|
'cpub-badge-red': contest.status === 'completed' || contest.status === 'cancelled',
|
|
41
72
|
}">{{ contest.status }}</span>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
</h3>
|
|
47
|
-
<p v-if="cardBlurb(contest)" class="cpub-contest-card-blurb" style="font-size: 12px; color: var(--text-dim); margin-bottom: 12px">
|
|
73
|
+
</div>
|
|
74
|
+
<div class="cpub-card-body">
|
|
75
|
+
<h3 class="cpub-contest-card-title">{{ contest.title }}</h3>
|
|
76
|
+
<p v-if="cardBlurb(contest)" class="cpub-contest-card-blurb">
|
|
48
77
|
{{ cardBlurb(contest) }}
|
|
49
78
|
</p>
|
|
50
79
|
<div v-if="contest.endDate" style="margin-top: 8px">
|
|
51
80
|
<CountdownTimer :target-date="contest.endDate" />
|
|
52
81
|
</div>
|
|
53
|
-
<div
|
|
82
|
+
<div class="cpub-contest-card-meta">
|
|
54
83
|
<span><i class="fa-solid fa-users"></i> {{ contest.entryCount }} entries</span>
|
|
55
84
|
</div>
|
|
56
85
|
</div>
|
|
57
|
-
</
|
|
86
|
+
</NuxtLink>
|
|
58
87
|
</div>
|
|
59
88
|
<div v-else class="cpub-empty-state">
|
|
60
89
|
<div class="cpub-empty-state-icon"><i class="fa-solid fa-trophy"></i></div>
|
|
@@ -71,7 +100,42 @@ const canCreateContest = computed(() => {
|
|
|
71
100
|
.cpub-card:hover { box-shadow: var(--shadow-lg); transform: translate(-1px, -1px); }
|
|
72
101
|
.cpub-card-body { padding: 16px; }
|
|
73
102
|
|
|
103
|
+
/* Whole card is a link */
|
|
104
|
+
.cpub-contest-card { display: block; text-decoration: none; color: inherit; }
|
|
105
|
+
|
|
106
|
+
/* Banner thumbnail — wide (banner-shaped), cover-cropped, with a grid+trophy
|
|
107
|
+
fallback when a contest has no bannerUrl. */
|
|
108
|
+
.cpub-contest-thumb {
|
|
109
|
+
position: relative;
|
|
110
|
+
aspect-ratio: 16 / 9;
|
|
111
|
+
background: var(--surface2);
|
|
112
|
+
border-bottom: var(--border-width-default) solid var(--border);
|
|
113
|
+
display: flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
overflow: hidden;
|
|
117
|
+
}
|
|
118
|
+
.cpub-contest-cover { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
119
|
+
.cpub-contest-thumb-grid {
|
|
120
|
+
position: absolute;
|
|
121
|
+
inset: 0;
|
|
122
|
+
background-image:
|
|
123
|
+
linear-gradient(var(--border2) 1px, transparent 1px),
|
|
124
|
+
linear-gradient(90deg, var(--border2) 1px, transparent 1px);
|
|
125
|
+
background-size: 20px 20px;
|
|
126
|
+
opacity: 0.25;
|
|
127
|
+
}
|
|
128
|
+
.cpub-contest-thumb-icon { position: relative; z-index: 1; font-size: 36px; color: var(--accent); opacity: 0.45; }
|
|
129
|
+
.cpub-contest-thumb-badge { position: absolute; top: 10px; left: 10px; z-index: 2; box-shadow: var(--shadow-sm); }
|
|
130
|
+
.cpub-contest-card:hover .cpub-contest-cover { opacity: 0.92; }
|
|
131
|
+
|
|
132
|
+
.cpub-contest-card-title { font-size: 15px; font-weight: 600; margin: 0 0 6px; color: var(--text); }
|
|
133
|
+
.cpub-contest-card-meta { display: flex; align-items: center; gap: 8px; margin-top: 12px; font-size: 11px; color: var(--text-faint); font-family: var(--font-mono); }
|
|
134
|
+
|
|
74
135
|
.cpub-contest-card-blurb {
|
|
136
|
+
font-size: 12px;
|
|
137
|
+
color: var(--text-dim);
|
|
138
|
+
margin-bottom: 12px;
|
|
75
139
|
display: -webkit-box;
|
|
76
140
|
-webkit-line-clamp: 3;
|
|
77
141
|
line-clamp: 3;
|