@brandlab-digital/site-next 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/analytics.d.ts +111 -52
  2. package/dist/analytics.d.ts.map +1 -1
  3. package/dist/analytics.js +281 -153
  4. package/dist/analytics.js.map +1 -1
  5. package/dist/blog.d.ts +4 -2
  6. package/dist/blog.d.ts.map +1 -1
  7. package/dist/blog.js +7 -7
  8. package/dist/blog.js.map +1 -1
  9. package/dist/contact/client-entry.d.ts +2 -0
  10. package/dist/contact/client-entry.d.ts.map +1 -1
  11. package/dist/contact/client-entry.js +1 -0
  12. package/dist/contact/client-entry.js.map +1 -1
  13. package/dist/contact/client.d.ts +12 -22
  14. package/dist/contact/client.d.ts.map +1 -1
  15. package/dist/contact/client.js +28 -21
  16. package/dist/contact/client.js.map +1 -1
  17. package/dist/contact/form-client.d.ts +148 -0
  18. package/dist/contact/form-client.d.ts.map +1 -0
  19. package/dist/contact/form-client.js +234 -0
  20. package/dist/contact/form-client.js.map +1 -0
  21. package/dist/contact/index.d.ts +1 -1
  22. package/dist/contact/index.d.ts.map +1 -1
  23. package/dist/contact/index.js +1 -1
  24. package/dist/contact/index.js.map +1 -1
  25. package/dist/contact/route-handler.d.ts +32 -0
  26. package/dist/contact/route-handler.d.ts.map +1 -0
  27. package/dist/contact/route-handler.js +44 -0
  28. package/dist/contact/route-handler.js.map +1 -0
  29. package/dist/contact/server-entry.d.ts +1 -0
  30. package/dist/contact/server-entry.d.ts.map +1 -1
  31. package/dist/contact/server-entry.js +1 -0
  32. package/dist/contact/server-entry.js.map +1 -1
  33. package/dist/contact/server.d.ts +5 -0
  34. package/dist/contact/server.d.ts.map +1 -1
  35. package/dist/contact/server.js +13 -7
  36. package/dist/contact/server.js.map +1 -1
  37. package/dist/contact/shared.d.ts +4 -0
  38. package/dist/contact/shared.d.ts.map +1 -1
  39. package/dist/contact/shared.js.map +1 -1
  40. package/dist/contact/testing.d.ts +29 -0
  41. package/dist/contact/testing.d.ts.map +1 -1
  42. package/dist/contact/testing.js +27 -0
  43. package/dist/contact/testing.js.map +1 -1
  44. package/dist/google-reviews-client.d.ts +12 -1
  45. package/dist/google-reviews-client.d.ts.map +1 -1
  46. package/dist/google-reviews-client.js +38 -41
  47. package/dist/google-reviews-client.js.map +1 -1
  48. package/dist/google-reviews-ui.d.ts +4 -2
  49. package/dist/google-reviews-ui.d.ts.map +1 -1
  50. package/dist/google-reviews-ui.js +10 -9
  51. package/dist/google-reviews-ui.js.map +1 -1
  52. package/dist/google-tag-plan.d.ts +1 -4
  53. package/dist/google-tag-plan.d.ts.map +1 -1
  54. package/dist/google-tag-plan.js +1 -14
  55. package/dist/google-tag-plan.js.map +1 -1
  56. package/dist/google-tag.d.ts +5 -4
  57. package/dist/google-tag.d.ts.map +1 -1
  58. package/dist/google-tag.js +5 -4
  59. package/dist/google-tag.js.map +1 -1
  60. package/dist/headers.d.ts +21 -28
  61. package/dist/headers.d.ts.map +1 -1
  62. package/dist/headers.js +62 -51
  63. package/dist/headers.js.map +1 -1
  64. package/dist/instrumentation-client.d.ts +21 -1
  65. package/dist/instrumentation-client.d.ts.map +1 -1
  66. package/dist/instrumentation-client.js +62 -7
  67. package/dist/instrumentation-client.js.map +1 -1
  68. package/dist/lead-registry.d.ts +34 -0
  69. package/dist/lead-registry.d.ts.map +1 -0
  70. package/dist/lead-registry.js +30 -0
  71. package/dist/lead-registry.js.map +1 -0
  72. package/dist/lead-reporting.d.ts +37 -0
  73. package/dist/lead-reporting.d.ts.map +1 -0
  74. package/dist/lead-reporting.js +74 -0
  75. package/dist/lead-reporting.js.map +1 -0
  76. package/dist/metadata.d.ts +10 -11
  77. package/dist/metadata.d.ts.map +1 -1
  78. package/dist/metadata.js +17 -19
  79. package/dist/metadata.js.map +1 -1
  80. package/dist/next-config.d.ts +102 -0
  81. package/dist/next-config.d.ts.map +1 -0
  82. package/dist/next-config.js +140 -0
  83. package/dist/next-config.js.map +1 -0
  84. package/dist/robots.d.ts +14 -6
  85. package/dist/robots.d.ts.map +1 -1
  86. package/dist/robots.js +26 -17
  87. package/dist/robots.js.map +1 -1
  88. package/dist/sitemaps.d.ts +34 -0
  89. package/dist/sitemaps.d.ts.map +1 -1
  90. package/dist/sitemaps.js +66 -1
  91. package/dist/sitemaps.js.map +1 -1
  92. package/dist/testing/server-only.d.ts +8 -0
  93. package/dist/testing/server-only.d.ts.map +1 -0
  94. package/dist/testing/server-only.js +8 -0
  95. package/dist/testing/server-only.js.map +1 -0
  96. package/dist/ui-client.d.ts +1 -12
  97. package/dist/ui-client.d.ts.map +1 -1
  98. package/dist/ui-client.js +0 -17
  99. package/dist/ui-client.js.map +1 -1
  100. package/dist/ui.d.ts +22 -2
  101. package/dist/ui.d.ts.map +1 -1
  102. package/dist/ui.js +67 -6
  103. package/dist/ui.js.map +1 -1
  104. package/package.json +16 -3
