@actuate-media/cms-admin 0.48.0 → 0.49.1

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 (72) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/AdminRoot.d.ts +10 -0
  3. package/dist/AdminRoot.d.ts.map +1 -1
  4. package/dist/AdminRoot.js +25 -43
  5. package/dist/AdminRoot.js.map +1 -1
  6. package/dist/LoginRoot.d.ts +54 -0
  7. package/dist/LoginRoot.d.ts.map +1 -0
  8. package/dist/LoginRoot.js +59 -0
  9. package/dist/LoginRoot.js.map +1 -0
  10. package/dist/__tests__/components/modal.render.test.d.ts +2 -0
  11. package/dist/__tests__/components/modal.render.test.d.ts.map +1 -0
  12. package/dist/__tests__/components/modal.render.test.js +37 -0
  13. package/dist/__tests__/components/modal.render.test.js.map +1 -0
  14. package/dist/__tests__/views/login-root.render.test.d.ts +2 -0
  15. package/dist/__tests__/views/login-root.render.test.d.ts.map +1 -0
  16. package/dist/__tests__/views/login-root.render.test.js +79 -0
  17. package/dist/__tests__/views/login-root.render.test.js.map +1 -0
  18. package/dist/actuate-admin.css +1 -1
  19. package/dist/components/MediaPickerModal.d.ts +1 -1
  20. package/dist/components/MediaPickerModal.d.ts.map +1 -1
  21. package/dist/components/MediaPickerModal.js +12 -11
  22. package/dist/components/MediaPickerModal.js.map +1 -1
  23. package/dist/components/SchedulePublishDialog.d.ts +1 -1
  24. package/dist/components/SchedulePublishDialog.d.ts.map +1 -1
  25. package/dist/components/SchedulePublishDialog.js +6 -4
  26. package/dist/components/SchedulePublishDialog.js.map +1 -1
  27. package/dist/components/SharePreviewLinkDialog.d.ts +1 -1
  28. package/dist/components/SharePreviewLinkDialog.d.ts.map +1 -1
  29. package/dist/components/SharePreviewLinkDialog.js +10 -9
  30. package/dist/components/SharePreviewLinkDialog.js.map +1 -1
  31. package/dist/components/VersionHistory.d.ts +1 -1
  32. package/dist/components/VersionHistory.d.ts.map +1 -1
  33. package/dist/components/VersionHistory.js +14 -14
  34. package/dist/components/VersionHistory.js.map +1 -1
  35. package/dist/components/ui/Modal.d.ts +17 -1
  36. package/dist/components/ui/Modal.d.ts.map +1 -1
  37. package/dist/components/ui/Modal.js +16 -15
  38. package/dist/components/ui/Modal.js.map +1 -1
  39. package/dist/fields/BlockBuilderField.d.ts.map +1 -1
  40. package/dist/fields/BlockBuilderField.js +6 -4
  41. package/dist/fields/BlockBuilderField.js.map +1 -1
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +1 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/lib/root-chrome.d.ts +15 -0
  47. package/dist/lib/root-chrome.d.ts.map +1 -0
  48. package/dist/lib/root-chrome.js +41 -0
  49. package/dist/lib/root-chrome.js.map +1 -0
  50. package/dist/views/HiddenLoginNotFound.d.ts +8 -0
  51. package/dist/views/HiddenLoginNotFound.d.ts.map +1 -0
  52. package/dist/views/HiddenLoginNotFound.js +29 -0
  53. package/dist/views/HiddenLoginNotFound.js.map +1 -0
  54. package/dist/views/page-builder/AIGenerateDialog.d.ts +1 -1
  55. package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -1
  56. package/dist/views/page-builder/AIGenerateDialog.js +23 -23
  57. package/dist/views/page-builder/AIGenerateDialog.js.map +1 -1
  58. package/package.json +2 -2
  59. package/src/AdminRoot.tsx +61 -59
  60. package/src/LoginRoot.tsx +166 -0
  61. package/src/__tests__/components/modal.render.test.tsx +57 -0
  62. package/src/__tests__/views/login-root.render.test.tsx +115 -0
  63. package/src/components/MediaPickerModal.tsx +143 -119
  64. package/src/components/SchedulePublishDialog.tsx +118 -93
  65. package/src/components/SharePreviewLinkDialog.tsx +115 -93
  66. package/src/components/VersionHistory.tsx +98 -82
  67. package/src/components/ui/Modal.tsx +48 -39
  68. package/src/fields/BlockBuilderField.tsx +26 -19
  69. package/src/index.ts +2 -0
  70. package/src/lib/root-chrome.ts +40 -0
  71. package/src/views/HiddenLoginNotFound.tsx +45 -0
  72. package/src/views/page-builder/AIGenerateDialog.tsx +347 -335
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { useState, useCallback } from 'react'
4
+ import * as Dialog from '@radix-ui/react-dialog'
4
5
  import {
5
6
  Sparkles,
6
7
  X,
@@ -17,6 +18,7 @@ import {
17
18
  } from 'lucide-react'
18
19
  import { toast } from 'sonner'
19
20
  import { cmsApi } from '../../lib/api.js'
21
+ import { adminPortalContainer } from '../../lib/portal-container.js'
20
22
  import type { PageNode, GenerationStep } from '@actuate-media/cms-core'
21
23
 
22
24
  export interface AIGenerateDialogProps {
@@ -205,377 +207,387 @@ export function AIGenerateDialog({ open, onClose, onAccept }: AIGenerateDialogPr
205
207
  }
206
208
  }, [result, onAccept, handleClose])
