@santi020k/lumen 3.0.1 → 4.0.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 (125) hide show
  1. package/README.md +207 -3
  2. package/dist/cli.js +49 -5
  3. package/dist/consumer-upgrade-audit.d.ts +10 -0
  4. package/dist/consumer-upgrade-audit.d.ts.map +1 -0
  5. package/dist/consumer-upgrade-audit.js +52 -0
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -0
  9. package/dist/installer.d.ts.map +1 -1
  10. package/dist/installer.js +16 -0
  11. package/dist/integration-diagnostics.d.ts.map +1 -1
  12. package/dist/integration-diagnostics.js +199 -3
  13. package/dist/registry-component-name.d.ts +2 -0
  14. package/dist/registry-component-name.d.ts.map +1 -0
  15. package/dist/registry-component-name.js +53 -0
  16. package/dist/registry-data.d.ts +339 -18
  17. package/dist/registry-data.d.ts.map +1 -1
  18. package/dist/registry-data.js +789 -3
  19. package/dist/registry.d.ts.map +1 -1
  20. package/dist/registry.js +11 -9
  21. package/dist/source-script-ranges.d.ts +3 -0
  22. package/dist/source-script-ranges.d.ts.map +1 -0
  23. package/dist/source-script-ranges.js +41 -0
  24. package/dist/v2-migration.d.ts +38 -0
  25. package/dist/v2-migration.d.ts.map +1 -1
  26. package/dist/v2-migration.js +26 -14
  27. package/dist/v4-migration.d.ts +26 -0
  28. package/dist/v4-migration.d.ts.map +1 -0
  29. package/dist/v4-migration.js +272 -0
  30. package/dist/version-migration-dependencies.d.ts +8 -0
  31. package/dist/version-migration-dependencies.d.ts.map +1 -0
  32. package/dist/version-migration-dependencies.js +10 -0
  33. package/dist/version-migration-source.d.ts +4 -0
  34. package/dist/version-migration-source.d.ts.map +1 -0
  35. package/dist/version-migration-source.js +281 -0
  36. package/dist/version-migration.d.ts +34 -0
  37. package/dist/version-migration.d.ts.map +1 -0
  38. package/dist/version-migration.js +158 -0
  39. package/native-components.json +563 -16
  40. package/package.json +26 -11
  41. package/release-manifest.json +22 -17
  42. package/styles/critical.css +634 -61
  43. package/styles/motion.css +61 -0
  44. package/styles/world-map.css +30 -0
  45. package/styles.css +1736 -455
  46. package/templates/astro/advanced-fields/src/lumen/advanced-fields.astro +35 -0
  47. package/templates/astro/ai-docs/llms.txt +3 -0
  48. package/templates/astro/analytics-dashboard/src/lumen/analytics-dashboard.astro +110 -0
  49. package/templates/astro/auth-onboarding/src/lumen/auth-onboarding.astro +107 -0
  50. package/templates/astro/command-center/src/lumen/command-center.astro +34 -0
  51. package/templates/astro/commerce-dashboard/src/lumen/commerce-dashboard.astro +108 -0
  52. package/templates/astro/content-flow-actions/src/lumen/content-flow-actions.astro +10 -0
  53. package/templates/astro/content-flow-header/src/lumen/content-flow-header.astro +14 -0
  54. package/templates/astro/content-flow-list/src/lumen/content-flow-list.astro +13 -0
  55. package/templates/astro/content-flow-settings/src/lumen/content-flow-settings.astro +17 -0
  56. package/templates/astro/data-collections/src/lumen/data-collections.astro +52 -0
  57. package/templates/astro/feature-preview/src/lumen/feature-preview.astro +32 -0
  58. package/templates/astro/guided-onboarding/src/lumen/guided-onboarding.astro +35 -0
  59. package/templates/astro/interactive-pricing/src/lumen/interactive-pricing.astro +32 -0
  60. package/templates/astro/media-workspace/src/lumen/adjustment-controls.astro +89 -0
  61. package/templates/astro/media-workspace/src/lumen/media-processing.astro +47 -0
  62. package/templates/astro/media-workspace/src/lumen/media-workspace.astro +77 -0
  63. package/templates/astro/page-header/src/lumen/page-header.astro +41 -0
  64. package/templates/astro/project-workspace/src/lumen/project-workspace.astro +103 -0
  65. package/templates/astro/rich-text-editor/src/lumen/rich-text-editor.astro +21 -0
  66. package/templates/astro/saas-admin/src/lumen/saas-admin.astro +110 -0
  67. package/templates/astro/scheduler/src/lumen/scheduler.astro +36 -0
  68. package/templates/astro/section-header/src/lumen/section-header.astro +35 -0
  69. package/templates/astro/theme-builder/src/lumen/theme-builder.astro +28 -0
  70. package/templates/elements/advanced-fields/src/lumen/advanced-fields.html +25 -0
  71. package/templates/elements/analytics-dashboard/src/lumen/analytics-dashboard.html +28 -0
  72. package/templates/elements/auth-onboarding/src/lumen/auth-onboarding.html +79 -0
  73. package/templates/elements/command-center/src/lumen/command-center.html +15 -0
  74. package/templates/elements/commerce-dashboard/src/lumen/commerce-dashboard.html +24 -0
  75. package/templates/elements/content-flow-actions/src/lumen/content-flow-actions.html +11 -0
  76. package/templates/elements/content-flow-header/src/lumen/content-flow-header.html +15 -0
  77. package/templates/elements/content-flow-list/src/lumen/content-flow-list.html +14 -0
  78. package/templates/elements/content-flow-settings/src/lumen/content-flow-settings.html +18 -0
  79. package/templates/elements/data-collections/src/lumen/data-collections.html +31 -0
  80. package/templates/elements/feature-preview/src/lumen/feature-preview.html +13 -0
  81. package/templates/elements/guided-onboarding/src/lumen/guided-onboarding.html +16 -0
  82. package/templates/elements/interactive-pricing/src/lumen/interactive-pricing.html +13 -0
  83. package/templates/elements/media-workspace/src/lumen/media-workspace.html +57 -0
  84. package/templates/elements/media-workspace/src/lumen/media-workspace.ts +163 -0
  85. package/templates/elements/page-header/src/lumen/page-header.html +20 -0
  86. package/templates/elements/project-workspace/src/lumen/project-workspace.html +17 -0
  87. package/templates/elements/rich-text-editor/src/lumen/rich-text-editor.html +17 -0
  88. package/templates/elements/saas-admin/src/lumen/saas-admin.html +18 -0
  89. package/templates/elements/scheduler/src/lumen/scheduler.html +27 -0
  90. package/templates/elements/section-header/src/lumen/section-header.html +21 -0
  91. package/templates/elements/theme-builder/src/lumen/theme-builder.html +59 -0
  92. package/templates/react/advanced-fields/src/lumen/advanced-fields.tsx +37 -0
  93. package/templates/react/analytics-dashboard/src/lumen/analytics-dashboard.tsx +143 -0
  94. package/templates/react/auth-onboarding/src/lumen/auth-onboarding.tsx +129 -0
  95. package/templates/react/command-center/src/lumen/command-center.tsx +85 -0
  96. package/templates/react/commerce-dashboard/src/lumen/commerce-dashboard.tsx +148 -0
  97. package/templates/react/content-flow-actions/src/lumen/content-flow-actions.tsx +11 -0
  98. package/templates/react/content-flow-header/src/lumen/content-flow-header.tsx +14 -0
  99. package/templates/react/content-flow-list/src/lumen/content-flow-list.tsx +19 -0
  100. package/templates/react/content-flow-settings/src/lumen/content-flow-settings.tsx +26 -0
  101. package/templates/react/data-collections/src/lumen/data-collections.tsx +60 -0
  102. package/templates/react/feature-preview/src/lumen/feature-preview.tsx +42 -0
  103. package/templates/react/guided-onboarding/src/lumen/guided-onboarding.tsx +100 -0
  104. package/templates/react/import-review/src/lumen/import-review.tsx +150 -0
  105. package/templates/react/interactive-pricing/src/lumen/interactive-pricing.tsx +60 -0
  106. package/templates/react/media-workspace/src/lumen/adjustment-controls.tsx +61 -0
  107. package/templates/react/media-workspace/src/lumen/media-processing.tsx +43 -0
  108. package/templates/react/media-workspace/src/lumen/media-workspace.tsx +100 -0
  109. package/templates/react/operational-records/src/lumen/operational-records.tsx +92 -0
  110. package/templates/react/page-header/src/lumen/page-header.tsx +41 -0
  111. package/templates/react/project-workspace/src/lumen/project-workspace.tsx +112 -0
  112. package/templates/react/record-workspace/src/lumen/record-workspace.tsx +67 -0
  113. package/templates/react/review-workflow/src/lumen/review-workflow.tsx +202 -0
  114. package/templates/react/rich-text-editor/src/lumen/rich-text-editor.tsx +33 -0
  115. package/templates/react/saas-admin/src/lumen/saas-admin.tsx +119 -0
  116. package/templates/react/scheduler/src/lumen/scheduler.tsx +45 -0
  117. package/templates/react/section-header/src/lumen/section-header.tsx +33 -0
  118. package/templates/react/styles.d.ts +1 -0
  119. package/templates/react/theme-builder/src/lumen/theme-builder.tsx +57 -0
  120. package/templates/react/tsconfig.json +14 -0
  121. package/templates/react/validated-form/src/lumen/validated-form.tsx +166 -0
  122. package/templates/shared/common/src/lumen/lumen-template.css +247 -0
  123. package/templates/shared/media-workspace/src/lumen/media-workspace.css +14 -0
  124. package/templates/shared/visual-blocks/src/lumen/product-blocks.ts +224 -0
  125. package/v4-migration.json +447 -0