package/dist/robots.js CHANGED
@@ -1,22 +1,31 @@
1
- import { getAbsoluteUrl } from '@brandlab-digital/site-core/metadata';
2
- import { shouldIndexSite } from '@brandlab-digital/site-core/sitemaps';
3
- export function buildRobotsMetadata(options) {
4
- const shouldIndex = shouldIndexSite({
5
- nodeEnv: options.nodeEnv,
6
- vercelEnv: options.vercelEnv,
7
- allowIndexing: options.allowIndexing,
8
- });
9
- if (!shouldIndex) {
1
+ import { resolveBrandlabDeployment, } from '@brandlab-digital/site-core/site';
2
+ /**
3
+ * Crawlers may index any production build except a Vercel Preview: the real
4
+ * production deployment and a local `next start`. `next dev` and every
5
+ * Preview are closed. There is no environment override.
6
+ */
7
+ export function isBrandlabIndexableDeployment(deployment) {
8
+ return deployment.environment === 'production' ||
9
+ (deployment.environment === 'development' && deployment.productionBuild);
10
+ }
11
+ /**
12
+ * `src/app/robots.ts`: `export default createBrandlabRobots(site)`.
13
+ * The deployment is resolved when Next renders robots.txt.
14
+ */
15
+ export function createBrandlabRobots(site, options = {}) {
16
+ const sitemap = `${site.canonicalUrl}/sitemap.xml`;
17
+ return function robots() {
18
+ const deployment = resolveBrandlabDeployment(options.environment ?? process.env);
19
+ if (!isBrandlabIndexableDeployment(deployment)) {
20
+ return { rules: [{ userAgent: '*', disallow: '/' }] };
21
+ }
22
+ // No `host` directive: it is a non-standard robots.txt extension ignored
23
+ // by Google and Bing; canonical-host consolidation is owned by permanent
24
+ // redirects and canonical URLs.
10
25
  return {
11
- rules: [{ userAgent: '*', disallow: '/' }],
26
+ rules: [{ userAgent: '*', allow: '/' }],
27
+ sitemap,
12
28
  };
13
- }
14
- // No `host` directive: it is a non-standard robots.txt extension ignored
15
- // by Google and Bing; canonical-host consolidation is owned by permanent
16
- // edge redirects + canonical URLs (Brandlab Project Standard §5).
17
- return {
18
- rules: [{ userAgent: '*', allow: '/' }],
19
- sitemap: getAbsoluteUrl(options.siteUrl, '/sitemap.xml'),
20
29
  };
21
30
  }
22
31
  //# sourceMappingURL=robots.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"robots.js","sourceRoot":"","sources":["../src/robots.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAA;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAA;AAOtE,MAAM,UAAU,mBAAmB,CAAC,OAKnC;IACC,MAAM,WAAW,GAAG,eAAe,CAAC;QAClC,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,aAAa,EAAE,OAAO,CAAC,aAAa;KACrC,CAAC,CAAA;IAEF,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO;YACL,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;SAC3C,CAAA;IACH,CAAC;IAED,yEAAyE;IACzE,yEAAyE;IACzE,kEAAkE;IAClE,OAAO;QACL,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;QACvC,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC;KACzD,CAAA;AACH,CAAC","sourcesContent":["import { getAbsoluteUrl } from '@brandlab-digital/site-core/metadata'\nimport { shouldIndexSite } from '@brandlab-digital/site-core/sitemaps'\n\nexport interface BrandlabRobotsMetadata {\n rules: Array<{ userAgent: string; allow?: string; disallow?: string }>\n sitemap?: string\n}\n\nexport function buildRobotsMetadata(options: {\n siteUrl: string\n nodeEnv?: string\n vercelEnv?: string\n allowIndexing?: string\n}): BrandlabRobotsMetadata {\n const shouldIndex = shouldIndexSite({\n nodeEnv: options.nodeEnv,\n vercelEnv: options.vercelEnv,\n allowIndexing: options.allowIndexing,\n })\n\n if (!shouldIndex) {\n return {\n rules: [{ userAgent: '*', disallow: '/' }],\n }\n }\n\n // No `host` directive: it is a non-standard robots.txt extension ignored\n // by Google and Bing; canonical-host consolidation is owned by permanent\n // edge redirects + canonical URLs (Brandlab Project Standard §5).\n return {\n rules: [{ userAgent: '*', allow: '/' }],\n sitemap: getAbsoluteUrl(options.siteUrl, '/sitemap.xml'),\n }\n}\n"]}
1
+ {"version":3,"file":"robots.js","sourceRoot":"","sources":["../src/robots.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,GAI1B,MAAM,kCAAkC,CAAA;AAOzC;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,UAA8B;IAC1E,OAAO,UAAU,CAAC,WAAW,KAAK,YAAY;QAC5C,CAAC,UAAU,CAAC,WAAW,KAAK,aAAa,IAAI,UAAU,CAAC,eAAe,CAAC,CAAA;AAC5E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAkB,EAClB,UAAiD,EAAE;IAEnD,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,YAAY,cAAc,CAAA;IAClD,OAAO,SAAS,MAAM;QACpB,MAAM,UAAU,GAAG,yBAAyB,CAAC,OAAO,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,CAAA;QAChF,IAAI,CAAC,6BAA6B,CAAC,UAAU,CAAC,EAAE,CAAC;YAC/C,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,EAAE,CAAA;QACvD,CAAC;QACD,yEAAyE;QACzE,yEAAyE;QACzE,gCAAgC;QAChC,OAAO;YACL,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;YACvC,OAAO;SACR,CAAA;IACH,CAAC,CAAA;AACH,CAAC","sourcesContent":["import {\n resolveBrandlabDeployment,\n type BrandlabDeployment,\n type BrandlabEnvironment,\n type BrandlabSite,\n} from '@brandlab-digital/site-core/site'\n\nexport interface BrandlabRobotsMetadata {\n rules: Array<{ userAgent: string; allow?: string; disallow?: string }>\n sitemap?: string\n}\n\n/**\n * Crawlers may index any production build except a Vercel Preview: the real\n * production deployment and a local `next start`. `next dev` and every\n * Preview are closed. There is no environment override.\n */\nexport function isBrandlabIndexableDeployment(deployment: BrandlabDeployment) {\n return deployment.environment === 'production' ||\n (deployment.environment === 'development' && deployment.productionBuild)\n}\n\n/**\n * `src/app/robots.ts`: `export default createBrandlabRobots(site)`.\n * The deployment is resolved when Next renders robots.txt.\n */\nexport function createBrandlabRobots(\n site: BrandlabSite,\n options: { environment?: BrandlabEnvironment } = {},\n) {\n const sitemap = `${site.canonicalUrl}/sitemap.xml`\n return function robots(): BrandlabRobotsMetadata {\n const deployment = resolveBrandlabDeployment(options.environment ?? process.env)\n if (!isBrandlabIndexableDeployment(deployment)) {\n return { rules: [{ userAgent: '*', disallow: '/' }] }\n }\n // No `host` directive: it is a non-standard robots.txt extension ignored\n // by Google and Bing; canonical-host consolidation is owned by permanent\n // redirects and canonical URLs.\n return {\n rules: [{ userAgent: '*', allow: '/' }],\n sitemap,\n }\n }\n}\n"]}
@@ -1,6 +1,40 @@
1
+ import type { BrandlabRouteCatalog } from '@brandlab-digital/site-core/routes';
2
+ import type { BrandlabSite } from '@brandlab-digital/site-core/site';
1
3
  import { type SitemapEntry } from '@brandlab-digital/site-core/sitemaps';
2
4
  export declare function sitemapXmlResponse(entries: readonly SitemapEntry[]): Response;
3
5
  export declare function sitemapIndexXmlResponse(paths: readonly string[], options: {
4
6
  absoluteUrl: (path: string) => string;
5
7
  }): Response;
8
+ export type BrandlabSitemapMode = 'single' | 'split';
9
+ export interface BrandlabSitemapRouteOptions {
10
+ /** `split` only when brandlab.config declares `sitemap.mode: 'split'`. */
11
+ mode: BrandlabSitemapMode;
12
+ /** Split mode: URLs per child sitemap (default 1000). */
13
+ maxUrlsPerSitemap?: number;
14
+ }
15
+ export interface BrandlabSitemapChunkRouteContext {
16
+ params: Promise<{
17
+ chunk: string;
18
+ }>;
19
+ }
20
+ export interface BrandlabSitemapRoute {
21
+ readonly mode: BrandlabSitemapMode;
22
+ /** `src/app/sitemap.xml/route.ts`: the URL set, or the sitemap index in split mode. */
23
+ GET(): Response;
24
+ /** Child sitemap paths listed by the index; empty in single mode. */
25
+ chunkPaths(): string[];
26
+ /** `src/app/sitemaps/[chunk]/route.ts` (split mode, with `dynamicParams = false`). */
27
+ chunk: {
28
+ generateStaticParams(): {
29
+ chunk: string;
30
+ }[];
31
+ GET(request: Request, context: BrandlabSitemapChunkRouteContext): Promise<Response>;
32
+ };
33
+ }
34
+ /**
35
+ * The route-catalogue sitemap. `lastmod` comes only from each route's
36
+ * `lastSignificantChange`, never from build time. Entries are resolved once,
37
+ * so an invalid catalogue fails the build.
38
+ */
39
+ export declare function createBrandlabSitemapRoute(routes: BrandlabRouteCatalog, site: BrandlabSite, options?: BrandlabSitemapRouteOptions): BrandlabSitemapRoute;
6
40
  //# sourceMappingURL=sitemaps.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sitemaps.d.ts","sourceRoot":"","sources":["../src/sitemaps.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,sCAAsC,CAAA;AAE7C,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,QAAQ,CAI7E;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,OAAO,EAAE;IAAE,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAAE,GACjD,QAAQ,CAIV"}
1
+ {"version":3,"file":"sitemaps.d.ts","sourceRoot":"","sources":["../src/sitemaps.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAqB,MAAM,oCAAoC,CAAA;AACjG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AACpE,OAAO,EAOL,KAAK,YAAY,EAClB,MAAM,sCAAsC,CAAA;AAE7C,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,QAAQ,CAI7E;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,OAAO,EAAE;IAAE,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAAE,GACjD,QAAQ,CAIV;AAED,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,OAAO,CAAA;AAEpD,MAAM,WAAW,2BAA2B;IAC1C,0EAA0E;IAC1E,IAAI,EAAE,mBAAmB,CAAA;IACzB,yDAAyD;IACzD,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,gCAAgC;IAC/C,MAAM,EAAE,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAA;IAClC,uFAAuF;IACvF,GAAG,IAAI,QAAQ,CAAA;IACf,qEAAqE;IACrE,UAAU,IAAI,MAAM,EAAE,CAAA;IACtB,sFAAsF;IACtF,KAAK,EAAE;QACL,oBAAoB,IAAI;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;QAC3C,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,gCAAgC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;KACpF,CAAA;CACF;AAiCD;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,oBAAoB,EAC5B,IAAI,EAAE,YAAY,EAClB,OAAO,GAAE,2BAAgD,GACxD,oBAAoB,CAsCtB"}
package/dist/sitemaps.js CHANGED
@@ -1,4 +1,5 @@
1
- import { getSitemapXmlHeaders, serializeSitemap, serializeSitemapIndex, } from '@brandlab-digital/site-core/sitemaps';
1
+ import { getAbsoluteUrl } from '@brandlab-digital/site-core/metadata';
2
+ import { buildSitemapEntries, createSitemapChunks, getSitemapXmlHeaders, serializeSitemap, serializeSitemapIndex, } from '@brandlab-digital/site-core/sitemaps';
2
3
  export function sitemapXmlResponse(entries) {
3
4
  return new Response(serializeSitemap(entries), {
4
5
  headers: getSitemapXmlHeaders(),
@@ -9,4 +10,68 @@ export function sitemapIndexXmlResponse(paths, options) {
9
10
  headers: getSitemapXmlHeaders(),
10
11
  });
11
12
  }
13
+ const SPLIT_SITEMAP_FAMILIES = [
14
+ { name: 'services', kinds: ['service'] },
15
+ { name: 'locations', kinds: ['location'] },
16
+ { name: 'service-locations', kinds: ['service-location'] },
17
+ { name: 'blog', kinds: ['blog-post'] },
18
+ { name: 'projects', kinds: ['project'] },
19
+ ];
20
+ const SPLIT_SITEMAP_PREFIX = '/sitemaps';
21
+ const DEFAULT_SPLIT_MAX_URLS = 1_000;
22
+ function splitSitemapChunks(routes, siteUrl, maxUrlsPerSitemap) {
23
+ const familyKinds = new Set(SPLIT_SITEMAP_FAMILIES.flatMap((family) => family.kinds));
24
+ const families = [
25
+ { name: 'core', routes: routes.filter((route) => !familyKinds.has(route.kind)) },
26
+ ...SPLIT_SITEMAP_FAMILIES.map((family) => ({
27
+ name: family.name,
28
+ routes: routes.filter((route) => family.kinds.includes(route.kind)),
29
+ })),
30
+ ];
31
+ return families.flatMap((family) => createSitemapChunks(buildSitemapEntries(family.routes, { siteUrl }), {
32
+ pathPrefix: `${SPLIT_SITEMAP_PREFIX}/${family.name}`,
33
+ maxUrlsPerSitemap,
34
+ }));
35
+ }
36
+ /**
37
+ * The route-catalogue sitemap. `lastmod` comes only from each route's
38
+ * `lastSignificantChange`, never from build time. Entries are resolved once,
39
+ * so an invalid catalogue fails the build.
40
+ */
41
+ export function createBrandlabSitemapRoute(routes, site, options = { mode: 'single' }) {
42
+ if (options.mode !== 'single' && options.mode !== 'split') {
43
+ throw new Error('Sitemap mode must be "single" or "split".');
44
+ }
45
+ if (options.mode === 'single' && options.maxUrlsPerSitemap !== undefined) {
46
+ throw new Error('maxUrlsPerSitemap applies only to split sitemaps.');
47
+ }
48
+ const entries = buildSitemapEntries(routes, { siteUrl: site.canonicalUrl });
49
+ const chunks = options.mode === 'split'
50
+ ? splitSitemapChunks(routes, site.canonicalUrl, options.maxUrlsPerSitemap ?? DEFAULT_SPLIT_MAX_URLS)
51
+ : [];
52
+ const chunkByFileName = new Map(chunks.map((chunk) => [chunk.path.slice(SPLIT_SITEMAP_PREFIX.length + 1), chunk]));
53
+ const absoluteUrl = (path) => getAbsoluteUrl(site.canonicalUrl, path);
54
+ return Object.freeze({
55
+ mode: options.mode,
56
+ GET() {
57
+ return options.mode === 'split'
58
+ ? sitemapIndexXmlResponse(chunks.map((chunk) => chunk.path), { absoluteUrl })
59
+ : sitemapXmlResponse(entries);
60
+ },
61
+ chunkPaths() {
62
+ return chunks.map((chunk) => chunk.path);
63
+ },
64
+ chunk: Object.freeze({
65
+ generateStaticParams() {
66
+ return [...chunkByFileName.keys()].map((chunk) => ({ chunk }));
67
+ },
68
+ async GET(_request, context) {
69
+ const found = chunkByFileName.get((await context.params).chunk);
70
+ return found
71
+ ? sitemapXmlResponse(found.entries)
72
+ : new Response('Not found', { status: 404 });
73
+ },
74
+ }),
75
+ });
76
+ }
12
77
  //# sourceMappingURL=sitemaps.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sitemaps.js","sourceRoot":"","sources":["../src/sitemaps.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,qBAAqB,GAEtB,MAAM,sCAAsC,CAAA;AAE7C,MAAM,UAAU,kBAAkB,CAAC,OAAgC;IACjE,OAAO,IAAI,QAAQ,CAAC,gBAAgB,CAAC,OAAO,CAAC,EAAE;QAC7C,OAAO,EAAE,oBAAoB,EAAE;KAChC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,KAAwB,EACxB,OAAkD;IAElD,OAAO,IAAI,QAAQ,CAAC,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE;QACzD,OAAO,EAAE,oBAAoB,EAAE;KAChC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import {\n getSitemapXmlHeaders,\n serializeSitemap,\n serializeSitemapIndex,\n type SitemapEntry,\n} from '@brandlab-digital/site-core/sitemaps'\n\nexport function sitemapXmlResponse(entries: readonly SitemapEntry[]): Response {\n return new Response(serializeSitemap(entries), {\n headers: getSitemapXmlHeaders(),\n })\n}\n\nexport function sitemapIndexXmlResponse(\n paths: readonly string[],\n options: { absoluteUrl: (path: string) => string },\n): Response {\n return new Response(serializeSitemapIndex(paths, options), {\n headers: getSitemapXmlHeaders(),\n })\n}\n"]}
1
+ {"version":3,"file":"sitemaps.js","sourceRoot":"","sources":["../src/sitemaps.ts"],"names":[],"mappings":"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","sourcesContent":["import { getAbsoluteUrl } from '@brandlab-digital/site-core/metadata'\nimport type { BrandlabRouteCatalog, BrandlabRouteKind } from '@brandlab-digital/site-core/routes'\nimport type { BrandlabSite } from '@brandlab-digital/site-core/site'\nimport {\n buildSitemapEntries,\n createSitemapChunks,\n getSitemapXmlHeaders,\n serializeSitemap,\n serializeSitemapIndex,\n type SitemapChunk,\n type SitemapEntry,\n} from '@brandlab-digital/site-core/sitemaps'\n\nexport function sitemapXmlResponse(entries: readonly SitemapEntry[]): Response {\n return new Response(serializeSitemap(entries), {\n headers: getSitemapXmlHeaders(),\n })\n}\n\nexport function sitemapIndexXmlResponse(\n paths: readonly string[],\n options: { absoluteUrl: (path: string) => string },\n): Response {\n return new Response(serializeSitemapIndex(paths, options), {\n headers: getSitemapXmlHeaders(),\n })\n}\n\nexport type BrandlabSitemapMode = 'single' | 'split'\n\nexport interface BrandlabSitemapRouteOptions {\n /** `split` only when brandlab.config declares `sitemap.mode: 'split'`. */\n mode: BrandlabSitemapMode\n /** Split mode: URLs per child sitemap (default 1000). */\n maxUrlsPerSitemap?: number\n}\n\nexport interface BrandlabSitemapChunkRouteContext {\n params: Promise<{ chunk: string }>\n}\n\nexport interface BrandlabSitemapRoute {\n readonly mode: BrandlabSitemapMode\n /** `src/app/sitemap.xml/route.ts`: the URL set, or the sitemap index in split mode. */\n GET(): Response\n /** Child sitemap paths listed by the index; empty in single mode. */\n chunkPaths(): string[]\n /** `src/app/sitemaps/[chunk]/route.ts` (split mode, with `dynamicParams = false`). */\n chunk: {\n generateStaticParams(): { chunk: string }[]\n GET(request: Request, context: BrandlabSitemapChunkRouteContext): Promise<Response>\n }\n}\n\nconst SPLIT_SITEMAP_FAMILIES: readonly { name: string; kinds: readonly BrandlabRouteKind[] }[] = [\n { name: 'services', kinds: ['service'] },\n { name: 'locations', kinds: ['location'] },\n { name: 'service-locations', kinds: ['service-location'] },\n { name: 'blog', kinds: ['blog-post'] },\n { name: 'projects', kinds: ['project'] },\n]\nconst SPLIT_SITEMAP_PREFIX = '/sitemaps'\nconst DEFAULT_SPLIT_MAX_URLS = 1_000\n\nfunction splitSitemapChunks(\n routes: BrandlabRouteCatalog,\n siteUrl: string,\n maxUrlsPerSitemap: number,\n): SitemapChunk[] {\n const familyKinds = new Set(SPLIT_SITEMAP_FAMILIES.flatMap((family) => family.kinds))\n const families = [\n { name: 'core', routes: routes.filter((route) => !familyKinds.has(route.kind)) },\n ...SPLIT_SITEMAP_FAMILIES.map((family) => ({\n name: family.name,\n routes: routes.filter((route) => family.kinds.includes(route.kind)),\n })),\n ]\n return families.flatMap((family) =>\n createSitemapChunks(buildSitemapEntries(family.routes, { siteUrl }), {\n pathPrefix: `${SPLIT_SITEMAP_PREFIX}/${family.name}`,\n maxUrlsPerSitemap,\n }),\n )\n}\n\n/**\n * The route-catalogue sitemap. `lastmod` comes only from each route's\n * `lastSignificantChange`, never from build time. Entries are resolved once,\n * so an invalid catalogue fails the build.\n */\nexport function createBrandlabSitemapRoute(\n routes: BrandlabRouteCatalog,\n site: BrandlabSite,\n options: BrandlabSitemapRouteOptions = { mode: 'single' },\n): BrandlabSitemapRoute {\n if (options.mode !== 'single' && options.mode !== 'split') {\n throw new Error('Sitemap mode must be \"single\" or \"split\".')\n }\n if (options.mode === 'single' && options.maxUrlsPerSitemap !== undefined) {\n throw new Error('maxUrlsPerSitemap applies only to split sitemaps.')\n }\n const entries = buildSitemapEntries(routes, { siteUrl: site.canonicalUrl })\n const chunks = options.mode === 'split'\n ? splitSitemapChunks(routes, site.canonicalUrl, options.maxUrlsPerSitemap ?? DEFAULT_SPLIT_MAX_URLS)\n : []\n const chunkByFileName = new Map(\n chunks.map((chunk) => [chunk.path.slice(SPLIT_SITEMAP_PREFIX.length + 1), chunk]),\n )\n const absoluteUrl = (path: string) => getAbsoluteUrl(site.canonicalUrl, path)\n\n return Object.freeze({\n mode: options.mode,\n GET() {\n return options.mode === 'split'\n ? sitemapIndexXmlResponse(chunks.map((chunk) => chunk.path), { absoluteUrl })\n : sitemapXmlResponse(entries)\n },\n chunkPaths() {\n return chunks.map((chunk) => chunk.path)\n },\n chunk: Object.freeze({\n generateStaticParams() {\n return [...chunkByFileName.keys()].map((chunk) => ({ chunk }))\n },\n async GET(_request: Request, context: BrandlabSitemapChunkRouteContext) {\n const found = chunkByFileName.get((await context.params).chunk)\n return found\n ? sitemapXmlResponse(found.entries)\n : new Response('Not found', { status: 404 })\n },\n }),\n })\n}\n"]}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Empty stand-in for the `server-only` guard in Vitest, where server modules
3
+ * run outside Next's react-server condition. Alias it in vitest.config.ts:
4
+ * `resolve: { alias: { 'server-only': '@brandlab-digital/site-next/testing/server-only' } }`.
5
+ * Never import it from application code.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=server-only.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-only.d.ts","sourceRoot":"","sources":["../../src/testing/server-only.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAA"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Empty stand-in for the `server-only` guard in Vitest, where server modules
3
+ * run outside Next's react-server condition. Alias it in vitest.config.ts:
4
+ * `resolve: { alias: { 'server-only': '@brandlab-digital/site-next/testing/server-only' } }`.
5
+ * Never import it from application code.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=server-only.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-only.js","sourceRoot":"","sources":["../../src/testing/server-only.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAA","sourcesContent":["/**\n * Empty stand-in for the `server-only` guard in Vitest, where server modules\n * run outside Next's react-server condition. Alias it in vitest.config.ts:\n * `resolve: { alias: { 'server-only': '@brandlab-digital/site-next/testing/server-only' } }`.\n * Never import it from application code.\n */\nexport {}\n"]}
@@ -1,5 +1,4 @@
1
- import { type AnchorHTMLAttributes, type DialogHTMLAttributes, type ReactNode, type RefObject } from 'react';
2
- import { type PhoneCallPlacement } from './analytics.js';
1
+ import { type DialogHTMLAttributes, type ReactNode, type RefObject } from 'react';
3
2
  export interface HeadlessDialogProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, 'open' | 'onClose'> {
4
3
  open: boolean;
5
4
  onClose: () => void;
@@ -10,16 +9,6 @@ export interface HeadlessDialogProps extends Omit<DialogHTMLAttributes<HTMLDialo
10
9
  children: ReactNode;
11
10
  }
12
11
  export declare function HeadlessDialog({ open, onClose, labelledBy, describedBy, initialFocusRef, returnFocusRef, children, ...props }: HeadlessDialogProps): import("react/jsx-runtime").JSX.Element;
13
- export interface TrackedTelephoneLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'onClick'> {
14
- phoneE164: string;
15
- placement: PhoneCallPlacement;
16
- onTrack: (input: {
17
- event: 'phone_link_click';
18
- placement: PhoneCallPlacement;
19
- }) => void;
20
- children: ReactNode;
21
- }
22
- export declare function TrackedTelephoneLink({ phoneE164, placement, onTrack, children, ...props }: TrackedTelephoneLinkProps): import("react/jsx-runtime").JSX.Element;
23
12
  export interface GalleryLightboxItem {
24
13
  id: string;
25
14
  alt: string;
@@ -1 +1 @@
1
- {"version":3,"file":"ui-client.d.ts","sourceRoot":"","sources":["../src/ui-client.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EAEzB,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AACd,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,gBAAgB,CAAA;AAEvB,MAAM,WAAW,mBACf,SAAQ,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzE,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAC/C,cAAc,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAC9C,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,OAAO,EACP,UAAU,EACV,WAAW,EACX,eAAe,EACf,cAAc,EACd,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,mBAAmB,2CA2CrB;AAED,MAAM,WAAW,yBACf,SAAQ,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzE,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,kBAAkB,CAAA;IAC7B,OAAO,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,kBAAkB,CAAC;QAAC,SAAS,EAAE,kBAAkB,CAAA;KAAE,KAAK,IAAI,CAAA;IACtF,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,oBAAoB,CAAC,EACnC,SAAS,EACT,SAAS,EACT,OAAO,EACP,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,yBAAyB,2CAsB3B;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,oBAAoB,CAAC,IAAI,SAAS,mBAAmB;IACpE,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,SAAS,IAAI,EAAE,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,SAAS,CAAA;IAChB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS,CAAA;IACrC,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,eAAe,CAAC,IAAI,SAAS,mBAAmB,EAAE,EAChE,IAAI,EACJ,KAAK,EACL,YAAgB,EAChB,OAAO,EACP,KAAK,EACL,OAAO,EACP,UAAU,EACV,aAAa,EACb,SAAS,EACT,UAAU,GACX,EAAE,oBAAoB,CAAC,IAAI,CAAC,2CAgD5B;AAED,wBAAgB,uBAAuB,YAUtC;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,uBAAuB,QAuBxF"}
1
+ {"version":3,"file":"ui-client.d.ts","sourceRoot":"","sources":["../src/ui-client.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,oBAAoB,EAEzB,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AAEd,MAAM,WAAW,mBACf,SAAQ,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzE,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAC/C,cAAc,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAC9C,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,OAAO,EACP,UAAU,EACV,WAAW,EACX,eAAe,EACf,cAAc,EACd,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,mBAAmB,2CA2CrB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,oBAAoB,CAAC,IAAI,SAAS,mBAAmB;IACpE,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,SAAS,IAAI,EAAE,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,SAAS,CAAA;IAChB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS,CAAA;IACrC,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,eAAe,CAAC,IAAI,SAAS,mBAAmB,EAAE,EAChE,IAAI,EACJ,KAAK,EACL,YAAgB,EAChB,OAAO,EACP,KAAK,EACL,OAAO,EACP,UAAU,EACV,aAAa,EACb,SAAS,EACT,UAAU,GACX,EAAE,oBAAoB,CAAC,IAAI,CAAC,2CAgD5B;AAED,wBAAgB,uBAAuB,YAUtC;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,uBAAuB,QAuBxF"}
package/dist/ui-client.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useState, } from 'react';
4
- import { PHONE_CALL_PLACEMENTS, } from './analytics.js';
5
4
  export function HeadlessDialog({ open, onClose, labelledBy, describedBy, initialFocusRef, returnFocusRef, children, ...props }) {
6
5
  const dialogRef = useRef(null);
7
6
  const previouslyFocused = useRef(null);
@@ -37,22 +36,6 @@ export function HeadlessDialog({ open, onClose, labelledBy, describedBy, initial
37
36
  onClose();
38
37
  }, onClose: onClose, ref: dialogRef, children: children }));
39
38
  }
40
- export function TrackedTelephoneLink({ phoneE164, placement, onTrack, children, ...props }) {
41
- if (!/^\+[1-9]\d{7,14}$/.test(phoneE164)) {
42
- throw new Error('TrackedTelephoneLink requires an E.164 phone number.');
43
- }
44
- if (!PHONE_CALL_PLACEMENTS.includes(placement)) {
45
- throw new Error('TrackedTelephoneLink requires a canonical phone-call placement.');
46
- }
47
- return (_jsx("a", { ...props, href: `tel:${phoneE164}`, onClick: () => {
48
- try {
49
- onTrack({ event: 'phone_link_click', placement });
50
- }
51
- catch {
52
- // Tracking must never block the telephone action.
53
- }
54
- }, children: children }));
55
- }
56
39
  export function GalleryLightbox({ open, items, initialIndex = 0, titleId, title, onClose, renderItem, previousLabel, nextLabel, closeLabel, }) {
57
40
  const [index, setIndex] = useState(initialIndex);
58
41
  const closeRef = useRef(null);
@@ -1 +1 @@
1
- {"version":3,"file":"ui-client.js","sourceRoot":"","sources":["../src/ui-client.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport {\n useEffect,\n useRef,\n useState,\n type AnchorHTMLAttributes,\n type DialogHTMLAttributes,\n type KeyboardEvent,\n type ReactNode,\n type RefObject,\n} from 'react'\nimport {\n PHONE_CALL_PLACEMENTS,\n type PhoneCallPlacement,\n} from './analytics.js'\n\nexport interface HeadlessDialogProps\n extends Omit<DialogHTMLAttributes<HTMLDialogElement>, 'open' | 'onClose'> {\n open: boolean\n onClose: () => void\n labelledBy: string\n describedBy?: string\n initialFocusRef?: RefObject<HTMLElement | null>\n returnFocusRef?: RefObject<HTMLElement | null>\n children: ReactNode\n}\n\nexport function HeadlessDialog({\n open,\n onClose,\n labelledBy,\n describedBy,\n initialFocusRef,\n returnFocusRef,\n children,\n ...props\n}: HeadlessDialogProps) {\n const dialogRef = useRef<HTMLDialogElement>(null)\n const previouslyFocused = useRef<HTMLElement | null>(null)\n\n useEffect(() => {\n const dialog = dialogRef.current\n if (!dialog) return\n if (open) {\n previouslyFocused.current = document.activeElement instanceof HTMLElement\n ? document.activeElement\n : null\n if (!dialog.open) {\n if (typeof dialog.showModal === 'function') dialog.showModal()\n else dialog.setAttribute('open', '')\n }\n const focusTarget = initialFocusRef?.current ?? dialog\n queueMicrotask(() => focusTarget.focus())\n return\n }\n if (dialog.open) {\n if (typeof dialog.close === 'function') dialog.close()\n else dialog.removeAttribute('open')\n }\n const returnTarget = returnFocusRef?.current ?? previouslyFocused.current\n queueMicrotask(() => returnTarget?.focus())\n }, [initialFocusRef, open, returnFocusRef])\n\n return (\n <dialog\n {...props}\n aria-describedby={describedBy}\n aria-labelledby={labelledBy}\n aria-modal=\"true\"\n onCancel={(event) => {\n event.preventDefault()\n onClose()\n }}\n onClose={onClose}\n ref={dialogRef}\n >\n {children}\n </dialog>\n )\n}\n\nexport interface TrackedTelephoneLinkProps\n extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'onClick'> {\n phoneE164: string\n placement: PhoneCallPlacement\n onTrack: (input: { event: 'phone_link_click'; placement: PhoneCallPlacement }) => void\n children: ReactNode\n}\n\nexport function TrackedTelephoneLink({\n phoneE164,\n placement,\n onTrack,\n children,\n ...props\n}: TrackedTelephoneLinkProps) {\n if (!/^\\+[1-9]\\d{7,14}$/.test(phoneE164)) {\n throw new Error('TrackedTelephoneLink requires an E.164 phone number.')\n }\n if (!PHONE_CALL_PLACEMENTS.includes(placement)) {\n throw new Error('TrackedTelephoneLink requires a canonical phone-call placement.')\n }\n return (\n <a\n {...props}\n href={`tel:${phoneE164}`}\n onClick={() => {\n try {\n onTrack({ event: 'phone_link_click', placement })\n } catch {\n // Tracking must never block the telephone action.\n }\n }}\n >\n {children}\n </a>\n )\n}\n\nexport interface GalleryLightboxItem {\n id: string\n alt: string\n}\n\nexport interface GalleryLightboxProps<Item extends GalleryLightboxItem> {\n open: boolean\n items: readonly Item[]\n initialIndex?: number\n titleId: string\n title: ReactNode\n onClose: () => void\n renderItem: (item: Item) => ReactNode\n previousLabel: string\n nextLabel: string\n closeLabel: string\n}\n\nexport function GalleryLightbox<Item extends GalleryLightboxItem>({\n open,\n items,\n initialIndex = 0,\n titleId,\n title,\n onClose,\n renderItem,\n previousLabel,\n nextLabel,\n closeLabel,\n}: GalleryLightboxProps<Item>) {\n const [index, setIndex] = useState(initialIndex)\n const closeRef = useRef<HTMLButtonElement>(null)\n\n useEffect(() => {\n if (open) setIndex(Math.min(Math.max(initialIndex, 0), Math.max(items.length - 1, 0)))\n }, [initialIndex, items.length, open])\n\n const move = (offset: number) => {\n if (items.length < 2) return\n setIndex((current) => (current + offset + items.length) % items.length)\n }\n const onKeyDown = (event: KeyboardEvent<HTMLDialogElement>) => {\n if (event.key === 'ArrowLeft') {\n event.preventDefault()\n move(-1)\n } else if (event.key === 'ArrowRight') {\n event.preventDefault()\n move(1)\n }\n }\n const item = items[index]\n\n return (\n <HeadlessDialog\n initialFocusRef={closeRef}\n labelledBy={titleId}\n onClose={onClose}\n onKeyDown={onKeyDown}\n open={open}\n >\n <h2 id={titleId}>{title}</h2>\n <button aria-label={closeLabel} onClick={onClose} ref={closeRef} type=\"button\">\n {closeLabel}\n </button>\n {item ? <figure key={item.id}>{renderItem(item)}<figcaption>{item.alt}</figcaption></figure> : null}\n {items.length > 1 ? (\n <nav aria-labelledby={titleId}>\n <button aria-label={previousLabel} onClick={() => move(-1)} type=\"button\">\n {previousLabel}\n </button>\n <button aria-label={nextLabel} onClick={() => move(1)} type=\"button\">\n {nextLabel}\n </button>\n </nav>\n ) : null}\n </HeadlessDialog>\n )\n}\n\nexport function usePrefersReducedMotion() {\n const [reduced, setReduced] = useState(false)\n useEffect(() => {\n const query = window.matchMedia('(prefers-reduced-motion: reduce)')\n const update = () => setReduced(query.matches)\n update()\n query.addEventListener('change', update)\n return () => query.removeEventListener('change', update)\n }, [])\n return reduced\n}\n\nexport interface HeadlessErrorFocusProps {\n active: boolean\n formId: string\n summaryId: string\n}\n\n/** Focuses the error summary, falling back to the first invalid field. */\nexport function HeadlessErrorFocus({ active, formId, summaryId }: HeadlessErrorFocusProps) {\n useEffect(() => {\n if (!active) return\n let cancelled = false\n queueMicrotask(() => {\n if (cancelled) return\n const form = document.getElementById(formId)\n if (!(form instanceof HTMLFormElement)) return\n const summary = document.getElementById(summaryId)\n const target = summary instanceof HTMLElement && form.contains(summary)\n ? summary\n : form.querySelector<HTMLElement>([\n 'input[aria-invalid=\"true\"]:not([disabled])',\n 'select[aria-invalid=\"true\"]:not([disabled])',\n 'textarea[aria-invalid=\"true\"]:not([disabled])',\n 'button[aria-invalid=\"true\"]:not([disabled])',\n '[aria-invalid=\"true\"][tabindex]:not([aria-disabled=\"true\"])',\n ].join(','))\n target?.focus()\n })\n return () => { cancelled = true }\n }, [active, formId, summaryId])\n return null\n}\n"]}
1
+ {"version":3,"file":"ui-client.js","sourceRoot":"","sources":["../src/ui-client.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport {\n useEffect,\n useRef,\n useState,\n type DialogHTMLAttributes,\n type KeyboardEvent,\n type ReactNode,\n type RefObject,\n} from 'react'\n\nexport interface HeadlessDialogProps\n extends Omit<DialogHTMLAttributes<HTMLDialogElement>, 'open' | 'onClose'> {\n open: boolean\n onClose: () => void\n labelledBy: string\n describedBy?: string\n initialFocusRef?: RefObject<HTMLElement | null>\n returnFocusRef?: RefObject<HTMLElement | null>\n children: ReactNode\n}\n\nexport function HeadlessDialog({\n open,\n onClose,\n labelledBy,\n describedBy,\n initialFocusRef,\n returnFocusRef,\n children,\n ...props\n}: HeadlessDialogProps) {\n const dialogRef = useRef<HTMLDialogElement>(null)\n const previouslyFocused = useRef<HTMLElement | null>(null)\n\n useEffect(() => {\n const dialog = dialogRef.current\n if (!dialog) return\n if (open) {\n previouslyFocused.current = document.activeElement instanceof HTMLElement\n ? document.activeElement\n : null\n if (!dialog.open) {\n if (typeof dialog.showModal === 'function') dialog.showModal()\n else dialog.setAttribute('open', '')\n }\n const focusTarget = initialFocusRef?.current ?? dialog\n queueMicrotask(() => focusTarget.focus())\n return\n }\n if (dialog.open) {\n if (typeof dialog.close === 'function') dialog.close()\n else dialog.removeAttribute('open')\n }\n const returnTarget = returnFocusRef?.current ?? previouslyFocused.current\n queueMicrotask(() => returnTarget?.focus())\n }, [initialFocusRef, open, returnFocusRef])\n\n return (\n <dialog\n {...props}\n aria-describedby={describedBy}\n aria-labelledby={labelledBy}\n aria-modal=\"true\"\n onCancel={(event) => {\n event.preventDefault()\n onClose()\n }}\n onClose={onClose}\n ref={dialogRef}\n >\n {children}\n </dialog>\n )\n}\n\nexport interface GalleryLightboxItem {\n id: string\n alt: string\n}\n\nexport interface GalleryLightboxProps<Item extends GalleryLightboxItem> {\n open: boolean\n items: readonly Item[]\n initialIndex?: number\n titleId: string\n title: ReactNode\n onClose: () => void\n renderItem: (item: Item) => ReactNode\n previousLabel: string\n nextLabel: string\n closeLabel: string\n}\n\nexport function GalleryLightbox<Item extends GalleryLightboxItem>({\n open,\n items,\n initialIndex = 0,\n titleId,\n title,\n onClose,\n renderItem,\n previousLabel,\n nextLabel,\n closeLabel,\n}: GalleryLightboxProps<Item>) {\n const [index, setIndex] = useState(initialIndex)\n const closeRef = useRef<HTMLButtonElement>(null)\n\n useEffect(() => {\n if (open) setIndex(Math.min(Math.max(initialIndex, 0), Math.max(items.length - 1, 0)))\n }, [initialIndex, items.length, open])\n\n const move = (offset: number) => {\n if (items.length < 2) return\n setIndex((current) => (current + offset + items.length) % items.length)\n }\n const onKeyDown = (event: KeyboardEvent<HTMLDialogElement>) => {\n if (event.key === 'ArrowLeft') {\n event.preventDefault()\n move(-1)\n } else if (event.key === 'ArrowRight') {\n event.preventDefault()\n move(1)\n }\n }\n const item = items[index]\n\n return (\n <HeadlessDialog\n initialFocusRef={closeRef}\n labelledBy={titleId}\n onClose={onClose}\n onKeyDown={onKeyDown}\n open={open}\n >\n <h2 id={titleId}>{title}</h2>\n <button aria-label={closeLabel} onClick={onClose} ref={closeRef} type=\"button\">\n {closeLabel}\n </button>\n {item ? <figure key={item.id}>{renderItem(item)}<figcaption>{item.alt}</figcaption></figure> : null}\n {items.length > 1 ? (\n <nav aria-labelledby={titleId}>\n <button aria-label={previousLabel} onClick={() => move(-1)} type=\"button\">\n {previousLabel}\n </button>\n <button aria-label={nextLabel} onClick={() => move(1)} type=\"button\">\n {nextLabel}\n </button>\n </nav>\n ) : null}\n </HeadlessDialog>\n )\n}\n\nexport function usePrefersReducedMotion() {\n const [reduced, setReduced] = useState(false)\n useEffect(() => {\n const query = window.matchMedia('(prefers-reduced-motion: reduce)')\n const update = () => setReduced(query.matches)\n update()\n query.addEventListener('change', update)\n return () => query.removeEventListener('change', update)\n }, [])\n return reduced\n}\n\nexport interface HeadlessErrorFocusProps {\n active: boolean\n formId: string\n summaryId: string\n}\n\n/** Focuses the error summary, falling back to the first invalid field. */\nexport function HeadlessErrorFocus({ active, formId, summaryId }: HeadlessErrorFocusProps) {\n useEffect(() => {\n if (!active) return\n let cancelled = false\n queueMicrotask(() => {\n if (cancelled) return\n const form = document.getElementById(formId)\n if (!(form instanceof HTMLFormElement)) return\n const summary = document.getElementById(summaryId)\n const target = summary instanceof HTMLElement && form.contains(summary)\n ? summary\n : form.querySelector<HTMLElement>([\n 'input[aria-invalid=\"true\"]:not([disabled])',\n 'select[aria-invalid=\"true\"]:not([disabled])',\n 'textarea[aria-invalid=\"true\"]:not([disabled])',\n 'button[aria-invalid=\"true\"]:not([disabled])',\n '[aria-invalid=\"true\"][tabindex]:not([aria-disabled=\"true\"])',\n ].join(','))\n target?.focus()\n })\n return () => { cancelled = true }\n }, [active, formId, summaryId])\n return null\n}\n"]}
package/dist/ui.d.ts CHANGED
@@ -74,8 +74,28 @@ export type SafeRichTextNode = {
74
74
  } | {
75
75
  type: 'line-break';
76
76
  };
77
- export interface SafeRichTextProps extends HTMLAttributes<HTMLDivElement> {
77
+ export type SafeRichTextElement = 'div' | 'p' | 'span';
78
+ export interface SafeRichTextProps extends HTMLAttributes<HTMLElement> {
78
79
  nodes: readonly SafeRichTextNode[];
80
+ /**
81
+ * The wrapper element. `div` (default) holds block content: paragraphs and
82
+ * lists. `p` and `span` hold inline content only (text, emphasis, links and
83
+ * line breaks), so the output has no block element and can sit where only
84
+ * phrasing content is allowed, such as inside a button; a block node under
85
+ * them throws.
86
+ */
87
+ as?: SafeRichTextElement;
88
+ /** Class for every link, rendered after `href`. */
89
+ linkClassName?: string;
79
90
  }
80
- export declare function SafeRichText({ nodes, ...props }: SafeRichTextProps): import("react/jsx-runtime").JSX.Element;
91
+ export declare function SafeRichText({ nodes, as: Element, linkClassName, ...props }: SafeRichTextProps): import("react/jsx-runtime").JSX.Element;
92
+ /**
93
+ * Inline rich-text markup as nodes, for content written as short HTML
94
+ * strings: `<strong>` and `<b>` become strong text, `<a href>` becomes a link
95
+ * when its href passes sanitizeRichTextHref (otherwise its text stays as plain
96
+ * text), every other tag is dropped and text is kept exactly as written
97
+ * (React escapes it). Nothing is ever rendered as HTML. Strong text holds text
98
+ * only; markup nested inside it throws.
99
+ */
100
+ export declare function parseInlineRichText(markup: string): SafeRichTextNode[];
81
101
  //# sourceMappingURL=ui.d.ts.map
package/dist/ui.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"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"}
package/dist/ui.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { assertSafeRichTextHref } from '@brandlab-digital/site-core/rich-text';
2
+ import { assertSafeRichTextHref, sanitizeRichTextHref } from '@brandlab-digital/site-core/rich-text';
3
3
  export function HeadlessSection({ heading, headingId, headingClassName, contentClassName, children, ...sectionProps }) {
4
4
  return (_jsxs("section", { ...sectionProps, "aria-labelledby": headingId, children: [_jsx("h2", { className: headingClassName, id: headingId, children: heading }), _jsx("div", { className: contentClassName, children: children })] }));
5
5
  }
@@ -35,7 +35,17 @@ function assertHtmlIdentifier(value, label) {
35
35
  }
36
36
  return value;
37
37
  }
38
- function renderSafeRichTextNode(node, key) {
38
+ const INLINE_RICH_TEXT_TYPES = new Set(['text', 'link', 'line-break']);
39
+ function assertInlineRichText(nodes, element) {
40
+ for (const node of nodes) {
41
+ if (!INLINE_RICH_TEXT_TYPES.has(node.type)) {
42
+ throw new Error(`SafeRichText as="${element}" renders inline content only; "${node.type}" is a block node.`);
43
+ }
44
+ if (node.type === 'link')
45
+ assertInlineRichText(node.children, element);
46
+ }
47
+ }
48
+ function renderSafeRichTextNode(node, key, linkClassName) {
39
49
  if (node.type === 'text') {
40
50
  if (node.emphasis === 'strong')
41
51
  return _jsx("strong", { children: node.value }, key);
@@ -45,7 +55,7 @@ function renderSafeRichTextNode(node, key) {
45
55
  }
46
56
  if (node.type === 'line-break')
47
57
  return _jsx("br", {}, key);
48
- const children = node.children.map((child, index) => renderSafeRichTextNode(child, `${key}-${index}`));
58
+ const children = node.children.map((child, index) => renderSafeRichTextNode(child, `${key}-${index}`, linkClassName));
49
59
  if (node.type === 'paragraph')
50
60
  return _jsx("p", { children: children }, key);
51
61
  if (node.type === 'list-item')
@@ -57,9 +67,60 @@ function renderSafeRichTextNode(node, key) {
57
67
  if (node.type !== 'link')
58
68
  throw new Error('Unsupported rich text node.');
59
69
  const href = assertSafeRichTextHref(node.href);
60
- return (_jsx("a", { href: href, ...(node.external ? { rel: 'noopener noreferrer', target: '_blank' } : {}), children: children }, key));
70
+ return (_jsx("a", { href: href, className: linkClassName, ...(node.external ? { rel: 'noopener noreferrer', target: '_blank' } : {}), children: children }, key));
71
+ }
72
+ export function SafeRichText({ nodes, as: Element = 'div', linkClassName, ...props }) {
73
+ if (Element !== 'div' && Element !== 'p' && Element !== 'span') {
74
+ throw new Error('SafeRichText renders as "div", "p" or "span".');
75
+ }
76
+ if (Element !== 'div')
77
+ assertInlineRichText(nodes, Element);
78
+ return (_jsx(Element, { ...props, children: nodes.map((node, index) => renderSafeRichTextNode(node, String(index), linkClassName)) }));
79
+ }
80
+ const INLINE_MARKUP_PATTERN = /<(strong|b)>(.*?)<\/\1>|<a\s+[^>]*href=(["'])(.*?)\3[^>]*>(.*?)<\/a>/gis;
81
+ /**
82
+ * Inline rich-text markup as nodes, for content written as short HTML
83
+ * strings: `<strong>` and `<b>` become strong text, `<a href>` becomes a link
84
+ * when its href passes sanitizeRichTextHref (otherwise its text stays as plain
85
+ * text), every other tag is dropped and text is kept exactly as written
86
+ * (React escapes it). Nothing is ever rendered as HTML. Strong text holds text
87
+ * only; markup nested inside it throws.
88
+ */
89
+ export function parseInlineRichText(markup) {
90
+ if (typeof markup !== 'string')
91
+ throw new TypeError('Inline rich text must be a string.');
92
+ return parseInlineMarkup(markup, false);
61
93
  }
62
- export function SafeRichText({ nodes, ...props }) {
63
- return (_jsx("div", { ...props, children: nodes.map((node, index) => renderSafeRichTextNode(node, String(index))) }));
94
+ function parseInlineMarkup(markup, insideLink) {
95
+ const nodes = [];
96
+ const pattern = new RegExp(INLINE_MARKUP_PATTERN.source, INLINE_MARKUP_PATTERN.flags);
97
+ const text = (value) => nodes.push({ type: 'text', value: value.replace(/<[^>]*>/g, '') });
98
+ let cursor = 0;
99
+ for (let match = pattern.exec(markup); match !== null; match = pattern.exec(markup)) {
100
+ if (match.index > cursor)
101
+ text(markup.slice(cursor, match.index));
102
+ const [, , strong, , href, linkText] = match;
103
+ if (typeof strong === 'string') {
104
+ const inner = parseInlineMarkup(strong, insideLink);
105
+ if (inner.length > 1 || (inner[0] && (inner[0].type !== 'text' || inner[0].emphasis))) {
106
+ throw new Error('Inline rich text supports only text inside <strong>.');
107
+ }
108
+ nodes.push({ type: 'text', value: inner[0]?.type === 'text' ? inner[0].value : '', emphasis: 'strong' });
109
+ }
110
+ else if (typeof href === 'string' && typeof linkText === 'string') {
111
+ const safeHref = insideLink ? null : sanitizeRichTextHref(href);
112
+ if (insideLink && sanitizeRichTextHref(href))
113
+ throw new Error('Inline rich text links cannot be nested.');
114
+ const children = parseInlineMarkup(linkText, true);
115
+ if (safeHref)
116
+ nodes.push({ type: 'link', href: safeHref, children });
117
+ else
118
+ nodes.push(...children);
119
+ }
120
+ cursor = match.index + match[0].length;
121
+ }
122
+ if (cursor < markup.length)
123
+ text(markup.slice(cursor));
124
+ return nodes;
64
125
  }
65
126
  //# sourceMappingURL=ui.js.map
package/dist/ui.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"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","sourcesContent":["import type {\n AnchorHTMLAttributes,\n DetailsHTMLAttributes,\n HTMLAttributes,\n ReactNode,\n} from 'react'\nimport { assertSafeRichTextHref } from '@brandlab-digital/site-core/rich-text'\n\nexport interface HeadlessSectionProps extends HTMLAttributes<HTMLElement> {\n heading: ReactNode\n headingId: string\n headingClassName?: string\n contentClassName?: string\n children: ReactNode\n}\n\nexport function HeadlessSection({\n heading,\n headingId,\n headingClassName,\n contentClassName,\n children,\n ...sectionProps\n}: HeadlessSectionProps) {\n return (\n <section {...sectionProps} aria-labelledby={headingId}>\n <h2 className={headingClassName} id={headingId}>\n {heading}\n </h2>\n <div className={contentClassName}>{children}</div>\n </section>\n )\n}\n\nexport interface HeadlessDisclosureProps extends DetailsHTMLAttributes<HTMLDetailsElement> {\n summary: ReactNode\n summaryClassName?: string\n contentClassName?: string\n children: ReactNode\n}\n\nexport function HeadlessDisclosure({\n summary,\n summaryClassName,\n contentClassName,\n children,\n ...detailsProps\n}: HeadlessDisclosureProps) {\n return (\n <details {...detailsProps}>\n <summary className={summaryClassName}>{summary}</summary>\n <div className={contentClassName}>{children}</div>\n </details>\n )\n}\n\nexport interface HeadlessFieldProps {\n id: string\n label: ReactNode\n description?: ReactNode\n error?: ReactNode\n required?: boolean\n className?: string\n labelClassName?: string\n descriptionClassName?: string\n errorClassName?: string\n children: (input: {\n id: string\n required: boolean\n 'aria-invalid': true | undefined\n 'aria-describedby': string | undefined\n }) => ReactNode\n}\n\nexport function HeadlessField({\n id,\n label,\n description,\n error,\n required = false,\n children,\n ...classes\n}: HeadlessFieldProps) {\n const descriptionId = description ? `${id}-description` : undefined\n const errorId = error ? `${id}-error` : undefined\n const describedBy = [descriptionId, errorId].filter(Boolean).join(' ') || undefined\n return (\n <div className={classes.className}>\n <label className={classes.labelClassName} htmlFor={id}>\n {label}\n </label>\n {description ? (\n <div className={classes.descriptionClassName} id={descriptionId}>\n {description}\n </div>\n ) : null}\n {children({\n id,\n required,\n 'aria-invalid': error ? true : undefined,\n 'aria-describedby': describedBy,\n })}\n {error ? (\n <div className={classes.errorClassName} id={errorId} role=\"alert\">\n {error}\n </div>\n ) : null}\n </div>\n )\n}\n\nexport interface HeadlessStatusProps extends HTMLAttributes<HTMLDivElement> {\n children: ReactNode\n kind?: 'status' | 'error'\n}\n\nexport function HeadlessStatus({ children, kind = 'status', ...props }: HeadlessStatusProps) {\n return (\n <div {...props} aria-live={kind === 'error' ? 'assertive' : 'polite'} role={kind === 'error' ? 'alert' : 'status'}>\n {children}\n </div>\n )\n}\n\nexport interface HeadlessErrorSummaryItem {\n fieldId: string\n message: ReactNode\n}\n\nexport interface HeadlessErrorSummaryProps extends HTMLAttributes<HTMLDivElement> {\n heading: ReactNode\n errors: readonly HeadlessErrorSummaryItem[]\n headingId: string\n linkClassName?: string\n}\n\nexport function HeadlessErrorSummary({\n heading,\n errors,\n headingId,\n linkClassName,\n ...props\n}: HeadlessErrorSummaryProps) {\n if (errors.length === 0) return null\n return (\n <div {...props} aria-labelledby={headingId} role=\"alert\" tabIndex={-1}>\n <h2 id={headingId}>{heading}</h2>\n <ul>\n {errors.map((error) => (\n <li key={error.fieldId}>\n <a\n className={linkClassName}\n href={`#${assertHtmlIdentifier(error.fieldId, 'Error summary fieldId')}`}\n >\n {error.message}\n </a>\n </li>\n ))}\n </ul>\n </div>\n )\n}\n\nexport interface SkipLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {\n targetId: string\n children: ReactNode\n}\n\nexport function SkipLink({ targetId, children, ...props }: SkipLinkProps) {\n assertHtmlIdentifier(targetId, 'SkipLink targetId')\n return (\n <a {...props} href={`#${targetId}`}>\n {children}\n </a>\n )\n}\n\nfunction assertHtmlIdentifier(value: string, label: string) {\n if (!/^[A-Za-z][A-Za-z0-9_:.-]*$/.test(value)) {\n throw new Error(`${label} must be a valid HTML identifier.`)\n }\n return value\n}\n\nexport type SafeRichTextNode =\n | { type: 'text'; value: string; emphasis?: 'strong' | 'emphasis' }\n | { type: 'paragraph'; children: readonly SafeRichTextNode[] }\n | { type: 'link'; href: string; children: readonly SafeRichTextNode[]; external?: boolean }\n | { type: 'unordered-list' | 'ordered-list'; children: readonly SafeRichTextNode[] }\n | { type: 'list-item'; children: readonly SafeRichTextNode[] }\n | { type: 'line-break' }\n\nexport interface SafeRichTextProps extends HTMLAttributes<HTMLDivElement> {\n nodes: readonly SafeRichTextNode[]\n}\n\nfunction renderSafeRichTextNode(node: SafeRichTextNode, key: string): ReactNode {\n if (node.type === 'text') {\n if (node.emphasis === 'strong') return <strong key={key}>{node.value}</strong>\n if (node.emphasis === 'emphasis') return <em key={key}>{node.value}</em>\n return node.value\n }\n if (node.type === 'line-break') return <br key={key} />\n const children = node.children.map((child, index) =>\n renderSafeRichTextNode(child, `${key}-${index}`),\n )\n if (node.type === 'paragraph') return <p key={key}>{children}</p>\n if (node.type === 'list-item') return <li key={key}>{children}</li>\n if (node.type === 'unordered-list') return <ul key={key}>{children}</ul>\n if (node.type === 'ordered-list') return <ol key={key}>{children}</ol>\n if (node.type !== 'link') throw new Error('Unsupported rich text node.')\n const href = assertSafeRichTextHref(node.href)\n return (\n <a\n href={href}\n key={key}\n {...(node.external ? { rel: 'noopener noreferrer', target: '_blank' } : {})}\n >\n {children}\n </a>\n )\n}\n\nexport function SafeRichText({ nodes, ...props }: SafeRichTextProps) {\n return (\n <div {...props}>\n {nodes.map((node, index) => renderSafeRichTextNode(node, String(index)))}\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":";AAMA,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAA;AAUpG,MAAM,UAAU,eAAe,CAAC,EAC9B,OAAO,EACP,SAAS,EACT,gBAAgB,EAChB,gBAAgB,EAChB,QAAQ,EACR,GAAG,YAAY,EACM;IACrB,OAAO,CACL,sBAAa,YAAY,qBAAmB,SAAS,aACnD,aAAI,SAAS,EAAE,gBAAgB,EAAE,EAAE,EAAE,SAAS,YAC3C,OAAO,GACL,EACL,cAAK,SAAS,EAAE,gBAAgB,YAAG,QAAQ,GAAO,IAC1C,CACX,CAAA;AACH,CAAC;AASD,MAAM,UAAU,kBAAkB,CAAC,EACjC,OAAO,EACP,gBAAgB,EAChB,gBAAgB,EAChB,QAAQ,EACR,GAAG,YAAY,EACS;IACxB,OAAO,CACL,sBAAa,YAAY,aACvB,kBAAS,SAAS,EAAE,gBAAgB,YAAG,OAAO,GAAW,EACzD,cAAK,SAAS,EAAE,gBAAgB,YAAG,QAAQ,GAAO,IAC1C,CACX,CAAA;AACH,CAAC;AAoBD,MAAM,UAAU,aAAa,CAAC,EAC5B,EAAE,EACF,KAAK,EACL,WAAW,EACX,KAAK,EACL,QAAQ,GAAG,KAAK,EAChB,QAAQ,EACR,GAAG,OAAO,EACS;IACnB,MAAM,aAAa,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC,SAAS,CAAA;IACnE,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAA;IACjD,MAAM,WAAW,GAAG,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,SAAS,CAAA;IACnF,OAAO,CACL,eAAK,SAAS,EAAE,OAAO,CAAC,SAAS,aAC/B,gBAAO,SAAS,EAAE,OAAO,CAAC,cAAc,EAAE,OAAO,EAAE,EAAE,YAClD,KAAK,GACA,EACP,WAAW,CAAC,CAAC,CAAC,CACb,cAAK,SAAS,EAAE,OAAO,CAAC,oBAAoB,EAAE,EAAE,EAAE,aAAa,YAC5D,WAAW,GACR,CACP,CAAC,CAAC,CAAC,IAAI,EACP,QAAQ,CAAC;gBACR,EAAE;gBACF,QAAQ;gBACR,cAAc,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;gBACxC,kBAAkB,EAAE,WAAW;aAChC,CAAC,EACD,KAAK,CAAC,CAAC,CAAC,CACP,cAAK,SAAS,EAAE,OAAO,CAAC,cAAc,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAC,OAAO,YAC9D,KAAK,GACF,CACP,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAA;AACH,CAAC;AAOD,MAAM,UAAU,cAAc,CAAC,EAAE,QAAQ,EAAE,IAAI,GAAG,QAAQ,EAAE,GAAG,KAAK,EAAuB;IACzF,OAAO,CACL,iBAAS,KAAK,eAAa,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,YAC9G,QAAQ,GACL,CACP,CAAA;AACH,CAAC;AAcD,MAAM,UAAU,oBAAoB,CAAC,EACnC,OAAO,EACP,MAAM,EACN,SAAS,EACT,aAAa,EACb,GAAG,KAAK,EACkB;IAC1B,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACpC,OAAO,CACL,kBAAS,KAAK,qBAAmB,SAAS,EAAE,IAAI,EAAC,OAAO,EAAC,QAAQ,EAAE,CAAC,CAAC,aACnE,aAAI,EAAE,EAAE,SAAS,YAAG,OAAO,GAAM,EACjC,uBACG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACrB,uBACE,YACE,SAAS,EAAE,aAAa,EACxB,IAAI,EAAE,IAAI,oBAAoB,CAAC,KAAK,CAAC,OAAO,EAAE,uBAAuB,CAAC,EAAE,YAEvE,KAAK,CAAC,OAAO,GACZ,IANG,KAAK,CAAC,OAAO,CAOjB,CACN,CAAC,GACC,IACD,CACP,CAAA;AACH,CAAC;AAOD,MAAM,UAAU,QAAQ,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAiB;IACtE,oBAAoB,CAAC,QAAQ,EAAE,mBAAmB,CAAC,CAAA;IACnD,OAAO,CACL,eAAO,KAAK,EAAE,IAAI,EAAE,IAAI,QAAQ,EAAE,YAC/B,QAAQ,GACP,CACL,CAAA;AACH,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAa,EAAE,KAAa;IACxD,IAAI,CAAC,4BAA4B,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,mCAAmC,CAAC,CAAA;IAC9D,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AA0BD,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAA;AAEhG,SAAS,oBAAoB,CAAC,KAAkC,EAAE,OAA4B;IAC5F,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3C,MAAM,IAAI,KAAK,CAAC,oBAAoB,OAAO,mCAAmC,IAAI,CAAC,IAAI,oBAAoB,CAAC,CAAA;QAC9G,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;YAAE,oBAAoB,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IACxE,CAAC;AACH,CAAC;AAED,SAAS,sBAAsB,CAAC,IAAsB,EAAE,GAAW,EAAE,aAAsB;IACzF,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ;YAAE,OAAO,2BAAmB,IAAI,CAAC,KAAK,IAAhB,GAAG,CAAuB,CAAA;QAC9E,IAAI,IAAI,CAAC,QAAQ,KAAK,UAAU;YAAE,OAAO,uBAAe,IAAI,CAAC,KAAK,IAAhB,GAAG,CAAmB,CAAA;QACxE,OAAO,IAAI,CAAC,KAAK,CAAA;IACnB,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,eAAS,GAAG,CAAI,CAAA;IACvD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAClD,sBAAsB,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI,KAAK,EAAE,EAAE,aAAa,CAAC,CAChE,CAAA;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW;QAAE,OAAO,sBAAc,QAAQ,IAAd,GAAG,CAAgB,CAAA;IACjE,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW;QAAE,OAAO,uBAAe,QAAQ,IAAd,GAAG,CAAiB,CAAA;IACnE,IAAI,IAAI,CAAC,IAAI,KAAK,gBAAgB;QAAE,OAAO,uBAAe,QAAQ,IAAd,GAAG,CAAiB,CAAA;IACxE,IAAI,IAAI,CAAC,IAAI,KAAK,cAAc;QAAE,OAAO,uBAAe,QAAQ,IAAd,GAAG,CAAiB,CAAA;IACtE,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAA;IACxE,MAAM,IAAI,GAAG,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC9C,OAAO,CACL,YACE,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,aAAa,KAEpB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,qBAAqB,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,YAE1E,QAAQ,IAHJ,GAAG,CAIN,CACL,CAAA;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,OAAO,GAAG,KAAK,EAAE,aAAa,EAAE,GAAG,KAAK,EAAqB;IACrG,IAAI,OAAO,KAAK,KAAK,IAAI,OAAO,KAAK,GAAG,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;QAC/D,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;IAClE,CAAC;IACD,IAAI,OAAO,KAAK,KAAK;QAAE,oBAAoB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IAC3D,OAAO,CACL,KAAC,OAAO,OAAK,KAAK,YACf,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,sBAAsB,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,CAAC,GAC/E,CACX,CAAA;AACH,CAAC;AAED,MAAM,qBAAqB,GAAG,yEAAyE,CAAA;AAEvG;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,MAAM,IAAI,SAAS,CAAC,oCAAoC,CAAC,CAAA;IACzF,OAAO,iBAAiB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;AACzC,CAAC;AAED,SAAS,iBAAiB,CAAC,MAAc,EAAE,UAAmB;IAC5D,MAAM,KAAK,GAAuB,EAAE,CAAA;IACpC,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,CAAC,KAAK,CAAC,CAAA;IACrF,MAAM,IAAI,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,EAAE,CAAC,CAAA;IAClG,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,IAAI,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,KAAK,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACpF,IAAI,KAAK,CAAC,KAAK,GAAG,MAAM;YAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAA;QACjE,MAAM,CAAC,EAAE,AAAD,EAAG,MAAM,EAAE,AAAD,EAAG,IAAI,EAAE,QAAQ,CAAC,GAAG,KAAK,CAAA;QAC5C,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC/B,MAAM,KAAK,GAAG,iBAAiB,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;YACnD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;gBACtF,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAA;YACzE,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAA;QAC1G,CAAC;aAAM,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACpE,MAAM,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAA;YAC/D,IAAI,UAAU,IAAI,oBAAoB,CAAC,IAAI,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAA;YACzG,MAAM,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;YAClD,IAAI,QAAQ;gBAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAA;;gBAC/D,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAA;QAC9B,CAAC;QACD,MAAM,GAAG,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;IACxC,CAAC;IACD,IAAI,MAAM,GAAG,MAAM,CAAC,MAAM;QAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAA;IACtD,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["import type {\n AnchorHTMLAttributes,\n DetailsHTMLAttributes,\n HTMLAttributes,\n ReactNode,\n} from 'react'\nimport { assertSafeRichTextHref, sanitizeRichTextHref } from '@brandlab-digital/site-core/rich-text'\n\nexport interface HeadlessSectionProps extends HTMLAttributes<HTMLElement> {\n heading: ReactNode\n headingId: string\n headingClassName?: string\n contentClassName?: string\n children: ReactNode\n}\n\nexport function HeadlessSection({\n heading,\n headingId,\n headingClassName,\n contentClassName,\n children,\n ...sectionProps\n}: HeadlessSectionProps) {\n return (\n <section {...sectionProps} aria-labelledby={headingId}>\n <h2 className={headingClassName} id={headingId}>\n {heading}\n </h2>\n <div className={contentClassName}>{children}</div>\n </section>\n )\n}\n\nexport interface HeadlessDisclosureProps extends DetailsHTMLAttributes<HTMLDetailsElement> {\n summary: ReactNode\n summaryClassName?: string\n contentClassName?: string\n children: ReactNode\n}\n\nexport function HeadlessDisclosure({\n summary,\n summaryClassName,\n contentClassName,\n children,\n ...detailsProps\n}: HeadlessDisclosureProps) {\n return (\n <details {...detailsProps}>\n <summary className={summaryClassName}>{summary}</summary>\n <div className={contentClassName}>{children}</div>\n </details>\n )\n}\n\nexport interface HeadlessFieldProps {\n id: string\n label: ReactNode\n description?: ReactNode\n error?: ReactNode\n required?: boolean\n className?: string\n labelClassName?: string\n descriptionClassName?: string\n errorClassName?: string\n children: (input: {\n id: string\n required: boolean\n 'aria-invalid': true | undefined\n 'aria-describedby': string | undefined\n }) => ReactNode\n}\n\nexport function HeadlessField({\n id,\n label,\n description,\n error,\n required = false,\n children,\n ...classes\n}: HeadlessFieldProps) {\n const descriptionId = description ? `${id}-description` : undefined\n const errorId = error ? `${id}-error` : undefined\n const describedBy = [descriptionId, errorId].filter(Boolean).join(' ') || undefined\n return (\n <div className={classes.className}>\n <label className={classes.labelClassName} htmlFor={id}>\n {label}\n </label>\n {description ? (\n <div className={classes.descriptionClassName} id={descriptionId}>\n {description}\n </div>\n ) : null}\n {children({\n id,\n required,\n 'aria-invalid': error ? true : undefined,\n 'aria-describedby': describedBy,\n })}\n {error ? (\n <div className={classes.errorClassName} id={errorId} role=\"alert\">\n {error}\n </div>\n ) : null}\n </div>\n )\n}\n\nexport interface HeadlessStatusProps extends HTMLAttributes<HTMLDivElement> {\n children: ReactNode\n kind?: 'status' | 'error'\n}\n\nexport function HeadlessStatus({ children, kind = 'status', ...props }: HeadlessStatusProps) {\n return (\n <div {...props} aria-live={kind === 'error' ? 'assertive' : 'polite'} role={kind === 'error' ? 'alert' : 'status'}>\n {children}\n </div>\n )\n}\n\nexport interface HeadlessErrorSummaryItem {\n fieldId: string\n message: ReactNode\n}\n\nexport interface HeadlessErrorSummaryProps extends HTMLAttributes<HTMLDivElement> {\n heading: ReactNode\n errors: readonly HeadlessErrorSummaryItem[]\n headingId: string\n linkClassName?: string\n}\n\nexport function HeadlessErrorSummary({\n heading,\n errors,\n headingId,\n linkClassName,\n ...props\n}: HeadlessErrorSummaryProps) {\n if (errors.length === 0) return null\n return (\n <div {...props} aria-labelledby={headingId} role=\"alert\" tabIndex={-1}>\n <h2 id={headingId}>{heading}</h2>\n <ul>\n {errors.map((error) => (\n <li key={error.fieldId}>\n <a\n className={linkClassName}\n href={`#${assertHtmlIdentifier(error.fieldId, 'Error summary fieldId')}`}\n >\n {error.message}\n </a>\n </li>\n ))}\n </ul>\n </div>\n )\n}\n\nexport interface SkipLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {\n targetId: string\n children: ReactNode\n}\n\nexport function SkipLink({ targetId, children, ...props }: SkipLinkProps) {\n assertHtmlIdentifier(targetId, 'SkipLink targetId')\n return (\n <a {...props} href={`#${targetId}`}>\n {children}\n </a>\n )\n}\n\nfunction assertHtmlIdentifier(value: string, label: string) {\n if (!/^[A-Za-z][A-Za-z0-9_:.-]*$/.test(value)) {\n throw new Error(`${label} must be a valid HTML identifier.`)\n }\n return value\n}\n\nexport type SafeRichTextNode =\n | { type: 'text'; value: string; emphasis?: 'strong' | 'emphasis' }\n | { type: 'paragraph'; children: readonly SafeRichTextNode[] }\n | { type: 'link'; href: string; children: readonly SafeRichTextNode[]; external?: boolean }\n | { type: 'unordered-list' | 'ordered-list'; children: readonly SafeRichTextNode[] }\n | { type: 'list-item'; children: readonly SafeRichTextNode[] }\n | { type: 'line-break' }\n\nexport type SafeRichTextElement = 'div' | 'p' | 'span'\n\nexport interface SafeRichTextProps extends HTMLAttributes<HTMLElement> {\n nodes: readonly SafeRichTextNode[]\n /**\n * The wrapper element. `div` (default) holds block content: paragraphs and\n * lists. `p` and `span` hold inline content only (text, emphasis, links and\n * line breaks), so the output has no block element and can sit where only\n * phrasing content is allowed, such as inside a button; a block node under\n * them throws.\n */\n as?: SafeRichTextElement\n /** Class for every link, rendered after `href`. */\n linkClassName?: string\n}\n\nconst INLINE_RICH_TEXT_TYPES = new Set<SafeRichTextNode['type']>(['text', 'link', 'line-break'])\n\nfunction assertInlineRichText(nodes: readonly SafeRichTextNode[], element: SafeRichTextElement) {\n for (const node of nodes) {\n if (!INLINE_RICH_TEXT_TYPES.has(node.type)) {\n throw new Error(`SafeRichText as=\"${element}\" renders inline content only; \"${node.type}\" is a block node.`)\n }\n if (node.type === 'link') assertInlineRichText(node.children, element)\n }\n}\n\nfunction renderSafeRichTextNode(node: SafeRichTextNode, key: string, linkClassName?: string): ReactNode {\n if (node.type === 'text') {\n if (node.emphasis === 'strong') return <strong key={key}>{node.value}</strong>\n if (node.emphasis === 'emphasis') return <em key={key}>{node.value}</em>\n return node.value\n }\n if (node.type === 'line-break') return <br key={key} />\n const children = node.children.map((child, index) =>\n renderSafeRichTextNode(child, `${key}-${index}`, linkClassName),\n )\n if (node.type === 'paragraph') return <p key={key}>{children}</p>\n if (node.type === 'list-item') return <li key={key}>{children}</li>\n if (node.type === 'unordered-list') return <ul key={key}>{children}</ul>\n if (node.type === 'ordered-list') return <ol key={key}>{children}</ol>\n if (node.type !== 'link') throw new Error('Unsupported rich text node.')\n const href = assertSafeRichTextHref(node.href)\n return (\n <a\n href={href}\n className={linkClassName}\n key={key}\n {...(node.external ? { rel: 'noopener noreferrer', target: '_blank' } : {})}\n >\n {children}\n </a>\n )\n}\n\nexport function SafeRichText({ nodes, as: Element = 'div', linkClassName, ...props }: SafeRichTextProps) {\n if (Element !== 'div' && Element !== 'p' && Element !== 'span') {\n throw new Error('SafeRichText renders as \"div\", \"p\" or \"span\".')\n }\n if (Element !== 'div') assertInlineRichText(nodes, Element)\n return (\n <Element {...props}>\n {nodes.map((node, index) => renderSafeRichTextNode(node, String(index), linkClassName))}\n </Element>\n )\n}\n\nconst INLINE_MARKUP_PATTERN = /<(strong|b)>(.*?)<\\/\\1>|<a\\s+[^>]*href=([\"'])(.*?)\\3[^>]*>(.*?)<\\/a>/gis\n\n/**\n * Inline rich-text markup as nodes, for content written as short HTML\n * strings: `<strong>` and `<b>` become strong text, `<a href>` becomes a link\n * when its href passes sanitizeRichTextHref (otherwise its text stays as plain\n * text), every other tag is dropped and text is kept exactly as written\n * (React escapes it). Nothing is ever rendered as HTML. Strong text holds text\n * only; markup nested inside it throws.\n */\nexport function parseInlineRichText(markup: string): SafeRichTextNode[] {\n if (typeof markup !== 'string') throw new TypeError('Inline rich text must be a string.')\n return parseInlineMarkup(markup, false)\n}\n\nfunction parseInlineMarkup(markup: string, insideLink: boolean): SafeRichTextNode[] {\n const nodes: SafeRichTextNode[] = []\n const pattern = new RegExp(INLINE_MARKUP_PATTERN.source, INLINE_MARKUP_PATTERN.flags)\n const text = (value: string) => nodes.push({ type: 'text', value: value.replace(/<[^>]*>/g, '') })\n let cursor = 0\n for (let match = pattern.exec(markup); match !== null; match = pattern.exec(markup)) {\n if (match.index > cursor) text(markup.slice(cursor, match.index))\n const [, , strong, , href, linkText] = match\n if (typeof strong === 'string') {\n const inner = parseInlineMarkup(strong, insideLink)\n if (inner.length > 1 || (inner[0] && (inner[0].type !== 'text' || inner[0].emphasis))) {\n throw new Error('Inline rich text supports only text inside <strong>.')\n }\n nodes.push({ type: 'text', value: inner[0]?.type === 'text' ? inner[0].value : '', emphasis: 'strong' })\n } else if (typeof href === 'string' && typeof linkText === 'string') {\n const safeHref = insideLink ? null : sanitizeRichTextHref(href)\n if (insideLink && sanitizeRichTextHref(href)) throw new Error('Inline rich text links cannot be nested.')\n const children = parseInlineMarkup(linkText, true)\n if (safeHref) nodes.push({ type: 'link', href: safeHref, children })\n else nodes.push(...children)\n }\n cursor = match.index + match[0].length\n }\n if (cursor < markup.length) text(markup.slice(cursor))\n return nodes\n}\n"]}