@commonpub/layer 0.21.3 → 0.21.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.
|
@@ -74,7 +74,7 @@ function dismissHero(): void {
|
|
|
74
74
|
.cpub-hero-dismiss { position: absolute; top: 12px; right: 16px; background: transparent; border: none; color: var(--text-faint); font-size: 12px; cursor: pointer; padding: 4px; z-index: 2; }
|
|
75
75
|
.cpub-hero-dismiss:hover { color: var(--text-dim); }
|
|
76
76
|
.cpub-hero-inner { position: relative; z-index: 1; max-width: 1280px; margin: 0 auto; padding: 36px 32px; width: 100%; display: flex; align-items: center; gap: 48px; }
|
|
77
|
-
.cpub-hero-content { flex: 1; }
|
|
77
|
+
.cpub-hero-content { flex: 1; min-width: 0; }
|
|
78
78
|
.cpub-hero-eyebrow { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
|
79
79
|
.cpub-hero-badge { font-size: 9px; font-family: var(--font-mono); letter-spacing: 0.1em; text-transform: uppercase; padding: 3px 9px; background: var(--yellow-bg); border: var(--border-width-default) solid var(--yellow); color: var(--yellow); }
|
|
80
80
|
.cpub-hero-badge-live { background: var(--green-bg); border-color: var(--green); color: var(--green); display: flex; align-items: center; gap: 5px; }
|
|
@@ -83,5 +83,13 @@ function dismissHero(): void {
|
|
|
83
83
|
.cpub-hero-title { font-size: 22px; font-weight: 700; line-height: 1.25; margin-bottom: 10px; }
|
|
84
84
|
.cpub-hero-title span { color: var(--accent); }
|
|
85
85
|
.cpub-hero-excerpt { font-size: 13px; color: var(--text-dim); line-height: 1.65; margin-bottom: 20px; max-width: 560px; }
|
|
86
|
-
.cpub-hero-actions { display: flex; gap: 8px; }
|
|
86
|
+
.cpub-hero-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
87
|
+
|
|
88
|
+
@media (max-width: 640px) {
|
|
89
|
+
.cpub-hero-inner { flex-direction: column; align-items: flex-start; gap: 20px; padding: 24px 16px; }
|
|
90
|
+
.cpub-hero-title { font-size: 19px; }
|
|
91
|
+
.cpub-hero-excerpt { font-size: 13px; }
|
|
92
|
+
.cpub-hero-actions { width: 100%; }
|
|
93
|
+
.cpub-hero-actions :deep(.cpub-btn) { flex: 1 1 140px; justify-content: center; }
|
|
94
|
+
}
|
|
87
95
|
</style>
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { HomepageSection } from '@commonpub/server';
|
|
3
3
|
|
|
4
|
-
defineProps<{
|
|
4
|
+
const props = defineProps<{
|
|
5
5
|
sections: HomepageSection[];
|
|
6
6
|
/** Which zone to render: 'main' for feed column, 'sidebar' for sidebar */
|
|
7
7
|
zone: 'main' | 'sidebar' | 'full-width';
|
|
8
|
+
/** If set, only render sections whose type is in this list (zone still applies). */
|
|
9
|
+
restrictTypes?: string[];
|
|
10
|
+
/** If set, skip sections whose type is in this list. */
|
|
11
|
+
excludeTypes?: string[];
|
|
8
12
|
}>();
|
|
9
13
|
|
|
14
|
+
function typeAllowed(type: string): boolean {
|
|
15
|
+
if (props.restrictTypes && !props.restrictTypes.includes(type)) return false;
|
|
16
|
+
if (props.excludeTypes && props.excludeTypes.includes(type)) return false;
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
const features = useFeatures();
|
|
11
21
|
|
|
12
22
|
function isFeatureEnabled(featureGate?: string): boolean {
|
|
@@ -29,7 +39,7 @@ function sectionZone(section: HomepageSection): 'full-width' | 'main' | 'sidebar
|
|
|
29
39
|
|
|
30
40
|
<template>
|
|
31
41
|
<template v-for="section in sections" :key="section.id">
|
|
32
|
-
<template v-if="section.enabled && sectionZone(section) === zone && isFeatureEnabled(section.config.featureGate)">
|
|
42
|
+
<template v-if="section.enabled && sectionZone(section) === zone && typeAllowed(section.type) && isFeatureEnabled(section.config.featureGate)">
|
|
33
43
|
<HomepageHeroSection
|
|
34
44
|
v-if="section.type === 'hero'"
|
|
35
45
|
:config="section.config"
|
package/layouts/default.vue
CHANGED
|
@@ -110,18 +110,22 @@ const userUsername = computed(() => user.value?.username ?? '');
|
|
|
110
110
|
<div class="cpub-topbar-spacer" />
|
|
111
111
|
|
|
112
112
|
<div class="cpub-topbar-actions">
|
|
113
|
-
|
|
113
|
+
<!-- Search/messages/notifications are desktop-only in the top bar.
|
|
114
|
+
On mobile they live in the hamburger menu (search) and the
|
|
115
|
+
avatar dropdown (messages/notifications) so the bar can't
|
|
116
|
+
overflow and hide the hamburger toggle. -->
|
|
117
|
+
<NuxtLink to="/search" class="cpub-search-btn cpub-topbar-desktop-only" aria-label="Search">
|
|
114
118
|
<i class="fa-solid fa-magnifying-glass"></i>
|
|
115
119
|
<span class="cpub-search-text">Search...</span>
|
|
116
120
|
<span class="cpub-kbd">⌈K</span>
|
|
117
121
|
</NuxtLink>
|
|
118
122
|
|
|
119
123
|
<template v-if="isAuthenticated">
|
|
120
|
-
<NuxtLink to="/messages" class="cpub-icon-btn" title="Messages" aria-label="Messages">
|
|
124
|
+
<NuxtLink to="/messages" class="cpub-icon-btn cpub-topbar-desktop-only" title="Messages" aria-label="Messages">
|
|
121
125
|
<i class="fa-solid fa-envelope"></i>
|
|
122
126
|
<span v-if="unreadMessages > 0" class="cpub-notif-badge" aria-label="unread messages">{{ unreadMessages > 99 ? '99+' : unreadMessages }}</span>
|
|
123
127
|
</NuxtLink>
|
|
124
|
-
<NuxtLink to="/notifications" class="cpub-icon-btn" title="Notifications" aria-label="Notifications">
|
|
128
|
+
<NuxtLink to="/notifications" class="cpub-icon-btn cpub-topbar-desktop-only" title="Notifications" aria-label="Notifications">
|
|
125
129
|
<i class="fa-solid fa-bell"></i>
|
|
126
130
|
<span v-if="unreadCount > 0" class="cpub-notif-badge" aria-label="unread notifications">{{ unreadCount > 99 ? '99+' : unreadCount }}</span>
|
|
127
131
|
</NuxtLink>
|
|
@@ -136,6 +140,17 @@ const userUsername = computed(() => user.value?.username ?? '');
|
|
|
136
140
|
</span>
|
|
137
141
|
</button>
|
|
138
142
|
<div v-if="userMenuOpen" class="cpub-user-dropdown" role="menu">
|
|
143
|
+
<!-- Mobile-only: messages/notifications relocated here from
|
|
144
|
+
the top bar (hidden on desktop, which keeps the icons). -->
|
|
145
|
+
<NuxtLink to="/messages" class="cpub-dropdown-item cpub-dropdown-item--mobile" role="menuitem" @click="userMenuOpen = false">
|
|
146
|
+
<i class="fa-solid fa-envelope"></i> Messages
|
|
147
|
+
<span v-if="unreadMessages > 0" class="cpub-dropdown-count">{{ unreadMessages > 99 ? '99+' : unreadMessages }}</span>
|
|
148
|
+
</NuxtLink>
|
|
149
|
+
<NuxtLink to="/notifications" class="cpub-dropdown-item cpub-dropdown-item--mobile" role="menuitem" @click="userMenuOpen = false">
|
|
150
|
+
<i class="fa-solid fa-bell"></i> Notifications
|
|
151
|
+
<span v-if="unreadCount > 0" class="cpub-dropdown-count">{{ unreadCount > 99 ? '99+' : unreadCount }}</span>
|
|
152
|
+
</NuxtLink>
|
|
153
|
+
<div class="cpub-dropdown-divider cpub-dropdown-item--mobile" />
|
|
139
154
|
<NuxtLink :to="`/u/${userUsername}`" class="cpub-dropdown-item" role="menuitem" @click="userMenuOpen = false"><i class="fa-solid fa-user"></i> Profile</NuxtLink>
|
|
140
155
|
<NuxtLink to="/dashboard" class="cpub-dropdown-item" role="menuitem" @click="userMenuOpen = false"><i class="fa-solid fa-gauge"></i> Dashboard</NuxtLink>
|
|
141
156
|
<NuxtLink to="/settings" class="cpub-dropdown-item" role="menuitem" @click="userMenuOpen = false"><i class="fa-solid fa-gear"></i> Settings</NuxtLink>
|
|
@@ -168,8 +183,7 @@ const userUsername = computed(() => user.value?.username ?? '');
|
|
|
168
183
|
<div class="cpub-mobile-divider" />
|
|
169
184
|
<NuxtLink to="/create" class="cpub-mobile-link" @click="mobileMenuOpen = false"><i class="fa-solid fa-plus"></i> Create</NuxtLink>
|
|
170
185
|
<NuxtLink to="/dashboard" class="cpub-mobile-link" @click="mobileMenuOpen = false"><i class="fa-solid fa-gauge"></i> Dashboard</NuxtLink>
|
|
171
|
-
|
|
172
|
-
<NuxtLink to="/notifications" class="cpub-mobile-link" @click="mobileMenuOpen = false"><i class="fa-solid fa-bell"></i> Notifications</NuxtLink>
|
|
186
|
+
<!-- Messages/Notifications live in the avatar dropdown on mobile. -->
|
|
173
187
|
</template>
|
|
174
188
|
</div>
|
|
175
189
|
</div>
|
|
@@ -323,6 +337,10 @@ const userUsername = computed(() => user.value?.username ?? '');
|
|
|
323
337
|
.cpub-dropdown-item:hover { background: var(--surface2); color: var(--text); }
|
|
324
338
|
.cpub-dropdown-item i { width: 14px; text-align: center; font-size: 11px; }
|
|
325
339
|
.cpub-dropdown-divider { height: 2px; background: var(--border2); margin: 4px 12px; }
|
|
340
|
+
.cpub-dropdown-count { margin-left: auto; min-width: 18px; height: 16px; padding: 0 5px; border-radius: 8px; background: var(--accent); color: var(--color-text-inverse); font-size: 9px; font-weight: 700; font-family: var(--font-mono); line-height: 16px; text-align: center; }
|
|
341
|
+
/* Messages/Notifications in the avatar dropdown are mobile-only —
|
|
342
|
+
desktop keeps the dedicated top-bar icons. */
|
|
343
|
+
.cpub-dropdown-item--mobile { display: none; }
|
|
326
344
|
|
|
327
345
|
.cpub-mobile-toggle { display: none; width: 32px; height: 32px; background: none; border: var(--border-width-default) solid transparent; color: var(--text-dim); font-size: 16px; cursor: pointer; align-items: center; justify-content: center; }
|
|
328
346
|
.cpub-mobile-menu { display: none; position: fixed; inset: 0; top: 48px; z-index: 99; background: var(--color-surface-overlay-light); }
|
|
@@ -352,7 +370,10 @@ const userUsername = computed(() => user.value?.username ?? '');
|
|
|
352
370
|
|
|
353
371
|
@media (max-width: 768px) {
|
|
354
372
|
:deep(.cpub-topbar-nav) { display: none; }
|
|
355
|
-
|
|
373
|
+
/* Search / messages / notifications move off the top bar on mobile so
|
|
374
|
+
the row can't overflow and clip the hamburger + avatar. */
|
|
375
|
+
.cpub-topbar-desktop-only { display: none !important; }
|
|
376
|
+
.cpub-dropdown-item--mobile { display: flex; }
|
|
356
377
|
.cpub-search-text, .cpub-kbd, .cpub-new-text { display: none; }
|
|
357
378
|
.cpub-mobile-toggle { display: flex; }
|
|
358
379
|
.cpub-mobile-menu { display: block; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -50,16 +50,16 @@
|
|
|
50
50
|
"vue": "^3.4.0",
|
|
51
51
|
"vue-router": "^4.3.0",
|
|
52
52
|
"zod": "^4.3.6",
|
|
53
|
-
"@commonpub/auth": "0.6.0",
|
|
54
|
-
"@commonpub/docs": "0.6.3",
|
|
55
|
-
"@commonpub/learning": "0.5.2",
|
|
56
53
|
"@commonpub/editor": "0.7.9",
|
|
57
|
-
"@commonpub/
|
|
54
|
+
"@commonpub/protocol": "0.9.10",
|
|
58
55
|
"@commonpub/schema": "0.16.0",
|
|
56
|
+
"@commonpub/docs": "0.6.3",
|
|
57
|
+
"@commonpub/auth": "0.6.0",
|
|
58
|
+
"@commonpub/explainer": "0.7.12",
|
|
59
|
+
"@commonpub/learning": "0.5.2",
|
|
59
60
|
"@commonpub/ui": "0.8.5",
|
|
60
|
-
"@commonpub/
|
|
61
|
-
"@commonpub/server": "2.53.0"
|
|
62
|
-
"@commonpub/config": "0.12.0"
|
|
61
|
+
"@commonpub/config": "0.12.0",
|
|
62
|
+
"@commonpub/server": "2.53.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@testing-library/jest-dom": "^6.9.1",
|
package/pages/index.vue
CHANGED
|
@@ -148,13 +148,28 @@ async function handleHubJoin(hubSlug: string): Promise<void> {
|
|
|
148
148
|
<!-- Full-width sections (hero) -->
|
|
149
149
|
<HomepageSectionRenderer :sections="sortedSections" zone="full-width" />
|
|
150
150
|
|
|
151
|
+
<!-- Mobile only: hoist the contests section above the feed (contests
|
|
152
|
+
are time-sensitive). Desktop keeps contests in the sidebar; hubs
|
|
153
|
+
and stats stay after the feed on mobile. -->
|
|
154
|
+
<div class="cpub-mobile-contest-hoist">
|
|
155
|
+
<HomepageSectionRenderer :sections="sortedSections" zone="sidebar" :restrict-types="['contests']" />
|
|
156
|
+
</div>
|
|
157
|
+
|
|
151
158
|
<!-- 2-column layout: main + sidebar -->
|
|
152
159
|
<div class="cpub-main-layout">
|
|
153
160
|
<main class="cpub-feed-col">
|
|
154
161
|
<HomepageSectionRenderer :sections="sortedSections" zone="main" />
|
|
155
162
|
</main>
|
|
156
163
|
<aside class="cpub-sidebar">
|
|
157
|
-
|
|
164
|
+
<!-- display:contents wrappers — layout-transparent, so the
|
|
165
|
+
sidebar's flex gap is unaffected. Desktop shows the full
|
|
166
|
+
sidebar; mobile shows it minus contests (hoisted above). -->
|
|
167
|
+
<div class="cpub-sidebar-desktop">
|
|
168
|
+
<HomepageSectionRenderer :sections="sortedSections" zone="sidebar" />
|
|
169
|
+
</div>
|
|
170
|
+
<div class="cpub-sidebar-mobile">
|
|
171
|
+
<HomepageSectionRenderer :sections="sortedSections" zone="sidebar" :exclude-types="['contests']" />
|
|
172
|
+
</div>
|
|
158
173
|
<!-- Powered badge -->
|
|
159
174
|
<div class="cpub-powered-badge">
|
|
160
175
|
<span class="cpub-powered-text">Powered by</span>
|
|
@@ -494,7 +509,7 @@ async function handleHubJoin(hubSlug: string): Promise<void> {
|
|
|
494
509
|
gap: 48px;
|
|
495
510
|
}
|
|
496
511
|
|
|
497
|
-
.cpub-hero-content { flex: 1; }
|
|
512
|
+
.cpub-hero-content { flex: 1; min-width: 0; }
|
|
498
513
|
|
|
499
514
|
.cpub-hero-eyebrow {
|
|
500
515
|
display: flex;
|
|
@@ -553,7 +568,7 @@ async function handleHubJoin(hubSlug: string): Promise<void> {
|
|
|
553
568
|
max-width: 560px;
|
|
554
569
|
}
|
|
555
570
|
|
|
556
|
-
.cpub-hero-actions { display: flex; gap: 8px; }
|
|
571
|
+
.cpub-hero-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
557
572
|
|
|
558
573
|
.cpub-hero-meta {
|
|
559
574
|
display: flex;
|
|
@@ -898,6 +913,17 @@ async function handleHubJoin(hubSlug: string): Promise<void> {
|
|
|
898
913
|
gap: 18px;
|
|
899
914
|
}
|
|
900
915
|
|
|
916
|
+
/* Contests hoisted above the feed on mobile only. display:contents keeps
|
|
917
|
+
the sidebar's flex gap intact on desktop (wrapper boxes vanish). */
|
|
918
|
+
.cpub-mobile-contest-hoist { display: none; }
|
|
919
|
+
.cpub-sidebar-desktop { display: contents; }
|
|
920
|
+
.cpub-sidebar-mobile { display: none; }
|
|
921
|
+
@media (max-width: 768px) {
|
|
922
|
+
.cpub-mobile-contest-hoist { display: block; max-width: 1280px; margin: 0 auto; padding: 16px 16px 0; }
|
|
923
|
+
.cpub-sidebar-desktop { display: none; }
|
|
924
|
+
.cpub-sidebar-mobile { display: contents; }
|
|
925
|
+
}
|
|
926
|
+
|
|
901
927
|
.cpub-sb-head {
|
|
902
928
|
font-size: 10px;
|
|
903
929
|
font-family: var(--font-mono);
|
|
@@ -1167,6 +1193,8 @@ async function handleHubJoin(hubSlug: string): Promise<void> {
|
|
|
1167
1193
|
.cpub-hero-inner {
|
|
1168
1194
|
padding: 24px 16px;
|
|
1169
1195
|
}
|
|
1196
|
+
.cpub-hero-actions { width: 100%; }
|
|
1197
|
+
.cpub-hero-actions .cpub-btn { flex: 1 1 140px; justify-content: center; }
|
|
1170
1198
|
.cpub-main-layout {
|
|
1171
1199
|
padding: 16px;
|
|
1172
1200
|
}
|