@commonpub/layer 0.23.1 → 0.23.3

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.
@@ -419,13 +419,31 @@ useJsonLd({
419
419
  font-weight: 400;
420
420
  }
421
421
 
422
- /* ── AVATARS ── */
422
+ /* ── AVATARS ──
423
+ * Two render modes share the .cpub-av class:
424
+ * <img class="cpub-av cpub-av-lg" ...> ← avatar photo
425
+ * <div class="cpub-av cpub-av-lg">JD</div> ← initials fallback when no avatar
426
+ *
427
+ * Sizing + border-radius is shared. But `display: flex` MUST NOT apply to
428
+ * the <img> — when a replaced element gets `display: flex` set, browsers
429
+ * (notably Chromium) treat the img content render-box inconsistently and
430
+ * the inline `object-fit: cover` is silently dropped, producing a squished
431
+ * (stretched-to-box) image instead of a center-cropped one. Visible on
432
+ * deveco.io blog pages where author avatars are vertical photos (e.g.
433
+ * 816×1456) rendered into a 44×44 square.
434
+ *
435
+ * Fix: scope display:flex centering to the div variant only.
436
+ */
423
437
  .cpub-av {
424
438
  width: 28px;
425
439
  height: 28px;
426
440
  border-radius: 50%;
427
441
  background: var(--surface3);
428
442
  border: var(--border-width-default) solid var(--border);
443
+ flex-shrink: 0;
444
+ }
445
+
446
+ div.cpub-av {
429
447
  display: flex;
430
448
  align-items: center;
431
449
  justify-content: center;
@@ -433,7 +451,12 @@ useJsonLd({
433
451
  font-weight: 600;
434
452
  color: var(--text-dim);
435
453
  font-family: var(--font-mono);
436
- flex-shrink: 0;
454
+ }
455
+
456
+ /* Defensive: even when consumers forget the inline `object-fit:cover`,
457
+ img.cpub-av crops instead of stretching. */
458
+ img.cpub-av {
459
+ object-fit: cover;
437
460
  }
438
461
 
439
462
  .cpub-av-lg { width: 44px; height: 44px; font-size: 14px; }
@@ -763,12 +763,19 @@ async function handleBuild(): Promise<void> {
763
763
  flex-wrap: wrap;
764
764
  }
765
765
 
766
+ /* See ArticleView.vue's .cpub-av comment for why display:flex is scoped
767
+ * to the div-variant only — stops img-variant from squishing portrait
768
+ * avatars (object-fit:cover gets dropped on flex-set replaced elements). */
766
769
  .cpub-av {
767
770
  width: 28px;
768
771
  height: 28px;
769
772
  border-radius: 50%;
770
773
  background: var(--surface3);
771
774
  border: var(--border-width-default) solid var(--border);
775
+ flex-shrink: 0;
776
+ }
777
+
778
+ div.cpub-av {
772
779
  display: flex;
773
780
  align-items: center;
774
781
  justify-content: center;
@@ -776,7 +783,10 @@ async function handleBuild(): Promise<void> {
776
783
  font-weight: 700;
777
784
  color: var(--text-dim);
778
785
  font-family: var(--font-mono);
779
- flex-shrink: 0;
786
+ }
787
+
788
+ img.cpub-av {
789
+ object-fit: cover;
780
790
  }
781
791
 
782
792
  .cpub-av-lg { width: 36px; height: 36px; font-size: 12px; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonpub/layer",
3
- "version": "0.23.1",
3
+ "version": "0.23.3",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "files": [
@@ -51,16 +51,16 @@
51
51
  "vue": "^3.4.0",
52
52
  "vue-router": "^4.3.0",
53
53
  "zod": "^4.3.6",
54
- "@commonpub/auth": "0.6.0",
55
54
  "@commonpub/config": "0.15.0",
55
+ "@commonpub/docs": "0.6.3",
56
+ "@commonpub/learning": "0.5.2",
56
57
  "@commonpub/editor": "0.7.11",
57
- "@commonpub/explainer": "0.7.15",
58
58
  "@commonpub/protocol": "0.12.0",
59
- "@commonpub/server": "2.57.0",
60
59
  "@commonpub/schema": "0.17.0",
61
- "@commonpub/learning": "0.5.2",
60
+ "@commonpub/server": "2.57.0",
62
61
  "@commonpub/ui": "0.9.0",
63
- "@commonpub/docs": "0.6.3"
62
+ "@commonpub/auth": "0.6.0",
63
+ "@commonpub/explainer": "0.7.15"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@testing-library/jest-dom": "^6.9.1",
package/pages/index.vue CHANGED
@@ -14,9 +14,22 @@ const sortedSections = computed(() =>
14
14
  );
15
15
 
16
16
  const { user: authUser } = useAuth();
17
- const { hubs: hubsEnabled, contests: contestsEnabled, learning: learningEnabled, video: videoEnabled, docs: docsEnabled, editorial: editorialEnabled, layoutEngine: layoutEngineEnabled } = useFeatures();
17
+ const { hubs: hubsEnabled, contests: contestsEnabled, learning: learningEnabled, video: videoEnabled, docs: docsEnabled, editorial: editorialEnabled, layoutEngine: layoutEngineFlag } = useFeatures();
18
18
  const { enabledTypeMeta } = useContentTypes();
19
19
 
20
+ // Layout engine path — only active when BOTH the flag is on AND a layout
21
+ // actually exists in the DB for this route. Prevents the "operator enables
22
+ // the flag without seeding a homepage layout → page goes blank" trap
23
+ // (reported by user, session 158 follow-up). When the flag's on but
24
+ // useLayout returns null (feature off OR no published layout for route),
25
+ // we fall through to the configurable/legacy render paths so the user
26
+ // still sees content. Admin can call POST /api/admin/layouts/seed-homepage
27
+ // to populate a default and start using the layout engine for real.
28
+ const { layout: homepageLayout } = useLayout('/');
29
+ const layoutEngineActive = computed(
30
+ () => layoutEngineFlag.value && homepageLayout.value !== null,
31
+ );
32
+
20
33
  const activeTab = ref(authUser.value ? 'foryou' : 'latest');
21
34
  const tabs = computed(() => [
22
35
  { value: 'foryou', label: 'For You', icon: 'fa-solid fa-sparkles' },
@@ -144,15 +157,16 @@ async function handleHubJoin(hubSlug: string): Promise<void> {
144
157
  <template>
145
158
  <div>
146
159
  <!-- ═══ LAYOUT ENGINE (Phase 1c — feature-flagged) ═══
147
- When `features.layoutEngine` is ON, render the homepage via
148
- <LayoutSlot> zones backed by the layouts table. Operators flip
149
- this on AFTER running POST /api/admin/layouts/seed-homepage so
150
- a default layout exists at scope ('route', '/'). If the flag is
151
- on but no layout exists, LayoutSlot renders nothing and the
152
- user sees an empty page — documented at
153
- docs/reference/guides/layout-engine.md. Falls through to the
154
- configurable section renderer when the flag is OFF (default). -->
155
- <template v-if="layoutEngineEnabled">
160
+ Renders DB-driven layout via <LayoutSlot> zones ONLY when BOTH
161
+ (a) features.layoutEngine is ON AND (b) a layout actually exists
162
+ at scope ('route', '/'). Falls through to the configurable or
163
+ legacy renderer otherwise including when the flag's ON but
164
+ no layout has been seeded yet (the "blank page" trap reported
165
+ in session 158 follow-up).
166
+
167
+ To enable for real: POST /api/admin/layouts/seed-homepage,
168
+ then flip the flag. See docs/reference/guides/layout-engine.md. -->
169
+ <template v-if="layoutEngineActive">
156
170
  <LayoutSlot route="/" zone="full-width" />
157
171
  <div class="cpub-main-layout">
158
172
  <main class="cpub-feed-col">
@@ -37,13 +37,15 @@ export default defineEventHandler(async (event) => {
37
37
 
38
38
  const merged = { ...existing, ...body.overrides };
39
39
 
40
- // Remove overrides that match the base config default (no point overriding to same value)
41
- const base = config.features as unknown as Record<string, boolean>;
42
- for (const [key, value] of Object.entries(merged)) {
43
- if (base[key] === value) {
44
- delete (merged as Record<string, unknown>)[key];
45
- }
46
- }
40
+ // NOTE: previously this block tried to "remove overrides that match the
41
+ // base config" as a dedup, but `config.features` is the EFFECTIVE config
42
+ // (with overrides ALREADY applied) — so re-saving a previously-overridden
43
+ // flag would see `base[key] === value` (because the override was applied
44
+ // to base) and delete the override. The flag would then revert to the
45
+ // build-time default on next read. User-visible symptom: "I flipped X on
46
+ // in the UI but it kept reverting off." The dedup is dropped — the user's
47
+ // explicit override is persisted verbatim. Future "reset to default" can
48
+ // be a separate DELETE-overrides handler.
47
49
 
48
50
  await setInstanceSetting(db, 'features.overrides', merged, user.id, getRequestIP(event) ?? undefined);
49
51