create-cogenta 0.2.1 → 0.5.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.
Files changed (183) hide show
  1. package/dist/blueprint-defaults.d.ts.map +1 -1
  2. package/dist/blueprint-defaults.js +6 -0
  3. package/dist/blueprint-defaults.js.map +1 -1
  4. package/dist/blueprints/assets/photos/association/community-cleanup.jpg +0 -0
  5. package/dist/blueprints/assets/photos/association/fundraising-dinner.jpg +0 -0
  6. package/dist/blueprints/assets/photos/association/garden-planting.jpg +0 -0
  7. package/dist/blueprints/assets/photos/association/harvest-food-drive.jpg +0 -0
  8. package/dist/blueprints/assets/photos/association/hero.jpg +0 -0
  9. package/dist/blueprints/assets/photos/association/volunteer-orientation.jpg +0 -0
  10. package/dist/blueprints/assets/photos/association/winter-coat-collection.jpg +0 -0
  11. package/dist/blueprints/assets/photos/blog/blog-runs-itself.jpg +0 -0
  12. package/dist/blueprints/assets/photos/blog/desk-setup.jpg +0 -0
  13. package/dist/blueprints/assets/photos/blog/editing.jpg +0 -0
  14. package/dist/blueprints/assets/photos/blog/hero.jpg +0 -0
  15. package/dist/blueprints/assets/photos/blog/notebooks.jpg +0 -0
  16. package/dist/blueprints/assets/photos/blog/plain-text-editor.jpg +0 -0
  17. package/dist/blueprints/assets/photos/blog/reading-nonfiction.jpg +0 -0
  18. package/dist/blueprints/assets/photos/blog/small-tool.jpg +0 -0
  19. package/dist/blueprints/assets/photos/blog/writing-schedule.jpg +0 -0
  20. package/dist/blueprints/assets/photos/magazine/business-2.jpg +0 -0
  21. package/dist/blueprints/assets/photos/magazine/business-3.jpg +0 -0
  22. package/dist/blueprints/assets/photos/magazine/business.jpg +0 -0
  23. package/dist/blueprints/assets/photos/magazine/culture-2.jpg +0 -0
  24. package/dist/blueprints/assets/photos/magazine/culture-3.jpg +0 -0
  25. package/dist/blueprints/assets/photos/magazine/culture.jpg +0 -0
  26. package/dist/blueprints/assets/photos/magazine/news-2.jpg +0 -0
  27. package/dist/blueprints/assets/photos/magazine/news-3.jpg +0 -0
  28. package/dist/blueprints/assets/photos/magazine/news.jpg +0 -0
  29. package/dist/blueprints/assets/photos/magazine/opinion-2.jpg +0 -0
  30. package/dist/blueprints/assets/photos/magazine/opinion-3.jpg +0 -0
  31. package/dist/blueprints/assets/photos/magazine/opinion.jpg +0 -0
  32. package/dist/blueprints/assets/photos/portfolio/adatum-publishing.jpg +0 -0
  33. package/dist/blueprints/assets/photos/portfolio/contoso-mobile-app.jpg +0 -0
  34. package/dist/blueprints/assets/photos/portfolio/fabrikam-annual-report.jpg +0 -0
  35. package/dist/blueprints/assets/photos/portfolio/hero.jpg +0 -0
  36. package/dist/blueprints/assets/photos/portfolio/litware-signage.jpg +0 -0
  37. package/dist/blueprints/assets/photos/portfolio/northwind-rebrand.jpg +0 -0
  38. package/dist/blueprints/assets/photos/portfolio/proseware-launch.jpg +0 -0
  39. package/dist/blueprints/assets/photos/portfolio/tailspin-streaming.jpg +0 -0
  40. package/dist/blueprints/assets/photos/portfolio/wingtip-wayfinding.jpg +0 -0
  41. package/dist/blueprints/assets/photos/restaurant/charred-octopus.jpg +0 -0
  42. package/dist/blueprints/assets/photos/restaurant/chocolate-tart.jpg +0 -0
  43. package/dist/blueprints/assets/photos/restaurant/creme-brulee.jpg +0 -0
  44. package/dist/blueprints/assets/photos/restaurant/hero.jpg +0 -0
  45. package/dist/blueprints/assets/photos/restaurant/house-red.jpg +0 -0
  46. package/dist/blueprints/assets/photos/restaurant/house-white.jpg +0 -0
  47. package/dist/blueprints/assets/photos/restaurant/pan-seared-trout.jpg +0 -0
  48. package/dist/blueprints/assets/photos/restaurant/poached-pear.jpg +0 -0
  49. package/dist/blueprints/assets/photos/restaurant/roasted-beet-salad.jpg +0 -0
  50. package/dist/blueprints/assets/photos/restaurant/slow-roast-duck-leg.jpg +0 -0
  51. package/dist/blueprints/assets/photos/restaurant/soup-of-the-day.jpg +0 -0
  52. package/dist/blueprints/assets/photos/restaurant/sparkling-water.jpg +0 -0
  53. package/dist/blueprints/assets/photos/restaurant/wild-mushroom-risotto.jpg +0 -0
  54. package/dist/blueprints/assets/photos/saas/avatar-1.jpg +0 -0
  55. package/dist/blueprints/assets/photos/saas/hero.jpg +0 -0
  56. package/dist/blueprints/assets/photos/store/camp-blanket.jpg +0 -0
  57. package/dist/blueprints/assets/photos/store/canvas-tote.jpg +0 -0
  58. package/dist/blueprints/assets/photos/store/cast-iron-skillet.jpg +0 -0
  59. package/dist/blueprints/assets/photos/store/ceramic-pour-over-set.jpg +0 -0
  60. package/dist/blueprints/assets/photos/store/enamel-mug.jpg +0 -0
  61. package/dist/blueprints/assets/photos/store/everyday-tee.jpg +0 -0
  62. package/dist/blueprints/assets/photos/store/field-jacket.jpg +0 -0
  63. package/dist/blueprints/assets/photos/store/hero.jpg +0 -0
  64. package/dist/blueprints/assets/photos/store/leather-card-holder.jpg +0 -0
  65. package/dist/blueprints/assets/photos/store/linen-table-runner.jpg +0 -0
  66. package/dist/blueprints/assets/photos/store/trail-tote.jpg +0 -0
  67. package/dist/blueprints/assets/photos/store/wool-beanie.jpg +0 -0
  68. package/dist/blueprints/assets/photos/store/wool-overshirt.jpg +0 -0
  69. package/dist/blueprints/assets/photos/vitrine/avatar-1.jpg +0 -0
  70. package/dist/blueprints/assets/photos/vitrine/avatar-2.jpg +0 -0
  71. package/dist/blueprints/assets/photos/vitrine/avatar-3.jpg +0 -0
  72. package/dist/blueprints/assets/photos/vitrine/dashboard.jpg +0 -0
  73. package/dist/blueprints/assets/photos/vitrine/hero.jpg +0 -0
  74. package/dist/blueprints/association.d.ts +93 -15
  75. package/dist/blueprints/association.d.ts.map +1 -1
  76. package/dist/blueprints/association.js +637 -130
  77. package/dist/blueprints/association.js.map +1 -1
  78. package/dist/blueprints/blog.d.ts +81 -94
  79. package/dist/blueprints/blog.d.ts.map +1 -1
  80. package/dist/blueprints/blog.js +513 -177
  81. package/dist/blueprints/blog.js.map +1 -1
  82. package/dist/blueprints/content-pack.d.ts +83 -2
  83. package/dist/blueprints/content-pack.d.ts.map +1 -1
  84. package/dist/blueprints/content-pack.js +54 -1
  85. package/dist/blueprints/content-pack.js.map +1 -1
  86. package/dist/blueprints/content-packs.d.ts +4 -3
  87. package/dist/blueprints/content-packs.d.ts.map +1 -1
  88. package/dist/blueprints/content-packs.js +6 -3
  89. package/dist/blueprints/content-packs.js.map +1 -1
  90. package/dist/blueprints/demo-media.d.ts +39 -0
  91. package/dist/blueprints/demo-media.d.ts.map +1 -0
  92. package/dist/blueprints/demo-media.js +30 -0
  93. package/dist/blueprints/demo-media.js.map +1 -0
  94. package/dist/blueprints/documentation.d.ts +78 -15
  95. package/dist/blueprints/documentation.d.ts.map +1 -1
  96. package/dist/blueprints/documentation.js +585 -104
  97. package/dist/blueprints/documentation.js.map +1 -1
  98. package/dist/blueprints/magazine.d.ts +81 -15
  99. package/dist/blueprints/magazine.d.ts.map +1 -1
  100. package/dist/blueprints/magazine.js +431 -85
  101. package/dist/blueprints/magazine.js.map +1 -1
  102. package/dist/blueprints/menus.d.ts +25 -0
  103. package/dist/blueprints/menus.d.ts.map +1 -0
  104. package/dist/blueprints/menus.js +33 -0
  105. package/dist/blueprints/menus.js.map +1 -0
  106. package/dist/blueprints/photo-assets.d.ts +3 -0
  107. package/dist/blueprints/photo-assets.d.ts.map +1 -0
  108. package/dist/blueprints/photo-assets.js +33 -0
  109. package/dist/blueprints/photo-assets.js.map +1 -0
  110. package/dist/blueprints/portfolio.d.ts +81 -13
  111. package/dist/blueprints/portfolio.d.ts.map +1 -1
  112. package/dist/blueprints/portfolio.js +605 -102
  113. package/dist/blueprints/portfolio.js.map +1 -1
  114. package/dist/blueprints/registry.d.ts +3 -1
  115. package/dist/blueprints/registry.d.ts.map +1 -1
  116. package/dist/blueprints/registry.js +4 -1
  117. package/dist/blueprints/registry.js.map +1 -1
  118. package/dist/blueprints/restaurant.d.ts +65 -20
  119. package/dist/blueprints/restaurant.d.ts.map +1 -1
  120. package/dist/blueprints/restaurant.js +350 -117
  121. package/dist/blueprints/restaurant.js.map +1 -1
  122. package/dist/blueprints/saas.d.ts +81 -14
  123. package/dist/blueprints/saas.d.ts.map +1 -1
  124. package/dist/blueprints/saas.js +469 -132
  125. package/dist/blueprints/saas.js.map +1 -1
  126. package/dist/blueprints/site-settings-seed.d.ts +17 -0
  127. package/dist/blueprints/site-settings-seed.d.ts.map +1 -0
  128. package/dist/blueprints/site-settings-seed.js +45 -0
  129. package/dist/blueprints/site-settings-seed.js.map +1 -0
  130. package/dist/blueprints/starting-skins.d.ts +19 -0
  131. package/dist/blueprints/starting-skins.d.ts.map +1 -0
  132. package/dist/blueprints/starting-skins.js +301 -0
  133. package/dist/blueprints/starting-skins.js.map +1 -0
  134. package/dist/blueprints/store.d.ts +173 -0
  135. package/dist/blueprints/store.d.ts.map +1 -0
  136. package/dist/blueprints/store.js +643 -0
  137. package/dist/blueprints/store.js.map +1 -0
  138. package/dist/blueprints/vitrine.d.ts +94 -16
  139. package/dist/blueprints/vitrine.d.ts.map +1 -1
  140. package/dist/blueprints/vitrine.js +421 -142
  141. package/dist/blueprints/vitrine.js.map +1 -1
  142. package/dist/demo-art/compositions.d.ts +48 -0
  143. package/dist/demo-art/compositions.d.ts.map +1 -0
  144. package/dist/demo-art/compositions.js +690 -0
  145. package/dist/demo-art/compositions.js.map +1 -0
  146. package/dist/demo-art/index.d.ts +8 -0
  147. package/dist/demo-art/index.d.ts.map +1 -0
  148. package/dist/demo-art/index.js +5 -0
  149. package/dist/demo-art/index.js.map +1 -0
  150. package/dist/demo-art/oklch.d.ts +39 -0
  151. package/dist/demo-art/oklch.d.ts.map +1 -0
  152. package/dist/demo-art/oklch.js +79 -0
  153. package/dist/demo-art/oklch.js.map +1 -0
  154. package/dist/demo-art/png.d.ts +24 -0
  155. package/dist/demo-art/png.d.ts.map +1 -0
  156. package/dist/demo-art/png.js +110 -0
  157. package/dist/demo-art/png.js.map +1 -0
  158. package/dist/demo-art/render.d.ts +228 -0
  159. package/dist/demo-art/render.d.ts.map +1 -0
  160. package/dist/demo-art/render.js +478 -0
  161. package/dist/demo-art/render.js.map +1 -0
  162. package/dist/llm-setup.d.ts +10 -1
  163. package/dist/llm-setup.d.ts.map +1 -1
  164. package/dist/llm-setup.js +12 -2
  165. package/dist/llm-setup.js.map +1 -1
  166. package/dist/playground-reset.d.ts.map +1 -1
  167. package/dist/playground-reset.js +22 -4
  168. package/dist/playground-reset.js.map +1 -1
  169. package/dist/prompts.d.ts +12 -0
  170. package/dist/prompts.d.ts.map +1 -1
  171. package/dist/prompts.js +32 -0
  172. package/dist/prompts.js.map +1 -1
  173. package/dist/recap.d.ts.map +1 -1
  174. package/dist/recap.js +6 -0
  175. package/dist/recap.js.map +1 -1
  176. package/dist/scaffold.d.ts +22 -2
  177. package/dist/scaffold.d.ts.map +1 -1
  178. package/dist/scaffold.js +185 -23
  179. package/dist/scaffold.js.map +1 -1
  180. package/dist/wizard.d.ts.map +1 -1
  181. package/dist/wizard.js +4 -1
  182. package/dist/wizard.js.map +1 -1
  183. package/package.json +14 -11
