@petrarca/sonnet-playground 0.6.0 → 0.8.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,401 @@
1
+ import {
2
+ PlaygroundSection
3
+ } from "./chunk-C2EZ2ECG.js";
4
+
5
+ // src/pages/PlayAuth.tsx
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
+ import { createPortal } from "react-dom";
8
+ import { useLocation, useNavigate } from "react-router-dom";
9
+ import {
10
+ Button,
11
+ Checkbox,
12
+ Input,
13
+ Label,
14
+ Select,
15
+ SelectContent,
16
+ SelectItem,
17
+ SelectTrigger,
18
+ SelectValue
19
+ } from "@petrarca/sonnet-ui";
20
+ import {
21
+ Login
22
+ } from "@petrarca/sonnet-shell/auth";
23
+ import { jsx, jsxs } from "react/jsx-runtime";
24
+ var SESSION_LABELS = {
25
+ anonymous: "Not authenticated -- login form",
26
+ "orgs-null": "Authenticated, orgs = null (not loaded)",
27
+ "orgs-empty": "Authenticated, orgs = [] (no memberships)",
28
+ "orgs-one": "Authenticated, one org (auto-selects)",
29
+ "orgs-many": "Authenticated, several orgs (picker)"
30
+ };
31
+ var ENTRY_LABELS = {
32
+ direct: "Direct visit -- no location state",
33
+ "tenant-selection": 'Redirected with step="tenant-selection"',
34
+ "org-selection": 'Redirected with step="org-selection"'
35
+ };
36
+ var MANY_ORGS = [
37
+ { id: "org-north", name: "Northwind Trading" },
38
+ { id: "org-acme", name: "Acme Industrial" },
39
+ { id: "org-globex", name: "Globex Research" }
40
+ ];
41
+ var ONE_ORG = [{ id: "org-north", name: "Northwind Trading" }];
42
+ function PlayAuth() {
43
+ const [flow, setFlow] = useState("identifier-first");
44
+ const [providers, setProviders] = useState("both");
45
+ const [session, setSession] = useState("anonymous");
46
+ const [error, setError] = useState("");
47
+ const [isLoading, setIsLoading] = useState(false);
48
+ const [isDiscovering, setIsDiscovering] = useState(false);
49
+ const [appName, setAppName] = useState("Example");
50
+ const [entry, setEntry] = useState("direct");
51
+ const [requiresOrg, setRequiresOrg] = useState(true);
52
+ const [pendingLaunch, setPendingLaunch] = useState(false);
53
+ const [open, setOpen] = useState(false);
54
+ const [events, setEvents] = useState([]);
55
+ const nextId = useRef(0);
56
+ const log = useCallback((message) => {
57
+ setEvents(
58
+ (prev) => [
59
+ {
60
+ id: nextId.current++,
61
+ time: (/* @__PURE__ */ new Date()).toLocaleTimeString(),
62
+ message
63
+ },
64
+ ...prev
65
+ ].slice(0, 40)
66
+ );
67
+ }, []);
68
+ useEffect(() => {
69
+ if (!open) return;
70
+ const onKey = (e) => {
71
+ if (e.key === "Escape") setOpen(false);
72
+ };
73
+ window.addEventListener("keydown", onKey);
74
+ return () => window.removeEventListener("keydown", onKey);
75
+ }, [open]);
76
+ const discovered = useMemo(() => {
77
+ const local = {
78
+ type: "local",
79
+ name: "local",
80
+ label: "Password"
81
+ };
82
+ const external = {
83
+ type: "external",
84
+ name: "acme-sso",
85
+ label: "Continue with Acme SSO",
86
+ // The app attaches whatever its onExternalLogin needs; the shell only
87
+ // carries it through. No `as any` on the provider.
88
+ metadata: {
89
+ loginUrl: "https://example.invalid/authorize",
90
+ targetId: "acme"
91
+ }
92
+ };
93
+ if (providers === "local") return [local];
94
+ if (providers === "external") return [external];
95
+ if (providers === "none") return [];
96
+ return [local, external];
97
+ }, [providers]);
98
+ const config = useMemo(() => {
99
+ const oauthOn = providers === "external" || providers === "both";
100
+ return {
101
+ enabled: true,
102
+ features: {
103
+ local_auth: providers === "local" || providers === "both",
104
+ oauth: oauthOn
105
+ },
106
+ oauth: oauthOn ? {
107
+ providers: {
108
+ "acme-sso": {
109
+ enabled: true,
110
+ name: "Acme SSO",
111
+ login_url: "https://example.invalid/authorize"
112
+ }
113
+ }
114
+ } : null,
115
+ // Never exposed as a control: Login acts on this with
116
+ // window.location.href, which would leave the playground.
117
+ ui: { direct_redirect: null }
118
+ };
119
+ }, [providers]);
120
+ const orgs = useMemo(() => {
121
+ if (session === "orgs-empty") return [];
122
+ if (session === "orgs-one") return ONE_ORG;
123
+ if (session === "orgs-many") return MANY_ORGS;
124
+ return null;
125
+ }, [session]);
126
+ const isAuthenticated = session !== "anonymous";
127
+ const onLogin = useCallback(
128
+ async (credentials) => {
129
+ log(
130
+ `onLogin({ identifier: "${credentials.identifier}", password: "${"*".repeat(credentials.password.length)}" })`
131
+ );
132
+ },
133
+ [log]
134
+ );
135
+ const onFetchCurrentUser = useCallback(async () => {
136
+ log("onFetchCurrentUser()");
137
+ }, [log]);
138
+ const onSelectOrg = useCallback(
139
+ async (id) => {
140
+ log(`onSelectOrg("${id}")`);
141
+ },
142
+ [log]
143
+ );
144
+ const onClearError = useCallback(() => {
145
+ log("onClearError()");
146
+ }, [log]);
147
+ const onDiscover = useCallback(
148
+ async (identifier) => {
149
+ log(`onDiscover("${identifier}") -> ${discovered.length} provider(s)`);
150
+ return discovered;
151
+ },
152
+ [log, discovered]
153
+ );
154
+ const onExternalLogin = useCallback(
155
+ (provider) => {
156
+ const loginUrl = provider.metadata?.loginUrl;
157
+ log(
158
+ `onExternalLogin("${provider.name}", loginUrl=${loginUrl ?? "none"}) -- a real app redirects to the IdP here, so the screen stays on "Redirecting..."`
159
+ );
160
+ },
161
+ [log]
162
+ );
163
+ const orgEmptyAction = useMemo(
164
+ () => /* @__PURE__ */ jsx(
165
+ Button,
166
+ {
167
+ variant: "outline",
168
+ className: "w-full",
169
+ onClick: () => log("orgEmptyAction: Sign out clicked"),
170
+ children: "Sign out"
171
+ }
172
+ ),
173
+ [log]
174
+ );
175
+ const navigate = useNavigate();
176
+ const location = useLocation();
177
+ const launch = useCallback(() => {
178
+ log(
179
+ `launched: flow=${flow}, providers=${providers}, session=${session}, entry=${entry}, requiresOrgSelection=${requiresOrg}`
180
+ );
181
+ navigate(location.pathname, {
182
+ replace: true,
183
+ state: entry === "direct" ? null : { step: entry, from: { pathname: "/dashboard" } }
184
+ });
185
+ setPendingLaunch(true);
186
+ }, [
187
+ log,
188
+ flow,
189
+ providers,
190
+ session,
191
+ entry,
192
+ requiresOrg,
193
+ navigate,
194
+ location.pathname
195
+ ]);
196
+ useEffect(() => {
197
+ if (!pendingLaunch) return;
198
+ const state = location.state;
199
+ const wanted = entry === "direct" ? void 0 : entry;
200
+ if ((state?.step ?? void 0) !== wanted) return;
201
+ setPendingLaunch(false);
202
+ setOpen(true);
203
+ }, [pendingLaunch, location.state, entry]);
204
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
205
+ /* @__PURE__ */ jsxs("div", { children: [
206
+ /* @__PURE__ */ jsx("h1", { children: "Auth Screens" }),
207
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mt-1", children: [
208
+ "Login and organization selection from",
209
+ " ",
210
+ /* @__PURE__ */ jsx("code", { children: "@petrarca/sonnet-shell/auth" }),
211
+ ". Configure a scenario, launch it full-screen, and read what the component called back."
212
+ ] })
213
+ ] }),
214
+ /* @__PURE__ */ jsxs(
215
+ PlaygroundSection,
216
+ {
217
+ title: "Scenario",
218
+ description: "Login renders four different screens depending on these props. Most are unreachable from the default state.",
219
+ variant: "card",
220
+ children: [
221
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
222
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
223
+ /* @__PURE__ */ jsx(Label, { htmlFor: "auth-flow", children: "Flow" }),
224
+ /* @__PURE__ */ jsxs(Select, { value: flow, onValueChange: (v) => setFlow(v), children: [
225
+ /* @__PURE__ */ jsx(SelectTrigger, { id: "auth-flow", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
226
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
227
+ /* @__PURE__ */ jsx(SelectItem, { value: "identifier-first", children: "Identifier-first (onDiscover wired)" }),
228
+ /* @__PURE__ */ jsx(SelectItem, { value: "single-screen", children: "Single screen (no onDiscover)" })
229
+ ] })
230
+ ] })
231
+ ] }),
232
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
233
+ /* @__PURE__ */ jsx(Label, { htmlFor: "auth-providers", children: "Providers" }),
234
+ /* @__PURE__ */ jsxs(
235
+ Select,
236
+ {
237
+ value: providers,
238
+ onValueChange: (v) => setProviders(v),
239
+ children: [
240
+ /* @__PURE__ */ jsx(SelectTrigger, { id: "auth-providers", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
241
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
242
+ /* @__PURE__ */ jsx(SelectItem, { value: "both", children: "Local and external" }),
243
+ /* @__PURE__ */ jsx(SelectItem, { value: "local", children: "Local only" }),
244
+ /* @__PURE__ */ jsx(SelectItem, { value: "external", children: "External only" }),
245
+ /* @__PURE__ */ jsx(SelectItem, { value: "none", children: "None available" })
246
+ ] })
247
+ ]
248
+ }
249
+ )
250
+ ] }),
251
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
252
+ /* @__PURE__ */ jsx(Label, { htmlFor: "auth-entry", children: "Entry state" }),
253
+ /* @__PURE__ */ jsxs(Select, { value: entry, onValueChange: (v) => setEntry(v), children: [
254
+ /* @__PURE__ */ jsx(SelectTrigger, { id: "auth-entry", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
255
+ /* @__PURE__ */ jsx(SelectContent, { children: Object.keys(ENTRY_LABELS).map((key) => /* @__PURE__ */ jsx(SelectItem, { value: key, children: ENTRY_LABELS[key] }, key)) })
256
+ ] })
257
+ ] }),
258
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
259
+ /* @__PURE__ */ jsx(Label, { htmlFor: "auth-session", children: "Session" }),
260
+ /* @__PURE__ */ jsxs(
261
+ Select,
262
+ {
263
+ value: session,
264
+ onValueChange: (v) => setSession(v),
265
+ children: [
266
+ /* @__PURE__ */ jsx(SelectTrigger, { id: "auth-session", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
267
+ /* @__PURE__ */ jsx(SelectContent, { children: Object.keys(SESSION_LABELS).map((key) => /* @__PURE__ */ jsx(SelectItem, { value: key, children: SESSION_LABELS[key] }, key)) })
268
+ ]
269
+ }
270
+ )
271
+ ] }),
272
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
273
+ /* @__PURE__ */ jsx(Label, { htmlFor: "auth-error", children: "Error message" }),
274
+ /* @__PURE__ */ jsx(
275
+ Input,
276
+ {
277
+ id: "auth-error",
278
+ value: error,
279
+ placeholder: "Leave empty for no error",
280
+ onChange: (e) => setError(e.target.value)
281
+ }
282
+ )
283
+ ] }),
284
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
285
+ /* @__PURE__ */ jsx(Label, { htmlFor: "auth-appname", children: "App name" }),
286
+ /* @__PURE__ */ jsx(
287
+ Input,
288
+ {
289
+ id: "auth-appname",
290
+ value: appName,
291
+ placeholder: "Shown above the form",
292
+ onChange: (e) => setAppName(e.target.value)
293
+ }
294
+ )
295
+ ] }),
296
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-6 sm:col-span-2", children: [
297
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
298
+ /* @__PURE__ */ jsx(
299
+ Checkbox,
300
+ {
301
+ id: "auth-loading",
302
+ checked: isLoading,
303
+ onCheckedChange: (v) => setIsLoading(v === true)
304
+ }
305
+ ),
306
+ /* @__PURE__ */ jsx(Label, { htmlFor: "auth-loading", children: "isLoading" })
307
+ ] }),
308
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
309
+ /* @__PURE__ */ jsx(
310
+ Checkbox,
311
+ {
312
+ id: "auth-discovering",
313
+ checked: isDiscovering,
314
+ onCheckedChange: (v) => setIsDiscovering(v === true)
315
+ }
316
+ ),
317
+ /* @__PURE__ */ jsx(Label, { htmlFor: "auth-discovering", children: "isDiscovering" })
318
+ ] }),
319
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
320
+ /* @__PURE__ */ jsx(
321
+ Checkbox,
322
+ {
323
+ id: "auth-requires-org",
324
+ checked: requiresOrg,
325
+ onCheckedChange: (v) => setRequiresOrg(v === true)
326
+ }
327
+ ),
328
+ /* @__PURE__ */ jsx(Label, { htmlFor: "auth-requires-org", children: "requiresOrgSelection" })
329
+ ] })
330
+ ] })
331
+ ] }),
332
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 pt-2", children: [
333
+ /* @__PURE__ */ jsx(Button, { onClick: launch, children: "Launch login screen" }),
334
+ /* @__PURE__ */ jsx(
335
+ Button,
336
+ {
337
+ variant: "outline",
338
+ onClick: () => setEvents([]),
339
+ disabled: events.length === 0,
340
+ children: "Clear log"
341
+ }
342
+ )
343
+ ] })
344
+ ]
345
+ }
346
+ ),
347
+ /* @__PURE__ */ jsx(
348
+ PlaygroundSection,
349
+ {
350
+ title: "Event log",
351
+ description: "Every prop callback the component invoked, most recent first.",
352
+ variant: "card",
353
+ children: events.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Nothing yet. Launch the screen and interact with it." }) : /* @__PURE__ */ jsx("ul", { className: "space-y-1", children: events.map((entry2) => /* @__PURE__ */ jsxs("li", { className: "flex gap-3 text-sm", children: [
354
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-xs text-tertiary-foreground shrink-0 pt-0.5", children: entry2.time }),
355
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-xs break-all", children: entry2.message })
356
+ ] }, entry2.id)) })
357
+ }
358
+ ),
359
+ open && createPortal(
360
+ /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 overflow-auto bg-background", children: [
361
+ /* @__PURE__ */ jsx(
362
+ Login,
363
+ {
364
+ config,
365
+ isAuthenticated,
366
+ activeTenantId: null,
367
+ isLoading,
368
+ error: error || null,
369
+ orgs,
370
+ appName: appName || void 0,
371
+ onLogin,
372
+ onFetchCurrentUser,
373
+ onSelectOrg,
374
+ onClearError,
375
+ onDiscover: flow === "identifier-first" ? onDiscover : void 0,
376
+ onExternalLogin,
377
+ isDiscovering,
378
+ requiresOrgSelection: requiresOrg,
379
+ orgEmptyAction
380
+ }
381
+ ),
382
+ /* @__PURE__ */ jsx(
383
+ Button,
384
+ {
385
+ variant: "outline",
386
+ size: "sm",
387
+ className: "fixed top-4 right-4 z-10",
388
+ onClick: () => setOpen(false),
389
+ children: "Exit preview (Esc)"
390
+ }
391
+ )
392
+ ] }),
393
+ document.body
394
+ )
395
+ ] });
396
+ }
397
+
398
+ export {
399
+ PlayAuth
400
+ };
401
+ //# sourceMappingURL=chunk-RYCRENYO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pages/PlayAuth.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useLocation, useNavigate } from \"react-router-dom\";\nimport {\n Button,\n Checkbox,\n Input,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@petrarca/sonnet-ui\";\nimport {\n Login,\n type AuthConfig,\n type LoginProvider,\n type OrgOption,\n} from \"@petrarca/sonnet-shell/auth\";\nimport { PlaygroundSection } from \"../PlaygroundSection\";\n\n/**\n * PlayAuth -- exercises the pre-application auth screens from\n * `@petrarca/sonnet-shell/auth`.\n *\n * Login is a full-page state machine (identifier -> auth -> credentials ->\n * org-selection) rather than a dialog, so the preview takes over the viewport\n * instead of rendering inline. Every callback is a logger: no backend, no\n * mocking, and the log is the only feedback channel.\n *\n * Login calls useNavigate and redirects once a session resolves. It cannot be\n * sandboxed in a nested MemoryRouter -- react-router forbids a Router inside a\n * Router -- so the scenarios stop short of that branch instead: the redirect\n * fires only when a tenant is already active, and no scenario here sets one.\n * activeTenantId is therefore pinned to null and Login never navigates.\n *\n * The preview is portalled to document.body. Rendered in place it would sit\n * inside the page's space-y-6 wrapper, whose margin-top applies to a fixed\n * child too and pushes the overlay off the viewport by 24px.\n *\n * `config.ui.direct_redirect` is deliberately fixed to null and not exposed --\n * Login acts on it with `window.location.href`, which would take the browser\n * off the playground entirely.\n */\n\ntype Flow = \"identifier-first\" | \"single-screen\";\n/** What ProtectedRoute puts in location.state when it bounces a user here. */\ntype Entry = \"direct\" | \"tenant-selection\" | \"org-selection\";\ntype Providers = \"both\" | \"local\" | \"external\" | \"none\";\ntype Session =\n | \"anonymous\"\n | \"orgs-null\"\n | \"orgs-empty\"\n | \"orgs-one\"\n | \"orgs-many\";\n\ninterface LogEntry {\n id: number;\n time: string;\n message: string;\n}\n\nconst SESSION_LABELS: Record<Session, string> = {\n anonymous: \"Not authenticated -- login form\",\n \"orgs-null\": \"Authenticated, orgs = null (not loaded)\",\n \"orgs-empty\": \"Authenticated, orgs = [] (no memberships)\",\n \"orgs-one\": \"Authenticated, one org (auto-selects)\",\n \"orgs-many\": \"Authenticated, several orgs (picker)\",\n};\n\nconst ENTRY_LABELS: Record<Entry, string> = {\n direct: \"Direct visit -- no location state\",\n \"tenant-selection\": 'Redirected with step=\"tenant-selection\"',\n \"org-selection\": 'Redirected with step=\"org-selection\"',\n};\n\nconst MANY_ORGS: OrgOption[] = [\n { id: \"org-north\", name: \"Northwind Trading\" },\n { id: \"org-acme\", name: \"Acme Industrial\" },\n { id: \"org-globex\", name: \"Globex Research\" },\n];\n\nconst ONE_ORG: OrgOption[] = [{ id: \"org-north\", name: \"Northwind Trading\" }];\n\nexport default function PlayAuth() {\n const [flow, setFlow] = useState<Flow>(\"identifier-first\");\n const [providers, setProviders] = useState<Providers>(\"both\");\n const [session, setSession] = useState<Session>(\"anonymous\");\n const [error, setError] = useState(\"\");\n const [isLoading, setIsLoading] = useState(false);\n const [isDiscovering, setIsDiscovering] = useState(false);\n const [appName, setAppName] = useState(\"Example\");\n const [entry, setEntry] = useState<Entry>(\"direct\");\n const [requiresOrg, setRequiresOrg] = useState(true);\n const [pendingLaunch, setPendingLaunch] = useState(false);\n const [open, setOpen] = useState(false);\n const [events, setEvents] = useState<LogEntry[]>([]);\n const nextId = useRef(0);\n\n const log = useCallback((message: string) => {\n setEvents((prev) =>\n [\n {\n id: nextId.current++,\n time: new Date().toLocaleTimeString(),\n message,\n },\n ...prev,\n ].slice(0, 40),\n );\n }, []);\n\n // Escape leaves the preview.\n useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [open]);\n\n const discovered = useMemo<LoginProvider[]>(() => {\n const local: LoginProvider = {\n type: \"local\",\n name: \"local\",\n label: \"Password\",\n };\n const external: LoginProvider = {\n type: \"external\",\n name: \"acme-sso\",\n label: \"Continue with Acme SSO\",\n // The app attaches whatever its onExternalLogin needs; the shell only\n // carries it through. No `as any` on the provider.\n metadata: {\n loginUrl: \"https://example.invalid/authorize\",\n targetId: \"acme\",\n },\n };\n if (providers === \"local\") return [local];\n if (providers === \"external\") return [external];\n if (providers === \"none\") return [];\n return [local, external];\n }, [providers]);\n\n const config = useMemo<AuthConfig>(() => {\n const oauthOn = providers === \"external\" || providers === \"both\";\n return {\n enabled: true,\n features: {\n local_auth: providers === \"local\" || providers === \"both\",\n oauth: oauthOn,\n },\n oauth: oauthOn\n ? {\n providers: {\n \"acme-sso\": {\n enabled: true,\n name: \"Acme SSO\",\n login_url: \"https://example.invalid/authorize\",\n },\n },\n }\n : null,\n // Never exposed as a control: Login acts on this with\n // window.location.href, which would leave the playground.\n ui: { direct_redirect: null },\n };\n }, [providers]);\n\n // Stable identity matters: Login runs an effect over `orgs` that calls\n // onSelectOrg for a single-org session. A fresh array each render would\n // re-fire it on every render.\n const orgs = useMemo<OrgOption[] | null>(() => {\n if (session === \"orgs-empty\") return [];\n if (session === \"orgs-one\") return ONE_ORG;\n if (session === \"orgs-many\") return MANY_ORGS;\n return null;\n }, [session]);\n\n const isAuthenticated = session !== \"anonymous\";\n\n const onLogin = useCallback(\n async (credentials: { identifier: string; password: string }) => {\n log(\n `onLogin({ identifier: \"${credentials.identifier}\", password: \"${\"*\".repeat(credentials.password.length)}\" })`,\n );\n },\n [log],\n );\n\n const onFetchCurrentUser = useCallback(async () => {\n log(\"onFetchCurrentUser()\");\n }, [log]);\n\n const onSelectOrg = useCallback(\n async (id: string) => {\n log(`onSelectOrg(\"${id}\")`);\n },\n [log],\n );\n\n const onClearError = useCallback(() => {\n log(\"onClearError()\");\n }, [log]);\n\n const onDiscover = useCallback(\n async (identifier: string) => {\n log(`onDiscover(\"${identifier}\") -> ${discovered.length} provider(s)`);\n return discovered;\n },\n [log, discovered],\n );\n\n const onExternalLogin = useCallback(\n (provider: LoginProvider) => {\n // Read back the per-provider metadata the discover step attached, with\n // one narrowing cast on the field -- not on the whole provider.\n const loginUrl = provider.metadata?.loginUrl as string | undefined;\n log(\n `onExternalLogin(\"${provider.name}\", loginUrl=${loginUrl ?? \"none\"}) -- a real app redirects to the IdP here, so the screen stays on \"Redirecting...\"`,\n );\n },\n [log],\n );\n\n // The empty-org escape hatch the consumer supplies. The library states the\n // fact; the app decides the way out.\n const orgEmptyAction = useMemo(\n () => (\n <Button\n variant=\"outline\"\n className=\"w-full\"\n onClick={() => log(\"orgEmptyAction: Sign out clicked\")}\n >\n Sign out\n </Button>\n ),\n [log],\n );\n\n const navigate = useNavigate();\n const location = useLocation();\n\n // Login seeds its step from location.state, which only ProtectedRoute sets.\n // Writing it onto the playground's own location is the only way to reach the\n // screens that entry state selects.\n //\n // The open flag cannot be set in the same handler: react-router resolves a\n // navigation asynchronously, so Login would mount and read location.state\n // before the new state landed. Wait for the location to agree first.\n const launch = useCallback(() => {\n log(\n `launched: flow=${flow}, providers=${providers}, session=${session}, entry=${entry}, requiresOrgSelection=${requiresOrg}`,\n );\n navigate(location.pathname, {\n replace: true,\n state:\n entry === \"direct\"\n ? null\n : { step: entry, from: { pathname: \"/dashboard\" } },\n });\n setPendingLaunch(true);\n }, [\n log,\n flow,\n providers,\n session,\n entry,\n requiresOrg,\n navigate,\n location.pathname,\n ]);\n\n useEffect(() => {\n if (!pendingLaunch) return;\n const state = location.state as { step?: string } | null;\n const wanted = entry === \"direct\" ? undefined : entry;\n if ((state?.step ?? undefined) !== wanted) return;\n setPendingLaunch(false);\n setOpen(true);\n }, [pendingLaunch, location.state, entry]);\n\n return (\n <div className=\"space-y-6\">\n <div>\n <h1>Auth Screens</h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n Login and organization selection from{\" \"}\n <code>@petrarca/sonnet-shell/auth</code>. Configure a scenario, launch\n it full-screen, and read what the component called back.\n </p>\n </div>\n\n <PlaygroundSection\n title=\"Scenario\"\n description=\"Login renders four different screens depending on these props. Most are unreachable from the default state.\"\n variant=\"card\"\n >\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"auth-flow\">Flow</Label>\n <Select value={flow} onValueChange={(v) => setFlow(v as Flow)}>\n <SelectTrigger id=\"auth-flow\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"identifier-first\">\n Identifier-first (onDiscover wired)\n </SelectItem>\n <SelectItem value=\"single-screen\">\n Single screen (no onDiscover)\n </SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"auth-providers\">Providers</Label>\n <Select\n value={providers}\n onValueChange={(v) => setProviders(v as Providers)}\n >\n <SelectTrigger id=\"auth-providers\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"both\">Local and external</SelectItem>\n <SelectItem value=\"local\">Local only</SelectItem>\n <SelectItem value=\"external\">External only</SelectItem>\n <SelectItem value=\"none\">None available</SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"auth-entry\">Entry state</Label>\n <Select value={entry} onValueChange={(v) => setEntry(v as Entry)}>\n <SelectTrigger id=\"auth-entry\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {(Object.keys(ENTRY_LABELS) as Entry[]).map((key) => (\n <SelectItem key={key} value={key}>\n {ENTRY_LABELS[key]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"auth-session\">Session</Label>\n <Select\n value={session}\n onValueChange={(v) => setSession(v as Session)}\n >\n <SelectTrigger id=\"auth-session\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {(Object.keys(SESSION_LABELS) as Session[]).map((key) => (\n <SelectItem key={key} value={key}>\n {SESSION_LABELS[key]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"auth-error\">Error message</Label>\n <Input\n id=\"auth-error\"\n value={error}\n placeholder=\"Leave empty for no error\"\n onChange={(e) => setError(e.target.value)}\n />\n </div>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"auth-appname\">App name</Label>\n <Input\n id=\"auth-appname\"\n value={appName}\n placeholder=\"Shown above the form\"\n onChange={(e) => setAppName(e.target.value)}\n />\n </div>\n\n <div className=\"flex items-center gap-6 sm:col-span-2\">\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"auth-loading\"\n checked={isLoading}\n onCheckedChange={(v) => setIsLoading(v === true)}\n />\n <Label htmlFor=\"auth-loading\">isLoading</Label>\n </div>\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"auth-discovering\"\n checked={isDiscovering}\n onCheckedChange={(v) => setIsDiscovering(v === true)}\n />\n <Label htmlFor=\"auth-discovering\">isDiscovering</Label>\n </div>\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"auth-requires-org\"\n checked={requiresOrg}\n onCheckedChange={(v) => setRequiresOrg(v === true)}\n />\n <Label htmlFor=\"auth-requires-org\">requiresOrgSelection</Label>\n </div>\n </div>\n </div>\n\n <div className=\"flex items-center gap-3 pt-2\">\n <Button onClick={launch}>Launch login screen</Button>\n <Button\n variant=\"outline\"\n onClick={() => setEvents([])}\n disabled={events.length === 0}\n >\n Clear log\n </Button>\n </div>\n </PlaygroundSection>\n\n <PlaygroundSection\n title=\"Event log\"\n description=\"Every prop callback the component invoked, most recent first.\"\n variant=\"card\"\n >\n {events.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n Nothing yet. Launch the screen and interact with it.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {events.map((entry) => (\n <li key={entry.id} className=\"flex gap-3 text-sm\">\n <span className=\"font-mono text-xs text-tertiary-foreground shrink-0 pt-0.5\">\n {entry.time}\n </span>\n <span className=\"font-mono text-xs break-all\">\n {entry.message}\n </span>\n </li>\n ))}\n </ul>\n )}\n </PlaygroundSection>\n\n {open &&\n createPortal(\n <div className=\"fixed inset-0 z-50 overflow-auto bg-background\">\n <Login\n config={config}\n isAuthenticated={isAuthenticated}\n activeTenantId={null}\n isLoading={isLoading}\n error={error || null}\n orgs={orgs}\n appName={appName || undefined}\n onLogin={onLogin}\n onFetchCurrentUser={onFetchCurrentUser}\n onSelectOrg={onSelectOrg}\n onClearError={onClearError}\n onDiscover={flow === \"identifier-first\" ? onDiscover : undefined}\n onExternalLogin={onExternalLogin}\n isDiscovering={isDiscovering}\n requiresOrgSelection={requiresOrg}\n orgEmptyAction={orgEmptyAction}\n />\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"fixed top-4 right-4 z-10\"\n onClick={() => setOpen(false)}\n >\n Exit preview (Esc)\n </Button>\n </div>,\n document.body,\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["entry"]}
package/dist/index.d.ts CHANGED
@@ -55,17 +55,21 @@ interface PlaygroundModuleOptions {
55
55
  grouped?: boolean;
56
56
  }
