@jfdevelops/react-layout 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create-config/create-resource-links.cjs +20 -4
- package/dist/create-config/create-resource-links.cjs.map +1 -1
- package/dist/create-config/create-resource-links.d.cts +9 -0
- package/dist/create-config/create-resource-links.d.cts.map +1 -1
- package/dist/create-config/create-resource-links.d.mts +9 -0
- package/dist/create-config/create-resource-links.d.mts.map +1 -1
- package/dist/create-config/create-resource-links.mjs +20 -4
- package/dist/create-config/create-resource-links.mjs.map +1 -1
- package/dist/create-config/define-layout.cjs +173 -1
- package/dist/create-config/define-layout.cjs.map +1 -1
- package/dist/create-config/define-layout.d.cts +174 -18
- package/dist/create-config/define-layout.d.cts.map +1 -1
- package/dist/create-config/define-layout.d.mts +174 -18
- package/dist/create-config/define-layout.d.mts.map +1 -1
- package/dist/create-config/define-layout.mjs +174 -2
- package/dist/create-config/define-layout.mjs.map +1 -1
- package/dist/create-config/for-resources/create-component.d.mts +1 -1
- package/dist/create-config/for-resources/create-for-resources.cjs +1 -1
- package/dist/create-config/for-resources/create-for-resources.mjs +1 -1
- package/dist/create-config/index.d.cts +2 -1
- package/dist/create-config/index.d.mts +2 -1
- package/dist/create-config/shell-context.cjs +22 -0
- package/dist/create-config/shell-context.cjs.map +1 -0
- package/dist/create-config/shell-context.d.cts +18 -0
- package/dist/create-config/shell-context.d.cts.map +1 -0
- package/dist/create-config/shell-context.d.mts +18 -0
- package/dist/create-config/shell-context.d.mts.map +1 -0
- package/dist/create-config/shell-context.mjs +21 -0
- package/dist/create-config/shell-context.mjs.map +1 -0
- package/dist/index.d.cts +3 -2
- package/dist/index.d.mts +3 -2
- package/dist/resource.d.mts +1 -1
- package/package.json +1 -1
- package/skills/build-react-layouts/SKILL.md +4 -0
- package/skills/build-react-layouts/references/architecture.md +92 -0
|
@@ -33,9 +33,24 @@ function createResourceLinksFromConfig(config) {
|
|
|
33
33
|
};
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Deterministic, not random: a random id (the previous implementation used
|
|
38
|
+
* `crypto.randomUUID()` / `Math.random()`) would make `withGroups()` unsafe to
|
|
39
|
+
* call from module scope — some runtimes (e.g. Cloudflare Workers) disallow
|
|
40
|
+
* generating random values outside a request handler, and config arrays like
|
|
41
|
+
* this are routinely defined as top-level constants.
|
|
42
|
+
*
|
|
43
|
+
* Falls back to the group's own shape — its `label` and its link resource
|
|
44
|
+
* keys — rather than just the array index, so groups from *separate*
|
|
45
|
+
* `withGroups()` calls that get concatenated into one list (a module-owned nav
|
|
46
|
+
* section next to a feature-owned one, say) don't collide just because they're
|
|
47
|
+
* both first in their own call. Two groups that are genuinely identical in
|
|
48
|
+
* shape will still collide; pass an explicit `id` on the group to rule that
|
|
49
|
+
* out entirely.
|
|
50
|
+
*/
|
|
51
|
+
function createResourceLinkGroupId(group, index) {
|
|
52
|
+
if (typeof group.id === "string" && group.id.length > 0) return group.id;
|
|
53
|
+
return `group-${index}-${typeof group.label === "string" ? group.label : ""}-${Object.keys(group.links).join(",")}`;
|
|
39
54
|
}
|
|
40
55
|
function createResourceLinksWithGroups(groups) {
|
|
41
56
|
if (!Array.isArray(groups)) throw new Error("[createResourceLinks.withGroups]: \"groups\" must be an array.");
|
|
@@ -43,10 +58,11 @@ function createResourceLinksWithGroups(groups) {
|
|
|
43
58
|
if (!group) throw new Error(`[createResourceLinks.withGroups]: group at index ${index} is required.`);
|
|
44
59
|
if (typeof group !== "object") throw new Error(`[createResourceLinks.withGroups]: group at index ${index} must be an object. Received ${typeof group}`);
|
|
45
60
|
if ("label" in group && group.label !== void 0 && typeof group.label !== "string") throw new Error(`[createResourceLinks.withGroups]: "label" must be a string for group at index ${index}. Received ${typeof group.label}`);
|
|
61
|
+
if ("id" in group && group.id !== void 0 && typeof group.id !== "string") throw new Error(`[createResourceLinks.withGroups]: "id" must be a string for group at index ${index}. Received ${typeof group.id}`);
|
|
46
62
|
if (!("links" in group)) throw new Error(`[createResourceLinks.withGroups]: "links" is required for group at index ${index}.`);
|
|
47
63
|
if (typeof group.links !== "object" || group.links === null || Array.isArray(group.links)) throw new Error(`[createResourceLinks.withGroups]: "links" must be an object for group at index ${index}. Received ${Array.isArray(group.links) ? "array" : typeof group.links}`);
|
|
48
64
|
return {
|
|
49
|
-
id: createResourceLinkGroupId(),
|
|
65
|
+
id: createResourceLinkGroupId(group, index),
|
|
50
66
|
label: "label" in group ? group.label ?? null : null,
|
|
51
67
|
icon: "icon" in group ? group.icon : null,
|
|
52
68
|
links: createResourceLinksFromConfig(group.links)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-resource-links.cjs","names":[],"sources":["../../src/create-config/create-resource-links.ts"],"sourcesContent":["import { ReactNode } from 'react';\nimport {\n type LayoutResourceKey,\n type ResourceDefinition,\n} from '../resource';\n\nexport type ResourceAnchorLinkFn<Resource extends string> = (\n resource: Resource,\n) => string;\n\nexport type ResourceLinkHref<Resource extends string> =\n | string\n | ResourceAnchorLinkFn<Resource>;\n\nexport type ResourceLinkConfigResource<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = LayoutResourceKey<Resources> | (string & {});\nexport interface CreateResourceLinkOptions<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources>,\n> {\n /**\n * The label of the link. This is the text that will be displayed in the link.\n */\n label: string;\n /**\n * An optional icon to register with the link. This is a headless approach, meaning you have full control\n * over the icon's rendering.\n */\n icon?: ReactNode;\n /**\n * The href of the link. By default, the href will be `\"/\"`. While you **don't** need to include a hash,\n * if you do, it will override the `hash` property (if provided).\n */\n href?: ResourceLinkHref<Resource>;\n /**\n * The hash of the link. By default, the hash will be the resource name. If you need to customize it, you can provide\n * a string or a function that returns a string.\n *\n * @remarks A `\"#\"` is not required. The generated href will include it automatically.\n */\n hash?: ResourceLinkHref<Resource>;\n}\n\nexport type CreateResourceLinkConfig<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources> = ResourceLinkConfigResource<Resources>,\n> = {\n [Key in Resource]?: CreateResourceLinkOptions<\n Resources,\n Key\n >;\n};\n\nexport type InferHashFromResourceLinkHref<\n Resource extends string,\n Hash extends ResourceLinkHref<Resource>,\n> = Hash extends string\n ? Hash\n : Hash extends ResourceAnchorLinkFn<Resource>\n ? ReturnType<Hash>\n : never;\n\nexport type CreatedResourceHref = {\n /**\n * The full generated href with the `hash` if provided.\n */\n full: string;\n /**\n * The given href. This is the same value as the `href` property of the config.\n */\n given: string;\n};\n\nexport type CreatedResourceLinkBase<Resource extends string> = {\n href: CreatedResourceHref;\n label: string;\n resource: Resource;\n icon: ReactNode;\n};\nexport type CreateResourceLinkWithHash<\n Resource extends string,\n Hash extends ResourceLinkHref<Resource>,\n> = Omit<CreatedResourceLinkBase<Resource>, 'href'> & {\n href: CreatedResourceHref & {\n hash: InferHashFromResourceLinkHref<Resource, Hash>;\n };\n};\n\nexport type CreatedResourceLink<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Config,\n> = {\n [Resource in keyof Config]-?: Resource extends ResourceLinkConfigResource<Resources>\n ? Config[Resource] extends {\n hash: infer hash extends ResourceLinkHref<Resource>;\n }\n ? CreateResourceLinkWithHash<Resource, hash>\n : CreatedResourceLinkBase<Resource>\n : never;\n}[keyof Config];\n\nexport interface CreateResourceLinksFn<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> {\n <const Resource extends ResourceLinkConfigResource<Resources>>(\n config: CreateResourceLinkConfig<Resources, Resource>,\n ): Array<\n CreatedResourceLink<\n Resources,\n CreateResourceLinkConfig<Resources, Resource>\n >\n >;\n withGroups: CreateResourceLinksWithGroupsFn<Resources>;\n}\n\nexport interface CreateResourceLinkGroupOptions<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Links extends object =\n CreateResourceLinkConfig<Resources>,\n> {\n /**\n * The label of the group. This is the text that will be displayed for the group.\n */\n label?: string;\n /**\n * An optional icon to register with the group. This is a headless approach, meaning you have full control\n * over the icon's rendering.\n */\n icon?: ReactNode;\n /**\n * The resource links in this group.\n */\n links: Links;\n}\n\nexport type CreateResourceLinkGroupInput<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources> = ResourceLinkConfigResource<Resources>,\n> = CreateResourceLinkGroupOptions<\n Resources,\n CreateResourceLinkConfig<Resources, Resource>\n>;\n\nexport type AnyCreatedResourceLink<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = {\n [Resource in LayoutResourceKey<Resources>]:\n | CreatedResourceLinkBase<Resource>\n | CreateResourceLinkWithHash<Resource, string>;\n}[LayoutResourceKey<Resources>];\n\nexport type CreateResourceLinksWithGroups<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = {\n id: string;\n label: string | null;\n icon: ReactNode;\n links: Array<AnyCreatedResourceLink<Resources>>;\n};\n\nexport type CreateResourceLinksWithGroupsFn<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = <const Resource extends ResourceLinkConfigResource<Resources>>(\n groups: ReadonlyArray<CreateResourceLinkGroupInput<Resources, Resource>>,\n) => Array<CreateResourceLinksWithGroups<Resources>>;\n\nfunction resolveResourceLinkHref<Resource extends string>(\n value: ResourceLinkHref<Resource> | undefined,\n resource: Resource,\n fallback: string,\n) {\n return typeof value === 'function' ? value(resource) : (value ?? fallback);\n}\n\nfunction normalizeResourceLinkHash(hash: string) {\n return hash.replace(/^#+/, '');\n}\n\nfunction createFullResourceLinkHref(href: string, hash: string) {\n if (href.includes('#')) {\n return href;\n }\n\n return `${href}#${normalizeResourceLinkHash(hash)}`;\n}\n\nfunction createResourceLinksFromConfig<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources>,\n>(config: CreateResourceLinkConfig<Resources, Resource>) {\n return Object.entries(config).map(([resource, config]) => {\n if (!config) {\n throw new Error(\n `[createResourceLinks]: \"config\" is required for the ${resource} resource.`,\n );\n }\n\n if (typeof config !== 'object') {\n throw new Error(\n `[createResourceLinks]: \"config\" must be an object for the ${resource} resource. Received ${typeof config}`,\n );\n }\n\n if (!('label' in config)) {\n throw new Error(\n `[createResourceLinks]: \"label\" is required for the ${resource} resource.`,\n );\n }\n\n if (typeof config.label !== 'string') {\n throw new Error(\n `[createResourceLinks]: \"label\" must be a string for the ${resource} resource. Received ${typeof config.label}`,\n );\n }\n\n const linkConfig = config as CreateResourceLinkOptions<\n Resources,\n typeof resource\n >;\n\n if (\n 'href' in linkConfig &&\n linkConfig.href !== undefined &&\n typeof linkConfig.href !== 'string' &&\n typeof linkConfig.href !== 'function'\n ) {\n throw new Error(\n `[createResourceLinks]: \"href\" must be a string or function for the ${resource} resource. Received ${typeof linkConfig.href}`,\n );\n }\n\n if (\n 'hash' in linkConfig &&\n linkConfig.hash !== undefined &&\n typeof linkConfig.hash !== 'string' &&\n typeof linkConfig.hash !== 'function'\n ) {\n throw new Error(\n `[createResourceLinks]: \"hash\" must be a string or function for the ${resource} resource. Received ${typeof linkConfig.hash}`,\n );\n }\n\n const givenHref = resolveResourceLinkHref(linkConfig.href, resource, '/');\n const givenHash = resolveResourceLinkHref(\n linkConfig.hash,\n resource,\n resource,\n );\n const href = {\n given: givenHref,\n full: createFullResourceLinkHref(givenHref, givenHash),\n ...(linkConfig.hash !== undefined && !givenHref.includes('#')\n ? { hash: normalizeResourceLinkHash(givenHash) }\n : {}),\n };\n\n return {\n href,\n label: config.label,\n resource,\n icon: 'icon' in config ? config.icon : null,\n };\n });\n}\n\nfunction createResourceLinkGroupId() {\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.randomUUID === 'function'\n ) {\n return crypto.randomUUID();\n }\n\n return `group-${Math.random().toString(36).slice(2, 11)}`;\n}\n\nfunction createResourceLinksWithGroups<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources>,\n>(\n groups: ReadonlyArray<CreateResourceLinkGroupInput<Resources, Resource>>,\n): Array<CreateResourceLinksWithGroups<Resources>> {\n if (!Array.isArray(groups)) {\n throw new Error(\n '[createResourceLinks.withGroups]: \"groups\" must be an array.',\n );\n }\n\n return groups.map((group, index) => {\n if (!group) {\n throw new Error(\n `[createResourceLinks.withGroups]: group at index ${index} is required.`,\n );\n }\n\n if (typeof group !== 'object') {\n throw new Error(\n `[createResourceLinks.withGroups]: group at index ${index} must be an object. Received ${typeof group}`,\n );\n }\n\n if (\n 'label' in group &&\n group.label !== undefined &&\n typeof group.label !== 'string'\n ) {\n throw new Error(\n `[createResourceLinks.withGroups]: \"label\" must be a string for group at index ${index}. Received ${typeof group.label}`,\n );\n }\n\n if (!('links' in group)) {\n throw new Error(\n `[createResourceLinks.withGroups]: \"links\" is required for group at index ${index}.`,\n );\n }\n\n if (\n typeof group.links !== 'object' ||\n group.links === null ||\n Array.isArray(group.links)\n ) {\n throw new Error(\n `[createResourceLinks.withGroups]: \"links\" must be an object for group at index ${index}. Received ${Array.isArray(group.links) ? 'array' : typeof group.links}`,\n );\n }\n\n return {\n id: createResourceLinkGroupId(),\n label: 'label' in group ? (group.label ?? null) : null,\n icon: 'icon' in group ? group.icon : null,\n links: createResourceLinksFromConfig(group.links),\n };\n }) as Array<CreateResourceLinksWithGroups<Resources>>;\n}\n\nexport function createResourceLinksFn<\n const Resources extends ReadonlyArray<ResourceDefinition>,\n>(_resources: Resources): CreateResourceLinksFn<Resources> {\n function createResourceLinks<\n const Resource extends ResourceLinkConfigResource<Resources>,\n >(config: CreateResourceLinkConfig<Resources, Resource>) {\n return createResourceLinksFromConfig(config);\n }\n\n function withGroups<\n const Resource extends ResourceLinkConfigResource<Resources>,\n >(\n groups: ReadonlyArray<CreateResourceLinkGroupInput<Resources, Resource>>,\n ) {\n return createResourceLinksWithGroups(groups);\n }\n\n return Object.assign(createResourceLinks, {\n withGroups,\n }) as CreateResourceLinksFn<Resources>;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"create-resource-links.cjs","names":[],"sources":["../../src/create-config/create-resource-links.ts"],"sourcesContent":["import { ReactNode } from 'react';\nimport {\n type LayoutResourceKey,\n type ResourceDefinition,\n} from '../resource';\n\nexport type ResourceAnchorLinkFn<Resource extends string> = (\n resource: Resource,\n) => string;\n\nexport type ResourceLinkHref<Resource extends string> =\n | string\n | ResourceAnchorLinkFn<Resource>;\n\nexport type ResourceLinkConfigResource<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = LayoutResourceKey<Resources> | (string & {});\nexport interface CreateResourceLinkOptions<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources>,\n> {\n /**\n * The label of the link. This is the text that will be displayed in the link.\n */\n label: string;\n /**\n * An optional icon to register with the link. This is a headless approach, meaning you have full control\n * over the icon's rendering.\n */\n icon?: ReactNode;\n /**\n * The href of the link. By default, the href will be `\"/\"`. While you **don't** need to include a hash,\n * if you do, it will override the `hash` property (if provided).\n */\n href?: ResourceLinkHref<Resource>;\n /**\n * The hash of the link. By default, the hash will be the resource name. If you need to customize it, you can provide\n * a string or a function that returns a string.\n *\n * @remarks A `\"#\"` is not required. The generated href will include it automatically.\n */\n hash?: ResourceLinkHref<Resource>;\n}\n\nexport type CreateResourceLinkConfig<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources> = ResourceLinkConfigResource<Resources>,\n> = {\n [Key in Resource]?: CreateResourceLinkOptions<\n Resources,\n Key\n >;\n};\n\nexport type InferHashFromResourceLinkHref<\n Resource extends string,\n Hash extends ResourceLinkHref<Resource>,\n> = Hash extends string\n ? Hash\n : Hash extends ResourceAnchorLinkFn<Resource>\n ? ReturnType<Hash>\n : never;\n\nexport type CreatedResourceHref = {\n /**\n * The full generated href with the `hash` if provided.\n */\n full: string;\n /**\n * The given href. This is the same value as the `href` property of the config.\n */\n given: string;\n};\n\nexport type CreatedResourceLinkBase<Resource extends string> = {\n href: CreatedResourceHref;\n label: string;\n resource: Resource;\n icon: ReactNode;\n};\nexport type CreateResourceLinkWithHash<\n Resource extends string,\n Hash extends ResourceLinkHref<Resource>,\n> = Omit<CreatedResourceLinkBase<Resource>, 'href'> & {\n href: CreatedResourceHref & {\n hash: InferHashFromResourceLinkHref<Resource, Hash>;\n };\n};\n\nexport type CreatedResourceLink<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Config,\n> = {\n [Resource in keyof Config]-?: Resource extends ResourceLinkConfigResource<Resources>\n ? Config[Resource] extends {\n hash: infer hash extends ResourceLinkHref<Resource>;\n }\n ? CreateResourceLinkWithHash<Resource, hash>\n : CreatedResourceLinkBase<Resource>\n : never;\n}[keyof Config];\n\nexport interface CreateResourceLinksFn<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> {\n <const Resource extends ResourceLinkConfigResource<Resources>>(\n config: CreateResourceLinkConfig<Resources, Resource>,\n ): Array<\n CreatedResourceLink<\n Resources,\n CreateResourceLinkConfig<Resources, Resource>\n >\n >;\n withGroups: CreateResourceLinksWithGroupsFn<Resources>;\n}\n\nexport interface CreateResourceLinkGroupOptions<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Links extends object =\n CreateResourceLinkConfig<Resources>,\n> {\n /**\n * An optional, caller-provided identity for the group. Provide this when you\n * combine groups from more than one `withGroups()` call into one list (e.g.\n * a module-owned nav section alongside a feature-owned one) — without it,\n * the id is derived from the group's `label` and its link resources, which\n * is enough to keep groups from a single call apart but can't distinguish\n * two independently-defined groups that happen to look identical.\n */\n id?: string;\n /**\n * The label of the group. This is the text that will be displayed for the group.\n */\n label?: string;\n /**\n * An optional icon to register with the group. This is a headless approach, meaning you have full control\n * over the icon's rendering.\n */\n icon?: ReactNode;\n /**\n * The resource links in this group.\n */\n links: Links;\n}\n\nexport type CreateResourceLinkGroupInput<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources> = ResourceLinkConfigResource<Resources>,\n> = CreateResourceLinkGroupOptions<\n Resources,\n CreateResourceLinkConfig<Resources, Resource>\n>;\n\nexport type AnyCreatedResourceLink<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = {\n [Resource in LayoutResourceKey<Resources>]:\n | CreatedResourceLinkBase<Resource>\n | CreateResourceLinkWithHash<Resource, string>;\n}[LayoutResourceKey<Resources>];\n\nexport type CreateResourceLinksWithGroups<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = {\n id: string;\n label: string | null;\n icon: ReactNode;\n links: Array<AnyCreatedResourceLink<Resources>>;\n};\n\nexport type CreateResourceLinksWithGroupsFn<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = <const Resource extends ResourceLinkConfigResource<Resources>>(\n groups: ReadonlyArray<CreateResourceLinkGroupInput<Resources, Resource>>,\n) => Array<CreateResourceLinksWithGroups<Resources>>;\n\nfunction resolveResourceLinkHref<Resource extends string>(\n value: ResourceLinkHref<Resource> | undefined,\n resource: Resource,\n fallback: string,\n) {\n return typeof value === 'function' ? value(resource) : (value ?? fallback);\n}\n\nfunction normalizeResourceLinkHash(hash: string) {\n return hash.replace(/^#+/, '');\n}\n\nfunction createFullResourceLinkHref(href: string, hash: string) {\n if (href.includes('#')) {\n return href;\n }\n\n return `${href}#${normalizeResourceLinkHash(hash)}`;\n}\n\nfunction createResourceLinksFromConfig<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources>,\n>(config: CreateResourceLinkConfig<Resources, Resource>) {\n return Object.entries(config).map(([resource, config]) => {\n if (!config) {\n throw new Error(\n `[createResourceLinks]: \"config\" is required for the ${resource} resource.`,\n );\n }\n\n if (typeof config !== 'object') {\n throw new Error(\n `[createResourceLinks]: \"config\" must be an object for the ${resource} resource. Received ${typeof config}`,\n );\n }\n\n if (!('label' in config)) {\n throw new Error(\n `[createResourceLinks]: \"label\" is required for the ${resource} resource.`,\n );\n }\n\n if (typeof config.label !== 'string') {\n throw new Error(\n `[createResourceLinks]: \"label\" must be a string for the ${resource} resource. Received ${typeof config.label}`,\n );\n }\n\n const linkConfig = config as CreateResourceLinkOptions<\n Resources,\n typeof resource\n >;\n\n if (\n 'href' in linkConfig &&\n linkConfig.href !== undefined &&\n typeof linkConfig.href !== 'string' &&\n typeof linkConfig.href !== 'function'\n ) {\n throw new Error(\n `[createResourceLinks]: \"href\" must be a string or function for the ${resource} resource. Received ${typeof linkConfig.href}`,\n );\n }\n\n if (\n 'hash' in linkConfig &&\n linkConfig.hash !== undefined &&\n typeof linkConfig.hash !== 'string' &&\n typeof linkConfig.hash !== 'function'\n ) {\n throw new Error(\n `[createResourceLinks]: \"hash\" must be a string or function for the ${resource} resource. Received ${typeof linkConfig.hash}`,\n );\n }\n\n const givenHref = resolveResourceLinkHref(linkConfig.href, resource, '/');\n const givenHash = resolveResourceLinkHref(\n linkConfig.hash,\n resource,\n resource,\n );\n const href = {\n given: givenHref,\n full: createFullResourceLinkHref(givenHref, givenHash),\n ...(linkConfig.hash !== undefined && !givenHref.includes('#')\n ? { hash: normalizeResourceLinkHash(givenHash) }\n : {}),\n };\n\n return {\n href,\n label: config.label,\n resource,\n icon: 'icon' in config ? config.icon : null,\n };\n });\n}\n\n/**\n * Deterministic, not random: a random id (the previous implementation used\n * `crypto.randomUUID()` / `Math.random()`) would make `withGroups()` unsafe to\n * call from module scope — some runtimes (e.g. Cloudflare Workers) disallow\n * generating random values outside a request handler, and config arrays like\n * this are routinely defined as top-level constants.\n *\n * Falls back to the group's own shape — its `label` and its link resource\n * keys — rather than just the array index, so groups from *separate*\n * `withGroups()` calls that get concatenated into one list (a module-owned nav\n * section next to a feature-owned one, say) don't collide just because they're\n * both first in their own call. Two groups that are genuinely identical in\n * shape will still collide; pass an explicit `id` on the group to rule that\n * out entirely.\n */\nfunction createResourceLinkGroupId(\n group: { id?: unknown; label?: unknown; links: object },\n index: number,\n) {\n if (typeof group.id === 'string' && group.id.length > 0) {\n return group.id;\n }\n\n const label = typeof group.label === 'string' ? group.label : '';\n const linkKeys = Object.keys(group.links).join(',');\n return `group-${index}-${label}-${linkKeys}`;\n}\n\nfunction createResourceLinksWithGroups<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources>,\n>(\n groups: ReadonlyArray<CreateResourceLinkGroupInput<Resources, Resource>>,\n): Array<CreateResourceLinksWithGroups<Resources>> {\n if (!Array.isArray(groups)) {\n throw new Error(\n '[createResourceLinks.withGroups]: \"groups\" must be an array.',\n );\n }\n\n return groups.map((group, index) => {\n if (!group) {\n throw new Error(\n `[createResourceLinks.withGroups]: group at index ${index} is required.`,\n );\n }\n\n if (typeof group !== 'object') {\n throw new Error(\n `[createResourceLinks.withGroups]: group at index ${index} must be an object. Received ${typeof group}`,\n );\n }\n\n if (\n 'label' in group &&\n group.label !== undefined &&\n typeof group.label !== 'string'\n ) {\n throw new Error(\n `[createResourceLinks.withGroups]: \"label\" must be a string for group at index ${index}. Received ${typeof group.label}`,\n );\n }\n\n if ('id' in group && group.id !== undefined && typeof group.id !== 'string') {\n throw new Error(\n `[createResourceLinks.withGroups]: \"id\" must be a string for group at index ${index}. Received ${typeof group.id}`,\n );\n }\n\n if (!('links' in group)) {\n throw new Error(\n `[createResourceLinks.withGroups]: \"links\" is required for group at index ${index}.`,\n );\n }\n\n if (\n typeof group.links !== 'object' ||\n group.links === null ||\n Array.isArray(group.links)\n ) {\n throw new Error(\n `[createResourceLinks.withGroups]: \"links\" must be an object for group at index ${index}. Received ${Array.isArray(group.links) ? 'array' : typeof group.links}`,\n );\n }\n\n return {\n id: createResourceLinkGroupId(group, index),\n label: 'label' in group ? (group.label ?? null) : null,\n icon: 'icon' in group ? group.icon : null,\n links: createResourceLinksFromConfig(group.links),\n };\n }) as Array<CreateResourceLinksWithGroups<Resources>>;\n}\n\nexport function createResourceLinksFn<\n const Resources extends ReadonlyArray<ResourceDefinition>,\n>(_resources: Resources): CreateResourceLinksFn<Resources> {\n function createResourceLinks<\n const Resource extends ResourceLinkConfigResource<Resources>,\n >(config: CreateResourceLinkConfig<Resources, Resource>) {\n return createResourceLinksFromConfig(config);\n }\n\n function withGroups<\n const Resource extends ResourceLinkConfigResource<Resources>,\n >(\n groups: ReadonlyArray<CreateResourceLinkGroupInput<Resources, Resource>>,\n ) {\n return createResourceLinksWithGroups(groups);\n }\n\n return Object.assign(createResourceLinks, {\n withGroups,\n }) as CreateResourceLinksFn<Resources>;\n}\n"],"mappings":"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"}
|
|
@@ -59,6 +59,15 @@ interface CreateResourceLinksFn<Resources extends ReadonlyArray<ResourceDefiniti
|
|
|
59
59
|
withGroups: CreateResourceLinksWithGroupsFn<Resources>;
|
|
60
60
|
}
|
|
61
61
|
interface CreateResourceLinkGroupOptions<Resources extends ReadonlyArray<ResourceDefinition>, Links extends object = CreateResourceLinkConfig<Resources>> {
|
|
62
|
+
/**
|
|
63
|
+
* An optional, caller-provided identity for the group. Provide this when you
|
|
64
|
+
* combine groups from more than one `withGroups()` call into one list (e.g.
|
|
65
|
+
* a module-owned nav section alongside a feature-owned one) — without it,
|
|
66
|
+
* the id is derived from the group's `label` and its link resources, which
|
|
67
|
+
* is enough to keep groups from a single call apart but can't distinguish
|
|
68
|
+
* two independently-defined groups that happen to look identical.
|
|
69
|
+
*/
|
|
70
|
+
id?: string;
|
|
62
71
|
/**
|
|
63
72
|
* The label of the group. This is the text that will be displayed for the group.
|
|
64
73
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-resource-links.d.cts","names":[],"sources":["../../src/create-config/create-resource-links.ts"],"mappings":";;;;KAMY,oBAAA,6BACV,QAAA,EAAU,QAAQ;AAAA,KAGR,gBAAA,qCAER,oBAAoB,CAAC,QAAA;AAAA,KAEb,0BAAA,mBACQ,aAAA,CAAc,kBAAA,KAC9B,iBAAA,CAAkB,SAAA;AAAA,UACL,yBAAA,mBACG,aAAA,CAAc,kBAAA,oBACf,0BAAA,CAA2B,SAAA;EAbd;;;EAkB9B,KAAA;EAjBA;;AAAkB;AAGpB;EAmBE,IAAA,GAAO,SAAA;EAnBmB;;;;EAwB1B,IAAA,GAAO,gBAAA,CAAiB,QAAA;EAtBO;AAAA;AAEjC;;;;EA2BE,IAAA,GAAO,gBAAA,CAAiB,QAAA;AAAA;AAAA,KAGd,wBAAA,mBACQ,aAAA,CAAc,kBAAA,oBACf,0BAAA,CAA2B,SAAA,IAAa,0BAAA,CAA2B,SAAA,eAE5E,QAAA,IAAY,yBAAA,CAClB,SAAA,EACA,GAAA;AAAA,KAIQ,6BAAA,uCAEG,gBAAA,CAAiB,QAAA,KAC5B,IAAA,kBACA,IAAA,GACA,IAAA,SAAa,oBAAA,CAAqB,QAAA,IAChC,UAAA,CAAW,IAAA;AAAA,KAGL,mBAAA;EAhDsB;;;EAoDhC,IAAA;EAnD6B;AAC/B;;EAsDE,KAAK;AAAA;AAAA,KAGK,uBAAA;EACV,IAAA,EAAM,mBAAA;EACN,KAAA;EACA,QAAA,EAAU,QAAA;EACV,IAAA,EAAM,SAAA;AAAA;AAAA,KAEI,0BAAA,uCAEG,gBAAA,CAAiB,QAAA,KAC5B,IAAA,CAAK,uBAAA,CAAwB,QAAA;EAC/B,IAAA,EAAM,mBAAA;IACJ,IAAA,EAAM,6BAAA,CAA8B,QAAA,EAAU,IAAA;EAAA;AAAA;AAAA,KAItC,mBAAA,mBACQ,aAAA,CAAc,kBAAA,kCAGb,MAAA,KAAW,QAAA,SAAiB,0BAAA,CAA2B,SAAA,IACtE,MAAA,CAAO,QAAA;EACL,IAAA,qBAAyB,gBAAA,CAAiB,QAAA;AAAA,IAE1C,0BAAA,CAA2B,QAAA,EAAU,IAAA,IACrC,uBAAA,CAAwB,QAAA,kBAExB,MAAA;AAAA,UAES,qBAAA,mBACG,aAAA,CAAc,kBAAA;EAAA,wBAER,0BAAA,CAA2B,SAAA,GACjD,MAAA,EAAQ,wBAAA,CAAyB,SAAA,EAAW,QAAA,IAC3C,KAAA,CACD,mBAAA,CACE,SAAA,EACA,wBAAA,CAAyB,SAAA,EAAW,QAAA;EAGxC,UAAA,EAAY,+BAAA,CAAgC,SAAA;AAAA;AAAA,UAG7B,8BAAA,mBACG,aAAA,CAAc,kBAAA,0BAE9B,wBAAA,CAAyB,SAAA;EArFH
|
|
1
|
+
{"version":3,"file":"create-resource-links.d.cts","names":[],"sources":["../../src/create-config/create-resource-links.ts"],"mappings":"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"}
|
|
@@ -59,6 +59,15 @@ interface CreateResourceLinksFn<Resources extends ReadonlyArray<ResourceDefiniti
|
|
|
59
59
|
withGroups: CreateResourceLinksWithGroupsFn<Resources>;
|
|
60
60
|
}
|
|
61
61
|
interface CreateResourceLinkGroupOptions<Resources extends ReadonlyArray<ResourceDefinition>, Links extends object = CreateResourceLinkConfig<Resources>> {
|
|
62
|
+
/**
|
|
63
|
+
* An optional, caller-provided identity for the group. Provide this when you
|
|
64
|
+
* combine groups from more than one `withGroups()` call into one list (e.g.
|
|
65
|
+
* a module-owned nav section alongside a feature-owned one) — without it,
|
|
66
|
+
* the id is derived from the group's `label` and its link resources, which
|
|
67
|
+
* is enough to keep groups from a single call apart but can't distinguish
|
|
68
|
+
* two independently-defined groups that happen to look identical.
|
|
69
|
+
*/
|
|
70
|
+
id?: string;
|
|
62
71
|
/**
|
|
63
72
|
* The label of the group. This is the text that will be displayed for the group.
|
|
64
73
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-resource-links.d.mts","names":[],"sources":["../../src/create-config/create-resource-links.ts"],"mappings":";;;;KAMY,oBAAA,6BACV,QAAA,EAAU,QAAQ;AAAA,KAGR,gBAAA,qCAER,oBAAoB,CAAC,QAAA;AAAA,KAEb,0BAAA,mBACQ,aAAA,CAAc,kBAAA,KAC9B,iBAAA,CAAkB,SAAA;AAAA,UACL,yBAAA,mBACG,aAAA,CAAc,kBAAA,oBACf,0BAAA,CAA2B,SAAA;EAbd;;;EAkB9B,KAAA;EAjBA;;AAAkB;AAGpB;EAmBE,IAAA,GAAO,SAAA;EAnBmB;;;;EAwB1B,IAAA,GAAO,gBAAA,CAAiB,QAAA;EAtBO;AAAA;AAEjC;;;;EA2BE,IAAA,GAAO,gBAAA,CAAiB,QAAA;AAAA;AAAA,KAGd,wBAAA,mBACQ,aAAA,CAAc,kBAAA,oBACf,0BAAA,CAA2B,SAAA,IAAa,0BAAA,CAA2B,SAAA,eAE5E,QAAA,IAAY,yBAAA,CAClB,SAAA,EACA,GAAA;AAAA,KAIQ,6BAAA,uCAEG,gBAAA,CAAiB,QAAA,KAC5B,IAAA,kBACA,IAAA,GACA,IAAA,SAAa,oBAAA,CAAqB,QAAA,IAChC,UAAA,CAAW,IAAA;AAAA,KAGL,mBAAA;EAhDsB;;;EAoDhC,IAAA;EAnD6B;AAC/B;;EAsDE,KAAK;AAAA;AAAA,KAGK,uBAAA;EACV,IAAA,EAAM,mBAAA;EACN,KAAA;EACA,QAAA,EAAU,QAAA;EACV,IAAA,EAAM,SAAA;AAAA;AAAA,KAEI,0BAAA,uCAEG,gBAAA,CAAiB,QAAA,KAC5B,IAAA,CAAK,uBAAA,CAAwB,QAAA;EAC/B,IAAA,EAAM,mBAAA;IACJ,IAAA,EAAM,6BAAA,CAA8B,QAAA,EAAU,IAAA;EAAA;AAAA;AAAA,KAItC,mBAAA,mBACQ,aAAA,CAAc,kBAAA,kCAGb,MAAA,KAAW,QAAA,SAAiB,0BAAA,CAA2B,SAAA,IACtE,MAAA,CAAO,QAAA;EACL,IAAA,qBAAyB,gBAAA,CAAiB,QAAA;AAAA,IAE1C,0BAAA,CAA2B,QAAA,EAAU,IAAA,IACrC,uBAAA,CAAwB,QAAA,kBAExB,MAAA;AAAA,UAES,qBAAA,mBACG,aAAA,CAAc,kBAAA;EAAA,wBAER,0BAAA,CAA2B,SAAA,GACjD,MAAA,EAAQ,wBAAA,CAAyB,SAAA,EAAW,QAAA,IAC3C,KAAA,CACD,mBAAA,CACE,SAAA,EACA,wBAAA,CAAyB,SAAA,EAAW,QAAA;EAGxC,UAAA,EAAY,+BAAA,CAAgC,SAAA;AAAA;AAAA,UAG7B,8BAAA,mBACG,aAAA,CAAc,kBAAA,0BAE9B,wBAAA,CAAyB,SAAA;EArFH
|
|
1
|
+
{"version":3,"file":"create-resource-links.d.mts","names":[],"sources":["../../src/create-config/create-resource-links.ts"],"mappings":"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"}
|
|
@@ -32,9 +32,24 @@ function createResourceLinksFromConfig(config) {
|
|
|
32
32
|
};
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Deterministic, not random: a random id (the previous implementation used
|
|
37
|
+
* `crypto.randomUUID()` / `Math.random()`) would make `withGroups()` unsafe to
|
|
38
|
+
* call from module scope — some runtimes (e.g. Cloudflare Workers) disallow
|
|
39
|
+
* generating random values outside a request handler, and config arrays like
|
|
40
|
+
* this are routinely defined as top-level constants.
|
|
41
|
+
*
|
|
42
|
+
* Falls back to the group's own shape — its `label` and its link resource
|
|
43
|
+
* keys — rather than just the array index, so groups from *separate*
|
|
44
|
+
* `withGroups()` calls that get concatenated into one list (a module-owned nav
|
|
45
|
+
* section next to a feature-owned one, say) don't collide just because they're
|
|
46
|
+
* both first in their own call. Two groups that are genuinely identical in
|
|
47
|
+
* shape will still collide; pass an explicit `id` on the group to rule that
|
|
48
|
+
* out entirely.
|
|
49
|
+
*/
|
|
50
|
+
function createResourceLinkGroupId(group, index) {
|
|
51
|
+
if (typeof group.id === "string" && group.id.length > 0) return group.id;
|
|
52
|
+
return `group-${index}-${typeof group.label === "string" ? group.label : ""}-${Object.keys(group.links).join(",")}`;
|
|
38
53
|
}
|
|
39
54
|
function createResourceLinksWithGroups(groups) {
|
|
40
55
|
if (!Array.isArray(groups)) throw new Error("[createResourceLinks.withGroups]: \"groups\" must be an array.");
|
|
@@ -42,10 +57,11 @@ function createResourceLinksWithGroups(groups) {
|
|
|
42
57
|
if (!group) throw new Error(`[createResourceLinks.withGroups]: group at index ${index} is required.`);
|
|
43
58
|
if (typeof group !== "object") throw new Error(`[createResourceLinks.withGroups]: group at index ${index} must be an object. Received ${typeof group}`);
|
|
44
59
|
if ("label" in group && group.label !== void 0 && typeof group.label !== "string") throw new Error(`[createResourceLinks.withGroups]: "label" must be a string for group at index ${index}. Received ${typeof group.label}`);
|
|
60
|
+
if ("id" in group && group.id !== void 0 && typeof group.id !== "string") throw new Error(`[createResourceLinks.withGroups]: "id" must be a string for group at index ${index}. Received ${typeof group.id}`);
|
|
45
61
|
if (!("links" in group)) throw new Error(`[createResourceLinks.withGroups]: "links" is required for group at index ${index}.`);
|
|
46
62
|
if (typeof group.links !== "object" || group.links === null || Array.isArray(group.links)) throw new Error(`[createResourceLinks.withGroups]: "links" must be an object for group at index ${index}. Received ${Array.isArray(group.links) ? "array" : typeof group.links}`);
|
|
47
63
|
return {
|
|
48
|
-
id: createResourceLinkGroupId(),
|
|
64
|
+
id: createResourceLinkGroupId(group, index),
|
|
49
65
|
label: "label" in group ? group.label ?? null : null,
|
|
50
66
|
icon: "icon" in group ? group.icon : null,
|
|
51
67
|
links: createResourceLinksFromConfig(group.links)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-resource-links.mjs","names":[],"sources":["../../src/create-config/create-resource-links.ts"],"sourcesContent":["import { ReactNode } from 'react';\nimport {\n type LayoutResourceKey,\n type ResourceDefinition,\n} from '../resource';\n\nexport type ResourceAnchorLinkFn<Resource extends string> = (\n resource: Resource,\n) => string;\n\nexport type ResourceLinkHref<Resource extends string> =\n | string\n | ResourceAnchorLinkFn<Resource>;\n\nexport type ResourceLinkConfigResource<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = LayoutResourceKey<Resources> | (string & {});\nexport interface CreateResourceLinkOptions<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources>,\n> {\n /**\n * The label of the link. This is the text that will be displayed in the link.\n */\n label: string;\n /**\n * An optional icon to register with the link. This is a headless approach, meaning you have full control\n * over the icon's rendering.\n */\n icon?: ReactNode;\n /**\n * The href of the link. By default, the href will be `\"/\"`. While you **don't** need to include a hash,\n * if you do, it will override the `hash` property (if provided).\n */\n href?: ResourceLinkHref<Resource>;\n /**\n * The hash of the link. By default, the hash will be the resource name. If you need to customize it, you can provide\n * a string or a function that returns a string.\n *\n * @remarks A `\"#\"` is not required. The generated href will include it automatically.\n */\n hash?: ResourceLinkHref<Resource>;\n}\n\nexport type CreateResourceLinkConfig<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources> = ResourceLinkConfigResource<Resources>,\n> = {\n [Key in Resource]?: CreateResourceLinkOptions<\n Resources,\n Key\n >;\n};\n\nexport type InferHashFromResourceLinkHref<\n Resource extends string,\n Hash extends ResourceLinkHref<Resource>,\n> = Hash extends string\n ? Hash\n : Hash extends ResourceAnchorLinkFn<Resource>\n ? ReturnType<Hash>\n : never;\n\nexport type CreatedResourceHref = {\n /**\n * The full generated href with the `hash` if provided.\n */\n full: string;\n /**\n * The given href. This is the same value as the `href` property of the config.\n */\n given: string;\n};\n\nexport type CreatedResourceLinkBase<Resource extends string> = {\n href: CreatedResourceHref;\n label: string;\n resource: Resource;\n icon: ReactNode;\n};\nexport type CreateResourceLinkWithHash<\n Resource extends string,\n Hash extends ResourceLinkHref<Resource>,\n> = Omit<CreatedResourceLinkBase<Resource>, 'href'> & {\n href: CreatedResourceHref & {\n hash: InferHashFromResourceLinkHref<Resource, Hash>;\n };\n};\n\nexport type CreatedResourceLink<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Config,\n> = {\n [Resource in keyof Config]-?: Resource extends ResourceLinkConfigResource<Resources>\n ? Config[Resource] extends {\n hash: infer hash extends ResourceLinkHref<Resource>;\n }\n ? CreateResourceLinkWithHash<Resource, hash>\n : CreatedResourceLinkBase<Resource>\n : never;\n}[keyof Config];\n\nexport interface CreateResourceLinksFn<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> {\n <const Resource extends ResourceLinkConfigResource<Resources>>(\n config: CreateResourceLinkConfig<Resources, Resource>,\n ): Array<\n CreatedResourceLink<\n Resources,\n CreateResourceLinkConfig<Resources, Resource>\n >\n >;\n withGroups: CreateResourceLinksWithGroupsFn<Resources>;\n}\n\nexport interface CreateResourceLinkGroupOptions<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Links extends object =\n CreateResourceLinkConfig<Resources>,\n> {\n /**\n * The label of the group. This is the text that will be displayed for the group.\n */\n label?: string;\n /**\n * An optional icon to register with the group. This is a headless approach, meaning you have full control\n * over the icon's rendering.\n */\n icon?: ReactNode;\n /**\n * The resource links in this group.\n */\n links: Links;\n}\n\nexport type CreateResourceLinkGroupInput<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources> = ResourceLinkConfigResource<Resources>,\n> = CreateResourceLinkGroupOptions<\n Resources,\n CreateResourceLinkConfig<Resources, Resource>\n>;\n\nexport type AnyCreatedResourceLink<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = {\n [Resource in LayoutResourceKey<Resources>]:\n | CreatedResourceLinkBase<Resource>\n | CreateResourceLinkWithHash<Resource, string>;\n}[LayoutResourceKey<Resources>];\n\nexport type CreateResourceLinksWithGroups<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = {\n id: string;\n label: string | null;\n icon: ReactNode;\n links: Array<AnyCreatedResourceLink<Resources>>;\n};\n\nexport type CreateResourceLinksWithGroupsFn<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = <const Resource extends ResourceLinkConfigResource<Resources>>(\n groups: ReadonlyArray<CreateResourceLinkGroupInput<Resources, Resource>>,\n) => Array<CreateResourceLinksWithGroups<Resources>>;\n\nfunction resolveResourceLinkHref<Resource extends string>(\n value: ResourceLinkHref<Resource> | undefined,\n resource: Resource,\n fallback: string,\n) {\n return typeof value === 'function' ? value(resource) : (value ?? fallback);\n}\n\nfunction normalizeResourceLinkHash(hash: string) {\n return hash.replace(/^#+/, '');\n}\n\nfunction createFullResourceLinkHref(href: string, hash: string) {\n if (href.includes('#')) {\n return href;\n }\n\n return `${href}#${normalizeResourceLinkHash(hash)}`;\n}\n\nfunction createResourceLinksFromConfig<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources>,\n>(config: CreateResourceLinkConfig<Resources, Resource>) {\n return Object.entries(config).map(([resource, config]) => {\n if (!config) {\n throw new Error(\n `[createResourceLinks]: \"config\" is required for the ${resource} resource.`,\n );\n }\n\n if (typeof config !== 'object') {\n throw new Error(\n `[createResourceLinks]: \"config\" must be an object for the ${resource} resource. Received ${typeof config}`,\n );\n }\n\n if (!('label' in config)) {\n throw new Error(\n `[createResourceLinks]: \"label\" is required for the ${resource} resource.`,\n );\n }\n\n if (typeof config.label !== 'string') {\n throw new Error(\n `[createResourceLinks]: \"label\" must be a string for the ${resource} resource. Received ${typeof config.label}`,\n );\n }\n\n const linkConfig = config as CreateResourceLinkOptions<\n Resources,\n typeof resource\n >;\n\n if (\n 'href' in linkConfig &&\n linkConfig.href !== undefined &&\n typeof linkConfig.href !== 'string' &&\n typeof linkConfig.href !== 'function'\n ) {\n throw new Error(\n `[createResourceLinks]: \"href\" must be a string or function for the ${resource} resource. Received ${typeof linkConfig.href}`,\n );\n }\n\n if (\n 'hash' in linkConfig &&\n linkConfig.hash !== undefined &&\n typeof linkConfig.hash !== 'string' &&\n typeof linkConfig.hash !== 'function'\n ) {\n throw new Error(\n `[createResourceLinks]: \"hash\" must be a string or function for the ${resource} resource. Received ${typeof linkConfig.hash}`,\n );\n }\n\n const givenHref = resolveResourceLinkHref(linkConfig.href, resource, '/');\n const givenHash = resolveResourceLinkHref(\n linkConfig.hash,\n resource,\n resource,\n );\n const href = {\n given: givenHref,\n full: createFullResourceLinkHref(givenHref, givenHash),\n ...(linkConfig.hash !== undefined && !givenHref.includes('#')\n ? { hash: normalizeResourceLinkHash(givenHash) }\n : {}),\n };\n\n return {\n href,\n label: config.label,\n resource,\n icon: 'icon' in config ? config.icon : null,\n };\n });\n}\n\nfunction createResourceLinkGroupId() {\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.randomUUID === 'function'\n ) {\n return crypto.randomUUID();\n }\n\n return `group-${Math.random().toString(36).slice(2, 11)}`;\n}\n\nfunction createResourceLinksWithGroups<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources>,\n>(\n groups: ReadonlyArray<CreateResourceLinkGroupInput<Resources, Resource>>,\n): Array<CreateResourceLinksWithGroups<Resources>> {\n if (!Array.isArray(groups)) {\n throw new Error(\n '[createResourceLinks.withGroups]: \"groups\" must be an array.',\n );\n }\n\n return groups.map((group, index) => {\n if (!group) {\n throw new Error(\n `[createResourceLinks.withGroups]: group at index ${index} is required.`,\n );\n }\n\n if (typeof group !== 'object') {\n throw new Error(\n `[createResourceLinks.withGroups]: group at index ${index} must be an object. Received ${typeof group}`,\n );\n }\n\n if (\n 'label' in group &&\n group.label !== undefined &&\n typeof group.label !== 'string'\n ) {\n throw new Error(\n `[createResourceLinks.withGroups]: \"label\" must be a string for group at index ${index}. Received ${typeof group.label}`,\n );\n }\n\n if (!('links' in group)) {\n throw new Error(\n `[createResourceLinks.withGroups]: \"links\" is required for group at index ${index}.`,\n );\n }\n\n if (\n typeof group.links !== 'object' ||\n group.links === null ||\n Array.isArray(group.links)\n ) {\n throw new Error(\n `[createResourceLinks.withGroups]: \"links\" must be an object for group at index ${index}. Received ${Array.isArray(group.links) ? 'array' : typeof group.links}`,\n );\n }\n\n return {\n id: createResourceLinkGroupId(),\n label: 'label' in group ? (group.label ?? null) : null,\n icon: 'icon' in group ? group.icon : null,\n links: createResourceLinksFromConfig(group.links),\n };\n }) as Array<CreateResourceLinksWithGroups<Resources>>;\n}\n\nexport function createResourceLinksFn<\n const Resources extends ReadonlyArray<ResourceDefinition>,\n>(_resources: Resources): CreateResourceLinksFn<Resources> {\n function createResourceLinks<\n const Resource extends ResourceLinkConfigResource<Resources>,\n >(config: CreateResourceLinkConfig<Resources, Resource>) {\n return createResourceLinksFromConfig(config);\n }\n\n function withGroups<\n const Resource extends ResourceLinkConfigResource<Resources>,\n >(\n groups: ReadonlyArray<CreateResourceLinkGroupInput<Resources, Resource>>,\n ) {\n return createResourceLinksWithGroups(groups);\n }\n\n return Object.assign(createResourceLinks, {\n withGroups,\n }) as CreateResourceLinksFn<Resources>;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"create-resource-links.mjs","names":[],"sources":["../../src/create-config/create-resource-links.ts"],"sourcesContent":["import { ReactNode } from 'react';\nimport {\n type LayoutResourceKey,\n type ResourceDefinition,\n} from '../resource';\n\nexport type ResourceAnchorLinkFn<Resource extends string> = (\n resource: Resource,\n) => string;\n\nexport type ResourceLinkHref<Resource extends string> =\n | string\n | ResourceAnchorLinkFn<Resource>;\n\nexport type ResourceLinkConfigResource<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = LayoutResourceKey<Resources> | (string & {});\nexport interface CreateResourceLinkOptions<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources>,\n> {\n /**\n * The label of the link. This is the text that will be displayed in the link.\n */\n label: string;\n /**\n * An optional icon to register with the link. This is a headless approach, meaning you have full control\n * over the icon's rendering.\n */\n icon?: ReactNode;\n /**\n * The href of the link. By default, the href will be `\"/\"`. While you **don't** need to include a hash,\n * if you do, it will override the `hash` property (if provided).\n */\n href?: ResourceLinkHref<Resource>;\n /**\n * The hash of the link. By default, the hash will be the resource name. If you need to customize it, you can provide\n * a string or a function that returns a string.\n *\n * @remarks A `\"#\"` is not required. The generated href will include it automatically.\n */\n hash?: ResourceLinkHref<Resource>;\n}\n\nexport type CreateResourceLinkConfig<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources> = ResourceLinkConfigResource<Resources>,\n> = {\n [Key in Resource]?: CreateResourceLinkOptions<\n Resources,\n Key\n >;\n};\n\nexport type InferHashFromResourceLinkHref<\n Resource extends string,\n Hash extends ResourceLinkHref<Resource>,\n> = Hash extends string\n ? Hash\n : Hash extends ResourceAnchorLinkFn<Resource>\n ? ReturnType<Hash>\n : never;\n\nexport type CreatedResourceHref = {\n /**\n * The full generated href with the `hash` if provided.\n */\n full: string;\n /**\n * The given href. This is the same value as the `href` property of the config.\n */\n given: string;\n};\n\nexport type CreatedResourceLinkBase<Resource extends string> = {\n href: CreatedResourceHref;\n label: string;\n resource: Resource;\n icon: ReactNode;\n};\nexport type CreateResourceLinkWithHash<\n Resource extends string,\n Hash extends ResourceLinkHref<Resource>,\n> = Omit<CreatedResourceLinkBase<Resource>, 'href'> & {\n href: CreatedResourceHref & {\n hash: InferHashFromResourceLinkHref<Resource, Hash>;\n };\n};\n\nexport type CreatedResourceLink<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Config,\n> = {\n [Resource in keyof Config]-?: Resource extends ResourceLinkConfigResource<Resources>\n ? Config[Resource] extends {\n hash: infer hash extends ResourceLinkHref<Resource>;\n }\n ? CreateResourceLinkWithHash<Resource, hash>\n : CreatedResourceLinkBase<Resource>\n : never;\n}[keyof Config];\n\nexport interface CreateResourceLinksFn<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> {\n <const Resource extends ResourceLinkConfigResource<Resources>>(\n config: CreateResourceLinkConfig<Resources, Resource>,\n ): Array<\n CreatedResourceLink<\n Resources,\n CreateResourceLinkConfig<Resources, Resource>\n >\n >;\n withGroups: CreateResourceLinksWithGroupsFn<Resources>;\n}\n\nexport interface CreateResourceLinkGroupOptions<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Links extends object =\n CreateResourceLinkConfig<Resources>,\n> {\n /**\n * An optional, caller-provided identity for the group. Provide this when you\n * combine groups from more than one `withGroups()` call into one list (e.g.\n * a module-owned nav section alongside a feature-owned one) — without it,\n * the id is derived from the group's `label` and its link resources, which\n * is enough to keep groups from a single call apart but can't distinguish\n * two independently-defined groups that happen to look identical.\n */\n id?: string;\n /**\n * The label of the group. This is the text that will be displayed for the group.\n */\n label?: string;\n /**\n * An optional icon to register with the group. This is a headless approach, meaning you have full control\n * over the icon's rendering.\n */\n icon?: ReactNode;\n /**\n * The resource links in this group.\n */\n links: Links;\n}\n\nexport type CreateResourceLinkGroupInput<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources> = ResourceLinkConfigResource<Resources>,\n> = CreateResourceLinkGroupOptions<\n Resources,\n CreateResourceLinkConfig<Resources, Resource>\n>;\n\nexport type AnyCreatedResourceLink<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = {\n [Resource in LayoutResourceKey<Resources>]:\n | CreatedResourceLinkBase<Resource>\n | CreateResourceLinkWithHash<Resource, string>;\n}[LayoutResourceKey<Resources>];\n\nexport type CreateResourceLinksWithGroups<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = {\n id: string;\n label: string | null;\n icon: ReactNode;\n links: Array<AnyCreatedResourceLink<Resources>>;\n};\n\nexport type CreateResourceLinksWithGroupsFn<\n Resources extends ReadonlyArray<ResourceDefinition>,\n> = <const Resource extends ResourceLinkConfigResource<Resources>>(\n groups: ReadonlyArray<CreateResourceLinkGroupInput<Resources, Resource>>,\n) => Array<CreateResourceLinksWithGroups<Resources>>;\n\nfunction resolveResourceLinkHref<Resource extends string>(\n value: ResourceLinkHref<Resource> | undefined,\n resource: Resource,\n fallback: string,\n) {\n return typeof value === 'function' ? value(resource) : (value ?? fallback);\n}\n\nfunction normalizeResourceLinkHash(hash: string) {\n return hash.replace(/^#+/, '');\n}\n\nfunction createFullResourceLinkHref(href: string, hash: string) {\n if (href.includes('#')) {\n return href;\n }\n\n return `${href}#${normalizeResourceLinkHash(hash)}`;\n}\n\nfunction createResourceLinksFromConfig<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources>,\n>(config: CreateResourceLinkConfig<Resources, Resource>) {\n return Object.entries(config).map(([resource, config]) => {\n if (!config) {\n throw new Error(\n `[createResourceLinks]: \"config\" is required for the ${resource} resource.`,\n );\n }\n\n if (typeof config !== 'object') {\n throw new Error(\n `[createResourceLinks]: \"config\" must be an object for the ${resource} resource. Received ${typeof config}`,\n );\n }\n\n if (!('label' in config)) {\n throw new Error(\n `[createResourceLinks]: \"label\" is required for the ${resource} resource.`,\n );\n }\n\n if (typeof config.label !== 'string') {\n throw new Error(\n `[createResourceLinks]: \"label\" must be a string for the ${resource} resource. Received ${typeof config.label}`,\n );\n }\n\n const linkConfig = config as CreateResourceLinkOptions<\n Resources,\n typeof resource\n >;\n\n if (\n 'href' in linkConfig &&\n linkConfig.href !== undefined &&\n typeof linkConfig.href !== 'string' &&\n typeof linkConfig.href !== 'function'\n ) {\n throw new Error(\n `[createResourceLinks]: \"href\" must be a string or function for the ${resource} resource. Received ${typeof linkConfig.href}`,\n );\n }\n\n if (\n 'hash' in linkConfig &&\n linkConfig.hash !== undefined &&\n typeof linkConfig.hash !== 'string' &&\n typeof linkConfig.hash !== 'function'\n ) {\n throw new Error(\n `[createResourceLinks]: \"hash\" must be a string or function for the ${resource} resource. Received ${typeof linkConfig.hash}`,\n );\n }\n\n const givenHref = resolveResourceLinkHref(linkConfig.href, resource, '/');\n const givenHash = resolveResourceLinkHref(\n linkConfig.hash,\n resource,\n resource,\n );\n const href = {\n given: givenHref,\n full: createFullResourceLinkHref(givenHref, givenHash),\n ...(linkConfig.hash !== undefined && !givenHref.includes('#')\n ? { hash: normalizeResourceLinkHash(givenHash) }\n : {}),\n };\n\n return {\n href,\n label: config.label,\n resource,\n icon: 'icon' in config ? config.icon : null,\n };\n });\n}\n\n/**\n * Deterministic, not random: a random id (the previous implementation used\n * `crypto.randomUUID()` / `Math.random()`) would make `withGroups()` unsafe to\n * call from module scope — some runtimes (e.g. Cloudflare Workers) disallow\n * generating random values outside a request handler, and config arrays like\n * this are routinely defined as top-level constants.\n *\n * Falls back to the group's own shape — its `label` and its link resource\n * keys — rather than just the array index, so groups from *separate*\n * `withGroups()` calls that get concatenated into one list (a module-owned nav\n * section next to a feature-owned one, say) don't collide just because they're\n * both first in their own call. Two groups that are genuinely identical in\n * shape will still collide; pass an explicit `id` on the group to rule that\n * out entirely.\n */\nfunction createResourceLinkGroupId(\n group: { id?: unknown; label?: unknown; links: object },\n index: number,\n) {\n if (typeof group.id === 'string' && group.id.length > 0) {\n return group.id;\n }\n\n const label = typeof group.label === 'string' ? group.label : '';\n const linkKeys = Object.keys(group.links).join(',');\n return `group-${index}-${label}-${linkKeys}`;\n}\n\nfunction createResourceLinksWithGroups<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Resource extends ResourceLinkConfigResource<Resources>,\n>(\n groups: ReadonlyArray<CreateResourceLinkGroupInput<Resources, Resource>>,\n): Array<CreateResourceLinksWithGroups<Resources>> {\n if (!Array.isArray(groups)) {\n throw new Error(\n '[createResourceLinks.withGroups]: \"groups\" must be an array.',\n );\n }\n\n return groups.map((group, index) => {\n if (!group) {\n throw new Error(\n `[createResourceLinks.withGroups]: group at index ${index} is required.`,\n );\n }\n\n if (typeof group !== 'object') {\n throw new Error(\n `[createResourceLinks.withGroups]: group at index ${index} must be an object. Received ${typeof group}`,\n );\n }\n\n if (\n 'label' in group &&\n group.label !== undefined &&\n typeof group.label !== 'string'\n ) {\n throw new Error(\n `[createResourceLinks.withGroups]: \"label\" must be a string for group at index ${index}. Received ${typeof group.label}`,\n );\n }\n\n if ('id' in group && group.id !== undefined && typeof group.id !== 'string') {\n throw new Error(\n `[createResourceLinks.withGroups]: \"id\" must be a string for group at index ${index}. Received ${typeof group.id}`,\n );\n }\n\n if (!('links' in group)) {\n throw new Error(\n `[createResourceLinks.withGroups]: \"links\" is required for group at index ${index}.`,\n );\n }\n\n if (\n typeof group.links !== 'object' ||\n group.links === null ||\n Array.isArray(group.links)\n ) {\n throw new Error(\n `[createResourceLinks.withGroups]: \"links\" must be an object for group at index ${index}. Received ${Array.isArray(group.links) ? 'array' : typeof group.links}`,\n );\n }\n\n return {\n id: createResourceLinkGroupId(group, index),\n label: 'label' in group ? (group.label ?? null) : null,\n icon: 'icon' in group ? group.icon : null,\n links: createResourceLinksFromConfig(group.links),\n };\n }) as Array<CreateResourceLinksWithGroups<Resources>>;\n}\n\nexport function createResourceLinksFn<\n const Resources extends ReadonlyArray<ResourceDefinition>,\n>(_resources: Resources): CreateResourceLinksFn<Resources> {\n function createResourceLinks<\n const Resource extends ResourceLinkConfigResource<Resources>,\n >(config: CreateResourceLinkConfig<Resources, Resource>) {\n return createResourceLinksFromConfig(config);\n }\n\n function withGroups<\n const Resource extends ResourceLinkConfigResource<Resources>,\n >(\n groups: ReadonlyArray<CreateResourceLinkGroupInput<Resources, Resource>>,\n ) {\n return createResourceLinksWithGroups(groups);\n }\n\n return Object.assign(createResourceLinks, {\n withGroups,\n }) as CreateResourceLinksFn<Resources>;\n}\n"],"mappings":"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"}
|
|
@@ -7,7 +7,9 @@ const require_get_component = require('./get-component.cjs');
|
|
|
7
7
|
const require_create_resource_links = require('./create-resource-links.cjs');
|
|
8
8
|
const require_for_resource = require('./for-resource.cjs');
|
|
9
9
|
const require_create_for_resources = require('./for-resources/create-for-resources.cjs');
|
|
10
|
+
const require_shell_context = require('./shell-context.cjs');
|
|
10
11
|
let _jfdevelops_react_layout_composables = require("@jfdevelops/react-layout-composables");
|
|
12
|
+
let react = require("react");
|
|
11
13
|
let _jfdevelops_react_layout_validator = require("@jfdevelops/react-layout-validator");
|
|
12
14
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
13
15
|
|
|
@@ -96,6 +98,7 @@ function assertUnreservedResourceSlugs(resources, owner) {
|
|
|
96
98
|
}
|
|
97
99
|
function defineResourceLayoutImpl(options) {
|
|
98
100
|
const { options: inProps = {}, resources, layout } = options;
|
|
101
|
+
const shellId = options.shellId;
|
|
99
102
|
assertUnreservedResourceSlugs(resources);
|
|
100
103
|
const resourcesAccessor = createLayoutResourcesAccessor(resources);
|
|
101
104
|
const normalizedResources = require_resource.normalizeResources(resources);
|
|
@@ -144,6 +147,11 @@ function defineResourceLayoutImpl(options) {
|
|
|
144
147
|
inProps: splitInProps
|
|
145
148
|
};
|
|
146
149
|
function Component(props) {
|
|
150
|
+
const publishResource = require_shell_context.useResourceLayoutShellPublisher();
|
|
151
|
+
(0, react.useLayoutEffect)(() => {
|
|
152
|
+
publishResource?.(layoutContext.resource, shellId);
|
|
153
|
+
return () => publishResource?.(void 0, shellId);
|
|
154
|
+
}, [publishResource, layoutContext.resource]);
|
|
147
155
|
const validatedProps = (0, _jfdevelops_react_layout_validator.validateProps)(resolvedLayoutProps, props, { ...validationContext });
|
|
148
156
|
const includedPropKeys = Object.keys(includeLayoutProps ?? {});
|
|
149
157
|
const includedPropDefinitions = require_utils.pick(mergedResolvedInProps, includedPropKeys);
|
|
@@ -247,9 +255,173 @@ function defineResourceLayoutForResources(...baseResources) {
|
|
|
247
255
|
});
|
|
248
256
|
});
|
|
249
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Builds the `Shell` component. Kept beside {@link defineResourceLayoutImpl} so
|
|
260
|
+
* it can reuse the same private prop-resolution helpers.
|
|
261
|
+
*
|
|
262
|
+
* Unlike a per-resource component, the shell re-reads the current resource on
|
|
263
|
+
* every render (reported by the mounted page) so resource-scoped composable
|
|
264
|
+
* *names* can update. Composable *component identities* are resolved once and
|
|
265
|
+
* kept stable: React reconciles by element type, so recreating
|
|
266
|
+
* `composables.Layout` each render would remount wrapWith targets, sidebar
|
|
267
|
+
* state, and the outlet. Display names are synced onto those stable functions
|
|
268
|
+
* when the resource changes.
|
|
269
|
+
*
|
|
270
|
+
* @param shellId - Identity of the owning `withLayout` definition. The shell
|
|
271
|
+
* only accepts resource reports tagged with this id, so a page or pane from a
|
|
272
|
+
* different definition rendered in the outlet cannot corrupt its state.
|
|
273
|
+
*/
|
|
274
|
+
function createResourceLayoutShellComponent(resources, inProps, shell, shellId) {
|
|
275
|
+
const shellName = shell.name ?? "ResourceLayoutShell";
|
|
276
|
+
const resourceKeys = Object.keys(require_resource.normalizeResources(resources));
|
|
277
|
+
const resourcesEnum = (0, _jfdevelops_react_layout_validator.createPrimitivePropBuilder)("string").enum(resourceKeys);
|
|
278
|
+
const { resolvedInProps, splitInProps } = splitLayoutInProps(typeof inProps === "function" ? inProps({
|
|
279
|
+
resource: resourcesEnum,
|
|
280
|
+
name: _jfdevelops_react_layout_validator.createProp.string().literal(shellName)
|
|
281
|
+
}) : inProps);
|
|
282
|
+
const includeShellProps = shell.props?.include;
|
|
283
|
+
const includeShellPropKeys = Object.keys(includeShellProps ?? {});
|
|
284
|
+
const shellCustomDefinitions = { ...shell.props?.custom ?? {} };
|
|
285
|
+
function syncShellComposableDisplayNames(stable, resource) {
|
|
286
|
+
if (!shell.composables) return;
|
|
287
|
+
const named = (0, _jfdevelops_react_layout_composables.resolveLayoutComposables)(shell.composables, {
|
|
288
|
+
resource,
|
|
289
|
+
name: shellName,
|
|
290
|
+
capitalize: require_capitalize.capitalize
|
|
291
|
+
});
|
|
292
|
+
for (const [key, component] of Object.entries(named)) {
|
|
293
|
+
const stableComponent = stable[key];
|
|
294
|
+
if (typeof stableComponent === "function" && typeof component === "function" && "displayName" in component) stableComponent.displayName = component.displayName;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function Shell(props) {
|
|
298
|
+
const { children: outlet, resource: resourceProp } = props;
|
|
299
|
+
const [reportedResource, setReportedResource] = (0, react.useState)(void 0);
|
|
300
|
+
const publishResource = (0, react.useCallback)((resource, ownerId) => {
|
|
301
|
+
if (ownerId !== shellId) return;
|
|
302
|
+
setReportedResource(resource);
|
|
303
|
+
}, []);
|
|
304
|
+
const resource = resourceProp ?? reportedResource;
|
|
305
|
+
const shellChildren = (0, react.useMemo)(() => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_shell_context.ResourceLayoutShellPublisherContext.Provider, {
|
|
306
|
+
value: publishResource,
|
|
307
|
+
children: outlet
|
|
308
|
+
}), [outlet, publishResource]);
|
|
309
|
+
const composablesRef = (0, react.useRef)(void 0);
|
|
310
|
+
if (shell.composables) {
|
|
311
|
+
const nameContextResource = resource ?? "";
|
|
312
|
+
if (!composablesRef.current) composablesRef.current = (0, _jfdevelops_react_layout_composables.resolveLayoutComposables)(shell.composables, {
|
|
313
|
+
resource: nameContextResource,
|
|
314
|
+
name: shellName,
|
|
315
|
+
capitalize: require_capitalize.capitalize
|
|
316
|
+
});
|
|
317
|
+
else syncShellComposableDisplayNames(composablesRef.current, nameContextResource);
|
|
318
|
+
}
|
|
319
|
+
const resolvedComposables = composablesRef.current;
|
|
320
|
+
const validationContext = {
|
|
321
|
+
layoutName: shellName,
|
|
322
|
+
resource: resource ?? ""
|
|
323
|
+
};
|
|
324
|
+
const mergedResolvedInProps = { ...resolvedInProps };
|
|
325
|
+
for (const { props: presetPropDefinitions } of (0, _jfdevelops_react_layout_composables.collectComposablePresetEntries)(resolvedComposables)) Object.assign(mergedResolvedInProps, presetPropDefinitions);
|
|
326
|
+
const includedPropDefinitions = require_utils.pick(mergedResolvedInProps, includeShellPropKeys);
|
|
327
|
+
const includedPropValues = { ...(0, _jfdevelops_react_layout_validator.resolvePropDefinitionValues)(includedPropDefinitions) };
|
|
328
|
+
for (const key of includeShellPropKeys) {
|
|
329
|
+
const definition = mergedResolvedInProps[key];
|
|
330
|
+
const behavior = includeShellProps?.[key];
|
|
331
|
+
const callSiteValue = allowsComponentPassthrough(behavior) ? readLayoutOptionValue(key, definition, props) : void 0;
|
|
332
|
+
const configValue = allowsConfigPassthrough(behavior) ? readLayoutOptionValue(key, definition, {
|
|
333
|
+
...includedPropValues,
|
|
334
|
+
...splitInProps
|
|
335
|
+
}) : void 0;
|
|
336
|
+
const splitValue = key in splitInProps ? splitInProps[key] : void 0;
|
|
337
|
+
const value = callSiteValue ?? configValue ?? splitValue;
|
|
338
|
+
if (value !== void 0) includedPropValues[key] = value;
|
|
339
|
+
}
|
|
340
|
+
const composablePresetProps = Object.fromEntries((0, _jfdevelops_react_layout_composables.collectComposablePresetEntries)(resolvedComposables).map(({ key, props: presetPropDefinitions }) => {
|
|
341
|
+
const entries = Object.entries(presetPropDefinitions);
|
|
342
|
+
return [key, (0, _jfdevelops_react_layout_validator.validateProps)(Object.fromEntries(entries.filter(([propKey]) => {
|
|
343
|
+
const behavior = includeShellProps?.[propKey];
|
|
344
|
+
if (behavior === void 0) return true;
|
|
345
|
+
if (allowsComponentPassthrough(behavior)) return true;
|
|
346
|
+
if (allowsConfigPassthrough(behavior)) return includedPropValues[propKey] !== void 0;
|
|
347
|
+
return false;
|
|
348
|
+
})), Object.fromEntries(entries.flatMap(([propKey]) => {
|
|
349
|
+
if (propKey in props) return [[propKey, props[propKey]]];
|
|
350
|
+
if (includedPropValues[propKey] !== void 0) return [[propKey, includedPropValues[propKey]]];
|
|
351
|
+
return [];
|
|
352
|
+
})), validationContext)];
|
|
353
|
+
}));
|
|
354
|
+
const validatedCustomProps = (0, _jfdevelops_react_layout_validator.validateProps)(shellCustomDefinitions, props, validationContext);
|
|
355
|
+
const requiredIncludedDefinitions = Object.fromEntries(Object.entries(includedPropDefinitions).filter(([key]) => {
|
|
356
|
+
const behavior = includeShellProps?.[key];
|
|
357
|
+
if (!isIncludedPropRequired(behavior)) return false;
|
|
358
|
+
if (allowsComponentPassthrough(behavior)) return true;
|
|
359
|
+
return includedPropValues[key] !== void 0;
|
|
360
|
+
}));
|
|
361
|
+
const optionalIncludedDefinitions = Object.fromEntries(Object.entries(includedPropDefinitions).filter(([key]) => !isIncludedPropRequired(includeShellProps?.[key]) && includedPropValues[key] !== void 0));
|
|
362
|
+
const validatedIncludedProps = (0, _jfdevelops_react_layout_validator.validateProps)({
|
|
363
|
+
...requiredIncludedDefinitions,
|
|
364
|
+
...optionalIncludedDefinitions
|
|
365
|
+
}, includedPropValues, validationContext);
|
|
366
|
+
const renderIncludedProps = Object.fromEntries(includeShellPropKeys.flatMap((key) => key in validatedIncludedProps ? [[toLayoutRenderPropKey(key, mergedResolvedInProps[key]), validatedIncludedProps[key]]] : []));
|
|
367
|
+
const renderProps = {
|
|
368
|
+
...validatedCustomProps,
|
|
369
|
+
...renderIncludedProps
|
|
370
|
+
};
|
|
371
|
+
const renderContext = {
|
|
372
|
+
composables: resolvedComposables,
|
|
373
|
+
resource,
|
|
374
|
+
resources: createLayoutRenderResourcesAccessor(resources, resource),
|
|
375
|
+
name: shellName,
|
|
376
|
+
inProps: splitInProps,
|
|
377
|
+
children: shellChildren
|
|
378
|
+
};
|
|
379
|
+
const rendered = shell.render(renderProps, renderContext);
|
|
380
|
+
return resolvedComposables ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_jfdevelops_react_layout_composables.LayoutComposablePresetProvider, {
|
|
381
|
+
value: composablePresetProps,
|
|
382
|
+
children: rendered
|
|
383
|
+
}) : rendered;
|
|
384
|
+
}
|
|
385
|
+
return Object.assign(Shell, {
|
|
386
|
+
displayName: shellName,
|
|
387
|
+
props: void 0
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Implementation of {@link defineResourceLayout.withLayout}. Delegates the
|
|
392
|
+
* config / links / `createResourceLayout` pieces to {@link defineResourceLayoutImpl}
|
|
393
|
+
* and adds the {@link createResourceLayoutShellComponent | Shell}.
|
|
394
|
+
*/
|
|
395
|
+
function defineResourceLayoutWithLayoutImpl(options) {
|
|
396
|
+
const { shell, layout, resources, options: inProps } = options;
|
|
397
|
+
const resolvedLayout = layout?.render ? layout : {
|
|
398
|
+
...layout,
|
|
399
|
+
props: {
|
|
400
|
+
...layout?.props,
|
|
401
|
+
custom: {
|
|
402
|
+
children: _jfdevelops_react_layout_validator.createProp.component({ type: "ReactNode" }).optional(),
|
|
403
|
+
...layout?.props?.custom ?? {}
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
render: (pageProps) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: pageProps.children ?? null })
|
|
407
|
+
};
|
|
408
|
+
const shellId = Symbol("resourceLayoutShell");
|
|
409
|
+
const base = defineResourceLayoutImpl({
|
|
410
|
+
resources,
|
|
411
|
+
options: inProps,
|
|
412
|
+
layout: resolvedLayout,
|
|
413
|
+
shellId
|
|
414
|
+
});
|
|
415
|
+
const Shell = createResourceLayoutShellComponent(resources, inProps ?? {}, shell, shellId);
|
|
416
|
+
return {
|
|
417
|
+
...base,
|
|
418
|
+
Shell
|
|
419
|
+
};
|
|
420
|
+
}
|
|
250
421
|
const defineResourceLayout = Object.assign(defineResourceLayoutImpl, {
|
|
251
422
|
forResources: defineResourceLayoutForResources,
|
|
252
|
-
defineResources
|
|
423
|
+
defineResources,
|
|
424
|
+
withLayout: defineResourceLayoutWithLayoutImpl
|
|
253
425
|
});
|
|
254
426
|
|
|
255
427
|
//#endregion
|