@petrarca/sonnet-playground 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.
Files changed (39) hide show
  1. package/README.md +63 -101
  2. package/dist/PlayAuth-SOEYPOOL.js +8 -0
  3. package/dist/{PlayCoreComponents-AG5D4SBS.js → PlayCoreComponents-YTEAS3GL.js} +2 -2
  4. package/dist/{PlayFormWidgets-IWJJQ5XL.js → PlayFormWidgets-6JYFRTAL.js} +2 -2
  5. package/dist/PlayForms-L532W3JL.js +7 -0
  6. package/dist/PlayGraph-M4KQSFI7.js +7 -0
  7. package/dist/PlaySchemaDesigner-S4DBB53G.js +7 -0
  8. package/dist/PlaySchemaDesigner-S4DBB53G.js.map +1 -0
  9. package/dist/{chunk-PBT6HTKY.js → chunk-2XMWOZ5O.js} +130 -6
  10. package/dist/chunk-2XMWOZ5O.js.map +1 -0
  11. package/dist/{chunk-Q6NLY2BZ.js → chunk-6WI3N7JI.js} +309 -1
  12. package/dist/chunk-6WI3N7JI.js.map +1 -0
  13. package/dist/{chunk-C2P3UQMZ.js → chunk-E5X7GVLU.js} +2 -2
  14. package/dist/chunk-E5X7GVLU.js.map +1 -0
  15. package/dist/{chunk-6DZ3TSV4.js → chunk-I2FLG6ZD.js} +102 -4
  16. package/dist/chunk-I2FLG6ZD.js.map +1 -0
  17. package/dist/chunk-RYCRENYO.js +401 -0
  18. package/dist/chunk-RYCRENYO.js.map +1 -0
  19. package/dist/{chunk-DZB6WFHA.js → chunk-UB7462QG.js} +106 -1
  20. package/dist/chunk-UB7462QG.js.map +1 -0
  21. package/dist/index.d.ts +59 -17
  22. package/dist/index.js +251 -53
  23. package/dist/index.js.map +1 -1
  24. package/dist/pages/index.d.ts +3 -1
  25. package/dist/pages/index.js +9 -5
  26. package/package.json +7 -7
  27. package/dist/PlayForms-FOF3XE6G.js +0 -7
  28. package/dist/PlayGraph-M7UE5MZC.js +0 -7
  29. package/dist/PlaySchemaDesigner-B2RVRRCH.js +0 -7
  30. package/dist/chunk-6DZ3TSV4.js.map +0 -1
  31. package/dist/chunk-C2P3UQMZ.js.map +0 -1
  32. package/dist/chunk-DZB6WFHA.js.map +0 -1
  33. package/dist/chunk-PBT6HTKY.js.map +0 -1
  34. package/dist/chunk-Q6NLY2BZ.js.map +0 -1
  35. /package/dist/{PlayCoreComponents-AG5D4SBS.js.map → PlayAuth-SOEYPOOL.js.map} +0 -0
  36. /package/dist/{PlayFormWidgets-IWJJQ5XL.js.map → PlayCoreComponents-YTEAS3GL.js.map} +0 -0
  37. /package/dist/{PlayForms-FOF3XE6G.js.map → PlayFormWidgets-6JYFRTAL.js.map} +0 -0
  38. /package/dist/{PlayGraph-M7UE5MZC.js.map → PlayForms-L532W3JL.js.map} +0 -0
  39. /package/dist/{PlaySchemaDesigner-B2RVRRCH.js.map → PlayGraph-M4KQSFI7.js.map} +0 -0
@@ -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"]}
@@ -16,6 +16,10 @@ import {
16
16
  CheckboxWidget,
17
17
  SelectWidget,
18
18
  QuantityWidget,
19
+ DateWidget,
20
+ TimeWidget,
21
+ DateTimeWidget,
22
+ DisplayWidget,
19
23
  DEFAULT_WIDGETS
20
24
  } from "@petrarca/sonnet-forms";
21
25
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -62,6 +66,10 @@ function PlayFormWidgets() {
62
66
  value: 49.99,
63
67
  unit: "EUR"
64
68
  });
69
+ const [dateValue, setDateValue] = useState("2026-06-15");
70
+ const [birthYear, setBirthYear] = useState("1980");
71
+ const [timeValue, setTimeValue] = useState("09:30");
72
+ const [dateTimeValue, setDateTimeValue] = useState("2026-06-15T09:30");
65
73
  const onTextChange = useCallback(
66
74
  (v) => setTextValue(typeof v === "string" ? v : ""),
67
75
  []
@@ -94,6 +102,10 @@ function PlayFormWidgets() {
94
102
  const onWeightChange = useMemo(() => makeQuantityHandler(setWeightValue), []);
95
103
  const onBpChange = useMemo(() => makeQuantityHandler(setBpValue), []);
96
104
  const onPriceChange = useMemo(() => makeQuantityHandler(setPriceValue), []);
105
+ const onStringChange = useCallback(
106
+ (set) => (v) => set(typeof v === "string" ? v : ""),
107
+ []
108
+ );
97
109
  return /* @__PURE__ */ jsxs("div", { className: "w-full max-w-4xl space-y-6", children: [
98
110
  /* @__PURE__ */ jsxs("div", { children: [
99
111
  /* @__PURE__ */ jsx("h1", { children: "Basic Form Widgets Playground" }),
@@ -295,6 +307,99 @@ function PlayFormWidgets() {
295
307
  ) })
296
308
  ] })
297
309
  ] }) }),
