@chiselandco/nexus 3.0.0 → 3.1.1

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Self-contained project portfolio components for Next.js App Router. Pass a `clientSlug`, `apiBase`, and `apiKey` — each component fetches, caches, and renders everything it needs with no client-side waterfall requests.
4
4
 
5
- **Version:** 3.0.0
5
+ **Version:** 3.1.1
6
6
 
7
7
  ---
8
8
 
@@ -29,7 +29,7 @@ The most common full setup — a filterable projects grid, a detail page with si
29
29
 
30
30
  ```tsx
31
31
  // app/projects/page.tsx
32
- import { FilteredPortfolio } from "@chiselandco/nexus"
32
+ import { ProjectPortfolio } from "@chiselandco/nexus"
33
33
 
34
34
  export default async function ProjectsPage({
35
35
  searchParams,
@@ -37,7 +37,7 @@ export default async function ProjectsPage({
37
37
  searchParams: Promise<Record<string, string | string[] | undefined>>
38
38
  }) {
39
39
  return (
40
- <FilteredPortfolio
40
+ <ProjectPortfolio
41
41
  clientSlug="your-client-slug"
42
42
  apiBase="https://your-api.com"
43
43
  apiKey={process.env.YOUR_CLIENT_API_KEY!}
@@ -115,58 +115,11 @@ export function Nav() {
115
115
 
116
116
  ## Components
117
117
 
118
- ### `FilteredPortfolio`
119
-
120
- Server component. The recommended primary projects grid. Fetches all projects once, reads `filter[key]=` URL params server-side to narrow results using AND-across-fields / OR-within-field logic, then renders a project count toolbar with a `FilterSidebar` trigger above a responsive card grid.
121
-
122
- ```tsx
123
- import { FilteredPortfolio } from "@chiselandco/nexus"
124
-
125
- export default async function ProjectsPage({
126
- searchParams,
127
- }: {
128
- searchParams: Promise<Record<string, string | string[] | undefined>>
129
- }) {
130
- return (
131
- <FilteredPortfolio
132
- clientSlug="your-client-slug"
133
- apiBase="https://your-api.com"
134
- apiKey={process.env.YOUR_CLIENT_API_KEY!}
135
- basePath="/projects"
136
- searchParams={await searchParams}
137
- />
138
- )
139
- }
140
- ```
141
-
142
- | Prop | Type | Required | Default | Description |
143
- |---|---|---|---|---|
144
- | `clientSlug` | `string` | Yes | — | Client identifier passed to the API |
145
- | `apiBase` | `string` | Yes | — | Base URL of the Chisel API |
146
- | `apiKey` | `string` | Yes | — | Client API key — always pass via environment variable, never hardcode |
147
- | `searchParams` | `Record<string, string \| string[] \| undefined>` | Yes | — | Resolved Next.js `searchParams` — await it before passing in Next.js 16+ |
148
- | `basePath` | `string` | No | `"/projects"` | Base path for project detail card links |
149
- | `filterKeys` | `string[]` | No | All eligible fields | Ordered list of field keys to expose in the filter drawer |
150
- | `font` | `string` | No | System font | Font family string |
151
- | `noCache` | `boolean` | No | `false` | Sets `cache: "no-store"` — useful during development |
152
- | `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds |
153
- | `filterBy` | `{ field: string; value: string }` | No | — | Pre-filter projects by any custom field value. See [Filtering by field](#filtering-by-field). |
154
-
155
- #### URL param format
156
-
157
- Filters are written to and read from URL params in the form `filter[fieldKey]=slug1,slug2`. Multiple values within a field are OR'd; multiple fields are AND'd.
158
-
159
- ```
160
- /projects?filter[application]=hospitality,education&filter[systems]=spacematic
161
- ```
162
-
163
- ---
164
-
165
118
  ### `FilterSidebar`
166
119
 
167
120
  Client component (`"use client"`). Renders an "Advanced Filters" trigger that opens a right-side drawer with one section per filterable field. Pills are solid black when active and outlined when inactive. Filter state is written to URL params so filtered views are shareable and survive page refresh.
168
121
 
169
- Used internally by `FilteredPortfolio` — only import it standalone if you need to build a custom layout around it.
122
+ Use alongside `ProjectPortfolio` when you want user-driven filtering place it wherever suits your layout and pass the same `searchParams` to `ProjectPortfolio`.
170
123
 
171
124
  ```tsx
172
125
  import { FilterSidebar } from "@chiselandco/nexus"
@@ -521,7 +474,7 @@ export function Nav() {
521
474
  | `dataUrl` | `string` | No* | — | URL of a local API route created with `createMenuHandler()` — recommended for production |
522
475
  | `clientSlug` | `string` | No* | — | Client slug for direct fetch mode |
523
476
  | `apiBase` | `string` | No* | — | API base URL for direct fetch mode |
524
- | `apiKey` | `string` | No* | | Client API key for direct fetch mode |
477
+ | `apiKey` | `string` | No* | ��� | Client API key for direct fetch mode |
525
478
  | `menuId` | `string` | No | — | Slug of a curated menu |
526
479
  | `basePath` | `string` | Yes | — | Base path for project detail links |
527
480
  | `viewAllPath` | `string` | Yes | — | Path for the "View All Projects" link |
@@ -536,7 +489,7 @@ export function Nav() {
536
489
 
537
490
  ### `ProjectPortfolio`
538
491
 
539
- Server component. Fetches all projects and renders a responsive baseball card grid (1 col mobile / 2 col tablet / 3 col desktop). Supports URL-driven filtering via `searchParams`. Prefer `FilteredPortfolio` for new projects — it includes the full filter sidebar and is the recommended default.
492
+ Server component. The primary projects grid. Fetches all projects, reads `filter[key]=` URL params server-side to narrow results, and renders a responsive card grid (1 col mobile / 2 col tablet / 3 col desktop). Pair with `FilterSidebar` when you want user-driven filteringplace it wherever suits your layout.
540
493
 
541
494
  ```tsx
542
495
  // app/projects/page.tsx
@@ -617,11 +570,11 @@ export default function ProjectsPage() {
617
570
 
618
571
  ## Filtering by field
619
572
 
620
- `FilteredPortfolio`, `ProjectPortfolio`, `ProjectPortfolioClient`, and `SimilarProjects` all accept an optional `filterBy` prop. It pre-filters the project list by any custom field value before any user-driven filters are applied.
573
+ `ProjectPortfolio`, `ProjectPortfolioClient`, and `SimilarProjects` all accept an optional `filterBy` prop. It pre-filters the project list by any custom field value before any user-driven filters are applied.
621
574
 
622
575
  ```tsx
623
576
  // Only show projects where custom field "side" equals "architectural" or "both"
624
- <FilteredPortfolio
577
+ <ProjectPortfolio
625
578
  clientSlug="hollaender"
626
579
  apiBase="https://your-api.com"
627
580
  apiKey={process.env.HOLLAENDER_API_KEY!}
@@ -631,7 +584,7 @@ export default function ProjectsPage() {
631
584
  />
632
585
 
633
586
  // Only show projects where custom field "side" equals "speedrail" or "both"
634
- <FilteredPortfolio
587
+ <ProjectPortfolio
635
588
  clientSlug="hollaender"
636
589
  apiBase="https://your-api.com"
637
590
  apiKey={process.env.HOLLAENDER_API_KEY!}
@@ -655,10 +608,24 @@ The `"both"` fallback is built in — if a project's field value is `"both"` it
655
608
 
656
609
  ```tsx
657
610
  // v2
658
- <FilteredPortfolio side="architectural" />
611
+ <ProjectPortfolio side="architectural" />
659
612
 
660
613
  // v3
661
- <FilteredPortfolio filterBy={{ field: "side", value: "architectural" }} />
614
+ <ProjectPortfolio filterBy={{ field: "side", value: "architectural" }} />
615
+ ```
616
+
617
+ ### Migration from v3.0 `FilteredPortfolio`
618
+
619
+ `FilteredPortfolio` was removed in v3.1. Use `ProjectPortfolio` directly — it has the same props. Pair with `FilterSidebar` if you want a filter drawer.
620
+
621
+ ```tsx
622
+ // v3.0
623
+ import { FilteredPortfolio } from "@chiselandco/nexus"
624
+ <FilteredPortfolio clientSlug="..." apiBase="..." apiKey={...} searchParams={searchParams} />
625
+
626
+ // v3.1
627
+ import { ProjectPortfolio } from "@chiselandco/nexus"
628
+ <ProjectPortfolio clientSlug="..." apiBase="..." apiKey={...} searchParams={searchParams} />
662
629
  ```
663
630
 
664
631
  ---
@@ -667,10 +634,9 @@ The `"both"` fallback is built in — if a project's field value is `"both"` it
667
634
 
668
635
  | Component | Type | Notes |
669
636
  |---|---|---|
670
- | `FilteredPortfolio` | Server | Recommended default for project grids |
671
- | `FilterSidebar` | Client | Used internally by `FilteredPortfolio` |
672
- | `ProjectPortfolio` | Server | Simpler grid without sidebar |
673
- | `ProjectPortfolioClient` | Client | For use inside client trees |
637
+ | `ProjectPortfolio` | Server | Primary projects grid |
638
+ | `FilterSidebar` | Client | Optional filter drawer — pair with `ProjectPortfolio` |
639
+ | `ProjectPortfolioClient` | Client | For use inside client component trees |
674
640
  | `ProjectDetail` | Server | Full project detail page |
675
641
  | `GalleryCarousel` | Client | Used internally by `ProjectDetail` |
676
642
  | `SimilarProjects` | Server | After `ProjectDetail` on detail pages |
@@ -685,7 +651,6 @@ All server components must be rendered in a server context. If your parent compo
685
651
 
686
652
  | Component | Server cache | Client cache |
687
653
  |---|---|---|
688
- | `FilteredPortfolio` | 24h via `next.revalidate` | — |
689
654
  | `ProjectPortfolio` | 24h via `next.revalidate` | — |
690
655
  | `ProjectDetail` | 24h via `next.revalidate` | — |
691
656
  | `SimilarProjects` | 24h via `next.revalidate` | — |
@@ -705,6 +670,20 @@ revalidateTag("chisel-menu-your-client-slug-main-nav")
705
670
 
706
671
  ---
707
672
 
673
+ ## Image optimisation
674
+
675
+ All components that render images use the built-in `thumbUrl()` helper to request appropriately-sized variants from Vercel Blob instead of loading full-resolution originals. The helper appends `?w=<width>&q=<quality>` query params which Vercel Blob handles on-the-fly. For non-Blob image sources the URL is returned unchanged.
676
+
677
+ | Location | Display size | Requested size | Quality |
678
+ |---|---|---|---|
679
+ | `ProjectMenuClient` card thumbnails | 72×54px | 144px wide (2× retina) | 60% |
680
+ | `GalleryCarousel` thumbnail strip | 72px wide | 160px wide (2× retina) | 60% |
681
+ | `GalleryCarousel` main image | Full width | 1600px wide | 80% |
682
+
683
+ If you are hosting images outside Vercel Blob (e.g. Cloudinary, imgix, your own CDN), you can override image rendering by passing pre-transformed URLs in your media objects — `thumbUrl()` will pass them through untouched.
684
+
685
+ ---
686
+
708
687
  ## Publishing
709
688
 
710
689
  ```bash
@@ -52,5 +52,5 @@ export interface FilteredPortfolioProps {
52
52
  * )
53
53
  * }
54
54
  */
55
- export declare function FilteredPortfolio({ clientSlug, apiBase, apiKey, searchParams, filterKeys, basePath, revalidate, noCache, font, filterBy, }: FilteredPortfolioProps): Promise<import("react").JSX.Element>;
55
+ export declare function FilteredPortfolio({ clientSlug, apiBase, apiKey, searchParams, filterKeys, basePath, revalidate, noCache, font, filterBy, }: FilteredPortfolioProps): Promise<import("react/jsx-runtime").JSX.Element>;
56
56
  //# sourceMappingURL=FilteredPortfolio.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FilteredPortfolio.d.ts","sourceRoot":"","sources":["../src/FilteredPortfolio.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAA;IAC5D;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CAC5C;AA8ED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,iBAAiB,CAAC,EACtC,UAAU,EACV,OAAO,EACP,MAAM,EACN,YAAiB,EACjB,UAAU,EACV,QAAsB,EACtB,UAAe,EACf,OAAe,EACf,IAAI,EACJ,QAAQ,GACT,EAAE,sBAAsB,wCAqGxB"}
1
+ {"version":3,"file":"FilteredPortfolio.d.ts","sourceRoot":"","sources":["../src/FilteredPortfolio.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAA;IAC5D;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CAC5C;AA8ED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,iBAAiB,CAAC,EACtC,UAAU,EACV,OAAO,EACP,MAAM,EACN,YAAiB,EACjB,UAAU,EACV,QAAsB,EACtB,UAAe,EACf,OAAe,EACf,IAAI,EACJ,QAAQ,GACT,EAAE,sBAAsB,oDAqGxB"}
@@ -1 +1 @@
1
- {"version":3,"file":"GalleryCarousel.d.ts","sourceRoot":"","sources":["../src/GalleryCarousel.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA4D,MAAM,OAAO,CAAA;AAEhF,OAAO,KAAK,EAAE,KAAK,EAAE,iBAAiB,EAAe,MAAM,SAAS,CAAA;AASpE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,KAAK,EAAE,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;IACpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAA;CAC7B;AAgOD,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,MAAW,EAAE,EAAE,oBAAoB,4BA2V1F"}
1
+ {"version":3,"file":"GalleryCarousel.d.ts","sourceRoot":"","sources":["../src/GalleryCarousel.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA4D,MAAM,OAAO,CAAA;AAEhF,OAAO,KAAK,EAAE,KAAK,EAAE,iBAAiB,EAAe,MAAM,SAAS,CAAA;AAUpE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,KAAK,EAAE,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;IACpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAA;CAC7B;AAgOD,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,MAAW,EAAE,EAAE,oBAAoB,4BA2V1F"}
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useMemo, useEffect, useCallback, useRef } from "react";
4
4
  import { useRouter, useSearchParams, usePathname } from "next/navigation";
5
+ import { thumbUrl } from "./types";
5
6
  const FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
6
7
  const ACCENT = "#C8872A";
7
8
  const INK = "#0f0f0f";
@@ -168,7 +169,7 @@ function FilterDropdown({ field, values, activeSlug, onSelect, }) {
168
169
  }
169
170
  // ─── Main component ────────────────────────────────────────────────────────
170
171
  export function GalleryCarousel({ images, projectTitle, schema = [] }) {
171
- var _a;
172
+ var _a, _b;
172
173
  const router = useRouter();
173
174
  const pathname = usePathname();
174
175
  const searchParams = useSearchParams();
@@ -286,7 +287,7 @@ export function GalleryCarousel({ images, projectTitle, schema = [] }) {
286
287
  cursor: "pointer",
287
288
  letterSpacing: "0.04em",
288
289
  textDecoration: "underline",
289
- }, children: "Clear filters" })] })), active && (_jsxs("div", { style: { position: "relative", width: "100%", aspectRatio: "16/9", backgroundColor: "#1a1916", overflow: "hidden" }, children: [_jsx("img", { src: active.url, alt: active.alt || projectTitle, style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", display: "block" } }), _jsx("div", { style: {
290
+ }, children: "Clear filters" })] })), active && (_jsxs("div", { style: { position: "relative", width: "100%", aspectRatio: "16/9", backgroundColor: "#1a1916", overflow: "hidden" }, children: [_jsx("img", { src: (_b = thumbUrl(active.url, 1600, 80)) !== null && _b !== void 0 ? _b : active.url, alt: active.alt || projectTitle, style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", display: "block" } }), _jsx("div", { style: {
290
291
  position: "absolute",
291
292
  inset: "50% 0 0 0",
292
293
  background: "linear-gradient(to bottom, transparent, rgba(0,0,0,0.52))",
@@ -344,7 +345,7 @@ export function GalleryCarousel({ images, projectTitle, schema = [] }) {
344
345
  marginTop: "6px",
345
346
  paddingBottom: "2px",
346
347
  }, children: filteredImages.map((img, i) => {
347
- var _a;
348
+ var _a, _b;
348
349
  const isActive = i === safeIndex;
349
350
  return (_jsx("button", { onClick: () => setCurrent(i), "aria-label": `View image ${i + 1}`, style: {
350
351
  position: "relative",
@@ -361,7 +362,7 @@ export function GalleryCarousel({ images, projectTitle, schema = [] }) {
361
362
  outlineOffset: isActive ? "1px" : "0",
362
363
  transition: "outline 0.12s, opacity 0.12s",
363
364
  opacity: isActive ? 1 : 0.55,
364
- }, children: _jsx("img", { src: img.url, alt: img.alt || `Image ${i + 1}`, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } }) }, (_a = img.id) !== null && _a !== void 0 ? _a : i));
365
+ }, children: _jsx("img", { src: (_a = thumbUrl(img.url, 160, 60)) !== null && _a !== void 0 ? _a : img.url, alt: img.alt || `Image ${i + 1}`, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } }) }, (_b = img.id) !== null && _b !== void 0 ? _b : i));
365
366
  }) }))] }));
