@stigmer/react 0.0.89 → 0.0.91

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 (128) hide show
  1. package/identity-provider/CreateIdentityProviderForm.d.ts.map +1 -1
  2. package/identity-provider/CreateIdentityProviderForm.js +60 -2
  3. package/identity-provider/CreateIdentityProviderForm.js.map +1 -1
  4. package/identity-provider/IdentityProviderDetailPanel.d.ts.map +1 -1
  5. package/identity-provider/IdentityProviderDetailPanel.js +87 -4
  6. package/identity-provider/IdentityProviderDetailPanel.js.map +1 -1
  7. package/identity-provider/IdentityProviderListPanel.js +5 -3
  8. package/identity-provider/IdentityProviderListPanel.js.map +1 -1
  9. package/identity-provider/IdentityProviderWizard.d.ts.map +1 -1
  10. package/identity-provider/IdentityProviderWizard.js +59 -4
  11. package/identity-provider/IdentityProviderWizard.js.map +1 -1
  12. package/index.d.ts +2 -0
  13. package/index.d.ts.map +1 -1
  14. package/index.js +2 -0
  15. package/index.js.map +1 -1
  16. package/package.json +7 -7
  17. package/platform-client/CreatePlatformClientForm.d.ts +42 -0
  18. package/platform-client/CreatePlatformClientForm.d.ts.map +1 -0
  19. package/platform-client/CreatePlatformClientForm.js +148 -0
  20. package/platform-client/CreatePlatformClientForm.js.map +1 -0
  21. package/platform-client/PlatformClientDetailPanel.d.ts +51 -0
  22. package/platform-client/PlatformClientDetailPanel.d.ts.map +1 -0
  23. package/platform-client/PlatformClientDetailPanel.js +247 -0
  24. package/platform-client/PlatformClientDetailPanel.js.map +1 -0
  25. package/platform-client/PlatformClientListPanel.d.ts +41 -0
  26. package/platform-client/PlatformClientListPanel.d.ts.map +1 -0
  27. package/platform-client/PlatformClientListPanel.js +123 -0
  28. package/platform-client/PlatformClientListPanel.js.map +1 -0
  29. package/platform-client/PlatformClientSecretAlert.d.ts +39 -0
  30. package/platform-client/PlatformClientSecretAlert.d.ts.map +1 -0
  31. package/platform-client/PlatformClientSecretAlert.js +74 -0
  32. package/platform-client/PlatformClientSecretAlert.js.map +1 -0
  33. package/platform-client/index.d.ts +11 -0
  34. package/platform-client/index.d.ts.map +1 -0
  35. package/platform-client/index.js +11 -0
  36. package/platform-client/index.js.map +1 -0
  37. package/platform-client/useCreatePlatformClient.d.ts +42 -0
  38. package/platform-client/useCreatePlatformClient.d.ts.map +1 -0
  39. package/platform-client/useCreatePlatformClient.js +49 -0
  40. package/platform-client/useCreatePlatformClient.js.map +1 -0
  41. package/platform-client/useDeletePlatformClient.d.ts +31 -0
  42. package/platform-client/useDeletePlatformClient.d.ts.map +1 -0
  43. package/platform-client/useDeletePlatformClient.js +42 -0
  44. package/platform-client/useDeletePlatformClient.js.map +1 -0
  45. package/platform-client/usePlatformClient.d.ts +37 -0
  46. package/platform-client/usePlatformClient.d.ts.map +1 -0
  47. package/platform-client/usePlatformClient.js +62 -0
  48. package/platform-client/usePlatformClient.js.map +1 -0
  49. package/platform-client/usePlatformClientList.d.ts +42 -0
  50. package/platform-client/usePlatformClientList.d.ts.map +1 -0
  51. package/platform-client/usePlatformClientList.js +71 -0
  52. package/platform-client/usePlatformClientList.js.map +1 -0
  53. package/platform-client/useRotatePlatformClientSecret.d.ts +35 -0
  54. package/platform-client/useRotatePlatformClientSecret.d.ts.map +1 -0
  55. package/platform-client/useRotatePlatformClientSecret.js +43 -0
  56. package/platform-client/useRotatePlatformClientSecret.js.map +1 -0
  57. package/platform-client/useUpdatePlatformClient.d.ts +39 -0
  58. package/platform-client/useUpdatePlatformClient.d.ts.map +1 -0
  59. package/platform-client/useUpdatePlatformClient.js +50 -0
  60. package/platform-client/useUpdatePlatformClient.js.map +1 -0
  61. package/src/identity-provider/CreateIdentityProviderForm.tsx +220 -0
  62. package/src/identity-provider/IdentityProviderDetailPanel.tsx +288 -6
  63. package/src/identity-provider/IdentityProviderListPanel.tsx +9 -2
  64. package/src/identity-provider/IdentityProviderWizard.tsx +231 -25
  65. package/src/index.ts +26 -0
  66. package/src/platform-client/CreatePlatformClientForm.tsx +519 -0
  67. package/src/platform-client/PlatformClientDetailPanel.tsx +898 -0
  68. package/src/platform-client/PlatformClientListPanel.tsx +413 -0
  69. package/src/platform-client/PlatformClientSecretAlert.tsx +252 -0
  70. package/src/platform-client/index.ts +49 -0
  71. package/src/platform-client/useCreatePlatformClient.ts +77 -0
  72. package/src/platform-client/useDeletePlatformClient.ts +64 -0
  73. package/src/platform-client/usePlatformClient.ts +86 -0
  74. package/src/platform-client/usePlatformClientList.ts +96 -0
  75. package/src/platform-client/useRotatePlatformClientSecret.ts +68 -0
  76. package/src/platform-client/useUpdatePlatformClient.ts +70 -0
  77. package/src/test/index.ts +6 -0
  78. package/src/{demo → test}/samples.ts +1 -1
  79. package/styles.css +1 -1
  80. package/test/__tests__/samples.test.d.ts.map +1 -0
  81. package/{demo → test}/__tests__/samples.test.js.map +1 -1
  82. package/test/index.d.ts +2 -0
  83. package/test/index.d.ts.map +1 -0
  84. package/test/index.js +6 -0
  85. package/test/index.js.map +1 -0
  86. package/{demo → test}/samples.d.ts +1 -1
  87. package/{demo → test}/samples.d.ts.map +1 -1
  88. package/{demo → test}/samples.js +1 -1
  89. package/{demo → test}/samples.js.map +1 -1
  90. package/demo/__tests__/demo-client.test.d.ts +0 -2
  91. package/demo/__tests__/demo-client.test.d.ts.map +0 -1
  92. package/demo/__tests__/demo-client.test.js +0 -133
  93. package/demo/__tests__/demo-client.test.js.map +0 -1
  94. package/demo/__tests__/fixtures.test.d.ts +0 -2
  95. package/demo/__tests__/fixtures.test.d.ts.map +0 -1
  96. package/demo/__tests__/fixtures.test.js +0 -135
  97. package/demo/__tests__/fixtures.test.js.map +0 -1
  98. package/demo/__tests__/samples.test.d.ts.map +0 -1
  99. package/demo/client.d.ts +0 -29
  100. package/demo/client.d.ts.map +0 -1
  101. package/demo/client.js +0 -52
  102. package/demo/client.js.map +0 -1
  103. package/demo/fixtures.d.ts +0 -194
  104. package/demo/fixtures.d.ts.map +0 -1
  105. package/demo/fixtures.js +0 -267
  106. package/demo/fixtures.js.map +0 -1
  107. package/demo/index.d.ts +0 -6
  108. package/demo/index.d.ts.map +0 -1
  109. package/demo/index.js +0 -6
  110. package/demo/index.js.map +0 -1
  111. package/demo/transport.d.ts +0 -59
  112. package/demo/transport.d.ts.map +0 -1
  113. package/demo/transport.js +0 -75
  114. package/demo/transport.js.map +0 -1
  115. package/demo/types.d.ts +0 -62
  116. package/demo/types.d.ts.map +0 -1
  117. package/demo/types.js +0 -16
  118. package/demo/types.js.map +0 -1
  119. package/src/demo/__tests__/demo-client.test.tsx +0 -213
  120. package/src/demo/__tests__/fixtures.test.ts +0 -214
  121. package/src/demo/client.ts +0 -78
  122. package/src/demo/fixtures.ts +0 -409
  123. package/src/demo/index.ts +0 -12
  124. package/src/demo/transport.ts +0 -116
  125. package/src/demo/types.ts +0 -69
  126. /package/src/{demo → test}/__tests__/samples.test.ts +0 -0
  127. /package/{demo → test}/__tests__/samples.test.d.ts +0 -0
  128. /package/{demo → test}/__tests__/samples.test.js +0 -0