57
57
 
58
- /**
59
- * PlaygroundRegistry -- mutable collection of playground entries.
60
- *
61
- * The library ships a default instance pre-populated with the built-in
62
- * sonnet component demos. Consumers register additional entries for
63
- * domain-specific playgrounds before passing the registry to
64
- * createPlaygroundModule().
65
- */
66
-
67
58
  declare class PlaygroundRegistry {
68
59
  private entries;
60
+ /**
61
+ * Report registrations that cannot work, rather than silently misbehaving.
62
+ *
63
+ * Entries are keyed by `id`, so two entries may legally share a `path` --
64
+ * and then both produce a route on the same URL and one is unreachable.
65
+ * Groups are bucketed by their exact label, so two labels differing only by
66
+ * case become two separate groups.
67
+ *
68
+ * Neither is corrected here: dropping an entry, or merging groups the caller
69
+ * spelled differently, would be its own surprise. The registration stands and
70
+ * the problem is made diagnosable.
71
+ */
72
+ private warnOnConflicts;
69
73
  /** Register a playground entry. Overwrites any existing entry with the same id. */
70
74
  register(entry: PlaygroundEntry): void;
71
75
  /** Register multiple entries at once. */
@@ -130,6 +134,12 @@ interface PlaygroundSectionProps {
130
134
  children: React$1.ReactNode;
131
135
  variant?: "default" | "card";
132
136
  }
137
+ interface ExampleBlockProps {
138
+ label: string;
139
+ children: React$1.ReactNode;
140
+ }
141
+ /** Labeled divider wrapper for individual component examples within a section. */
142
+ declare function ExampleBlock({ label, children, }: ExampleBlockProps): React$1.ReactElement;
133
143
  /** Titled section wrapper used across playground pages. */
134
144
  declare function PlaygroundSection({ title, description, children, variant, }: PlaygroundSectionProps): React$1.ReactElement;
135
145
 
@@ -200,4 +210,4 @@ declare const builtinEntries: PlaygroundEntry[];
200
210
  */
201
211
  declare function createPlaygroundRegistry(): PlaygroundRegistry;
202
212
 
203
- export { type PlaygroundColor, type PlaygroundEntry, type PlaygroundModuleOptions, PlaygroundOverview, PlaygroundRegistry, PlaygroundSection, type PlaygroundSettings, PlaygroundSettingsPanel, PlaygroundSettingsProvider, builtinEntries, createPlaygroundModule, createPlaygroundRegistry, usePlaygroundSettings };
213
+ export { ExampleBlock, type PlaygroundColor, type PlaygroundEntry, type PlaygroundModuleOptions, PlaygroundOverview, PlaygroundRegistry, PlaygroundSection, type PlaygroundSettings, PlaygroundSettingsPanel, PlaygroundSettingsProvider, builtinEntries, createPlaygroundModule, createPlaygroundRegistry, usePlaygroundSettings };
package/dist/index.js CHANGED
@@ -1,18 +1,50 @@
1
1
  import {
2
+ ExampleBlock,
2
3
  PlaygroundSection
3
4
  } from "./chunk-C2EZ2ECG.js";
4
5
 
5
6
  // src/PlaygroundRegistry.ts
7
+ import { warnLog } from "@petrarca/sonnet-core";
6
8
  var PlaygroundRegistry = class _PlaygroundRegistry {
7
9
  entries = /* @__PURE__ */ new Map();
10
+ /**
11
+ * Report registrations that cannot work, rather than silently misbehaving.
12
+ *
13
+ * Entries are keyed by `id`, so two entries may legally share a `path` --
14
+ * and then both produce a route on the same URL and one is unreachable.
15
+ * Groups are bucketed by their exact label, so two labels differing only by
16
+ * case become two separate groups.
17
+ *
18
+ * Neither is corrected here: dropping an entry, or merging groups the caller
19
+ * spelled differently, would be its own surprise. The registration stands and
20
+ * the problem is made diagnosable.
21
+ */
22
+ warnOnConflicts(entry) {
23
+ for (const existing of this.entries.values()) {
24
+ if (existing.id === entry.id) continue;
25
+ if (existing.path === entry.path) {
26
+ warnLog(
27
+ `[PlaygroundRegistry] entries "${existing.id}" and "${entry.id}" share the path "${entry.path}". Both will route to the same URL and one will be unreachable.`
28
+ );
29
+ }
30
+ const group = entry.group ?? _PlaygroundRegistry.DEFAULT_GROUP;
31
+ const existingGroup = existing.group ?? _PlaygroundRegistry.DEFAULT_GROUP;
32
+ if (group !== existingGroup && group.toLowerCase() === existingGroup.toLowerCase()) {
33
+ warnLog(
34
+ `[PlaygroundRegistry] group "${group}" differs from "${existingGroup}" only by case; they will render as two separate groups.`
35
+ );
36
+ }
37
+ }
38
+ }
8
39
  /** Register a playground entry. Overwrites any existing entry with the same id. */
9
40
  register(entry) {
41
+ this.warnOnConflicts(entry);
10
42
  this.entries.set(entry.id, entry);
11
43
  }
12
44
  /** Register multiple entries at once. */
13
45
  registerAll(entries) {
14
46
  for (const entry of entries) {
15
- this.entries.set(entry.id, entry);
47
+ this.register(entry);
16
48
  }
17
49
  }
18
50
  /** Remove an entry by id. Returns true if it existed. */
@@ -87,7 +119,8 @@ import {
87
119
  Table2,
88
120
  Network,
89
121
  SquareTerminal,
90
- PencilRuler
122
+ PencilRuler,
123
+ ShieldCheck
91
124
  } from "lucide-react";
92
125
  var builtinEntries = [
93
126
  {
@@ -163,7 +196,7 @@ var builtinEntries = [
163
196
  icon: Edit,
164
197
  color: "purple",
165
198
  path: "forms",
166
- component: lazy(() => import("./PlayForms-2G7PROCH.js")),
199
+ component: lazy(() => import("./PlayForms-U2TWUX5L.js")),
167
200
  group: "Forms",
168
201
  order: 1
169
202
  },
@@ -185,7 +218,7 @@ var builtinEntries = [
185
218
  icon: PencilRuler,
186
219
  color: "teal",
187
220
  path: "schema-designer",
188
- component: lazy(() => import("./PlaySchemaDesigner-ZM4F7KBU.js")),
221
+ component: lazy(() => import("./PlaySchemaDesigner-S4DBB53G.js")),
189
222
  group: "Schemas",
190
223
  order: 0
191
224
  },
@@ -207,7 +240,7 @@ var builtinEntries = [
207
240
  icon: Network,
208
241
  color: "cyan",
209
242
  path: "graph",
210
- component: lazy(() => import("./PlayGraph-M7UE5MZC.js")),
243
+ component: lazy(() => import("./PlayGraph-M4KQSFI7.js")),
211
244
  group: "Graph",
212
245
  order: 0
213
246
  },
@@ -221,6 +254,17 @@ var builtinEntries = [
221
254
  component: lazy(() => import("./PlayShellApi-5O6VYR2X.js")),
222
255
  group: "Shell",
223
256
  order: 0
257
+ },
258
+ {
259
+ id: "sonnet.auth",
260
+ title: "Auth Screens",
261
+ description: "The pre-application login and organization selection screens: identifier-first and legacy flows, provider combinations, error and loading states, and every org-selection outcome.",
262
+ icon: ShieldCheck,
263
+ color: "teal",
264
+ path: "auth",
265
+ component: lazy(() => import("./PlayAuth-SOEYPOOL.js")),
266
+ group: "Shell",
267
+ order: 1
224
268
  }
225
269
  ];
226
270
 
@@ -536,6 +580,7 @@ function createPlaygroundRegistry() {
536
580
  return registry;
537
581
  }
538
582
  export {
583
+ ExampleBlock,
539
584
  PlaygroundOverview,
540
585
  PlaygroundRegistry,
541
586
  PlaygroundSection,