@petrarca/sonnet-shell 0.5.0 → 0.7.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
@@ -1,31 +1,36 @@
1
1
  # @petrarca/sonnet-shell
2
2
 
3
- Application shell, layout, and navigation for the Petrarca Sonnet component library.
3
+ The application frame of the Petrarca Sonnet component library. You write
4
+ feature areas as modules; the shell composes them into navigation, routes, a
5
+ command menu and a set of surfaces any module can reach imperatively.
4
6
 
5
- ## What's included
7
+ ## What it provides
6
8
 
7
- **RootLayout / AppShell** — Full application shell: TopBar, navigation
8
- (icon-rail or sidebar), SidePane, content area, overlays, command menu.
9
+ **A module system.** A `ShellModule` declares its routes, navigation, command
10
+ menu entries, side pane and contributions to other modules.
11
+ `createModuleRegistry` aggregates them and derives the navigation zones and the
12
+ flattened route list once.
9
13
 
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
14
+ **Two navigation layouts.** `ShellRail`, a narrow icon strip with a contextual
15
+ sub-nav panel, and `ShellSidebar`, a single-column sidebar with collapsible
16
+ sections. Both read the same module metadata, and `IconRail`, `Sidebar`,
17
+ `SidebarGroup`, `SidebarItem` and `SubNavPanel` are exported for a custom one.
13
18
 
14
- **Navigation primitives** — Compose custom layouts from `IconRail`, `RailIcon`,
15
- `RailSeparator`, `Sidebar`, `SidebarGroup`, `SidebarItem`, `SubNavPanel`.
19
+ **Shell chrome.** `RootLayout` and `AppShell`, `TopBar`, `ShellFooter`,
20
+ `ShellVersion`, `SearchTrigger` and `UserMenu`.
16
21
 
17
- **Shell chrome** — `TopBar`, `ShellFooter`, `ShellVersion` for header/footer slots.
22
+ **An imperative API.** `notification`, `dialog`, `navigation`, `panel`,
23
+ `sidePane`, `fullscreen` and `events` are plain function calls, so a page
24
+ reaches a toast, a confirm dialog, a slide-over or another module without prop
25
+ drilling.
18
26
 
19
- **Imperative API** — Shell capabilities as simple function calls from any module:
20
- `notification`, `dialog`, `navigation`, `panel`, `sidePane`, `fullscreen`, `events`.
27
+ **Capability gating.** A module can declare what it `requires`; the application
28
+ supplies `resolveCapability` and a policy decides whether an unmet module is
29
+ hidden, disabled or shown with an explanation.
21
30
 
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
- ---
31
+ **Auth screens** on the `./auth` subpath: `Login`, `ProtectedRoute` and
32
+ `OrgSelection`. Prop-driven, with no auth logic of their own -- organization
33
+ choice is the `orgs` and `onSelectOrg` props of `Login`.
29
34
 
30
35
  ## Install
31
36
 
@@ -33,92 +38,29 @@ Cmd+K commands, and side pane config. `createModuleRegistry()` aggregates them.
33
38
  pnpm add @petrarca/sonnet-shell @petrarca/sonnet-ui @petrarca/sonnet-core
34
39
  ```
35
40
 
36
- Peer dependencies: `react >=19`, `react-dom >=19`, `react-router-dom`, `tailwindcss`.
37
-
38
- ---
39
-
40
- ## Setup
41
+ | Peer | Range | Needed for |
42
+ |---|---|---|
43
+ | `react`, `react-dom` | `>=19` | Everything |
44
+ | `react-router-dom` | `>=7` | Routing, `SidebarItem`, `ProtectedRoute`; the shell is built on the router, not adapted to it |
45
+ | `tailwindcss` | `>=3` | The classes the shell renders |
41
46
 
42
- ### 1. Define modules
47
+ `lucide-react` and `sonner` arrive as regular dependencies.
43
48
 
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
- };
49
+ ## Usage
72
50
 
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
51
+ Declare modules, build a registry, mount `RootLayout` as a layout route.
91
52
 
92
53
  ```tsx
