/** * Pure helper that turns a SMRT manifest into the `NavItem[]` shape the * existing `NavTree` / `RoleShell` primitives expect. * * This is the "manifest → admin UI" adapter called out as a friction gap in * happyvertical/smrt#1235 / #1226 / #1248: every consumer hand-writes its nav * config today, with dozens of `@smrt()` classes that boilerplate drifts. * Opt in here to manifest-driven nav while keeping the primitives themselves * SmrtObject-agnostic. * * Design constraints (see #1248 for the full brief): * - Pure function. No SvelteKit imports, no SSR coupling, no module side * effects. Data in → data out. * - Cross-industry-safe. Never assumes apparel / furniture / automotive * vocabulary; consumers supply their own `sectionHints`. * - Deterministic output. Sections and items both sort alphabetically by * title so manifest-order churn doesn't shuffle the rendered nav. * - Decoupled from any concrete role/permission package. `permittedResources` * is just a plain string array of qualified class names. * * The structural `SmrtManifestLike` / `SmrtManifestEntryLike` shapes here * intentionally subset the real `SmartObjectManifest` from `@happyvertical/smrt-core`. * Adding a peer dependency on core would create a circular reference (core * pulls in svelte primitives downstream), so the helper accepts anything * that matches the documented shape. Pass a `SmartObjectManifest` directly * and TypeScript is happy. */ /** * Visibility flavours emitted by `@smrt({ visibility })`. Mirrors * `SmrtVisibility` in `@happyvertical/smrt-core` without importing it * (see the file-top note on why we avoid a core peer dep). */ type ManifestVisibility = 'public' | 'internal' | 'test'; /** Subset of `SmartObjectDefinition` the nav helper reads from. */ export interface SmrtManifestEntryLike { /** Qualified name in `@package/name:ClassName` form. Primary key. */ qualifiedName?: string; /** Simple class name (PascalCase). */ className: string; /** Package name where the class is defined. */ packageName?: string; /** Pluralized resource name (used by the REST generator for the URL path). */ collection?: string; /** Base class name as written in source. */ extends?: string; /** * Top-level visibility flag — `SmartObjectDefinition` hoists this out of * `decoratorConfig` so it can be read without parsing the raw config. The * helper reads here first, then falls back to `decoratorConfig.visibility` * for older / hand-built manifests. */ visibility?: ManifestVisibility; /** * The `@smrt({...})` config object captured by the AST scanner. Fields * are optional in the structural type so partial manifests still satisfy * it. The helper specifically looks at `ui?: { icon?, label? }`. */ decoratorConfig?: { ui?: { icon?: string; label?: string; }; /** * Subset of the `@smrt({ api })` shape we care about for nav filtering — * entries with `api: false` (or an `include` that drops `list` / an * `exclude` that drops `list`) don't get a REST list route and so * can't anchor a nav link. */ api?: boolean | { include?: string[]; exclude?: string[]; [key: string]: unknown; }; /** * Raw visibility flag as written in source. Read as a fallback when * the top-level `visibility` field is absent. */ visibility?: ManifestVisibility; [key: string]: unknown; }; } /** Subset of `SmartObjectManifest`. */ export interface SmrtManifestLike { objects: Record; } import type { NavItem } from './types.js'; /** * A nav section emitted by `navTreeFromManifest`. Structurally identical to * `NavItem` — a top-level entry whose `children` are the resources grouped * under it. Aliased for documentation clarity. */ export type NavSection = NavItem; /** Options consumed by {@link navTreeFromManifest}. */ export interface NavTreeFromManifestOptions { /** * Qualified class names a role is allowed to see (e.g. * `'@happyvertical/smrt-content:Article'`). Entries outside this list are * dropped. Omit to include every visible resource. * * Plain string array on purpose — this helper must not depend on * `smrt-users` or any concrete role / permission package. Wire your * resolver (`PermissionResolver` from smrt-users etc.) at the * `+layout.server.ts` level and pass the resulting allow-list in. */ permittedResources?: string[]; /** * Map of qualifier substring → section title. A class's qualified name * is checked against each key with `String.includes()`; the first match * wins. When no hint matches, the section title defaults to the package * suffix after the last `/` and before the `:` — * `@happyvertical/smrt-content:Article` → "smrt-content". * * @example * ```ts * sectionHints: { * '@happyvertical/smrt-content': 'Content', * '@happyvertical/smrt-commerce': 'Commerce', * '@acme/apparel': 'Catalog', * } * ``` */ sectionHints?: Record; /** * Base path for generated hrefs. Defaults to `/api/v1` so the emitted * URLs match SMRT's REST generator (`/api/v1/{collection}`). Pass an * empty string to emit unprefixed paths like `/articles`. */ basePath?: string; } /** * Minimal English pluralizer. Handles the common suffix rules sufficient * for class-name → label conversion (Article → Articles, Category → * Categories, Box → Boxes, etc.). For irregulars or exotic plurals, * override with `@smrt({ ui: { label: 'Foo bars' } })`. * * Exported only for tests / advanced callers; most code should let * `navTreeFromManifest` apply it. */ export declare function pluralizeClassName(name: string): string; /** * Walk a SMRT manifest and emit the `NavSection[]` shape that * `` and `RoleConfig.sections` consume. * * Algorithm: * 1. Drop collection classes (`*Collection` / `extends: 'SmrtCollection'`). * 2. Drop entries marked `@smrt({ visibility: 'internal' | 'test' })` — * they're plumbing / fixtures and never belong in admin nav. * 3. Drop entries that don't expose a REST `list` route (`@smrt({ api: false })`, * `include` without `list`, or `exclude` containing `list`). * 4. Drop STI subtypes that share their parent's `collection` value — * the REST endpoint at the shared collection URL is polymorphic and * shows all subtypes through one link. * 5. If `permittedResources` is provided, drop entries whose qualified * name isn't in the list. Otherwise keep all remaining entries. * 6. Group remaining entries by their resolved section title. * 4. For each entry, emit a `NavItem` with: * - `label` = `decoratorConfig.ui.label` ?? `pluralizeClassName(className)` * - `href` = `${basePath}/${collection}` (defaults to `/api/v1/{collection}`) * falling back to `${basePath}/{kebab-case-class}` if the * manifest entry omits `collection`. * - `icon` = `decoratorConfig.ui.icon` if present. * 5. Sort items inside each section by label, then sort sections by title. * The result is fully deterministic given a manifest input. * * @example Plug into RoleShell * ```ts * import { manifest } from '@my-app/manifest'; * import { navTreeFromManifest } from '@happyvertical/smrt-svelte/workspace'; * * const sections = navTreeFromManifest(manifest, { * sectionHints: { * '@happyvertical/smrt-content': 'Content', * '@happyvertical/smrt-commerce': 'Commerce', * }, * }); * const roles: RoleConfig[] = [ * { id: 'admin', label: 'Admin', sections }, * ]; * ``` * * @example Per-role filtering * ```ts * const editor = navTreeFromManifest(manifest, { * permittedResources: [ * '@happyvertical/smrt-content:Article', * '@happyvertical/smrt-content:Document', * ], * }); * // editor: [{ label: 'Content', children: [Articles, Documents] }] * ``` * * @example Unprefixed hrefs * ```ts * const sections = navTreeFromManifest(manifest, { basePath: '' }); * // items[0].href === '/articles' (not '/api/v1/articles') * ``` */ export declare function navTreeFromManifest(manifest: SmrtManifestLike, options?: NavTreeFromManifestOptions): NavSection[]; export {}; //# sourceMappingURL=manifest-nav.d.ts.map