@petrarca/sonnet-shell 0.3.0 → 0.4.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/README.md CHANGED
@@ -4,13 +4,28 @@ Application shell, layout, and navigation for the Petrarca Sonnet component libr
4
4
 
5
5
  ## What's included
6
6
 
7
- **AppShell** -- Full application shell with icon rail, top bar, side pane, sub-navigation, command menu, and confirm dialogs.
7
+ **RootLayout / AppShell** Full application shell: TopBar, navigation
8
+ (icon-rail or sidebar), SidePane, content area, overlays, command menu.
8
9
 
9
- **Imperative API** -- Shell capabilities exposed as simple function calls: `notification.success()`, `panel.open()`, `navigation.go()`, `dialog.confirm()`, `fullscreen.enter()`.
10
+ **Navigation layouts** Two built-in layouts driven by the same module metadata:
11
+ - `ShellRail` — narrow icon strip + contextual sub-nav panel (default)
12
+ - `ShellSidebar` — single-column sidebar with icons, labels, and collapsible sections
10
13
 
11
- **Module registry** -- Register application modules with navigation, routes, and search providers. The shell renders the navigation and routes automatically.
14
+ **Navigation primitives** Compose custom layouts from `IconRail`, `RailIcon`,
15
+ `RailSeparator`, `Sidebar`, `SidebarGroup`, `SidebarItem`, `SubNavPanel`.
12
16
 
13
- **Auth components** (subpath: `@petrarca/sonnet-shell/auth`) -- Login, ProtectedRoute, and TenantSelection components with prop-driven auth configuration. No auth logic baked in -- the host app provides the auth state.
17
+ **Shell chrome** `TopBar`, `ShellFooter`, `ShellVersion` for header/footer slots.
18
+
19
+ **Imperative API** — Shell capabilities as simple function calls from any module:
20
+ `notification`, `dialog`, `navigation`, `panel`, `sidePane`, `fullscreen`, `events`.
21
+
22
+ **Module system** — Register app modules (`ShellModule`) with routes, navigation,
23
+ Cmd+K commands, and side pane config. `createModuleRegistry()` aggregates them.
24
+
25
+ **Auth components** (`@petrarca/sonnet-shell/auth`) — `Login`, `ProtectedRoute`,
26
+ `TenantSelection`. Prop-driven — no auth logic baked in.
27
+
28
+ ---
14
29
 
15
30
  ## Install
16
31
 
@@ -18,8 +33,228 @@ Application shell, layout, and navigation for the Petrarca Sonnet component libr
18
33
  pnpm add @petrarca/sonnet-shell @petrarca/sonnet-ui @petrarca/sonnet-core
