@santi020k/lumen 3.0.0 → 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.
- package/README.md +207 -3
- package/dist/cli.js +49 -5
- package/dist/consumer-upgrade-audit.d.ts +10 -0
- package/dist/consumer-upgrade-audit.d.ts.map +1 -0
- package/dist/consumer-upgrade-audit.js +52 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +16 -0
- package/dist/integration-diagnostics.d.ts.map +1 -1
- package/dist/integration-diagnostics.js +199 -3
- package/dist/registry-component-name.d.ts +2 -0
- package/dist/registry-component-name.d.ts.map +1 -0
- package/dist/registry-component-name.js +53 -0
- package/dist/registry-data.d.ts +339 -18
- package/dist/registry-data.d.ts.map +1 -1
- package/dist/registry-data.js +789 -3
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +11 -9
- package/dist/source-script-ranges.d.ts +3 -0
- package/dist/source-script-ranges.d.ts.map +1 -0
- package/dist/source-script-ranges.js +41 -0
- package/dist/v2-migration.d.ts +38 -0
- package/dist/v2-migration.d.ts.map +1 -1
- package/dist/v2-migration.js +26 -14
- package/dist/v4-migration.d.ts +26 -0
- package/dist/v4-migration.d.ts.map +1 -0
- package/dist/v4-migration.js +272 -0
- package/dist/version-migration-dependencies.d.ts +8 -0
- package/dist/version-migration-dependencies.d.ts.map +1 -0
- package/dist/version-migration-dependencies.js +10 -0
- package/dist/version-migration-source.d.ts +4 -0
- package/dist/version-migration-source.d.ts.map +1 -0
- package/dist/version-migration-source.js +281 -0
- package/dist/version-migration.d.ts +34 -0
- package/dist/version-migration.d.ts.map +1 -0
- package/dist/version-migration.js +158 -0
- package/native-components.json +563 -16
- package/package.json +26 -11
- package/release-manifest.json +22 -17
- package/styles/critical.css +634 -61
- package/styles/motion.css +61 -0
- package/styles/world-map.css +30 -0
- package/styles.css +1750 -432
- package/templates/astro/advanced-fields/src/lumen/advanced-fields.astro +35 -0
- package/templates/astro/ai-docs/llms.txt +3 -0
- package/templates/astro/analytics-dashboard/src/lumen/analytics-dashboard.astro +110 -0
- package/templates/astro/auth-onboarding/src/lumen/auth-onboarding.astro +107 -0
- package/templates/astro/command-center/src/lumen/command-center.astro +34 -0
- package/templates/astro/commerce-dashboard/src/lumen/commerce-dashboard.astro +108 -0
- package/templates/astro/content-flow-actions/src/lumen/content-flow-actions.astro +10 -0
- package/templates/astro/content-flow-header/src/lumen/content-flow-header.astro +14 -0
- package/templates/astro/content-flow-list/src/lumen/content-flow-list.astro +13 -0
- package/templates/astro/content-flow-settings/src/lumen/content-flow-settings.astro +17 -0
- package/templates/astro/data-collections/src/lumen/data-collections.astro +52 -0
- package/templates/astro/feature-preview/src/lumen/feature-preview.astro +32 -0
- package/templates/astro/guided-onboarding/src/lumen/guided-onboarding.astro +35 -0
- package/templates/astro/interactive-pricing/src/lumen/interactive-pricing.astro +32 -0
- package/templates/astro/media-workspace/src/lumen/adjustment-controls.astro +89 -0
- package/templates/astro/media-workspace/src/lumen/media-processing.astro +47 -0
- package/templates/astro/media-workspace/src/lumen/media-workspace.astro +77 -0
- package/templates/astro/page-header/src/lumen/page-header.astro +41 -0
- package/templates/astro/project-workspace/src/lumen/project-workspace.astro +103 -0
- package/templates/astro/rich-text-editor/src/lumen/rich-text-editor.astro +21 -0
- package/templates/astro/saas-admin/src/lumen/saas-admin.astro +110 -0
- package/templates/astro/scheduler/src/lumen/scheduler.astro +36 -0
- package/templates/astro/section-header/src/lumen/section-header.astro +35 -0
- package/templates/astro/theme-builder/src/lumen/theme-builder.astro +28 -0
- package/templates/elements/advanced-fields/src/lumen/advanced-fields.html +25 -0
- package/templates/elements/analytics-dashboard/src/lumen/analytics-dashboard.html +28 -0
- package/templates/elements/auth-onboarding/src/lumen/auth-onboarding.html +79 -0
- package/templates/elements/command-center/src/lumen/command-center.html +15 -0
- package/templates/elements/commerce-dashboard/src/lumen/commerce-dashboard.html +24 -0
- package/templates/elements/content-flow-actions/src/lumen/content-flow-actions.html +11 -0
- package/templates/elements/content-flow-header/src/lumen/content-flow-header.html +15 -0
- package/templates/elements/content-flow-list/src/lumen/content-flow-list.html +14 -0
- package/templates/elements/content-flow-settings/src/lumen/content-flow-settings.html +18 -0
- package/templates/elements/data-collections/src/lumen/data-collections.html +31 -0
- package/templates/elements/feature-preview/src/lumen/feature-preview.html +13 -0
- package/templates/elements/guided-onboarding/src/lumen/guided-onboarding.html +16 -0
- package/templates/elements/interactive-pricing/src/lumen/interactive-pricing.html +13 -0
- package/templates/elements/media-workspace/src/lumen/media-workspace.html +57 -0
- package/templates/elements/media-workspace/src/lumen/media-workspace.ts +163 -0
- package/templates/elements/page-header/src/lumen/page-header.html +20 -0
- package/templates/elements/project-workspace/src/lumen/project-workspace.html +17 -0
- package/templates/elements/rich-text-editor/src/lumen/rich-text-editor.html +17 -0
- package/templates/elements/saas-admin/src/lumen/saas-admin.html +18 -0
- package/templates/elements/scheduler/src/lumen/scheduler.html +27 -0
- package/templates/elements/section-header/src/lumen/section-header.html +21 -0
- package/templates/elements/theme-builder/src/lumen/theme-builder.html +59 -0
- package/templates/react/advanced-fields/src/lumen/advanced-fields.tsx +37 -0
- package/templates/react/analytics-dashboard/src/lumen/analytics-dashboard.tsx +143 -0
- package/templates/react/auth-onboarding/src/lumen/auth-onboarding.tsx +129 -0
- package/templates/react/command-center/src/lumen/command-center.tsx +85 -0
- package/templates/react/commerce-dashboard/src/lumen/commerce-dashboard.tsx +148 -0
- package/templates/react/content-flow-actions/src/lumen/content-flow-actions.tsx +11 -0
- package/templates/react/content-flow-header/src/lumen/content-flow-header.tsx +14 -0
- package/templates/react/content-flow-list/src/lumen/content-flow-list.tsx +19 -0
- package/templates/react/content-flow-settings/src/lumen/content-flow-settings.tsx +26 -0
- package/templates/react/data-collections/src/lumen/data-collections.tsx +60 -0
- package/templates/react/feature-preview/src/lumen/feature-preview.tsx +42 -0
- package/templates/react/guided-onboarding/src/lumen/guided-onboarding.tsx +100 -0
- package/templates/react/import-review/src/lumen/import-review.tsx +150 -0
- package/templates/react/interactive-pricing/src/lumen/interactive-pricing.tsx +60 -0
- package/templates/react/media-workspace/src/lumen/adjustment-controls.tsx +61 -0
- package/templates/react/media-workspace/src/lumen/media-processing.tsx +43 -0
- package/templates/react/media-workspace/src/lumen/media-workspace.tsx +100 -0
- package/templates/react/operational-records/src/lumen/operational-records.tsx +92 -0
- package/templates/react/page-header/src/lumen/page-header.tsx +41 -0
- package/templates/react/project-workspace/src/lumen/project-workspace.tsx +112 -0
- package/templates/react/record-workspace/src/lumen/record-workspace.tsx +67 -0
- package/templates/react/review-workflow/src/lumen/review-workflow.tsx +202 -0
- package/templates/react/rich-text-editor/src/lumen/rich-text-editor.tsx +33 -0
- package/templates/react/saas-admin/src/lumen/saas-admin.tsx +119 -0
- package/templates/react/scheduler/src/lumen/scheduler.tsx +45 -0
- package/templates/react/section-header/src/lumen/section-header.tsx +33 -0
- package/templates/react/styles.d.ts +1 -0
- package/templates/react/theme-builder/src/lumen/theme-builder.tsx +57 -0
- package/templates/react/tsconfig.json +14 -0
- package/templates/react/validated-form/src/lumen/validated-form.tsx +166 -0
- package/templates/shared/common/src/lumen/lumen-template.css +247 -0
- package/templates/shared/media-workspace/src/lumen/media-workspace.css +14 -0
- package/templates/shared/visual-blocks/src/lumen/product-blocks.ts +224 -0
- 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
|
+
)
|