@santi020k/lumen 3.0.1 → 4.1.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 +211 -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 +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- 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 +564 -17
- package/package.json +26 -11
- package/release-manifest.json +22 -17
- package/styles/critical.css +696 -66
- package/styles/motion.css +61 -0
- package/styles/world-map.css +30 -0
- package/styles.css +1756 -409
- 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 +448 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Field, Input, Label, Stack } from '@santi020k/lumen-react'
|
|
2
|
+
|
|
3
|
+
export const ContentFlowSettingsRecipe = () => (
|
|
4
|
+
<Card as="section" aria-labelledby="notification-heading">
|
|
5
|
+
<CardHeader>
|
|
6
|
+
<CardTitle as="h2" id="notification-heading">Notifications</CardTitle>
|
|
7
|
+
<CardDescription>Choose where your team receives updates.</CardDescription>
|
|
8
|
+
</CardHeader>
|
|
9
|
+
<CardContent>
|
|
10
|
+
<Stack gap="group">
|
|
11
|
+
<Field>
|
|
12
|
+
<Label htmlFor="team-name">Team name</Label>
|
|
13
|
+
<Input id="team-name" name="team" autoComplete="organization" />
|
|
14
|
+
</Field>
|
|
15
|
+
<Field>
|
|
16
|
+
<Label htmlFor="team-email">Team email</Label>
|
|
17
|
+
<Input id="team-email" name="email" type="email" autoComplete="email" />
|
|
18
|
+
</Field>
|
|
19
|
+
</Stack>
|
|
20
|
+
</CardContent>
|
|
21
|
+
<CardFooter>
|
|
22
|
+
<Button>Save preferences</Button>
|
|
23
|
+
<Button variant="secondary">Discard changes</Button>
|
|
24
|
+
</CardFooter>
|
|
25
|
+
</Card>
|
|
26
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Command,
|
|
5
|
+
DataTable,
|
|
6
|
+
Pagination,
|
|
7
|
+
Tree,
|
|
8
|
+
TreeGrid,
|
|
9
|
+
VirtualList
|
|
10
|
+
} from '@santi020k/lumen-react'
|
|
11
|
+
|
|
12
|
+
const virtualListStyle = { '--ui-list-height': '12rem' } as CSSProperties
|
|
13
|
+
|
|
14
|
+
export const DataCollectionsRecipe = () => (
|
|
15
|
+
<section className="lumen-recipe lumen-recipe--data-collections">
|
|
16
|
+
<Command>
|
|
17
|
+
<input type="search" placeholder="Filter records" />
|
|
18
|
+
<button data-ui-command-item type="button">
|
|
19
|
+
Open docs
|
|
20
|
+
</button>
|
|
21
|
+
</Command>
|
|
22
|
+
<DataTable>
|
|
23
|
+
<table>
|
|
24
|
+
<thead>
|
|
25
|
+
<tr>
|
|
26
|
+
<th>Name</th>
|
|
27
|
+
<th>Status</th>
|
|
28
|
+
</tr>
|
|
29
|
+
</thead>
|
|
30
|
+
<tbody>
|
|
31
|
+
<tr>
|
|
32
|
+
<td>Docs</td>
|
|
33
|
+
<td>Ready</td>
|
|
34
|
+
</tr>
|
|
35
|
+
</tbody>
|
|
36
|
+
</table>
|
|
37
|
+
</DataTable>
|
|
38
|
+
<Tree aria-label="Files">
|
|
39
|
+
<div role="treeitem" aria-expanded="true">
|
|
40
|
+
src
|
|
41
|
+
</div>
|
|
42
|
+
<div role="treeitem">index.ts</div>
|
|
43
|
+
</Tree>
|
|
44
|
+
<TreeGrid aria-label="Project status">
|
|
45
|
+
<div role="row">
|
|
46
|
+
<span role="gridcell">Docs</span>
|
|
47
|
+
<span role="gridcell">Ready</span>
|
|
48
|
+
</div>
|
|
49
|
+
</TreeGrid>
|
|
50
|
+
<VirtualList data-ui-item-size="44" style={virtualListStyle}>
|
|
51
|
+
<div>Row 1</div>
|
|
52
|
+
<div>Row 2</div>
|
|
53
|
+
<div>Row 3</div>
|
|
54
|
+
</VirtualList>
|
|
55
|
+
<Pagination>
|
|
56
|
+
<a href="?page=1">Previous</a>
|
|
57
|
+
<a href="?page=2">Next</a>
|
|
58
|
+
</Pagination>
|
|
59
|
+
</section>
|
|
60
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { Button, Card, MotionGroup, Stack, Typography, VisualEffect } from '@santi020k/lumen-react'
|
|
6
|
+
|
|
7
|
+
const features = ['Plan', 'Review', 'Ship'] as const
|
|
8
|
+
const descriptions = { Plan: 'Bring the next three tasks into focus.', Review: 'Compare a proposal before deciding.', Ship: 'Check the release evidence before publishing.' }
|
|
9
|
+
|
|
10
|
+
export const FeaturePreviewRecipe = () => {
|
|
11
|
+
const [feature, setFeature] = useState<typeof features[number]>('Plan')
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Stack gap="group">
|
|
15
|
+
<Typography><h2>From idea to delivery</h2></Typography>
|
|
16
|
+
<Stack direction="horizontal" gap="related" role="group" aria-label="Preview a workflow stage">
|
|
17
|
+
{features.map(item => (
|
|
18
|
+
<Button
|
|
19
|
+
key={item}
|
|
20
|
+
aria-pressed={feature === item}
|
|
21
|
+
onClick={() => {
|
|
22
|
+
setFeature(item)
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
{item}
|
|
26
|
+
</Button>
|
|
27
|
+
))}
|
|
28
|
+
</Stack>
|
|
29
|
+
<MotionGroup>
|
|
30
|
+
<Card key={feature} data-ui-motion-key={feature}>
|
|
31
|
+
<VisualEffect variant="mesh">
|
|
32
|
+
<Typography>
|
|
33
|
+
<h3>{feature}</h3>
|
|
34
|
+
<p>{descriptions[feature]}</p>
|
|
35
|
+
</Typography>
|
|
36
|
+
</VisualEffect>
|
|
37
|
+
</Card>
|
|
38
|
+
</MotionGroup>
|
|
39
|
+
<span role="status">{`Previewing ${feature.toLowerCase()}.`}</span>
|
|
40
|
+
</Stack>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useId, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { Button, Card, Form, Input, Label, Stack, Stepper, Typography } from '@santi020k/lumen-react'
|
|
6
|
+
|
|
7
|
+
export const GuidedOnboardingRecipe = ({ onComplete }: { onComplete?: (workspace: string) => void }) => {
|
|
8
|
+
const id = useId()
|
|
9
|
+
const [step, setStep] = useState(0)
|
|
10
|
+
const [workspace, setWorkspace] = useState('')
|
|
11
|
+
const stepHeadingRef = useRef<HTMLHeadingElement>(null)
|
|
12
|
+
const mountedRef = useRef(false)
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (mountedRef.current) stepHeadingRef.current?.focus()
|
|
16
|
+
|
|
17
|
+
mountedRef.current = true
|
|
18
|
+
}, [step])
|
|
19
|
+
|
|
20
|
+
const steps = [{ title: 'Workspace' }, { title: 'Review' }, { title: 'Ready' }]
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Card>
|
|
24
|
+
<Stack gap="group">
|
|
25
|
+
<Typography>
|
|
26
|
+
<h2>Prepare your workspace</h2>
|
|
27
|
+
<p>This draft stays local until your application handles completion.</p>
|
|
28
|
+
</Typography>
|
|
29
|
+
<Stepper steps={steps} currentStep={step} />
|
|
30
|
+
<Form
|
|
31
|
+
enhance={false}
|
|
32
|
+
onSubmit={event => {
|
|
33
|
+
event.preventDefault()
|
|
34
|
+
|
|
35
|
+
if (step === 0) setStep(1); else {
|
|
36
|
+
onComplete?.(workspace.trim())
|
|
37
|
+
|
|
38
|
+
setStep(2)
|
|
39
|
+
}
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<Stack gap="group">
|
|
43
|
+
{step === 0 && (
|
|
44
|
+
<Stack gap="related">
|
|
45
|
+
<h3 ref={stepHeadingRef} tabIndex={-1}>Workspace</h3>
|
|
46
|
+
<Label htmlFor={id}>Workspace name</Label>
|
|
47
|
+
<Input
|
|
48
|
+
id={id}
|
|
49
|
+
required
|
|
50
|
+
maxLength={80}
|
|
51
|
+
value={workspace}
|
|
52
|
+
onChange={event => {
|
|
53
|
+
setWorkspace(event.currentTarget.value)
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
</Stack>
|
|
57
|
+
)}
|
|
58
|
+
{step === 1 && (
|
|
59
|
+
<Typography>
|
|
60
|
+
<h3 ref={stepHeadingRef} tabIndex={-1}>Review your draft</h3>
|
|
61
|
+
<p>{`Workspace: ${workspace}`}</p>
|
|
62
|
+
</Typography>
|
|
63
|
+
)}
|
|
64
|
+
{step === 2 && (
|
|
65
|
+
<Typography>
|
|
66
|
+
<h3 ref={stepHeadingRef} tabIndex={-1}>Ready</h3>
|
|
67
|
+
<p role="status">Workspace draft completed.</p>
|
|
68
|
+
</Typography>
|
|
69
|
+
)}
|
|
70
|
+
<Stack direction="horizontal" gap="related">
|
|
71
|
+
{step === 1 && (
|
|
72
|
+
<Button
|
|
73
|
+
type="button"
|
|
74
|
+
variant="secondary"
|
|
75
|
+
onClick={() => {
|
|
76
|
+
setStep(0)
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
Back
|
|
80
|
+
</Button>
|
|
81
|
+
)}
|
|
82
|
+
{step < 2 && <Button type="submit" disabled={!workspace.trim()}>{step === 0 ? 'Continue' : 'Complete setup'}</Button>}
|
|
83
|
+
{step === 2 && (
|
|
84
|
+
<Button
|
|
85
|
+
type="button"
|
|
86
|
+
variant="secondary"
|
|
87
|
+
onClick={() => {
|
|
88
|
+
setStep(0)
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
Edit draft
|
|
92
|
+
</Button>
|
|
93
|
+
)}
|
|
94
|
+
</Stack>
|
|
95
|
+
</Stack>
|
|
96
|
+
</Form>
|
|
97
|
+
</Stack>
|
|
98
|
+
</Card>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { useId, useRef, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Alert, Badge, Button, Card, ChangeSummary, type ChangeSummaryProps, Stack } from '@santi020k/lumen-react'
|
|
5
|
+
|
|
6
|
+
export interface ImportReviewIssue {
|
|
7
|
+
id: string
|
|
8
|
+
location: string
|
|
9
|
+
message: string
|
|
10
|
+
blocking: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ImportReviewRecord {
|
|
14
|
+
id: string
|
|
15
|
+
label: string
|
|
16
|
+
changes: NonNullable<ChangeSummaryProps['items']>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ImportReviewRecipeProps {
|
|
20
|
+
label: string
|
|
21
|
+
issues: readonly ImportReviewIssue[]
|
|
22
|
+
records: readonly ImportReviewRecord[]
|
|
23
|
+
/** Already localized counts and destination supplied by the host. */
|
|
24
|
+
summary: ReactNode
|
|
25
|
+
pending?: boolean
|
|
26
|
+
onConfirm: () => void
|
|
27
|
+
/** Application controls parsing, versioned proposals, confirmation and idempotency. */
|
|
28
|
+
onIssue?: (id: string) => void
|
|
29
|
+
labels: {
|
|
30
|
+
issues: string
|
|
31
|
+
records: string
|
|
32
|
+
blocking: string
|
|
33
|
+
warning: string
|
|
34
|
+
empty: string
|
|
35
|
+
previous: string
|
|
36
|
+
next: string
|
|
37
|
+
confirm: string
|
|
38
|
+
before: string
|
|
39
|
+
after: string
|
|
40
|
+
changed: string
|
|
41
|
+
unchanged: string
|
|
42
|
+
position: (page: number, count: number) => string
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Pure review presentation. No CSV parsing, record matching or implicit reconciliation. */
|
|
47
|
+
export const ImportReviewRecipe = ({
|
|
48
|
+
label, issues, records, summary, pending = false, onConfirm, onIssue, labels
|
|
49
|
+
}: ImportReviewRecipeProps) => {
|
|
50
|
+
const [selectedId, setSelectedId] = useState<string | undefined>(undefined)
|
|
51
|
+
const index = Math.max(0, records.findIndex(record => record.id === selectedId))
|
|
52
|
+
const record = records[index]
|
|
53
|
+
const headingId = useId()
|
|
54
|
+
const detailRef = useRef<HTMLElement>(null)
|
|
55
|
+
|
|
56
|
+
const move = (next: number) => {
|
|
57
|
+
const target = records[next]
|
|
58
|
+
|
|
59
|
+
if (!target) return
|
|
60
|
+
|
|
61
|
+
setSelectedId(target.id)
|
|
62
|
+
|
|
63
|
+
detailRef.current?.focus()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<section aria-label={label} aria-busy={pending}>
|
|
68
|
+
<Stack gap="group">
|
|
69
|
+
{summary}
|
|
70
|
+
{issues.length > 0 && (
|
|
71
|
+
<Alert variant={issues.some(issue => issue.blocking) ? 'warning' : 'default'}>
|
|
72
|
+
<h3>{labels.issues}</h3>
|
|
73
|
+
<ul>
|
|
74
|
+
{issues.map(issue => (
|
|
75
|
+
<li key={issue.id}>
|
|
76
|
+
<Badge variant={issue.blocking ? 'warning' : 'outline'}>{issue.blocking ? labels.blocking : labels.warning}</Badge>
|
|
77
|
+
{' '}
|
|
78
|
+
{onIssue ?
|
|
79
|
+
(
|
|
80
|
+
<Button
|
|
81
|
+
type="button"
|
|
82
|
+
variant="link"
|
|
83
|
+
disabled={pending}
|
|
84
|
+
onClick={() => {
|
|
85
|
+
onIssue(issue.id)
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{issue.location}
|
|
89
|
+
</Button>
|
|
90
|
+
) :
|
|
91
|
+
<strong>{issue.location}</strong>}
|
|
92
|
+
{' '}
|
|
93
|
+
{issue.message}
|
|
94
|
+
</li>
|
|
95
|
+
))}
|
|
96
|
+
</ul>
|
|
97
|
+
</Alert>
|
|
98
|
+
)}
|
|
99
|
+
{record ?
|
|
100
|
+
(
|
|
101
|
+
<Card>
|
|
102
|
+
<section ref={detailRef} tabIndex={-1} aria-labelledby={headingId}>
|
|
103
|
+
<h3 id={headingId}>{record.label}</h3>
|
|
104
|
+
<ChangeSummary
|
|
105
|
+
label={labels.records}
|
|
106
|
+
items={record.changes}
|
|
107
|
+
beforeLabel={labels.before}
|
|
108
|
+
afterLabel={labels.after}
|
|
109
|
+
changedLabel={labels.changed}
|
|
110
|
+
unchangedLabel={labels.unchanged}
|
|
111
|
+
/>
|
|
112
|
+
</section>
|
|
113
|
+
<Stack direction="horizontal" wrap gap="related">
|
|
114
|
+
<Button
|
|
115
|
+
type="button"
|
|
116
|
+
variant="outline"
|
|
117
|
+
disabled={pending || index === 0}
|
|
118
|
+
onClick={() => {
|
|
119
|
+
move(index - 1)
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
{labels.previous}
|
|
123
|
+
</Button>
|
|
124
|
+
<p role="status">{labels.position(index + 1, records.length)}</p>
|
|
125
|
+
<Button
|
|
126
|
+
type="button"
|
|
127
|
+
variant="outline"
|
|
128
|
+
disabled={pending || index >= records.length - 1}
|
|
129
|
+
onClick={() => {
|
|
130
|
+
move(index + 1)
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{labels.next}
|
|
134
|
+
</Button>
|
|
135
|
+
</Stack>
|
|
136
|
+
</Card>
|
|
137
|
+
) :
|
|
138
|
+
<p role="status">{labels.empty}</p>}
|
|
139
|
+
<Button
|
|
140
|
+
type="button"
|
|
141
|
+
loading={pending}
|
|
142
|
+
disabled={pending || records.length === 0 || issues.some(issue => issue.blocking)}
|
|
143
|
+
onClick={onConfirm}
|
|
144
|
+
>
|
|
145
|
+
{labels.confirm}
|
|
146
|
+
</Button>
|
|
147
|
+
</Stack>
|
|
148
|
+
</section>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { Button, Card, Grid, Stack, Typography, VisualEffect } from '@santi020k/lumen-react'
|
|
6
|
+
|
|
7
|
+
export interface PlanSelection { billing: 'monthly' | 'annual', plan: 'starter' | 'team' }
|
|
8
|
+
export const InteractivePricingRecipe = ({ onSelect }: { onSelect?: (selection: PlanSelection) => void }) => {
|
|
9
|
+
const [billing, setBilling] = useState<PlanSelection['billing']>('monthly')
|
|
10
|
+
const [selected, setSelected] = useState('')
|
|
11
|
+
const prices = { starter: { monthly: 10, annual: 96 }, team: { monthly: 24, annual: 240 } }
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Stack gap="group" aria-label="Example pricing">
|
|
15
|
+
<Typography>
|
|
16
|
+
<h2>A plan for your next project</h2>
|
|
17
|
+
<p>Illustrative prices. Connect selections to your own checkout.</p>
|
|
18
|
+
</Typography>
|
|
19
|
+
<Stack direction="horizontal" gap="related" role="group" aria-label="Billing period">
|
|
20
|
+
<Button
|
|
21
|
+
aria-pressed={billing === 'monthly'}
|
|
22
|
+
onClick={() => {
|
|
23
|
+
setBilling('monthly')
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
Monthly
|
|
27
|
+
</Button>
|
|
28
|
+
<Button
|
|
29
|
+
aria-pressed={billing === 'annual'}
|
|
30
|
+
onClick={() => {
|
|
31
|
+
setBilling('annual')
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
Annual
|
|
35
|
+
</Button>
|
|
36
|
+
</Stack>
|
|
37
|
+
<Grid columns="auto">
|
|
38
|
+
{(['starter', 'team'] as const).map(plan => (
|
|
39
|
+
<Card key={plan}>
|
|
40
|
+
<VisualEffect variant={plan === 'team' ? 'mesh' : 'border'}>
|
|
41
|
+
<Typography>
|
|
42
|
+
<h3>{plan === 'starter' ? 'Starter' : 'Team'}</h3>
|
|
43
|
+
<p>{`$${prices[plan][billing]} / ${billing === 'annual' ? 'year' : 'month'}`}</p>
|
|
44
|
+
</Typography>
|
|
45
|
+
<Button onClick={() => {
|
|
46
|
+
setSelected(`${plan}, ${billing}`)
|
|
47
|
+
|
|
48
|
+
onSelect?.({ plan, billing })
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
{`Choose ${plan}`}
|
|
52
|
+
</Button>
|
|
53
|
+
</VisualEffect>
|
|
54
|
+
</Card>
|
|
55
|
+
))}
|
|
56
|
+
</Grid>
|
|
57
|
+
<span role="status">{selected ? `Selected ${selected}.` : 'Select a plan to compare billing.'}</span>
|
|
58
|
+
</Stack>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useId } from 'react'
|
|
4
|
+
|
|
5
|
+
import { Badge, Button, Field, Label, Slider, Stack } from '@santi020k/lumen-react'
|
|
6
|
+
|
|
7
|
+
export interface AdjustmentControlProps {
|
|
8
|
+
defaultValue: number
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
formatValue: (value: number) => string
|
|
11
|
+
label: string
|
|
12
|
+
max: number
|
|
13
|
+
min: number
|
|
14
|
+
modifiedLabel: string
|
|
15
|
+
onValueChange: (value: number) => void
|
|
16
|
+
resetLabel: string
|
|
17
|
+
step: number
|
|
18
|
+
value: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Draft adjustment state belongs to the host; a reset requests the declared neutral value. */
|
|
22
|
+
export const AdjustmentControlRecipe = ({
|
|
23
|
+
defaultValue, disabled, formatValue, label, max, min, modifiedLabel, onValueChange, resetLabel,
|
|
24
|
+
step, value
|
|
25
|
+
}: AdjustmentControlProps) => {
|
|
26
|
+
const id = useId()
|
|
27
|
+
const outputId = useId()
|
|
28
|
+
const modified = value !== defaultValue
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Field>
|
|
32
|
+
<Stack direction="horizontal" align="center" justify="between" wrap>
|
|
33
|
+
<Label htmlFor={id}>{label}</Label>
|
|
34
|
+
<output id={outputId} htmlFor={id}>{formatValue(value)}</output>
|
|
35
|
+
{modified && <Badge variant="secondary">{modifiedLabel}</Badge>}
|
|
36
|
+
</Stack>
|
|
37
|
+
<Slider
|
|
38
|
+
id={id}
|
|
39
|
+
aria-describedby={outputId}
|
|
40
|
+
aria-valuetext={formatValue(value)}
|
|
41
|
+
min={min}
|
|
42
|
+
max={max}
|
|
43
|
+
step={step}
|
|
44
|
+
value={value}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
onChange={event => {
|
|
47
|
+
onValueChange(event.currentTarget.valueAsNumber)
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
<Button
|
|
51
|
+
variant="ghost"
|
|
52
|
+
disabled={disabled === true || !modified}
|
|
53
|
+
onClick={() => {
|
|
54
|
+
onValueChange(defaultValue)
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{resetLabel}
|
|
58
|
+
</Button>
|
|
59
|
+
</Field>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Button, ErrorState, Progress, Stack } from '@santi020k/lumen-react'
|
|
4
|
+
|
|
5
|
+
export type MediaProcessingState =
|
|
6
|
+
| { phase: 'idle' | 'cancelled' | 'success', message: string } |
|
|
7
|
+
{ phase: 'pending', message: string, progress: number | null } |
|
|
8
|
+
{ phase: 'error', message: string, recovery: string }
|
|
9
|
+
|
|
10
|
+
export interface MediaProcessingRecipeProps {
|
|
11
|
+
cancelLabel: string
|
|
12
|
+
onCancel: () => void
|
|
13
|
+
onRetry: () => void
|
|
14
|
+
onStart: () => void
|
|
15
|
+
progressLabel: string
|
|
16
|
+
retryLabel: string
|
|
17
|
+
startLabel: string
|
|
18
|
+
state: MediaProcessingState
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Host-owned jobs control all transitions; cancellation is complete only after acknowledgement. */
|
|
22
|
+
export const MediaProcessingRecipe = ({
|
|
23
|
+
cancelLabel, onCancel, onRetry, onStart, progressLabel, retryLabel, startLabel, state
|
|
24
|
+
}: MediaProcessingRecipeProps) => (
|
|
25
|
+
<Stack gap="related">
|
|
26
|
+
{state.phase === 'error' ?
|
|
27
|
+
(
|
|
28
|
+
<ErrorState title={state.message} description={state.recovery} graphic={false} announce="polite" headingLevel={3}>
|
|
29
|
+
<Button onClick={onRetry}>{retryLabel}</Button>
|
|
30
|
+
</ErrorState>
|
|
31
|
+
) :
|
|
32
|
+
<p role="status">{state.message}</p>}
|
|
33
|
+
{state.phase === 'pending' && (
|
|
34
|
+
<>
|
|
35
|
+
{state.progress !== null && Number.isFinite(state.progress) && (
|
|
36
|
+
<Progress aria-label={progressLabel} value={Math.min(100, Math.max(0, state.progress))} />
|
|
37
|
+
)}
|
|
38
|
+
<Button variant="outline" onClick={onCancel}>{cancelLabel}</Button>
|
|
39
|
+
</>
|
|
40
|
+
)}
|
|
41
|
+
{state.phase !== 'pending' && state.phase !== 'error' && <Button onClick={onStart}>{startLabel}</Button>}
|
|
42
|
+
</Stack>
|
|
43
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
import { moveLumenMediaItem, resolveLumenMediaSelection, toggleLumenMediaSelection } from '@santi020k/lumen-core/media-workspace'
|
|
6
|
+
import { Button, Card, Image, MediaFilmstrip, MediaThumbnail, Stack, Toolbar } from '@santi020k/lumen-react'
|
|
7
|
+
|
|
8
|
+
// Import the installed media-workspace.css once at the application's style boundary.
|
|
9
|
+
|
|
10
|
+
export interface MediaWorkspaceItem {
|
|
11
|
+
alt: string
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
height: number
|
|
14
|
+
id: string
|
|
15
|
+
label: string
|
|
16
|
+
src: string
|
|
17
|
+
width: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MediaWorkspaceRecipeProps {
|
|
21
|
+
bottomControls?: ReactNode
|
|
22
|
+
collectionLabel: string
|
|
23
|
+
formatSelection: (count: number) => string
|
|
24
|
+
inspector: ReactNode
|
|
25
|
+
inspectorLabel: string
|
|
26
|
+
items: readonly MediaWorkspaceItem[]
|
|
27
|
+
moveEarlierLabel: string
|
|
28
|
+
moveLaterLabel: string
|
|
29
|
+
onItemsChange: (items: MediaWorkspaceItem[]) => void
|
|
30
|
+
onSelectionChange: (ids: string[]) => void
|
|
31
|
+
preview: ReactNode
|
|
32
|
+
previewLabel: string
|
|
33
|
+
selectedIds: readonly string[]
|
|
34
|
+
showTitle?: boolean
|
|
35
|
+
title: string
|
|
36
|
+
tools: ReactNode
|
|
37
|
+
toolsLabel: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Compose a preview, supporting controls and inspector without owning photo services or drafts. */
|
|
41
|
+
export const MediaWorkspaceRecipe = ({
|
|
42
|
+
bottomControls, collectionLabel, formatSelection, inspector, inspectorLabel, items,
|
|
43
|
+
moveEarlierLabel, moveLaterLabel, onItemsChange, onSelectionChange, preview, previewLabel,
|
|
44
|
+
selectedIds, showTitle = true, title, tools, toolsLabel
|
|
45
|
+
}: MediaWorkspaceRecipeProps) => {
|
|
46
|
+
const selection = resolveLumenMediaSelection(items, selectedIds)
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<section className="lumen-media-workspace" aria-label={title}>
|
|
50
|
+
{showTitle && <h2>{title}</h2>}
|
|
51
|
+
<div className="lumen-media-workspace__layout">
|
|
52
|
+
<div className="lumen-media-workspace__preview" role="group" aria-label={previewLabel}>{preview}</div>
|
|
53
|
+
<Card className="lumen-media-workspace__inspector" aria-label={inspectorLabel}>{inspector}</Card>
|
|
54
|
+
</div>
|
|
55
|
+
<Card glass="subtle" className="lumen-media-workspace__tools">
|
|
56
|
+
<Toolbar aria-label={toolsLabel}>{tools}</Toolbar>
|
|
57
|
+
</Card>
|
|
58
|
+
<MediaFilmstrip label={collectionLabel} selectionLabel={formatSelection(selection.length)}>
|
|
59
|
+
{items.map((item, index) => (
|
|
60
|
+
<li key={item.id}>
|
|
61
|
+
<MediaThumbnail
|
|
62
|
+
label={item.label}
|
|
63
|
+
order={index + 1}
|
|
64
|
+
selected={selection.includes(item.id)}
|
|
65
|
+
disabled={item.disabled}
|
|
66
|
+
onClick={() => {
|
|
67
|
+
onSelectionChange(toggleLumenMediaSelection(items, selection, item.id))
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<Image src={item.src} alt={item.alt} width={item.width} height={item.height} />
|
|
71
|
+
</MediaThumbnail>
|
|
72
|
+
<Stack gap="related">
|
|
73
|
+
<Button
|
|
74
|
+
variant="outline"
|
|
75
|
+
aria-label={`${moveEarlierLabel}: ${item.label}`}
|
|
76
|
+
disabled={item.disabled === true || index === 0}
|
|
77
|
+
onClick={() => {
|
|
78
|
+
onItemsChange(moveLumenMediaItem(items, item.id, index - 1))
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{moveEarlierLabel}
|
|
82
|
+
</Button>
|
|
83
|
+
<Button
|
|
84
|
+
variant="outline"
|
|
85
|
+
aria-label={`${moveLaterLabel}: ${item.label}`}
|
|
86
|
+
disabled={item.disabled === true || index === items.length - 1}
|
|
87
|
+
onClick={() => {
|
|
88
|
+
onItemsChange(moveLumenMediaItem(items, item.id, index + 1))
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{moveLaterLabel}
|
|
92
|
+
</Button>
|
|
93
|
+
</Stack>
|
|
94
|
+
</li>
|
|
95
|
+
))}
|
|
96
|
+
</MediaFilmstrip>
|
|
97
|
+
{bottomControls && <Card className="lumen-media-workspace__bottom">{bottomControls}</Card>}
|
|
98
|
+
</section>
|
|
99
|
+
)
|
|
100
|
+
}
|