19
34
  ```
20
35
 
21
- Peer dependencies: `react`, `react-dom`, `react-router-dom`, `tailwindcss`.
36
+ Peer dependencies: `react >=19`, `react-dom >=19`, `react-router-dom`, `tailwindcss`.
37
+
38
+ ---
39
+
40
+ ## Setup
41
+
42
+ ### 1. Define modules
43
+
44
+ Each feature area is a `ShellModule`:
45
+
46
+ ```ts
47
+ // src/modules/home/index.ts
48
+ import { Home } from "lucide-react";
49
+ import type { ShellModule } from "@petrarca/sonnet-shell";
50
+ import { routes } from "./routes";
51
+
52
+ const homeModule: ShellModule = {
53
+ id: "home",
54
+ label: "Home",
55
+ icon: Home,
56
+ basePath: "/home",
57
+ navigation: [
58
+ {
59
+ id: "main",
60
+ links: [
61
+ { id: "home.overview", label: "Overview", path: "/home" },
62
+ { id: "home.settings", label: "Settings", path: "/home/settings" },
63
+ ],
64
+ },
65
+ ],
66
+ // Optional: fixed top-zone links in sidebar mode (no heading, separator below)
67
+ topNav: [
68
+ { id: "home.overview", label: "Overview", path: "/home" },
69
+ ],
70
+ routes,
71
+ };
72
+
73
+ export default homeModule;
74
+ ```
75
+
76
+ ### 2. Create the registry
77
+
78
+ ```ts
79
+ // src/modules/registry.ts
80
+ import { createModuleRegistry } from "@petrarca/sonnet-shell";
81
+ import home from "./home";
82
+ import settings from "./settings";
83
+
84
+ const registry = createModuleRegistry([home, settings]);
85
+
86
+ export const { allRoutes } = registry;
87
+ export default registry;
88
+ ```
89
+
90
+ ### 3. Wire the shell
91
+
92
+ ```tsx
93
+ // src/routes/AppRouter.tsx
94
+ import { createBrowserRouter, RouterProvider, Navigate } from "react-router-dom";
95
+ import {
96
+ RootLayout,
97
+ SearchTrigger,
98
+ UserMenu,
99
+ ShellVersion,
100
+ type ShellConfig,
101
+ } from "@petrarca/sonnet-shell";
102
+ import pkg from "../../package.json";
103
+ import registry from "@/modules/registry";
104
+
105
+ function TopBarContent() {
106
+ return (
107
+ <>
108
+ <div className="flex items-center gap-3">
109
+ <span className="text-sm font-semibold">MY APP</span>
110
+ </div>
111
+ <div className="flex items-center gap-2">
112
+ <SearchTrigger />
113
+ <UserMenu user={...} onSignOut={...} />
114
+ </div>
115
+ </>
116
+ );
117
+ }
118
+
119
+ const shellConfig: ShellConfig = {
120
+ topBar: <TopBarContent />,
121
+ footer: <ShellVersion name="My App" version={pkg.version} />,
122
+ };
123
+
124
+ const router = createBrowserRouter([
125
+ {
126
+ element: <RootLayout config={shellConfig} registry={registry} />,
127
+ children: [
128
+ { index: true, element: <Navigate to="/home" replace /> },
129
+ ...registry.allRoutes,
130
+ ],
131
+ },
132
+ ]);
133
+
134
+ export function AppRouter() {
135
+ return <RouterProvider router={router} />;
136
+ }
137
+ ```
138
+
139
+ ### 4. CSS and Tailwind
140
+
141
+ ```css
142
+ /* index.css */
143
+ @import "@petrarca/sonnet-ui/styles.css";
144
+
145
+ /* Shell layout — app-level, not provided by the library */
146
+ html, body, #root { height: 100%; }
147
+ #root { display: flex; flex-direction: column; overflow: hidden; }
148
+ body { min-height: 100vh; overflow: hidden; }
149
+ ```
150
+
151
+ ```js
152
+ // tailwind.config.js
153
+ module.exports = {
154
+ presets: [require("@petrarca/sonnet-ui/tailwind-preset")],
155
+ content: [
156
+ "./src/**/*.{ts,tsx}",
157
+ "./node_modules/@petrarca/sonnet-*/dist/**/*.js",
158
+ ],
159
+ plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
160
+ };
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Sidebar layout
166
+
167
+ Pass a `sidebar` prop to `RootLayout` to replace the default icon-rail with a
168
+ single-column sidebar:
169
+
170
+ ```tsx
171
+ import { ShellSidebar } from "@petrarca/sonnet-shell";
172
+
173
+ // Auto-wired from registry metadata:
174
+ <RootLayout config={shellConfig} registry={registry} sidebar={<ShellSidebar />} />
175
+
176
+ // Or fully custom:
177
+ import { Sidebar, SidebarGroup, SidebarItem } from "@petrarca/sonnet-shell";
178
+
179
+ <RootLayout
180
+ config={shellConfig}
181
+ registry={registry}
182
+ sidebar={
183
+ <Sidebar>
184
+ <SidebarGroup separator>
185
+ <SidebarItem icon={Home} label="Overview" path="/home" />
186
+ </SidebarGroup>
187
+ <SidebarGroup heading="PROJECTS" collapsible>
188
+ <SidebarItem icon={Folder} label="Alpha" path="/projects/alpha" />
189
+ </SidebarGroup>
190
+ </Sidebar>
191
+ }
192
+ />
193
+ ```
194
+
195
+ `topNav` on a `ShellModule` populates the top separator zone automatically
196
+ when using `ShellSidebar`.
197
+
198
+ ---
199
+
200
+ ## Imperative API
201
+
202
+ Any module can call the shell API without prop drilling:
203
+
204
+ ```ts
205
+ import { notification, dialog, navigation, panel, sidePane } from "@petrarca/sonnet-shell";
206
+
207
+ notification.success("Saved.");
208
+ notification.error("Failed to save.");
209
+
210
+ const confirmed = await dialog.confirm({
211
+ title: "Delete item?",
212
+ confirmLabel: "Delete",
213
+ variant: "destructive",
214
+ });
215
+
216
+ navigation.goTo("/home");
217
+ navigation.goToFeature("home.settings"); // stable id, path-independent
218
+
219
+ panel.open({
220
+ title: "Details",
221
+ content: <MyPanel />,
222
+ width: "default",
223
+ });
224
+
225
+ sidePane.toggle({ moduleId: "my-module", content: <MySidePane /> });
226
+ ```
227
+
228
+ ---
229
+
230
+ ## ShellModule reference
231
+
232
+ ```ts
233
+ interface ShellModule {
234
+ id: string; // unique, e.g. "terminology"
235
+ label: string; // shown in rail tooltip, sub-nav header
236
+ description?: string; // shown in Cmd+K
237
+ icon: LucideIcon;
238
+
239
+ basePath: string; // URL prefix, e.g. "/terminology"
240
+ routes: RouteObject[]; // React Router routes
241
+
242
+ topNav?: NavLink[]; // fixed top-zone links (sidebar only)
243
+ navigation: NavGroup[]; // grouped nav links (rail + sidebar)
244
+
245
+ pinBottom?: boolean; // pin to bottom of rail / sidebar tools group
246
+ hidden?: boolean; // hide from navigation entirely
247
+
248
+ contributions?: Contribution[]; // inject links into other modules' nav
249
+ commands?: ModuleCommand[]; // Cmd+K actions
250
+
251
+ sidePane?: { ... }; // inline resizable panel config
252
+ layout?: "default" | "full"; // "full" removes scroll wrapper and padding
253
+ }
254
+ ```
255
+
256
+ ---
22
257
 
