@pro-laico/payload-icons 0.1.0 → 0.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AA8DpB;;sFAEsF;AACtF,eAAO,MAAM,UAAU,GAAU,MAAM,MAAM,EAAE,eAAa,KAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAA0C,CAAA;AAKnI;gGACgG;AAChG,eAAO,MAAM,eAAe,GAAU,MAAM,MAAM,EAAE,eAAa,KAAG,OAAO,CAAC,IAAI,CA0B/E,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAsFpB;;sFAEsF;AACtF,eAAO,MAAM,UAAU,GAAU,MAAM,MAAM,EAAE,eAAa,KAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAGxF,CAAA;AAKD;gGACgG;AAChG,eAAO,MAAM,eAAe,GAAU,MAAM,MAAM,EAAE,eAAa,KAAG,OAAO,CAAC,IAAI,CA0B/E,CAAA"}
@@ -1,6 +1,7 @@
1
1
  import "server-only";
2
2
  import { cache } from "react";
3
- import { getIconSetSlug, getPayloadClient } from "../lib/getPayloadClient.js";
3
+ import { getConfig, getIconSetSlug, getPayloadClient } from "../lib/getPayloadClient.js";
4
+ import { ICONS_REVALIDATE_TAG } from "../lib/revalidateTag.js";
4
5
  /** The lane-correct active-set filter: published-lane reads also require `_status: 'published'`,
5
6
  * but only when the collection actually has drafts (safe if `drafts: false`). */ const activeWhere = (payload, slug, draft)=>{
6
7
  const hasDrafts = Boolean(payload.collections?.[slug]?.config?.versions?.drafts);
@@ -64,9 +65,33 @@ import { getIconSetSlug, getPayloadClient } from "../lib/getPayloadClient.js";
64
65
  }
65
66
  return map;
66
67
  });
68
+ /**
69
+ * When `@pro-laico/payload-revalidate` is installed (detected via its data-only
70
+ * `custom.payloadRevalidate` config marker — no dependency) AND this read is running inside
71
+ * a consumer's `'use cache'` scope, tag the entry with the shared icons tag, so pages that
72
+ * bake rendered SVGs into their cache refresh when icons or the active set change (the
73
+ * `icon`/`iconSet` collections carry the matching `extraTags` marker). Outside a cache
74
+ * scope, or without the revalidate plugin, this is a silent no-op — the base plugin does
75
+ * no tag revalidation on its own.
76
+ *
77
+ * Lives OUTSIDE the React-`cache()`-memoized set resolver on purpose: memoization is
78
+ * per-request, so a second cached scope in the same request would never re-run the
79
+ * resolver — the tag must be applied per read, not per query.
80
+ */ const tagIconRead = async ()=>{
81
+ try {
82
+ if (!(await getConfig()).custom?.payloadRevalidate) return;
83
+ const { cacheTag } = await import("next/cache");
84
+ cacheTag(ICONS_REVALIDATE_TAG);
85
+ } catch {
86
+ // No resolvable config, no next/cache, or not inside 'use cache' — nothing to tag.
87
+ }
88
+ };
67
89
  /** The SVG string for an icon name, resolved through the active set. Returns
68
90
  * `undefined` when the name isn't in the active set. The single seam for
69
- * rendering an icon yourself (the `<Icon>` component is this plus `extractSvg*`). */ export const getIconSvg = async (name, draft = false)=>(await getActiveIconSet(draft))[name];
91
+ * rendering an icon yourself (the `<Icon>` component is this plus `extractSvg*`). */ export const getIconSvg = async (name, draft = false)=>{
92
+ await tagIconRead();
93
+ return (await getActiveIconSet(draft))[name];
94
+ };
70
95
  /** Names already warned about, so each miss logs once per process. */ const warnedMisses = new Set();