@@ -1,9 +1,23 @@
1
+ import { CogentaError } from '@cogenta/core';
1
2
  import { createContentStore, defineCollection, f, validateCollectionSet, } from '@cogenta/schema';
2
- import { definePageCollection, toBlockZoneEntry, } from './content-pack.js';
3
+ import { avatarArt, coverArt, heroArt, logoArt } from '../demo-art/compositions.js';
4
+ import { definePageCollection, richTextParagraph, SEO_FIELDS, toBlockZoneEntry, } from './content-pack.js';
5
+ import { STARTING_SKINS } from './starting-skins.js';
3
6
  /**
4
- * The `portfolio` blueprint's content model (L9 task 8, batch A): a
5
- * freelance/creative portfolioa `project` collection with its own
6
- * routed page, and two demo pages.
7
+ * The `portfolio` blueprint (L25 Annexe brief, `theme-portfolio` pro pass):
8
+ * a `project` collection cover image, `role`/`year` as plain structured
9
+ * fields (a `collectionList` grid card and a project's own page both read
10
+ * them straight off the entry, `@cogenta/theme-portfolio`'s own doing, not
11
+ * this blueprint's) — plus a genuinely rich single-page home the theme
12
+ * renders as full-bleed project cards, a services grid with real icons, a
13
+ * client logo strip and a two-part client voice (`quote` then
14
+ * `testimonial`).
15
+ *
16
+ * Rewritten in full for the L25 pro pass (its L9/L23 predecessor seeded
17
+ * three home blocks, three projects, no visuals and no menus at all):
18
+ * eight projects with distinct cover compositions, a nine-block home, an
19
+ * about page, a contact page, a legal page, real header/footer/header-
20
+ * action navigation, and `@cogenta/theme-portfolio` as the default theme.
7
21
  */
