@open-mercato/ui 0.6.7-develop.6785.1.1dd7cfac55 → 0.6.7-develop.6795.1.8a3f27921c
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/.turbo/turbo-build.log +1 -1
- package/build.mjs +2 -54
- package/dist/backend/NextStepCallout.js +4 -4
- package/dist/backend/NextStepCallout.js.map +1 -1
- package/dist/backend/confirm-dialog/ConfirmDialog.js +12 -0
- package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
- package/dist/backend/detail/AddressTiles.js +4 -5
- package/dist/backend/detail/AddressTiles.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +1 -25
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.js +3 -3
- package/dist/backend/icons/lucideRegistry.js.map +2 -2
- package/dist/backend/icons/lucideRegistryRuntime.js +28 -0
- package/dist/backend/icons/lucideRegistryRuntime.js.map +7 -0
- package/dist/backend/schedule/ScheduleToolbar.js +87 -70
- package/dist/backend/schedule/ScheduleToolbar.js.map +2 -2
- package/jest.config.cjs +2 -1
- package/package.json +17 -16
- package/scripts/lucideRegistrySource.cjs +133 -0
- package/src/backend/NextStepCallout.tsx +4 -4
- package/src/backend/confirm-dialog/ConfirmDialog.tsx +15 -0
- package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +33 -1
- package/src/backend/detail/AddressTiles.tsx +3 -6
- package/src/backend/icons/__tests__/lucideRegistryGenerator.test.ts +177 -0
- package/src/backend/icons/lucideRegistry.generated.tsx +3 -37
- package/src/backend/icons/lucideRegistry.ts +2 -2
- package/src/backend/icons/lucideRegistryRuntime.tsx +36 -0
- package/src/backend/schedule/ScheduleToolbar.tsx +89 -68
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { Project, ScriptKind, SyntaxKind, type ObjectLiteralExpression, type SourceFile } from 'ts-morph'
|
|
5
|
+
import { buildLucideRegistrySource } from '../../../../scripts/lucideRegistrySource.cjs'
|
|
6
|
+
|
|
7
|
+
const packageDir = join(__dirname, '..', '..', '..', '..')
|
|
8
|
+
const iconsDir = join(packageDir, 'src', 'backend', 'icons')
|
|
9
|
+
|
|
10
|
+
type ResolvedIcon = { kebab: string; exportName: string }
|
|
11
|
+
|
|
12
|
+
const populatedFixture: ResolvedIcon[] = [
|
|
13
|
+
{ kebab: 'bell', exportName: 'Bell' },
|
|
14
|
+
{ kebab: 'alert-circle', exportName: 'AlertCircle' },
|
|
15
|
+
{ kebab: 'bar-chart-2', exportName: 'BarChart2' },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const duplicateExportFixture: ResolvedIcon[] = [
|
|
19
|
+
{ kebab: 'trash', exportName: 'Trash' },
|
|
20
|
+
{ kebab: 'trash-can', exportName: 'Trash' },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
function parse(source: string): SourceFile {
|
|
24
|
+
const project = new Project({ useInMemoryFileSystem: true })
|
|
25
|
+
return project.createSourceFile('lucideRegistry.generated.tsx', source, {
|
|
26
|
+
overwrite: true,
|
|
27
|
+
scriptKind: ScriptKind.TSX,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function syntacticDiagnostics(source: string): string[] {
|
|
32
|
+
const sourceFile = parse(source)
|
|
33
|
+
return sourceFile
|
|
34
|
+
.getProject()
|
|
35
|
+
.getProgram()
|
|
36
|
+
.getSyntacticDiagnostics(sourceFile)
|
|
37
|
+
.map((diagnostic) => `${diagnostic.getLineNumber() ?? 0}: ${diagnostic.getMessageText()}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function registryObject(source: string): ObjectLiteralExpression {
|
|
41
|
+
const declaration = parse(source).getVariableDeclarationOrThrow('LUCIDE_ICON_REGISTRY')
|
|
42
|
+
return declaration.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function registryEntries(source: string): Record<string, string> {
|
|
46
|
+
const entries: Record<string, string> = {}
|
|
47
|
+
for (const property of registryObject(source).getProperties()) {
|
|
48
|
+
const assignment = property.asKindOrThrow(SyntaxKind.PropertyAssignment)
|
|
49
|
+
entries[assignment.getSymbolOrThrow().getName()] = assignment.getInitializerOrThrow().getText()
|
|
50
|
+
}
|
|
51
|
+
return entries
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function valueImportNames(source: string, moduleSpecifier: string): string[] {
|
|
55
|
+
return parse(source)
|
|
56
|
+
.getImportDeclarations()
|
|
57
|
+
.filter((declaration) => declaration.getModuleSpecifierValue() === moduleSpecifier)
|
|
58
|
+
.filter((declaration) => !declaration.isTypeOnly())
|
|
59
|
+
.flatMap((declaration) => declaration.getNamedImports().map((named) => named.getName()))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('buildLucideRegistrySource', () => {
|
|
63
|
+
it('maps every discovered kebab name to its lucide export', () => {
|
|
64
|
+
expect(registryEntries(buildLucideRegistrySource(populatedFixture))).toEqual({
|
|
65
|
+
'alert-circle': 'AlertCircle',
|
|
66
|
+
'bar-chart-2': 'BarChart2',
|
|
67
|
+
bell: 'Bell',
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('emits the registry keys sorted by kebab name', () => {
|
|
72
|
+
const keys = registryObject(buildLucideRegistrySource(populatedFixture))
|
|
73
|
+
.getProperties()
|
|
74
|
+
.map((property) => property.asKindOrThrow(SyntaxKind.PropertyAssignment).getSymbolOrThrow().getName())
|
|
75
|
+
expect(keys).toEqual(['alert-circle', 'bar-chart-2', 'bell'])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('imports each lucide export exactly once', () => {
|
|
79
|
+
expect(valueImportNames(buildLucideRegistrySource(populatedFixture), 'lucide-react')).toEqual([
|
|
80
|
+
'AlertCircle',
|
|
81
|
+
'BarChart2',
|
|
82
|
+
'Bell',
|
|
83
|
+
])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('quotes keys that are not valid identifiers', () => {
|
|
87
|
+
const property = registryObject(buildLucideRegistrySource(populatedFixture))
|
|
88
|
+
.getPropertyOrThrow("'bar-chart-2'")
|
|
89
|
+
.asKindOrThrow(SyntaxKind.PropertyAssignment)
|
|
90
|
+
expect(property.getNameNode().getKind()).toBe(SyntaxKind.StringLiteral)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('de-duplicates the value import when two names share one export', () => {
|
|
94
|
+
const source = buildLucideRegistrySource(duplicateExportFixture)
|
|
95
|
+
expect(valueImportNames(source, 'lucide-react')).toEqual(['Trash'])
|
|
96
|
+
expect(registryEntries(source)).toEqual({ trash: 'Trash', 'trash-can': 'Trash' })
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('emits a valid module with an empty registry when no icons are discovered', () => {
|
|
100
|
+
const source = buildLucideRegistrySource([])
|
|
101
|
+
expect(registryObject(source).getProperties()).toHaveLength(0)
|
|
102
|
+
expect(valueImportNames(source, 'lucide-react')).toEqual([])
|
|
103
|
+
expect(source).not.toContain("import {} from 'lucide-react'")
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('keeps the registry a plain mutable object literal', () => {
|
|
107
|
+
const source = buildLucideRegistrySource(populatedFixture)
|
|
108
|
+
const declaration = parse(source).getVariableDeclarationOrThrow('LUCIDE_ICON_REGISTRY')
|
|
109
|
+
expect(declaration.getTypeNodeOrThrow().getText()).toBe('Record<string, LucideIcon>')
|
|
110
|
+
expect(declaration.getVariableStatementOrThrow().isExported()).toBe(true)
|
|
111
|
+
expect(source).not.toContain('as const')
|
|
112
|
+
expect(source).not.toContain('Object.freeze')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('keeps the LucideIcon import type-only', () => {
|
|
116
|
+
const typeOnly = parse(buildLucideRegistrySource(populatedFixture))
|
|
117
|
+
.getImportDeclarations()
|
|
118
|
+
.filter((declaration) => declaration.isTypeOnly())
|
|
119
|
+
.flatMap((declaration) => declaration.getNamedImports().map((named) => named.getName()))
|
|
120
|
+
expect(typeOnly).toEqual(['LucideIcon'])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it.each([
|
|
124
|
+
['populated', populatedFixture],
|
|
125
|
+
['empty', [] as ResolvedIcon[]],
|
|
126
|
+
['duplicate exports', duplicateExportFixture],
|
|
127
|
+
])('emits %s output with zero syntactic diagnostics', (_label, fixture) => {
|
|
128
|
+
expect(syntacticDiagnostics(buildLucideRegistrySource(fixture))).toEqual([])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('fails loudly when an export name would produce an unparseable module', () => {
|
|
132
|
+
expect(() => buildLucideRegistrySource([{ kebab: 'broken', exportName: 'not a valid export' }])).toThrow(
|
|
133
|
+
/not syntactically valid/
|
|
134
|
+
)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('lucideRegistry barrel', () => {
|
|
139
|
+
it('keeps its public export list unchanged', () => {
|
|
140
|
+
const project = new Project({ useInMemoryFileSystem: true })
|
|
141
|
+
const barrel = project.createSourceFile(
|
|
142
|
+
'lucideRegistry.ts',
|
|
143
|
+
readFileSync(join(iconsDir, 'lucideRegistry.ts'), 'utf-8'),
|
|
144
|
+
{ overwrite: true }
|
|
145
|
+
)
|
|
146
|
+
expect([...barrel.getExportedDeclarations().keys()].sort()).toEqual([
|
|
147
|
+
'LUCIDE_ICON_REGISTRY',
|
|
148
|
+
'registerAdditionalIcons',
|
|
149
|
+
'resolveRegisteredLucideIcon',
|
|
150
|
+
'resolveRegisteredLucideIconNode',
|
|
151
|
+
])
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('lucideRegistry.generated importers', () => {
|
|
156
|
+
it('is imported only from within src/backend/icons', () => {
|
|
157
|
+
const repoRoot = join(packageDir, '..', '..')
|
|
158
|
+
let matches: string[] = []
|
|
159
|
+
try {
|
|
160
|
+
matches = execFileSync(
|
|
161
|
+
'git',
|
|
162
|
+
['grep', '-l', 'lucideRegistry.generated', '--', '*.ts', '*.tsx'],
|
|
163
|
+
{ cwd: repoRoot, encoding: 'utf-8' }
|
|
164
|
+
)
|
|
165
|
+
.split('\n')
|
|
166
|
+
.filter(Boolean)
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const status = (error as { status?: number }).status
|
|
169
|
+
if (status !== 1) throw error
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const outsideIconsFolder = matches.filter(
|
|
173
|
+
(path) => !path.startsWith('packages/ui/src/backend/icons/')
|
|
174
|
+
)
|
|
175
|
+
expect(outsideIconsFolder).toEqual([])
|
|
176
|
+
})
|
|
177
|
+
})
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.
|
|
3
3
|
// Generated from discovered icon string usages across the repo.
|
|
4
4
|
|
|
5
|
-
import type
|
|
6
|
-
import type { LucideIcon } from 'lucide-react'
|
|
5
|
+
import type { LucideIcon } from 'lucide-react';
|
|
7
6
|
import {
|
|
8
7
|
Activity,
|
|
9
8
|
AlertCircle,
|
|
@@ -150,7 +149,7 @@ import {
|
|
|
150
149
|
Wrench,
|
|
151
150
|
X,
|
|
152
151
|
XCircle,
|
|
153
|
-
} from 'lucide-react'
|
|
152
|
+
} from 'lucide-react';
|
|
154
153
|
|
|
155
154
|
export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
|
|
156
155
|
'activity': Activity,
|
|
@@ -298,37 +297,4 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
|
|
|
298
297
|
'wrench': Wrench,
|
|
299
298
|
'x': X,
|
|
300
299
|
'x-circle': XCircle,
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function normalizeKebabIconName(input: string): string {
|
|
304
|
-
const trimmed = input.trim()
|
|
305
|
-
if (!trimmed) return ''
|
|
306
|
-
const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed
|
|
307
|
-
if (!withoutPrefix) return ''
|
|
308
|
-
if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {
|
|
309
|
-
return withoutPrefix
|
|
310
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
311
|
-
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
312
|
-
.toLowerCase()
|
|
313
|
-
}
|
|
314
|
-
return withoutPrefix
|
|
315
|
-
.replace(/[_\s]+/g, '-')
|
|
316
|
-
.replace(/-+/g, '-')
|
|
317
|
-
.toLowerCase()
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
export function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {
|
|
321
|
-
if (!name) return null
|
|
322
|
-
const normalized = normalizeKebabIconName(name)
|
|
323
|
-
if (!normalized) return null
|
|
324
|
-
return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
export function resolveRegisteredLucideIconNode(
|
|
328
|
-
name: string | undefined,
|
|
329
|
-
className: string
|
|
330
|
-
): React.ReactNode | null {
|
|
331
|
-
const Icon = resolveRegisteredLucideIcon(name)
|
|
332
|
-
if (!Icon) return null
|
|
333
|
-
return <Icon className={className} />
|
|
334
|
-
}
|
|
300
|
+
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export {
|
|
2
2
|
resolveRegisteredLucideIcon,
|
|
3
3
|
resolveRegisteredLucideIconNode,
|
|
4
|
-
|
|
5
|
-
} from './lucideRegistry.generated'
|
|
4
|
+
} from './lucideRegistryRuntime'
|
|
5
|
+
export { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'
|
|
6
6
|
|
|
7
7
|
import type { LucideIcon } from 'lucide-react'
|
|
8
8
|
import { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type * as React from 'react'
|
|
2
|
+
import type { LucideIcon } from 'lucide-react'
|
|
3
|
+
import { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'
|
|
4
|
+
|
|
5
|
+
function normalizeKebabIconName(input: string): string {
|
|
6
|
+
const trimmed = input.trim()
|
|
7
|
+
if (!trimmed) return ''
|
|
8
|
+
const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed
|
|
9
|
+
if (!withoutPrefix) return ''
|
|
10
|
+
if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {
|
|
11
|
+
return withoutPrefix
|
|
12
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
13
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
}
|
|
16
|
+
return withoutPrefix
|
|
17
|
+
.replace(/[_\s]+/g, '-')
|
|
18
|
+
.replace(/-+/g, '-')
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {
|
|
23
|
+
if (!name) return null
|
|
24
|
+
const normalized = normalizeKebabIconName(name)
|
|
25
|
+
if (!normalized) return null
|
|
26
|
+
return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveRegisteredLucideIconNode(
|
|
30
|
+
name: string | undefined,
|
|
31
|
+
className: string
|
|
32
|
+
): React.ReactNode | null {
|
|
33
|
+
const Icon = resolveRegisteredLucideIcon(name)
|
|
34
|
+
if (!Icon) return null
|
|
35
|
+
return <Icon className={className} />
|
|
36
|
+
}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { DateRangePicker } from '../../primitives/date-range-picker'
|
|
5
|
+
import { IconButton } from '../../primitives/icon-button'
|
|
6
|
+
import {
|
|
7
|
+
SegmentedControl,
|
|
8
|
+
SegmentedControlItem,
|
|
9
|
+
} from '../../primitives/segmented-control'
|
|
10
|
+
import {
|
|
11
|
+
Select,
|
|
12
|
+
SelectContent,
|
|
13
|
+
SelectItem,
|
|
14
|
+
SelectTrigger,
|
|
15
|
+
SelectValue,
|
|
16
|
+
} from '../../primitives/select'
|
|
6
17
|
import type { ScheduleRange, ScheduleViewMode } from './types'
|
|
7
18
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
8
19
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -27,17 +38,19 @@ const VIEW_OPTIONS: Array<{ id: ScheduleViewMode; labelKey: string; fallback: st
|
|
|
27
38
|
{ id: 'agenda', labelKey: 'schedule.view.agenda', fallback: 'Agenda' },
|
|
28
39
|
]
|
|
29
40
|
|
|
30
|
-
function
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
function getTimezoneOptions(current?: string): string[] {
|
|
42
|
+
const options = new Set<string>()
|
|
43
|
+
if (current) options.add(current)
|
|
44
|
+
options.add('UTC')
|
|
45
|
+
const resolved = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
46
|
+
if (resolved) options.add(resolved)
|
|
47
|
+
const intlWithSupportedValues = Intl as typeof Intl & { supportedValuesOf?: (input: 'timeZone') => string[] }
|
|
48
|
+
if (typeof intlWithSupportedValues.supportedValuesOf === 'function') {
|
|
49
|
+
intlWithSupportedValues.supportedValuesOf('timeZone').forEach((timezone) => {
|
|
50
|
+
if (timezone) options.add(timezone)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
return Array.from(options).sort((a, b) => a.localeCompare(b))
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
export type ScheduleToolbarProps = {
|
|
@@ -120,68 +133,76 @@ export function ScheduleToolbar({
|
|
|
120
133
|
onRangeChange({ start: nextStart, end: endOfDay(addDays(nextStart, rangeLength - 1)) })
|
|
121
134
|
}, [onRangeChange, range.start, rangeLength, view])
|
|
122
135
|
|
|
136
|
+
const timezoneOptions = React.useMemo(() => getTimezoneOptions(timezone), [timezone])
|
|
137
|
+
|
|
123
138
|
return (
|
|
124
|
-
<div className={cn('flex flex-
|
|
125
|
-
<
|
|
139
|
+
<div className={cn('flex flex-wrap items-center gap-x-4 gap-y-2 rounded-xl border bg-card p-3', className)}>
|
|
140
|
+
<SegmentedControl
|
|
141
|
+
value={view}
|
|
142
|
+
onValueChange={(value) => {
|
|
143
|
+
const nextView = value as ScheduleViewMode
|
|
144
|
+
if (nextView === view) return
|
|
145
|
+
onViewChange(nextView)
|
|
146
|
+
onRangeChange(deriveRangeForView(new Date(), nextView))
|
|
147
|
+
}}
|
|
148
|
+
aria-label={t('schedule.view.label', 'Schedule view')}
|
|
149
|
+
className="shrink-0"
|
|
150
|
+
>
|
|
126
151
|
{VIEW_OPTIONS.map((option) => (
|
|
127
|
-
<
|
|
128
|
-
key={option.id}
|
|
129
|
-
variant={view === option.id ? 'default' : 'outline'}
|
|
130
|
-
size="sm"
|
|
131
|
-
onClick={() => {
|
|
132
|
-
if (option.id === view) return
|
|
133
|
-
onViewChange(option.id)
|
|
134
|
-
onRangeChange(deriveRangeForView(new Date(), option.id))
|
|
135
|
-
}}
|
|
136
|
-
>
|
|
152
|
+
<SegmentedControlItem key={option.id} value={option.id}>
|
|
137
153
|
{t(option.labelKey, option.fallback)}
|
|
138
|
-
</
|
|
154
|
+
</SegmentedControlItem>
|
|
139
155
|
))}
|
|
140
|
-
</
|
|
141
|
-
<div className="flex
|
|
142
|
-
<
|
|
156
|
+
</SegmentedControl>
|
|
157
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
158
|
+
<IconButton
|
|
159
|
+
type="button"
|
|
160
|
+
variant="outline"
|
|
161
|
+
onClick={() => shiftRange('prev')}
|
|
162
|
+
aria-label={t('schedule.range.prev', 'Previous')}
|
|
163
|
+
>
|
|
143
164
|
<ChevronLeft className="size-4" aria-hidden />
|
|
144
|
-
</
|
|
145
|
-
<div className="text-sm font-medium text-foreground">{rangeLabel}</div>
|
|
146
|
-
<
|
|
165
|
+
</IconButton>
|
|
166
|
+
<div className="min-w-0 whitespace-nowrap px-1 text-sm font-medium text-foreground">{rangeLabel}</div>
|
|
167
|
+
<IconButton
|
|
168
|
+
type="button"
|
|
169
|
+
variant="outline"
|
|
170
|
+
onClick={() => shiftRange('next')}
|
|
171
|
+
aria-label={t('schedule.range.next', 'Next')}
|
|
172
|
+
>
|
|
147
173
|
<ChevronRight className="size-4" aria-hidden />
|
|
148
|
-
</
|
|
174
|
+
</IconButton>
|
|
149
175
|
</div>
|
|
150
|
-
<div className="flex flex-wrap items-center gap-3">
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
onChange={(event) => onTimezoneChange?.(event.target.value)}
|
|
181
|
-
className="h-8 w-full sm:w-[180px]"
|
|
182
|
-
placeholder={t('schedule.range.timezone.placeholder', 'UTC')}
|
|
183
|
-
/>
|
|
184
|
-
</label>
|
|
176
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2 md:ml-auto">
|
|
177
|
+
<DateRangePicker
|
|
178
|
+
value={range}
|
|
179
|
+
onChange={(next) => {
|
|
180
|
+
if (!next) return
|
|
181
|
+
onRangeChange({ start: startOfDay(next.start), end: endOfDay(next.end) })
|
|
182
|
+
}}
|
|
183
|
+
size="sm"
|
|
184
|
+
showPresets={false}
|
|
185
|
+
numberOfMonths={2}
|
|
186
|
+
aria-label={t('schedule.range.label', 'Date range')}
|
|
187
|
+
/>
|
|
188
|
+
{onTimezoneChange ? (
|
|
189
|
+
<Select value={timezone ?? undefined} onValueChange={onTimezoneChange}>
|
|
190
|
+
<SelectTrigger
|
|
191
|
+
size="sm"
|
|
192
|
+
className="w-auto min-w-[10rem]"
|
|
193
|
+
aria-label={t('schedule.range.timezone', 'Timezone')}
|
|
194
|
+
>
|
|
195
|
+
<SelectValue placeholder={t('schedule.range.timezone.placeholder', 'UTC')} />
|
|
196
|
+
</SelectTrigger>
|
|
197
|
+
<SelectContent>
|
|
198
|
+
{timezoneOptions.map((option) => (
|
|
199
|
+
<SelectItem key={option} value={option}>
|
|
200
|
+
{option}
|
|
201
|
+
</SelectItem>
|
|
202
|
+
))}
|
|
203
|
+
</SelectContent>
|
|
204
|
+
</Select>
|
|
205
|
+
) : null}
|
|
185
206
|
</div>
|
|
186
207
|
</div>
|
|
187
208
|
)
|