@fuf-stack/atelier 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/core/types.ts","../src/core/tanstack.ts","../src/core/createAtelierApp.ts","../src/core/createAuthAdapter.ts","../src/core/navigation.ts","../src/core/renderers.ts","../src/AtelierFrame/AtelierFrame.tsx"],"mappings":"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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,280 @@
1
+ import { useMemo, useState } from "react";
2
+ import { tv, variantsToClassNames } from "@fuf-stack/pixel-utils";
3
+ import Button from "@fuf-stack/pixels/Button";
4
+ import Drawer from "@fuf-stack/pixels/Drawer";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/core/createAuthAdapter.ts
7
+ /**
8
+ * Built-in role-based access fallback.
9
+ * - No declared roles means public access.
10
+ * - Role checks only pass for authenticated users.
11
+ */
12
+ const defaultCanAccess = (roles, state) => {
13
+ if (roles === void 0 || roles.length <= 0) return true;
14
+ if (state.status !== "authenticated" || !state.user) return false;
15
+ const userRoles = state.user.roles ?? [];
16
+ return roles.some((role) => {
17
+ return userRoles.includes(role);
18
+ });
19
+ };
20
+ /**
21
+ * Builds an auth adapter with safe defaults.
22
+ *
23
+ * Consumers can override any adapter method; `canAccess` always has a default.
24
+ */
25
+ const createAuthAdapter = (adapter) => {
26
+ return {
27
+ canAccess: defaultCanAccess,
28
+ ...adapter
29
+ };
30
+ };
31
+ //#endregion
32
+ //#region src/core/navigation.ts
33
+ /**
34
+ * Filters a navigation tree according to auth state, item visibility rules,
35
+ * and adapter-level access checks.
36
+ */
37
+ const filterNavigationForState = (navigation, state, auth = createAuthAdapter()) => {
38
+ return navigation.map((section) => {
39
+ const items = section.items.filter((item) => {
40
+ if (item.requiresAuth && state.status !== "authenticated") return false;
41
+ if (item.isVisible && !item.isVisible(state)) return false;
42
+ if (!auth.canAccess(item.roles, state, item.id)) return false;
43
+ return true;
44
+ });
45
+ return {
46
+ ...section,
47
+ items
48
+ };
49
+ }).filter((section) => {
50
+ return section.items.length > 0;
51
+ });
52
+ };
53
+ //#endregion
54
+ //#region src/core/renderers.ts
55
+ /**
56
+ * Resolves the renderer for a resource:
57
+ * 1) inline `resource.renderer`
58
+ * 2) registry lookup via `resource.rendererId`
59
+ * 3) text-based fallback renderer for loading/ready/empty/error states
60
+ */
61
+ const getResourceRenderer = (resource, registry = {}) => {
62
+ if (resource.renderer) return resource.renderer;
63
+ if (resource.rendererId && registry[resource.rendererId]) return registry[resource.rendererId];
64
+ return ({ error, state, title }) => {
65
+ if (state === "loading") return `${title ?? resource.title} is loading...`;
66
+ if (state === "error") {
67
+ const suffix = error instanceof Error ? ` (${error.message})` : "";
68
+ return `${title ?? resource.title} failed${suffix}`;
69
+ }
70
+ if (state === "empty") return `${title ?? resource.title} has no data`;
71
+ return `${title ?? resource.title} is ready`;
72
+ };
73
+ };
74
+ //#endregion
75
+ //#region src/AtelierFrame/AtelierFrame.tsx
76
+ /**
77
+ * Slot-based class recipe for `AtelierFrame`.
78
+ */
79
+ const atelierFrameVariants = tv({ slots: {
80
+ appName: "text-base font-semibold tracking-tight",
81
+ base: "flex min-h-[32rem] flex-col rounded-xl border border-default-200 bg-content1",
82
+ content: "grid gap-4 p-4 md:grid-cols-[16rem_1fr]",
83
+ header: "flex min-h-14 items-center justify-between border-b border-default-200 px-4",
84
+ navButton: "w-full justify-start rounded-md px-2 py-1 text-sm text-default-700 hover:bg-default-100",
85
+ resourceArea: "rounded-lg border border-default-200 bg-content2 p-4",
86
+ resourceTitle: "mb-2 text-sm font-medium text-default-700",
87
+ shellMeta: "flex items-center gap-2 text-xs text-default-500",
88
+ sidebar: "hidden rounded-lg border border-default-200 bg-content2 p-3 md:block",
89
+ sidebarFooter: "mt-3 border-t border-default-200 pt-3 text-xs text-default-600",
90
+ sidebarSection: "mb-3",
91
+ sidebarSectionLabel: "mb-1 text-xs font-semibold uppercase text-default-500",
92
+ topBarActions: "ml-auto flex items-center gap-2"
93
+ } });
94
+ const defaultResourceId = "default";
95
+ const getResourceById = (resources, resourceId) => {
96
+ return resources.find((resource) => {
97
+ return resource.id === resourceId;
98
+ });
99
+ };
100
+ const defaultResource = {
101
+ id: defaultResourceId,
102
+ title: "Workspace"
103
+ };
104
+ /**
105
+ * Reusable frame component for app chrome, navigation, and pluggable
106
+ * resource rendering.
107
+ */
108
+ const AtelierFrame = ({ appName, authState, className = void 0, currentPath = "/", dataState = "ready", navigation = [], onNavigate = void 0, rendererRegistry = {}, resourceData = void 0, resourceError = void 0, resourceId = defaultResourceId, resources = [], slots = void 0, testId = "atelier-frame", topBarActions = void 0, userMenu = void 0, withMobileDrawer = true }) => {
109
+ const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);
110
+ const classNames = variantsToClassNames(atelierFrameVariants(), className, "base");
111
+ const auth = createAuthAdapter();
112
+ const visibleNavigation = useMemo(() => {
113
+ return filterNavigationForState(navigation, authState, auth);
114
+ }, [
115
+ navigation,
116
+ authState,
117
+ auth
118
+ ]);
119
+ const resolvedResource = getResourceById(resources, resourceId) ?? resources[0] ?? {
120
+ ...defaultResource,
121
+ id: resourceId
122
+ };
123
+ const renderResource = getResourceRenderer(resolvedResource, rendererRegistry);
124
+ const sharedSlots = slots ?? {};
125
+ const resolvedUserMenu = userMenu ?? sharedSlots.userMenu;
126
+ /**
127
+ * Renders the same navigation structure for desktop and mobile contexts.
128
+ */
129
+ const renderSidebar = ({ mobile }) => {
130
+ return /* @__PURE__ */ jsxs("aside", {
131
+ className: mobile ? "rounded-lg border border-default-200 bg-content2 p-3" : classNames.sidebar,
132
+ "data-testid": `${testId}__sidebar`,
133
+ children: [visibleNavigation.map((section) => {
134
+ return /* @__PURE__ */ jsxs("div", {
135
+ className: classNames.sidebarSection,
136
+ "data-testid": `${testId}__section-${section.id}`,
137
+ children: [section.label ? /* @__PURE__ */ jsx("div", {
138
+ className: classNames.sidebarSectionLabel,
139
+ children: section.label
140
+ }) : null, /* @__PURE__ */ jsx("nav", {
141
+ className: "grid gap-1",
142
+ children: section.items.map((item) => {
143
+ return /* @__PURE__ */ jsxs(Button, {
144
+ className: classNames.navButton,
145
+ color: item.to === currentPath ? "primary" : "default",
146
+ onClick: () => {
147
+ onNavigate?.(item.to);
148
+ setIsMobileNavOpen(false);
149
+ },
150
+ variant: item.to === currentPath ? "solid" : "light",
151
+ children: [
152
+ /* @__PURE__ */ jsx("span", {
153
+ className: "mr-2 inline-flex",
154
+ children: item.icon
155
+ }),
156
+ /* @__PURE__ */ jsx("span", { children: item.label }),
157
+ item.badge ? /* @__PURE__ */ jsx("span", {
158
+ className: "ml-auto",
159
+ children: item.badge
160
+ }) : null
161
+ ]
162
+ }, item.id);
163
+ })
164
+ })]
165
+ }, section.id);
166
+ }), sharedSlots.sidebarFooter ? /* @__PURE__ */ jsx("div", {
167
+ className: classNames.sidebarFooter,
168
+ children: sharedSlots.sidebarFooter
169
+ }) : null]
170
+ });
171
+ };
172
+ return /* @__PURE__ */ jsxs("section", {
173
+ className: classNames.base,
174
+ "data-testid": testId,
175
+ children: [
176
+ /* @__PURE__ */ jsxs("header", {
177
+ className: classNames.header,
178
+ children: [/* @__PURE__ */ jsxs("div", {
179
+ className: classNames.shellMeta,
180
+ children: [
181
+ withMobileDrawer ? /* @__PURE__ */ jsx(Button, {
182
+ className: "md:hidden",
183
+ onClick: () => {
184
+ setIsMobileNavOpen(true);
185
+ },
186
+ size: "sm",
187
+ variant: "flat",
188
+ children: "Menu"
189
+ }) : null,
190
+ /* @__PURE__ */ jsx("span", {
191
+ className: classNames.appName,
192
+ children: appName
193
+ }),
194
+ /* @__PURE__ */ jsxs("span", { children: ["status: ", authState.status] })
195
+ ]
196
+ }), /* @__PURE__ */ jsxs("div", {
197
+ className: classNames.topBarActions,
198
+ children: [topBarActions ?? sharedSlots.headerActions, resolvedUserMenu]
199
+ })]
200
+ }),
201
+ /* @__PURE__ */ jsxs("div", {
202
+ className: classNames.content,
203
+ children: [renderSidebar({ mobile: false }), /* @__PURE__ */ jsxs("main", {
204
+ className: classNames.resourceArea,
205
+ children: [
206
+ /* @__PURE__ */ jsx("div", {
207
+ className: classNames.resourceTitle,
208
+ children: resolvedResource.title
209
+ }),
210
+ sharedSlots.pageChrome,
211
+ renderResource({
212
+ data: resourceData,
213
+ error: resourceError,
214
+ resourceId: resolvedResource.id,
215
+ state: dataState,
216
+ title: resolvedResource.title
217
+ })
218
+ ]
219
+ })]
220
+ }),
221
+ withMobileDrawer ? /* @__PURE__ */ jsx(Drawer, {
222
+ header: appName,
223
+ isOpen: isMobileNavOpen,
224
+ onClose: () => {
225
+ setIsMobileNavOpen(false);
226
+ },
227
+ placement: "left",
228
+ children: renderSidebar({ mobile: true })
229
+ }) : null
230
+ ]
231
+ });
232
+ };
233
+ //#endregion
234
+ //#region src/core/createAtelierApp.ts
235
+ /**
236
+ * Clones and sanitizes navigation config by dropping empty sections.
237
+ */
238
+ const normalizeNavigation = (sections) => {
239
+ if (!sections || sections.length <= 0) return [];
240
+ return sections.filter((section) => {
241
+ return section.items.length > 0;
242
+ }).map((section) => {
243
+ return {
244
+ ...section,
245
+ items: [...section.items]
246
+ };
247
+ });
248
+ };
249
+ /**
250
+ * Creates a normalized app config object for shell usage.
251
+ *
252
+ * Normalization goals:
253
+ * - always provide an auth adapter with defaults
254
+ * - always provide collections (`navigation`, `renderers`, `resources`)
255
+ * - remove empty navigation sections
256
+ */
257
+ const createAtelierApp = (config) => {
258
+ return {
259
+ ...config,
260
+ auth: createAuthAdapter(config.auth),
261
+ navigation: normalizeNavigation(config.navigation),
262
+ renderers: config.renderers ?? {},
263
+ resources: config.resources ?? []
264
+ };
265
+ };
266
+ //#endregion
267
+ //#region src/core/tanstack.ts
268
+ /**
269
+ * Lightweight helper to normalize route context creation in consuming apps.
270
+ */
271
+ const createAtelierRouteContext = (context) => {
272
+ return context;
273
+ };
274
+ //#endregion
275
+ //#region src/index.ts
276
+ /* v8 ignore stop */
277
+ //#endregion
278
+ export { AtelierFrame, atelierFrameVariants, createAtelierApp, createAtelierRouteContext, createAuthAdapter, filterNavigationForState, getResourceRenderer };
279
+
280
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/core/createAuthAdapter.ts","../src/core/navigation.ts","../src/core/renderers.ts","../src/AtelierFrame/AtelierFrame.tsx","../src/core/createAtelierApp.ts","../src/core/tanstack.ts","../src/index.ts"],"sourcesContent":["/* eslint-disable import-x/prefer-default-export */\n\nimport type { AtelierAuthAdapter, AtelierAuthState } from './types';\n\n/**\n * Built-in role-based access fallback.\n * - No declared roles means public access.\n * - Role checks only pass for authenticated users.\n */\nconst defaultCanAccess = (\n roles: string[] | undefined,\n state: AtelierAuthState,\n): boolean => {\n if (roles === undefined || roles.length <= 0) {\n return true;\n }\n\n if (state.status !== 'authenticated' || !state.user) {\n return false;\n }\n\n const userRoles = state.user.roles ?? [];\n return roles.some((role) => {\n return userRoles.includes(role);\n });\n};\n\n/**\n * Builds an auth adapter with safe defaults.\n *\n * Consumers can override any adapter method; `canAccess` always has a default.\n */\nexport const createAuthAdapter = (\n adapter?: AtelierAuthAdapter,\n): Required<Pick<AtelierAuthAdapter, 'canAccess'>> & AtelierAuthAdapter => {\n return {\n canAccess: defaultCanAccess,\n ...adapter,\n };\n};\n","/* eslint-disable import-x/prefer-default-export */\n\nimport type { AtelierAuthState, AtelierNavigation } from './types';\n\nimport { createAuthAdapter } from './createAuthAdapter';\n\n/**\n * Filters a navigation tree according to auth state, item visibility rules,\n * and adapter-level access checks.\n */\nexport const filterNavigationForState = (\n navigation: AtelierNavigation,\n state: AtelierAuthState,\n auth = createAuthAdapter(),\n): AtelierNavigation => {\n return navigation\n .map((section) => {\n const items = section.items.filter((item) => {\n if (item.requiresAuth && state.status !== 'authenticated') {\n return false;\n }\n\n if (item.isVisible && !item.isVisible(state)) {\n return false;\n }\n\n if (!auth.canAccess(item.roles, state, item.id)) {\n return false;\n }\n\n return true;\n });\n\n return {\n ...section,\n items,\n };\n })\n .filter((section) => {\n return section.items.length > 0;\n });\n};\n","/* eslint-disable import-x/prefer-default-export */\n\nimport type {\n AtelierRenderer,\n AtelierRendererProps,\n AtelierRendererRegistry,\n AtelierResourceDefinition,\n} from './types';\n\n/**\n * Resolves the renderer for a resource:\n * 1) inline `resource.renderer`\n * 2) registry lookup via `resource.rendererId`\n * 3) text-based fallback renderer for loading/ready/empty/error states\n */\nexport const getResourceRenderer = (\n resource: AtelierResourceDefinition,\n registry: AtelierRendererRegistry = {},\n): AtelierRenderer => {\n if (resource.renderer) {\n return resource.renderer;\n }\n\n if (resource.rendererId && registry[resource.rendererId]) {\n return registry[resource.rendererId];\n }\n\n return ({ error, state, title }: AtelierRendererProps) => {\n if (state === 'loading') {\n return `${title ?? resource.title} is loading...`;\n }\n\n if (state === 'error') {\n const suffix = error instanceof Error ? ` (${error.message})` : '';\n return `${title ?? resource.title} failed${suffix}`;\n }\n\n if (state === 'empty') {\n return `${title ?? resource.title} has no data`;\n }\n\n return `${title ?? resource.title} is ready`;\n };\n};\n","import type { TVClassName } from '@fuf-stack/pixel-utils';\nimport type { ReactNode } from 'react';\nimport type {\n AtelierAuthState,\n AtelierDataState,\n AtelierFrameSlots,\n AtelierNavigation,\n AtelierRendererRegistry,\n AtelierResourceDefinition,\n} from '../core';\n\nimport { useMemo, useState } from 'react';\n\nimport { tv, variantsToClassNames } from '@fuf-stack/pixel-utils';\nimport Button from '@fuf-stack/pixels/Button';\nimport Drawer from '@fuf-stack/pixels/Drawer';\n\nimport { createAuthAdapter } from '../core/createAuthAdapter';\nimport { filterNavigationForState } from '../core/navigation';\nimport { getResourceRenderer } from '../core/renderers';\n\n/**\n * Slot-based class recipe for `AtelierFrame`.\n */\nexport const atelierFrameVariants = tv({\n slots: {\n appName: 'text-base font-semibold tracking-tight',\n base: 'flex min-h-[32rem] flex-col rounded-xl border border-default-200 bg-content1',\n content: 'grid gap-4 p-4 md:grid-cols-[16rem_1fr]',\n header:\n 'flex min-h-14 items-center justify-between border-b border-default-200 px-4',\n navButton:\n 'w-full justify-start rounded-md px-2 py-1 text-sm text-default-700 hover:bg-default-100',\n resourceArea: 'rounded-lg border border-default-200 bg-content2 p-4',\n resourceTitle: 'mb-2 text-sm font-medium text-default-700',\n shellMeta: 'flex items-center gap-2 text-xs text-default-500',\n sidebar:\n 'hidden rounded-lg border border-default-200 bg-content2 p-3 md:block',\n sidebarFooter:\n 'mt-3 border-t border-default-200 pt-3 text-xs text-default-600',\n sidebarSection: 'mb-3',\n sidebarSectionLabel:\n 'mb-1 text-xs font-semibold uppercase text-default-500',\n topBarActions: 'ml-auto flex items-center gap-2',\n },\n});\n\ntype ClassName = TVClassName<typeof atelierFrameVariants>;\n\n/**\n * Props for the core frame component.\n */\nexport interface AtelierFrameProps {\n /** App name shown in the top bar and mobile drawer header. */\n appName: string;\n /** Current auth state used for status and navigation filtering. */\n authState: AtelierAuthState;\n /** Optional slot-based className overrides. */\n className?: ClassName;\n /** Current pathname used to mark active navigation items. */\n currentPath?: string;\n /** Current resource data state passed to renderers. */\n dataState?: AtelierDataState;\n /** Navigation callback triggered when an item is clicked. */\n onNavigate?: (to: string) => void;\n /** Sidebar navigation tree. */\n navigation?: AtelierNavigation;\n /** Resource payload forwarded to the active renderer. */\n resourceData?: unknown;\n /** Resource error forwarded to the active renderer. */\n resourceError?: unknown;\n /** Active resource id. */\n resourceId?: string;\n /** Available resource definitions. */\n resources?: AtelierResourceDefinition[];\n /** Optional slot content overrides. */\n slots?: AtelierFrameSlots;\n /** Prefix for test ids. */\n testId?: string;\n /** Header actions override. */\n topBarActions?: ReactNode;\n /** User menu override. */\n userMenu?: ReactNode;\n /** Enables drawer-based navigation on small screens. */\n withMobileDrawer?: boolean;\n /** Named renderer registry resolved by `rendererId`. */\n rendererRegistry?: AtelierRendererRegistry;\n}\n\nconst defaultResourceId = 'default';\n\nconst getResourceById = (\n resources: AtelierResourceDefinition[],\n resourceId: string,\n): AtelierResourceDefinition | undefined => {\n return resources.find((resource) => {\n return resource.id === resourceId;\n });\n};\n\nconst defaultResource: AtelierResourceDefinition = {\n id: defaultResourceId,\n title: 'Workspace',\n};\n\n/**\n * Reusable frame component for app chrome, navigation, and pluggable\n * resource rendering.\n */\nconst AtelierFrame = ({\n appName,\n authState,\n className = undefined,\n currentPath = '/',\n dataState = 'ready',\n navigation = [],\n onNavigate = undefined,\n rendererRegistry = {},\n resourceData = undefined,\n resourceError = undefined,\n resourceId = defaultResourceId,\n resources = [],\n slots = undefined,\n testId = 'atelier-frame',\n topBarActions = undefined,\n userMenu = undefined,\n withMobileDrawer = true,\n}: AtelierFrameProps) => {\n const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);\n const variants = atelierFrameVariants();\n const classNames = variantsToClassNames(variants, className, 'base');\n\n const auth = createAuthAdapter();\n const visibleNavigation = useMemo(() => {\n return filterNavigationForState(navigation, authState, auth);\n }, [navigation, authState, auth]);\n\n const resolvedResource = getResourceById(resources, resourceId) ??\n resources[0] ?? { ...defaultResource, id: resourceId };\n\n const renderResource = getResourceRenderer(\n resolvedResource,\n rendererRegistry,\n );\n const sharedSlots = slots ?? {};\n const resolvedUserMenu = userMenu ?? sharedSlots.userMenu;\n\n /**\n * Renders the same navigation structure for desktop and mobile contexts.\n */\n const renderSidebar = ({ mobile }: { mobile: boolean }) => {\n return (\n <aside\n className={\n mobile\n ? 'rounded-lg border border-default-200 bg-content2 p-3'\n : classNames.sidebar\n }\n data-testid={`${testId}__sidebar`}\n >\n {visibleNavigation.map((section) => {\n return (\n <div\n key={section.id}\n className={classNames.sidebarSection}\n data-testid={`${testId}__section-${section.id}`}\n >\n {section.label ? (\n <div className={classNames.sidebarSectionLabel}>\n {section.label}\n </div>\n ) : null}\n <nav className=\"grid gap-1\">\n {section.items.map((item) => {\n return (\n <Button\n key={item.id}\n className={classNames.navButton}\n color={item.to === currentPath ? 'primary' : 'default'}\n onClick={() => {\n onNavigate?.(item.to);\n setIsMobileNavOpen(false);\n }}\n variant={item.to === currentPath ? 'solid' : 'light'}\n >\n <span className=\"mr-2 inline-flex\">{item.icon}</span>\n <span>{item.label}</span>\n {item.badge ? (\n <span className=\"ml-auto\">{item.badge}</span>\n ) : null}\n </Button>\n );\n })}\n </nav>\n </div>\n );\n })}\n {sharedSlots.sidebarFooter ? (\n <div className={classNames.sidebarFooter}>\n {sharedSlots.sidebarFooter}\n </div>\n ) : null}\n </aside>\n );\n };\n\n return (\n <section className={classNames.base} data-testid={testId}>\n <header className={classNames.header}>\n <div className={classNames.shellMeta}>\n {withMobileDrawer ? (\n <Button\n className=\"md:hidden\"\n onClick={() => {\n setIsMobileNavOpen(true);\n }}\n size=\"sm\"\n variant=\"flat\"\n >\n Menu\n </Button>\n ) : null}\n <span className={classNames.appName}>{appName}</span>\n <span>status: {authState.status}</span>\n </div>\n <div className={classNames.topBarActions}>\n {topBarActions ?? sharedSlots.headerActions}\n {resolvedUserMenu}\n </div>\n </header>\n <div className={classNames.content}>\n {renderSidebar({ mobile: false })}\n <main className={classNames.resourceArea}>\n <div className={classNames.resourceTitle}>\n {resolvedResource.title}\n </div>\n {sharedSlots.pageChrome}\n {renderResource({\n data: resourceData,\n error: resourceError,\n resourceId: resolvedResource.id,\n state: dataState,\n title: resolvedResource.title,\n })}\n </main>\n </div>\n {withMobileDrawer ? (\n <Drawer\n header={appName}\n isOpen={isMobileNavOpen}\n onClose={() => {\n setIsMobileNavOpen(false);\n }}\n placement=\"left\"\n >\n {renderSidebar({ mobile: true })}\n </Drawer>\n ) : null}\n </section>\n );\n};\n\nexport default AtelierFrame;\n","/* eslint-disable import-x/prefer-default-export */\n\nimport type { AtelierAppConfig, AtelierNavigationSection } from './types';\n\nimport { createAuthAdapter } from './createAuthAdapter';\n\n/**\n * Clones and sanitizes navigation config by dropping empty sections.\n */\nconst normalizeNavigation = (\n sections?: AtelierNavigationSection[],\n): AtelierNavigationSection[] => {\n if (!sections || sections.length <= 0) {\n return [];\n }\n\n return sections\n .filter((section) => {\n return section.items.length > 0;\n })\n .map((section) => {\n return {\n ...section,\n items: [...section.items],\n };\n });\n};\n\n/**\n * Creates a normalized app config object for shell usage.\n *\n * Normalization goals:\n * - always provide an auth adapter with defaults\n * - always provide collections (`navigation`, `renderers`, `resources`)\n * - remove empty navigation sections\n */\nexport const createAtelierApp = (\n config: AtelierAppConfig,\n): Required<\n Pick<AtelierAppConfig, 'appName' | 'navigation' | 'renderers' | 'resources'>\n> &\n AtelierAppConfig => {\n return {\n ...config,\n auth: createAuthAdapter(config.auth),\n navigation: normalizeNavigation(config.navigation),\n renderers: config.renderers ?? {},\n resources: config.resources ?? [],\n };\n};\n","import type { AtelierAuthState, AtelierRendererRegistry } from './types';\n\n/**\n * Shape intended for TanStack Start root route context.\n * Consumers keep route ownership and can extend this interface.\n */\nexport interface AtelierRouteContext {\n authState: AtelierAuthState;\n queryClient?: unknown;\n renderers: AtelierRendererRegistry;\n}\n\n/**\n * Lightweight helper to normalize route context creation in consuming apps.\n */\nexport const createAtelierRouteContext = <T extends AtelierRouteContext>(\n context: T,\n): T => {\n return context;\n};\n","/* v8 ignore start */\n\nexport * from './AtelierFrame';\nexport * from './core';\n\n/* v8 ignore stop */\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@fuf-stack/atelier",
3
+ "version": "0.2.0",
4
+ "description": "fuf customizable base app package",
5
+ "author": "Fröhlich ∧ Frei",
6
+ "homepage": "https://github.com/fuf-stack/pixels/tree/main/packages/atelier",
7
+ "license": "MIT",
8
+ "type": "module",
9
+ "main": "./dist/index.cjs",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.cts",
12
+ "sideEffects": false,
13
+ "exports": {
14
+ ".": {
15
+ "import": "./dist/index.js",
16
+ "require": "./dist/index.cjs"
17
+ },
18
+ "./AtelierFrame": {
19
+ "import": "./dist/AtelierFrame/index.js",
20
+ "require": "./dist/AtelierFrame/index.cjs"
21
+ },
22
+ "./core": {
23
+ "import": "./dist/core/index.js",
24
+ "require": "./dist/core/index.cjs"
25
+ },
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/fuf-stack/pixels.git",
37
+ "directory": "packages/atelier"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/fuf-stack/pixels/issues"
41
+ },
42
+ "scripts": {
43
+ "build": "tsdown",
44
+ "prepack": "pnpm build",
45
+ "test": "vitest ./src"
46
+ },
47
+ "peerDependencies": {
48
+ "react": ">=18",
49
+ "react-dom": ">=18"
50
+ },
51
+ "dependencies": {
52
+ "@fuf-stack/pixel-utils": "workspace:*",
53
+ "@fuf-stack/pixels": "workspace:*"
54
+ },
55
+ "devDependencies": {
56
+ "@types/react": "19.2.17",
57
+ "@types/react-dom": "19.2.3",
58
+ "react": "19.2.7",
59
+ "react-dom": "19.2.7"
60
+ }
61
+ }