23
258
  ## License
24
259
 
25
- Apache 2.0
260
+ See [LICENSE.md](../../LICENSE.md).
package/dist/index.d.ts CHANGED
@@ -199,7 +199,50 @@ interface ShellModule {
199
199
  * canvases, or any route that manages its own scroll and height.
200
200
  */
201
201
  layout?: "default" | "full";
202
+ /**
203
+ * Optional module-owned effect hook, invoked once by the shell inside the
204
+ * shell tree (so it has Router / QueryClient / provider context available).
205
+ *
206
+ * Use this for app-state reactions that belong to the module rather than to
207
+ * central app code — e.g. subscribing to a shell event and invalidating the
208
+ * module's own caches. The shell calls it unconditionally on every render, so
209
+ * it MUST obey the Rules of Hooks (call hooks unconditionally, no early
210
+ * returns) and the set of modules MUST be stable for the app's lifetime.
211
+ *
212
+ * It renders no UI; return nothing.
213
+ */
214
+ useEffects?: () => void;
215
+ /**
216
+ * Capabilities this module requires to be available, as "domain:name" strings
217
+ * (e.g. "state:graph.selected", "auth:templates.read"). Combined with implicit
218
+ * AND. Absent ⇒ the module is always available.
219
+ *
220
+ * The shell does not interpret these — the app's `resolveCapability` evaluates
221
+ * each one. See docs/design/module-capabilities.md.
222
+ */
223
+ requires?: string[];
202
224
  }