93
- // src/routes/AppRouter.tsx
94
54
  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";
55
+ import { RootLayout, createModuleRegistry } from "@petrarca/sonnet-shell";
56
+ import type { ShellConfig } from "@petrarca/sonnet-shell";
57
+ import home from "@/modules/home";
58
+ import widgets from "@/modules/widgets";
104
59
 
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
- }
60
+ const registry = createModuleRegistry([home, widgets]);
118
61
 
119
62
  const shellConfig: ShellConfig = {
120
63
  topBar: <TopBarContent />,
121
- footer: <ShellVersion name="My App" version={pkg.version} />,
122
64
  };
123
65
 
124
66
  const router = createBrowserRouter([
@@ -136,125 +78,28 @@ export function AppRouter() {
136
78
  }
137
79
  ```
138
80
 
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
- ---
81
+ A module is a plain object with `id`, `label`, `icon`, `basePath`, `navigation`
82
+ and `routes`, plus optional `matchPaths`, `topNav`, `commands`,
83
+ `contributions`, `sidePane`, `layout`, `useEffects`, `requires`, `pinBottom`
84
+ and `hidden`. The full contract is in [modules](docs/usage/modules.md).
164
85
 
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";
86
+ The shell sizes itself from the document and the library does not set document
87
+ styles, so the app-level layout CSS in
88
+ [getting started](docs/usage/getting-started.md) is required -- omitting it is
89
+ the most common setup failure.
172
90
 
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
- ```
91
+ ## Documentation
255
92
 
256
- ---
93
+ - [Usage guides](docs/usage/index.md) -- getting started,
94
+ [modules](docs/usage/modules.md), [navigation](docs/usage/navigation.md),
95
+ the [imperative API](docs/usage/imperative-api.md),
96
+ [events](docs/usage/events.md), [capabilities](docs/usage/capabilities.md)
97
+ and [auth](docs/usage/auth.md).
98
+ - [Design docs](docs/design/index.md) -- the navigation layout model and
99
+ module capabilities.
100
+ - [Workspace documentation](../../docs/README.md) -- how the packages layer and
101
+ the architecture decision records.
257
102
 
258
103
  ## License
259
104
 
260
- See [LICENSE.md](../../LICENSE.md).
105
+ Apache 2.0. See [LICENSE.md](../../LICENSE.md).
@@ -1,43 +1,60 @@
1
1
  import { ReactNode } from 'react';
2
2
 
3
3
  /**
4
- * TenantSelection -- shown after authentication when the user has
5
- * multiple tenants and no default is set. The user must pick a tenant
6
- * before the app loads.
4
+ * OrgSelection -- shown after authentication when the user belongs to
5
+ * multiple organizations and no default is set. The user selects which
6
+ * organization context to act in before the app loads.
7
+ *
8
+ * "Organization" is used generically: an app might map it to an org-unit,
9
+ * tenant, workspace, team, or company. The consumer maps its domain type to
10
+ * OrgOption.
7
11
  *
8
12
  * Fully props-driven with no app-specific imports.
9
13
  * Target package: @petrarca/sonnet-shell
10
14
  */
11
- /** Minimal tenant descriptor. Consumers map their domain type to this shape. */
12
- interface TenantOption {
15
+ /** Minimal organization descriptor. Consumers map their domain type to this shape. */
16
+ interface OrgOption {
13
17
  id: string;
14
18
  name: string;
15
19
  }
16
- interface TenantSelectionProps {
17
- tenants: TenantOption[];
20
+ interface OrgSelectionProps {
21
+ orgs: OrgOption[];
18
22
  isLoading: boolean;
19
23
  error: string | null;
20
24
  onSelect: (id: string) => void;
25
+ /**
26
+ * Rendered in place of the picker when `orgs` is empty. The library states
27
+ * the fact ("No organizations assigned") but cannot know why the list is
28
+ * empty -- pending provisioning, filtered memberships, or none at all -- so
29
+ * the escape (sign out, retry, contact support) is the consumer's to supply.
30
+ * Omitted, the message stands alone.
31
+ */
32
+ emptyAction?: React.ReactNode;
21
33
  }
22
- declare function TenantSelection({ tenants, isLoading, error, onSelect, }: TenantSelectionProps): React.ReactElement;
34
+ declare function OrgSelection({ orgs, isLoading, error, onSelect, emptyAction, }: OrgSelectionProps): React.ReactElement;
23
35
 
24
36
  /**
25
37
  * Login -- multi-step authentication flow.
26
38
  *
27
- * Step 1: Credentials (local auth form or OIDC redirect)
28
- * Step 2: Tenant selection (if user has multiple tenants and no default)
39
+ * Steps:
40
+ * identifier -- email/username entry (when onDiscover is provided)
41
+ * auth -- resolved providers: password form first, SSO buttons below
42
+ * credentials -- single-screen login (no onDiscover)
43
+ * org-selection -- organization picker after successful auth
44
+ *
45
+ * When onDiscover is NOT provided, the identifier and password sit on one
46
+ * screen instead of the identifier-first two-step flow.
29
47
  *
30
- * Steps are shown conditionally. If the user has a default tenant,
31
- * step 2 is skipped and the app loads immediately.
48
+ * All async operations are injected via props. The component knows nothing
49
+ * about OIDC, PKCE, or discover endpoints.
32
50
  *
33
- * ProtectedRoute passes a `step` hint in location.state so this
34
- * component always opens on the correct step when redirected.
51
+ * Design: a centred card on a quiet page, in the spirit of Auth0 Universal
52
+ * Login / WorkOS AuthKit. Every colour comes from a design token, so the screen
53
+ * follows the consumer's theme -- the distinct composition is deliberate, the
54
+ * palette is not a private one.
35
55
  *
36
- * All auth state and operations are injected via props -- no direct
37
- * store or context imports.
38
56
  * Target package: @petrarca/sonnet-shell
39
57
  */
40
- /** Auth feature flags and provider configuration. Consumer-supplied. */
41
58
  interface AuthConfig {
42
59
  enabled: boolean;
43
60
  features: {
@@ -55,30 +72,58 @@ interface AuthConfig {
55
72
  direct_redirect: string | null;
56
73
  };
57
74
  }
75
+ interface LoginProvider {
76
+ type: "local" | "external";
77
+ name: string;
78
+ label: string;
79
+ /**
80
+ * Opaque per-provider data the app's discover step attaches and its
81
+ * `onExternalLogin` reads back -- e.g. an OIDC authorize URL and target id.
82
+ * The shell never inspects it; it only carries it through. This keeps Login
83
+ * domain-agnostic (it "knows nothing about OIDC, PKCE, or discover
84
+ * endpoints") while sparing the consumer an `as any` cast on the provider.
85
+ */
86
+ metadata?: Record<string, unknown>;
87
+ }
58
88
  interface LoginProps {
59
- /** Auth feature flags and OAuth provider config. */
60
89
  config: AuthConfig | null;
61
- /** Whether the user is currently authenticated (token present). */
62
90
  isAuthenticated: boolean;
63
- /** Active tenant id, null when not yet selected. */
64
91
  activeTenantId: string | null;
65
- /** Whether an async operation is in flight. */
66
92
  isLoading: boolean;
67
- /** Auth error message to display, null when none. */
68
93
  error: string | null;
69
- /** The authenticated user's available tenants. */
70
- tenants: TenantOption[] | null;
71
- /** Perform credential login. Throws on failure. */
94
+ orgs: OrgOption[] | null;
72
95
  onLogin: (credentials: {
73
96
  identifier: string;
74
97
  password: string;
75
98
  }) => Promise<void>;
76
- /** Fetch the current user after login to resolve tenant state. */
77
99
  onFetchCurrentUser: () => Promise<void>;
78
- /** Switch to the given tenant. Throws on failure. */
79
- onSwitchTenant: (id: string) => Promise<void>;
80
- /** Clear the current auth error. */
100
+ onSelectOrg: (id: string) => Promise<void>;
81
101
  onClearError: () => void;
102
+ /** Application name shown above the card as a brand label. */
103
+ appName?: string;
104
+ /** Logo URL shown above the card. If provided, appName is used as alt text. */
105
+ logoUrl?: string;
106
+ /** Enables identifier-first flow. App calls discover endpoint and returns providers. */
107
+ onDiscover?: (identifier: string) => Promise<LoginProvider[]>;
108
+ /** Called when user clicks an external provider button. App handles PKCE + redirect. */
109
+ onExternalLogin?: (provider: LoginProvider) => Promise<void> | void;
110
+ /** Whether discovery is in flight. */
111
+ isDiscovering?: boolean;
112
+ /**
113
+ * Rendered on the organization step when the user has no organizations to
114
+ * select. The library cannot know why the list is empty, so the escape --
115
+ * sign out, retry, contact support -- is the consumer's to supply.
116
+ * See `OrgSelection`'s `emptyAction`.
117
+ */
118
+ orgEmptyAction?: React.ReactNode;
119
+ /**
120
+ * Whether the user must select an organization after authenticating. Default
121
+ * true. Set false for apps with no organization/tenant concept: the org step
122
+ * is never entered, a stale org-selection step from location state is
123
+ * ignored, and an authenticated user proceeds into the app even when
124
+ * `activeTenantId` is null. Pair with `ProtectedRoute`'s matching prop.
125
+ */
126
+ requiresOrgSelection?: boolean;
82
127
  }
83
128
  declare function Login(props: LoginProps): React.ReactElement | null;
84
129
 
@@ -92,7 +137,10 @@ declare function Login(props: LoginProps): React.ReactElement | null;
92
137
  * Once initialization is complete, guards based on:
93
138
  * - Auth disabled (`authEnabled` false): allow all access
94
139
  * - Not authenticated: redirect to /login with step=credentials
95
- * - Authenticated but no tenant: redirect to /login with step=tenant-selection
140
+ * - Authenticated but no tenant: redirect to /login with step=org-selection
141
+ *
142
+ * Apps with no organization/tenant concept set `requiresOrgSelection={false}`;
143
+ * an authenticated user is then let through and `activeTenantId` is ignored.
96
144
  *
97
145
  * All auth state is injected via props -- no direct store or context imports.
98
146
  * Target package: @petrarca/sonnet-shell
@@ -111,7 +159,13 @@ interface ProtectedRouteProps {
111
159
  activeTenantId: string | null;
112
160
  /** Called once on mount to restore and validate a persisted session. */
113
161
  initializeAuth: () => Promise<void>;
162
+ /**
163
+ * Whether an organization must be selected before entering the app. Default
164
+ * true. Set false for apps with no organization/tenant concept: an
165
+ * authenticated user is admitted and `activeTenantId` is not consulted.
166
+ */
167
+ requiresOrgSelection?: boolean;
114
168
  }
115
- declare function ProtectedRoute({ children, authEnabled, authConfigLoading, isAuthenticated, accessToken, activeTenantId, initializeAuth, }: ProtectedRouteProps): React.ReactElement;
169
+ declare function ProtectedRoute({ children, authEnabled, authConfigLoading, isAuthenticated, accessToken, activeTenantId, initializeAuth, requiresOrgSelection, }: ProtectedRouteProps): React.ReactElement;
116
170
 
117
- export { type AuthConfig, Login, type LoginProps, ProtectedRoute, type ProtectedRouteProps, type TenantOption, TenantSelection };
171
+ export { type AuthConfig, Login, type LoginProps, type LoginProvider, type OrgOption, OrgSelection, ProtectedRoute, type ProtectedRouteProps };