207
209
 
208
- if (!open) return null
209
-
210
210
  return (
211
- <div className="fixed inset-0 z-50 flex items-center justify-center">
212
- <div className="absolute inset-0 bg-black/50" onClick={handleClose} />
213
- <div className="bg-card border-border relative flex max-h-[85vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl border shadow-2xl">
214
- {/* Header */}
215
- <div className="border-border flex items-center justify-between border-b px-6 py-4">
216
- <div className="flex items-center gap-2.5">
217
- <div className="bg-primary/10 rounded-lg p-1.5">
218
- <Sparkles size={18} className="text-primary" />
219
- </div>
220
- <div>
221
- <h2 className="text-foreground text-base font-medium">AI Page Generator</h2>
222
- <p className="text-muted-foreground text-xs">
223
- Describe your page and let AI build it
224
- </p>
211
+ <Dialog.Root open={open} onOpenChange={(next) => !next && handleClose()}>
212
+ <Dialog.Portal container={adminPortalContainer()}>
213
+ <Dialog.Overlay className="fixed inset-0 z-50 bg-black/50" />
214
+ <Dialog.Content className="bg-card border-border text-card-foreground fixed top-1/2 left-1/2 z-50 flex max-h-[85vh] w-[calc(100%-2rem)] max-w-2xl -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl border shadow-2xl focus:outline-none">
215
+ {/* Header */}
216
+ <div className="border-border flex items-center justify-between border-b px-6 py-4">
217
+ <div className="flex items-center gap-2.5">
218
+ <div className="bg-primary/10 rounded-lg p-1.5">
219
+ <Sparkles size={18} className="text-primary" aria-hidden />
220
+ </div>
221
+ <div>
222
+ <Dialog.Title className="text-foreground text-base font-medium">
223
+ AI Page Generator
224
+ </Dialog.Title>
225
+ <Dialog.Description className="text-muted-foreground text-xs">
226
+ Describe your page and let AI build it
227
+ </Dialog.Description>
228
+ </div>
225
229
  </div>
230
+ <Dialog.Close asChild>
231
+ <button
232
+ type="button"
233
+ className="hover:bg-muted text-muted-foreground hover:text-foreground focus-visible:ring-ring rounded-md p-1.5 transition-colors focus-visible:ring-2 focus-visible:outline-none"
234
+ aria-label="Close AI page generator"
235
+ >
236
+ <X size={16} aria-hidden />
237
+ </button>
238
+ </Dialog.Close>
226
239
  </div>
227
- <button
228
- onClick={handleClose}
229
- className="hover:bg-muted rounded-md p-1.5 transition-colors"
230
- aria-label="Close"
231
- >
232
- <X size={16} className="text-muted-foreground" />
233
- </button>
234
- </div>
235
-
236
- {/* Content */}
237
- <div className="flex-1 overflow-y-auto p-6">
238
- {phase === 'prompt' && (
239
- <div className="space-y-5">
240
- {error && (
241
- <div className="bg-destructive/5 border-destructive/20 flex items-start gap-2.5 rounded-lg border p-3">
242
- <AlertTriangle size={16} className="text-destructive mt-0.5 shrink-0" />
243
- <p className="text-destructive text-sm">{error}</p>
244
- </div>
245
- )}
246
240
 
247
- {/* Prompt */}
248
- <div>
249
- <label className="text-foreground mb-1.5 block text-sm font-medium">
250
- Describe your page
251
- </label>
252
- <textarea
253
- value={prompt}
254
- onChange={(e) => setPrompt(e.target.value)}
255
- placeholder="e.g. A landing page for a plumbing company with a hero section, service cards, testimonials, and a contact form"
256
- rows={4}
257
- className="bg-background border-border focus:ring-primary/40 text-foreground placeholder:text-muted-foreground w-full resize-none rounded-lg border px-3 py-2.5 text-sm focus:ring-2 focus:outline-none"
258
- autoFocus
259
- />
260
- </div>
241
+ {/* Content */}
242
+ <div className="flex-1 overflow-y-auto p-6">
243
+ {phase === 'prompt' && (
244
+ <div className="space-y-5">
245
+ {error && (
246
+ <div className="bg-destructive/5 border-destructive/20 flex items-start gap-2.5 rounded-lg border p-3">
247
+ <AlertTriangle size={16} className="text-destructive mt-0.5 shrink-0" />
248
+ <p className="text-destructive text-sm">{error}</p>
249
+ </div>
250
+ )}
261
251
 
262
- {/* Tone */}
263
- <div>
264
- <label className="text-foreground mb-1.5 block text-sm font-medium">Tone</label>
265
- <div className="flex flex-wrap gap-2">
266
- {TONES.map((t) => (
267
- <button
268
- key={t.value}
269
- onClick={() => setTone(t.value)}
270
- className={`rounded-full border px-3 py-1.5 text-xs transition-colors ${
271
- tone === t.value
272
- ? 'border-primary bg-primary/10 text-primary'
273
- : 'border-border bg-background text-muted-foreground hover:border-primary/40'
274
- }`}
275
- >
276
- {t.label}
277
- </button>
278
- ))}
252
+ {/* Prompt */}
253
+ <div>
254
+ <label className="text-foreground mb-1.5 block text-sm font-medium">
255
+ Describe your page
256
+ </label>
257
+ <textarea
258
+ value={prompt}
259
+ onChange={(e) => setPrompt(e.target.value)}
260
+ placeholder="e.g. A landing page for a plumbing company with a hero section, service cards, testimonials, and a contact form"
261
+ rows={4}
262
+ className="bg-background border-border focus:ring-primary/40 text-foreground placeholder:text-muted-foreground w-full resize-none rounded-lg border px-3 py-2.5 text-sm focus:ring-2 focus:outline-none"
263
+ autoFocus
264
+ />
279
265
  </div>
280
- </div>
281
266
 
282
- {/* Steps */}
283
- <div>
284
- <label className="text-foreground mb-1.5 block text-sm font-medium">
285
- Generation steps
286
- </label>
287
- <div className="space-y-1.5">
288
- {ALL_STEPS.map((step) => (
289
- <label
290
- key={step.key}
291
- className={`flex cursor-pointer items-center gap-3 rounded-lg border p-2.5 transition-colors ${
292
- enabledSteps.has(step.key)
293
- ? 'border-primary/30 bg-primary/5'
294
- : 'border-border bg-background'
295
- } ${step.key === 'structure' ? 'cursor-not-allowed opacity-70' : 'hover:border-primary/20'}`}
296
- >
297
- <input
298
- type="checkbox"
299
- checked={enabledSteps.has(step.key)}
300
- onChange={() => toggleStep(step.key)}
301
- disabled={step.key === 'structure'}
302
- className="sr-only"
303
- />
304
- <div
305
- className={`flex h-5 w-5 shrink-0 items-center justify-center rounded ${
306
- enabledSteps.has(step.key)
307
- ? 'bg-primary text-white'
308
- : 'bg-muted border-border border'
267
+ {/* Tone */}
268
+ <div>
269
+ <label className="text-foreground mb-1.5 block text-sm font-medium">Tone</label>
270
+ <div className="flex flex-wrap gap-2">
271
+ {TONES.map((t) => (
272
+ <button
273
+ key={t.value}
274
+ onClick={() => setTone(t.value)}
275
+ className={`rounded-full border px-3 py-1.5 text-xs transition-colors ${
276
+ tone === t.value
277
+ ? 'border-primary bg-primary/10 text-primary'
278
+ : 'border-border bg-background text-muted-foreground hover:border-primary/40'
309
279
  }`}
310
280
  >
311
- {enabledSteps.has(step.key) && <Check size={12} />}
312
- </div>
313
- <div className="flex min-w-0 flex-1 items-center gap-2">
314
- <span className="text-muted-foreground">{step.icon}</span>
315
- <div>
316
- <span className="text-foreground text-sm">{step.label}</span>
317
- <span className="text-muted-foreground ml-2 text-xs">
318
- {step.description}
319
- </span>
320
- </div>
321
- </div>
322
- </label>
323
- ))}
281
+ {t.label}
282
+ </button>
283
+ ))}
284
+ </div>
324
285
  </div>
325
- </div>
326
286
 
327
- {/* Business context (collapsible) */}
328
- <div>
329
- <button
330
- onClick={() => setShowContext(!showContext)}
331
- className="text-muted-foreground hover:text-foreground flex items-center gap-1.5 text-sm transition-colors"
332
- >
333
- {showContext ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
334
- Business context (optional)
335
- </button>
336
- {showContext && (
337
- <div className="bg-muted/50 border-border mt-3 space-y-3 rounded-lg border p-4">
338
- <div className="grid grid-cols-2 gap-3">
339
- <div>
340
- <label className="text-muted-foreground mb-1 block text-xs">
341
- Business name
342
- </label>
343
- <input
344
- type="text"
345
- value={businessName}
346
- onChange={(e) => setBusiness(e.target.value)}
347
- className="bg-background border-border focus:ring-primary/40 text-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
348
- />
349
- </div>
350
- <div>
351
- <label className="text-muted-foreground mb-1 block text-xs">Industry</label>
352
- <input
353
- type="text"
354
- value={industry}
355
- onChange={(e) => setIndustry(e.target.value)}
356
- className="bg-background border-border focus:ring-primary/40 text-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
357
- />
358
- </div>
359
- <div>
360
- <label className="text-muted-foreground mb-1 block text-xs">Phone</label>
287
+ {/* Steps */}
288
+ <div>
289
+ <label className="text-foreground mb-1.5 block text-sm font-medium">
290
+ Generation steps
291
+ </label>
292
+ <div className="space-y-1.5">
293
+ {ALL_STEPS.map((step) => (
294
+ <label
295
+ key={step.key}
296
+ className={`flex cursor-pointer items-center gap-3 rounded-lg border p-2.5 transition-colors ${
297
+ enabledSteps.has(step.key)
298
+ ? 'border-primary/30 bg-primary/5'
299
+ : 'border-border bg-background'
300
+ } ${step.key === 'structure' ? 'cursor-not-allowed opacity-70' : 'hover:border-primary/20'}`}
301
+ >
361
302
  <input
362
- type="text"
363
- value={phone}
364
- onChange={(e) => setPhone(e.target.value)}
365
- className="bg-background border-border focus:ring-primary/40 text-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
303
+ type="checkbox"
304
+ checked={enabledSteps.has(step.key)}
305
+ onChange={() => toggleStep(step.key)}
306
+ disabled={step.key === 'structure'}
307
+ className="sr-only"
366
308
  />
309
+ <div
310
+ className={`flex h-5 w-5 shrink-0 items-center justify-center rounded ${
311
+ enabledSteps.has(step.key)
312
+ ? 'bg-primary text-primary-foreground'
313
+ : 'bg-muted border-border border'
314
+ }`}
315
+ >
316
+ {enabledSteps.has(step.key) && <Check size={12} />}
317
+ </div>
318
+ <div className="flex min-w-0 flex-1 items-center gap-2">
319
+ <span className="text-muted-foreground">{step.icon}</span>
320
+ <div>
321
+ <span className="text-foreground text-sm">{step.label}</span>
322
+ <span className="text-muted-foreground ml-2 text-xs">
323
+ {step.description}
324
+ </span>
325
+ </div>
326
+ </div>
327
+ </label>
328
+ ))}
329
+ </div>
330
+ </div>
331
+
332
+ {/* Business context (collapsible) */}
333
+ <div>
334
+ <button
335
+ onClick={() => setShowContext(!showContext)}
336
+ className="text-muted-foreground hover:text-foreground flex items-center gap-1.5 text-sm transition-colors"
337
+ >
338
+ {showContext ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
339
+ Business context (optional)
340
+ </button>
341
+ {showContext && (
342
+ <div className="bg-muted/50 border-border mt-3 space-y-3 rounded-lg border p-4">
343
+ <div className="grid grid-cols-2 gap-3">
344
+ <div>
345
+ <label className="text-muted-foreground mb-1 block text-xs">
346
+ Business name
347
+ </label>
348
+ <input
349
+ type="text"
350
+ value={businessName}
351
+ onChange={(e) => setBusiness(e.target.value)}
352
+ className="bg-background border-border focus:ring-primary/40 text-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
353
+ />
354
+ </div>
355
+ <div>
356
+ <label className="text-muted-foreground mb-1 block text-xs">
357
+ Industry
358
+ </label>
359
+ <input
360
+ type="text"
361
+ value={industry}
362
+ onChange={(e) => setIndustry(e.target.value)}
363
+ className="bg-background border-border focus:ring-primary/40 text-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
364
+ />
365
+ </div>
366
+ <div>
367
+ <label className="text-muted-foreground mb-1 block text-xs">Phone</label>
368
+ <input
369
+ type="text"
370
+ value={phone}
371
+ onChange={(e) => setPhone(e.target.value)}
372
+ className="bg-background border-border focus:ring-primary/40 text-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
373
+ />
374
+ </div>
375
+ <div>
376
+ <label className="text-muted-foreground mb-1 block text-xs">
377
+ Address
378
+ </label>
379
+ <input
380
+ type="text"
381
+ value={address}
382
+ onChange={(e) => setAddress(e.target.value)}
383
+ className="bg-background border-border focus:ring-primary/40 text-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
384
+ />
385
+ </div>
367
386
  </div>
368
387
  <div>
369
- <label className="text-muted-foreground mb-1 block text-xs">Address</label>
388
+ <label className="text-muted-foreground mb-1 block text-xs">
389
+ Services (comma-separated)
390
+ </label>
370
391
  <input
371
392
  type="text"
372
- value={address}
373
- onChange={(e) => setAddress(e.target.value)}
374
- className="bg-background border-border focus:ring-primary/40 text-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
393
+ value={services}
394
+ onChange={(e) => setServices(e.target.value)}
395
+ placeholder="e.g. Web design, SEO, Content marketing"
396
+ className="bg-background border-border focus:ring-primary/40 text-foreground placeholder:text-muted-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
375
397
  />
376
398
  </div>
377
399
  </div>
378
- <div>
379
- <label className="text-muted-foreground mb-1 block text-xs">
380
- Services (comma-separated)
381
- </label>
382
- <input
383
- type="text"
384
- value={services}
385
- onChange={(e) => setServices(e.target.value)}
386
- placeholder="e.g. Web design, SEO, Content marketing"
387
- className="bg-background border-border focus:ring-primary/40 text-foreground placeholder:text-muted-foreground w-full rounded-md border px-2.5 py-1.5 text-sm focus:ring-2 focus:outline-none"
388
- />
389
- </div>
390
- </div>
391
- )}
392
- </div>
393
- </div>
394
- )}
395
-
396
- {phase === 'generating' && (
397
- <div className="space-y-6 py-8">
398
- <div className="text-center">
399
- <Loader2 size={32} className="text-primary mx-auto mb-3 animate-spin" />
400
- <p className="text-foreground text-sm font-medium">
401
- {stepMessage || 'Generating...'}
402
- </p>
403
- <p className="text-muted-foreground mt-1 text-xs">This may take 15-30 seconds</p>
400
+ )}
401
+ </div>
404
402
  </div>
403
+ )}
404
+
405
+ {phase === 'generating' && (
406
+ <div className="space-y-6 py-8">
407
+ <div className="text-center">
408
+ <Loader2 size={32} className="text-primary mx-auto mb-3 animate-spin" />
409
+ <p className="text-foreground text-sm font-medium">
410
+ {stepMessage || 'Generating...'}
411
+ </p>
412
+ <p className="text-muted-foreground mt-1 text-xs">This may take 15-30 seconds</p>
413
+ </div>
405
414
 
406
- <div className="space-y-2">
407
- {ALL_STEPS.filter((s) => enabledSteps.has(s.key)).map((step) => {
408
- const isComplete = completedSteps.includes(step.key)
409
- const isCurrent = currentStep === step.key
410
-
411
- return (
412
- <div
413
- key={step.key}
414
- className={`flex items-center gap-3 rounded-lg border p-3 transition-colors ${
415
- isComplete
416
- ? 'border-primary/30 bg-primary/5'
417
- : isCurrent
418
- ? 'border-primary/50 bg-primary/10'
419
- : 'border-border bg-muted/30'
420
- }`}
421
- >
422
- <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full">
423
- {isComplete ? (
424
- <div className="bg-primary flex h-6 w-6 items-center justify-center rounded-full">
425
- <Check size={12} className="text-white" />
426
- </div>
427
- ) : isCurrent ? (
428
- <Loader2 size={16} className="text-primary animate-spin" />
429
- ) : (
430
- <div className="bg-muted border-border h-6 w-6 rounded-full border" />
431
- )}
432
- </div>
433
- <div className="flex items-center gap-2">
434
- <span className="text-muted-foreground">{step.icon}</span>
435
- <span
436
- className={`text-sm ${isCurrent || isComplete ? 'text-foreground' : 'text-muted-foreground'}`}
437
- >
438
- {step.label}
439
- </span>
415
+ <div className="space-y-2">
416
+ {ALL_STEPS.filter((s) => enabledSteps.has(s.key)).map((step) => {
417
+ const isComplete = completedSteps.includes(step.key)
418
+ const isCurrent = currentStep === step.key
419
+
420
+ return (
421
+ <div
422
+ key={step.key}
423
+ className={`flex items-center gap-3 rounded-lg border p-3 transition-colors ${
424
+ isComplete
425
+ ? 'border-primary/30 bg-primary/5'
426
+ : isCurrent
427
+ ? 'border-primary/50 bg-primary/10'
428
+ : 'border-border bg-muted/30'
429
+ }`}
430
+ >
431
+ <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full">
432
+ {isComplete ? (
433
+ <div className="bg-primary flex h-6 w-6 items-center justify-center rounded-full">
434
+ <Check size={12} className="text-primary-foreground" />
435
+ </div>
436
+ ) : isCurrent ? (
437
+ <Loader2 size={16} className="text-primary animate-spin" />
438
+ ) : (
439
+ <div className="bg-muted border-border h-6 w-6 rounded-full border" />
440
+ )}
441
+ </div>
442
+ <div className="flex items-center gap-2">
443
+ <span className="text-muted-foreground">{step.icon}</span>
444
+ <span
445
+ className={`text-sm ${isCurrent || isComplete ? 'text-foreground' : 'text-muted-foreground'}`}
446
+ >
447
+ {step.label}
448
+ </span>
449
+ </div>
440
450
  </div>
441
- </div>
442
- )
443
- })}
444
- </div>
445
- </div>
446
- )}
447
-
448
- {phase === 'review' && result && (
449
- <div className="space-y-5">
450
- {/* Summary */}
451
- <div className="bg-primary/5 border-primary/20 flex items-center gap-3 rounded-lg border p-4">
452
- <div className="bg-primary/10 flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
453
- <Sparkles size={18} className="text-primary" />
454
- </div>
455
- <div>
456
- <p className="text-foreground text-sm font-medium">Page generated successfully</p>
457
- <p className="text-muted-foreground mt-0.5 text-xs">
458
- {result.totalTokensUsed.toLocaleString()} tokens
459
- {' \u00B7 '}
460
- {(result.totalDurationMs / 1000).toFixed(1)}s{' \u00B7 '}
461
- {result.steps.length} step{result.steps.length !== 1 ? 's' : ''}
462
- </p>
451
+ )
452
+ })}
463
453
  </div>
464
454
  </div>
455
+ )}
456
+
457
+ {phase === 'review' && result && (
458
+ <div className="space-y-5">
459
+ {/* Summary */}
460
+ <div className="bg-primary/5 border-primary/20 flex items-center gap-3 rounded-lg border p-4">
461
+ <div className="bg-primary/10 flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
462
+ <Sparkles size={18} className="text-primary" />
463
+ </div>
464
+ <div>
465
+ <p className="text-foreground text-sm font-medium">
466
+ Page generated successfully
467
+ </p>
468
+ <p className="text-muted-foreground mt-0.5 text-xs">
469
+ {result.totalTokensUsed.toLocaleString()} tokens
470
+ {' \u00B7 '}
471
+ {(result.totalDurationMs / 1000).toFixed(1)}s{' \u00B7 '}
472
+ {result.steps.length} step{result.steps.length !== 1 ? 's' : ''}
473
+ </p>
474
+ </div>
475
+ </div>
465
476
 
466
- {/* Step details */}
467
- <div className="space-y-2">
468
- <h3 className="text-foreground text-sm font-medium">Generation steps</h3>
469
- {result.steps.map((step) => {
470
- const info = ALL_STEPS.find((s) => s.key === step.step)
471
- const isExpanded = expandedStep === step.step
472
-
473
- return (
474
- <div
475
- key={step.step}
476
- className="border-border overflow-hidden rounded-lg border"
477
- >
478
- <button
479
- onClick={() => setExpandedStep(isExpanded ? null : step.step)}
480
- className="hover:bg-muted/50 flex w-full items-center gap-3 p-3 text-left transition-colors"
477
+ {/* Step details */}
478
+ <div className="space-y-2">
479
+ <h3 className="text-foreground text-sm font-medium">Generation steps</h3>
480
+ {result.steps.map((step) => {
481
+ const info = ALL_STEPS.find((s) => s.key === step.step)
482
+ const isExpanded = expandedStep === step.step
483
+
484
+ return (
485
+ <div
486
+ key={step.step}
487
+ className="border-border overflow-hidden rounded-lg border"
481
488
  >
482
- <div className="bg-primary flex h-6 w-6 shrink-0 items-center justify-center rounded-full">
483
- <Check size={12} className="text-white" />
484
- </div>
485
- <span className="text-muted-foreground">{info?.icon}</span>
486
- <span className="text-foreground flex-1 text-sm">{info?.label}</span>
487
- <span className="text-muted-foreground text-xs">
488
- {(step.metadata.durationMs / 1000).toFixed(1)}s
489
- </span>
490
- {isExpanded ? (
491
- <ChevronUp size={14} className="text-muted-foreground" />
492
- ) : (
493
- <ChevronDown size={14} className="text-muted-foreground" />
489
+ <button
490
+ onClick={() => setExpandedStep(isExpanded ? null : step.step)}
491
+ className="hover:bg-muted/50 flex w-full items-center gap-3 p-3 text-left transition-colors"
492
+ >
493
+ <div className="bg-primary flex h-6 w-6 shrink-0 items-center justify-center rounded-full">
494
+ <Check size={12} className="text-primary-foreground" />
495
+ </div>
496
+ <span className="text-muted-foreground">{info?.icon}</span>
497
+ <span className="text-foreground flex-1 text-sm">{info?.label}</span>
498
+ <span className="text-muted-foreground text-xs">
499
+ {(step.metadata.durationMs / 1000).toFixed(1)}s
500
+ </span>
501
+ {isExpanded ? (
502
+ <ChevronUp size={14} className="text-muted-foreground" />
503
+ ) : (
504
+ <ChevronDown size={14} className="text-muted-foreground" />
505
+ )}
506
+ </button>
507
+ {isExpanded && (
508
+ <div className="border-border border-t px-3 pt-2 pb-3">
509
+ <ul className="space-y-1">
510
+ {step.metadata.changes.map((change, i) => (
511
+ <li
512
+ key={i}
513
+ className="text-muted-foreground flex items-start gap-2 text-xs"
514
+ >
515
+ <ArrowRight size={10} className="mt-0.5 shrink-0" />
516
+ {change}
517
+ </li>
518
+ ))}
519
+ </ul>
520
+ <p className="text-muted-foreground mt-2 text-xs">
521
+ Tokens: {step.metadata.tokensUsed.toLocaleString()}
522
+ </p>
523
+ </div>
494
524
  )}
495
- </button>
496
- {isExpanded && (
497
- <div className="border-border border-t px-3 pt-2 pb-3">
498
- <ul className="space-y-1">
499
- {step.metadata.changes.map((change, i) => (
500
- <li
501
- key={i}
502
- className="text-muted-foreground flex items-start gap-2 text-xs"
503
- >
504
- <ArrowRight size={10} className="mt-0.5 shrink-0" />
505
- {change}
506
- </li>
507
- ))}
508
- </ul>
509
- <p className="text-muted-foreground mt-2 text-xs">
510
- Tokens: {step.metadata.tokensUsed.toLocaleString()}
511
- </p>
512
- </div>
513
- )}
514
- </div>
515
- )
516
- })}
525
+ </div>
526
+ )
527
+ })}
528
+ </div>
529
+
530
+ <p className="text-muted-foreground text-xs">
531
+ Review the generated page in the canvas after accepting. You can always undo to
532
+ revert.
533
+ </p>
517
534
  </div>
535
+ )}
536
+ </div>
518
537
 
519
- <p className="text-muted-foreground text-xs">
520
- Review the generated page in the canvas after accepting. You can always undo to
521
- revert.
522
- </p>
523
- </div>
524
- )}
525
- </div>
538
+ {/* Footer */}
539
+ <div className="border-border flex items-center justify-end gap-3 border-t px-6 py-4">
540
+ {phase === 'prompt' && (
541
+ <>
542
+ <button
543
+ onClick={handleClose}
544
+ className="text-muted-foreground hover:text-foreground px-4 py-2 text-sm transition-colors"
545
+ >
546
+ Cancel
547
+ </button>
548
+ <button
549
+ onClick={handleGenerate}
550
+ disabled={!prompt.trim()}
551
+ className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50"
552
+ >
553
+ <Sparkles size={14} />
554
+ Generate Page
555
+ </button>
556
+ </>
557
+ )}
526
558
 
527
- {/* Footer */}
528
- <div className="border-border flex items-center justify-end gap-3 border-t px-6 py-4">
529
- {phase === 'prompt' && (
530
- <>
559
+ {phase === 'generating' && (
531
560
  <button
532
561
  onClick={handleClose}
533
562
  className="text-muted-foreground hover:text-foreground px-4 py-2 text-sm transition-colors"
534
563
  >
535
564
  Cancel
536
565
  </button>
537
- <button
538
- onClick={handleGenerate}
539
- disabled={!prompt.trim()}
540
- className="bg-primary hover:bg-primary/90 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
541
- >
542
- <Sparkles size={14} />
543
- Generate Page
544
- </button>
545
- </>
546
- )}
547
-
548
- {phase === 'generating' && (
549
- <button
550
- onClick={handleClose}
551
- className="text-muted-foreground hover:text-foreground px-4 py-2 text-sm transition-colors"
552
- >
553
- Cancel
554
- </button>
555
- )}
556
-
557
- {phase === 'review' && (
558
- <>
559
- <button
560
- onClick={() => {
561
- setPhase('prompt')
562
- setResult(null)
563
- }}
564
- className="text-muted-foreground hover:text-foreground px-4 py-2 text-sm transition-colors"
565
- >
566
- Regenerate
567
- </button>
568
- <button
569
- onClick={handleAccept}
570
- className="bg-primary hover:bg-primary/90 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white transition-colors"
571
- >
572
- <Check size={14} />
573
- Accept &amp; Apply
574
- </button>
575
- </>
576
- )}
577
- </div>
578
- </div>
579
- </div>
566
+ )}
567
+
568
+ {phase === 'review' && (
569
+ <>
570
+ <button
571
+ onClick={() => {
572
+ setPhase('prompt')
573
+ setResult(null)
574
+ }}
575
+ className="text-muted-foreground hover:text-foreground px-4 py-2 text-sm transition-colors"
576
+ >
577
+ Regenerate
578
+ </button>
579
+ <button
580
+ onClick={handleAccept}
581
+ className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
582
+ >
583
+ <Check size={14} />
584
+ Accept &amp; Apply
585
+ </button>
586
+ </>
587
+ )}
588
+ </div>
589
+ </Dialog.Content>
590
+ </Dialog.Portal>
591
+ </Dialog.Root>
580
592
  )
581
593
  }