@commonpub/layer 0.43.0 → 0.43.2

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.
@@ -1,4 +1,4 @@
1
- import type { Serialized, ContentListItem, PaginatedResponse } from '@commonpub/server';
1
+ import type { Serialized, ContentListItem } from '@commonpub/server';
2
2
 
3
3
  /**
4
4
  * Unified content-feed loader with transparent keyset/offset pagination.
@@ -24,9 +24,15 @@ import type { Serialized, ContentListItem, PaginatedResponse } from '@commonpub/
24
24
  */
25
25
 
26
26
  type FeedQuery = Record<string, unknown> & { sort?: string; limit?: number };
27
- type Item = Serialized<ContentListItem>;
27
+ /** Public item type callers consume (for ContentCard). */
28
+ export type FeedItem = Serialized<ContentListItem>;
28
29
 
29
- interface KeysetResponse { items: Item[]; nextCursor: string | null }
30
+ // Wire response shape for BOTH endpoints (keyset → nextCursor, offset total). The
31
+ // items are typed `Record<string, unknown>[]`, NOT `Serialized<ContentListItem>[]`, on
32
+ // purpose: feeding the deeply-recursive Serialized<…> mapped type through useFetch's own
33
+ // generic wrapper tripped TS2589 "excessively deep" under the consumer apps' stricter
34
+ // typecheck (deveco). We cast to FeedItem[] once, at the `items` boundary callers read.
35
+ interface FeedResponse { items: Array<Record<string, unknown>>; nextCursor?: string | null; total?: number }
30
36
 