225
+ /** Semantic reason a capability is unmet. Drives the presentation matrix. */
226
+ type UnmetKind = "missing-state" | "not-authorized" | "not-authorized-soft" | "feature-off" | "error";
227
+ /** Result of evaluating a single capability. Decision only — never presentation. */
228
+ interface CapabilityResult {
229
+ met: boolean;
230
+ /** Why the capability is unmet (only when met === false). */
231
+ kind?: UnmetKind;
232
+ /** Optional human-readable hint, surfaced in tooltips / adjacent text. */
233
+ reason?: string;
234
+ }
235
+ /** App-provided function that evaluates a single capability string. */
236
+ type CapabilityResolver = (capability: string) => CapabilityResult;
237
+ /** A shell surface that gates modules. */
238
+ type Surface = "rail" | "command" | "route";
239
+ /** How an unavailable module renders on a surface. */
240
+ type Effect = "visible" | "disabled" | "hidden" | "redirect";
241
+ /**
242
+ * Maps (unmet reason kind, surface) → effect. Sparse overrides are merged over
243
+ * the shell defaults (`DEFAULT_CAPABILITY_POLICY`).
244
+ */
245
+ type CapabilityPolicy = Partial<Record<UnmetKind, Partial<Record<Surface, Effect>>>>;
203
246
 
204
247
  /**
205
248
  * Module registry factory.
@@ -256,6 +299,18 @@ interface ShellConfig {
256
299
  * Omit to render no footer.
257
300
  */
258
301
  footer?: ReactNode;
302
+ /**
303
+ * Evaluates a module capability (from `ShellModule.requires`) for gating the
304
+ * icon rail / command menu / routes. Absent ⇒ no gating (all modules
305
+ * available). See docs/design/module-capabilities.md.
306
+ */
307
+ resolveCapability?: CapabilityResolver;
308
+ /**
309
+ * Sparse overrides for the (unmet reason kind, surface) → effect matrix,
310
+ * merged over the shell defaults. Lets the app tune hide-vs-disable per
311
+ * reason kind and surface without touching modules.
312
+ */
313
+ capabilityPolicy?: CapabilityPolicy;
259
314
  }
260
315
  /**
261
316
  * Read the shell configuration from context.
@@ -309,9 +364,13 @@ interface RailIconProps {
309
364
  label: string;
310
365
  active?: boolean;
311
366
  onClick?: () => void;
367
+ /** Render greyed and non-interactive (e.g. a capability requirement is unmet). */
368
+ disabled?: boolean;
369
+ /** Optional tooltip override (e.g. why the item is disabled). Defaults to `label`. */
370
+ tooltip?: string;
312
371
  }
313
372
  /** Single icon button with tooltip inside an IconRail. */
314
- declare function RailIcon({ icon: Icon, label, active, onClick, }: RailIconProps): React__default.ReactElement;
373
+ declare function RailIcon({ icon: Icon, label, active, onClick, disabled, tooltip, }: RailIconProps): React__default.ReactElement;
315
374
  /** Visual divider between icon groups inside an IconRail. */
316
375
  declare function RailSeparator(): React__default.ReactElement;
317
376
 
@@ -510,6 +569,25 @@ interface UserMenuProps {
510
569
  }
511
570
  declare function UserMenu({ user, onSignOut }: UserMenuProps): react_jsx_runtime.JSX.Element;
512
571
 
572
+ /** Default (kind, surface) → effect matrix. App overrides are merged over this. */
573
+ declare const DEFAULT_CAPABILITY_POLICY: Record<UnmetKind, Record<Surface, Effect>>;
574
+ /** Merge a sparse app override over the default matrix. */
575
+ declare function resolveCapabilityPolicy(override?: CapabilityPolicy): Record<UnmetKind, Record<Surface, Effect>>;
576
+ interface ModuleEffect {
577
+ effect: Effect;
578
+ /** Hint from the first most-restrictive unmet capability (for tooltips). */
579
+ reason?: string;
580
+ }
581
+ /**
582
+ * Evaluate a module's requirements for a given surface.
583
+ *
584
+ * Returns `visible` when there are no requirements or all are met. Otherwise
585
+ * each unmet capability maps (kind, surface) → effect and the most restrictive
586
+ * wins (hidden > redirect > disabled > visible). When capabilities share the
587
+ * same effect rank, the first one with a non-empty reason provides the tooltip.
588
+ */
589
+ declare function evaluateModule(module: ShellModule, surface: Surface, resolve: CapabilityResolver | undefined, policy: Record<UnmetKind, Record<Surface, Effect>>): ModuleEffect;
590
+
513
591
  /**
514
592
  * Shell API -- imperative domain capabilities for modules.
515
593
  *
@@ -614,6 +692,22 @@ interface PanelOptions {
614
692
  * - "full": panel covers the full viewport height including the TopBar.
615
693
  */