310
+ /* @__PURE__ */ jsx(Card, { className: "border shadow-sm p-4", children: /* @__PURE__ */ jsxs(SimpleStack, { gap: "sm", children: [
311
+ /* @__PURE__ */ jsx("h2", { children: "DisplayWidget" }),
312
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Read-only text block (type: null / x-ui-widget: display). Renders title + description as static text, collects no value. For instructions and notes placed between fields." }),
313
+ /* @__PURE__ */ jsx("div", { className: "rounded-md border border-dashed p-3", children: /* @__PURE__ */ jsx(
314
+ DisplayWidget,
315
+ {
316
+ id: "display-demo",
317
+ name: "displayNote",
318
+ value: null,
319
+ onChange: () => {
320
+ },
321
+ label: "Section 3: Medical history",
322
+ description: "Answer the following to the best of your knowledge. Leave blank if unsure.",
323
+ propertySchema: { type: "null" },
324
+ schema: dummySchema,
325
+ registry: DEFAULT_WIDGETS
326
+ }
327
+ ) })
328
+ ] }) }),
329
+ /* @__PURE__ */ jsx(Card, { className: "border shadow-sm p-4", children: /* @__PURE__ */ jsxs(SimpleStack, { gap: "sm", children: [
330
+ /* @__PURE__ */ jsx("h2", { children: "DateWidget / TimeWidget / DateTimeWidget" }),
331
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Date, time, and datetime fields. Auto-resolve from schema format (date / time / date-time). Values are ISO strings; display and parsing follow the playground locale (gear icon)." }),
332
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3 pt-2", children: [
333
+ /* @__PURE__ */ jsx(ExampleBlock, { label: "Date (format: date)", children: /* @__PURE__ */ jsx(
334
+ DateWidget,
335
+ {
336
+ id: "date-widget-demo",
337
+ name: "date",
338
+ value: dateValue,
339
+ onChange: onStringChange(setDateValue),
340
+ label: "Appointment date",
341
+ description: "Resolved from format: date",
342
+ propertySchema: { type: "string", format: "date" },
343
+ schema: dummySchema,
344
+ registry: DEFAULT_WIDGETS
345
+ }
346
+ ) }),
347
+ /* @__PURE__ */ jsx(ExampleBlock, { label: "Date, year precision", children: /* @__PURE__ */ jsx(
348
+ DateWidget,
349
+ {
350
+ id: "date-widget-year",
351
+ name: "birthYear",
352
+ value: birthYear,
353
+ onChange: onStringChange(setBirthYear),
354
+ label: "Birth year",
355
+ description: "x-ui-options.precision: year",
356
+ propertySchema: { type: "string", format: "date" },
357
+ schema: dummySchema,
358
+ registry: DEFAULT_WIDGETS,
359
+ options: { precision: "year" }
360
+ }
361
+ ) }),
362
+ /* @__PURE__ */ jsx(ExampleBlock, { label: "Time (format: time)", children: /* @__PURE__ */ jsx(
363
+ TimeWidget,
364
+ {
365
+ id: "time-widget-demo",
366
+ name: "time",
367
+ value: timeValue,
368
+ onChange: onStringChange(setTimeValue),
369
+ label: "Start time",
370
+ description: "Zone-free time of day",
371
+ propertySchema: { type: "string", format: "time" },
372
+ schema: dummySchema,
373
+ registry: DEFAULT_WIDGETS
374
+ }
375
+ ) }),
376
+ /* @__PURE__ */ jsx(ExampleBlock, { label: "Date & time (format: date-time)", children: /* @__PURE__ */ jsx(
377
+ DateTimeWidget,
378
+ {
379
+ id: "datetime-widget-demo",
380
+ name: "dateTime",
381
+ value: dateTimeValue,
382
+ onChange: onStringChange(setDateTimeValue),
383
+ label: "Scheduled at",
384
+ description: "Plain wall-clock, timezone hint",
385
+ propertySchema: { type: "string", format: "date-time" },
386
+ schema: dummySchema,
387
+ registry: DEFAULT_WIDGETS,
388
+ options: { timeZone: "Europe/Berlin" }
389
+ }
390
+ ) })
391
+ ] }),
392
+ /* @__PURE__ */ jsx("pre", { className: "text-[11px] bg-muted p-2 rounded overflow-x-auto", children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(
393
+ {
394
+ date: dateValue,
395
+ birthYear,
396
+ time: timeValue,
397
+ dateTime: dateTimeValue
398
+ },
399
+ null,
400
+ 2
401
+ ) }) })
402
+ ] }) }),
298
403
  /* @__PURE__ */ jsx(Card, { className: "border shadow-sm p-4", children: /* @__PURE__ */ jsxs(SimpleStack, { gap: "sm", children: [
299
404
  /* @__PURE__ */ jsx("h2", { children: "QuantityWidget" }),
300
405
  /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Compound numeric value + unit selector. Multi-unit shows a dropdown, single-unit shows static text, prefix mode places the unit before the value." }),
@@ -398,4 +503,4 @@ function PlayFormWidgets() {
398
503
  export {
399
504
  PlayFormWidgets
400
505
  };
401
- //# sourceMappingURL=chunk-DZB6WFHA.js.map
506
+ //# sourceMappingURL=chunk-UB7462QG.js.map