31
37
  export function useContentFeed(query: Ref<FeedQuery> | ComputedRef<FeedQuery>) {
32
38
  const toast = useToast();
@@ -46,17 +52,28 @@ export function useContentFeed(query: Ref<FeedQuery> | ComputedRef<FeedQuery>) {
46
52
 
47
53
  // Initial page — SSR-friendly via useFetch. Both endpoints accept the same query;
48
54
  // the keyset one returns { items, nextCursor }, the offset one { items, total }.
55
+ //
56
+ // No explicit useFetch<…> generic: parameterising it makes TS instantiate useFetch's
57
+ // own deep transform/pick generic machinery, which trips TS2589 "excessively deep"
58
+ // under the consumer apps' stricter typecheck (deveco already @ts-ignores the same on
59
+ // its own useFetch calls). We let it infer and read `data` through a typed `page`
60
+ // computed cast to the shallow FeedResponse instead.
49
61
  const endpoint = computed(() => (isKeyset.value ? '/api/content/feed' : '/api/content'));
50
- const { data, pending } = useFetch<KeysetResponse | PaginatedResponse<Item>>(endpoint, {
51
- query,
52
- watch: [query],
53
- });
62
+ // @ts-ignore TS2589: a computed endpoint that is a UNION of two typed routes makes
63
+ // Nuxt's useFetch resolve typed route data for both, blowing the instantiation depth
64
+ // under the consumer apps' stricter typecheck (deveco @ts-ignores the same on its own
65
+ // useFetch calls). Runtime is unaffected; `data` is read through the typed `page` below.
66
+ const { data, pending } = useFetch(endpoint, { query, watch: [query] });
67
+ const page = computed<FeedResponse | null>(() => (data.value as FeedResponse | null) ?? null);
54
68
 
55
69
  // Local accumulator: the first page from useFetch, plus any load-more pages. Kept
56
70
  // separate from `data` so we never mutate the useFetch payload (which it re-creates
57
71
  // on refetch) — and so a tab switch cleanly replaces the list.
58
- const extra = ref<Item[]>([]);
59
- const items = computed<Item[]>(() => [...(data.value?.items ?? []), ...extra.value]);
72
+ const extra = ref<Array<Record<string, unknown>>>([]);
73
+ // Single cast from the shallow wire type to the public FeedItem[] callers consume.
74
+ const items = computed<FeedItem[]>(
75
+ () => [...(page.value?.items ?? []), ...extra.value] as FeedItem[],
76
+ );
60
77
 
61
78
  // Reset pagination whenever the underlying query (tab/filter) changes.
62
79
  watch(
@@ -71,10 +88,10 @@ export function useContentFeed(query: Ref<FeedQuery> | ComputedRef<FeedQuery>) {
71
88
 
72
89
  // Seed the keyset cursor from the first page once it arrives.
73
90
  watch(
74
- data,
75
- (d) => {
91
+ page,
92
+ (p) => {
76
93
  if (isKeyset.value) {
77
- cursor.value = (d as KeysetResponse | null)?.nextCursor ?? null;
94
+ cursor.value = p?.nextCursor ?? null;
78
95
  if (cursor.value === null) exhausted.value = true;
79
96
  }
80
97
  },
@@ -95,15 +112,15 @@ export function useContentFeed(query: Ref<FeedQuery> | ComputedRef<FeedQuery>) {
95
112
  try {
96
113
  if (isKeyset.value) {
97
114
  if (!cursor.value) { exhausted.value = true; return; }
98
- const res = await $fetch<KeysetResponse>('/api/content/feed', {
115
+ const res = await $fetch<FeedResponse>('/api/content/feed', {
99
116
  query: { ...query.value, cursor: cursor.value },
100
117
  });
101
118
  if (res.items?.length) extra.value.push(...res.items);
102
- cursor.value = res.nextCursor;
119
+ cursor.value = res.nextCursor ?? null;
103
120
  if (!res.nextCursor) exhausted.value = true;
104
121
  } else {
105
122
  const offset = items.value.length;
106
- const res = await $fetch<PaginatedResponse<Item>>('/api/content', {
123
+ const res = await $fetch<FeedResponse>('/api/content', {
107
124
  query: { ...query.value, offset },
108
125
  });
109
126
  if (res.items?.length) extra.value.push(...res.items);
@@ -266,20 +266,50 @@ const userUsername = computed(() => user.value?.username ?? '');
266
266
 
267
267
  .cpub-layout { min-height: 100vh; display: flex; flex-direction: column; }
268
268
 
269
- /* ═══ TOPBAR ═══ */
269
+ /* ═══ TOPBAR ═══
270
+ Structure is token-driven (--cpub-topbar-*) so a theme can change the bar's SHAPE
271
+ — height, radius, shadow, position, padding — not just its colors, without forking
272
+ this layout. Every default reproduces the current flat 48px bar exactly.
273
+ (Centering the bar's CONTENT at a max width while keeping a full-bleed background
274
+ needs an inner wrapper element, which the base markup doesn't have — that one
275
+ aspect stays a structural choice, not a token.) */
270
276
  .cpub-topbar {
271
- position: fixed; top: 0; left: 0; right: 0; height: 48px;
272
- background: var(--surface); border-bottom: var(--border-width-default) solid var(--border);
273
- display: flex; align-items: center; padding: 0 20px; gap: 0; z-index: 100;
277
+ position: var(--cpub-topbar-position, fixed); top: 0; left: 0; right: 0;
278
+ height: var(--cpub-topbar-height, 48px);
279
+ background: var(--cpub-topbar-bg, var(--surface));
280
+ border-bottom: var(--cpub-topbar-border, var(--border-width-default) solid var(--border));
281
+ border-bottom-left-radius: var(--cpub-topbar-radius, 0);
282
+ border-bottom-right-radius: var(--cpub-topbar-radius, 0);
283
+ box-shadow: var(--cpub-topbar-shadow, none);
284
+ backdrop-filter: var(--cpub-topbar-blur, none);
285
+ display: flex; align-items: center;
286
+ padding: 0 var(--cpub-topbar-padding-x, 20px); gap: 0; z-index: 100;
274
287
  }
275
288
  .cpub-topbar-logo { display: flex; align-items: center; flex-shrink: 0; text-decoration: none; color: var(--text); }
276
289
 
277
290
  /* Nav styles use :deep() to reach into NavRenderer/NavDropdown/NavLink child components */
278
291
  :deep(.cpub-topbar-nav) { display: flex; align-items: center; gap: 2px; margin-left: 24px; }
279
- :deep(.cpub-nav-link) { font-size: 12px; color: var(--text-dim); padding: 5px 12px; border: var(--border-width-default) solid transparent; background: none; text-decoration: none; transition: color 0.15s, background 0.15s; display: flex; align-items: center; gap: 6px; }
292
+ /* Nav-link shape + active state are token-driven (--cpub-nav-link-*) so a theme can
293
+ make pill-shaped/larger/accent-colored nav links (deveco) without forking. Defaults
294
+ = the current 12px square neutral link. */
295
+ :deep(.cpub-nav-link) {
296
+ font-size: var(--cpub-nav-link-size, 12px);
297
+ font-weight: var(--cpub-nav-link-weight, 400);
298
+ color: var(--cpub-nav-link-color, var(--text-dim));
299
+ padding: var(--cpub-nav-link-padding, 5px 12px);
300
+ border: var(--border-width-default) solid transparent;
301
+ border-radius: var(--cpub-nav-link-radius, var(--radius));
302
+ background: none; text-decoration: none;
303
+ transition: color 0.15s, background 0.15s; display: flex; align-items: center; gap: 6px;
304
+ }
280
305
  :deep(.cpub-nav-link i) { font-size: 10px; }
281
306
  :deep(.cpub-nav-link:hover) { color: var(--text); background: var(--surface2); }
282
- :deep(.cpub-nav-link.router-link-active) { color: var(--text); background: var(--surface2); border-color: var(--border); }
307
+ :deep(.cpub-nav-link.router-link-active) {
308
+ color: var(--cpub-nav-link-active-color, var(--text));
309
+ background: var(--cpub-nav-link-active-bg, var(--surface2));
310
+ border-color: var(--cpub-nav-link-active-border, var(--border));
311
+ font-weight: var(--cpub-nav-link-active-weight, 400);
312
+ }
283
313
  :deep(.cpub-nav-link--disabled) { opacity: 0.35; cursor: not-allowed; pointer-events: none; }
284
314
 
285
315
  /* Nav dropdowns */
@@ -343,7 +373,7 @@ const userUsername = computed(() => user.value?.username ?? '');
343
373
  .cpub-dropdown-item--mobile { display: none; }
344
374
 
345
375
  .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; }
346
- .cpub-mobile-menu { display: none; position: fixed; inset: 0; top: 48px; z-index: 99; background: var(--color-surface-overlay-light); }
376
+ .cpub-mobile-menu { display: none; position: fixed; inset: 0; top: var(--cpub-topbar-height, 48px); z-index: 99; background: var(--color-surface-overlay-light); }
347
377
  :deep(.cpub-mobile-nav) { background: var(--surface); border-bottom: var(--border-width-default) solid var(--border); padding: 8px 0; display: flex; flex-direction: column; box-shadow: var(--shadow-md); }
348
378
  :deep(.cpub-mobile-link) { display: flex; align-items: center; gap: 10px; padding: 10px 20px; font-size: 13px; color: var(--text-dim); text-decoration: none; transition: background 0.1s; }
349
379
  :deep(.cpub-mobile-link:hover) { background: var(--surface2); color: var(--text); }
@@ -351,22 +381,27 @@ const userUsername = computed(() => user.value?.username ?? '');
351
381
  .cpub-mobile-divider { height: 2px; background: var(--border2); margin: 4px 16px; }
352
382
  .cpub-mobile-nav-extra { border-top: var(--border-width-default) solid var(--border2); }
353
383
 
354
- #main-content { margin-top: 48px; flex: 1; }
384
+ /* Offsets the fixed top bar MUST track --cpub-topbar-height (only when the bar is
385
+ actually fixed; a sticky bar reserves its own space, so a theme that switches to
386
+ sticky should zero this via --cpub-content-top-offset). */
387
+ #main-content { margin-top: var(--cpub-content-top-offset, var(--cpub-topbar-height, 48px)); flex: 1; }
355
388
 
356
389
  /* ═══ FOOTER ═══ */
357
- .cpub-footer { background: var(--surface); border-top: var(--border-width-default) solid var(--border); margin-top: auto; }
390
+ /* Footer bg + text are token-driven (--cpub-footer-*) so a theme can ship a dark/branded
391
+ footer (deveco green) without forking. Defaults = the current neutral surface footer. */
392
+ .cpub-footer { background: var(--cpub-footer-bg, var(--surface)); border-top: var(--border-width-default) solid var(--cpub-footer-border, var(--border)); margin-top: auto; }
358
393
  .cpub-footer-inner { max-width: 1200px; margin: 0 auto; padding: 40px 32px 32px; display: grid; grid-template-columns: 1.5fr repeat(3, 1fr); gap: 32px; }
359
394
  .cpub-footer-brand { display: flex; flex-direction: column; gap: 8px; }
360
- .cpub-footer-logo { font-family: var(--font-mono); font-size: 14px; font-weight: 700; color: var(--text); }
361
- .cpub-footer-tagline { font-size: 12px; color: var(--text-dim); }
395
+ .cpub-footer-logo { font-family: var(--font-mono); font-size: 14px; font-weight: 700; color: var(--cpub-footer-heading, var(--text)); }
396
+ .cpub-footer-tagline { font-size: 12px; color: var(--cpub-footer-text, var(--text-dim)); }
362
397
  .cpub-footer-social { display: flex; gap: 8px; margin-top: 8px; }
363
- .cpub-footer-social-link { width: 28px; height: 28px; background: var(--surface2); border: var(--border-width-default) solid var(--border); display: flex; align-items: center; justify-content: center; color: var(--text-dim); font-size: 12px; text-decoration: none; transition: all 0.12s; }
398
+ .cpub-footer-social-link { width: 28px; height: 28px; background: var(--surface2); border: var(--border-width-default) solid var(--border); display: flex; align-items: center; justify-content: center; color: var(--cpub-footer-text, var(--text-dim)); font-size: 12px; text-decoration: none; transition: all 0.12s; }
364
399
  .cpub-footer-social-link:hover { background: var(--accent); color: var(--color-text-inverse); border-color: var(--accent); }
365
400
  .cpub-footer-col { display: flex; flex-direction: column; gap: 6px; }
366
- .cpub-footer-col-title { font-family: var(--font-mono); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em; color: var(--text-faint); margin-bottom: 4px; }
367
- .cpub-footer-link { font-size: 12px; color: var(--text-dim); text-decoration: none; transition: color 0.12s; }
368
- .cpub-footer-link:hover { color: var(--text); }
369
- .cpub-footer-bottom { max-width: 1200px; margin: 0 auto; padding: 16px 32px; border-top: var(--border-width-default) solid var(--border); font-size: 10px; font-family: var(--font-mono); color: var(--text-faint); }
401
+ .cpub-footer-col-title { font-family: var(--font-mono); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em; color: var(--cpub-footer-muted, var(--text-faint)); margin-bottom: 4px; }
402
+ .cpub-footer-link { font-size: 12px; color: var(--cpub-footer-text, var(--text-dim)); text-decoration: none; transition: color 0.12s; }
403
+ .cpub-footer-link:hover { color: var(--cpub-footer-link-hover, var(--text)); }
404
+ .cpub-footer-bottom { max-width: 1200px; margin: 0 auto; padding: 16px 32px; border-top: var(--border-width-default) solid var(--cpub-footer-border, var(--border)); font-size: 10px; font-family: var(--font-mono); color: var(--cpub-footer-muted, var(--text-faint)); }
370
405
 
371
406
  @media (max-width: 768px) {
372
407
  :deep(.cpub-topbar-nav) { display: none; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonpub/layer",
3
- "version": "0.43.0",
3
+ "version": "0.43.2",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "files": [
@@ -53,16 +53,16 @@
53
53
  "vue": "^3.4.0",
54
54
  "vue-router": "^4.3.0",
55
55
  "zod": "^4.3.6",
56
+ "@commonpub/auth": "0.7.0",
57
+ "@commonpub/config": "0.16.0",
56
58
  "@commonpub/docs": "0.6.3",
57
59
  "@commonpub/learning": "0.5.2",
58
- "@commonpub/auth": "0.7.0",
59
- "@commonpub/explainer": "0.7.15",
60
+ "@commonpub/editor": "0.7.11",
60
61
  "@commonpub/schema": "0.25.0",
61
- "@commonpub/server": "2.71.0",
62
+ "@commonpub/explainer": "0.7.15",
63
+ "@commonpub/ui": "0.9.2",
62
64
  "@commonpub/protocol": "0.12.0",
63
- "@commonpub/editor": "0.7.11",
64
- "@commonpub/ui": "0.9.1",
65
- "@commonpub/config": "0.16.0"
65
+ "@commonpub/server": "2.71.0"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@testing-library/jest-dom": "^6.9.1",
package/theme/base.css CHANGED
@@ -231,6 +231,46 @@
231
231
  --cpub-card-min: 260px; /* min column width before wrapping (auto-fill) */
232
232
  --cpub-card-gap: 20px; /* gutter between cards */
233
233
 
234
+ /* === CHROME (topbar / nav / footer / banner) ===
235
+ The global layout chrome is fully token-driven so an instance theme can change
236
+ its SHAPE — not just colors — without forking the layout (the deveco use case).
237
+ Every default below EXACTLY reproduces the current base look, so existing
238
+ instances + built-in themes are unaffected unless they opt in by overriding a
239
+ token. A theme that wants e.g. deveco's elevated rounded bar sets these in its
240
+ own [data-theme="…"] / :root block. See layers/base/layouts/default.vue. */
241
+
242
+ /* Top bar */
243
+ --cpub-topbar-height: 48px; /* MUST match the content top-offset + mobile-menu top */
244
+ --cpub-topbar-bg: var(--surface);
245
+ --cpub-topbar-border: var(--border-width-default) solid var(--border); /* shorthand: full bottom border */
246
+ --cpub-topbar-radius: 0; /* deveco: 12px rounded bottom */
247
+ --cpub-topbar-shadow: none; /* deveco: 0 2px 8px rgba(0,0,0,.08) */
248
+ --cpub-topbar-position: fixed; /* deveco: sticky */
249
+ --cpub-topbar-padding-x: 20px;
250
+ --cpub-topbar-blur: none; /* deveco: blur(8px) backdrop */
251
+ /* Top offset reserved for the fixed bar. A theme that makes the bar `sticky`
252
+ (which reserves its own space) should set this to 0. Default = the bar height. */
253
+ --cpub-content-top-offset: var(--cpub-topbar-height, 48px);
254
+
255
+ /* Nav links (rendered by NavRenderer → .cpub-nav-link) */
256
+ --cpub-nav-link-size: 12px; /* deveco: 14px */
257
+ --cpub-nav-link-weight: 400; /* deveco: 500 */
258
+ --cpub-nav-link-padding: 5px 12px; /* deveco: 8px 14px */
259
+ --cpub-nav-link-radius: var(--radius); /* deveco: 6px (pill) */
260
+ --cpub-nav-link-color: var(--text-dim);
261
+ --cpub-nav-link-active-color: var(--text); /* deveco: var(--deveco-dark-green) */
262
+ --cpub-nav-link-active-bg: var(--surface2); /* deveco: var(--accent-bg) */
263
+ --cpub-nav-link-active-weight: 400; /* deveco: 600 */
264
+ --cpub-nav-link-active-border: var(--border); /* color of the active link border */
265
+
266
+ /* Footer */
267
+ --cpub-footer-bg: var(--surface); /* deveco: var(--deveco-dark-green) */
268
+ --cpub-footer-text: var(--text-dim); /* body/link text; deveco: rgba(255,255,255,.7) */
269
+ --cpub-footer-muted: var(--text-faint); /* col titles / bottom bar; deveco: rgba(255,255,255,.4) */
270
+ --cpub-footer-border: var(--border); /* deveco: rgba(255,255,255,.1) */
271
+ --cpub-footer-link-hover: var(--text); /* link hover color; deveco: var(--accent) */
272
+ --cpub-footer-heading: var(--text); /* brand logo text; deveco uses its dark-bg logo */
273
+
234
274
  /* === FOCUS === */
235
275
  --focus-ring: var(--shadow-accent);
236
276
  }