@petrarca/sonnet-shell 0.6.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 +56 -211
- package/dist/auth/index.d.ts +54 -12
- package/dist/auth/index.js +136 -113
- package/dist/auth/index.js.map +1 -1
- package/dist/index.d.ts +27 -5
- package/dist/index.js +118 -43
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
# @petrarca/sonnet-shell
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
7
|
+
## What it provides
|
|
6
8
|
|
|
7
|
-
**
|
|
8
|
-
|
|
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
|
-
**
|
|
11
|
-
-
|
|
12
|
-
|
|
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
|
-
**
|
|
15
|
-
`
|
|
19
|
+
**Shell chrome.** `RootLayout` and `AppShell`, `TopBar`, `ShellFooter`,
|
|
20
|
+
`ShellVersion`, `SearchTrigger` and `UserMenu`.
|
|
16
21
|
|
|
17
|
-
**
|
|
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
|
-
**
|
|
20
|
-
|
|
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
|
-
**
|
|
23
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
47
|
+
`lucide-react` and `sonner` arrive as regular dependencies.
|
|
43
48
|
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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).
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -5,9 +5,9 @@ import { ReactNode } from 'react';
|
|
|
5
5
|
* multiple organizations and no default is set. The user selects which
|
|
6
6
|
* organization context to act in before the app loads.
|
|
7
7
|
*
|
|
8
|
-
* "Organization" is used generically:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
11
11
|
*
|
|
12
12
|
* Fully props-driven with no app-specific imports.
|
|
13
13
|
* Target package: @petrarca/sonnet-shell
|
|
@@ -22,8 +22,16 @@ interface OrgSelectionProps {
|
|
|
22
22
|
isLoading: boolean;
|
|
23
23
|
error: string | null;
|
|
24
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;
|
|
25
33
|
}
|
|
26
|
-
declare function OrgSelection({ orgs, isLoading, error, onSelect, }: OrgSelectionProps): React.ReactElement;
|
|
34
|
+
declare function OrgSelection({ orgs, isLoading, error, onSelect, emptyAction, }: OrgSelectionProps): React.ReactElement;
|
|
27
35
|
|
|
28
36
|
/**
|
|
29
37
|
* Login -- multi-step authentication flow.
|
|
@@ -31,17 +39,19 @@ declare function OrgSelection({ orgs, isLoading, error, onSelect, }: OrgSelectio
|
|
|
31
39
|
* Steps:
|
|
32
40
|
* identifier -- email/username entry (when onDiscover is provided)
|
|
33
41
|
* auth -- resolved providers: password form first, SSO buttons below
|
|
34
|
-
* credentials --
|
|
42
|
+
* credentials -- single-screen login (no onDiscover)
|
|
35
43
|
* org-selection -- organization picker after successful auth
|
|
36
44
|
*
|
|
37
|
-
* When onDiscover is NOT provided,
|
|
38
|
-
*
|
|
45
|
+
* When onDiscover is NOT provided, the identifier and password sit on one
|
|
46
|
+
* screen instead of the identifier-first two-step flow.
|
|
39
47
|
*
|
|
40
48
|
* All async operations are injected via props. The component knows nothing
|
|
41
|
-
* about OIDC, PKCE,
|
|
49
|
+
* about OIDC, PKCE, or discover endpoints.
|
|
42
50
|
*
|
|
43
|
-
* Design:
|
|
44
|
-
*
|
|
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.
|
|
45
55
|
*
|
|
46
56
|
* Target package: @petrarca/sonnet-shell
|
|
47
57
|
*/
|
|
@@ -66,6 +76,14 @@ interface LoginProvider {
|
|
|
66
76
|
type: "local" | "external";
|
|
67
77
|
name: string;
|
|
68
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>;
|
|
69
87
|
}
|
|
70
88
|
interface LoginProps {
|
|
71
89
|
config: AuthConfig | null;
|
|
@@ -91,6 +109,21 @@ interface LoginProps {
|
|
|
91
109
|
onExternalLogin?: (provider: LoginProvider) => Promise<void> | void;
|
|
92
110
|
/** Whether discovery is in flight. */
|
|
93
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;
|
|
94
127
|
}
|
|
95
128
|
declare function Login(props: LoginProps): React.ReactElement | null;
|
|
96
129
|
|
|
@@ -104,7 +137,10 @@ declare function Login(props: LoginProps): React.ReactElement | null;
|
|
|
104
137
|
* Once initialization is complete, guards based on:
|
|
105
138
|
* - Auth disabled (`authEnabled` false): allow all access
|
|
106
139
|
* - Not authenticated: redirect to /login with step=credentials
|
|
107
|
-
* - Authenticated but no tenant: redirect to /login with step=
|
|
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.
|
|
108
144
|
*
|
|
109
145
|
* All auth state is injected via props -- no direct store or context imports.
|
|
110
146
|
* Target package: @petrarca/sonnet-shell
|
|
@@ -123,7 +159,13 @@ interface ProtectedRouteProps {
|
|
|
123
159
|
activeTenantId: string | null;
|
|
124
160
|
/** Called once on mount to restore and validate a persisted session. */
|
|
125
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;
|
|
126
168
|
}
|
|
127
|
-
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;
|
|
128
170
|
|
|
129
171
|
export { type AuthConfig, Login, type LoginProps, type LoginProvider, type OrgOption, OrgSelection, ProtectedRoute, type ProtectedRouteProps };
|