71
96
  /** Dev-only diagnosis for an unresolved icon name: one `console.warn` per name per process, naming
72
97
  * the cause — no active set / active set only a draft / name not in the set — with the fix. */ export const warnIconMissDev = async (name, draft = false)=>{
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/cache/index.ts"],"sourcesContent":["import 'server-only'\n\nimport { cache } from 'react'\nimport type { CollectionSlug, Payload, Where } from 'payload'\n\nimport { getIconSetSlug, getPayloadClient } from '../lib/getPayloadClient'\n\n/** A `name → svgString` map for the active set's icons. */\ntype IconSetMap = Record<string, string>\n\n/** The lane-correct active-set filter: published-lane reads also require `_status: 'published'`,\n * but only when the collection actually has drafts (safe if `drafts: false`). */\nconst activeWhere = (payload: Payload, slug: string, draft: boolean): Where => {\n const hasDrafts = Boolean(\n (payload.collections as Record<string, { config?: { versions?: { drafts?: unknown } } }>)?.[slug]?.config?.versions?.drafts,\n )\n return !draft && hasDrafts ? { and: [{ active: { equals: true } }, { _status: { equals: 'published' } }] } : { active: { equals: true } }\n}\n\n/**\n * The active icon set's `name → svgString` map, resolved in a SINGLE query and\n * memoized per request via React `cache()`. Finds the set with `active: true`\n * and populates each row's icon `svgString` in one go (depth 1 + a scoped\n * `populate`), so a page with K icons costs one query, not K+1.\n *\n * On the published frontend (`draft: false`) it filters `_status: 'published'`,\n * so it reads the published-lane active set — a set activated only in the draft\n * lane (staged but not published) resolves to nothing, never leaking to prod.\n * In draft mode it reads the draft-lane active set. The `_status` filter is only\n * applied when the collection actually has drafts (safe if `drafts: false`).\n */\nconst getActiveIconSet = cache(async (draft: boolean): Promise<IconSetMap> => {\n const payload = await getPayloadClient()\n // Read the slug AFTER getPayloadClient(): resolving the config applies the plugin, which stashes it.\n const slug = getIconSetSlug()\n\n const set = (await payload\n .find({\n collection: slug as CollectionSlug,\n where: activeWhere(payload, slug, draft),\n limit: 1,\n depth: 1,\n draft,\n pagination: false,\n overrideAccess: true,\n select: { iconsArray: true },\n // Scope the populated icon docs to just the svgString we inline.\n populate: { icon: { svgString: true } },\n })\n .then((res) => res.docs[0] || null)) as {\n iconsArray?: { name?: string | null; icon?: { svgString?: string | null } | string | number | null }[]\n } | null\n\n if (!set) return {}\n const map: IconSetMap = {}\n for (const row of set.iconsArray ?? []) {\n const svg = row?.icon && typeof row.icon === 'object' ? row.icon.svgString : undefined\n if (row?.name && svg) map[row.name] = svg\n }\n return map\n})\n\n/** The SVG string for an icon name, resolved through the active set. Returns\n * `undefined` when the name isn't in the active set. The single seam for\n * rendering an icon yourself (the `<Icon>` component is this plus `extractSvg*`). */\nexport const getIconSvg = async (name: string, draft = false): Promise<string | undefined> => (await getActiveIconSet(draft))[name]\n\n/** Names already warned about, so each miss logs once per process. */\nconst warnedMisses = new Set<string>()\n\n/** Dev-only diagnosis for an unresolved icon name: one `console.warn` per name per process, naming\n * the cause — no active set / active set only a draft / name not in the set — with the fix. */\nexport const warnIconMissDev = async (name: string, draft = false): Promise<void> => {\n if (process.env.NODE_ENV === 'production' || warnedMisses.has(name)) return\n warnedMisses.add(name)\n try {\n const payload = await getPayloadClient()\n const slug = getIconSetSlug()\n const activeSetExists = async (d: boolean): Promise<boolean> => {\n const find = {\n collection: slug as CollectionSlug,\n where: activeWhere(payload, slug, d),\n limit: 1,\n depth: 0,\n draft: d,\n overrideAccess: true,\n }\n return (await payload.find(find)).docs.length > 0\n }\n const cause = (await activeSetExists(draft))\n ? `name '${name}' not in the active set — add it to the set's Icons array`\n : (await activeSetExists(true))\n ? 'active set exists only as a draft — publish it'\n : 'no active icon set — activate one'\n console.warn(`[payload-icons] <Icon name=\"${name}\"> did not resolve: ${cause}`)\n } catch {\n // Diagnostics only — never surface failures into render.\n }\n}\n"],"names":["cache","getIconSetSlug","getPayloadClient","activeWhere","payload","slug","draft","hasDrafts","Boolean","collections","config","versions","drafts","and","active","equals","_status","getActiveIconSet","set","find","collection","where","limit","depth","pagination","overrideAccess","select","iconsArray","populate","icon","svgString","then","res","docs","map","row","svg","undefined","name","getIconSvg","warnedMisses","Set","warnIconMissDev","process","env","NODE_ENV","has","add","activeSetExists","d","length","cause","console","warn"],"mappings":"AAAA,OAAO,cAAa;AAEpB,SAASA,KAAK,QAAQ,QAAO;AAG7B,SAASC,cAAc,EAAEC,gBAAgB,QAAQ,6BAAyB;AAK1E;gFACgF,GAChF,MAAMC,cAAc,CAACC,SAAkBC,MAAcC;IACnD,MAAMC,YAAYC,QACfJ,QAAQK,WAAW,EAAuE,CAACJ,KAAK,EAAEK,QAAQC,UAAUC;IAEvH,OAAO,CAACN,SAASC,YAAY;QAAEM,KAAK;YAAC;gBAAEC,QAAQ;oBAAEC,QAAQ;gBAAK;YAAE;YAAG;gBAAEC,SAAS;oBAAED,QAAQ;gBAAY;YAAE;SAAE;IAAC,IAAI;QAAED,QAAQ;YAAEC,QAAQ;QAAK;IAAE;AAC1I;AAEA;;;;;;;;;;;CAWC,GACD,MAAME,mBAAmBjB,MAAM,OAAOM;IACpC,MAAMF,UAAU,MAAMF;IACtB,qGAAqG;IACrG,MAAMG,OAAOJ;IAEb,MAAMiB,MAAO,MAAMd,QAChBe,IAAI,CAAC;QACJC,YAAYf;QACZgB,OAAOlB,YAAYC,SAASC,MAAMC;QAClCgB,OAAO;QACPC,OAAO;QACPjB;QACAkB,YAAY;QACZC,gBAAgB;QAChBC,QAAQ;YAAEC,YAAY;QAAK;QAC3B,iEAAiE;QACjEC,UAAU;YAAEC,MAAM;gBAAEC,WAAW;YAAK;QAAE;IACxC,GACCC,IAAI,CAAC,CAACC,MAAQA,IAAIC,IAAI,CAAC,EAAE,IAAI;IAIhC,IAAI,CAACf,KAAK,OAAO,CAAC;IAClB,MAAMgB,MAAkB,CAAC;IACzB,KAAK,MAAMC,OAAOjB,IAAIS,UAAU,IAAI,EAAE,CAAE;QACtC,MAAMS,MAAMD,KAAKN,QAAQ,OAAOM,IAAIN,IAAI,KAAK,WAAWM,IAAIN,IAAI,CAACC,SAAS,GAAGO;QAC7E,IAAIF,KAAKG,QAAQF,KAAKF,GAAG,CAACC,IAAIG,IAAI,CAAC,GAAGF;IACxC;IACA,OAAOF;AACT;AAEA;;oFAEoF,GACpF,OAAO,MAAMK,aAAa,OAAOD,MAAchC,QAAQ,KAAK,GAAkC,AAAC,CAAA,MAAMW,iBAAiBX,MAAK,CAAE,CAACgC,KAAK,CAAA;AAEnI,oEAAoE,GACpE,MAAME,eAAe,IAAIC;AAEzB;8FAC8F,GAC9F,OAAO,MAAMC,kBAAkB,OAAOJ,MAAchC,QAAQ,KAAK;IAC/D,IAAIqC,QAAQC,GAAG,CAACC,QAAQ,KAAK,gBAAgBL,aAAaM,GAAG,CAACR,OAAO;IACrEE,aAAaO,GAAG,CAACT;IACjB,IAAI;QACF,MAAMlC,UAAU,MAAMF;QACtB,MAAMG,OAAOJ;QACb,MAAM+C,kBAAkB,OAAOC;YAC7B,MAAM9B,OAAO;gBACXC,YAAYf;gBACZgB,OAAOlB,YAAYC,SAASC,MAAM4C;gBAClC3B,OAAO;gBACPC,OAAO;gBACPjB,OAAO2C;gBACPxB,gBAAgB;YAClB;YACA,OAAO,AAAC,CAAA,MAAMrB,QAAQe,IAAI,CAACA,KAAI,EAAGc,IAAI,CAACiB,MAAM,GAAG;QAClD;QACA,MAAMC,QAAQ,AAAC,MAAMH,gBAAgB1C,SACjC,CAAC,MAAM,EAAEgC,KAAK,yDAAyD,CAAC,GACxE,AAAC,MAAMU,gBAAgB,QACrB,mDACA;QACNI,QAAQC,IAAI,CAAC,CAAC,4BAA4B,EAAEf,KAAK,oBAAoB,EAAEa,OAAO;IAChF,EAAE,OAAM;IACN,yDAAyD;IAC3D;AACF,EAAC"}
1
+ {"version":3,"sources":["../../src/cache/index.ts"],"sourcesContent":["import 'server-only'\n\nimport { cache } from 'react'\nimport type { CollectionSlug, Payload, Where } from 'payload'\n\nimport { getConfig, getIconSetSlug, getPayloadClient } from '../lib/getPayloadClient'\nimport { ICONS_REVALIDATE_TAG } from '../lib/revalidateTag'\n\n/** A `name → svgString` map for the active set's icons. */\ntype IconSetMap = Record<string, string>\n\n/** The lane-correct active-set filter: published-lane reads also require `_status: 'published'`,\n * but only when the collection actually has drafts (safe if `drafts: false`). */\nconst activeWhere = (payload: Payload, slug: string, draft: boolean): Where => {\n const hasDrafts = Boolean(\n (payload.collections as Record<string, { config?: { versions?: { drafts?: unknown } } }>)?.[slug]?.config?.versions?.drafts,\n )\n return !draft && hasDrafts ? { and: [{ active: { equals: true } }, { _status: { equals: 'published' } }] } : { active: { equals: true } }\n}\n\n/**\n * The active icon set's `name → svgString` map, resolved in a SINGLE query and\n * memoized per request via React `cache()`. Finds the set with `active: true`\n * and populates each row's icon `svgString` in one go (depth 1 + a scoped\n * `populate`), so a page with K icons costs one query, not K+1.\n *\n * On the published frontend (`draft: false`) it filters `_status: 'published'`,\n * so it reads the published-lane active set — a set activated only in the draft\n * lane (staged but not published) resolves to nothing, never leaking to prod.\n * In draft mode it reads the draft-lane active set. The `_status` filter is only\n * applied when the collection actually has drafts (safe if `drafts: false`).\n */\nconst getActiveIconSet = cache(async (draft: boolean): Promise<IconSetMap> => {\n const payload = await getPayloadClient()\n // Read the slug AFTER getPayloadClient(): resolving the config applies the plugin, which stashes it.\n const slug = getIconSetSlug()\n\n const set = (await payload\n .find({\n collection: slug as CollectionSlug,\n where: activeWhere(payload, slug, draft),\n limit: 1,\n depth: 1,\n draft,\n pagination: false,\n overrideAccess: true,\n select: { iconsArray: true },\n // Scope the populated icon docs to just the svgString we inline.\n populate: { icon: { svgString: true } },\n })\n .then((res) => res.docs[0] || null)) as {\n iconsArray?: { name?: string | null; icon?: { svgString?: string | null } | string | number | null }[]\n } | null\n\n if (!set) return {}\n const map: IconSetMap = {}\n for (const row of set.iconsArray ?? []) {\n const svg = row?.icon && typeof row.icon === 'object' ? row.icon.svgString : undefined\n if (row?.name && svg) map[row.name] = svg\n }\n return map\n})\n\n/**\n * When `@pro-laico/payload-revalidate` is installed (detected via its data-only\n * `custom.payloadRevalidate` config marker — no dependency) AND this read is running inside\n * a consumer's `'use cache'` scope, tag the entry with the shared icons tag, so pages that\n * bake rendered SVGs into their cache refresh when icons or the active set change (the\n * `icon`/`iconSet` collections carry the matching `extraTags` marker). Outside a cache\n * scope, or without the revalidate plugin, this is a silent no-op — the base plugin does\n * no tag revalidation on its own.\n *\n * Lives OUTSIDE the React-`cache()`-memoized set resolver on purpose: memoization is\n * per-request, so a second cached scope in the same request would never re-run the\n * resolver — the tag must be applied per read, not per query.\n */\nconst tagIconRead = async (): Promise<void> => {\n try {\n if (!(await getConfig()).custom?.payloadRevalidate) return\n const { cacheTag } = (await import('next/cache')) as unknown as { cacheTag: (...tags: string[]) => void }\n cacheTag(ICONS_REVALIDATE_TAG)\n } catch {\n // No resolvable config, no next/cache, or not inside 'use cache' — nothing to tag.\n }\n}\n\n/** The SVG string for an icon name, resolved through the active set. Returns\n * `undefined` when the name isn't in the active set. The single seam for\n * rendering an icon yourself (the `<Icon>` component is this plus `extractSvg*`). */\nexport const getIconSvg = async (name: string, draft = false): Promise<string | undefined> => {\n await tagIconRead()\n return (await getActiveIconSet(draft))[name]\n}\n\n/** Names already warned about, so each miss logs once per process. */\nconst warnedMisses = new Set<string>()\n\n/** Dev-only diagnosis for an unresolved icon name: one `console.warn` per name per process, naming\n * the cause — no active set / active set only a draft / name not in the set — with the fix. */\nexport const warnIconMissDev = async (name: string, draft = false): Promise<void> => {\n if (process.env.NODE_ENV === 'production' || warnedMisses.has(name)) return\n warnedMisses.add(name)\n try {\n const payload = await getPayloadClient()\n const slug = getIconSetSlug()\n const activeSetExists = async (d: boolean): Promise<boolean> => {\n const find = {\n collection: slug as CollectionSlug,\n where: activeWhere(payload, slug, d),\n limit: 1,\n depth: 0,\n draft: d,\n overrideAccess: true,\n }\n return (await payload.find(find)).docs.length > 0\n }\n const cause = (await activeSetExists(draft))\n ? `name '${name}' not in the active set — add it to the set's Icons array`\n : (await activeSetExists(true))\n ? 'active set exists only as a draft — publish it'\n : 'no active icon set — activate one'\n console.warn(`[payload-icons] <Icon name=\"${name}\"> did not resolve: ${cause}`)\n } catch {\n // Diagnostics only — never surface failures into render.\n }\n}\n"],"names":["cache","getConfig","getIconSetSlug","getPayloadClient","ICONS_REVALIDATE_TAG","activeWhere","payload","slug","draft","hasDrafts","Boolean","collections","config","versions","drafts","and","active","equals","_status","getActiveIconSet","set","find","collection","where","limit","depth","pagination","overrideAccess","select","iconsArray","populate","icon","svgString","then","res","docs","map","row","svg","undefined","name","tagIconRead","custom","payloadRevalidate","cacheTag","getIconSvg","warnedMisses","Set","warnIconMissDev","process","env","NODE_ENV","has","add","activeSetExists","d","length","cause","console","warn"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Icon.d.ts","sourceRoot":"","sources":["../../src/collections/Icon.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAI/C,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAA;AAEvD,gDAAgD;AAChD,eAAO,MAAM,SAAS,SAAS,CAAA;AAM/B;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,GAAI,UAAS,uBAA4B,KAAG,gBA0C5D,CAAA"}
1
+ {"version":3,"file":"Icon.d.ts","sourceRoot":"","sources":["../../src/collections/Icon.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAK/C,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAA;AAEvD,gDAAgD;AAChD,eAAO,MAAM,SAAS,SAAS,CAAA;AAM/B;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,GAAI,UAAS,uBAA4B,KAAG,gBA6C5D,CAAA"}
@@ -1,6 +1,7 @@
1
1
  import { formatSVGHook } from "../hooks/formatSVG.js";
2
2
  import { defaultAdminAccess, defaultReadAccess } from "../lib/defaultAccess.js";
3
3
  import { mergeHooks } from "../lib/mergeHooks.js";
4
+ import { ICONS_REVALIDATE_TAG } from "../lib/revalidateTag.js";
4
5
  /** The default slug for the icon collection. */ export const ICON_SLUG = 'icon';
5
6
  /** Admin import-map paths for the theme-aware inline previews (the stored file `<img>`s as black — invisible on the dark theme). */ const IconPreviewFieldPath = '@pro-laico/payload-icons/admin/iconPreview#IconPreviewField';
6
7
  const IconPreviewCellPath = '@pro-laico/payload-icons/admin/iconPreview#IconPreviewCell';
@@ -35,6 +36,15 @@ const IconPreviewCellPath = '@pro-laico/payload-icons/admin/iconPreview#IconPrev
35
36
  'updatedAt'
36
37
  ]
37
38
  },
39
+ // Data-only marker for @pro-laico/payload-revalidate (no dependency): its hooks bust
40
+ // the shared icons tag on every icon write, matching the tag `getIconSvg` applies.
41
+ custom: {
42
+ revalidate: {
43
+ extraTags: [
44
+ ICONS_REVALIDATE_TAG
45
+ ]
46
+ }
47
+ },
38
48
  // Payload blocks SVG uploads by default (they're on its restricted-types list). This is an
39
49
  // SVG-only collection and `formatSVGHook` sanitizes every file (scripts / on* handlers /
40
50
  // javascript: URLs stripped) before storage, so we opt in. `mimeTypes` still scopes uploads
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collections/Icon.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\nimport { formatSVGHook } from '../hooks/formatSVG'\nimport { defaultAdminAccess, defaultReadAccess } from '../lib/defaultAccess'\nimport { mergeHooks } from '../lib/mergeHooks'\nimport type { IconCollectionOverrides } from '../types'\n\n/** The default slug for the icon collection. */\nexport const ICON_SLUG = 'icon'\n\n/** Admin import-map paths for the theme-aware inline previews (the stored file `<img>`s as black — invisible on the dark theme). */\nconst IconPreviewFieldPath = '@pro-laico/payload-icons/admin/iconPreview#IconPreviewField'\nconst IconPreviewCellPath = '@pro-laico/payload-icons/admin/iconPreview#IconPreviewCell'\n\n/**\n * Build the `Icon` upload collection: accepts `image/svg+xml` uploads, optimizes + sanitizes\n * each SVG on `beforeChange` (`formatSVGHook`), and stores the result as `svgString` for inline\n * frontend rendering. Public read by default; logged-in-admin writes.\n *\n * Pass {@link IconCollectionOverrides} to append fields/hooks, override access, or rename the slug —\n * additions stack on top of the defaults.\n */\nexport const Icon = (options: IconCollectionOverrides = {}): CollectionConfig => {\n const { slug = ICON_SLUG, adminGroup = 'Assets', access, fields = [], hooks, upload } = options\n\n return {\n slug,\n labels: { singular: 'Icon', plural: 'Icons' },\n access: {\n read: access?.read ?? defaultReadAccess,\n create: access?.create ?? defaultAdminAccess,\n update: access?.update ?? defaultAdminAccess,\n delete: access?.delete ?? defaultAdminAccess,\n },\n admin: { group: adminGroup, useAsTitle: 'filename', defaultColumns: ['filename', 'svgString', 'filesize', 'updatedAt'] },\n // Payload blocks SVG uploads by default (they're on its restricted-types list). This is an\n // SVG-only collection and `formatSVGHook` sanitizes every file (scripts / on* handlers /\n // javascript: URLs stripped) before storage, so we opt in. `mimeTypes` still scopes uploads\n // to SVG, and the opt-in stays overridable via `options.upload`.\n upload: { mimeTypes: ['image/svg+xml'], allowRestrictedFileTypes: true, ...upload },\n fields: [\n {\n name: 'iconPreview',\n type: 'ui',\n admin: { components: { Field: IconPreviewFieldPath }, condition: (data) => Boolean(data?.svgString) },\n },\n { name: 'optimized', type: 'text', admin: { readOnly: true, condition: (data) => Boolean(data?.optimized) } },\n {\n name: 'svgString',\n type: 'code',\n admin: {\n components: { Cell: IconPreviewCellPath },\n language: 'xml',\n // Read-only: a direct edit would bypass formatSVGHook's sanitization (only runs on file\n // uploads) and the string is later inlined via dangerouslySetInnerHTML — re-upload to change.\n readOnly: true,\n condition: (data) => Boolean(data?.svgString),\n editorOptions: { wordWrap: 'off', scrollBeyondLastLine: false },\n },\n },\n ...fields,\n ],\n hooks: mergeHooks({ beforeChange: [formatSVGHook] }, hooks),\n }\n}\n"],"names":["formatSVGHook","defaultAdminAccess","defaultReadAccess","mergeHooks","ICON_SLUG","IconPreviewFieldPath","IconPreviewCellPath","Icon","options","slug","adminGroup","access","fields","hooks","upload","labels","singular","plural","read","create","update","delete","admin","group","useAsTitle","defaultColumns","mimeTypes","allowRestrictedFileTypes","name","type","components","Field","condition","data","Boolean","svgString","readOnly","optimized","Cell","language","editorOptions","wordWrap","scrollBeyondLastLine","beforeChange"],"mappings":"AACA,SAASA,aAAa,QAAQ,wBAAoB;AAClD,SAASC,kBAAkB,EAAEC,iBAAiB,QAAQ,0BAAsB;AAC5E,SAASC,UAAU,QAAQ,uBAAmB;AAG9C,8CAA8C,GAC9C,OAAO,MAAMC,YAAY,OAAM;AAE/B,kIAAkI,GAClI,MAAMC,uBAAuB;AAC7B,MAAMC,sBAAsB;AAE5B;;;;;;;CAOC,GACD,OAAO,MAAMC,OAAO,CAACC,UAAmC,CAAC,CAAC;IACxD,MAAM,EAAEC,OAAOL,SAAS,EAAEM,aAAa,QAAQ,EAAEC,MAAM,EAAEC,SAAS,EAAE,EAAEC,KAAK,EAAEC,MAAM,EAAE,GAAGN;IAExF,OAAO;QACLC;QACAM,QAAQ;YAAEC,UAAU;YAAQC,QAAQ;QAAQ;QAC5CN,QAAQ;YACNO,MAAMP,QAAQO,QAAQhB;YACtBiB,QAAQR,QAAQQ,UAAUlB;YAC1BmB,QAAQT,QAAQS,UAAUnB;YAC1BoB,QAAQV,QAAQU,UAAUpB;QAC5B;QACAqB,OAAO;YAAEC,OAAOb;YAAYc,YAAY;YAAYC,gBAAgB;gBAAC;gBAAY;gBAAa;gBAAY;aAAY;QAAC;QACvH,2FAA2F;QAC3F,yFAAyF;QACzF,4FAA4F;QAC5F,iEAAiE;QACjEX,QAAQ;YAAEY,WAAW;gBAAC;aAAgB;YAAEC,0BAA0B;YAAM,GAAGb,MAAM;QAAC;QAClFF,QAAQ;YACN;gBACEgB,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBAAEQ,YAAY;wBAAEC,OAAO1B;oBAAqB;oBAAG2B,WAAW,CAACC,OAASC,QAAQD,MAAME;gBAAW;YACtG;YACA;gBAAEP,MAAM;gBAAaC,MAAM;gBAAQP,OAAO;oBAAEc,UAAU;oBAAMJ,WAAW,CAACC,OAASC,QAAQD,MAAMI;gBAAW;YAAE;YAC5G;gBACET,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBACLQ,YAAY;wBAAEQ,MAAMhC;oBAAoB;oBACxCiC,UAAU;oBACV,wFAAwF;oBACxF,8FAA8F;oBAC9FH,UAAU;oBACVJ,WAAW,CAACC,OAASC,QAAQD,MAAME;oBACnCK,eAAe;wBAAEC,UAAU;wBAAOC,sBAAsB;oBAAM;gBAChE;YACF;eACG9B;SACJ;QACDC,OAAOV,WAAW;YAAEwC,cAAc;gBAAC3C;aAAc;QAAC,GAAGa;IACvD;AACF,EAAC"}
1
+ {"version":3,"sources":["../../src/collections/Icon.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\nimport { formatSVGHook } from '../hooks/formatSVG'\nimport { defaultAdminAccess, defaultReadAccess } from '../lib/defaultAccess'\nimport { mergeHooks } from '../lib/mergeHooks'\nimport { ICONS_REVALIDATE_TAG } from '../lib/revalidateTag'\nimport type { IconCollectionOverrides } from '../types'\n\n/** The default slug for the icon collection. */\nexport const ICON_SLUG = 'icon'\n\n/** Admin import-map paths for the theme-aware inline previews (the stored file `<img>`s as black — invisible on the dark theme). */\nconst IconPreviewFieldPath = '@pro-laico/payload-icons/admin/iconPreview#IconPreviewField'\nconst IconPreviewCellPath = '@pro-laico/payload-icons/admin/iconPreview#IconPreviewCell'\n\n/**\n * Build the `Icon` upload collection: accepts `image/svg+xml` uploads, optimizes + sanitizes\n * each SVG on `beforeChange` (`formatSVGHook`), and stores the result as `svgString` for inline\n * frontend rendering. Public read by default; logged-in-admin writes.\n *\n * Pass {@link IconCollectionOverrides} to append fields/hooks, override access, or rename the slug —\n * additions stack on top of the defaults.\n */\nexport const Icon = (options: IconCollectionOverrides = {}): CollectionConfig => {\n const { slug = ICON_SLUG, adminGroup = 'Assets', access, fields = [], hooks, upload } = options\n\n return {\n slug,\n labels: { singular: 'Icon', plural: 'Icons' },\n access: {\n read: access?.read ?? defaultReadAccess,\n create: access?.create ?? defaultAdminAccess,\n update: access?.update ?? defaultAdminAccess,\n delete: access?.delete ?? defaultAdminAccess,\n },\n admin: { group: adminGroup, useAsTitle: 'filename', defaultColumns: ['filename', 'svgString', 'filesize', 'updatedAt'] },\n // Data-only marker for @pro-laico/payload-revalidate (no dependency): its hooks bust\n // the shared icons tag on every icon write, matching the tag `getIconSvg` applies.\n custom: { revalidate: { extraTags: [ICONS_REVALIDATE_TAG] } },\n // Payload blocks SVG uploads by default (they're on its restricted-types list). This is an\n // SVG-only collection and `formatSVGHook` sanitizes every file (scripts / on* handlers /\n // javascript: URLs stripped) before storage, so we opt in. `mimeTypes` still scopes uploads\n // to SVG, and the opt-in stays overridable via `options.upload`.\n upload: { mimeTypes: ['image/svg+xml'], allowRestrictedFileTypes: true, ...upload },\n fields: [\n {\n name: 'iconPreview',\n type: 'ui',\n admin: { components: { Field: IconPreviewFieldPath }, condition: (data) => Boolean(data?.svgString) },\n },\n { name: 'optimized', type: 'text', admin: { readOnly: true, condition: (data) => Boolean(data?.optimized) } },\n {\n name: 'svgString',\n type: 'code',\n admin: {\n components: { Cell: IconPreviewCellPath },\n language: 'xml',\n // Read-only: a direct edit would bypass formatSVGHook's sanitization (only runs on file\n // uploads) and the string is later inlined via dangerouslySetInnerHTML — re-upload to change.\n readOnly: true,\n condition: (data) => Boolean(data?.svgString),\n editorOptions: { wordWrap: 'off', scrollBeyondLastLine: false },\n },\n },\n ...fields,\n ],\n hooks: mergeHooks({ beforeChange: [formatSVGHook] }, hooks),\n }\n}\n"],"names":["formatSVGHook","defaultAdminAccess","defaultReadAccess","mergeHooks","ICONS_REVALIDATE_TAG","ICON_SLUG","IconPreviewFieldPath","IconPreviewCellPath","Icon","options","slug","adminGroup","access","fields","hooks","upload","labels","singular","plural","read","create","update","delete","admin","group","useAsTitle","defaultColumns","custom","revalidate","extraTags","mimeTypes","allowRestrictedFileTypes","name","type","components","Field","condition","data","Boolean","svgString","readOnly","optimized","Cell","language","editorOptions","wordWrap","scrollBeyondLastLine","beforeChange"],"mappings":"AACA,SAASA,aAAa,QAAQ,wBAAoB;AAClD,SAASC,kBAAkB,EAAEC,iBAAiB,QAAQ,0BAAsB;AAC5E,SAASC,UAAU,QAAQ,uBAAmB;AAC9C,SAASC,oBAAoB,QAAQ,0BAAsB;AAG3D,8CAA8C,GAC9C,OAAO,MAAMC,YAAY,OAAM;AAE/B,kIAAkI,GAClI,MAAMC,uBAAuB;AAC7B,MAAMC,sBAAsB;AAE5B;;;;;;;CAOC,GACD,OAAO,MAAMC,OAAO,CAACC,UAAmC,CAAC,CAAC;IACxD,MAAM,EAAEC,OAAOL,SAAS,EAAEM,aAAa,QAAQ,EAAEC,MAAM,EAAEC,SAAS,EAAE,EAAEC,KAAK,EAAEC,MAAM,EAAE,GAAGN;IAExF,OAAO;QACLC;QACAM,QAAQ;YAAEC,UAAU;YAAQC,QAAQ;QAAQ;QAC5CN,QAAQ;YACNO,MAAMP,QAAQO,QAAQjB;YACtBkB,QAAQR,QAAQQ,UAAUnB;YAC1BoB,QAAQT,QAAQS,UAAUpB;YAC1BqB,QAAQV,QAAQU,UAAUrB;QAC5B;QACAsB,OAAO;YAAEC,OAAOb;YAAYc,YAAY;YAAYC,gBAAgB;gBAAC;gBAAY;gBAAa;gBAAY;aAAY;QAAC;QACvH,qFAAqF;QACrF,mFAAmF;QACnFC,QAAQ;YAAEC,YAAY;gBAAEC,WAAW;oBAACzB;iBAAqB;YAAC;QAAE;QAC5D,2FAA2F;QAC3F,yFAAyF;QACzF,4FAA4F;QAC5F,iEAAiE;QACjEW,QAAQ;YAAEe,WAAW;gBAAC;aAAgB;YAAEC,0BAA0B;YAAM,GAAGhB,MAAM;QAAC;QAClFF,QAAQ;YACN;gBACEmB,MAAM;gBACNC,MAAM;gBACNV,OAAO;oBAAEW,YAAY;wBAAEC,OAAO7B;oBAAqB;oBAAG8B,WAAW,CAACC,OAASC,QAAQD,MAAME;gBAAW;YACtG;YACA;gBAAEP,MAAM;gBAAaC,MAAM;gBAAQV,OAAO;oBAAEiB,UAAU;oBAAMJ,WAAW,CAACC,OAASC,QAAQD,MAAMI;gBAAW;YAAE;YAC5G;gBACET,MAAM;gBACNC,MAAM;gBACNV,OAAO;oBACLW,YAAY;wBAAEQ,MAAMnC;oBAAoB;oBACxCoC,UAAU;oBACV,wFAAwF;oBACxF,8FAA8F;oBAC9FH,UAAU;oBACVJ,WAAW,CAACC,OAASC,QAAQD,MAAME;oBACnCK,eAAe;wBAAEC,UAAU;wBAAOC,sBAAsB;oBAAM;gBAChE;YACF;eACGjC;SACJ;QACDC,OAAOX,WAAW;YAAE4C,cAAc;gBAAC/C;aAAc;QAAC,GAAGc;IACvD;AACF,EAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"IconRequest.d.ts","sourceRoot":"","sources":["../../src/collections/IconRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAItD,8DAA8D;AAC9D,eAAO,MAAM,iBAAiB,gBAAgB,CAAA;AAE9C;;;GAGG;AACH,MAAM,WAAW,8BAA8B;IAC7C,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iDAAiD;IACjD,MAAM,CAAC,EAAE,KAAK,EAAE,CAAA;IAChB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;CAClC;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,2BAA2B,GAAI,OAAM,8BAAmC,KAAG,gBA4BvF,CAAA"}
1
+ {"version":3,"file":"IconRequest.d.ts","sourceRoot":"","sources":["../../src/collections/IconRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAItD,8DAA8D;AAC9D,eAAO,MAAM,iBAAiB,gBAAgB,CAAA;AAE9C;;;GAGG;AACH,MAAM,WAAW,8BAA8B;IAC7C,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iDAAiD;IACjD,MAAM,CAAC,EAAE,KAAK,EAAE,CAAA;IAChB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;CAClC;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,2BAA2B,GAAI,OAAM,8BAAmC,KAAG,gBA+BvF,CAAA"}
@@ -22,6 +22,11 @@ import { authd } from "../lib/authenticated.js";
22
22
  read: authd,
23
23
  update: authd
24
24
  },
25
+ // Internal diagnostic feed, written at runtime by the miss recorder — opt it out of
26
+ // @pro-laico/payload-revalidate's auto-attached hooks (no frontend read ever caches it).
27
+ custom: {
28
+ revalidate: false
29
+ },
25
30
  admin: {
26
31
  group,
27
32
  useAsTitle: 'name',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collections/IconRequest.ts"],"sourcesContent":["import type { CollectionConfig, Field } from 'payload'\n\nimport { authd } from '../lib/authenticated'\n\n/** Slug of the runtime icon-request diagnostic collection. */\nexport const ICON_REQUEST_SLUG = 'iconRequest'\n\n/**\n * Overrides for {@link createIconRequestCollection}. All additive on top of the\n * built-in `name` / `count` / `firstRequestedAt` / `lastRequestedAt` fields.\n */\nexport interface IconRequestCollectionOverrides {\n /** Override the `admin.group` sidebar label. @default 'Sets' */\n group?: string\n /** Extra fields appended after the built-ins. */\n fields?: Field[]\n /** Additional Payload hooks merged onto the collection. */\n hooks?: CollectionConfig['hooks']\n}\n\n/**\n * Diagnostic collection that records icon names requested at runtime which did\n * NOT resolve to an icon in the active set — the live counterpart to the\n * build-time usage manifest. Populated (throttled, fire-and-forget) by the\n * `<Icon>` server component when `iconsPlugin({ trackRequests: true })` is set,\n * and surfaced in the IconSet \"Requested icons\" panel.\n *\n * Unlike the static scan, this captures DYNAMIC names (`<Icon name={slug} />`)\n * and genuine production misses — exactly the names a static pass can't see.\n *\n * Rows are written by the recorder via `overrideAccess`; admin access is\n * read/update/delete for authenticated users (it's data you prune, not author).\n */\nexport const createIconRequestCollection = (opts: IconRequestCollectionOverrides = {}): CollectionConfig => {\n const { group = 'Sets', fields: extraFields = [], hooks } = opts\n\n return {\n slug: ICON_REQUEST_SLUG,\n access: { create: authd, delete: authd, read: authd, update: authd },\n admin: {\n group,\n useAsTitle: 'name',\n defaultColumns: ['name', 'count', 'lastRequestedAt'],\n // Hidden from the sidebar nav — it's a diagnostic feed surfaced through the\n // IconSet \"Requested icons\" panel (which reads/clears it via the API), not\n // a collection editors browse directly. `hidden` only affects nav/admin\n // visibility; the REST + local API still work, so the panel and recorder\n // are unaffected.\n hidden: true,\n description:\n 'Icon names requested at runtime that did not resolve to an icon in the active set. Populated when iconsPlugin({ trackRequests: true }) is set; compare against the IconSet usage panel.',\n },\n fields: [\n { name: 'name', type: 'text', required: true, unique: true, index: true, admin: { readOnly: true } },\n { name: 'count', type: 'number', defaultValue: 1, admin: { readOnly: true } },\n { name: 'firstRequestedAt', type: 'date', admin: { readOnly: true } },\n { name: 'lastRequestedAt', type: 'date', admin: { readOnly: true } },\n ...extraFields,\n ],\n ...(hooks ? { hooks } : {}),\n }\n}\n"],"names":["authd","ICON_REQUEST_SLUG","createIconRequestCollection","opts","group","fields","extraFields","hooks","slug","access","create","delete","read","update","admin","useAsTitle","defaultColumns","hidden","description","name","type","required","unique","index","readOnly","defaultValue"],"mappings":"AAEA,SAASA,KAAK,QAAQ,0BAAsB;AAE5C,4DAA4D,GAC5D,OAAO,MAAMC,oBAAoB,cAAa;AAe9C;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,8BAA8B,CAACC,OAAuC,CAAC,CAAC;IACnF,MAAM,EAAEC,QAAQ,MAAM,EAAEC,QAAQC,cAAc,EAAE,EAAEC,KAAK,EAAE,GAAGJ;IAE5D,OAAO;QACLK,MAAMP;QACNQ,QAAQ;YAAEC,QAAQV;YAAOW,QAAQX;YAAOY,MAAMZ;YAAOa,QAAQb;QAAM;QACnEc,OAAO;YACLV;YACAW,YAAY;YACZC,gBAAgB;gBAAC;gBAAQ;gBAAS;aAAkB;YACpD,4EAA4E;YAC5E,2EAA2E;YAC3E,wEAAwE;YACxE,yEAAyE;YACzE,kBAAkB;YAClBC,QAAQ;YACRC,aACE;QACJ;QACAb,QAAQ;YACN;gBAAEc,MAAM;gBAAQC,MAAM;gBAAQC,UAAU;gBAAMC,QAAQ;gBAAMC,OAAO;gBAAMT,OAAO;oBAAEU,UAAU;gBAAK;YAAE;YACnG;gBAAEL,MAAM;gBAASC,MAAM;gBAAUK,cAAc;gBAAGX,OAAO;oBAAEU,UAAU;gBAAK;YAAE;YAC5E;gBAAEL,MAAM;gBAAoBC,MAAM;gBAAQN,OAAO;oBAAEU,UAAU;gBAAK;YAAE;YACpE;gBAAEL,MAAM;gBAAmBC,MAAM;gBAAQN,OAAO;oBAAEU,UAAU;gBAAK;YAAE;eAChElB;SACJ;QACD,GAAIC,QAAQ;YAAEA;QAAM,IAAI,CAAC,CAAC;IAC5B;AACF,EAAC"}
1
+ {"version":3,"sources":["../../src/collections/IconRequest.ts"],"sourcesContent":["import type { CollectionConfig, Field } from 'payload'\n\nimport { authd } from '../lib/authenticated'\n\n/** Slug of the runtime icon-request diagnostic collection. */\nexport const ICON_REQUEST_SLUG = 'iconRequest'\n\n/**\n * Overrides for {@link createIconRequestCollection}. All additive on top of the\n * built-in `name` / `count` / `firstRequestedAt` / `lastRequestedAt` fields.\n */\nexport interface IconRequestCollectionOverrides {\n /** Override the `admin.group` sidebar label. @default 'Sets' */\n group?: string\n /** Extra fields appended after the built-ins. */\n fields?: Field[]\n /** Additional Payload hooks merged onto the collection. */\n hooks?: CollectionConfig['hooks']\n}\n\n/**\n * Diagnostic collection that records icon names requested at runtime which did\n * NOT resolve to an icon in the active set — the live counterpart to the\n * build-time usage manifest. Populated (throttled, fire-and-forget) by the\n * `<Icon>` server component when `iconsPlugin({ trackRequests: true })` is set,\n * and surfaced in the IconSet \"Requested icons\" panel.\n *\n * Unlike the static scan, this captures DYNAMIC names (`<Icon name={slug} />`)\n * and genuine production misses — exactly the names a static pass can't see.\n *\n * Rows are written by the recorder via `overrideAccess`; admin access is\n * read/update/delete for authenticated users (it's data you prune, not author).\n */\nexport const createIconRequestCollection = (opts: IconRequestCollectionOverrides = {}): CollectionConfig => {\n const { group = 'Sets', fields: extraFields = [], hooks } = opts\n\n return {\n slug: ICON_REQUEST_SLUG,\n access: { create: authd, delete: authd, read: authd, update: authd },\n // Internal diagnostic feed, written at runtime by the miss recorder — opt it out of\n // @pro-laico/payload-revalidate's auto-attached hooks (no frontend read ever caches it).\n custom: { revalidate: false },\n admin: {\n group,\n useAsTitle: 'name',\n defaultColumns: ['name', 'count', 'lastRequestedAt'],\n // Hidden from the sidebar nav — it's a diagnostic feed surfaced through the\n // IconSet \"Requested icons\" panel (which reads/clears it via the API), not\n // a collection editors browse directly. `hidden` only affects nav/admin\n // visibility; the REST + local API still work, so the panel and recorder\n // are unaffected.\n hidden: true,\n description:\n 'Icon names requested at runtime that did not resolve to an icon in the active set. Populated when iconsPlugin({ trackRequests: true }) is set; compare against the IconSet usage panel.',\n },\n fields: [\n { name: 'name', type: 'text', required: true, unique: true, index: true, admin: { readOnly: true } },\n { name: 'count', type: 'number', defaultValue: 1, admin: { readOnly: true } },\n { name: 'firstRequestedAt', type: 'date', admin: { readOnly: true } },\n { name: 'lastRequestedAt', type: 'date', admin: { readOnly: true } },\n ...extraFields,\n ],\n ...(hooks ? { hooks } : {}),\n }\n}\n"],"names":["authd","ICON_REQUEST_SLUG","createIconRequestCollection","opts","group","fields","extraFields","hooks","slug","access","create","delete","read","update","custom","revalidate","admin","useAsTitle","defaultColumns","hidden","description","name","type","required","unique","index","readOnly","defaultValue"],"mappings":"AAEA,SAASA,KAAK,QAAQ,0BAAsB;AAE5C,4DAA4D,GAC5D,OAAO,MAAMC,oBAAoB,cAAa;AAe9C;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,8BAA8B,CAACC,OAAuC,CAAC,CAAC;IACnF,MAAM,EAAEC,QAAQ,MAAM,EAAEC,QAAQC,cAAc,EAAE,EAAEC,KAAK,EAAE,GAAGJ;IAE5D,OAAO;QACLK,MAAMP;QACNQ,QAAQ;YAAEC,QAAQV;YAAOW,QAAQX;YAAOY,MAAMZ;YAAOa,QAAQb;QAAM;QACnE,oFAAoF;QACpF,yFAAyF;QACzFc,QAAQ;YAAEC,YAAY;QAAM;QAC5BC,OAAO;YACLZ;YACAa,YAAY;YACZC,gBAAgB;gBAAC;gBAAQ;gBAAS;aAAkB;YACpD,4EAA4E;YAC5E,2EAA2E;YAC3E,wEAAwE;YACxE,yEAAyE;YACzE,kBAAkB;YAClBC,QAAQ;YACRC,aACE;QACJ;QACAf,QAAQ;YACN;gBAAEgB,MAAM;gBAAQC,MAAM;gBAAQC,UAAU;gBAAMC,QAAQ;gBAAMC,OAAO;gBAAMT,OAAO;oBAAEU,UAAU;gBAAK;YAAE;YACnG;gBAAEL,MAAM;gBAASC,MAAM;gBAAUK,cAAc;gBAAGX,OAAO;oBAAEU,UAAU;gBAAK;YAAE;YAC5E;gBAAEL,MAAM;gBAAoBC,MAAM;gBAAQN,OAAO;oBAAEU,UAAU;gBAAK;YAAE;YACpE;gBAAEL,MAAM;gBAAmBC,MAAM;gBAAQN,OAAO;oBAAEU,UAAU;gBAAK;YAAE;eAChEpB;SACJ;QACD,GAAIC,QAAQ;YAAEA;QAAM,IAAI,CAAC,CAAC;IAC5B;AACF,EAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"IconSet.d.ts","sourceRoot":"","sources":["../../src/collections/IconSet.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAOtF,oDAAoD;AACpD,eAAO,MAAM,aAAa,YAAY,CAAA;AAWtC;;;;GAIG;AACH,MAAM,WAAW,0BAA0B;IACzC,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,GAAG,EAAE,cAAc,CAAA;KAAE,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC3G,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mCAAmC;IACnC,KAAK,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;IACjC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,KAAK,EAAE,CAAA;IAChB,0FAA0F;IAC1F,aAAa,CAAC,EAAE,KAAK,EAAE,CAAA;IACvB,wFAAwF;IACxF,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAClC,OAAM,0BAA0B,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,KAClF,gBAiFF,CAAA"}
1
+ {"version":3,"file":"IconSet.d.ts","sourceRoot":"","sources":["../../src/collections/IconSet.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAQtF,oDAAoD;AACpD,eAAO,MAAM,aAAa,YAAY,CAAA;AAWtC;;;;GAIG;AACH,MAAM,WAAW,0BAA0B;IACzC,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,GAAG,EAAE,cAAc,CAAA;KAAE,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC3G,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mCAAmC;IACnC,KAAK,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;IACjC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,KAAK,EAAE,CAAA;IAChB,0FAA0F;IAC1F,aAAa,CAAC,EAAE,KAAK,EAAE,CAAA;IACvB,wFAAwF;IACxF,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAClC,OAAM,0BAA0B,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,KAClF,gBAqFF,CAAA"}
@@ -1,6 +1,7 @@
1
1
  import { activeField, enforceSingleActive } from "../lib/activeField.js";
2
2
  import { authd } from "../lib/authenticated.js";
3
3
  import { mergeHooks } from "../lib/mergeHooks.js";
4
+ import { ICONS_REVALIDATE_TAG } from "../lib/revalidateTag.js";
4
5
  import { toKebabCase } from "../lib/titleCase.js";
5
6
  /** The default slug for the icon-set collection. */ export const ICON_SET_SLUG = 'iconSet';
6
7
  /** Admin import-map path for the IconSet "requested icons" usage panel. */ const IconUsagePanelPath = '@pro-laico/payload-icons/admin/iconUsagePanel';
@@ -47,6 +48,16 @@ import { toKebabCase } from "../lib/titleCase.js";
47
48
  read: authd,
48
49
  update: authd
49
50
  },
51
+ // Data-only marker for @pro-laico/payload-revalidate (no dependency): its hooks bust
52
+ // the shared icons tag on every published set write — activating a set, editing its
53
+ // iconsArray, publishing — matching the tag `getIconSvg` applies.
54
+ custom: {
55
+ revalidate: {
56
+ extraTags: [
57
+ ICONS_REVALIDATE_TAG
58
+ ]
59
+ }
60
+ },
50
61
  admin: {
51
62
  group,
52
63
  useAsTitle: 'title',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collections/IconSet.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug, Field, PayloadRequest } from 'payload'\n\nimport { activeField, enforceSingleActive } from '../lib/activeField'\nimport { authd } from '../lib/authenticated'\nimport { mergeHooks } from '../lib/mergeHooks'\nimport { toKebabCase } from '../lib/titleCase'\n\n/** The default slug for the icon-set collection. */\nexport const ICON_SET_SLUG = 'iconSet'\n\n/** Admin import-map path for the IconSet \"requested icons\" usage panel. */\nconst IconUsagePanelPath = '@pro-laico/payload-icons/admin/iconUsagePanel'\n\n/** Admin import-map path for the per-row label inside `iconsArray`. */\nconst IconRowLabelPath = '@pro-laico/payload-icons/admin/iconRowLabel'\n\n/** Inline title field so the collection has no template dependency. */\nconst titleField = (defaultValue = 'New Icon Set'): Field => ({ name: 'title', type: 'text', required: true, unique: true, defaultValue })\n\n/**\n * Overrides for the `iconSet` collection — a named, ordered `name → icon` mapping\n * into the shared `icon` pool. One set is `active` at a time; the frontend renders\n * it (see {@link enforceSingleActive}).\n */\nexport interface IconSetCollectionOverrides {\n /** Slug for the icon-set collection. @default 'iconSet' */\n slug?: string\n /**\n * Live-preview URL generator. When provided, wires both `admin.preview`\n * (legacy iframe) and `admin.livePreview.url`. Omitted by default.\n */\n livePreviewUrl?: (args: { data: Record<string, unknown>; req: PayloadRequest }) => string | Promise<string>\n /** Override the `admin.group` sidebar label. @default 'Sets' */\n group?: string\n /** Additional collection hooks. */\n hooks?: CollectionConfig['hooks']\n /** Extra set-level fields appended to the Settings tab, below the title. */\n fields?: Field[]\n /** Extra fields appended to each `iconsArray` row, after the built-in `name` + `icon`. */\n iconRowFields?: Field[]\n /** Enable drafts/versions on the collection (per-set draft → publish). @default true */\n drafts?: boolean\n}\n\n/**\n * Builds the `iconSet` collection — a named `name → icon` mapping with a\n * single-active toggle; the frontend renders the active set.\n *\n * `iconSlug` and `usagePanel` are wired by the plugin (from the icon collection's\n * slug and the top-level `usagePanel` option); they aren't part of the public\n * override surface.\n */\nexport const createIconSetCollection = (\n opts: IconSetCollectionOverrides & { iconSlug?: string; usagePanel?: boolean } = {},\n): CollectionConfig => {\n const {\n slug = ICON_SET_SLUG,\n iconSlug = 'icon',\n livePreviewUrl,\n group = 'Sets',\n hooks,\n fields: extraFields = [],\n iconRowFields = [],\n usagePanel = true,\n drafts = true,\n } = opts\n\n // The \"Requested icons\" panel — a server UI field that scans usage (live in dev,\n // manifest in prod) and diffs it against this set's icons. Config-free: the panel\n // uses its defaults (ICON_USAGE_MANIFEST env for the manifest path in prod).\n const usageField: Field[] = usagePanel ? [{ name: 'iconUsage', type: 'ui', admin: { components: { Field: IconUsagePanelPath } } }] : []\n\n return {\n slug,\n labels: { singular: 'Icon Set', plural: 'Icon Sets' },\n access: { create: authd, delete: authd, read: authd, update: authd },\n admin: {\n group,\n useAsTitle: 'title',\n defaultColumns: ['title', 'active', '_status'],\n ...(livePreviewUrl && {\n preview: (data, { req }) => livePreviewUrl({ data: data as Record<string, unknown>, req }),\n livePreview: { url: ({ data, req }) => livePreviewUrl({ data: data as Record<string, unknown>, req }) },\n }),\n },\n fields: [\n {\n type: 'tabs',\n tabs: [\n { label: 'Settings', fields: [{ type: 'row', fields: [activeField, titleField('New Icon Set')] }, ...extraFields, ...usageField] },\n {\n label: 'Icons',\n fields: [\n {\n name: 'iconsArray',\n type: 'array',\n label: 'Icons',\n labels: { singular: 'Icon', plural: 'Icons' },\n admin: { initCollapsed: true, components: { RowLabel: IconRowLabelPath } },\n fields: [\n {\n type: 'row',\n fields: [\n {\n name: 'name',\n type: 'text',\n required: true,\n // Normalize to kebab-case so the typed name matches `<Icon name>`.\n hooks: { beforeValidate: [({ value }) => (typeof value === 'string' ? toKebabCase(value) : value)] },\n admin: {\n width: '25%',\n description: 'The name the frontend looks the icon up by (auto kebab-cased).',\n style: { maxWidth: '350px' },\n },\n },\n {\n name: 'icon',\n type: 'upload',\n relationTo: iconSlug as CollectionSlug,\n displayPreview: false,\n admin: { allowCreate: false, width: '75%', description: 'Select an icon.', style: { maxWidth: '350px' } },\n },\n ...iconRowFields,\n ],\n },\n ],\n },\n ],\n },\n ],\n },\n ],\n hooks: mergeHooks({ beforeChange: [enforceSingleActive] }, hooks),\n ...(drafts ? { versions: { drafts: { schedulePublish: true }, maxPerDoc: 50 } } : {}),\n }\n}\n"],"names":["activeField","enforceSingleActive","authd","mergeHooks","toKebabCase","ICON_SET_SLUG","IconUsagePanelPath","IconRowLabelPath","titleField","defaultValue","name","type","required","unique","createIconSetCollection","opts","slug","iconSlug","livePreviewUrl","group","hooks","fields","extraFields","iconRowFields","usagePanel","drafts","usageField","admin","components","Field","labels","singular","plural","access","create","delete","read","update","useAsTitle","defaultColumns","preview","data","req","livePreview","url","tabs","label","initCollapsed","RowLabel","beforeValidate","value","width","description","style","maxWidth","relationTo","displayPreview","allowCreate","beforeChange","versions","schedulePublish","maxPerDoc"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/collections/IconSet.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug, Field, PayloadRequest } from 'payload'\n\nimport { activeField, enforceSingleActive } from '../lib/activeField'\nimport { authd } from '../lib/authenticated'\nimport { mergeHooks } from '../lib/mergeHooks'\nimport { ICONS_REVALIDATE_TAG } from '../lib/revalidateTag'\nimport { toKebabCase } from '../lib/titleCase'\n\n/** The default slug for the icon-set collection. */\nexport const ICON_SET_SLUG = 'iconSet'\n\n/** Admin import-map path for the IconSet \"requested icons\" usage panel. */\nconst IconUsagePanelPath = '@pro-laico/payload-icons/admin/iconUsagePanel'\n\n/** Admin import-map path for the per-row label inside `iconsArray`. */\nconst IconRowLabelPath = '@pro-laico/payload-icons/admin/iconRowLabel'\n\n/** Inline title field so the collection has no template dependency. */\nconst titleField = (defaultValue = 'New Icon Set'): Field => ({ name: 'title', type: 'text', required: true, unique: true, defaultValue })\n\n/**\n * Overrides for the `iconSet` collection — a named, ordered `name → icon` mapping\n * into the shared `icon` pool. One set is `active` at a time; the frontend renders\n * it (see {@link enforceSingleActive}).\n */\nexport interface IconSetCollectionOverrides {\n /** Slug for the icon-set collection. @default 'iconSet' */\n slug?: string\n /**\n * Live-preview URL generator. When provided, wires both `admin.preview`\n * (legacy iframe) and `admin.livePreview.url`. Omitted by default.\n */\n livePreviewUrl?: (args: { data: Record<string, unknown>; req: PayloadRequest }) => string | Promise<string>\n /** Override the `admin.group` sidebar label. @default 'Sets' */\n group?: string\n /** Additional collection hooks. */\n hooks?: CollectionConfig['hooks']\n /** Extra set-level fields appended to the Settings tab, below the title. */\n fields?: Field[]\n /** Extra fields appended to each `iconsArray` row, after the built-in `name` + `icon`. */\n iconRowFields?: Field[]\n /** Enable drafts/versions on the collection (per-set draft → publish). @default true */\n drafts?: boolean\n}\n\n/**\n * Builds the `iconSet` collection — a named `name → icon` mapping with a\n * single-active toggle; the frontend renders the active set.\n *\n * `iconSlug` and `usagePanel` are wired by the plugin (from the icon collection's\n * slug and the top-level `usagePanel` option); they aren't part of the public\n * override surface.\n */\nexport const createIconSetCollection = (\n opts: IconSetCollectionOverrides & { iconSlug?: string; usagePanel?: boolean } = {},\n): CollectionConfig => {\n const {\n slug = ICON_SET_SLUG,\n iconSlug = 'icon',\n livePreviewUrl,\n group = 'Sets',\n hooks,\n fields: extraFields = [],\n iconRowFields = [],\n usagePanel = true,\n drafts = true,\n } = opts\n\n // The \"Requested icons\" panel — a server UI field that scans usage (live in dev,\n // manifest in prod) and diffs it against this set's icons. Config-free: the panel\n // uses its defaults (ICON_USAGE_MANIFEST env for the manifest path in prod).\n const usageField: Field[] = usagePanel ? [{ name: 'iconUsage', type: 'ui', admin: { components: { Field: IconUsagePanelPath } } }] : []\n\n return {\n slug,\n labels: { singular: 'Icon Set', plural: 'Icon Sets' },\n access: { create: authd, delete: authd, read: authd, update: authd },\n // Data-only marker for @pro-laico/payload-revalidate (no dependency): its hooks bust\n // the shared icons tag on every published set write — activating a set, editing its\n // iconsArray, publishing — matching the tag `getIconSvg` applies.\n custom: { revalidate: { extraTags: [ICONS_REVALIDATE_TAG] } },\n admin: {\n group,\n useAsTitle: 'title',\n defaultColumns: ['title', 'active', '_status'],\n ...(livePreviewUrl && {\n preview: (data, { req }) => livePreviewUrl({ data: data as Record<string, unknown>, req }),\n livePreview: { url: ({ data, req }) => livePreviewUrl({ data: data as Record<string, unknown>, req }) },\n }),\n },\n fields: [\n {\n type: 'tabs',\n tabs: [\n { label: 'Settings', fields: [{ type: 'row', fields: [activeField, titleField('New Icon Set')] }, ...extraFields, ...usageField] },\n {\n label: 'Icons',\n fields: [\n {\n name: 'iconsArray',\n type: 'array',\n label: 'Icons',\n labels: { singular: 'Icon', plural: 'Icons' },\n admin: { initCollapsed: true, components: { RowLabel: IconRowLabelPath } },\n fields: [\n {\n type: 'row',\n fields: [\n {\n name: 'name',\n type: 'text',\n required: true,\n // Normalize to kebab-case so the typed name matches `<Icon name>`.\n hooks: { beforeValidate: [({ value }) => (typeof value === 'string' ? toKebabCase(value) : value)] },\n admin: {\n width: '25%',\n description: 'The name the frontend looks the icon up by (auto kebab-cased).',\n style: { maxWidth: '350px' },\n },\n },\n {\n name: 'icon',\n type: 'upload',\n relationTo: iconSlug as CollectionSlug,\n displayPreview: false,\n admin: { allowCreate: false, width: '75%', description: 'Select an icon.', style: { maxWidth: '350px' } },\n },\n ...iconRowFields,\n ],\n },\n ],\n },\n ],\n },\n ],\n },\n ],\n hooks: mergeHooks({ beforeChange: [enforceSingleActive] }, hooks),\n ...(drafts ? { versions: { drafts: { schedulePublish: true }, maxPerDoc: 50 } } : {}),\n }\n}\n"],"names":["activeField","enforceSingleActive","authd","mergeHooks","ICONS_REVALIDATE_TAG","toKebabCase","ICON_SET_SLUG","IconUsagePanelPath","IconRowLabelPath","titleField","defaultValue","name","type","required","unique","createIconSetCollection","opts","slug","iconSlug","livePreviewUrl","group","hooks","fields","extraFields","iconRowFields","usagePanel","drafts","usageField","admin","components","Field","labels","singular","plural","access","create","delete","read","update","custom","revalidate","extraTags","useAsTitle","defaultColumns","preview","data","req","livePreview","url","tabs","label","initCollapsed","RowLabel","beforeValidate","value","width","description","style","maxWidth","relationTo","displayPreview","allowCreate","beforeChange","versions","schedulePublish","maxPerDoc"],"mappings":"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"}
@@ -13,23 +13,6 @@ export interface IconProps extends React.SVGAttributes<SVGSVGElement> {
13
13
  */
14
14
  fallback?: string;
15
15
  }
16
- /**
17
- * Renders a CMS-managed icon by name. Server component — looks up the active
18
- * `iconSet`, finds the entry matching `name`, and inlines its `<svg>` with the
19
- * source's intrinsic attributes (viewBox, fill, …) merged under any JSX props
20
- * you pass (so `className`, `style`, `width`, etc. always win). It inherits CSS
21
- * `color` via `currentColor`. Falls back to {@link IconProps.fallback} (or a
22
- * built-in warning glyph) when the name doesn't resolve.
23
- *
24
- * @example
25
- * ```tsx
26
- * import { Icon } from '@pro-laico/payload-icons/components/Icon'
27
- *
28
- * <Icon name="arrow-right" />
29
- * <Icon name="arrow-right" className="size-6 text-primary" />
30
- * <Icon name="logo" fallback={myCustomSvgString} />
31
- * ```
32
- */
33
16
  export declare const Icon: ({ name, fallback, ...svgProps }: IconProps) => Promise<React.ReactElement>;
34
17
  export default Icon;
35
18
  //# sourceMappingURL=Icon.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Icon.d.ts","sourceRoot":"","sources":["../../src/components/Icon.tsx"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAc9B,MAAM,WAAW,SAAU,SAAQ,KAAK,CAAC,aAAa,CAAC,aAAa,CAAC;IACnE;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,IAAI,GAAU,iCAAiC,SAAS,KAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAwBjG,CAAA;AAED,eAAe,IAAI,CAAA"}
1
+ {"version":3,"file":"Icon.d.ts","sourceRoot":"","sources":["../../src/components/Icon.tsx"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAc9B,MAAM,WAAW,SAAU,SAAQ,KAAK,CAAC,aAAa,CAAC,aAAa,CAAC;IACnE;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AA8BD,eAAO,MAAM,IAAI,GAAU,iCAAiC,SAAS,KAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAwBjG,CAAA;AAED,eAAe,IAAI,CAAA"}
@@ -25,8 +25,17 @@ import { trackIconMiss } from "../usage/trackIconMiss.js";
25
25
  * <Icon name="arrow-right" className="size-6 text-primary" />
26
26
  * <Icon name="logo" fallback={myCustomSvgString} />
27
27
  * ```
28
- */ export const Icon = async ({ name, fallback, ...svgProps })=>{
29
- const { isEnabled: draft } = await draftMode();
28
+ */ /** The draft-mode lane, resolved safely from ANY scope. `draftMode()` is a request API that
29
+ * throws inside a `'use cache'` scope — where an inlined icon should read the published
30
+ * lane anyway (draft previews render dynamically, so the request path still resolves). */ const draftLane = async ()=>{
31
+ try {
32
+ return (await draftMode()).isEnabled;
33
+ } catch {
34
+ return false;
35
+ }
36
+ };
37
+ export const Icon = async ({ name, fallback, ...svgProps })=>{
38
+ const draft = await draftLane();
30
39
  const svg = await getIconSvg(name, draft);
31
40
  // Name didn't resolve against the active set — record it (throttled,
32
41
  // fire-and-forget) so the admin "Requested icons" panel can surface real
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/Icon.tsx"],"sourcesContent":["import 'server-only'\n\nimport type React from 'react'\nimport { draftMode } from 'next/headers'\n\nimport { getIconSvg, warnIconMissDev } from '../cache'\nimport { extractSvgContent, extractSvgProps } from '../lib/extractSVG'\nimport { trackIconMiss } from '../usage/trackIconMiss'\n\n/**\n * Inline fallback rendered when `name` doesn't resolve. Kept local so the\n * component carries no extra dependency — pass {@link IconProps.fallback} to\n * override it with your own SVG string.\n */\nconst FALLBACK_WARNING_SVG = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"10.7 10.7 106.6 106.6\" fill=\"currentColor\" stroke=\"currentColor\"><path d=\"M64 37.3a5.3 5.3 0 015.3 5.4v26.6a5.3 5.3 0 11-10.6 0V42.7a5.3 5.3 0 015.3-5.4m0 53.4A5.3 5.3 0 0064 80a5.3 5.3 0 000 10.7\"/><path fill-rule=\"evenodd\" d=\"M10.7 64a53.3 53.3 0 11106.6 0 53.3 53.3 0 01-106.6 0M64 21.3a42.7 42.7 0 100 85.4 42.7 42.7 0 000-85.4\"/></svg>`\n\nexport interface IconProps extends React.SVGAttributes<SVGSVGElement> {\n /**\n * Icon name as defined in the active `iconSet`'s `iconsArray` (each entry's\n * `name` field). Resolved server-side through the active set, so swapping the\n * active set re-skins every icon.\n */\n name: string\n /**\n * Optional fallback SVG string used when `name` doesn't match any icon in the\n * active set. Defaults to a small inline warning glyph.\n */\n fallback?: string\n}\n\n/**\n * Renders a CMS-managed icon by name. Server component — looks up the active\n * `iconSet`, finds the entry matching `name`, and inlines its `<svg>` with the\n * source's intrinsic attributes (viewBox, fill, …) merged under any JSX props\n * you pass (so `className`, `style`, `width`, etc. always win). It inherits CSS\n * `color` via `currentColor`. Falls back to {@link IconProps.fallback} (or a\n * built-in warning glyph) when the name doesn't resolve.\n *\n * @example\n * ```tsx\n * import { Icon } from '@pro-laico/payload-icons/components/Icon'\n *\n * <Icon name=\"arrow-right\" />\n * <Icon name=\"arrow-right\" className=\"size-6 text-primary\" />\n * <Icon name=\"logo\" fallback={myCustomSvgString} />\n * ```\n */\nexport const Icon = async ({ name, fallback, ...svgProps }: IconProps): Promise<React.ReactElement> => {\n const { isEnabled: draft } = await draftMode()\n const svg = await getIconSvg(name, draft)\n // Name didn't resolve against the active set — record it (throttled,\n // fire-and-forget) so the admin \"Requested icons\" panel can surface real\n // runtime misses, including dynamic names a static scan can't see.\n if (!svg) {\n trackIconMiss(name)\n void warnIconMissDev(name, draft) // dev-only, once per name — diagnoses WHY the name missed\n }\n const source = svg || fallback || FALLBACK_WARNING_SVG\n\n // Default to decorative (aria-hidden); callers announcing the icon override\n // with `aria-hidden={false}` + a title since svgProps wins over this default.\n // `data-icon-missing` marks fallback renders so multiple misses on a page are distinguishable.\n return (\n <svg\n aria-hidden=\"true\"\n {...(svg ? {} : { 'data-icon-missing': name })}\n {...extractSvgProps(source)}\n {...svgProps}\n dangerouslySetInnerHTML={{ __html: extractSvgContent(source) }}\n />\n )\n}\n\nexport default Icon\n"],"names":["draftMode","getIconSvg","warnIconMissDev","extractSvgContent","extractSvgProps","trackIconMiss","FALLBACK_WARNING_SVG","Icon","name","fallback","svgProps","isEnabled","draft","svg","source","aria-hidden","dangerouslySetInnerHTML","__html"],"mappings":";AAAA,OAAO,cAAa;AAGpB,SAASA,SAAS,QAAQ,eAAc;AAExC,SAASC,UAAU,EAAEC,eAAe,QAAQ,oBAAU;AACtD,SAASC,iBAAiB,EAAEC,eAAe,QAAQ,uBAAmB;AACtE,SAASC,aAAa,QAAQ,4BAAwB;AAEtD;;;;CAIC,GACD,MAAMC,uBAAuB,CAAC,qYAAqY,CAAC;AAgBpa;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMC,OAAO,OAAO,EAAEC,IAAI,EAAEC,QAAQ,EAAE,GAAGC,UAAqB;IACnE,MAAM,EAAEC,WAAWC,KAAK,EAAE,GAAG,MAAMZ;IACnC,MAAMa,MAAM,MAAMZ,WAAWO,MAAMI;IACnC,qEAAqE;IACrE,yEAAyE;IACzE,mEAAmE;IACnE,IAAI,CAACC,KAAK;QACRR,cAAcG;QACd,KAAKN,gBAAgBM,MAAMI,QAAO,0DAA0D;IAC9F;IACA,MAAME,SAASD,OAAOJ,YAAYH;IAElC,4EAA4E;IAC5E,8EAA8E;IAC9E,+FAA+F;IAC/F,qBACE,KAACO;QACCE,eAAY;QACX,GAAIF,MAAM,CAAC,IAAI;YAAE,qBAAqBL;QAAK,CAAC;QAC5C,GAAGJ,gBAAgBU,OAAO;QAC1B,GAAGJ,QAAQ;QACZM,yBAAyB;YAAEC,QAAQd,kBAAkBW;QAAQ;;AAGnE,EAAC;AAED,eAAeP,KAAI"}
1
+ {"version":3,"sources":["../../src/components/Icon.tsx"],"sourcesContent":["import 'server-only'\n\nimport type React from 'react'\nimport { draftMode } from 'next/headers'\n\nimport { getIconSvg, warnIconMissDev } from '../cache'\nimport { extractSvgContent, extractSvgProps } from '../lib/extractSVG'\nimport { trackIconMiss } from '../usage/trackIconMiss'\n\n/**\n * Inline fallback rendered when `name` doesn't resolve. Kept local so the\n * component carries no extra dependency — pass {@link IconProps.fallback} to\n * override it with your own SVG string.\n */\nconst FALLBACK_WARNING_SVG = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"10.7 10.7 106.6 106.6\" fill=\"currentColor\" stroke=\"currentColor\"><path d=\"M64 37.3a5.3 5.3 0 015.3 5.4v26.6a5.3 5.3 0 11-10.6 0V42.7a5.3 5.3 0 015.3-5.4m0 53.4A5.3 5.3 0 0064 80a5.3 5.3 0 000 10.7\"/><path fill-rule=\"evenodd\" d=\"M10.7 64a53.3 53.3 0 11106.6 0 53.3 53.3 0 01-106.6 0M64 21.3a42.7 42.7 0 100 85.4 42.7 42.7 0 000-85.4\"/></svg>`\n\nexport interface IconProps extends React.SVGAttributes<SVGSVGElement> {\n /**\n * Icon name as defined in the active `iconSet`'s `iconsArray` (each entry's\n * `name` field). Resolved server-side through the active set, so swapping the\n * active set re-skins every icon.\n */\n name: string\n /**\n * Optional fallback SVG string used when `name` doesn't match any icon in the\n * active set. Defaults to a small inline warning glyph.\n */\n fallback?: string\n}\n\n/**\n * Renders a CMS-managed icon by name. Server component — looks up the active\n * `iconSet`, finds the entry matching `name`, and inlines its `<svg>` with the\n * source's intrinsic attributes (viewBox, fill, …) merged under any JSX props\n * you pass (so `className`, `style`, `width`, etc. always win). It inherits CSS\n * `color` via `currentColor`. Falls back to {@link IconProps.fallback} (or a\n * built-in warning glyph) when the name doesn't resolve.\n *\n * @example\n * ```tsx\n * import { Icon } from '@pro-laico/payload-icons/components/Icon'\n *\n * <Icon name=\"arrow-right\" />\n * <Icon name=\"arrow-right\" className=\"size-6 text-primary\" />\n * <Icon name=\"logo\" fallback={myCustomSvgString} />\n * ```\n */\n/** The draft-mode lane, resolved safely from ANY scope. `draftMode()` is a request API that\n * throws inside a `'use cache'` scope — where an inlined icon should read the published\n * lane anyway (draft previews render dynamically, so the request path still resolves). */\nconst draftLane = async (): Promise<boolean> => {\n try {\n return (await draftMode()).isEnabled\n } catch {\n return false\n }\n}\n\nexport const Icon = async ({ name, fallback, ...svgProps }: IconProps): Promise<React.ReactElement> => {\n const draft = await draftLane()\n const svg = await getIconSvg(name, draft)\n // Name didn't resolve against the active set — record it (throttled,\n // fire-and-forget) so the admin \"Requested icons\" panel can surface real\n // runtime misses, including dynamic names a static scan can't see.\n if (!svg) {\n trackIconMiss(name)\n void warnIconMissDev(name, draft) // dev-only, once per name — diagnoses WHY the name missed\n }\n const source = svg || fallback || FALLBACK_WARNING_SVG\n\n // Default to decorative (aria-hidden); callers announcing the icon override\n // with `aria-hidden={false}` + a title since svgProps wins over this default.\n // `data-icon-missing` marks fallback renders so multiple misses on a page are distinguishable.\n return (\n <svg\n aria-hidden=\"true\"\n {...(svg ? {} : { 'data-icon-missing': name })}\n {...extractSvgProps(source)}\n {...svgProps}\n dangerouslySetInnerHTML={{ __html: extractSvgContent(source) }}\n />\n )\n}\n\nexport default Icon\n"],"names":["draftMode","getIconSvg","warnIconMissDev","extractSvgContent","extractSvgProps","trackIconMiss","FALLBACK_WARNING_SVG","draftLane","isEnabled","Icon","name","fallback","svgProps","draft","svg","source","aria-hidden","dangerouslySetInnerHTML","__html"],"mappings":";AAAA,OAAO,cAAa;AAGpB,SAASA,SAAS,QAAQ,eAAc;AAExC,SAASC,UAAU,EAAEC,eAAe,QAAQ,oBAAU;AACtD,SAASC,iBAAiB,EAAEC,eAAe,QAAQ,uBAAmB;AACtE,SAASC,aAAa,QAAQ,4BAAwB;AAEtD;;;;CAIC,GACD,MAAMC,uBAAuB,CAAC,qYAAqY,CAAC;AAgBpa;;;;;;;;;;;;;;;;CAgBC,GACD;;yFAEyF,GACzF,MAAMC,YAAY;IAChB,IAAI;QACF,OAAO,AAAC,CAAA,MAAMP,WAAU,EAAGQ,SAAS;IACtC,EAAE,OAAM;QACN,OAAO;IACT;AACF;AAEA,OAAO,MAAMC,OAAO,OAAO,EAAEC,IAAI,EAAEC,QAAQ,EAAE,GAAGC,UAAqB;IACnE,MAAMC,QAAQ,MAAMN;IACpB,MAAMO,MAAM,MAAMb,WAAWS,MAAMG;IACnC,qEAAqE;IACrE,yEAAyE;IACzE,mEAAmE;IACnE,IAAI,CAACC,KAAK;QACRT,cAAcK;QACd,KAAKR,gBAAgBQ,MAAMG,QAAO,0DAA0D;IAC9F;IACA,MAAME,SAASD,OAAOH,YAAYL;IAElC,4EAA4E;IAC5E,8EAA8E;IAC9E,+FAA+F;IAC/F,qBACE,KAACQ;QACCE,eAAY;QACX,GAAIF,MAAM,CAAC,IAAI;YAAE,qBAAqBJ;QAAK,CAAC;QAC5C,GAAGN,gBAAgBW,OAAO;QAC1B,GAAGH,QAAQ;QACZK,yBAAyB;YAAEC,QAAQf,kBAAkBY;QAAQ;;AAGnE,EAAC;AAED,eAAeN,KAAI"}
@@ -1 +1 @@
1
- {"version":3,"file":"activeField.d.ts","sourceRoot":"","sources":["../../src/lib/activeField.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,0BAA0B,EAAkB,MAAM,SAAS,CAAA;AAExF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,aASzB,CAAA;AAKD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,EAAE,0BAwBjC,CAAA"}
1
+ {"version":3,"file":"activeField.d.ts","sourceRoot":"","sources":["../../src/lib/activeField.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,0BAA0B,EAAkB,MAAM,SAAS,CAAA;AAExF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,aASzB,CAAA;AAKD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,EAAE,0BAkCjC,CAAA"}
@@ -36,35 +36,44 @@
36
36
  const draft = hasDrafts && data._status === 'draft';
37
37
  const id = originalDoc?.id // undefined on create — then there are no other rows to exclude
38
38
  ;
39
- await req.payload.update({
40
- req,
41
- draft,
42
- collection: collection.slug,
43
- // `active` isn't a field on every collection slug, so the bulk-update `data`
44
- // union has no matching branch under a partial schema cast past it.
45
- data: {
46
- active: false
47
- },
48
- where: {
49
- active: {
50
- equals: true
39
+ try {
40
+ await req.payload.update({
41
+ req,
42
+ draft,
43
+ collection: collection.slug,
44
+ // `active` isn't a field on every collection slug, so the bulk-update `data`
45
+ // union has no matching branch under a partial schema — cast past it.
46
+ data: {
47
+ active: false
51
48
  },
52
- ...id != null ? {
53
- id: {
54
- not_equals: id
55
- }
56
- } : {},
57
- ...hasDrafts ? {
58
- _status: {
59
- equals: draft ? 'draft' : 'published'
60
- }
61
- } : {}
62
- },
63
- context: {
64
- [CASCADE]: true
65
- },
66
- overrideAccess: true
67
- });
49
+ where: {
50
+ active: {
51
+ equals: true
52
+ },
53
+ ...id != null ? {
54
+ id: {
55
+ not_equals: id
56
+ }
57
+ } : {},
58
+ ...hasDrafts ? {
59
+ _status: {
60
+ equals: draft ? 'draft' : 'published'
61
+ }
62
+ } : {}
63
+ },
64
+ context: {
65
+ [CASCADE]: true
66
+ },
67
+ overrideAccess: true
68
+ });
69
+ } catch (err) {
70
+ // Rethrow WITH context: the raw adapter/validation error gives no hint it came from the
71
+ // single-active cascade, leaving the "why did my save fail?" story untold.
72
+ const inner = err instanceof Error ? err.message : String(err);
73
+ throw new Error(`[payload-icons] could not deactivate the other active ${collection.slug} set(s) — the save was rolled back to avoid two active sets. Cause: ${inner}`, {
74
+ cause: err
75
+ });
76
+ }
68
77
  return data;
69
78
  };
70
79
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/activeField.ts"],"sourcesContent":["import type { CheckboxField, CollectionBeforeChangeHook, CollectionSlug } from 'payload'\n\n/**\n * The single-active toggle for the `iconSet` collection. The frontend renders\n * whichever set has `active: true` (in its status lane), so flipping this on one\n * set re-skins every `<Icon>` across the site. The invariant is enforced by\n * {@link enforceSingleActive}.\n */\nexport const activeField: CheckboxField = {\n name: 'active',\n type: 'checkbox',\n defaultValue: false,\n index: true,\n admin: {\n description: 'Render this set across the frontend. Activating a set deactivates the others.',\n style: { maxWidth: '160px', alignSelf: 'center' },\n },\n}\n\n/** Hook-`context` flag so the cascade of deactivations we trigger doesn't recurse. */\nconst CASCADE = 'iconSetEnforceSingleActive'\n\n/**\n * `beforeChange` hook enforcing the single-active invariant, adapted (standalone,\n * no `@pro-laico/core`) from Atomic's `unsetActive`. When a set is saved\n * `active`, every other set in the **same status lane** is flipped `active:\n * false` in the same request/transaction:\n *\n * - The `_status: draft | published` filter scopes the deactivation to the lane\n * being written, so staging a new active set as a *draft* doesn't disturb the\n * live (published) active set — the swap only goes live on publish. (Skipped\n * when the collection has no drafts.)\n * - It runs in `beforeChange` and **rethrows** on failure, so a failed\n * deactivation rolls the whole save back atomically rather than leaving two\n * active sets.\n */\nexport const enforceSingleActive: CollectionBeforeChangeHook = async ({ data, originalDoc, collection, req, context }) => {\n if (!data?.active || context[CASCADE]) return data\n\n const hasDrafts = Boolean((collection.versions as { drafts?: unknown } | undefined)?.drafts)\n const draft = hasDrafts && data._status === 'draft'\n const id = originalDoc?.id // undefined on create — then there are no other rows to exclude\n\n await req.payload.update({\n req,\n draft,\n collection: collection.slug as CollectionSlug,\n // `active` isn't a field on every collection slug, so the bulk-update `data`\n // union has no matching branch under a partial schema — cast past it.\n data: { active: false } as unknown as never,\n where: {\n active: { equals: true },\n ...(id != null ? { id: { not_equals: id } } : {}),\n ...(hasDrafts ? { _status: { equals: draft ? 'draft' : 'published' } } : {}),\n },\n context: { [CASCADE]: true },\n overrideAccess: true,\n })\n\n return data\n}\n"],"names":["activeField","name","type","defaultValue","index","admin","description","style","maxWidth","alignSelf","CASCADE","enforceSingleActive","data","originalDoc","collection","req","context","active","hasDrafts","Boolean","versions","drafts","draft","_status","id","payload","update","slug","where","equals","not_equals","overrideAccess"],"mappings":"AAEA;;;;;CAKC,GACD,OAAO,MAAMA,cAA6B;IACxCC,MAAM;IACNC,MAAM;IACNC,cAAc;IACdC,OAAO;IACPC,OAAO;QACLC,aAAa;QACbC,OAAO;YAAEC,UAAU;YAASC,WAAW;QAAS;IAClD;AACF,EAAC;AAED,oFAAoF,GACpF,MAAMC,UAAU;AAEhB;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMC,sBAAkD,OAAO,EAAEC,IAAI,EAAEC,WAAW,EAAEC,UAAU,EAAEC,GAAG,EAAEC,OAAO,EAAE;IACnH,IAAI,CAACJ,MAAMK,UAAUD,OAAO,CAACN,QAAQ,EAAE,OAAOE;IAE9C,MAAMM,YAAYC,QAASL,WAAWM,QAAQ,EAAuCC;IACrF,MAAMC,QAAQJ,aAAaN,KAAKW,OAAO,KAAK;IAC5C,MAAMC,KAAKX,aAAaW,GAAG,gEAAgE;;IAE3F,MAAMT,IAAIU,OAAO,CAACC,MAAM,CAAC;QACvBX;QACAO;QACAR,YAAYA,WAAWa,IAAI;QAC3B,6EAA6E;QAC7E,sEAAsE;QACtEf,MAAM;YAAEK,QAAQ;QAAM;QACtBW,OAAO;YACLX,QAAQ;gBAAEY,QAAQ;YAAK;YACvB,GAAIL,MAAM,OAAO;gBAAEA,IAAI;oBAAEM,YAAYN;gBAAG;YAAE,IAAI,CAAC,CAAC;YAChD,GAAIN,YAAY;gBAAEK,SAAS;oBAAEM,QAAQP,QAAQ,UAAU;gBAAY;YAAE,IAAI,CAAC,CAAC;QAC7E;QACAN,SAAS;YAAE,CAACN,QAAQ,EAAE;QAAK;QAC3BqB,gBAAgB;IAClB;IAEA,OAAOnB;AACT,EAAC"}
1
+ {"version":3,"sources":["../../src/lib/activeField.ts"],"sourcesContent":["import type { CheckboxField, CollectionBeforeChangeHook, CollectionSlug } from 'payload'\n\n/**\n * The single-active toggle for the `iconSet` collection. The frontend renders\n * whichever set has `active: true` (in its status lane), so flipping this on one\n * set re-skins every `<Icon>` across the site. The invariant is enforced by\n * {@link enforceSingleActive}.\n */\nexport const activeField: CheckboxField = {\n name: 'active',\n type: 'checkbox',\n defaultValue: false,\n index: true,\n admin: {\n description: 'Render this set across the frontend. Activating a set deactivates the others.',\n style: { maxWidth: '160px', alignSelf: 'center' },\n },\n}\n\n/** Hook-`context` flag so the cascade of deactivations we trigger doesn't recurse. */\nconst CASCADE = 'iconSetEnforceSingleActive'\n\n/**\n * `beforeChange` hook enforcing the single-active invariant, adapted (standalone,\n * no `@pro-laico/core`) from Atomic's `unsetActive`. When a set is saved\n * `active`, every other set in the **same status lane** is flipped `active:\n * false` in the same request/transaction:\n *\n * - The `_status: draft | published` filter scopes the deactivation to the lane\n * being written, so staging a new active set as a *draft* doesn't disturb the\n * live (published) active set — the swap only goes live on publish. (Skipped\n * when the collection has no drafts.)\n * - It runs in `beforeChange` and **rethrows** on failure, so a failed\n * deactivation rolls the whole save back atomically rather than leaving two\n * active sets.\n */\nexport const enforceSingleActive: CollectionBeforeChangeHook = async ({ data, originalDoc, collection, req, context }) => {\n if (!data?.active || context[CASCADE]) return data\n\n const hasDrafts = Boolean((collection.versions as { drafts?: unknown } | undefined)?.drafts)\n const draft = hasDrafts && data._status === 'draft'\n const id = originalDoc?.id // undefined on create — then there are no other rows to exclude\n\n try {\n await req.payload.update({\n req,\n draft,\n collection: collection.slug as CollectionSlug,\n // `active` isn't a field on every collection slug, so the bulk-update `data`\n // union has no matching branch under a partial schema — cast past it.\n data: { active: false } as unknown as never,\n where: {\n active: { equals: true },\n ...(id != null ? { id: { not_equals: id } } : {}),\n ...(hasDrafts ? { _status: { equals: draft ? 'draft' : 'published' } } : {}),\n },\n context: { [CASCADE]: true },\n overrideAccess: true,\n })\n } catch (err) {\n // Rethrow WITH context: the raw adapter/validation error gives no hint it came from the\n // single-active cascade, leaving the \"why did my save fail?\" story untold.\n const inner = err instanceof Error ? err.message : String(err)\n throw new Error(\n `[payload-icons] could not deactivate the other active ${collection.slug} set(s) — the save was rolled back to avoid two active sets. Cause: ${inner}`,\n { cause: err },\n )\n }\n\n return data\n}\n"],"names":["activeField","name","type","defaultValue","index","admin","description","style","maxWidth","alignSelf","CASCADE","enforceSingleActive","data","originalDoc","collection","req","context","active","hasDrafts","Boolean","versions","drafts","draft","_status","id","payload","update","slug","where","equals","not_equals","overrideAccess","err","inner","Error","message","String","cause"],"mappings":"AAEA;;;;;CAKC,GACD,OAAO,MAAMA,cAA6B;IACxCC,MAAM;IACNC,MAAM;IACNC,cAAc;IACdC,OAAO;IACPC,OAAO;QACLC,aAAa;QACbC,OAAO;YAAEC,UAAU;YAASC,WAAW;QAAS;IAClD;AACF,EAAC;AAED,oFAAoF,GACpF,MAAMC,UAAU;AAEhB;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMC,sBAAkD,OAAO,EAAEC,IAAI,EAAEC,WAAW,EAAEC,UAAU,EAAEC,GAAG,EAAEC,OAAO,EAAE;IACnH,IAAI,CAACJ,MAAMK,UAAUD,OAAO,CAACN,QAAQ,EAAE,OAAOE;IAE9C,MAAMM,YAAYC,QAASL,WAAWM,QAAQ,EAAuCC;IACrF,MAAMC,QAAQJ,aAAaN,KAAKW,OAAO,KAAK;IAC5C,MAAMC,KAAKX,aAAaW,GAAG,gEAAgE;;IAE3F,IAAI;QACF,MAAMT,IAAIU,OAAO,CAACC,MAAM,CAAC;YACvBX;YACAO;YACAR,YAAYA,WAAWa,IAAI;YAC3B,6EAA6E;YAC7E,sEAAsE;YACtEf,MAAM;gBAAEK,QAAQ;YAAM;YACtBW,OAAO;gBACLX,QAAQ;oBAAEY,QAAQ;gBAAK;gBACvB,GAAIL,MAAM,OAAO;oBAAEA,IAAI;wBAAEM,YAAYN;oBAAG;gBAAE,IAAI,CAAC,CAAC;gBAChD,GAAIN,YAAY;oBAAEK,SAAS;wBAAEM,QAAQP,QAAQ,UAAU;oBAAY;gBAAE,IAAI,CAAC,CAAC;YAC7E;YACAN,SAAS;gBAAE,CAACN,QAAQ,EAAE;YAAK;YAC3BqB,gBAAgB;QAClB;IACF,EAAE,OAAOC,KAAK;QACZ,wFAAwF;QACxF,2EAA2E;QAC3E,MAAMC,QAAQD,eAAeE,QAAQF,IAAIG,OAAO,GAAGC,OAAOJ;QAC1D,MAAM,IAAIE,MACR,CAAC,sDAAsD,EAAEpB,WAAWa,IAAI,CAAC,oEAAoE,EAAEM,OAAO,EACtJ;YAAEI,OAAOL;QAAI;IAEjB;IAEA,OAAOpB;AACT,EAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The single synthetic cache tag the icons surface lives under when
3
+ * `@pro-laico/payload-revalidate` is installed. Two decoupled halves meet on this string:
4
+ *
5
+ * - **Write side** — the `icon` and `iconSet` collections carry it in their
6
+ * `custom.revalidate.extraTags` marker, so revalidatePlugin's auto-attached hooks bust
7
+ * it on every published icon/set write and delete (no import of the revalidate package).
8
+ * - **Read side** — `getIconSvg` applies it via `cacheTag` whenever it runs inside a
9
+ * consumer's `'use cache'` scope (and the revalidate plugin is detected), so a page that
10
+ * bakes rendered SVGs into its cache entry refreshes when icons change.
11
+ *
12
+ * One coarse tag on purpose: the active-set read isn't keyed by a doc id (it queries
13
+ * `active: true`), icon writes are rare, and the whole surface re-materializes in one
14
+ * query — per-doc granularity would buy nothing.
15
+ *
16
+ * Deliberately NOT prefixed with the revalidate plugin's `prefix` option: `extraTags` are
17
+ * busted verbatim, so the read side must apply the identical verbatim string.
18
+ */
19
+ export declare const ICONS_REVALIDATE_TAG = "payload-icons";
20
+ //# sourceMappingURL=revalidateTag.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revalidateTag.d.ts","sourceRoot":"","sources":["../../src/lib/revalidateTag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,oBAAoB,kBAAkB,CAAA"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The single synthetic cache tag the icons surface lives under when
3
+ * `@pro-laico/payload-revalidate` is installed. Two decoupled halves meet on this string:
4
+ *
5
+ * - **Write side** — the `icon` and `iconSet` collections carry it in their
6
+ * `custom.revalidate.extraTags` marker, so revalidatePlugin's auto-attached hooks bust
7
+ * it on every published icon/set write and delete (no import of the revalidate package).
8
+ * - **Read side** — `getIconSvg` applies it via `cacheTag` whenever it runs inside a
9
+ * consumer's `'use cache'` scope (and the revalidate plugin is detected), so a page that
10
+ * bakes rendered SVGs into its cache entry refreshes when icons change.
11
+ *
12
+ * One coarse tag on purpose: the active-set read isn't keyed by a doc id (it queries
13
+ * `active: true`), icon writes are rare, and the whole surface re-materializes in one
14
+ * query — per-doc granularity would buy nothing.
15
+ *
16
+ * Deliberately NOT prefixed with the revalidate plugin's `prefix` option: `extraTags` are
17
+ * busted verbatim, so the read side must apply the identical verbatim string.
18
+ */ export const ICONS_REVALIDATE_TAG = 'payload-icons';
19
+
20
+ //# sourceMappingURL=revalidateTag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/revalidateTag.ts"],"sourcesContent":["/**\n * The single synthetic cache tag the icons surface lives under when\n * `@pro-laico/payload-revalidate` is installed. Two decoupled halves meet on this string:\n *\n * - **Write side** — the `icon` and `iconSet` collections carry it in their\n * `custom.revalidate.extraTags` marker, so revalidatePlugin's auto-attached hooks bust\n * it on every published icon/set write and delete (no import of the revalidate package).\n * - **Read side** — `getIconSvg` applies it via `cacheTag` whenever it runs inside a\n * consumer's `'use cache'` scope (and the revalidate plugin is detected), so a page that\n * bakes rendered SVGs into its cache entry refreshes when icons change.\n *\n * One coarse tag on purpose: the active-set read isn't keyed by a doc id (it queries\n * `active: true`), icon writes are rare, and the whole surface re-materializes in one\n * query — per-doc granularity would buy nothing.\n *\n * Deliberately NOT prefixed with the revalidate plugin's `prefix` option: `extraTags` are\n * busted verbatim, so the read side must apply the identical verbatim string.\n */\nexport const ICONS_REVALIDATE_TAG = 'payload-icons'\n"],"names":["ICONS_REVALIDATE_TAG"],"mappings":"AAAA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,MAAMA,uBAAuB,gBAAe"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pro-laico/payload-icons",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "SVG icons for Payload CMS: an Icon upload collection that optimizes and sanitizes SVGs on save (svgo + viewBox tightening + currentColor theming), a drop-in <Icon name> server component that fetches and inlines the SVG, and one-line declarative seeding via @pro-laico/payload-seed.",
5
5
  "license": "MIT",
6
6
  "repository": {