@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.
Files changed (125) hide show
  1. package/README.md +211 -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 +564 -17
  40. package/package.json +26 -11
  41. package/release-manifest.json +22 -17
  42. package/styles/critical.css +696 -66
  43. package/styles/motion.css +61 -0
  44. package/styles/world-map.css +30 -0
  45. package/styles.css +1756 -409
  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 +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
+ }