@@ -0,0 +1,92 @@
1
+ import { useState } from 'react'
2
+
3
+ import { Button, DataTable, type DataTableCell, type DataTableColumn, type DataTableRow, type DataTableSort, Dialog, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Stack } from '@santi020k/lumen-react'
4
+
5
+ export interface OperationalRecordsRecipeProps {
6
+ columns: DataTableColumn[]
7
+ rows: DataTableRow[]
8
+ sort: DataTableSort | null
9
+ onSortChange: (sort: DataTableSort | null) => void
10
+ label: string
11
+ actionsLabel: string
12
+ editLabel: string
13
+ closeLabel: string
14
+ detailsLabel: string
15
+ expandLabel: string
16
+ collapseLabel: string
17
+ }
18
+
19
+ const recordText = (cell: DataTableCell): string => typeof cell === 'object' && cell !== null ?
20
+ String(cell.label ?? cell.value ?? '') :
21
+ String(cell ?? '')
22
+
23
+ /** Manual sorting delegates requests and row order to the host. Stable IDs are required. */
24
+ export const OperationalRecordsRecipe = ({
25
+ columns, rows, sort, onSortChange, label, actionsLabel, editLabel, closeLabel,
26
+ detailsLabel, expandLabel, collapseLabel
27
+ }: OperationalRecordsRecipeProps) => {
28
+ const [active, setActive] = useState<DataTableRow | undefined>(undefined)
29
+
30
+ return (
31
+ <>
32
+ <Stack direction="horizontal" wrap gap="related">
33
+ {columns.filter(column => column.sortable).map(column => (
34
+ <Button
35
+ key={column.key}
36
+ variant="outline"
37
+ aria-pressed={sort?.key === column.key}
38
+ onClick={() => {
39
+ onSortChange({ key: column.key, direction: sort?.key === column.key && sort.direction === 'ascending' ? 'descending' : 'ascending' })
40
+ }}
41
+ >
42
+ {column.header}
43
+ </Button>
44
+ ))}
45
+ </Stack>
46
+ <DataTable
47
+ role="region"
48
+ tabIndex={0}
49
+ aria-label={label}
50
+ layout="records"
51
+ rows={rows}
52
+ columns={[...columns.map(column => ({ ...column, sortable: false })), { key: 'actions',
53
+ header: actionsLabel,
54
+ render: (_cell, row) => (
55
+ <DropdownMenu>
56
+ <DropdownMenuTrigger aria-label={`${actionsLabel}: ${recordText(row.name ?? row.id)}`}>{actionsLabel}</DropdownMenuTrigger>
57
+ <DropdownMenuContent>
58
+ <DropdownMenuItem onClick={() => {
59
+ setActive(row)
60
+ }}
61
+ >
62
+ {editLabel}
63
+ </DropdownMenuItem>
64
+ </DropdownMenuContent>
65
+ </DropdownMenu>
66
+ ) }]}
67
+ sortMode="manual"
68
+ sort={sort}
69
+ onSortChange={onSortChange}
70
+ expandLabel={expandLabel}
71
+ collapseLabel={collapseLabel}
72
+ detailsLabel={detailsLabel}
73
+ renderDetails={row => <p>{recordText(row.detail)}</p>}
74
+ />
75
+ <Dialog
76
+ open={active !== undefined}
77
+ onOpenChange={open => {
78
+ if (!open) setActive(undefined)
79
+ }}
80
+ aria-label={editLabel}
81
+ >
82
+ <p>{recordText(active?.name)}</p>
83
+ <Button onClick={() => {
84
+ setActive(undefined)
85
+ }}
86
+ >
87
+ {closeLabel}
88
+ </Button>
89
+ </Dialog>
90
+ </>
91
+ )
92
+ }
@@ -0,0 +1,41 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ import { Badge, Breadcrumb, Link, Stack, Typography } from '@santi020k/lumen-react'
4
+
5
+ export interface PageHeaderRecipeProps {
6
+ actions?: ReactNode
7
+ actionsLabel?: string
8
+ breadcrumbLabel?: string
9
+ breadcrumbs?: readonly { href: string, label: string }[]
10
+ description?: string
11
+ headingId: string
12
+ status?: string
13
+ title: string
14
+ }
15
+
16
+ const emptyBreadcrumbs: NonNullable<PageHeaderRecipeProps['breadcrumbs']> = []
17
+
18
+ export const PageHeaderRecipe = ({ actions, actionsLabel = 'Page actions', breadcrumbLabel = 'Breadcrumb', breadcrumbs = emptyBreadcrumbs, description, headingId, status, title }: PageHeaderRecipeProps) => (
19
+ <header aria-labelledby={headingId}>
20
+ <Stack gap="group">
21
+ {breadcrumbs.length > 0 && (
22
+ <Breadcrumb aria-label={breadcrumbLabel}>
23
+ <ol>
24
+ {breadcrumbs.map(item => <li key={item.href}><Link href={item.href}>{item.label}</Link></li>)}
25
+ <li aria-current="page">{title}</li>
26
+ </ol>
27
+ </Breadcrumb>
28
+ )}
29
+ <Stack direction="horizontal" align="start" justify="between" gap="group" wrap>
30
+ <Stack gap="related" style={{ flex: '1 1 20rem', minWidth: 0, overflowWrap: 'anywhere' }}>
31
+ <Typography>
32
+ <h1 id={headingId}>{title}</h1>
33
+ {description && <p>{description}</p>}
34
+ </Typography>
35
+ {status && <Badge variant="outline" style={{ alignSelf: 'flex-start' }}>{status}</Badge>}
36
+ </Stack>
37
+ {actions && <Stack direction="horizontal" align="center" gap="related" wrap role="group" aria-label={actionsLabel}>{actions}</Stack>}
38
+ </Stack>
39
+ </Stack>
40
+ </header>
41
+ )
@@ -0,0 +1,112 @@
1
+ import './lumen-template.css'
2
+
3
+ import {
4
+ Badge,
5
+ Button,
6
+ Card,
7
+ Progress,
8
+ Sidebar,
9
+ Stat,
10
+ Table,
11
+ Timeline,
12
+ TimelineItem
13
+ } from '@santi020k/lumen-react'
14
+
15
+ export const ProjectWorkspaceTemplate = () => (
16
+ <div className="lumen-template lumen-template__shell">
17
+ <Sidebar className="lumen-template__sidebar">
18
+ <strong>Relay Projects</strong>
19
+ <nav aria-label="Projects">
20
+ <a aria-current="page" href="#overview">
21
+ Overview
22
+ </a>
23
+ <a href="#projects">Projects</a>
24
+ <a href="#activity">Activity</a>
25
+ </nav>
26
+ </Sidebar>
27
+ <main className="lumen-template__main" id="overview">
28
+ <header className="lumen-template__header">
29
+ <div>
30
+ <Badge variant="outline">Team delivery</Badge>
31
+ <h1>Team workspace</h1>
32
+ <p>Priorities, delivery health, and next decisions.</p>
33
+ </div>
34
+ <Button>New project</Button>
35
+ </header>
36
+ <section
37
+ className="lumen-template__metrics"
38
+ aria-label="Delivery metrics"
39
+ >
40
+ <Card className="lumen-template__metric">
41
+ <Stat label="Open tasks" value="42" />
42
+ <Badge variant="warning">6 due soon</Badge>
43
+ </Card>
44
+ <Card className="lumen-template__metric">
45
+ <Stat label="Milestones reached" value="18" />
46
+ <Badge variant="success">+4</Badge>
47
+ </Card>
48
+ <Card className="lumen-template__metric">
49
+ <Stat label="At-risk projects" value="2" />
50
+ <Badge variant="destructive">Needs help</Badge>
51
+ </Card>
52
+ <Card className="lumen-template__metric">
53
+ <Stat label="Delivery velocity" value="46 pts" />
54
+ <Badge variant="success">+6.4%</Badge>
55
+ </Card>
56
+ </section>
57
+ <section className="lumen-template__grid">
58
+ <Card className="lumen-template__panel">
59
+ <h2>Active workstreams</h2>
60
+ <p>Mobile checkout · 72%</p>
61
+ <Progress aria-label="Mobile checkout is 72% complete" value={72} />
62
+ <p>Billing migration · 54%</p>
63
+ <Progress aria-label="Billing migration is 54% complete" value={54} />
64
+ </Card>
65
+ <Card className="lumen-template__panel" id="activity">
66
+ <h2>Recent activity</h2>
67
+ <Timeline>
68
+ <TimelineItem>Amina completed checkout validation</TimelineItem>
69
+ <TimelineItem>Jon reviewed billing retries</TimelineItem>
70
+ <TimelineItem>Nora moved tokens to review</TimelineItem>
71
+ </Timeline>
72
+ </Card>
73
+ </section>
74
+ <Card className="lumen-template__panel" id="projects">
75
+ <div className="lumen-template__section-header">
76
+ <h2>Priority projects</h2>
77
+ <Button variant="ghost">View all</Button>
78
+ </div>
79
+ <Table className="lumen-template__table">
80
+ <table>
81
+ <thead>
82
+ <tr>
83
+ <th>Project</th>
84
+ <th>Owner</th>
85
+ <th>Status</th>
86
+ <th>Progress</th>
87
+ </tr>
88
+ </thead>
89
+ <tbody>
90
+ <tr>
91
+ <td>Mobile checkout</td>
92
+ <td>Amina</td>
93
+ <td>
94
+ <Badge variant="success">Active</Badge>
95
+ </td>
96
+ <td>72%</td>
97
+ </tr>
98
+ <tr>
99
+ <td>Billing migration</td>
100
+ <td>Jon</td>
101
+ <td>
102
+ <Badge variant="destructive">At risk</Badge>
103
+ </td>
104
+ <td>54%</td>
105
+ </tr>
106
+ </tbody>
107
+ </table>
108
+ </Table>
109
+ </Card>
110
+ </main>
111
+ </div>
112
+ )
@@ -0,0 +1,67 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ import { Badge, Card, Descriptions, type DescriptionsProps, FormattedDate, Grid, Stack, Timeline, TimelineItem } from '@santi020k/lumen-react'
4
+
5
+ export interface RecordWorkspaceEvent {
6
+ id: string
7
+ dateTime: string
8
+ dateLabel: string
9
+ title: string
10
+ description: ReactNode
11
+ stateLabel?: string
12
+ /** Authored link or action connecting a correction to its original event. */
13
+ related?: ReactNode
14
+ }
15
+
16
+ export interface RecordWorkspaceRecipeProps {
17
+ title: string
18
+ headingId: string
19
+ status: ReactNode
20
+ actions?: ReactNode
21
+ facts: NonNullable<DescriptionsProps['items']>
22
+ children?: ReactNode
23
+ events: readonly RecordWorkspaceEvent[]
24
+ historyLabel: string
25
+ emptyHistoryLabel: string
26
+ /** Supply ErrorState for failures or loading feedback instead of claiming empty history. */
27
+ historyFeedback?: ReactNode
28
+ }
29
+
30
+ /** Generic record composition. Balances, event order and correction semantics remain host-owned. */
31
+ export const RecordWorkspaceRecipe = ({
32
+ title, headingId, status, actions, facts, children, events, historyLabel, emptyHistoryLabel, historyFeedback
33
+ }: RecordWorkspaceRecipeProps) => (
34
+ <section aria-labelledby={headingId}>
35
+ <Stack gap="group">
36
+ <Stack direction="horizontal" wrap gap="related">
37
+ <h2 id={headingId}>{title}</h2>
38
+ {status}
39
+ {actions}
40
+ </Stack>
41
+ <Grid minItemWidth="18rem" gap="group">
42
+ <Card>
43
+ <Descriptions items={facts} />
44
+ {children}
45
+ </Card>
46
+ <Card>
47
+ <h3>{historyLabel}</h3>
48
+ {historyFeedback ?? (events.length === 0 ?
49
+ <p>{emptyHistoryLabel}</p> :
50
+ (
51
+ <Timeline aria-label={historyLabel}>
52
+ {events.map(event => (
53
+ <TimelineItem key={event.id}>
54
+ <FormattedDate dateTime={event.dateTime}>{event.dateLabel}</FormattedDate>
55
+ <h4>{event.title}</h4>
56
+ {event.stateLabel && <Badge variant="outline">{event.stateLabel}</Badge>}
57
+ <div>{event.description}</div>
58
+ {event.related}
59
+ </TimelineItem>
60
+ ))}
61
+ </Timeline>
62
+ ))}
63
+ </Card>
64
+ </Grid>
65
+ </Stack>
66
+ </section>
67
+ )
@@ -0,0 +1,202 @@
1
+ import type { ReactNode } from 'react'
2
+ import { useLayoutEffect, useRef, useState } from 'react'
3
+
4
+ import { type LumenChangeSummaryItem } from '@santi020k/lumen-core'
5
+ import { Alert, Button, ChangeSummary, ErrorSummary, Form, type LumenFormIssue, type LumenFormWorkflowOptions, type LumenReviewResult, Stack, useLumenFormWorkflow, useLumenReviewWorkflow } from '@santi020k/lumen-react'
6
+
7
+ export interface ReviewProposal<T> {
8
+ value: T
9
+ changes: readonly LumenChangeSummaryItem[]
10
+ }
11
+
12
+ export interface ReviewWorkflowRecipeProps<T> {
13
+ label: string
14
+ /** Also changes when an external/programmatic draft or server revision changes. */
15
+ revision: string
16
+ children: (state: { pending: boolean, errors: readonly LumenFormIssue[] }) => ReactNode
17
+ prepare: (values: FormData) => ReviewProposal<T> | undefined
18
+ submit: (proposal: T) => Promise<LumenReviewResult>
19
+ /** Check the command outcome at the server; never submit again from this callback. */
20
+ reconcile?: () => Promise<{ status: 'success' } | { status: 'failure', message: string }>
21
+ validation?: LumenFormWorkflowOptions
22
+ labels: {
23
+ errors: string
24
+ review: string
25
+ confirm: string
26
+ edit: string
27
+ success: string
28
+ uncertain: string
29
+ checkResult: string
30
+ before: string
31
+ after: string
32
+ changed: string
33
+ unchanged: string
34
+ }
35
+ }
36
+
37
+ type ReviewController<T> = ReturnType<typeof useLumenReviewWorkflow<ReviewProposal<T>>>
38
+
39
+ const ResultCheck = ({ reconcile, onResult, label }: {
40
+ reconcile: NonNullable<ReviewWorkflowRecipeProps<unknown>['reconcile']>
41
+ onResult: (result: Awaited<ReturnType<NonNullable<ReviewWorkflowRecipeProps<unknown>['reconcile']>>>) => void
42
+ label: string
43
+ }) => {
44
+ const [checking, setChecking] = useState(false)
45
+ const checkingRef = useRef(false)
46
+
47
+ const check = async () => {
48
+ if (checkingRef.current) return
49
+
50
+ checkingRef.current = true
51
+
52
+ setChecking(true)
53
+
54
+ try {
55
+ onResult(await reconcile())
56
+ } catch {
57
+ // Keep the visible unknown-outcome guidance and permit another status check.
58
+ } finally {
59
+ checkingRef.current = false
60
+
61
+ setChecking(false)
62
+ }
63
+ }
64
+
65
+ return (
66
+ <Button
67
+ type="button"
68
+ loading={checking}
69
+ disabled={checking}
70
+ onClick={() => {
71
+ void check()
72
+ }}
73
+ >
74
+ {label}
75
+ </Button>
76
+ )
77
+ }
78
+
79
+ const ReviewActions = <T,>({ workflow, labels, reconcile, edit }: {
80
+ workflow: ReviewController<T>
81
+ labels: ReviewWorkflowRecipeProps<T>['labels']
82
+ reconcile: ReviewWorkflowRecipeProps<T>['reconcile']
83
+ edit: () => void
84
+ }) => {
85
+ if (workflow.status === 'editing') return <Button type="submit">{labels.review}</Button>
86
+
87
+ if (workflow.status === 'success') return null
88
+
89
+ if (workflow.status === 'uncertain') return reconcile ?
90
+ <ResultCheck reconcile={reconcile} onResult={workflow.reconcile} label={labels.checkResult} /> :
91
+ null
92
+
93
+ return (
94
+ <Stack direction="horizontal" wrap gap="related">
95
+ <Button
96
+ type="button"
97
+ loading={workflow.status === 'pending'}
98
+ disabled={workflow.status === 'pending'}
99
+ onClick={() => {
100
+ void workflow.confirm()
101
+ }}
102
+ >
103
+ {labels.confirm}
104
+ </Button>
105
+ <Button type="button" variant="outline" disabled={workflow.status === 'pending'} onClick={edit}>{labels.edit}</Button>
106
+ </Stack>
107
+ )
108
+ }
109
+
110
+ /** Supply an explicit allowlist in prepare; never include credentials or uploaded documents in changes. */
111
+ export const ReviewWorkflowRecipe = <T,>({
112
+ label, revision, children, prepare, submit, reconcile, validation, labels
113
+ }: ReviewWorkflowRecipeProps<T>) => {
114
+ const form = useLumenFormWorkflow(validation)
115
+ const [editRevision, setEditRevision] = useState(0)
116
+ const reviewRef = useRef<HTMLElement>(null)
117
+ const feedbackRef = useRef<HTMLDivElement>(null)
118
+
119
+ const workflow = useLumenReviewWorkflow<ReviewProposal<T>>({
120
+ revision: `${revision}:${editRevision}`,
121
+ submit: proposal => submit(proposal.value),
122
+ uncertainMessage: labels.uncertain
123
+ })
124
+
125
+ const pending = workflow.status === 'pending' || workflow.status === 'uncertain'
126
+ const review = workflow.proposal
127
+
128
+ useLayoutEffect(() => {
129
+ if (workflow.status === 'review') reviewRef.current?.focus()
130
+ else if (['failure', 'uncertain', 'success'].includes(workflow.status)) feedbackRef.current?.focus()
131
+ }, [workflow.status])
132
+
133
+ const invalidate = () => {
134
+ if (workflow.edit()) setEditRevision(current => current + 1)
135
+ }
136
+
137
+ return (
138
+ <Form
139
+ {...form.formProps}
140
+ aria-label={label}
141
+ aria-busy={workflow.status === 'pending'}
142
+ onInput={event => {
143
+ form.formProps.onInput(event)
144
+
145
+ invalidate()
146
+ }}
147
+ onChange={event => {
148
+ form.formProps.onChange(event)
149
+
150
+ invalidate()
151
+ }}
152
+ onReset={event => {
153
+ if (pending) event.preventDefault()
154
+ else invalidate()
155
+
156
+ form.formProps.onReset(event)
157
+ }}
158
+ onSubmit={event => {
159
+ event.preventDefault()
160
+
161
+ if (pending || !form.validate(event.currentTarget)) return
162
+
163
+ const proposal = prepare(new FormData(event.currentTarget))
164
+
165
+ if (proposal) workflow.review(proposal)
166
+ }}
167
+ >
168
+ <ErrorSummary heading={labels.errors} errors={{ fields: [...form.errors], form: [] }} />
169
+ <fieldset disabled={pending} style={{ border: 0, margin: 0, padding: 0, minWidth: 0 }}>
170
+ <legend className="sr-only">{label}</legend>
171
+ {children({ pending, errors: form.errors })}
172
+ </fieldset>
173
+ {review && workflow.status !== 'success' && (
174
+ <ChangeSummary
175
+ ref={reviewRef}
176
+ tabIndex={-1}
177
+ label={labels.review}
178
+ items={review.changes}
179
+ beforeLabel={labels.before}
180
+ afterLabel={labels.after}
181
+ changedLabel={labels.changed}
182
+ unchangedLabel={labels.unchanged}
183
+ />
184
+ )}
185
+ <div ref={feedbackRef} tabIndex={-1}>
186
+ {(workflow.status === 'failure' || workflow.status === 'uncertain') &&
187
+ <Alert role="alert" variant="warning">{workflow.message}</Alert>}
188
+ {workflow.status === 'success' && <Alert role="status" variant="success">{labels.success}</Alert>}
189
+ </div>
190
+ <ReviewActions
191
+ workflow={workflow}
192
+ labels={labels}
193
+ reconcile={reconcile}
194
+ edit={() => {
195
+ invalidate()
196
+
197
+ form.formRef.current?.querySelector<HTMLElement>('input:not([type="hidden"]), select, textarea')?.focus()
198
+ }}
199
+ />
200
+ </Form>
201
+ )
202
+ }
@@ -0,0 +1,33 @@
1
+ import {
2
+ Button,
3
+ ButtonGroup,
4
+ RichTextEditor,
5
+ Textarea,
6
+ ToggleGroup,
7
+ useRichTextEditor
8
+ } from '@santi020k/lumen-react'
9
+
10
+ export const RichTextEditorRecipe = () => {
11
+ const editor = useRichTextEditor()
12
+
13
+ return (
14
+ <RichTextEditor className="lumen-recipe lumen-recipe--rich-text-editor">
15
+ <div role="toolbar" aria-label="Editor toolbar">
16
+ <ButtonGroup>
17
+ <Button {...editor.getCommandProps('bold')}>Bold</Button>
18
+ <Button {...editor.getCommandProps('italic')}>Italic</Button>
19
+ </ButtonGroup>
20
+ <ToggleGroup>
21
+ <button
22
+ {...editor.getCommandProps('insertUnorderedList')}
23
+ type="button"
24
+ >
25
+ List
26
+ </button>
27
+ </ToggleGroup>
28
+ </div>
29
+ <div {...editor.getEditableProps()}>Draft release notes...</div>
30
+ <Textarea name="fallback" placeholder="Plain text fallback" />
31
+ </RichTextEditor>
32
+ )
33
+ }
@@ -0,0 +1,119 @@
1
+ import './lumen-template.css'
2
+
3
+ import {
4
+ Alert,
5
+ Badge,
6
+ Button,
7
+ Card,
8
+ Progress,
9
+ SearchField,
10
+ Sidebar,
11
+ Stat,
12
+ Table
13
+ } from '@santi020k/lumen-react'
14
+
15
+ export const SaasAdminTemplate = () => (
16
+ <div className="lumen-template lumen-template__shell">
17
+ <Sidebar className="lumen-template__sidebar">
18
+ <strong>Orbit Admin</strong>
19
+ <nav aria-label="Administration">
20
+ <a aria-current="page" href="#overview">
21
+ Overview
22
+ </a>
23
+ <a href="#members">Members</a>
24
+ <a href="#billing">Billing</a>
25
+ </nav>
26
+ </Sidebar>
27
+ <main className="lumen-template__main" id="overview">
28
+ <header className="lumen-template__header">
29
+ <div>
30
+ <Badge variant="outline">Workspace administration</Badge>
31
+ <h1>Good morning, Maya</h1>
32
+ <p>Access, billing, and operational health.</p>
33
+ </div>
34
+ <Button>Invite members</Button>
35
+ </header>
36
+ <Alert>
37
+ <strong>Quarterly access review is ready.</strong>
38
+ {' '}
39
+ Confirm eight
40
+ elevated roles before July 12.
41
+ </Alert>
42
+ <section
43
+ className="lumen-template__metrics"
44
+ aria-label="Workspace metrics"
45
+ >
46
+ <Card className="lumen-template__metric">
47
+ <Stat label="Workspace members" value="248" />
48
+ <Badge variant="success">+24</Badge>
49
+ </Card>
50
+ <Card className="lumen-template__metric">
51
+ <Stat label="Seat activation" value="91%" />
52
+ <Badge variant="success">Healthy</Badge>
53
+ </Card>
54
+ <Card className="lumen-template__metric">
55
+ <Stat label="Access reviews" value="8" />
56
+ <Badge variant="warning">3 pending</Badge>
57
+ </Card>
58
+ <Card className="lumen-template__metric">
59
+ <Stat label="Availability" value="99.99%" />
60
+ <Badge variant="success">Normal</Badge>
61
+ </Card>
62
+ </section>
63
+ <section className="lumen-template__grid" id="billing">
64
+ <Card className="lumen-template__panel">
65
+ <h2>Plan utilization</h2>
66
+ <Stat label="Seats used" value="248 / 300" />
67
+ <Progress aria-label="248 of 300 seats used" max={300} value={248} />
68
+ </Card>
69
+ <Card className="lumen-template__panel">
70
+ <h2>Security posture</h2>
71
+ <ul className="lumen-template__list">
72
+ <li>Single sign-on enforced</li>
73
+ <li>Domain verification complete</li>
74
+ <li>3 recovery codes need rotation</li>
75
+ </ul>
76
+ </Card>
77
+ </section>
78
+ <Card className="lumen-template__panel" id="members">
79
+ <div className="lumen-template__section-header">
80
+ <h2>Recently active members</h2>
81
+ <SearchField
82
+ aria-label="Search members"
83
+ placeholder="Search members"
84
+ />
85
+ </div>
86
+ <Table className="lumen-template__table">
87
+ <table>
88
+ <thead>
89
+ <tr>
90
+ <th>Member</th>
91
+ <th>Team</th>
92
+ <th>Status</th>
93
+ <th>Role</th>
94
+ </tr>
95
+ </thead>
96
+ <tbody>
97
+ <tr>
98
+ <td>Amina Bello</td>
99
+ <td>Design</td>
100
+ <td>
101
+ <Badge variant="success">Active</Badge>
102
+ </td>
103
+ <td>Admin</td>
104
+ </tr>
105
+ <tr>
106
+ <td>Theo Martin</td>
107
+ <td>Success</td>
108
+ <td>
109
+ <Badge variant="warning">Pending</Badge>
110
+ </td>
111
+ <td>Member</td>
112
+ </tr>
113
+ </tbody>
114
+ </table>
115
+ </Table>
116
+ </Card>
117
+ </main>
118
+ </div>
119
+ )