@nexpress/theme-portfolio 0.1.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.
- package/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/components/error.d.ts +26 -0
- package/dist/components/error.js +122 -0
- package/dist/components/error.js.map +1 -0
- package/dist/components/members-error.d.ts +29 -0
- package/dist/components/members-error.js +121 -0
- package/dist/components/members-error.js.map +1 -0
- package/dist/components/mobile-nav.d.ts +14 -0
- package/dist/components/mobile-nav.js +57 -0
- package/dist/components/mobile-nav.js.map +1 -0
- package/dist/index.d.ts +167 -0
- package/dist/index.js +1396 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1396 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { defineTheme } from "@nexpress/theme";
|
|
3
|
+
|
|
4
|
+
// src/settings-helpers.ts
|
|
5
|
+
import { getCachedThemeSettings } from "@nexpress/next";
|
|
6
|
+
|
|
7
|
+
// src/settings.ts
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
var portfolioSettingsSchema = z.object({
|
|
10
|
+
// Layout
|
|
11
|
+
gridColumns: z.number().int().min(1).max(6).default(3).describe("Number of columns in the project archive grid (1\u20136)."),
|
|
12
|
+
cardAspect: z.enum(["square", "portrait", "landscape", "golden"]).default("square").describe(
|
|
13
|
+
"Aspect ratio of project cards: square (1:1), portrait (3:4), landscape (4:3), or golden (1:1.618)."
|
|
14
|
+
),
|
|
15
|
+
hoverStyle: z.enum(["fade", "scale", "slide", "lift"]).default("fade").describe(
|
|
16
|
+
"Hover effect on project cards. fade: caption fades in. scale: image zooms 1.05x. slide: caption slides up. lift: card lifts with shadow."
|
|
17
|
+
),
|
|
18
|
+
galleryGutter: z.number().int().min(0).max(64).default(16).describe("Gap between project cards in pixels (0\u201364)."),
|
|
19
|
+
// Project meta
|
|
20
|
+
showProjectMeta: z.boolean().default(true).describe("Show role / year / client meta strip on project detail pages."),
|
|
21
|
+
showProjectTags: z.boolean().default(false).describe("Show tag chips below project titles on the index grid."),
|
|
22
|
+
// Brand
|
|
23
|
+
accentColor: z.string().regex(/^#[0-9a-f]{6}$/i).optional().describe(
|
|
24
|
+
"Optional accent color override (hex). Used for hover states and the masthead underline."
|
|
25
|
+
),
|
|
26
|
+
studioName: z.string().default("Studio").describe("Studio / personal name shown in the masthead and footer."),
|
|
27
|
+
aboutCopy: z.string().default("").meta({ widget: "textarea", rows: 4 }).describe(
|
|
28
|
+
"Optional short bio for the studio. Renders as a multi-line textarea in admin (4 rows) and as a small paragraph above the footer contact line on the public site."
|
|
29
|
+
),
|
|
30
|
+
// Footer
|
|
31
|
+
showFooterCredit: z.boolean().default(true).describe(
|
|
32
|
+
"Show 'Built with NexPress' credit in the footer. Some studios prefer an unbranded footer."
|
|
33
|
+
),
|
|
34
|
+
copyrightYear: z.number().int().min(2e3).max(2100).optional().describe(
|
|
35
|
+
"Optional fixed copyright year. Defaults to the current year when omitted."
|
|
36
|
+
),
|
|
37
|
+
// Client logos
|
|
38
|
+
clientLogos: z.array(
|
|
39
|
+
z.object({
|
|
40
|
+
name: z.string().describe("Client name (alt text + caption)"),
|
|
41
|
+
logoUrl: z.string().url().describe("Logo image URL"),
|
|
42
|
+
link: z.string().url().optional().describe("Optional case-study link")
|
|
43
|
+
})
|
|
44
|
+
).default([]).describe(
|
|
45
|
+
"Client logos rendered in the homepage 'Selected clients' strip. Edit per project."
|
|
46
|
+
)
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// src/settings-helpers.ts
|
|
50
|
+
async function resolvePortfolioSettings() {
|
|
51
|
+
const raw = await getCachedThemeSettings("portfolio");
|
|
52
|
+
const parsed = portfolioSettingsSchema.safeParse(raw);
|
|
53
|
+
if (parsed.success) return parsed.data;
|
|
54
|
+
return portfolioSettingsSchema.parse({});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/blocks.tsx
|
|
58
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
59
|
+
function CaseStudyHero(props) {
|
|
60
|
+
const { title, subtitle, client, year, role, imageUrl } = props;
|
|
61
|
+
return /* @__PURE__ */ jsx(
|
|
62
|
+
"section",
|
|
63
|
+
{
|
|
64
|
+
className: "np-portfolio-case-study-hero",
|
|
65
|
+
style: {
|
|
66
|
+
position: "relative",
|
|
67
|
+
margin: "0 0 2rem",
|
|
68
|
+
padding: 0,
|
|
69
|
+
minHeight: imageUrl ? "60vh" : "auto",
|
|
70
|
+
backgroundImage: imageUrl ? `url(${imageUrl})` : void 0,
|
|
71
|
+
backgroundSize: "cover",
|
|
72
|
+
backgroundPosition: "center",
|
|
73
|
+
color: imageUrl ? "white" : "inherit",
|
|
74
|
+
display: "flex",
|
|
75
|
+
flexDirection: "column",
|
|
76
|
+
justifyContent: "flex-end"
|
|
77
|
+
},
|
|
78
|
+
children: /* @__PURE__ */ jsxs(
|
|
79
|
+
"div",
|
|
80
|
+
{
|
|
81
|
+
style: {
|
|
82
|
+
padding: "3rem 1.5rem 2rem",
|
|
83
|
+
background: imageUrl ? "linear-gradient(180deg, transparent, rgba(0,0,0,0.65))" : void 0
|
|
84
|
+
},
|
|
85
|
+
children: [
|
|
86
|
+
/* @__PURE__ */ jsx(
|
|
87
|
+
"h1",
|
|
88
|
+
{
|
|
89
|
+
style: {
|
|
90
|
+
fontFamily: "var(--np-font-heading)",
|
|
91
|
+
fontSize: "clamp(2rem, 5vw, 3.75rem)",
|
|
92
|
+
fontWeight: 600,
|
|
93
|
+
margin: 0,
|
|
94
|
+
letterSpacing: "-0.02em"
|
|
95
|
+
},
|
|
96
|
+
children: title
|
|
97
|
+
}
|
|
98
|
+
),
|
|
99
|
+
subtitle ? /* @__PURE__ */ jsx(
|
|
100
|
+
"p",
|
|
101
|
+
{
|
|
102
|
+
style: {
|
|
103
|
+
margin: "0.75rem 0 0",
|
|
104
|
+
fontSize: "1.125rem",
|
|
105
|
+
maxWidth: "60ch",
|
|
106
|
+
opacity: 0.9
|
|
107
|
+
},
|
|
108
|
+
children: subtitle
|
|
109
|
+
}
|
|
110
|
+
) : null,
|
|
111
|
+
client || year || role ? /* @__PURE__ */ jsxs(
|
|
112
|
+
"div",
|
|
113
|
+
{
|
|
114
|
+
style: {
|
|
115
|
+
display: "flex",
|
|
116
|
+
flexWrap: "wrap",
|
|
117
|
+
gap: "2rem",
|
|
118
|
+
marginTop: "1.5rem",
|
|
119
|
+
fontSize: "0.875rem",
|
|
120
|
+
opacity: 0.8
|
|
121
|
+
},
|
|
122
|
+
children: [
|
|
123
|
+
client ? /* @__PURE__ */ jsxs("div", { children: [
|
|
124
|
+
/* @__PURE__ */ jsx("span", { style: { display: "block", opacity: 0.6, fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.08em" }, children: "Client" }),
|
|
125
|
+
client
|
|
126
|
+
] }) : null,
|
|
127
|
+
year ? /* @__PURE__ */ jsxs("div", { children: [
|
|
128
|
+
/* @__PURE__ */ jsx("span", { style: { display: "block", opacity: 0.6, fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.08em" }, children: "Year" }),
|
|
129
|
+
year
|
|
130
|
+
] }) : null,
|
|
131
|
+
role ? /* @__PURE__ */ jsxs("div", { children: [
|
|
132
|
+
/* @__PURE__ */ jsx("span", { style: { display: "block", opacity: 0.6, fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.08em" }, children: "Role" }),
|
|
133
|
+
role
|
|
134
|
+
] }) : null
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
) : null
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
function ImageGrid(props) {
|
|
145
|
+
const { columns, items } = props;
|
|
146
|
+
const cols = typeof columns === "number" && columns > 0 ? columns : 2;
|
|
147
|
+
return /* @__PURE__ */ jsx(
|
|
148
|
+
"section",
|
|
149
|
+
{
|
|
150
|
+
className: "np-portfolio-image-grid",
|
|
151
|
+
style: {
|
|
152
|
+
margin: "2rem 0",
|
|
153
|
+
display: "grid",
|
|
154
|
+
gap: "1rem",
|
|
155
|
+
gridTemplateColumns: `repeat(${cols}, 1fr)`
|
|
156
|
+
},
|
|
157
|
+
children: items.map((item, i) => /* @__PURE__ */ jsxs("figure", { style: { margin: 0 }, children: [
|
|
158
|
+
/* @__PURE__ */ jsx(
|
|
159
|
+
"img",
|
|
160
|
+
{
|
|
161
|
+
src: item.url,
|
|
162
|
+
alt: item.alt ?? "",
|
|
163
|
+
style: {
|
|
164
|
+
display: "block",
|
|
165
|
+
width: "100%",
|
|
166
|
+
height: "auto",
|
|
167
|
+
borderRadius: "0.25rem"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
),
|
|
171
|
+
item.caption ? /* @__PURE__ */ jsx(
|
|
172
|
+
"figcaption",
|
|
173
|
+
{
|
|
174
|
+
style: {
|
|
175
|
+
fontSize: "0.8125rem",
|
|
176
|
+
color: "var(--np-color-muted-foreground)",
|
|
177
|
+
marginTop: "0.5rem"
|
|
178
|
+
},
|
|
179
|
+
children: item.caption
|
|
180
|
+
}
|
|
181
|
+
) : null
|
|
182
|
+
] }, i))
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
var portfolioBlocks = [
|
|
187
|
+
{
|
|
188
|
+
type: "portfolio.case-study-hero",
|
|
189
|
+
label: "Case study hero",
|
|
190
|
+
iconKind: "lucide",
|
|
191
|
+
icon: "image",
|
|
192
|
+
keywords: ["hero", "case-study", "portfolio", "project"],
|
|
193
|
+
defaultProps: {
|
|
194
|
+
title: "Project name",
|
|
195
|
+
subtitle: "One-sentence project summary.",
|
|
196
|
+
client: "Client name",
|
|
197
|
+
year: "2026",
|
|
198
|
+
role: "Design + Engineering",
|
|
199
|
+
imageUrl: ""
|
|
200
|
+
},
|
|
201
|
+
propsSchema: [
|
|
202
|
+
{ name: "title", label: "Project title", type: "text" },
|
|
203
|
+
{ name: "subtitle", label: "Subtitle", type: "textarea" },
|
|
204
|
+
{ name: "client", label: "Client", type: "text" },
|
|
205
|
+
{ name: "year", label: "Year", type: "text" },
|
|
206
|
+
{ name: "role", label: "Role", type: "text" },
|
|
207
|
+
{ name: "imageUrl", label: "Hero image URL", type: "url" }
|
|
208
|
+
],
|
|
209
|
+
render: (props) => /* @__PURE__ */ jsx(CaseStudyHero, { ...props })
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
type: "portfolio.image-grid",
|
|
213
|
+
label: "Image grid",
|
|
214
|
+
iconKind: "lucide",
|
|
215
|
+
icon: "grid-3x3",
|
|
216
|
+
keywords: ["images", "gallery", "grid", "portfolio"],
|
|
217
|
+
defaultProps: {
|
|
218
|
+
columns: 2,
|
|
219
|
+
items: [
|
|
220
|
+
{ url: "https://placehold.co/800x600", alt: "", caption: "" },
|
|
221
|
+
{ url: "https://placehold.co/800x600", alt: "", caption: "" }
|
|
222
|
+
]
|
|
223
|
+
},
|
|
224
|
+
propsSchema: [
|
|
225
|
+
{ name: "columns", label: "Columns", type: "number" },
|
|
226
|
+
// `items` edited as JSON in v0.2; richer per-item editor
|
|
227
|
+
// (drag-to-reorder, add/remove) tracked as F.5.1 polish.
|
|
228
|
+
{ name: "items", label: "Items (JSON)", type: "textarea" }
|
|
229
|
+
],
|
|
230
|
+
render: (props) => /* @__PURE__ */ jsx(ImageGrid, { ...props })
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
type: "portfolio.client-logos",
|
|
234
|
+
label: "Client logos strip",
|
|
235
|
+
iconKind: "lucide",
|
|
236
|
+
icon: "users",
|
|
237
|
+
keywords: ["clients", "logos", "portfolio", "selected work"],
|
|
238
|
+
defaultProps: {
|
|
239
|
+
heading: "Selected clients"
|
|
240
|
+
},
|
|
241
|
+
propsSchema: [
|
|
242
|
+
{ name: "heading", label: "Section heading", type: "text" }
|
|
243
|
+
],
|
|
244
|
+
// `ClientLogosStrip` is itself an async server component —
|
|
245
|
+
// it reads `settings.clientLogos` so the operator manages
|
|
246
|
+
// logos in admin's Theme settings panel (a single canonical
|
|
247
|
+
// source) rather than re-typing per block instance. The render
|
|
248
|
+
// arrow itself is sync (just constructs the element); React
|
|
249
|
+
// resolves the inner async at render time.
|
|
250
|
+
render: (props) => /* @__PURE__ */ jsx(ClientLogosStrip, { ...props })
|
|
251
|
+
}
|
|
252
|
+
];
|
|
253
|
+
async function ClientLogosStrip({
|
|
254
|
+
heading
|
|
255
|
+
}) {
|
|
256
|
+
const settings = await resolvePortfolioSettings();
|
|
257
|
+
const logos = settings.clientLogos;
|
|
258
|
+
if (logos.length === 0) return null;
|
|
259
|
+
return /* @__PURE__ */ jsxs(
|
|
260
|
+
"section",
|
|
261
|
+
{
|
|
262
|
+
className: "np-portfolio-client-logos",
|
|
263
|
+
style: {
|
|
264
|
+
margin: "3rem 0"
|
|
265
|
+
},
|
|
266
|
+
children: [
|
|
267
|
+
heading ? /* @__PURE__ */ jsx(
|
|
268
|
+
"h2",
|
|
269
|
+
{
|
|
270
|
+
style: {
|
|
271
|
+
margin: "0 0 1.5rem",
|
|
272
|
+
fontSize: "0.8125rem",
|
|
273
|
+
textTransform: "uppercase",
|
|
274
|
+
letterSpacing: "0.1em",
|
|
275
|
+
color: "var(--np-color-muted-foreground)",
|
|
276
|
+
textAlign: "center"
|
|
277
|
+
},
|
|
278
|
+
children: heading
|
|
279
|
+
}
|
|
280
|
+
) : null,
|
|
281
|
+
/* @__PURE__ */ jsx(
|
|
282
|
+
"div",
|
|
283
|
+
{
|
|
284
|
+
style: {
|
|
285
|
+
display: "grid",
|
|
286
|
+
gridTemplateColumns: `repeat(auto-fit, minmax(140px, 1fr))`,
|
|
287
|
+
gap: "2rem",
|
|
288
|
+
alignItems: "center",
|
|
289
|
+
justifyItems: "center"
|
|
290
|
+
},
|
|
291
|
+
children: logos.map((logo, i) => {
|
|
292
|
+
const img = /* @__PURE__ */ jsx(
|
|
293
|
+
"img",
|
|
294
|
+
{
|
|
295
|
+
src: logo.logoUrl,
|
|
296
|
+
alt: logo.name,
|
|
297
|
+
style: {
|
|
298
|
+
maxHeight: "48px",
|
|
299
|
+
maxWidth: "100%",
|
|
300
|
+
opacity: 0.7,
|
|
301
|
+
transition: "opacity 0.2s ease"
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
);
|
|
305
|
+
return /* @__PURE__ */ jsx("div", { children: logo.link ? /* @__PURE__ */ jsx("a", { href: logo.link, target: "_blank", rel: "noreferrer", children: img }) : img }, `portfolio-logo-${i.toString()}`);
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/index.ts
|
|
315
|
+
import { PortfolioMobileNav as PortfolioMobileNav2 } from "./components/mobile-nav.js";
|
|
316
|
+
|
|
317
|
+
// src/components/project-card.tsx
|
|
318
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
319
|
+
function coverUrl(value) {
|
|
320
|
+
if (!value) return null;
|
|
321
|
+
if (typeof value === "string") return value;
|
|
322
|
+
return value.url ?? null;
|
|
323
|
+
}
|
|
324
|
+
function coverAlt(value, fallback) {
|
|
325
|
+
if (value && typeof value === "object" && value.alt) return value.alt;
|
|
326
|
+
return fallback;
|
|
327
|
+
}
|
|
328
|
+
function projectHref(doc) {
|
|
329
|
+
if (doc.slug) {
|
|
330
|
+
return doc.slug.startsWith("/") ? doc.slug : `/work/${doc.slug}`;
|
|
331
|
+
}
|
|
332
|
+
return "#";
|
|
333
|
+
}
|
|
334
|
+
async function PortfolioProjectCard({
|
|
335
|
+
doc
|
|
336
|
+
}) {
|
|
337
|
+
const settings = await resolvePortfolioSettings();
|
|
338
|
+
const href = projectHref(doc);
|
|
339
|
+
const cover = coverUrl(doc.cover);
|
|
340
|
+
const title = doc.title ?? "Untitled";
|
|
341
|
+
return /* @__PURE__ */ jsx2("a", { href, className: "np-portfolio-project-card", children: /* @__PURE__ */ jsxs2("figure", { className: "np-portfolio-project-cover", children: [
|
|
342
|
+
cover ? /* @__PURE__ */ jsx2("img", { src: cover, alt: coverAlt(doc.cover, title), loading: "lazy" }) : /* @__PURE__ */ jsx2("span", { className: "np-portfolio-project-placeholder", "aria-hidden": "true" }),
|
|
343
|
+
/* @__PURE__ */ jsxs2("figcaption", { className: "np-portfolio-project-caption", children: [
|
|
344
|
+
/* @__PURE__ */ jsx2("span", { className: "np-portfolio-project-title", children: title }),
|
|
345
|
+
settings.showProjectTags && doc.category ? /* @__PURE__ */ jsx2("span", { className: "np-portfolio-project-category", children: doc.category }) : null
|
|
346
|
+
] })
|
|
347
|
+
] }) });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/footer.tsx
|
|
351
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
352
|
+
async function PortfolioFooter() {
|
|
353
|
+
const settings = await resolvePortfolioSettings();
|
|
354
|
+
const year = settings.copyrightYear ?? (/* @__PURE__ */ new Date()).getFullYear();
|
|
355
|
+
const email = process.env.NP_SOCIAL_EMAIL;
|
|
356
|
+
const social = [
|
|
357
|
+
{ href: process.env.NP_SOCIAL_GITHUB, label: "GitHub" },
|
|
358
|
+
{ href: process.env.NP_SOCIAL_TWITTER, label: "Twitter" },
|
|
359
|
+
{ href: process.env.NP_SOCIAL_LINKEDIN, label: "LinkedIn" },
|
|
360
|
+
{ href: process.env.NP_SOCIAL_MASTODON, label: "Mastodon" },
|
|
361
|
+
{ href: process.env.NP_SOCIAL_DRIBBBLE, label: "Dribbble" },
|
|
362
|
+
{ href: process.env.NP_SOCIAL_INSTAGRAM, label: "Instagram" }
|
|
363
|
+
].filter((s) => Boolean(s.href));
|
|
364
|
+
const studio = settings.studioName;
|
|
365
|
+
return /* @__PURE__ */ jsx3("footer", { className: "np-site-footer np-portfolio-footer", children: /* @__PURE__ */ jsxs3("div", { className: "np-portfolio-footer-inner", children: [
|
|
366
|
+
settings.aboutCopy.length > 0 ? /* @__PURE__ */ jsx3(
|
|
367
|
+
"p",
|
|
368
|
+
{
|
|
369
|
+
className: "np-portfolio-footer-bio",
|
|
370
|
+
style: {
|
|
371
|
+
maxWidth: "60ch",
|
|
372
|
+
fontSize: "0.875rem",
|
|
373
|
+
color: "var(--np-color-muted-foreground)",
|
|
374
|
+
margin: "0 0 1.25rem"
|
|
375
|
+
},
|
|
376
|
+
children: settings.aboutCopy
|
|
377
|
+
}
|
|
378
|
+
) : null,
|
|
379
|
+
/* @__PURE__ */ jsx3("div", { className: "np-portfolio-footer-contact", children: email ? /* @__PURE__ */ jsx3(
|
|
380
|
+
"a",
|
|
381
|
+
{
|
|
382
|
+
href: email.startsWith("mailto:") ? email : `mailto:${email}`,
|
|
383
|
+
className: "np-portfolio-footer-email",
|
|
384
|
+
children: email.replace(/^mailto:/, "")
|
|
385
|
+
}
|
|
386
|
+
) : /* @__PURE__ */ jsx3("span", { className: "np-portfolio-footer-email", children: "Available for select work" }) }),
|
|
387
|
+
social.length > 0 ? /* @__PURE__ */ jsx3("ul", { className: "np-portfolio-footer-social", children: social.map((s) => /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsx3("a", { href: s.href, target: "_blank", rel: "noopener noreferrer", children: s.label }) }, s.href)) }) : null,
|
|
388
|
+
/* @__PURE__ */ jsxs3("p", { className: "np-portfolio-footer-mark", children: [
|
|
389
|
+
"\xA9 ",
|
|
390
|
+
year.toString(),
|
|
391
|
+
" \xB7 ",
|
|
392
|
+
studio,
|
|
393
|
+
settings.showFooterCredit ? " \xB7 Built with NexPress" : ""
|
|
394
|
+
] })
|
|
395
|
+
] }) });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/header.tsx
|
|
399
|
+
import { getCachedNavigation } from "@nexpress/next";
|
|
400
|
+
import { PortfolioMobileNav } from "./components/mobile-nav.js";
|
|
401
|
+
import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
402
|
+
async function PortfolioHeader() {
|
|
403
|
+
const items = await getCachedNavigation("header");
|
|
404
|
+
const settings = await resolvePortfolioSettings();
|
|
405
|
+
return /* @__PURE__ */ jsxs4("header", { className: "np-site-header np-portfolio-header", children: [
|
|
406
|
+
/* @__PURE__ */ jsx4("a", { href: "/", className: "np-portfolio-logo", children: settings.studioName }),
|
|
407
|
+
items.length > 0 ? /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
408
|
+
/* @__PURE__ */ jsx4("nav", { "aria-label": "Main", className: "np-portfolio-nav-desktop", children: /* @__PURE__ */ jsx4("ul", { className: "np-portfolio-nav", children: items.map((item, index) => /* @__PURE__ */ jsxs4("li", { className: "np-portfolio-nav-item", children: [
|
|
409
|
+
/* @__PURE__ */ jsx4("a", { href: item.url, children: item.label }),
|
|
410
|
+
item.children && item.children.length > 0 ? /* @__PURE__ */ jsx4("ul", { className: "np-portfolio-subnav", children: item.children.map((child, childIndex) => /* @__PURE__ */ jsx4("li", { children: /* @__PURE__ */ jsx4("a", { href: child.url, children: child.label }) }, `portfolio-nav-${index.toString()}-${childIndex.toString()}`)) }) : null
|
|
411
|
+
] }, `portfolio-nav-${index.toString()}`)) }) }),
|
|
412
|
+
/* @__PURE__ */ jsx4(PortfolioMobileNav, { items })
|
|
413
|
+
] }) : null
|
|
414
|
+
] });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/members-not-found.tsx
|
|
418
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
419
|
+
function PortfolioMembersNotFound() {
|
|
420
|
+
return /* @__PURE__ */ jsxs5(
|
|
421
|
+
"div",
|
|
422
|
+
{
|
|
423
|
+
className: "np-portfolio-members-not-found",
|
|
424
|
+
style: {
|
|
425
|
+
maxWidth: 480,
|
|
426
|
+
margin: "6rem auto",
|
|
427
|
+
padding: "0 1.5rem",
|
|
428
|
+
textAlign: "center"
|
|
429
|
+
},
|
|
430
|
+
children: [
|
|
431
|
+
/* @__PURE__ */ jsx5(
|
|
432
|
+
"p",
|
|
433
|
+
{
|
|
434
|
+
style: {
|
|
435
|
+
margin: 0,
|
|
436
|
+
fontSize: "0.75rem",
|
|
437
|
+
textTransform: "uppercase",
|
|
438
|
+
letterSpacing: "0.18em",
|
|
439
|
+
color: "var(--np-color-muted-foreground)",
|
|
440
|
+
fontFamily: "var(--np-font-body)"
|
|
441
|
+
},
|
|
442
|
+
children: "Account"
|
|
443
|
+
}
|
|
444
|
+
),
|
|
445
|
+
/* @__PURE__ */ jsx5(
|
|
446
|
+
"h1",
|
|
447
|
+
{
|
|
448
|
+
style: {
|
|
449
|
+
margin: "1rem 0 0",
|
|
450
|
+
fontSize: "clamp(1.75rem, 4vw, 2.5rem)",
|
|
451
|
+
fontFamily: "var(--np-font-heading)",
|
|
452
|
+
fontWeight: 500,
|
|
453
|
+
letterSpacing: "-0.02em"
|
|
454
|
+
},
|
|
455
|
+
children: "Link no longer valid."
|
|
456
|
+
}
|
|
457
|
+
),
|
|
458
|
+
/* @__PURE__ */ jsx5(
|
|
459
|
+
"p",
|
|
460
|
+
{
|
|
461
|
+
style: {
|
|
462
|
+
margin: "1.25rem 0 0",
|
|
463
|
+
color: "var(--np-color-muted-foreground)",
|
|
464
|
+
fontSize: "0.9375rem",
|
|
465
|
+
lineHeight: 1.6
|
|
466
|
+
},
|
|
467
|
+
children: "Verification and password-reset links are single-use and short-lived. Request a fresh one from the sign-in page."
|
|
468
|
+
}
|
|
469
|
+
),
|
|
470
|
+
/* @__PURE__ */ jsx5(
|
|
471
|
+
"a",
|
|
472
|
+
{
|
|
473
|
+
href: "/members/login",
|
|
474
|
+
style: {
|
|
475
|
+
display: "inline-block",
|
|
476
|
+
marginTop: "2rem",
|
|
477
|
+
padding: "0.625rem 1.5rem",
|
|
478
|
+
borderRadius: "0.25rem",
|
|
479
|
+
background: "var(--np-color-primary)",
|
|
480
|
+
color: "var(--np-color-primary-foreground)",
|
|
481
|
+
textDecoration: "none",
|
|
482
|
+
fontSize: "0.875rem",
|
|
483
|
+
fontWeight: 500,
|
|
484
|
+
letterSpacing: "0.02em"
|
|
485
|
+
},
|
|
486
|
+
children: "Go to sign in"
|
|
487
|
+
}
|
|
488
|
+
)
|
|
489
|
+
]
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/members-shell.tsx
|
|
495
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
496
|
+
var ASPECT_VALUES = {
|
|
497
|
+
square: "1 / 1",
|
|
498
|
+
portrait: "3 / 4",
|
|
499
|
+
landscape: "4 / 3",
|
|
500
|
+
golden: "1 / 1.618"
|
|
501
|
+
};
|
|
502
|
+
async function PortfolioMembersShell({
|
|
503
|
+
children
|
|
504
|
+
}) {
|
|
505
|
+
const settings = await resolvePortfolioSettings();
|
|
506
|
+
const aspect = ASPECT_VALUES[settings.cardAspect];
|
|
507
|
+
const styleVars = {
|
|
508
|
+
"--np-portfolio-card-aspect": aspect
|
|
509
|
+
};
|
|
510
|
+
if (settings.accentColor) {
|
|
511
|
+
styleVars["--np-color-primary"] = settings.accentColor;
|
|
512
|
+
}
|
|
513
|
+
return /* @__PURE__ */ jsxs6(
|
|
514
|
+
"div",
|
|
515
|
+
{
|
|
516
|
+
className: "np-portfolio",
|
|
517
|
+
"data-hover-style": settings.hoverStyle,
|
|
518
|
+
style: styleVars,
|
|
519
|
+
children: [
|
|
520
|
+
/* @__PURE__ */ jsx6(PortfolioHeader, {}),
|
|
521
|
+
/* @__PURE__ */ jsx6("div", { className: "np-portfolio-members", children: /* @__PURE__ */ jsx6("div", { className: "np-portfolio-members-column", children }) }),
|
|
522
|
+
/* @__PURE__ */ jsx6(PortfolioFooter, {})
|
|
523
|
+
]
|
|
524
|
+
}
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/not-found.tsx
|
|
529
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
530
|
+
function PortfolioNotFound() {
|
|
531
|
+
return /* @__PURE__ */ jsxs7(
|
|
532
|
+
"div",
|
|
533
|
+
{
|
|
534
|
+
className: "np-portfolio-not-found",
|
|
535
|
+
style: {
|
|
536
|
+
minHeight: "60vh",
|
|
537
|
+
maxWidth: 480,
|
|
538
|
+
margin: "0 auto",
|
|
539
|
+
padding: "6rem 1.5rem",
|
|
540
|
+
display: "flex",
|
|
541
|
+
flexDirection: "column",
|
|
542
|
+
justifyContent: "center",
|
|
543
|
+
alignItems: "center",
|
|
544
|
+
textAlign: "center"
|
|
545
|
+
},
|
|
546
|
+
children: [
|
|
547
|
+
/* @__PURE__ */ jsx7(
|
|
548
|
+
"p",
|
|
549
|
+
{
|
|
550
|
+
style: {
|
|
551
|
+
margin: 0,
|
|
552
|
+
fontSize: "0.75rem",
|
|
553
|
+
textTransform: "uppercase",
|
|
554
|
+
letterSpacing: "0.15em",
|
|
555
|
+
color: "var(--np-color-muted-foreground)"
|
|
556
|
+
},
|
|
557
|
+
children: "404"
|
|
558
|
+
}
|
|
559
|
+
),
|
|
560
|
+
/* @__PURE__ */ jsx7(
|
|
561
|
+
"h1",
|
|
562
|
+
{
|
|
563
|
+
style: {
|
|
564
|
+
margin: "1rem 0 0",
|
|
565
|
+
fontSize: "clamp(1.75rem, 4vw, 2.5rem)",
|
|
566
|
+
fontWeight: 500,
|
|
567
|
+
letterSpacing: "-0.02em"
|
|
568
|
+
},
|
|
569
|
+
children: "Project not found"
|
|
570
|
+
}
|
|
571
|
+
),
|
|
572
|
+
/* @__PURE__ */ jsx7(
|
|
573
|
+
"p",
|
|
574
|
+
{
|
|
575
|
+
style: {
|
|
576
|
+
margin: "1rem 0 2rem",
|
|
577
|
+
color: "var(--np-color-muted-foreground)",
|
|
578
|
+
fontSize: "0.9375rem"
|
|
579
|
+
},
|
|
580
|
+
children: "The page you're looking for moved or doesn't exist."
|
|
581
|
+
}
|
|
582
|
+
),
|
|
583
|
+
/* @__PURE__ */ jsx7(
|
|
584
|
+
"a",
|
|
585
|
+
{
|
|
586
|
+
href: "/",
|
|
587
|
+
style: {
|
|
588
|
+
display: "inline-block",
|
|
589
|
+
padding: "0.5rem 1.5rem",
|
|
590
|
+
border: "1px solid var(--np-color-border)",
|
|
591
|
+
borderRadius: "0.25rem",
|
|
592
|
+
color: "var(--np-color-foreground)",
|
|
593
|
+
textDecoration: "none",
|
|
594
|
+
fontSize: "0.875rem"
|
|
595
|
+
},
|
|
596
|
+
children: "See selected work \u2192"
|
|
597
|
+
}
|
|
598
|
+
)
|
|
599
|
+
]
|
|
600
|
+
}
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/routes/project-detail.tsx
|
|
605
|
+
import { findDocuments } from "@nexpress/core";
|
|
606
|
+
import { notFound } from "next/navigation";
|
|
607
|
+
|
|
608
|
+
// src/templates/project-detail.tsx
|
|
609
|
+
import { renderBlocks } from "@nexpress/blocks";
|
|
610
|
+
import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
611
|
+
function coverUrl2(value) {
|
|
612
|
+
if (!value) return null;
|
|
613
|
+
if (typeof value === "string") return value;
|
|
614
|
+
return value.url ?? null;
|
|
615
|
+
}
|
|
616
|
+
function coverAlt2(value, fallback) {
|
|
617
|
+
if (value && typeof value === "object" && value.alt) return value.alt;
|
|
618
|
+
return fallback;
|
|
619
|
+
}
|
|
620
|
+
async function ProjectDetailTemplate({
|
|
621
|
+
doc,
|
|
622
|
+
blockCtx
|
|
623
|
+
}) {
|
|
624
|
+
const project = doc;
|
|
625
|
+
const settings = await resolvePortfolioSettings();
|
|
626
|
+
const title = project.title ?? "Untitled";
|
|
627
|
+
const cover = coverUrl2(project.cover);
|
|
628
|
+
return /* @__PURE__ */ jsxs8("article", { className: "np-portfolio-project-detail", children: [
|
|
629
|
+
cover ? /* @__PURE__ */ jsx8("figure", { className: "np-portfolio-project-hero", children: /* @__PURE__ */ jsx8("img", { src: cover, alt: coverAlt2(project.cover, title) }) }) : null,
|
|
630
|
+
/* @__PURE__ */ jsxs8("header", { className: "np-portfolio-project-header", children: [
|
|
631
|
+
/* @__PURE__ */ jsx8("h1", { children: title }),
|
|
632
|
+
project.excerpt ? /* @__PURE__ */ jsx8("p", { className: "np-portfolio-project-excerpt", children: project.excerpt }) : null,
|
|
633
|
+
settings.showProjectMeta && (project.role || project.year || project.client) ? /* @__PURE__ */ jsxs8("dl", { className: "np-portfolio-project-meta", children: [
|
|
634
|
+
project.client ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
|
|
635
|
+
/* @__PURE__ */ jsx8("dt", { children: "Client" }),
|
|
636
|
+
/* @__PURE__ */ jsx8("dd", { children: project.client })
|
|
637
|
+
] }) : null,
|
|
638
|
+
project.role ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
|
|
639
|
+
/* @__PURE__ */ jsx8("dt", { children: "Role" }),
|
|
640
|
+
/* @__PURE__ */ jsx8("dd", { children: project.role })
|
|
641
|
+
] }) : null,
|
|
642
|
+
project.year ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
|
|
643
|
+
/* @__PURE__ */ jsx8("dt", { children: "Year" }),
|
|
644
|
+
/* @__PURE__ */ jsx8("dd", { children: String(project.year) })
|
|
645
|
+
] }) : null
|
|
646
|
+
] }) : null
|
|
647
|
+
] }),
|
|
648
|
+
project.blocks && project.blocks.length > 0 ? /* @__PURE__ */ jsx8("div", { className: "np-portfolio-project-body", children: renderBlocks(project.blocks, { ctx: blockCtx }) }) : null
|
|
649
|
+
] });
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/routes/project-detail.tsx
|
|
653
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
654
|
+
async function PortfolioProjectDetailRoute({
|
|
655
|
+
params,
|
|
656
|
+
blockCtx
|
|
657
|
+
}) {
|
|
658
|
+
const slug = typeof params.slug === "string" ? params.slug : "";
|
|
659
|
+
if (!slug) notFound();
|
|
660
|
+
const result = await findDocuments("posts", {
|
|
661
|
+
where: { slug, status: "published" },
|
|
662
|
+
limit: 1
|
|
663
|
+
});
|
|
664
|
+
const doc = result.docs[0];
|
|
665
|
+
if (!doc) notFound();
|
|
666
|
+
const templateProps = {
|
|
667
|
+
doc,
|
|
668
|
+
blockCtx
|
|
669
|
+
};
|
|
670
|
+
return /* @__PURE__ */ jsx9(ProjectDetailTemplate, { ...templateProps });
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/shell.tsx
|
|
674
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
675
|
+
var ASPECT_VALUES2 = {
|
|
676
|
+
square: "1 / 1",
|
|
677
|
+
portrait: "3 / 4",
|
|
678
|
+
landscape: "4 / 3",
|
|
679
|
+
golden: "1 / 1.618"
|
|
680
|
+
};
|
|
681
|
+
async function PortfolioShell({ children }) {
|
|
682
|
+
const settings = await resolvePortfolioSettings();
|
|
683
|
+
const aspect = ASPECT_VALUES2[settings.cardAspect];
|
|
684
|
+
const styleVars = {
|
|
685
|
+
"--np-portfolio-card-aspect": aspect
|
|
686
|
+
};
|
|
687
|
+
if (settings.accentColor) {
|
|
688
|
+
styleVars["--np-color-primary"] = settings.accentColor;
|
|
689
|
+
}
|
|
690
|
+
return /* @__PURE__ */ jsx10(
|
|
691
|
+
"div",
|
|
692
|
+
{
|
|
693
|
+
className: "np-portfolio",
|
|
694
|
+
"data-hover-style": settings.hoverStyle,
|
|
695
|
+
style: styleVars,
|
|
696
|
+
children
|
|
697
|
+
}
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/styles.ts
|
|
702
|
+
var portfolioCss = `
|
|
703
|
+
.np-portfolio {
|
|
704
|
+
background: var(--np-color-background);
|
|
705
|
+
color: var(--np-color-foreground);
|
|
706
|
+
min-height: 100vh;
|
|
707
|
+
font-family: var(--np-font-body, "Inter", system-ui, sans-serif);
|
|
708
|
+
}
|
|
709
|
+
.np-portfolio a { color: inherit; }
|
|
710
|
+
.np-portfolio ::selection {
|
|
711
|
+
background: var(--np-color-primary);
|
|
712
|
+
color: var(--np-color-primary-foreground);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/* ----------------------------------------------------------------
|
|
716
|
+
* Header
|
|
717
|
+
* --------------------------------------------------------------- */
|
|
718
|
+
.np-portfolio-header {
|
|
719
|
+
background: color-mix(in oklab, var(--np-color-background) 85%, transparent);
|
|
720
|
+
backdrop-filter: blur(8px);
|
|
721
|
+
-webkit-backdrop-filter: blur(8px);
|
|
722
|
+
border-bottom: 1px solid color-mix(in oklab, var(--np-color-foreground) 8%, transparent);
|
|
723
|
+
display: flex;
|
|
724
|
+
align-items: center;
|
|
725
|
+
justify-content: space-between;
|
|
726
|
+
padding: 1rem 2rem;
|
|
727
|
+
position: sticky;
|
|
728
|
+
top: 0;
|
|
729
|
+
z-index: 30;
|
|
730
|
+
gap: 1rem;
|
|
731
|
+
}
|
|
732
|
+
.np-portfolio-logo {
|
|
733
|
+
font-weight: 600;
|
|
734
|
+
letter-spacing: 0.02em;
|
|
735
|
+
text-decoration: none;
|
|
736
|
+
font-size: 0.95rem;
|
|
737
|
+
}
|
|
738
|
+
.np-portfolio-nav {
|
|
739
|
+
list-style: none;
|
|
740
|
+
margin: 0;
|
|
741
|
+
padding: 0;
|
|
742
|
+
display: flex;
|
|
743
|
+
gap: 1.5rem;
|
|
744
|
+
font-size: 0.875rem;
|
|
745
|
+
}
|
|
746
|
+
.np-portfolio-nav a {
|
|
747
|
+
text-decoration: none;
|
|
748
|
+
opacity: 0.75;
|
|
749
|
+
transition: opacity 0.15s ease;
|
|
750
|
+
}
|
|
751
|
+
.np-portfolio-nav a:hover { opacity: 1; }
|
|
752
|
+
.np-portfolio-nav-item {
|
|
753
|
+
position: relative;
|
|
754
|
+
}
|
|
755
|
+
.np-portfolio-subnav {
|
|
756
|
+
position: absolute;
|
|
757
|
+
top: 100%;
|
|
758
|
+
left: 0;
|
|
759
|
+
display: none;
|
|
760
|
+
min-width: 11rem;
|
|
761
|
+
padding: 0.5rem 0;
|
|
762
|
+
margin: 0;
|
|
763
|
+
list-style: none;
|
|
764
|
+
background: var(--np-color-card, #fff);
|
|
765
|
+
border: 1px solid var(--np-color-border, #e5e7eb);
|
|
766
|
+
border-radius: var(--np-radius-md, 0.5rem);
|
|
767
|
+
box-shadow: 0 4px 16px -8px rgba(0, 0, 0, 0.08);
|
|
768
|
+
z-index: 10;
|
|
769
|
+
}
|
|
770
|
+
.np-portfolio-nav-item:hover > .np-portfolio-subnav,
|
|
771
|
+
.np-portfolio-nav-item:focus-within > .np-portfolio-subnav {
|
|
772
|
+
display: block;
|
|
773
|
+
}
|
|
774
|
+
.np-portfolio-subnav a {
|
|
775
|
+
display: block;
|
|
776
|
+
padding: 0.4rem 1rem;
|
|
777
|
+
font-size: 0.875rem;
|
|
778
|
+
}
|
|
779
|
+
.np-portfolio-mobile-subnav {
|
|
780
|
+
list-style: none;
|
|
781
|
+
margin: 0;
|
|
782
|
+
padding-left: 1.25rem;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/* Mobile drawer */
|
|
786
|
+
.np-portfolio-nav-toggle {
|
|
787
|
+
display: none;
|
|
788
|
+
align-items: center;
|
|
789
|
+
justify-content: center;
|
|
790
|
+
padding: 0.4rem 0.85rem;
|
|
791
|
+
border: 1px solid color-mix(in oklab, var(--np-color-foreground) 20%, transparent);
|
|
792
|
+
border-radius: 999px;
|
|
793
|
+
background: transparent;
|
|
794
|
+
color: inherit;
|
|
795
|
+
font: inherit;
|
|
796
|
+
font-size: 0.75rem;
|
|
797
|
+
letter-spacing: 0.06em;
|
|
798
|
+
cursor: pointer;
|
|
799
|
+
}
|
|
800
|
+
.np-portfolio-nav-toggle:hover {
|
|
801
|
+
border-color: color-mix(in oklab, var(--np-color-foreground) 50%, transparent);
|
|
802
|
+
}
|
|
803
|
+
.np-portfolio-nav-drawer {
|
|
804
|
+
position: fixed;
|
|
805
|
+
inset: 0;
|
|
806
|
+
background: color-mix(in oklab, var(--np-color-background) 95%, transparent);
|
|
807
|
+
backdrop-filter: blur(12px);
|
|
808
|
+
-webkit-backdrop-filter: blur(12px);
|
|
809
|
+
z-index: 50;
|
|
810
|
+
display: flex;
|
|
811
|
+
align-items: center;
|
|
812
|
+
justify-content: center;
|
|
813
|
+
opacity: 0;
|
|
814
|
+
visibility: hidden;
|
|
815
|
+
transition: opacity 0.25s ease, visibility 0.25s ease;
|
|
816
|
+
}
|
|
817
|
+
.np-portfolio-nav-drawer[data-open="true"] {
|
|
818
|
+
opacity: 1;
|
|
819
|
+
visibility: visible;
|
|
820
|
+
}
|
|
821
|
+
.np-portfolio-nav-drawer-list {
|
|
822
|
+
list-style: none;
|
|
823
|
+
margin: 0;
|
|
824
|
+
padding: 0;
|
|
825
|
+
display: flex;
|
|
826
|
+
flex-direction: column;
|
|
827
|
+
gap: 1.5rem;
|
|
828
|
+
text-align: center;
|
|
829
|
+
font-size: clamp(1.4rem, 3vw, 2rem);
|
|
830
|
+
font-weight: 500;
|
|
831
|
+
letter-spacing: -0.01em;
|
|
832
|
+
}
|
|
833
|
+
.np-portfolio-nav-drawer-list a {
|
|
834
|
+
color: inherit;
|
|
835
|
+
text-decoration: none;
|
|
836
|
+
opacity: 0.85;
|
|
837
|
+
transition: opacity 0.15s ease;
|
|
838
|
+
}
|
|
839
|
+
.np-portfolio-nav-drawer-list a:hover { opacity: 1; }
|
|
840
|
+
|
|
841
|
+
@media (max-width: 720px) {
|
|
842
|
+
.np-portfolio-nav-desktop { display: none; }
|
|
843
|
+
.np-portfolio-nav-toggle { display: inline-flex; }
|
|
844
|
+
}
|
|
845
|
+
@media (min-width: 721px) {
|
|
846
|
+
.np-portfolio-nav-drawer { display: none; }
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/* ----------------------------------------------------------------
|
|
850
|
+
* Footer
|
|
851
|
+
* --------------------------------------------------------------- */
|
|
852
|
+
.np-portfolio-footer {
|
|
853
|
+
border-top: 1px solid color-mix(in oklab, var(--np-color-foreground) 8%, transparent);
|
|
854
|
+
margin-top: 6rem;
|
|
855
|
+
background: transparent;
|
|
856
|
+
text-align: center;
|
|
857
|
+
}
|
|
858
|
+
.np-portfolio-footer-inner {
|
|
859
|
+
max-width: 960px;
|
|
860
|
+
margin: 0 auto;
|
|
861
|
+
padding: 2.5rem 1.5rem;
|
|
862
|
+
display: flex;
|
|
863
|
+
flex-direction: column;
|
|
864
|
+
gap: 1rem;
|
|
865
|
+
align-items: center;
|
|
866
|
+
}
|
|
867
|
+
.np-portfolio-footer-contact { font-size: 1.05rem; }
|
|
868
|
+
.np-portfolio-footer-email {
|
|
869
|
+
text-decoration: none;
|
|
870
|
+
letter-spacing: 0.02em;
|
|
871
|
+
border-bottom: 1px solid color-mix(in oklab, var(--np-color-foreground) 40%, transparent);
|
|
872
|
+
padding-bottom: 0.15rem;
|
|
873
|
+
}
|
|
874
|
+
.np-portfolio-footer-email:hover {
|
|
875
|
+
border-bottom-color: color-mix(in oklab, var(--np-color-foreground) 85%, transparent);
|
|
876
|
+
}
|
|
877
|
+
.np-portfolio-footer-social {
|
|
878
|
+
list-style: none;
|
|
879
|
+
margin: 0;
|
|
880
|
+
padding: 0;
|
|
881
|
+
display: flex;
|
|
882
|
+
flex-wrap: wrap;
|
|
883
|
+
justify-content: center;
|
|
884
|
+
gap: 1.5rem;
|
|
885
|
+
font-size: 0.85rem;
|
|
886
|
+
text-transform: uppercase;
|
|
887
|
+
letter-spacing: 0.16em;
|
|
888
|
+
}
|
|
889
|
+
.np-portfolio-footer-social a {
|
|
890
|
+
text-decoration: none;
|
|
891
|
+
opacity: 0.65;
|
|
892
|
+
transition: opacity 0.15s ease;
|
|
893
|
+
}
|
|
894
|
+
.np-portfolio-footer-social a:hover { opacity: 1; }
|
|
895
|
+
.np-portfolio-footer-mark {
|
|
896
|
+
margin: 0;
|
|
897
|
+
font-size: 0.78rem;
|
|
898
|
+
opacity: 0.5;
|
|
899
|
+
letter-spacing: 0.06em;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/* ----------------------------------------------------------------
|
|
903
|
+
* Page templates
|
|
904
|
+
* --------------------------------------------------------------- */
|
|
905
|
+
.np-portfolio-page {
|
|
906
|
+
max-width: 720px;
|
|
907
|
+
margin: 0 auto;
|
|
908
|
+
padding: 4rem 1.5rem;
|
|
909
|
+
line-height: 1.7;
|
|
910
|
+
}
|
|
911
|
+
.np-portfolio-page h1,
|
|
912
|
+
.np-portfolio-page h2,
|
|
913
|
+
.np-portfolio-page h3 { letter-spacing: -0.01em; }
|
|
914
|
+
|
|
915
|
+
.np-portfolio-gallery {
|
|
916
|
+
max-width: 1280px;
|
|
917
|
+
margin: 0 auto;
|
|
918
|
+
padding: 3rem 1.5rem 4rem;
|
|
919
|
+
}
|
|
920
|
+
.np-portfolio-gallery > h1 {
|
|
921
|
+
text-align: center;
|
|
922
|
+
font-size: clamp(2rem, 4vw, 3.5rem);
|
|
923
|
+
margin: 0 0 2.5rem;
|
|
924
|
+
letter-spacing: -0.02em;
|
|
925
|
+
}
|
|
926
|
+
.np-portfolio-gallery-grid {
|
|
927
|
+
display: grid;
|
|
928
|
+
grid-template-columns: 1fr;
|
|
929
|
+
gap: 1.5rem;
|
|
930
|
+
}
|
|
931
|
+
@media (min-width: 720px) {
|
|
932
|
+
.np-portfolio-gallery-grid { grid-template-columns: 1fr 1fr; }
|
|
933
|
+
}
|
|
934
|
+
.np-portfolio-gallery-grid img {
|
|
935
|
+
width: 100%;
|
|
936
|
+
height: auto;
|
|
937
|
+
display: block;
|
|
938
|
+
border-radius: 8px;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/* ----------------------------------------------------------------
|
|
942
|
+
* Project index (grid of cards)
|
|
943
|
+
* --------------------------------------------------------------- */
|
|
944
|
+
.np-portfolio-index {
|
|
945
|
+
max-width: 1320px;
|
|
946
|
+
margin: 0 auto;
|
|
947
|
+
padding: 3.5rem 1.5rem 4rem;
|
|
948
|
+
}
|
|
949
|
+
.np-portfolio-index-header {
|
|
950
|
+
text-align: center;
|
|
951
|
+
margin-bottom: 3rem;
|
|
952
|
+
}
|
|
953
|
+
.np-portfolio-index-header h1 {
|
|
954
|
+
font-size: clamp(2.25rem, 4vw, 3rem);
|
|
955
|
+
letter-spacing: -0.02em;
|
|
956
|
+
margin: 0 0 0.65rem;
|
|
957
|
+
font-weight: 600;
|
|
958
|
+
}
|
|
959
|
+
.np-portfolio-index-header p {
|
|
960
|
+
margin: 0 auto;
|
|
961
|
+
max-width: 38rem;
|
|
962
|
+
opacity: 0.75;
|
|
963
|
+
line-height: 1.6;
|
|
964
|
+
}
|
|
965
|
+
.np-portfolio-index-empty {
|
|
966
|
+
text-align: center;
|
|
967
|
+
padding: 4rem 1.5rem;
|
|
968
|
+
opacity: 0.6;
|
|
969
|
+
}
|
|
970
|
+
.np-portfolio-index-grid {
|
|
971
|
+
/* Phase F.9.1-A \u2014 operator's settings.gridColumns
|
|
972
|
+
* sets --np-portfolio-grid-cols on this element via the
|
|
973
|
+
* project-index template. Mobile clamps to 1 column
|
|
974
|
+
* regardless; tablet caps at min(2, --np-portfolio-grid-cols);
|
|
975
|
+
* desktop honors the operator's choice up to 6. Operator
|
|
976
|
+
* stays in control without breaking responsive design.
|
|
977
|
+
*/
|
|
978
|
+
--np-portfolio-grid-cols: 3;
|
|
979
|
+
--np-portfolio-grid-gutter: 1.5rem;
|
|
980
|
+
display: grid;
|
|
981
|
+
grid-template-columns: 1fr;
|
|
982
|
+
gap: var(--np-portfolio-grid-gutter);
|
|
983
|
+
}
|
|
984
|
+
@media (min-width: 640px) {
|
|
985
|
+
.np-portfolio-index-grid {
|
|
986
|
+
grid-template-columns: repeat(min(2, var(--np-portfolio-grid-cols, 3)), 1fr);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
@media (min-width: 1024px) {
|
|
990
|
+
.np-portfolio-index-grid {
|
|
991
|
+
grid-template-columns: repeat(var(--np-portfolio-grid-cols, 3), 1fr);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/* ----------------------------------------------------------------
|
|
996
|
+
* Project card
|
|
997
|
+
* --------------------------------------------------------------- */
|
|
998
|
+
.np-portfolio-project-card {
|
|
999
|
+
display: block;
|
|
1000
|
+
text-decoration: none;
|
|
1001
|
+
color: inherit;
|
|
1002
|
+
position: relative;
|
|
1003
|
+
overflow: hidden;
|
|
1004
|
+
border-radius: 4px;
|
|
1005
|
+
background: var(--np-color-card);
|
|
1006
|
+
}
|
|
1007
|
+
.np-portfolio-project-cover {
|
|
1008
|
+
margin: 0;
|
|
1009
|
+
position: relative;
|
|
1010
|
+
/* Phase F.9.1-B \u2014 operator-tunable aspect via shell-set
|
|
1011
|
+
* --np-portfolio-card-aspect (square / portrait / landscape /
|
|
1012
|
+
* golden). Falls back to 4/3 when the variable isn't set. */
|
|
1013
|
+
aspect-ratio: var(--np-portfolio-card-aspect, 4 / 3);
|
|
1014
|
+
overflow: hidden;
|
|
1015
|
+
}
|
|
1016
|
+
.np-portfolio-project-cover img {
|
|
1017
|
+
width: 100%;
|
|
1018
|
+
height: 100%;
|
|
1019
|
+
object-fit: cover;
|
|
1020
|
+
display: block;
|
|
1021
|
+
transition: transform 0.5s ease, filter 0.4s ease;
|
|
1022
|
+
}
|
|
1023
|
+
/* Phase F.9.1-B \u2014 hoverStyle variants. Selected via the shell's
|
|
1024
|
+
* data-hover-style attribute. Default ("fade") = caption fades in
|
|
1025
|
+
* + image scale; the others swap the image effect.
|
|
1026
|
+
* - scale: only the image zooms (caption stays subtle)
|
|
1027
|
+
* - slide: image stays put; caption slides up from below
|
|
1028
|
+
* - lift: card lifts with shadow; image static
|
|
1029
|
+
*/
|
|
1030
|
+
.np-portfolio[data-hover-style="fade"] .np-portfolio-project-card:hover .np-portfolio-project-cover img,
|
|
1031
|
+
.np-portfolio[data-hover-style="scale"] .np-portfolio-project-card:hover .np-portfolio-project-cover img {
|
|
1032
|
+
transform: scale(1.04);
|
|
1033
|
+
}
|
|
1034
|
+
.np-portfolio[data-hover-style="lift"] .np-portfolio-project-card {
|
|
1035
|
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
1036
|
+
}
|
|
1037
|
+
.np-portfolio[data-hover-style="lift"] .np-portfolio-project-card:hover {
|
|
1038
|
+
transform: translateY(-4px);
|
|
1039
|
+
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.35);
|
|
1040
|
+
}
|
|
1041
|
+
.np-portfolio[data-hover-style="slide"] .np-portfolio-project-caption {
|
|
1042
|
+
/* Slide-up reveal \u2014 caption starts further below + opacity 0 */
|
|
1043
|
+
transform: translateY(24px);
|
|
1044
|
+
}
|
|
1045
|
+
.np-portfolio-project-placeholder {
|
|
1046
|
+
display: block;
|
|
1047
|
+
width: 100%;
|
|
1048
|
+
height: 100%;
|
|
1049
|
+
background: linear-gradient(
|
|
1050
|
+
135deg,
|
|
1051
|
+
var(--np-color-muted) 0%,
|
|
1052
|
+
var(--np-color-accent) 100%
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
.np-portfolio-project-caption {
|
|
1056
|
+
position: absolute;
|
|
1057
|
+
inset: auto 0 0 0;
|
|
1058
|
+
padding: 1rem 1.25rem;
|
|
1059
|
+
background: linear-gradient(to top, rgba(0, 0, 0, 0.85) 0%, transparent 100%);
|
|
1060
|
+
display: flex;
|
|
1061
|
+
flex-direction: column;
|
|
1062
|
+
gap: 0.2rem;
|
|
1063
|
+
opacity: 0;
|
|
1064
|
+
transform: translateY(8px);
|
|
1065
|
+
transition: opacity 0.25s ease, transform 0.25s ease;
|
|
1066
|
+
}
|
|
1067
|
+
.np-portfolio-project-card:hover .np-portfolio-project-caption {
|
|
1068
|
+
opacity: 1;
|
|
1069
|
+
transform: translateY(0);
|
|
1070
|
+
}
|
|
1071
|
+
.np-portfolio-project-title {
|
|
1072
|
+
font-weight: 600;
|
|
1073
|
+
letter-spacing: 0.01em;
|
|
1074
|
+
font-size: 1rem;
|
|
1075
|
+
}
|
|
1076
|
+
.np-portfolio-project-category {
|
|
1077
|
+
font-size: 0.72rem;
|
|
1078
|
+
text-transform: uppercase;
|
|
1079
|
+
letter-spacing: 0.16em;
|
|
1080
|
+
opacity: 0.8;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/* ----------------------------------------------------------------
|
|
1084
|
+
* Project detail
|
|
1085
|
+
* --------------------------------------------------------------- */
|
|
1086
|
+
.np-portfolio-project-detail {
|
|
1087
|
+
margin: 0;
|
|
1088
|
+
padding: 0 0 4rem;
|
|
1089
|
+
}
|
|
1090
|
+
.np-portfolio-project-hero {
|
|
1091
|
+
margin: 0;
|
|
1092
|
+
width: 100%;
|
|
1093
|
+
aspect-ratio: 21 / 9;
|
|
1094
|
+
overflow: hidden;
|
|
1095
|
+
background: var(--np-color-card);
|
|
1096
|
+
}
|
|
1097
|
+
.np-portfolio-project-hero img {
|
|
1098
|
+
width: 100%;
|
|
1099
|
+
height: 100%;
|
|
1100
|
+
object-fit: cover;
|
|
1101
|
+
display: block;
|
|
1102
|
+
}
|
|
1103
|
+
.np-portfolio-project-header {
|
|
1104
|
+
max-width: 760px;
|
|
1105
|
+
margin: 0 auto;
|
|
1106
|
+
padding: 3rem 1.5rem 2rem;
|
|
1107
|
+
text-align: center;
|
|
1108
|
+
}
|
|
1109
|
+
.np-portfolio-project-header h1 {
|
|
1110
|
+
font-size: clamp(2rem, 4vw, 3rem);
|
|
1111
|
+
letter-spacing: -0.02em;
|
|
1112
|
+
margin: 0 0 0.85rem;
|
|
1113
|
+
font-weight: 600;
|
|
1114
|
+
}
|
|
1115
|
+
.np-portfolio-project-excerpt {
|
|
1116
|
+
margin: 0 auto;
|
|
1117
|
+
max-width: 38rem;
|
|
1118
|
+
opacity: 0.75;
|
|
1119
|
+
font-size: 1.075rem;
|
|
1120
|
+
line-height: 1.55;
|
|
1121
|
+
}
|
|
1122
|
+
.np-portfolio-project-meta {
|
|
1123
|
+
margin: 2rem auto 0;
|
|
1124
|
+
display: grid;
|
|
1125
|
+
grid-template-columns: repeat(auto-fit, minmax(8rem, max-content));
|
|
1126
|
+
justify-content: center;
|
|
1127
|
+
gap: 0 2rem;
|
|
1128
|
+
font-size: 0.85rem;
|
|
1129
|
+
text-align: start;
|
|
1130
|
+
}
|
|
1131
|
+
.np-portfolio-project-meta dt {
|
|
1132
|
+
text-transform: uppercase;
|
|
1133
|
+
letter-spacing: 0.16em;
|
|
1134
|
+
font-size: 0.7rem;
|
|
1135
|
+
opacity: 0.55;
|
|
1136
|
+
margin-bottom: 0.2rem;
|
|
1137
|
+
}
|
|
1138
|
+
.np-portfolio-project-meta dd {
|
|
1139
|
+
margin: 0 0 0.75rem;
|
|
1140
|
+
font-weight: 500;
|
|
1141
|
+
}
|
|
1142
|
+
.np-portfolio-project-body {
|
|
1143
|
+
max-width: 720px;
|
|
1144
|
+
margin: 0 auto;
|
|
1145
|
+
padding: 0 1.5rem;
|
|
1146
|
+
font-size: 1.05rem;
|
|
1147
|
+
line-height: 1.7;
|
|
1148
|
+
opacity: 0.92;
|
|
1149
|
+
}
|
|
1150
|
+
.np-portfolio-project-body img {
|
|
1151
|
+
max-width: 100%;
|
|
1152
|
+
height: auto;
|
|
1153
|
+
border-radius: 6px;
|
|
1154
|
+
margin: 1.5rem 0;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/* Re-cast the .np-page baseline so links pick up the theme's
|
|
1158
|
+
primary token. Dark theme: primary is light-on-dark, so the
|
|
1159
|
+
link reads correctly. */
|
|
1160
|
+
.np-portfolio .np-page a {
|
|
1161
|
+
color: var(--np-color-primary);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/* M.* member surface \u2014 narrow auth-form column under the
|
|
1165
|
+
masthead. The portfolio's public layout is image-led wide;
|
|
1166
|
+
stretching a login form across that would look weird. */
|
|
1167
|
+
.np-portfolio-members {
|
|
1168
|
+
display: flex;
|
|
1169
|
+
justify-content: center;
|
|
1170
|
+
min-height: 60vh;
|
|
1171
|
+
padding: 3rem 1.5rem;
|
|
1172
|
+
}
|
|
1173
|
+
.np-portfolio-members-column {
|
|
1174
|
+
width: 100%;
|
|
1175
|
+
max-width: 420px;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/* Member form token overrides \u2014 portfolio's minimal aesthetic:
|
|
1179
|
+
sharp corners, hairline borders, theme primary on focus. */
|
|
1180
|
+
.np-portfolio .np-members-form {
|
|
1181
|
+
--np-member-form-input-bg: transparent;
|
|
1182
|
+
--np-member-form-input-border: var(--np-color-border);
|
|
1183
|
+
--np-member-form-input-border-focus: var(--np-color-primary);
|
|
1184
|
+
--np-member-form-input-radius: 0.25rem;
|
|
1185
|
+
--np-member-form-button-radius: 0.25rem;
|
|
1186
|
+
}
|
|
1187
|
+
.np-portfolio .np-members-form .np-form-label {
|
|
1188
|
+
font-size: 0.75rem;
|
|
1189
|
+
text-transform: uppercase;
|
|
1190
|
+
letter-spacing: 0.12em;
|
|
1191
|
+
}
|
|
1192
|
+
`.trim();
|
|
1193
|
+
|
|
1194
|
+
// src/templates/page-default.tsx
|
|
1195
|
+
import { renderBlocks as renderBlocks2 } from "@nexpress/blocks";
|
|
1196
|
+
import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1197
|
+
function PageDefaultTemplate({ doc, blockCtx }) {
|
|
1198
|
+
const blocks = doc.blocks;
|
|
1199
|
+
const title = doc.title;
|
|
1200
|
+
return /* @__PURE__ */ jsxs9("article", { className: "np-page np-portfolio-page", children: [
|
|
1201
|
+
title ? /* @__PURE__ */ jsx11("h1", { children: title }) : null,
|
|
1202
|
+
blocks ? renderBlocks2(blocks, { ctx: blockCtx }) : null
|
|
1203
|
+
] });
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// src/templates/page-gallery.tsx
|
|
1207
|
+
import { renderBlocks as renderBlocks3 } from "@nexpress/blocks";
|
|
1208
|
+
import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1209
|
+
function PageGalleryTemplate({ doc, blockCtx }) {
|
|
1210
|
+
const blocks = doc.blocks;
|
|
1211
|
+
const title = doc.title;
|
|
1212
|
+
return /* @__PURE__ */ jsxs10("section", { className: "np-portfolio-gallery", children: [
|
|
1213
|
+
title ? /* @__PURE__ */ jsx12("h1", { children: title }) : null,
|
|
1214
|
+
/* @__PURE__ */ jsx12("div", { className: "np-portfolio-gallery-grid", children: blocks ? renderBlocks3(blocks, { ctx: blockCtx }) : null })
|
|
1215
|
+
] });
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// src/templates/project-index.tsx
|
|
1219
|
+
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1220
|
+
async function ProjectIndexTemplate({
|
|
1221
|
+
doc
|
|
1222
|
+
}) {
|
|
1223
|
+
const data = doc;
|
|
1224
|
+
const settings = await resolvePortfolioSettings();
|
|
1225
|
+
const heading = data.heading ?? "Selected work";
|
|
1226
|
+
const intro = data.intro;
|
|
1227
|
+
const docs = data.docs ?? [];
|
|
1228
|
+
const gridStyle = {
|
|
1229
|
+
"--np-portfolio-grid-cols": settings.gridColumns,
|
|
1230
|
+
"--np-portfolio-grid-gutter": `${settings.galleryGutter}px`
|
|
1231
|
+
};
|
|
1232
|
+
return /* @__PURE__ */ jsxs11("section", { className: "np-portfolio-index", children: [
|
|
1233
|
+
/* @__PURE__ */ jsxs11("header", { className: "np-portfolio-index-header", children: [
|
|
1234
|
+
/* @__PURE__ */ jsx13("h1", { children: heading }),
|
|
1235
|
+
intro ? /* @__PURE__ */ jsx13("p", { children: intro }) : null
|
|
1236
|
+
] }),
|
|
1237
|
+
docs.length === 0 ? /* @__PURE__ */ jsx13("p", { className: "np-portfolio-index-empty", children: "Nothing on display yet. Add projects from the admin to fill the grid." }) : /* @__PURE__ */ jsx13("div", { className: "np-portfolio-index-grid", style: gridStyle, children: docs.map((project) => /* @__PURE__ */ jsx13(
|
|
1238
|
+
PortfolioProjectCard,
|
|
1239
|
+
{
|
|
1240
|
+
doc: project
|
|
1241
|
+
},
|
|
1242
|
+
project.id ?? project.slug ?? project.title
|
|
1243
|
+
)) })
|
|
1244
|
+
] });
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// src/index.ts
|
|
1248
|
+
var portfolioTheme = defineTheme({
|
|
1249
|
+
manifest: {
|
|
1250
|
+
id: "portfolio",
|
|
1251
|
+
name: "Portfolio",
|
|
1252
|
+
version: "0.1.0",
|
|
1253
|
+
description: "Image-led dark theme for studios and designers. Hero-led project detail template, archive grid, gallery and centered page templates.",
|
|
1254
|
+
author: { name: "NexPress" },
|
|
1255
|
+
nexpress: { minVersion: "0.1.0" },
|
|
1256
|
+
// Phase F.1 — declared data-shape requirements. The CLI
|
|
1257
|
+
// (`pnpm nexpress theme:install @nexpress/theme-portfolio`)
|
|
1258
|
+
// patches operator collections to satisfy these.
|
|
1259
|
+
requires: {
|
|
1260
|
+
collections: {
|
|
1261
|
+
posts: {
|
|
1262
|
+
fields: {
|
|
1263
|
+
heroImage: { type: "upload" },
|
|
1264
|
+
client: { type: "text", hard: false },
|
|
1265
|
+
year: { type: "number", hard: false },
|
|
1266
|
+
role: { type: "text", hard: false }
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
},
|
|
1271
|
+
// Phase F.3 — operator-tunable settings. Stresses the
|
|
1272
|
+
// auto-form on deep schema (10 fields, range-constrained
|
|
1273
|
+
// numbers, color regex, nested array of objects).
|
|
1274
|
+
settingsSchema: portfolioSettingsSchema
|
|
1275
|
+
},
|
|
1276
|
+
impl: {
|
|
1277
|
+
shell: PortfolioShell,
|
|
1278
|
+
slots: {
|
|
1279
|
+
header: PortfolioHeader,
|
|
1280
|
+
footer: PortfolioFooter
|
|
1281
|
+
},
|
|
1282
|
+
// Dark palette is now token-driven — previously the dark
|
|
1283
|
+
// surface was hardcoded as `#0b0b0c` in `styles.ts`, so admin
|
|
1284
|
+
// overrides couldn't reach it. Tokens here flip background +
|
|
1285
|
+
// foreground for the whole shell; `styles.ts` reads them via
|
|
1286
|
+
// `var(--np-color-*)` so a single token change reflows the
|
|
1287
|
+
// entire theme. Light variant: override these in the admin's
|
|
1288
|
+
// theme settings tab — no fork required.
|
|
1289
|
+
tokens: {
|
|
1290
|
+
colors: {
|
|
1291
|
+
primary: "oklch(0.985 0.001 106)",
|
|
1292
|
+
primaryForeground: "oklch(0.145 0.005 285)",
|
|
1293
|
+
background: "oklch(0.16 0.005 285)",
|
|
1294
|
+
foreground: "oklch(0.91 0.003 286)",
|
|
1295
|
+
muted: "oklch(0.22 0.006 286)",
|
|
1296
|
+
mutedForeground: "oklch(0.66 0.005 286)",
|
|
1297
|
+
border: "oklch(0.28 0.008 286)",
|
|
1298
|
+
card: "oklch(0.20 0.006 286)",
|
|
1299
|
+
cardForeground: "oklch(0.91 0.003 286)",
|
|
1300
|
+
accent: "oklch(0.32 0.012 286)",
|
|
1301
|
+
accentForeground: "oklch(0.985 0.001 106)"
|
|
1302
|
+
},
|
|
1303
|
+
typography: {
|
|
1304
|
+
fontHeading: '"Inter", system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
1305
|
+
fontBody: '"Inter", system-ui, -apple-system, "Segoe UI", sans-serif'
|
|
1306
|
+
}
|
|
1307
|
+
},
|
|
1308
|
+
css: portfolioCss,
|
|
1309
|
+
templates: {
|
|
1310
|
+
pages: {
|
|
1311
|
+
default: {
|
|
1312
|
+
label: "Default",
|
|
1313
|
+
description: "Centered text column on dark background.",
|
|
1314
|
+
component: PageDefaultTemplate
|
|
1315
|
+
},
|
|
1316
|
+
gallery: {
|
|
1317
|
+
label: "Gallery",
|
|
1318
|
+
description: "Two-column block grid for image-led project pages and case studies.",
|
|
1319
|
+
component: PageGalleryTemplate
|
|
1320
|
+
}
|
|
1321
|
+
},
|
|
1322
|
+
posts: {
|
|
1323
|
+
detail: {
|
|
1324
|
+
label: "Project detail",
|
|
1325
|
+
description: "Hero image, centered title and excerpt, role / year / client meta strip, then the body blocks.",
|
|
1326
|
+
component: ProjectDetailTemplate
|
|
1327
|
+
},
|
|
1328
|
+
index: {
|
|
1329
|
+
label: "Project index",
|
|
1330
|
+
description: "Archive grid of square project cards with hover-fade captions.",
|
|
1331
|
+
component: ProjectIndexTemplate
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
},
|
|
1335
|
+
// F.2 — theme routes. `/work/:slug` dispatches a posts row
|
|
1336
|
+
// through `ProjectDetailTemplate` (#613). Without this, the
|
|
1337
|
+
// `/work/<slug>` URLs `PortfolioProjectCard` emits would
|
|
1338
|
+
// 404 — the framework catch-all only resolves `pages` rows
|
|
1339
|
+
// by URL, so case studies (`posts` collection) need a theme
|
|
1340
|
+
// route to be reachable.
|
|
1341
|
+
routes: [
|
|
1342
|
+
{ pattern: "/work/:slug", component: PortfolioProjectDetailRoute }
|
|
1343
|
+
],
|
|
1344
|
+
// Phase F.4 — portfolio-shipped block types.
|
|
1345
|
+
blocks: portfolioBlocks,
|
|
1346
|
+
// Phase F.6 — declared nav locations.
|
|
1347
|
+
navLocations: {
|
|
1348
|
+
primary: {
|
|
1349
|
+
label: "Primary nav",
|
|
1350
|
+
description: "Top nav links (Work / About / Contact).",
|
|
1351
|
+
maxItems: 5
|
|
1352
|
+
},
|
|
1353
|
+
footerSocial: {
|
|
1354
|
+
label: "Footer social links",
|
|
1355
|
+
description: "Social profile links shown in the footer.",
|
|
1356
|
+
maxItems: 6
|
|
1357
|
+
}
|
|
1358
|
+
},
|
|
1359
|
+
// Phase F.7 — error chrome.
|
|
1360
|
+
notFound: PortfolioNotFound,
|
|
1361
|
+
// M.* adoption (2026-05-11). Portfolio gains purpose-built
|
|
1362
|
+
// member chrome: narrow column wrapping the auth forms,
|
|
1363
|
+
// tonally matched 404 + error pages. The fallback chain in
|
|
1364
|
+
// `<ShellWrap surface="member">` would have walked back to
|
|
1365
|
+
// `impl.shell` + the public slots, which would have stretched
|
|
1366
|
+
// a 320-wide login form across the image-led wide layout.
|
|
1367
|
+
// - `shell`: PortfolioMembersShell (narrow column, same
|
|
1368
|
+
// header/footer chrome so a masthead bump cascades).
|
|
1369
|
+
// - `notFound`: PortfolioMembersNotFound (stale-auth-link
|
|
1370
|
+
// framing with /members/login CTA).
|
|
1371
|
+
// - `error`: forward-compat type marker; the actual render
|
|
1372
|
+
// goes through `./components/members-error`'s client
|
|
1373
|
+
// subpath, lazy-imported by
|
|
1374
|
+
// `apps/web/src/app/(member)/error.tsx`'s registry
|
|
1375
|
+
// (F.7.1 delegation — Next mandates `error.tsx` is "use
|
|
1376
|
+
// client").
|
|
1377
|
+
members: {
|
|
1378
|
+
shell: PortfolioMembersShell,
|
|
1379
|
+
notFound: PortfolioMembersNotFound
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
export {
|
|
1384
|
+
PortfolioFooter,
|
|
1385
|
+
PortfolioHeader,
|
|
1386
|
+
PortfolioMembersNotFound,
|
|
1387
|
+
PortfolioMembersShell,
|
|
1388
|
+
PortfolioMobileNav2 as PortfolioMobileNav,
|
|
1389
|
+
PortfolioNotFound,
|
|
1390
|
+
PortfolioProjectCard,
|
|
1391
|
+
PortfolioShell,
|
|
1392
|
+
portfolioCss,
|
|
1393
|
+
portfolioSettingsSchema,
|
|
1394
|
+
portfolioTheme
|
|
1395
|
+
};
|
|
1396
|
+
//# sourceMappingURL=index.js.map
|