8
22
  export const project = defineCollection({
9
23
  name: 'project',
@@ -15,6 +29,17 @@ export const project = defineCollection({
15
29
  summary: f.text({ max: 300, multiline: true }),
16
30
  role: f.text({ max: 120 }),
17
31
  year: f.text({ max: 4 }),
32
+ coverImage: f.media({ accept: ['image'] }),
33
+ // Optional body a project page renders after its own header — the
34
+ // seed below places one auto-built "Role / Year" panel here (a flat,
35
+ // muted-background `prose` block, `blocks@2.0`'s `variant`), the
36
+ // in-scope way this theme shows those two raw fields on the project's
37
+ // own page (`renderEntryHeader` draws only title/cover/excerpt from
38
+ // contract D `theme@1.4`'s fixed `PageEntryMeta` shape, which has no
39
+ // room for a collection's own custom fields) — never required, so a
40
+ // project an editor creates by hand without one still renders fine.
41
+ blocks: f.blocks(),
42
+ ...SEO_FIELDS,
18
43
  },
19
44
  indexes: [['slug']],
20
45
  permissions: {
@@ -32,122 +57,481 @@ export const PORTFOLIO_DEMO_PROJECTS = [
32
57
  {
33
58
  title: 'Northwind rebrand',
34
59
  slug: 'northwind-rebrand',
35
- summary: 'A full visual identity refresh for a regional grocery chain, from logotype to packaging.',
60
+ summary: 'A full identity refresh for a 41-store regional grocery chain built by acquisition — one logotype and one packaging system across 340 private-label SKUs, rolled out without closing a single store.',
36
61
  role: 'Art direction',
37
62
  year: '2025',
63
+ coverSeed: 12,
64
+ narrative: [
65
+ 'Northwind runs forty-one stores across the Pacific Northwest, most of them acquired rather than built — which meant four legacy logotypes, three shades of the same green, and a private-label range that looked like six different companies. The brief was blunt: make it read as one grocer without asking anyone to repaint a storefront this year.',
66
+ 'We shipped a logotype family with two registers (a plain wordmark for the storefront, a warmer hand for seasonal signage), a 68-page brand guideline, and new packaging for 340 private-label SKUs, phased across three print runs so nothing hit the shelf half-rebranded. Every store kept trading through every stage — no reprint weekend, no relaunch, no closure.',
67
+ ],
38
68
  },
39
69
  {
40
70
  title: 'Contoso mobile app',
41
71
  slug: 'contoso-mobile-app',
42
- summary: 'Interaction design and a component library for a fintech app used by two million people.',
72
+ summary: 'A ground-up component library and interaction redesign for a fintech app used by two million people, rebuilt around one confirmation pattern for every place money moves.',
43
73
  role: 'Product design',
44
- year: '2024',
74
+ year: '2025',
75
+ coverSeed: 27,
76
+ narrative: [
77
+ "Contoso's banking app had grown a screen at a time for six years, and it showed: four different date pickers, three ways to confirm a transfer, and a design team that had never run a full component audit. Two million people used it anyway, mostly out of habit.",
78
+ 'We rebuilt the library from the transaction list outward — forty-two components, one motion language, one confirmation pattern used everywhere money changes hands — and shipped it behind a flag to five percent of accounts before the full rollout. Support tickets about "where did my transfer go" dropped by a third in the first month.',
79
+ ],
45
80
  },
46
81
  {
47
82
  title: 'Fabrikam annual report',
48
83
  slug: 'fabrikam-annual-report',
49
- summary: 'Editorial design for a 60-page annual report, printed and interactive.',
84
+ summary: 'Editorial design and typesetting for a 60-page annual report print and an interactive web edition from the same grid, at a 4,000-copy print run.',
50
85
  role: 'Editorial design',
86
+ year: '2024',
87
+ coverSeed: 41,
88
+ narrative: [
89
+ "A 60-page annual report is read closely by maybe a hundred people and skimmed by ten thousand more, so it has to work as a spreadsheet and as a piece of writing at the same time. Fabrikam's finance team wrote the copy; we built the grid that lets a dense results table share a page with a proper photo essay without either one losing.",
90
+ "Print run of 4,000, plus an interactive web edition built from the same InDesign grid so the numbers stay legible on a phone. The typesetting keeps a single serif for every numeral in the book — auditors notice when a table changes font mid-report, and we'd rather they didn't have the chance to.",
91
+ ],
92
+ },
93
+ {
94
+ title: 'Litware signage system',
95
+ slug: 'litware-signage-system',
96
+ summary: 'Wayfinding and environmental graphics for a 12-storey, four-wing campus — 214 signs and a lobby directory that updates from a spreadsheet, not a repaint.',
97
+ role: 'Signage & environmental',
98
+ year: '2024',
99
+ coverSeed: 58,
100
+ narrative: [
101
+ "Twelve storeys, four wings, one loading dock nobody could find without asking twice. Litware's campus had been designed by three different architecture firms over eight years, and the wayfinding showed it — or rather, the near-total absence of it.",
102
+ 'We proofed every sign on the actual material before a single one went into production: brushed aluminium in daylight reads nothing like the same file on a monitor. 214 signs, one pictogram set, and a lobby directory that updates from a single spreadsheet rather than a repaint every time a department changes floors.',
103
+ ],
104
+ },
105
+ {
106
+ title: 'Tailspin streaming identity',
107
+ slug: 'tailspin-streaming-identity',
108
+ summary: 'A motion-first brand system for a live-sports streaming launch across five markets — one grid, five market variants, built to survive a jumbotron and a phone screen alike.',
109
+ role: 'Brand & motion',
51
110
  year: '2023',
111
+ coverSeed: 63,
112
+ narrative: [
113
+ "Tailspin launched live sports streaming in five markets on the same night, which meant the identity had to survive a stadium jumbotron, a phone held sideways and a broadcast truck's colour grading — often all three during the same match.",
114
+ 'The system is built motion-first: the mark only fully resolves once it moves, so every static application — kit, ticket, merchandise — is a single frame pulled from the same animation rather than a logo redrawn to match. Five market-specific colour variants share one grid, and the sixth market that signed on two months after launch needed zero redraws to join it.',
115
+ ],
52
116
  },
53
- ];
54
- const BLOCK_VERSION = '1.0.0';
55
- /**
56
- * `home` (hero + a live grid of projects) and `about` (a short prose bio
57
- * plus a static `stats` summary — years of experience and projects
58
- * delivered are page-authored numbers, not something to model as a
59
- * separate queryable collection).
60
- */
61
- export const PORTFOLIO_DEMO_PAGES = [
62
117
  {
63
- title: 'Home',
64
- slug: 'home',
65
- blocks: [
66
- {
67
- _key: 'demo-home-hero',
68
- _type: 'hero',
69
- _version: BLOCK_VERSION,
70
- eyebrow: 'Portfolio',
71
- title: 'Selected work',
72
- subtitle: 'Scaffolded by create-cogenta from the "portfolio" blueprint, with real demo projects already in place.',
73
- },
74
- {
75
- _key: 'demo-home-projects',
76
- _type: 'collectionList',
77
- _version: BLOCK_VERSION,
78
- title: 'Projects',
79
- collection: 'project',
80
- sort: { field: 'createdAt', direction: 'desc' },
81
- limit: 10,
82
- layout: 'grid',
83
- },
84
- {
85
- _key: 'demo-home-services',
86
- _type: 'featureGrid',
87
- _version: BLOCK_VERSION,
88
- title: 'What I take on',
89
- items: [
90
- {
91
- _key: 'demo-service-1',
92
- icon: 'identity',
93
- title: 'Identity',
94
- text: 'Naming, marks and the small rules that keep a brand recognisable once other people apply it.',
95
- },
96
- {
97
- _key: 'demo-service-2',
98
- icon: 'editorial',
99
- title: 'Editorial design',
100
- text: 'Books, reports and long-form sites. Typography first, because that is where the reading happens.',
101
- },
102
- {
103
- _key: 'demo-service-3',
104
- icon: 'signage',
105
- title: 'Signage and print',
106
- text: 'Things that get made once and have to be right. Proofed on the material, not on a screen.',
107
- },
108
- ],
109
- },
118
+ title: 'Adatum publishing house',
119
+ slug: 'adatum-publishing-house',
120
+ summary: 'Cover systems and typesetting for a twelve-title fiction imprint, with one fixed typographic rule and imagery that shifts by genre.',
121
+ role: 'Editorial design',
122
+ year: '2023',
123
+ coverSeed: 79,
124
+ narrative: [
125
+ 'Twelve titles under one imprint, and a house style that had to hold across a literary novel, a thriller and a story collection without any of the three looking like they belonged on the wrong shelf.',
126
+ 'We built a cover system with one fixed typographic rule — the title always sets in the same face, always in the same position on the spine — and let the imagery vary completely by genre: commissioned illustration for the literary list, photography for the thrillers. Booksellers could tell the imprint from three metres away by the second season.',
110
127
  ],
111
128
  },
112
129
  {
113
- title: 'About',
114
- slug: 'about',
115
- blocks: [
116
- {
117
- _key: 'demo-about-prose',
118
- _type: 'prose',
119
- _version: BLOCK_VERSION,
120
- body: [
121
- {
122
- _key: 'demo-about-p1',
123
- _type: 'block',
124
- style: 'normal',
125
- children: [
126
- {
127
- _key: 'demo-about-p1-span',
128
- _type: 'span',
129
- text: 'This is a demo portfolio, scaffolded by create-cogenta from the "portfolio" blueprint. Its projects and this page were seeded by the installer so there is real content to look at from the first run.',
130
- marks: [],
131
- },
132
- ],
133
- markDefs: [],
134
- },
135
- ],
136
- },
137
- {
138
- _key: 'demo-about-stats',
139
- _type: 'stats',
140
- _version: BLOCK_VERSION,
141
- title: 'At a glance',
142
- items: [
143
- { _key: 'demo-stat-1', value: '8', unit: 'years', label: 'in practice' },
144
- { _key: 'demo-stat-2', value: '40+', label: 'projects delivered' },
145
- { _key: 'demo-stat-3', value: '12', label: 'ongoing clients' },
146
- ],
147
- },
130
+ title: 'Wingtip terminal wayfinding',
131
+ slug: 'wingtip-terminal-wayfinding',
132
+ summary: 'Signage, pictograms and a colour-coded wayfinding system for a regional airport terminal, tested against real foot traffic before fabrication.',
133
+ role: 'Signage & environmental',
134
+ year: '2022',
135
+ coverSeed: 88,
136
+ narrative: [
137
+ "A regional airport's wayfinding has about four seconds to work before a traveller who is already late stops trusting it and starts asking a stranger. Wingtip's terminal had grown three extensions since 1994, and its signage had grown with it — six typefaces, and two colour codes that meant different things on different concourses.",
138
+ 'The new system reduces the terminal to three colour-coded zones and one pictogram family, tested against real foot traffic during an actual boarding rush before a single panel was fabricated. Gate-finding complaints at the information desk fell by half in the first reporting quarter.',
139
+ ],
140
+ },
141
+ {
142
+ title: 'Proseware product launch',
143
+ slug: 'proseware-product-launch',
144
+ summary: 'Packaging, retail displays and a launch film for a home-audio product line, shot and shipped in eleven weeks to hit 900 stores on schedule.',
145
+ role: 'Art direction',
146
+ year: '2022',
147
+ coverSeed: 96,
148
+ narrative: [
149
+ "Proseware's new speaker line needed to launch retail-ready in eleven weeks — packaging, in-store display and a two-minute film, all built around a product that was still changing in engineering the week we started shooting.",
150
+ 'We designed the packaging to double as its own retail display, so no separate cardboard unit had to be fabricated and shipped, then shot the film against the same modular set the display uses. Assets were delivered in time for the line to hit 900 stores on the date the client had already announced. Nothing slipped.',
148
151
  ],
149
152
  },
150
153
  ];
154
+ const BLOCK_VERSION = '1.0.0';
155
+ /**
156
+ * A short, structural rich-text list — no HTML in a block (R3), a bullet
157
+ * list is the honest equivalent of a two-row "spec sheet".
158
+ */
159
+ function labelValueRow(key, label, value) {
160
+ return [
161
+ {
162
+ _key: `${key}-p`,
163
+ _type: 'block',
164
+ style: 'normal',
165
+ children: [
166
+ { _key: `${key}-label`, _type: 'span', text: `${label} `, marks: ['strong'] },
167
+ { _key: `${key}-value`, _type: 'span', text: value, marks: [] },
168
+ ],
169
+ markDefs: [],
170
+ },
171
+ ];
172
+ }
173
+ /**
174
+ * The auto-built "Role / Year" panel a project's own page shows beneath
175
+ * `renderEntryHeader` — a flat, muted-background `prose` block
176
+ * (`blocks@2.0`'s `variant.background`, an existing envelope field every
177
+ * block already carries, not a new one) built from the same `role`/`year`
178
+ * text the project's own fields hold. `@cogenta/theme-portfolio`'s
179
+ * `[data-block][data-variant-background="muted"]` rule is what turns this
180
+ * into a flat panel — no block-specific CSS needed for it.
181
+ */
182
+ function roleYearPanelBlock(project) {
183
+ const body = [
184
+ ...labelValueRow(`${project.slug}-role`, 'Role', project.role),
185
+ ...labelValueRow(`${project.slug}-year`, 'Year', project.year),
186
+ ];
187
+ return {
188
+ _key: `${project.slug}-meta`,
189
+ _type: 'prose',
190
+ _version: BLOCK_VERSION,
191
+ body,
192
+ variant: { background: 'muted' },
193
+ };
194
+ }
195
+ /**
196
+ * The project's own narrative (L26 D1) — two real paragraphs of process and
197
+ * outcome, not the one-line `summary` repeated. Placed after
198
+ * {@link roleYearPanelBlock} on the project's own page.
199
+ */
200
+ function projectNarrativeBlock(project) {
201
+ const body = project.narrative.flatMap((paragraph, index) => richTextParagraph(`${project.slug}-narrative-${index}`, paragraph));
202
+ return {
203
+ _key: `${project.slug}-narrative`,
204
+ _type: 'prose',
205
+ _version: BLOCK_VERSION,
206
+ body,
207
+ };
208
+ }
209
+ /**
210
+ * The home page's nine blocks (L25 Annexe brief): hero, "Selected work" (a
211
+ * full-bleed project grid), the numbers, services, clients, a quote, the
212
+ * full project index, a testimonial and the closing call to action. Every
213
+ * block's `_key` doubles as a real in-page anchor
214
+ * (`@cogenta/theme-portfolio`'s `renderPage`, which stamps a real `id` from
215
+ * it) — the header/footer menus below link straight into "Selected work"
216
+ * and "Services" rather than a dead `#fragment`.
217
+ *
218
+ * A function of `media` (`SeedContext.media`), not a static const: the
219
+ * hero/logo media fields need ids `seedDemoMedia` only knows at scaffold
220
+ * time. `buildPortfolioHomeBlocks({})` (no media) still returns a complete,
221
+ * valid block list — `hero`/`logoStrip` simply drop their media-bearing
222
+ * fields rather than emit an invalid block.
223
+ */
224
+ export function buildPortfolioHomeBlocks(media) {
225
+ const logoItems = [1, 2, 3, 4, 5]
226
+ .map((n) => media[`logo-${n}`])
227
+ .filter((id) => id !== undefined)
228
+ .map((id, index) => ({ _key: `home-clients-${index}`, media: id }));
229
+ return [
230
+ {
231
+ _key: 'home-hero',
232
+ _type: 'hero',
233
+ _version: BLOCK_VERSION,
234
+ eyebrow: 'Independent studio, est. 2017',
235
+ title: 'A studio that ships in the open',
236
+ subtitle: 'Brand systems, product design and editorial work for teams who want to see it made, not just delivered.',
237
+ ...(media.hero === undefined ? {} : { media: media.hero }),
238
+ actions: [
239
+ { label: 'Selected work', target: { href: '/#home-work' }, emphasis: 'primary' },
240
+ { label: "Let's talk", target: { href: '/contact' } },
241
+ ],
242
+ },
243
+ {
244
+ _key: 'home-work',
245
+ _type: 'collectionList',
246
+ _version: BLOCK_VERSION,
247
+ title: 'Selected work',
248
+ collection: 'project',
249
+ sort: { field: 'createdAt', direction: 'desc' },
250
+ limit: 6,
251
+ layout: 'grid',
252
+ },
253
+ {
254
+ _key: 'home-stats',
255
+ _type: 'stats',
256
+ _version: BLOCK_VERSION,
257
+ title: 'At a glance',
258
+ items: [
259
+ { _key: 'home-stats-1', value: '8', unit: 'years', label: 'in practice' },
260
+ { _key: 'home-stats-2', value: '40+', label: 'projects delivered' },
261
+ { _key: 'home-stats-3', value: '12', label: 'awards & mentions' },
262
+ ],
263
+ },
264
+ {
265
+ _key: 'home-services',
266
+ _type: 'featureGrid',
267
+ _version: BLOCK_VERSION,
268
+ title: 'Services',
269
+ items: [
270
+ {
271
+ _key: 'home-service-1',
272
+ icon: 'pen',
273
+ title: 'Identity',
274
+ text: 'Naming, marks and the small rules that keep a brand recognisable once other people apply it.',
275
+ },
276
+ {
277
+ _key: 'home-service-2',
278
+ icon: 'book',
279
+ title: 'Editorial design',
280
+ text: 'Books, reports and long-form sites. Typography first, because that is where the reading happens.',
281
+ },
282
+ {
283
+ _key: 'home-service-3',
284
+ icon: 'layers',
285
+ title: 'Signage & environmental',
286
+ text: 'Things that get made once and have to be right. Proofed on the material, not on a screen.',
287
+ },
288
+ ],
289
+ },
290
+ ...(logoItems.length === 0
291
+ ? []
292
+ : [
293
+ {
294
+ _key: 'home-clients',
295
+ _type: 'logoStrip',
296
+ _version: BLOCK_VERSION,
297
+ logos: logoItems,
298
+ caption: 'Selected clients',
299
+ },
300
+ ]),
301
+ {
302
+ _key: 'home-quote',
303
+ _type: 'quote',
304
+ _version: BLOCK_VERSION,
305
+ text: 'They shipped a design system and a working site in the same sprint — and explained every decision along the way.',
306
+ author: 'A. Reviewer',
307
+ role: 'Head of brand, Northwind',
308
+ ...(media['quote-avatar'] === undefined ? {} : { avatar: media['quote-avatar'] }),
309
+ },
310
+ {
311
+ _key: 'home-index',
312
+ _type: 'collectionList',
313
+ _version: BLOCK_VERSION,
314
+ title: 'The full index',
315
+ collection: 'project',
316
+ sort: { field: 'createdAt', direction: 'asc' },
317
+ limit: 8,
318
+ layout: 'list',
319
+ },
320
+ {
321
+ _key: 'home-testimonial',
322
+ _type: 'testimonial',
323
+ _version: BLOCK_VERSION,
324
+ quote: richTextParagraph('home-testimonial-quote', 'A quietly confident studio — the kind of considered work we keep coming back for.'),
325
+ attribution: {
326
+ name: 'M. Bernard',
327
+ role: 'Founder, Adatum',
328
+ ...(media['testimonial-avatar'] === undefined
329
+ ? {}
330
+ : { avatar: media['testimonial-avatar'] }),
331
+ },
332
+ },
333
+ {
334
+ _key: 'home-cta',
335
+ _type: 'cta',
336
+ _version: BLOCK_VERSION,
337
+ title: "Let's talk about your project",
338
+ text: 'One call, no obligation.',
339
+ actions: [{ label: 'Get in touch', target: { href: '/contact' }, emphasis: 'primary' }],
340
+ },
341
+ ];
342
+ }
343
+ export function buildPortfolioDemoPages(media) {
344
+ return [
345
+ { title: 'Home', slug: 'home', blocks: buildPortfolioHomeBlocks(media) },
346
+ {
347
+ title: 'About',
348
+ slug: 'about',
349
+ blocks: [
350
+ {
351
+ _key: 'about-prose',
352
+ _type: 'prose',
353
+ _version: BLOCK_VERSION,
354
+ body: [
355
+ {
356
+ _key: 'about-p1',
357
+ _type: 'block',
358
+ style: 'normal',
359
+ children: [
360
+ {
361
+ _key: 'about-p1-span',
362
+ _type: 'span',
363
+ text: 'We are a small studio working across identity, product and editorial design — usually all three on the same project, since a brand rarely stays inside one discipline for long. Founded in 2017, the team has stayed under ten people on purpose: the person who takes the brief is the person who ships the work, on every project, every time.',
364
+ marks: [],
365
+ },
366
+ ],
367
+ markDefs: [],
368
+ },
369
+ {
370
+ _key: 'about-p2',
371
+ _type: 'block',
372
+ style: 'normal',
373
+ children: [
374
+ {
375
+ _key: 'about-p2-span',
376
+ _type: 'span',
377
+ text: "Most of what we do starts with an audit, not a mood board — cataloguing what a client already has, good and bad, before proposing anything new. It is slower at the start and faster everywhere after, because nobody discovers six months in that the new identity does not fit the loading dock, the packaging line, or an app's existing component library.",
378
+ marks: [],
379
+ },
380
+ ],
381
+ markDefs: [],
382
+ },
383
+ {
384
+ _key: 'about-p3',
385
+ _type: 'block',
386
+ style: 'normal',
387
+ children: [
388
+ {
389
+ _key: 'about-p3-span',
390
+ _type: 'span',
391
+ text: 'We work from a studio in Lisbon with clients across Europe and North America, in the open where the brief allows it — process notes, working files and the odd dead end, published as we go rather than saved for a polished case study months later.',
392
+ marks: [],
393
+ },
394
+ ],
395
+ markDefs: [],
396
+ },
397
+ ],
398
+ },
399
+ ...(media['about-figure'] === undefined
400
+ ? []
401
+ : [
402
+ {
403
+ _key: 'about-figure',
404
+ _type: 'mediaFigure',
405
+ _version: BLOCK_VERSION,
406
+ media: media['about-figure'],
407
+ caption: 'The studio floor, mid-review.',
408
+ ratio: '16:9',
409
+ align: 'center',
410
+ },
411
+ ]),
412
+ {
413
+ _key: 'about-stats',
414
+ _type: 'stats',
415
+ _version: BLOCK_VERSION,
416
+ title: 'At a glance',
417
+ items: [
418
+ { _key: 'about-stat-1', value: '8', unit: 'years', label: 'in practice' },
419
+ { _key: 'about-stat-2', value: '40+', label: 'projects delivered' },
420
+ { _key: 'about-stat-3', value: '12', label: 'ongoing clients' },
421
+ ],
422
+ },
423
+ ],
424
+ },
425
+ {
426
+ title: 'Contact',
427
+ slug: 'contact',
428
+ blocks: [
429
+ {
430
+ _key: 'contact-prose',
431
+ _type: 'prose',
432
+ _version: BLOCK_VERSION,
433
+ body: [
434
+ {
435
+ _key: 'contact-p1',
436
+ _type: 'block',
437
+ style: 'normal',
438
+ children: [
439
+ {
440
+ _key: 'contact-p1-span',
441
+ _type: 'span',
442
+ text: 'Tell us about the project — timeline, budget range and what "done" looks like. We reply within two working days, always with a real person, never a form response.',
443
+ marks: [],
444
+ },
445
+ ],
446
+ markDefs: [],
447
+ },
448
+ {
449
+ _key: 'contact-p2',
450
+ _type: 'block',
451
+ style: 'normal',
452
+ children: [
453
+ {
454
+ _key: 'contact-p2-span',
455
+ _type: 'span',
456
+ text: 'If it looks like a fit, the next step is a short call — thirty minutes, no deck — followed by a written scope within a week. We start most projects with the audit described on the About page, so the first invoice is usually smaller than people expect.',
457
+ marks: [],
458
+ },
459
+ ],
460
+ markDefs: [],
461
+ },
462
+ ],
463
+ },
464
+ {
465
+ _key: 'contact-features',
466
+ _type: 'featureGrid',
467
+ _version: BLOCK_VERSION,
468
+ title: 'What to expect',
469
+ items: [
470
+ {
471
+ _key: 'contact-feature-1',
472
+ icon: 'pen',
473
+ title: 'A real reply',
474
+ text: 'Two working days, from someone who will actually work on the project — not a scheduling bot.',
475
+ },
476
+ {
477
+ _key: 'contact-feature-2',
478
+ icon: 'layers',
479
+ title: 'A written scope',
480
+ text: 'Timeline, deliverables and price in writing before anything is billed.',
481
+ },
482
+ {
483
+ _key: 'contact-feature-3',
484
+ icon: 'book',
485
+ title: 'One point of contact',
486
+ text: 'The person who takes the brief stays on the project through delivery.',
487
+ },
488
+ ],
489
+ },
490
+ {
491
+ _key: 'contact-cta',
492
+ _type: 'cta',
493
+ _version: BLOCK_VERSION,
494
+ title: "Let's talk",
495
+ text: 'One call, no obligation.',
496
+ actions: [
497
+ {
498
+ label: 'Email the studio',
499
+ target: { href: 'mailto:hello@studio.example' },
500
+ emphasis: 'primary',
501
+ },
502
+ ],
503
+ },
504
+ ],
505
+ },
506
+ {
507
+ title: 'Legal',
508
+ slug: 'legal',
509
+ blocks: [
510
+ {
511
+ _key: 'legal-prose',
512
+ _type: 'prose',
513
+ _version: BLOCK_VERSION,
514
+ body: [
515
+ {
516
+ _key: 'legal-p1',
517
+ _type: 'block',
518
+ style: 'normal',
519
+ children: [
520
+ {
521
+ _key: 'legal-p1-span',
522
+ _type: 'span',
523
+ text: 'This is a demo site, scaffolded by create-cogenta from the "portfolio" blueprint. No data collected here is real.',
524
+ marks: [],
525
+ },
526
+ ],
527
+ markDefs: [],
528
+ },
529
+ ],
530
+ },
531
+ ],
532
+ },
533
+ ];
534
+ }
151
535
  export const PORTFOLIO_RECOMMENDED_AGENTS = [
152
536
  {
153
537
  name: 'performanceAgent',
@@ -160,14 +544,125 @@ export const PORTFOLIO_RECOMMENDED_AGENTS = [
160
544
  reason: 'Audits project pages so each one is findable on its own, not only from the grid.',
161
545
  },
162
546
  ];
547
+ /**
548
+ * `portfolio`'s own starting skin (`starting-skins.js`) — asserted present
549
+ * with a real check, not a `!`, since `STARTING_SKINS` is keyed by
550
+ * blueprint id and TypeScript cannot see that this particular key is
551
+ * always populated.
552
+ */
553
+ function portfolioPalette() {
554
+ const skin = STARTING_SKINS.portfolio;
555
+ if (skin === undefined) {
556
+ throw new CogentaError({
557
+ code: 'BLUEPRINT_REGISTRY_CORRUPT',
558
+ message: 'STARTING_SKINS.portfolio is missing.',
559
+ hint: 'The "portfolio" entry must stay declared in starting-skins.ts for this blueprint to render its demo art.',
560
+ });
561
+ }
562
+ return skin.color;
563
+ }
564
+ /**
565
+ * Procedural demo visuals (L25 D1/D5): an "editorial mark on dark" hero
566
+ * backdrop (the strongest of the flat families against this theme's own
567
+ * dark-mode identity), one distinct cover per project (`coverSeed`, so the
568
+ * eight read as genuinely different compositions rather than a palette
569
+ * repeated eight times), five client marks, and two avatars — one for the
570
+ * home `quote`, one for the `testimonial` — kept as two separate specs
571
+ * rather than one shared avatar, since a real site would rarely show the
572
+ * same face twice on the same page.
573
+ */
574
+ /** Bundled photography (`assets/photos/portfolio/`), keyed by the same slug `PORTFOLIO_DEMO_PROJECTS` uses. */
575
+ const PORTFOLIO_PROJECT_PHOTOS = {
576
+ 'northwind-rebrand': 'portfolio/northwind-rebrand.jpg',
577
+ 'contoso-mobile-app': 'portfolio/contoso-mobile-app.jpg',
578
+ 'fabrikam-annual-report': 'portfolio/fabrikam-annual-report.jpg',
579
+ 'litware-signage-system': 'portfolio/litware-signage.jpg',
580
+ 'tailspin-streaming-identity': 'portfolio/tailspin-streaming.jpg',
581
+ 'adatum-publishing-house': 'portfolio/adatum-publishing.jpg',
582
+ 'wingtip-terminal-wayfinding': 'portfolio/wingtip-wayfinding.jpg',
583
+ 'proseware-product-launch': 'portfolio/proseware-launch.jpg',
584
+ };
585
+ export const PORTFOLIO_MEDIA_SPECS = [
586
+ {
587
+ name: 'hero',
588
+ spec: heroArt(portfolioPalette(), 'ink', 4),
589
+ alt: 'A design studio workspace',
590
+ photo: 'portfolio/hero.jpg',
591
+ },
592
+ // The About page's own `mediaFigure` (L26 D2) — the same bundled workspace
593
+ // photograph as the home hero, a second real photo of the studio floor
594
+ // rather than a literal reuse of the hero's own media id, since a real
595
+ // studio site frequently shows its own workspace more than once.
596
+ {
597
+ name: 'about-figure',
598
+ spec: heroArt(portfolioPalette(), 'ink', 17),
599
+ alt: 'The studio floor, mid-review',
600
+ photo: 'portfolio/hero.jpg',
601
+ },
602
+ ...PORTFOLIO_DEMO_PROJECTS.map((demo) => {
603
+ const photo = PORTFOLIO_PROJECT_PHOTOS[demo.slug];
604
+ return {
605
+ name: `project-${demo.slug}`,
606
+ spec: coverArt(portfolioPalette(), demo.coverSeed),
607
+ alt: `${demo.title}, cover composition`,
608
+ ...(photo === undefined ? {} : { photo }),
609
+ };
610
+ }),
611
+ ...[1, 2, 3, 4, 5].map((n) => ({
612
+ name: `logo-${n}`,
613
+ spec: logoArt(n * 13),
614
+ alt: 'An abstract client mark',
615
+ })),
616
+ {
617
+ name: 'quote-avatar',
618
+ spec: avatarArt(portfolioPalette(), 201),
619
+ alt: 'An abstract avatar composition, standing in for a client photo',
620
+ },
621
+ {
622
+ name: 'testimonial-avatar',
623
+ spec: avatarArt(portfolioPalette(), 202),
624
+ alt: 'An abstract avatar composition, standing in for a client photo',
625
+ },
626
+ ];
627
+ /** Header/footer navigation and the header "Let's talk" button (L25 D4) — "Work"/"Services" are real, working in-page anchors into the home page's own blocks; "About"/"Contact"/"Legal" are real pages. */
628
+ export const PORTFOLIO_MENUS = {
629
+ header: [
630
+ { label: 'Work', url: '/#home-work' },
631
+ { label: 'Services', url: '/#home-services' },
632
+ { label: 'About', url: '/about' },
633
+ { label: 'Contact', url: '/contact' },
634
+ ],
635
+ footer: [
636
+ { label: 'Work', url: '/#home-work' },
637
+ { label: 'About', url: '/about' },
638
+ { label: 'Contact', url: '/contact' },
639
+ { label: 'Legal', url: '/legal' },
640
+ ],
641
+ headerAction: { label: "Let's talk", url: '/contact' },
642
+ };
643
+ export const PORTFOLIO_SITE_SETTINGS = {
644
+ 'general.tagline': 'Brand, product and editorial design, made in the open.',
645
+ 'general.socialLinks': [
646
+ { label: 'Instagram', url: 'https://instagram.com/example' },
647
+ { label: 'Dribbble', url: 'https://dribbble.com/example' },
648
+ { label: 'LinkedIn', url: 'https://linkedin.com/company/example' },
649
+ ],
650
+ 'general.footerNote': 'Studio, est. 2017 · Replies within two working days.',
651
+ };
163
652
  /**
164
653
  * Inserts the `portfolio` blueprint's demo content through the real
165
- * `ContentStore` — never mocked (house rule).
654
+ * `ContentStore` — never mocked (house rule). Each project's `coverImage`
655
+ * and the home page's hero/logos/quote/testimonial media come from
656
+ * `ctx.media` (`seedDemoMedia`/`PORTFOLIO_MEDIA_SPECS`) — absent (a caller
657
+ * that never ran `seedDemoMedia`) simply leaves those fields unset, since
658
+ * none of them is `required` on its collection/block.
166
659
  */
167
- async function seedPortfolioDemoContent(db, defaultLocale, adminId) {
660
+ async function seedPortfolioDemoContent(ctx) {
661
+ const { db, defaultLocale, adminId, media } = ctx;
168
662
  const projectStore = createContentStore({ db, collection: project, defaultLocale });
169
663
  const pageStore = createContentStore({ db, collection: page, defaultLocale });
170
664
  for (const demo of PORTFOLIO_DEMO_PROJECTS) {
665
+ const coverImage = media[`project-${demo.slug}`];
171
666
  await projectStore.create({
172
667
  status: 'published',
173
668
  createdBy: adminId,
@@ -177,10 +672,14 @@ async function seedPortfolioDemoContent(db, defaultLocale, adminId) {
177
672
  summary: demo.summary,
178
673
  role: demo.role,
179
674
  year: demo.year,
675
+ ...(coverImage === undefined ? {} : { coverImage }),
676
+ },
677
+ blocks: {
678
+ blocks: [roleYearPanelBlock(demo), projectNarrativeBlock(demo)].map(toBlockZoneEntry),
180
679
  },
181
680
  });
182
681
  }
183
- for (const demo of PORTFOLIO_DEMO_PAGES) {
682
+ for (const demo of buildPortfolioDemoPages(media)) {
184
683
  await pageStore.create({
185
684
  status: 'published',
186
685
  createdBy: adminId,
@@ -193,5 +692,9 @@ export const portfolioContentPack = {
193
692
  collections: PORTFOLIO_COLLECTIONS,
194
693
  recommendedAgents: PORTFOLIO_RECOMMENDED_AGENTS,
195
694
  seedDemoContent: seedPortfolioDemoContent,
695
+ defaultTheme: '@cogenta/theme-portfolio',
696
+ menus: PORTFOLIO_MENUS,
697
+ siteSettings: PORTFOLIO_SITE_SETTINGS,
698
+ mediaSpecs: PORTFOLIO_MEDIA_SPECS,
196
699
  };
197
700
  //# sourceMappingURL=portfolio.js.map