@@ -0,0 +1,413 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+ import { getUserMessage } from "@stigmer/sdk";
6
+ import { timestampDate } from "@bufbuild/protobuf/wkt";
7
+ import type { PlatformClient } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
8
+ import { usePlatformClientList } from "./usePlatformClientList";
9
+ import { useDeletePlatformClient } from "./useDeletePlatformClient";
10
+
11
+ /** Props for {@link PlatformClientListPanel}. */
12
+ export interface PlatformClientListPanelProps {
13
+ /** Organization slug whose platform clients should be listed. */
14
+ readonly org: string;
15
+ /** Fired when the user wants to edit a platform client. */
16
+ readonly onEdit?: (pc: PlatformClient) => void;
17
+ /** Expose the refetch function so parents can trigger a list refresh. */
18
+ readonly onRefetchRef?: (refetch: () => void) => void;
19
+ /** Additional CSS class names for the root container. */
20
+ readonly className?: string;
21
+ }
22
+
23
+ /**
24
+ * Displays a list of {@link PlatformClient} resources for an
25
+ * organization with inline delete confirmation.
26
+ *
27
+ * Each row shows the client name, `client_id` (monospace), secret
28
+ * fingerprint, expiry status, and creation date. A delete button
29
+ * triggers an inline confirmation flow.
30
+ *
31
+ * Platform clients are admin-level resources with small cardinality
32
+ * (typically 1–5 per org), so the list is rendered without pagination.
33
+ *
34
+ * All visual properties flow through `--stgm-*` design tokens.
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * <PlatformClientListPanel org="acme" />
39
+ * ```
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * <PlatformClientListPanel
44
+ * org="acme"
45
+ * onEdit={(pc) => setFlow({ phase: "editing", platformClient: pc })}
46
+ * onRefetchRef={(refetch) => { listRefetchRef.current = refetch; }}
47
+ * />
48
+ * ```
49
+ */
50
+ export function PlatformClientListPanel({
51
+ org,
52
+ onEdit,
53
+ onRefetchRef,
54
+ className,
55
+ }: PlatformClientListPanelProps) {
56
+ const { platformClients, isLoading, error, refetch } =
57
+ usePlatformClientList(org);
58
+ const [confirmingId, setConfirmingId] = useState<string | null>(null);
59
+
60
+ if (onRefetchRef) {
61
+ onRefetchRef(refetch);
62
+ }
63
+
64
+ if (isLoading) {
65
+ return (
66
+ <div
67
+ className={cn("space-y-2", className)}
68
+ aria-busy="true"
69
+ aria-label="Loading platform clients"
70
+ >
71
+ {Array.from({ length: 2 }, (_, i) => (
72
+ <div
73
+ key={i}
74
+ className="bg-muted/40 h-14 animate-pulse rounded-lg"
75
+ />
76
+ ))}
77
+ </div>
78
+ );
79
+ }
80
+
81
+ if (error) {
82
+ return (
83
+ <p className={cn("text-destructive text-xs", className)} role="alert">
84
+ {getUserMessage(error)}
85
+ </p>
86
+ );
87
+ }
88
+
89
+ if (platformClients.length === 0) {
90
+ return (
91
+ <p
92
+ className={cn(
93
+ "text-muted-foreground py-4 text-center text-xs",
94
+ className,
95
+ )}
96
+ >
97
+ No platform clients configured.
98
+ </p>
99
+ );
100
+ }
101
+
102
+ return (
103
+ <div
104
+ className={cn("space-y-2", className)}
105
+ role="list"
106
+ aria-label="Platform clients"
107
+ >
108
+ {platformClients.map((pc) => {
109
+ const id = pc.metadata?.id ?? "";
110
+ return (
111
+ <PlatformClientRow
112
+ key={id}
113
+ platformClient={pc}
114
+ isConfirming={confirmingId === id}
115
+ onEdit={onEdit ? () => onEdit(pc) : undefined}
116
+ onConfirmDelete={() => setConfirmingId(id)}
117
+ onCancelDelete={() => setConfirmingId(null)}
118
+ onDeleted={() => {
119
+ setConfirmingId(null);
120
+ refetch();
121
+ }}
122
+ />
123
+ );
124
+ })}
125
+ </div>
126
+ );
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // PlatformClientRow (internal)
131
+ // ---------------------------------------------------------------------------
132
+
133
+ function PlatformClientRow({
134
+ platformClient,
135
+ isConfirming,
136
+ onEdit,
137
+ onConfirmDelete,
138
+ onCancelDelete,
139
+ onDeleted,
140
+ }: {
141
+ platformClient: PlatformClient;
142
+ isConfirming: boolean;
143
+ onEdit?: () => void;
144
+ onConfirmDelete: () => void;
145
+ onCancelDelete: () => void;
146
+ onDeleted: () => void;
147
+ }) {
148
+ const { deletePlatformClient, isDeleting, error } =
149
+ useDeletePlatformClient();
150
+
151
+ const id = platformClient.metadata?.id ?? "";
152
+ const spec = platformClient.spec;
153
+ const name = platformClient.metadata?.name ?? "Unnamed client";
154
+ const clientId = spec?.clientId ?? "";
155
+ const fingerprint = spec?.secretFingerprint ?? "";
156
+ const createdAt =
157
+ platformClient.status?.audit?.specAudit?.createdAt;
158
+
159
+ const handleDelete = useCallback(async () => {
160
+ try {
161
+ await deletePlatformClient({ resourceId: id });
162
+ onDeleted();
163
+ } catch {
164
+ // error state is surfaced via the hook
165
+ }
166
+ }, [id, deletePlatformClient, onDeleted]);
167
+
168
+ if (isConfirming) {
169
+ return (
170
+ <div
171
+ role="listitem"
172
+ className="flex items-center justify-between rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2.5"
173
+ >
174
+ <div className="min-w-0 flex-1">
175
+ <p className="text-xs text-foreground">
176
+ Delete <span className="font-medium">{name}</span>?
177
+ {clientId && (
178
+ <span className="ml-1 text-muted-foreground">
179
+ This will invalidate client ID{" "}
180
+ <code className="font-mono">{clientId}</code>.
181
+ </span>
182
+ )}
183
+ </p>
184
+ {error && (
185
+ <p className="mt-0.5 text-[0.65rem] text-destructive">
186
+ {getUserMessage(error)}
187
+ </p>
188
+ )}
189
+ </div>
190
+
191
+ <div className="flex shrink-0 items-center gap-1.5">
192
+ <button
193
+ type="button"
194
+ onClick={handleDelete}
195
+ disabled={isDeleting}
196
+ className={cn(
197
+ "inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium",
198
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
199
+ "disabled:pointer-events-none disabled:opacity-50",
200
+ )}
201
+ >
202
+ {isDeleting && <SpinnerIcon />}
203
+ Delete
204
+ </button>
205
+ <button
206
+ type="button"
207
+ onClick={onCancelDelete}
208
+ disabled={isDeleting}
209
+ className={cn(
210
+ "rounded-md px-2.5 py-1 text-xs",
211
+ "text-muted-foreground hover:text-foreground hover:bg-accent/50",
212
+ "disabled:pointer-events-none disabled:opacity-50",
213
+ )}
214
+ >
215
+ Cancel
216
+ </button>
217
+ </div>
218
+ </div>
219
+ );
220
+ }
221
+
222
+ return (
223
+ <div
224
+ role="listitem"
225
+ className="flex items-center gap-3 rounded-lg border border-border/60 px-3 py-2.5 hover:border-border transition-colors"
226
+ >
227
+ <KeyIcon />
228
+
229
+ <div className="min-w-0 flex-1">
230
+ <span className="block truncate text-sm font-medium text-foreground">
231
+ {name}
232
+ </span>
233
+ {clientId && (
234
+ <span className="block truncate text-xs text-muted-foreground font-mono">
235
+ {clientId}
236
+ </span>
237
+ )}
238
+ </div>
239
+
240
+ <div className="hidden sm:flex shrink-0 items-center gap-3 text-xs text-muted-foreground">
241
+ {fingerprint && (
242
+ <span
243
+ className="font-mono"
244
+ title={`Secret fingerprint: ${fingerprint}`}
245
+ >
246
+ ••••{fingerprint.slice(-4)}
247
+ </span>
248
+ )}
249
+ <ExpiryBadge spec={spec} />
250
+ {spec?.autoProvisionAccounts && (
251
+ <span className="inline-flex items-center rounded-full border border-primary/30 bg-primary-subtle px-2 py-0.5 text-[0.65rem] font-medium text-primary">
252
+ JIT
253
+ </span>
254
+ )}
255
+ {createdAt && (
256
+ <span title={`Created ${timestampDate(createdAt).toISOString()}`}>
257
+ {formatShortDate(timestampDate(createdAt))}
258
+ </span>
259
+ )}
260
+ </div>
261
+
262
+ <div className="flex shrink-0 items-center gap-1">
263
+ {onEdit && (
264
+ <button
265
+ type="button"
266
+ onClick={onEdit}
267
+ aria-label={`Edit ${name}`}
268
+ className={cn(
269
+ "shrink-0 rounded p-1",
270
+ "text-muted-foreground hover:text-foreground hover:bg-accent/50",
271
+ "transition-colors",
272
+ )}
273
+ >
274
+ <PencilIcon />
275
+ </button>
276
+ )}
277
+ <button
278
+ type="button"
279
+ onClick={onConfirmDelete}
280
+ aria-label={`Delete ${name}`}
281
+ className={cn(
282
+ "shrink-0 rounded p-1",
283
+ "text-muted-foreground hover:text-destructive hover:bg-destructive/10",
284
+ "transition-colors",
285
+ )}
286
+ >
287
+ <TrashIcon />
288
+ </button>
289
+ </div>
290
+ </div>
291
+ );
292
+ }
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // ExpiryBadge (internal)
296
+ // ---------------------------------------------------------------------------
297
+
298
+ function ExpiryBadge({ spec }: { spec: PlatformClient["spec"] }) {
299
+ if (spec?.neverExpires) {
300
+ return (
301
+ <span className="text-[0.65rem] text-muted-foreground">No expiry</span>
302
+ );
303
+ }
304
+ if (spec?.expiresAt) {
305
+ const date = timestampDate(spec.expiresAt);
306
+ const isExpired = date < new Date();
307
+ return (
308
+ <span
309
+ className={cn(
310
+ "text-[0.65rem]",
311
+ isExpired ? "text-destructive font-medium" : "text-muted-foreground",
312
+ )}
313
+ title={`Expires ${date.toISOString()}`}
314
+ >
315
+ {isExpired ? "Expired" : `Exp ${formatShortDate(date)}`}
316
+ </span>
317
+ );
318
+ }
319
+ return null;
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Formatting helpers
324
+ // ---------------------------------------------------------------------------
325
+
326
+ function formatShortDate(date: Date): string {
327
+ return date.toLocaleDateString(undefined, {
328
+ month: "short",
329
+ day: "numeric",
330
+ year: "numeric",
331
+ });
332
+ }
333
+
334
+ // ---------------------------------------------------------------------------
335
+ // Icons
336
+ // ---------------------------------------------------------------------------
337
+
338
+ function KeyIcon() {
339
+ return (
340
+ <svg
341
+ width="14"
342
+ height="14"
343
+ viewBox="0 0 16 16"
344
+ fill="none"
345
+ stroke="currentColor"
346
+ strokeWidth="1.5"
347
+ strokeLinecap="round"
348
+ strokeLinejoin="round"
349
+ aria-hidden="true"
350
+ className="shrink-0 text-muted-foreground"
351
+ >
352
+ <circle cx="10.5" cy="5.5" r="3" />
353
+ <path d="M8.5 7.5L3 13l-.5-2.5L5 10l1-1-1-1 3.5.5z" />
354
+ </svg>
355
+ );
356
+ }
357
+
358
+ function PencilIcon() {
359
+ return (
360
+ <svg
361
+ width="14"
362
+ height="14"
363
+ viewBox="0 0 16 16"
364
+ fill="none"
365
+ stroke="currentColor"
366
+ strokeWidth="1.5"
367
+ strokeLinecap="round"
368
+ strokeLinejoin="round"
369
+ aria-hidden="true"
370
+ >
371
+ <path d="M11 2.5l2.5 2.5L5 13.5H2.5V11L11 2.5z" />
372
+ </svg>
373
+ );
374
+ }
375
+
376
+ function TrashIcon() {
377
+ return (
378
+ <svg
379
+ width="14"
380
+ height="14"
381
+ viewBox="0 0 16 16"
382
+ fill="none"
383
+ stroke="currentColor"
384
+ strokeWidth="1.5"
385
+ strokeLinecap="round"
386
+ strokeLinejoin="round"
387
+ aria-hidden="true"
388
+ >
389
+ <path d="M2.5 4h11M5.5 4V2.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V4" />
390
+ <path d="M12.5 4v9a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V4" />
391
+ <line x1="6.5" y1="7" x2="6.5" y2="11" />
392
+ <line x1="9.5" y1="7" x2="9.5" y2="11" />
393
+ </svg>
394
+ );
395
+ }
396
+
397
+ function SpinnerIcon() {
398
+ return (
399
+ <svg
400
+ width="12"
401
+ height="12"
402
+ viewBox="0 0 16 16"
403
+ fill="none"
404
+ stroke="currentColor"
405
+ strokeWidth="2"
406
+ strokeLinecap="round"
407
+ className="animate-spin"
408
+ aria-hidden="true"
409
+ >
410
+ <path d="M8 2a6 6 0 1 0 6 6" />
411
+ </svg>
412
+ );
413
+ }
@@ -0,0 +1,252 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+
6
+ /** Props for {@link PlatformClientSecretAlert}. */
7
+ export interface PlatformClientSecretAlertProps {
8
+ /** The `client_id` to display alongside the secret for pairing context. */
9
+ readonly clientId: string;
10
+ /** The raw client secret value to display. Shown exactly once. */
11
+ readonly clientSecret: string;
12
+ /** Whether this alert follows a creation or a secret rotation. */
13
+ readonly context: "created" | "rotated";
14
+ /** Fired when the user dismisses the alert. */
15
+ readonly onDismiss: () => void;
16
+ /** Additional CSS class names for the root container. */
17
+ readonly className?: string;
18
+ }
19
+
20
+ const COPIED_FEEDBACK_MS = 2000;
21
+
22
+ /**
23
+ * One-time reveal component displayed after a platform client is
24
+ * created or its secret is rotated.
25
+ *
26
+ * Shows both the `client_id` (read-only, for context) and the raw
27
+ * `client_secret` with a **Copy** button. The secret is only
28
+ * available once — the server never returns it again — so this
29
+ * component prominently warns the user to copy it immediately.
30
+ *
31
+ * This is a standalone alert component, not a modal — the parent
32
+ * decides how to present and position it.
33
+ *
34
+ * All visual properties flow through `--stgm-*` design tokens.
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * <PlatformClientSecretAlert
39
+ * clientId="stgm_pc_abc123"
40
+ * clientSecret="stgm_secret_xyz..."
41
+ * context="created"
42
+ * onDismiss={() => setFlow({ phase: "idle" })}
43
+ * />
44
+ * ```
45
+ */
46
+ export function PlatformClientSecretAlert({
47
+ clientId,
48
+ clientSecret,
49
+ context,
50
+ onDismiss,
51
+ className,
52
+ }: PlatformClientSecretAlertProps) {
53
+ const [copiedField, setCopiedField] = useState<
54
+ "id" | "secret" | null
55
+ >(null);
56
+
57
+ const handleCopy = useCallback(
58
+ async (value: string, field: "id" | "secret") => {
59
+ try {
60
+ await navigator.clipboard.writeText(value);
61
+ setCopiedField(field);
62
+ setTimeout(() => setCopiedField(null), COPIED_FEEDBACK_MS);
63
+ } catch {
64
+ const el = document.getElementById(
65
+ field === "secret"
66
+ ? "stgm-pc-secret-reveal"
67
+ : "stgm-pc-client-id-reveal",
68
+ );
69
+ if (el) {
70
+ const selection = window.getSelection();
71
+ const range = document.createRange();
72
+ range.selectNodeContents(el);
73
+ selection?.removeAllRanges();
74
+ selection?.addRange(range);
75
+ }
76
+ }
77
+ },
78
+ [],
79
+ );
80
+
81
+ const title =
82
+ context === "created"
83
+ ? "Platform client created"
84
+ : "Client secret rotated";
85
+
86
+ return (
87
+ <div
88
+ role="alert"
89
+ className={cn(
90
+ "rounded-lg border border-primary/30 bg-primary-subtle p-4",
91
+ className,
92
+ )}
93
+ >
94
+ <div className="mb-3 flex items-start justify-between gap-3">
95
+ <div className="min-w-0">
96
+ <p className="text-sm font-medium text-foreground">{title}</p>
97
+ <p className="mt-0.5 text-xs text-muted-foreground">
98
+ Copy the client secret now. It will not be shown again.
99
+ </p>
100
+ </div>
101
+ <button
102
+ type="button"
103
+ onClick={onDismiss}
104
+ aria-label="Dismiss"
105
+ className={cn(
106
+ "shrink-0 rounded p-1",
107
+ "text-muted-foreground hover:text-foreground hover:bg-accent/50",
108
+ "transition-colors",
109
+ )}
110
+ >
111
+ <CloseIcon />
112
+ </button>
113
+ </div>
114
+
115
+ <div className="space-y-2">
116
+ <CopyableField
117
+ id="stgm-pc-client-id-reveal"
118
+ label="Client ID"
119
+ value={clientId}
120
+ copied={copiedField === "id"}
121
+ onCopy={() => handleCopy(clientId, "id")}
122
+ />
123
+ <CopyableField
124
+ id="stgm-pc-secret-reveal"
125
+ label="Client secret"
126
+ value={clientSecret}
127
+ copied={copiedField === "secret"}
128
+ onCopy={() => handleCopy(clientSecret, "secret")}
129
+ />
130
+ </div>
131
+
132
+ <div
133
+ role="status"
134
+ aria-live="polite"
135
+ aria-atomic="true"
136
+ className="sr-only"
137
+ >
138
+ {copiedField === "id" && "Client ID copied to clipboard"}
139
+ {copiedField === "secret" && "Client secret copied to clipboard"}
140
+ </div>
141
+ </div>
142
+ );
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // CopyableField (internal)
147
+ // ---------------------------------------------------------------------------
148
+
149
+ function CopyableField({
150
+ id,
151
+ label,
152
+ value,
153
+ copied,
154
+ onCopy,
155
+ }: {
156
+ id: string;
157
+ label: string;
158
+ value: string;
159
+ copied: boolean;
160
+ onCopy: () => void;
161
+ }) {
162
+ return (
163
+ <div>
164
+ <span className="text-[0.65rem] font-medium text-muted-foreground">
165
+ {label}
166
+ </span>
167
+ <div className="mt-0.5 flex items-center gap-2">
168
+ <code
169
+ id={id}
170
+ className={cn(
171
+ "min-w-0 flex-1 select-all truncate rounded-md",
172
+ "border border-input bg-background px-2.5 py-1.5",
173
+ "font-mono text-xs text-foreground",
174
+ )}
175
+ >
176
+ {value}
177
+ </code>
178
+ <button
179
+ type="button"
180
+ onClick={onCopy}
181
+ aria-label={`Copy ${label}`}
182
+ className={cn(
183
+ "inline-flex shrink-0 items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium",
184
+ "bg-primary text-primary-foreground hover:bg-primary/90",
185
+ "transition-colors",
186
+ )}
187
+ >
188
+ {copied ? <CheckIcon /> : <CopyIcon />}
189
+ {copied ? "Copied" : "Copy"}
190
+ </button>
191
+ </div>
192
+ </div>
193
+ );
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Icons
198
+ // ---------------------------------------------------------------------------
199
+
200
+ function CopyIcon() {
201
+ return (
202
+ <svg
203
+ width="12"
204
+ height="12"
205
+ viewBox="0 0 16 16"
206
+ fill="none"
207
+ stroke="currentColor"
208
+ strokeWidth="1.5"
209
+ strokeLinecap="round"
210
+ strokeLinejoin="round"
211
+ aria-hidden="true"
212
+ >
213
+ <rect x="5" y="5" width="9" height="9" rx="1.5" />
214
+ <path d="M5 11H3.5A1.5 1.5 0 0 1 2 9.5V3.5A1.5 1.5 0 0 1 3.5 2h6A1.5 1.5 0 0 1 11 3.5V5" />
215
+ </svg>
216
+ );
217
+ }
218
+
219
+ function CheckIcon() {
220
+ return (
221
+ <svg
222
+ width="12"
223
+ height="12"
224
+ viewBox="0 0 16 16"
225
+ fill="none"
226
+ stroke="currentColor"
227
+ strokeWidth="2"
228
+ strokeLinecap="round"
229
+ strokeLinejoin="round"
230
+ aria-hidden="true"
231
+ >
232
+ <path d="M3 8.5l3.5 3.5L13 5" />
233
+ </svg>
234
+ );
235
+ }
236
+
237
+ function CloseIcon() {
238
+ return (
239
+ <svg
240
+ width="14"
241
+ height="14"
242
+ viewBox="0 0 16 16"
243
+ fill="none"
244
+ stroke="currentColor"
245
+ strokeWidth="1.5"
246
+ strokeLinecap="round"
247
+ aria-hidden="true"
248
+ >
249
+ <path d="M4 4l8 8M12 4l-8 8" />
250
+ </svg>
251
+ );
252
+ }
@@ -0,0 +1,49 @@
1
+ export {
2
+ usePlatformClientList,
3
+ type UsePlatformClientListReturn,
4
+ } from "./usePlatformClientList";
5
+
6
+ export {
7
+ usePlatformClient,
8
+ type UsePlatformClientReturn,
9
+ } from "./usePlatformClient";
10
+
11
+ export {
12
+ useCreatePlatformClient,
13
+ type UseCreatePlatformClientReturn,
14
+ } from "./useCreatePlatformClient";
15
+
16
+ export {
17
+ useUpdatePlatformClient,
18
+ type UseUpdatePlatformClientReturn,
19
+ } from "./useUpdatePlatformClient";
20
+
21
+ export {
22
+ useDeletePlatformClient,
23
+ type UseDeletePlatformClientReturn,
24
+ } from "./useDeletePlatformClient";
25
+
26
+ export {
27
+ useRotatePlatformClientSecret,
28
+ type UseRotatePlatformClientSecretReturn,
29
+ } from "./useRotatePlatformClientSecret";
30
+
31
+ export {
32
+ PlatformClientListPanel,
33
+ type PlatformClientListPanelProps,
34
+ } from "./PlatformClientListPanel";
35
+
36
+ export {
37
+ CreatePlatformClientForm,
38
+ type CreatePlatformClientFormProps,
39
+ } from "./CreatePlatformClientForm";
40
+
41
+ export {
42
+ PlatformClientDetailPanel,
43
+ type PlatformClientDetailPanelProps,
44
+ } from "./PlatformClientDetailPanel";
45
+
46
+ export {
47
+ PlatformClientSecretAlert,
48
+ type PlatformClientSecretAlertProps,
49
+ } from "./PlatformClientSecretAlert";