616
694
  coverage?: "below-header" | "full";
695
+ /**
696
+ * Allow the user to drag the left edge of the panel to resize it.
697
+ * Defaults to false. When true, the fixed `width` token is used as the
698
+ * initial width and the panel width is controlled dynamically.
699
+ */
700
+ resizable?: boolean;
701
+ /**
702
+ * Minimum width in pixels when resizable. Defaults to 240.
703
+ * Ignored when resizable is false.
704
+ */
705
+ minWidth?: number;
706
+ /**
707
+ * Maximum width in pixels when resizable. Defaults to 90% of viewport width.
708
+ * Ignored when resizable is false.
709
+ */
710
+ maxWidth?: number;
617
711
  /** Called after the panel finishes closing. Use to restore focus to the triggering element. */
618
712
  onClose?: () => void;
619
713
  }
@@ -789,4 +883,4 @@ declare function useMainModules(): ShellModule[];
789
883
  */
790
884
  declare function useShellEvent<K extends keyof ShellEventMap>(key: K, handler: (payload: ShellEventMap[K]) => void): void;
791
885
 
792
- export { AppShell, CommandMenu, ConfirmDialog, type ConfirmDialogProps, type ConfirmOptions, type Contribution, FeatureLink, type FullscreenOptions, IconRail, type IconRailProps, type ModuleCommand, type ModuleRegistry, type NavGroup, type NavLink, OverviewCard, type PanelOptions, PlaceholderPage, RailIcon, type RailIconProps, RailSeparator, RootLayout, SearchTrigger, type ShellConfig, ShellConfigProvider, type ShellEventMap, ShellFooter, type ShellFooterProps, type ShellModule, type ShellNavigationState, ShellRail, ShellSidebar, type ShellSidebarProps, ShellVersion, type ShellVersionProps, SidePane, type SidePaneOptions as SidePaneApiOptions, SidePaneContext, type SidePaneOptions, type SidePaneState, Sidebar, SidebarGroup, type SidebarGroupProps, SidebarItem, type SidebarItemProps, type SidebarProps, SubNavPanel, type SubNavPanelProps, TopBar, UserMenu, type UserMenuUser, createModuleRegistry, dialog, events, fullscreen, initDialog, initFeatureNav, initFullscreen, initNavigation, initPanel, initSidePane, navigation, notification, panel, sidePane, useExtensionPoint, useMainModules, useShellConfig, useShellEvent, useShellModules, useShellNavigation, useSidePaneState };
886
+ export { AppShell, type CapabilityPolicy, type CapabilityResolver, type CapabilityResult, CommandMenu, ConfirmDialog, type ConfirmDialogProps, type ConfirmOptions, type Contribution, DEFAULT_CAPABILITY_POLICY, type Effect, FeatureLink, type FullscreenOptions, IconRail, type IconRailProps, type ModuleCommand, type ModuleEffect, type ModuleRegistry, type NavGroup, type NavLink, OverviewCard, type PanelOptions, PlaceholderPage, RailIcon, type RailIconProps, RailSeparator, RootLayout, SearchTrigger, type ShellConfig, ShellConfigProvider, type ShellEventMap, ShellFooter, type ShellFooterProps, type ShellModule, type ShellNavigationState, ShellRail, ShellSidebar, type ShellSidebarProps, ShellVersion, type ShellVersionProps, SidePane, type SidePaneOptions as SidePaneApiOptions, SidePaneContext, type SidePaneOptions, type SidePaneState, Sidebar, SidebarGroup, type SidebarGroupProps, SidebarItem, type SidebarItemProps, type SidebarProps, SubNavPanel, type SubNavPanelProps, type Surface, TopBar, type UnmetKind, UserMenu, type UserMenuUser, createModuleRegistry, dialog, evaluateModule, events, fullscreen, initDialog, initFeatureNav, initFullscreen, initNavigation, initPanel, initSidePane, navigation, notification, panel, resolveCapabilityPolicy, sidePane, useExtensionPoint, useMainModules, useShellConfig, useShellEvent, useShellModules, useShellNavigation, useSidePaneState };