366
367
  }
367
368
  function arrowBtn(side) {
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectMenuClient.d.ts","sourceRoot":"","sources":["../src/ProjectMenuClient.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAA;AAClD,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAoB,MAAM,SAAS,CAAA;AAa3E,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;IACpB,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAA;IAC5B,aAAa,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC/C,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAkBD,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,UAAU,EACV,OAAO,EACP,MAAM,EACN,MAAM,EACN,OAAe,EACf,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,UAAU,EAClB,aAAa,EAAE,iBAAiB,EAChC,cAAc,EAAE,kBAAkB,EAClC,eAAe,EAAE,mBAAoC,EACrD,eAAe,EAAE,mBAAwB,EACzC,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,IAAmB,EACnB,WAAe,GAChB,EAAE,sBAAsB,qBAqfxB"}
1
+ {"version":3,"file":"ProjectMenuClient.d.ts","sourceRoot":"","sources":["../src/ProjectMenuClient.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAA;AAClD,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAoB,MAAM,SAAS,CAAA;AAc3E,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;IACpB,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAA;IAC5B,aAAa,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC/C,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAkBD,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,UAAU,EACV,OAAO,EACP,MAAM,EACN,MAAM,EACN,OAAe,EACf,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,UAAU,EAClB,aAAa,EAAE,iBAAiB,EAChC,cAAc,EAAE,kBAAkB,EAClC,eAAe,EAAE,mBAAoC,EACrD,eAAe,EAAE,mBAAwB,EACzC,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,IAAmB,EACnB,WAAe,GAChB,EAAE,sBAAsB,qBAqfxB"}
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useState, useEffect } from "react";
4
+ import { thumbUrl } from "./types";
4
5
  function parseSingleValue(raw) {
5
6
  if (typeof raw === "string")
6
7
  return raw.replace(/`/g, "").trim();
@@ -318,11 +319,11 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, apiKey, menuId
318
319
  margin: "0 0 16px 0",
319
320
  display: "none",
320
321
  }, className: "chisel-menu-subtitle", children: subtitle })), displayed.length === 0 ? (_jsx("p", { style: { fontSize: "14px", color: "#a1a1aa", margin: 0 }, children: "No projects found." })) : (_jsx("div", { className: "chisel-menu-card-grid", children: displayed.map((project) => {
321
- var _a, _b, _c, _d, _e, _f;
322
- const imageUrl = (_d = (_a = project.image_url) !== null && _a !== void 0 ? _a : (_c = (_b = project.media) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.url) !== null && _d !== void 0 ? _d : null;
322
+ var _a, _b, _c, _d, _e;
323
+ const imageUrl = thumbUrl((_a = project.image_url) !== null && _a !== void 0 ? _a : (_c = (_b = project.media) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.url, 144, 60);
323
324
  const badgeRaw = badgeField ? parseSingleValue(project.custom_field_values[badgeField.key]) : null;
324
- const badgeOptMap = badgeField ? ((_e = fieldOptionsMap[badgeField.key]) !== null && _e !== void 0 ? _e : {}) : {};
325
- const badge = badgeRaw ? ((_f = badgeOptMap[badgeRaw]) !== null && _f !== void 0 ? _f : badgeRaw) : null;
325
+ const badgeOptMap = badgeField ? ((_d = fieldOptionsMap[badgeField.key]) !== null && _d !== void 0 ? _d : {}) : {};
326
+ const badge = badgeRaw ? ((_e = badgeOptMap[badgeRaw]) !== null && _e !== void 0 ? _e : badgeRaw) : null;
326
327
  const tags = tagsField ? parseMultiValue(project.custom_field_values[tagsField.key]) : [];
327
328
  const href = `${basePath}/${project.slug}`;
328
329
  const isHovered = hoveredCard === project.id;
package/dist/index.d.ts CHANGED
@@ -16,7 +16,5 @@ export { ProjectCard } from "./ProjectCard";
16
16
  export type { CardVariant } from "./ProjectCard";
17
17
  export { FilterSidebar } from "./FilterSidebar";
18
18
  export type { FilterSidebarProps } from "./FilterSidebar";
19
- export { FilteredPortfolio } from "./FilteredPortfolio";
20
- export type { FilteredPortfolioProps } from "./FilteredPortfolio";
21
19
  export type { Project, CustomFieldSchema, CustomFieldValue, LocationValue, Media, } from "./types";
22
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAA;AACjE,YAAY,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAA;AAC3E,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACpF,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACvD,YAAY,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACvD,YAAY,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AACjE,YAAY,EACV,OAAO,EACP,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,KAAK,GACN,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAA;AACjE,YAAY,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAA;AAC3E,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACpF,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACvD,YAAY,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,YAAY,EACV,OAAO,EACP,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,KAAK,GACN,MAAM,SAAS,CAAA"}
package/dist/index.js CHANGED
@@ -7,4 +7,3 @@ export { ProjectMenu, fetchProjectMenuData, createMenuHandler } from "./ProjectM
7
7
  export { ProjectMenuClient } from "./ProjectMenuClient";
8
8
  export { ProjectCard } from "./ProjectCard";
9
9
  export { FilterSidebar } from "./FilterSidebar";
10
- export { FilteredPortfolio } from "./FilteredPortfolio";
package/dist/types.d.ts CHANGED
@@ -30,6 +30,12 @@ export interface LocationValue {
30
30
  state?: string;
31
31
  }
32
32
  export type CustomFieldValue = string | number | string[] | LocationValue | null;
33
+ /**
34
+ * Returns a resized image URL for Vercel Blob-hosted images.
35
+ * Appends ?w=<width>&q=<quality> for on-the-fly resizing.
36
+ * Falls back to the original URL unchanged for non-Blob sources.
37
+ */
38
+ export declare function thumbUrl(url: string | null | undefined, width: number, quality?: number): string | null;
33
39
  export interface Project {
34
40
  id: string;
35
41
  title: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,cAAc,GAAG,UAAU,CAAA;IAChE,OAAO,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,CAAA;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,aAAa,EAAE,OAAO,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;CACpE;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,EAAE,OAAO,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,+FAA+F;IAC/F,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC,CAAA;CAC/D;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,aAAa,GAAG,IAAI,CAAA;AAEhF,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;IACpB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;IACrD,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,KAAK,EAAE,CAAA;CACf"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,cAAc,GAAG,UAAU,CAAA;IAChE,OAAO,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,CAAA;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,aAAa,EAAE,OAAO,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;CACpE;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,EAAE,OAAO,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,+FAA+F;IAC/F,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC,CAAA;CAC/D;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,aAAa,GAAG,IAAI,CAAA;AAEhF;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG,MAAM,GAAG,IAAI,CAanG;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;IACpB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;IACrD,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,KAAK,EAAE,CAAA;CACf"}
package/dist/types.js CHANGED
@@ -1 +1,21 @@
1
- export {};
1
+ /**
2
+ * Returns a resized image URL for Vercel Blob-hosted images.
3
+ * Appends ?w=<width>&q=<quality> for on-the-fly resizing.
4
+ * Falls back to the original URL unchanged for non-Blob sources.
5
+ */
6
+ export function thumbUrl(url, width, quality = 75) {
7
+ if (!url)
8
+ return null;
9
+ try {
10
+ const u = new URL(url);
11
+ if (u.hostname.endsWith(".public.blob.vercel-storage.com")) {
12
+ u.searchParams.set("w", String(width));
13
+ u.searchParams.set("q", String(quality));
14
+ return u.toString();
15
+ }
16
+ }
17
+ catch (_a) {
18
+ // not a valid absolute URL — return as-is
19
+ }
20
+ return url;
21
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chiselandco/nexus",
3
- "version": "3.0.0",
4
- "description": "Self-contained project portfolio components for Next.js App Router. Includes ProjectPortfolio, ProjectPortfolioClient, ProjectDetail, SimilarProjects, ProjectMenu, ProjectMenuClient, GalleryCarousel, FilterSidebar, and FilteredPortfolio. Pass a clientSlug and apiBase — done.",
3
+ "version": "3.1.1",
4
+ "description": "Self-contained project portfolio components for Next.js App Router. Includes ProjectPortfolio, ProjectPortfolioClient, ProjectDetail, SimilarProjects, ProjectMenu, ProjectMenuClient, GalleryCarousel, and FilterSidebar. Pass a clientSlug and apiBase — done.",
5
5
  "keywords": [
6
6
  "nextjs",
7
7
  "react",