@djangocfg/ui-tools 2.1.409 → 2.1.412
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/package.json +13 -13
- package/src/{tools/Chat/highlight → lib/browser-bridge}/README.md +46 -18
- package/src/lib/browser-bridge/commands/chat.ts +42 -0
- package/src/lib/browser-bridge/commands/highlight.ts +70 -0
- package/src/lib/browser-bridge/commands/index.ts +15 -0
- package/src/lib/browser-bridge/commands/inspect.ts +31 -0
- package/src/lib/browser-bridge/commands/scroll.ts +31 -0
- package/src/lib/browser-bridge/commands/write.ts +45 -0
- package/src/lib/browser-bridge/directive-bus.ts +120 -0
- package/src/lib/browser-bridge/index.ts +56 -0
- package/src/lib/browser-bridge/logger.ts +27 -0
- package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/HighlightOverlay.tsx +14 -0
- package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/__tests__/HighlightOverlay.test.tsx +52 -0
- package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/__tests__/resolveRef.test.ts +39 -0
- package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/index.ts +8 -5
- package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/resolveRef.ts +5 -0
- package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/useHighlightTargets.ts +58 -27
- package/src/lib/browser-bridge/overlay/waitForVisible.ts +70 -0
- package/src/lib/browser-bridge/registry.ts +41 -0
- package/src/lib/browser-bridge/setBridgeResolver.ts +42 -0
- package/src/lib/browser-bridge/window.ts +76 -0
- package/src/lib/page-snapshot/capture/walk.ts +13 -5
- package/src/lib/page-snapshot/engine.ts +9 -4
- package/src/lib/page-snapshot/index.ts +5 -0
- package/src/lib/page-snapshot/react/provider.tsx +70 -3
- package/src/lib/page-snapshot/react/use-page-snapshot.ts +10 -0
- package/src/lib/page-snapshot/refs/__tests__/locator.test.ts +94 -0
- package/src/lib/page-snapshot/refs/__tests__/registry.test.ts +59 -3
- package/src/lib/page-snapshot/refs/locator.ts +218 -0
- package/src/lib/page-snapshot/refs/registry.ts +29 -14
- package/src/tools/Chat/README.md +1 -1
- package/src/tools/Chat/composer/AttachContext.tsx +22 -0
- package/src/tools/Chat/composer/Composer.tsx +108 -6
- package/src/tools/Chat/composer/ComposerMenuButton.tsx +39 -2
- package/src/tools/Chat/composer/fileToAttachment.ts +53 -0
- package/src/tools/Chat/composer/index.ts +16 -1
- package/src/tools/Chat/composer/types.ts +71 -0
- package/src/tools/Chat/composer/useComposerAttach.tsx +218 -0
- package/src/tools/Chat/constants.ts +24 -1
- package/src/tools/Chat/context/ChatProvider.tsx +17 -2
- package/src/tools/Chat/core/logger.ts +15 -2
- package/src/tools/Chat/hooks/useChat.ts +32 -0
- package/src/tools/Chat/hooks/useChatComposer.ts +13 -0
- package/src/tools/Chat/index.ts +34 -2
- package/src/tools/Chat/launcher/ChatDock.tsx +13 -3
- package/src/tools/Chat/launcher/ChatFAB.tsx +4 -2
- package/src/tools/Chat/launcher/ChatGreeting.tsx +3 -2
- package/src/tools/Chat/launcher/ChatLauncher.tsx +42 -7
- package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +3 -2
- package/src/tools/Chat/launcher/header/ChatHeader.tsx +2 -0
- package/src/tools/Chat/launcher/header/ChatHeaderActionButton.tsx +2 -0
- package/src/tools/Chat/launcher/header/ChatHeaderLanguageButton.tsx +2 -2
- package/src/tools/Chat/launcher/header/HeaderSlots.tsx +16 -9
- package/src/tools/Chat/lazy.tsx +34 -2
- package/src/tools/Chat/messages/MessageBubble.tsx +1 -1
- package/src/tools/Chat/public.ts +17 -0
- package/src/tools/Chat/settings/README.md +87 -0
- package/src/tools/Chat/settings/__tests__/useChatSettings.test.tsx +84 -0
- package/src/tools/Chat/settings/__tests__/useLocalStorage.test.tsx +138 -0
- package/src/tools/Chat/settings/index.ts +23 -0
- package/src/tools/Chat/settings/types.ts +108 -0
- package/src/tools/Chat/settings/useChatSettings.ts +168 -0
- package/src/tools/Chat/types/events.ts +50 -0
- package/src/tools/Chat/types/index.ts +1 -1
- package/src/tools/Chat/types/message.ts +5 -0
- package/src/tools/CronScheduler/CronScheduler.client.tsx +42 -15
- package/src/tools/CronScheduler/components/CustomInput.tsx +26 -7
- package/src/tools/CronScheduler/components/DayChips.tsx +20 -7
- package/src/tools/CronScheduler/components/MonthDayGrid.tsx +35 -10
- package/src/tools/CronScheduler/components/SchedulePreview.tsx +8 -5
- package/src/tools/CronScheduler/components/ScheduleTypeSelector.tsx +12 -3
- package/src/tools/CronScheduler/components/TimeSelector.tsx +36 -13
- package/src/tools/CronScheduler/context/CronSchedulerContext.tsx +4 -0
- package/src/tools/CronScheduler/context/hooks.ts +8 -0
- package/src/tools/CronScheduler/context/index.ts +1 -0
- package/src/tools/CronScheduler/index.tsx +2 -0
- package/src/tools/CronScheduler/lazy.tsx +1 -0
- package/src/tools/CronScheduler/types/index.ts +18 -1
- package/src/tools/Map/lazy.tsx +11 -4
- package/src/tools/Uploader/hooks/useClipboardPaste.ts +3 -1
- package/src/tools/index.ts +2 -0
- /package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/SpotlightCanvas.tsx +0 -0
- /package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/types.ts +0 -0
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from '@djangocfg/ui-core/components';
|
|
19
19
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
20
20
|
import { CronSchedulerProvider } from './context/CronSchedulerContext';
|
|
21
|
-
import { useCronType, useCronPreview } from './context/hooks';
|
|
21
|
+
import { useCronType, useCronPreview, useCronSize } from './context/hooks';
|
|
22
22
|
import {
|
|
23
23
|
ScheduleTypeSelector,
|
|
24
24
|
TimeSelector,
|
|
@@ -39,9 +39,11 @@ interface ScheduleEditorProps {
|
|
|
39
39
|
|
|
40
40
|
function ScheduleEditor({ timeFormat, disabled }: ScheduleEditorProps) {
|
|
41
41
|
const { type } = useCronType();
|
|
42
|
+
const size = useCronSize();
|
|
43
|
+
const isSm = size === 'sm';
|
|
42
44
|
|
|
43
45
|
return (
|
|
44
|
-
<div className=
|
|
46
|
+
<div className={isSm ? 'space-y-1.5' : 'space-y-3'}>
|
|
45
47
|
<ScheduleTypeSelector disabled={disabled} />
|
|
46
48
|
|
|
47
49
|
{type !== 'custom' && (
|
|
@@ -67,6 +69,7 @@ function CronExpressionLine({
|
|
|
67
69
|
}) {
|
|
68
70
|
const [copied, setCopied] = useState(false);
|
|
69
71
|
const { cronExpression, isValid } = useCronPreview();
|
|
72
|
+
const isSm = useCronSize() === 'sm';
|
|
70
73
|
|
|
71
74
|
const handleCopy = async (e: React.MouseEvent) => {
|
|
72
75
|
e.stopPropagation();
|
|
@@ -80,8 +83,13 @@ function CronExpressionLine({
|
|
|
80
83
|
};
|
|
81
84
|
|
|
82
85
|
return (
|
|
83
|
-
<div className={cn('flex items-center gap-1.5', className)}>
|
|
84
|
-
<code
|
|
86
|
+
<div className={cn('flex items-center', isSm ? 'gap-1' : 'gap-1.5', className)}>
|
|
87
|
+
<code
|
|
88
|
+
className={cn(
|
|
89
|
+
'font-mono text-muted-foreground',
|
|
90
|
+
isSm ? 'text-[11px]' : 'text-xs'
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
85
93
|
{cronExpression}
|
|
86
94
|
</code>
|
|
87
95
|
{allowCopy && (
|
|
@@ -131,9 +139,10 @@ function CompactTrigger({
|
|
|
131
139
|
}: CompactTriggerProps) {
|
|
132
140
|
const [open, setOpen] = useState(false);
|
|
133
141
|
const { humanDescription, isValid } = useCronPreview();
|
|
142
|
+
const isSm = useCronSize() === 'sm';
|
|
134
143
|
|
|
135
144
|
return (
|
|
136
|
-
<div className={cn('space-y-1.5', className)}>
|
|
145
|
+
<div className={cn(isSm ? 'space-y-1' : 'space-y-1.5', className)}>
|
|
137
146
|
<Popover open={open} onOpenChange={setOpen}>
|
|
138
147
|
<PopoverTrigger asChild>
|
|
139
148
|
<button
|
|
@@ -142,17 +151,19 @@ function CompactTrigger({
|
|
|
142
151
|
aria-expanded={open}
|
|
143
152
|
disabled={disabled}
|
|
144
153
|
className={cn(
|
|
145
|
-
'flex
|
|
146
|
-
'border-input bg-transparent
|
|
154
|
+
'flex w-full items-center rounded-md border',
|
|
155
|
+
'border-input bg-transparent shadow-xs transition-colors',
|
|
147
156
|
'hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50',
|
|
148
157
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
158
|
+
isSm ? 'h-8 gap-1.5 px-2 text-xs' : 'h-9 gap-2 px-3 text-sm',
|
|
149
159
|
!isValid && 'border-destructive/50'
|
|
150
160
|
)}
|
|
151
161
|
>
|
|
152
162
|
<Calendar
|
|
153
163
|
aria-hidden="true"
|
|
154
164
|
className={cn(
|
|
155
|
-
'
|
|
165
|
+
'shrink-0',
|
|
166
|
+
isSm ? 'h-3.5 w-3.5' : 'h-4 w-4',
|
|
156
167
|
isValid ? 'text-muted-foreground' : 'text-destructive'
|
|
157
168
|
)}
|
|
158
169
|
/>
|
|
@@ -166,12 +177,15 @@ function CompactTrigger({
|
|
|
166
177
|
</span>
|
|
167
178
|
<ChevronsUpDown
|
|
168
179
|
aria-hidden="true"
|
|
169
|
-
className=
|
|
180
|
+
className={cn('shrink-0 opacity-50', isSm ? 'h-3.5 w-3.5' : 'h-4 w-4')}
|
|
170
181
|
/>
|
|
171
182
|
</button>
|
|
172
183
|
</PopoverTrigger>
|
|
173
184
|
<PopoverContent
|
|
174
|
-
className=
|
|
185
|
+
className={cn(
|
|
186
|
+
'w-[var(--radix-popover-trigger-width)]',
|
|
187
|
+
isSm ? 'min-w-[280px] p-2' : 'min-w-[320px] p-3'
|
|
188
|
+
)}
|
|
175
189
|
align="start"
|
|
176
190
|
>
|
|
177
191
|
<ScheduleEditor timeFormat={timeFormat} disabled={disabled} />
|
|
@@ -202,8 +216,10 @@ function InlineScheduler({
|
|
|
202
216
|
allowCopy,
|
|
203
217
|
className,
|
|
204
218
|
}: InlineSchedulerProps) {
|
|
219
|
+
const isSm = useCronSize() === 'sm';
|
|
220
|
+
|
|
205
221
|
return (
|
|
206
|
-
<div className={cn('space-y-3', className)}>
|
|
222
|
+
<div className={cn(isSm ? 'space-y-1.5' : 'space-y-3', className)}>
|
|
207
223
|
<ScheduleEditor timeFormat={timeFormat} disabled={disabled} />
|
|
208
224
|
<InlinePreview showCronExpression={showCronExpression} allowCopy={allowCopy} />
|
|
209
225
|
</div>
|
|
@@ -218,26 +234,35 @@ function InlinePreview({
|
|
|
218
234
|
allowCopy: boolean;
|
|
219
235
|
}) {
|
|
220
236
|
const { humanDescription, isValid } = useCronPreview();
|
|
237
|
+
const isSm = useCronSize() === 'sm';
|
|
221
238
|
|
|
222
239
|
return (
|
|
223
240
|
<div
|
|
224
241
|
role="status"
|
|
225
242
|
aria-live="polite"
|
|
226
243
|
className={cn(
|
|
227
|
-
'flex items-center justify-between gap-2 rounded-md border
|
|
244
|
+
'flex items-center justify-between gap-2 rounded-md border',
|
|
228
245
|
'bg-muted/30 border-border/50',
|
|
246
|
+
isSm ? 'px-2 py-1' : 'px-3 py-2',
|
|
229
247
|
!isValid && 'border-destructive/30 bg-destructive/5'
|
|
230
248
|
)}
|
|
231
249
|
>
|
|
232
|
-
<div className=
|
|
250
|
+
<div className={cn('flex min-w-0 items-center', isSm ? 'gap-1.5' : 'gap-2')}>
|
|
233
251
|
<Calendar
|
|
234
252
|
aria-hidden="true"
|
|
235
253
|
className={cn(
|
|
236
|
-
'
|
|
254
|
+
'shrink-0',
|
|
255
|
+
isSm ? 'h-3 w-3' : 'h-4 w-4',
|
|
237
256
|
isValid ? 'text-primary' : 'text-destructive'
|
|
238
257
|
)}
|
|
239
258
|
/>
|
|
240
|
-
<span
|
|
259
|
+
<span
|
|
260
|
+
className={cn(
|
|
261
|
+
'truncate',
|
|
262
|
+
isSm ? 'text-[11px]' : 'text-sm',
|
|
263
|
+
!isValid && 'text-destructive'
|
|
264
|
+
)}
|
|
265
|
+
>
|
|
241
266
|
{humanDescription}
|
|
242
267
|
</span>
|
|
243
268
|
</div>
|
|
@@ -276,6 +301,7 @@ export function CronScheduler({
|
|
|
276
301
|
disabled = false,
|
|
277
302
|
inline = false,
|
|
278
303
|
placeholder = 'Set a schedule',
|
|
304
|
+
size = 'default',
|
|
279
305
|
className,
|
|
280
306
|
}: CronSchedulerProps) {
|
|
281
307
|
return (
|
|
@@ -283,6 +309,7 @@ export function CronScheduler({
|
|
|
283
309
|
value={value}
|
|
284
310
|
onChange={onChange}
|
|
285
311
|
defaultType={defaultType}
|
|
312
|
+
size={size}
|
|
286
313
|
>
|
|
287
314
|
{inline ? (
|
|
288
315
|
<InlineScheduler
|
|
@@ -10,7 +10,7 @@ import { useState, useEffect } from 'react';
|
|
|
10
10
|
import { Input } from '@djangocfg/ui-core/components';
|
|
11
11
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
12
12
|
import { AlertCircle, CheckCircle2 } from 'lucide-react';
|
|
13
|
-
import { useCronCustom } from '../context/hooks';
|
|
13
|
+
import { useCronCustom, useCronSize } from '../context/hooks';
|
|
14
14
|
import { isValidCron } from '../utils/cron-parser';
|
|
15
15
|
|
|
16
16
|
export interface CustomInputProps {
|
|
@@ -20,6 +20,7 @@ export interface CustomInputProps {
|
|
|
20
20
|
|
|
21
21
|
export function CustomInput({ disabled, className }: CustomInputProps) {
|
|
22
22
|
const { customCron, isValid, setCustomCron } = useCronCustom();
|
|
23
|
+
const isSm = useCronSize() === 'sm';
|
|
23
24
|
const [localValue, setLocalValue] = useState(customCron);
|
|
24
25
|
const [localValid, setLocalValid] = useState(isValid);
|
|
25
26
|
|
|
@@ -39,8 +40,14 @@ export function CustomInput({ disabled, className }: CustomInputProps) {
|
|
|
39
40
|
const showError = !localValid && localValue.trim().length > 0;
|
|
40
41
|
|
|
41
42
|
return (
|
|
42
|
-
<div className={cn('space-y-2', className)}>
|
|
43
|
-
<label
|
|
43
|
+
<div className={cn(isSm ? 'space-y-1.5' : 'space-y-2', className)}>
|
|
44
|
+
<label
|
|
45
|
+
htmlFor="cron-custom-input"
|
|
46
|
+
className={cn(
|
|
47
|
+
'text-muted-foreground',
|
|
48
|
+
isSm ? 'text-xs' : 'text-sm'
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
44
51
|
Cron expression
|
|
45
52
|
</label>
|
|
46
53
|
|
|
@@ -59,16 +66,28 @@ export function CustomInput({ disabled, className }: CustomInputProps) {
|
|
|
59
66
|
aria-invalid={showError}
|
|
60
67
|
aria-describedby={showError ? 'cron-custom-error' : undefined}
|
|
61
68
|
className={cn(
|
|
62
|
-
'font-mono
|
|
69
|
+
'font-mono',
|
|
70
|
+
isSm ? 'text-xs h-8 pr-8' : 'text-base h-11 pr-10',
|
|
63
71
|
showError && 'border-destructive focus-visible:ring-destructive/50'
|
|
64
72
|
)}
|
|
65
73
|
/>
|
|
66
|
-
<div
|
|
74
|
+
<div
|
|
75
|
+
className={cn(
|
|
76
|
+
'absolute top-1/2 -translate-y-1/2',
|
|
77
|
+
isSm ? 'right-2' : 'right-3'
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
67
80
|
{localValue.trim() && (
|
|
68
81
|
localValid ? (
|
|
69
|
-
<CheckCircle2
|
|
82
|
+
<CheckCircle2
|
|
83
|
+
aria-hidden="true"
|
|
84
|
+
className={cn('text-success', isSm ? 'h-4 w-4' : 'h-5 w-5')}
|
|
85
|
+
/>
|
|
70
86
|
) : (
|
|
71
|
-
<AlertCircle
|
|
87
|
+
<AlertCircle
|
|
88
|
+
aria-hidden="true"
|
|
89
|
+
className={cn('text-destructive', isSm ? 'h-4 w-4' : 'h-5 w-5')}
|
|
90
|
+
/>
|
|
72
91
|
)
|
|
73
92
|
)}
|
|
74
93
|
</div>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
11
|
-
import { useCronWeekDays } from '../context/hooks';
|
|
11
|
+
import { useCronWeekDays, useCronSize } from '../context/hooks';
|
|
12
12
|
import type { WeekDay } from '../types';
|
|
13
13
|
|
|
14
14
|
const DAYS: { value: WeekDay; label: string; full: string }[] = [
|
|
@@ -33,6 +33,7 @@ export function DayChips({
|
|
|
33
33
|
className,
|
|
34
34
|
}: DayChipsProps) {
|
|
35
35
|
const { weekDays, toggleWeekDay, setWeekDays } = useCronWeekDays();
|
|
36
|
+
const isSm = useCronSize() === 'sm';
|
|
36
37
|
|
|
37
38
|
// Ensure weekDays is always an array
|
|
38
39
|
const safeWeekDays = Array.isArray(weekDays) ? weekDays : [];
|
|
@@ -54,7 +55,10 @@ export function DayChips({
|
|
|
54
55
|
isWeekendDay,
|
|
55
56
|
className: cn(
|
|
56
57
|
'flex flex-col items-center justify-center',
|
|
57
|
-
'
|
|
58
|
+
'font-medium',
|
|
59
|
+
isSm
|
|
60
|
+
? 'h-6 rounded-md text-[11px] leading-none'
|
|
61
|
+
: 'py-2.5 rounded-lg text-xs',
|
|
58
62
|
'transition-all duration-150',
|
|
59
63
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
|
|
60
64
|
'active:scale-[0.97]',
|
|
@@ -76,9 +80,13 @@ export function DayChips({
|
|
|
76
80
|
];
|
|
77
81
|
|
|
78
82
|
return (
|
|
79
|
-
<div className={cn('space-y-3', className)}>
|
|
83
|
+
<div className={cn(isSm ? 'space-y-1.5' : 'space-y-3', className)}>
|
|
80
84
|
{/* Day Grid - full width */}
|
|
81
|
-
<div
|
|
85
|
+
<div
|
|
86
|
+
className={cn('grid grid-cols-7', isSm ? 'gap-1' : 'gap-1')}
|
|
87
|
+
role="group"
|
|
88
|
+
aria-label="Days of week"
|
|
89
|
+
>
|
|
82
90
|
{dayButtons.map((day) => (
|
|
83
91
|
<button
|
|
84
92
|
key={day.value}
|
|
@@ -96,7 +104,7 @@ export function DayChips({
|
|
|
96
104
|
|
|
97
105
|
{/* Quick Presets */}
|
|
98
106
|
{showPresets && (
|
|
99
|
-
<div className=
|
|
107
|
+
<div className={cn('flex', isSm ? 'gap-1' : 'gap-2')}>
|
|
100
108
|
{presets.map((preset) => (
|
|
101
109
|
<PresetButton
|
|
102
110
|
key={preset.label}
|
|
@@ -104,6 +112,7 @@ export function DayChips({
|
|
|
104
112
|
isActive={preset.isActive}
|
|
105
113
|
onClick={() => setWeekDays(preset.days)}
|
|
106
114
|
disabled={disabled}
|
|
115
|
+
compact={isSm}
|
|
107
116
|
/>
|
|
108
117
|
))}
|
|
109
118
|
</div>
|
|
@@ -117,16 +126,20 @@ interface PresetButtonProps {
|
|
|
117
126
|
isActive: boolean;
|
|
118
127
|
onClick: () => void;
|
|
119
128
|
disabled?: boolean;
|
|
129
|
+
compact?: boolean;
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
function PresetButton({ label, isActive, onClick, disabled }: PresetButtonProps) {
|
|
132
|
+
function PresetButton({ label, isActive, onClick, disabled, compact }: PresetButtonProps) {
|
|
123
133
|
return (
|
|
124
134
|
<button
|
|
125
135
|
type="button"
|
|
126
136
|
disabled={disabled}
|
|
127
137
|
onClick={onClick}
|
|
128
138
|
className={cn(
|
|
129
|
-
'flex-1
|
|
139
|
+
'flex-1 rounded-md font-medium',
|
|
140
|
+
compact
|
|
141
|
+
? 'h-6 px-1.5 text-[11px] leading-none'
|
|
142
|
+
: 'px-3 py-1.5 text-xs',
|
|
130
143
|
'transition-colors duration-150',
|
|
131
144
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
|
|
132
145
|
isActive
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
11
|
-
import { useCronMonthDays } from '../context/hooks';
|
|
11
|
+
import { useCronMonthDays, useCronSize } from '../context/hooks';
|
|
12
12
|
import type { MonthDay } from '../types';
|
|
13
13
|
|
|
14
14
|
// Generate days 1-31
|
|
@@ -29,6 +29,7 @@ export function MonthDayGrid({
|
|
|
29
29
|
className,
|
|
30
30
|
}: MonthDayGridProps) {
|
|
31
31
|
const { monthDays, toggleMonthDay, setMonthDays } = useCronMonthDays();
|
|
32
|
+
const isSm = useCronSize() === 'sm';
|
|
32
33
|
|
|
33
34
|
// Ensure monthDays is always an array
|
|
34
35
|
const safeMonthDays = Array.isArray(monthDays) ? monthDays : [];
|
|
@@ -61,8 +62,11 @@ export function MonthDayGrid({
|
|
|
61
62
|
day: day as MonthDay,
|
|
62
63
|
isSelected,
|
|
63
64
|
className: cn(
|
|
64
|
-
'
|
|
65
|
-
'
|
|
65
|
+
'flex items-center justify-center',
|
|
66
|
+
'font-medium',
|
|
67
|
+
isSm
|
|
68
|
+
? 'h-7 rounded-[5px] text-[10px] leading-none'
|
|
69
|
+
: 'aspect-square rounded-lg text-sm',
|
|
66
70
|
'transition-all duration-150',
|
|
67
71
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
|
|
68
72
|
'active:scale-[0.95]',
|
|
@@ -82,10 +86,10 @@ export function MonthDayGrid({
|
|
|
82
86
|
const selectionCount = safeMonthDays.length;
|
|
83
87
|
|
|
84
88
|
return (
|
|
85
|
-
<div className={cn('space-y-3', className)}>
|
|
89
|
+
<div className={cn(isSm ? 'space-y-1.5' : 'space-y-3', className)}>
|
|
86
90
|
{/* Quick Presets */}
|
|
87
91
|
{showPresets && (
|
|
88
|
-
<div className=
|
|
92
|
+
<div className={cn('flex', isSm ? 'gap-1' : 'gap-2')}>
|
|
89
93
|
{presets.map((preset) => (
|
|
90
94
|
<PresetButton
|
|
91
95
|
key={preset.label}
|
|
@@ -93,16 +97,27 @@ export function MonthDayGrid({
|
|
|
93
97
|
isActive={preset.isActive}
|
|
94
98
|
onClick={() => setMonthDays(preset.days)}
|
|
95
99
|
disabled={disabled}
|
|
100
|
+
compact={isSm}
|
|
96
101
|
/>
|
|
97
102
|
))}
|
|
98
103
|
</div>
|
|
99
104
|
)}
|
|
100
105
|
|
|
101
106
|
{/* Calendar Grid - full width */}
|
|
102
|
-
<div
|
|
107
|
+
<div
|
|
108
|
+
className={cn('grid grid-cols-7', isSm ? 'gap-0.5' : 'gap-1')}
|
|
109
|
+
role="group"
|
|
110
|
+
aria-label="Days of month"
|
|
111
|
+
>
|
|
103
112
|
{gridCells.map((cell) => {
|
|
104
113
|
if (cell.type === 'empty') {
|
|
105
|
-
return
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
key={cell.key}
|
|
117
|
+
aria-hidden="true"
|
|
118
|
+
className={isSm ? 'h-7' : 'aspect-square'}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
return (
|
|
@@ -123,7 +138,13 @@ export function MonthDayGrid({
|
|
|
123
138
|
|
|
124
139
|
{/* Selection hint */}
|
|
125
140
|
{selectionCount > 1 && (
|
|
126
|
-
<p
|
|
141
|
+
<p
|
|
142
|
+
aria-live="polite"
|
|
143
|
+
className={cn(
|
|
144
|
+
'text-muted-foreground text-center',
|
|
145
|
+
isSm ? 'text-[10px]' : 'text-xs'
|
|
146
|
+
)}
|
|
147
|
+
>
|
|
127
148
|
{selectionCount} days selected
|
|
128
149
|
</p>
|
|
129
150
|
)}
|
|
@@ -136,16 +157,20 @@ interface PresetButtonProps {
|
|
|
136
157
|
isActive: boolean;
|
|
137
158
|
onClick: () => void;
|
|
138
159
|
disabled?: boolean;
|
|
160
|
+
compact?: boolean;
|
|
139
161
|
}
|
|
140
162
|
|
|
141
|
-
function PresetButton({ label, isActive, onClick, disabled }: PresetButtonProps) {
|
|
163
|
+
function PresetButton({ label, isActive, onClick, disabled, compact }: PresetButtonProps) {
|
|
142
164
|
return (
|
|
143
165
|
<button
|
|
144
166
|
type="button"
|
|
145
167
|
disabled={disabled}
|
|
146
168
|
onClick={onClick}
|
|
147
169
|
className={cn(
|
|
148
|
-
'flex-1
|
|
170
|
+
'flex-1 rounded-md font-medium',
|
|
171
|
+
compact
|
|
172
|
+
? 'h-6 px-1.5 text-[11px] leading-none'
|
|
173
|
+
: 'px-3 py-1.5 text-xs',
|
|
149
174
|
'transition-colors duration-150',
|
|
150
175
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
|
|
151
176
|
isActive
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { useState, useCallback, useMemo } from 'react';
|
|
11
11
|
import { Calendar, Copy, Check, ArrowRight } from 'lucide-react';
|
|
12
12
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
13
|
-
import { useCronPreview } from '../context/hooks';
|
|
13
|
+
import { useCronPreview, useCronSize } from '../context/hooks';
|
|
14
14
|
import { CronCheatsheet } from './CronCheatsheet';
|
|
15
15
|
|
|
16
16
|
export interface SchedulePreviewProps {
|
|
@@ -31,6 +31,7 @@ export function SchedulePreview({
|
|
|
31
31
|
className,
|
|
32
32
|
}: SchedulePreviewProps) {
|
|
33
33
|
const { cronExpression, humanDescription, isValid, initialValue } = useCronPreview();
|
|
34
|
+
const isSm = useCronSize() === 'sm';
|
|
34
35
|
|
|
35
36
|
// Check if value has changed from initial
|
|
36
37
|
const hasChanged = useMemo(
|
|
@@ -54,7 +55,8 @@ export function SchedulePreview({
|
|
|
54
55
|
role="status"
|
|
55
56
|
aria-live="polite"
|
|
56
57
|
className={cn(
|
|
57
|
-
'
|
|
58
|
+
'rounded-lg border',
|
|
59
|
+
isSm ? 'px-2 py-1.5' : 'px-3 py-2.5',
|
|
58
60
|
'bg-muted/30 border-border/50',
|
|
59
61
|
!isValid && 'border-destructive/30 bg-destructive/5',
|
|
60
62
|
className
|
|
@@ -62,17 +64,18 @@ export function SchedulePreview({
|
|
|
62
64
|
>
|
|
63
65
|
{/* Human description */}
|
|
64
66
|
<div className="flex items-center justify-between gap-2">
|
|
65
|
-
<div className=
|
|
67
|
+
<div className={cn('flex items-center min-w-0', isSm ? 'gap-1.5' : 'gap-2')}>
|
|
66
68
|
<Calendar
|
|
67
69
|
aria-hidden="true"
|
|
68
70
|
className={cn(
|
|
69
|
-
'
|
|
71
|
+
'shrink-0',
|
|
72
|
+
isSm ? 'h-3.5 w-3.5' : 'h-4 w-4',
|
|
70
73
|
isValid ? 'text-primary' : 'text-destructive'
|
|
71
74
|
)}
|
|
72
75
|
/>
|
|
73
76
|
<span
|
|
74
77
|
className={cn(
|
|
75
|
-
'text-sm',
|
|
78
|
+
isSm ? 'text-xs' : 'text-sm',
|
|
76
79
|
isValid ? 'text-foreground' : 'text-destructive'
|
|
77
80
|
)}
|
|
78
81
|
>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { Tabs, TabsList, TabsTrigger } from '@djangocfg/ui-core/components';
|
|
11
11
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
12
|
-
import { useCronType } from '../context/hooks';
|
|
12
|
+
import { useCronType, useCronSize } from '../context/hooks';
|
|
13
13
|
import type { ScheduleType } from '../types';
|
|
14
14
|
|
|
15
15
|
const SCHEDULE_TYPES: { value: ScheduleType; label: string }[] = [
|
|
@@ -29,9 +29,13 @@ export function ScheduleTypeSelector({
|
|
|
29
29
|
className,
|
|
30
30
|
}: ScheduleTypeSelectorProps) {
|
|
31
31
|
const { type, setType } = useCronType();
|
|
32
|
+
const isSm = useCronSize() === 'sm';
|
|
32
33
|
|
|
33
34
|
const triggerClassName = cn(
|
|
34
|
-
'
|
|
35
|
+
'font-medium',
|
|
36
|
+
isSm
|
|
37
|
+
? 'text-[11px] leading-none px-1 py-0.5 h-6 rounded-[5px]'
|
|
38
|
+
: 'text-xs px-2 py-1.5',
|
|
35
39
|
'data-[state=active]:shadow-sm',
|
|
36
40
|
'transition-all duration-150'
|
|
37
41
|
);
|
|
@@ -49,7 +53,12 @@ export function ScheduleTypeSelector({
|
|
|
49
53
|
onValueChange={(v) => setType(v as ScheduleType)}
|
|
50
54
|
className={cn('w-full', className)}
|
|
51
55
|
>
|
|
52
|
-
<TabsList
|
|
56
|
+
<TabsList
|
|
57
|
+
className={cn(
|
|
58
|
+
'grid w-full grid-cols-4',
|
|
59
|
+
isSm ? 'h-7 p-0.5 gap-0.5' : 'h-9 p-0.5'
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
53
62
|
{triggers.map((trigger) => (
|
|
54
63
|
<TabsTrigger
|
|
55
64
|
key={trigger.value}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from '@djangocfg/ui-core/components';
|
|
16
16
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
17
17
|
import { Clock } from 'lucide-react';
|
|
18
|
-
import { useCronTime } from '../context/hooks';
|
|
18
|
+
import { useCronTime, useCronSize } from '../context/hooks';
|
|
19
19
|
|
|
20
20
|
const HOURS = Array.from({ length: 24 }, (_, i) => i);
|
|
21
21
|
const MINUTES = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
|
|
@@ -32,6 +32,7 @@ export function TimeSelector({
|
|
|
32
32
|
className,
|
|
33
33
|
}: TimeSelectorProps) {
|
|
34
34
|
const { hour, minute, setTime } = useCronTime();
|
|
35
|
+
const isSm = useCronSize() === 'sm';
|
|
35
36
|
|
|
36
37
|
const is24h = format === '24h';
|
|
37
38
|
const displayHour = is24h ? hour : (hour % 12) || 12;
|
|
@@ -60,28 +61,50 @@ export function TimeSelector({
|
|
|
60
61
|
|
|
61
62
|
const hours = is24h ? HOURS : Array.from({ length: 12 }, (_, i) => i + 1);
|
|
62
63
|
|
|
64
|
+
// Compact-aware class fragments
|
|
65
|
+
const triggerClassName = cn(
|
|
66
|
+
isSm ? 'w-[58px] h-7 px-2 text-xs' : 'w-[70px] h-9'
|
|
67
|
+
);
|
|
68
|
+
const itemClassName = isSm ? 'text-xs' : undefined;
|
|
69
|
+
|
|
63
70
|
return (
|
|
64
|
-
<div
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
<div
|
|
72
|
+
className={cn(
|
|
73
|
+
'flex items-center',
|
|
74
|
+
isSm ? 'gap-2' : 'gap-3',
|
|
75
|
+
className
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
<div
|
|
79
|
+
className={cn(
|
|
80
|
+
'flex items-center text-muted-foreground',
|
|
81
|
+
isSm ? 'gap-1.5' : 'gap-2'
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
<Clock
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
className={isSm ? 'h-3.5 w-3.5' : 'h-4 w-4'}
|
|
87
|
+
/>
|
|
88
|
+
<span className={isSm ? 'text-xs' : 'text-sm'}>Run at</span>
|
|
68
89
|
</div>
|
|
69
90
|
|
|
70
|
-
<div
|
|
91
|
+
<div
|
|
92
|
+
className={cn('flex items-center flex-1', isSm ? 'gap-1' : 'gap-1.5')}
|
|
93
|
+
>
|
|
71
94
|
{/* Hour */}
|
|
72
95
|
<Select
|
|
73
96
|
value={displayHour.toString()}
|
|
74
97
|
onValueChange={handleHourChange}
|
|
75
98
|
disabled={disabled}
|
|
76
99
|
>
|
|
77
|
-
<SelectTrigger className=
|
|
100
|
+
<SelectTrigger className={triggerClassName}>
|
|
78
101
|
<SelectValue>
|
|
79
102
|
{displayHour.toString().padStart(2, '0')}
|
|
80
103
|
</SelectValue>
|
|
81
104
|
</SelectTrigger>
|
|
82
105
|
<SelectContent className="max-h-48">
|
|
83
106
|
{hours.map((h) => (
|
|
84
|
-
<SelectItem key={h} value={h.toString()}>
|
|
107
|
+
<SelectItem key={h} value={h.toString()} className={itemClassName}>
|
|
85
108
|
{h.toString().padStart(2, '0')}
|
|
86
109
|
</SelectItem>
|
|
87
110
|
))}
|
|
@@ -96,14 +119,14 @@ export function TimeSelector({
|
|
|
96
119
|
onValueChange={handleMinuteChange}
|
|
97
120
|
disabled={disabled}
|
|
98
121
|
>
|
|
99
|
-
<SelectTrigger className=
|
|
122
|
+
<SelectTrigger className={triggerClassName}>
|
|
100
123
|
<SelectValue>
|
|
101
124
|
{minute.toString().padStart(2, '0')}
|
|
102
125
|
</SelectValue>
|
|
103
126
|
</SelectTrigger>
|
|
104
127
|
<SelectContent className="max-h-48">
|
|
105
128
|
{MINUTES.map((m) => (
|
|
106
|
-
<SelectItem key={m} value={m.toString()}>
|
|
129
|
+
<SelectItem key={m} value={m.toString()} className={itemClassName}>
|
|
107
130
|
{m.toString().padStart(2, '0')}
|
|
108
131
|
</SelectItem>
|
|
109
132
|
))}
|
|
@@ -117,12 +140,12 @@ export function TimeSelector({
|
|
|
117
140
|
onValueChange={handlePeriodChange}
|
|
118
141
|
disabled={disabled}
|
|
119
142
|
>
|
|
120
|
-
<SelectTrigger className=
|
|
143
|
+
<SelectTrigger className={triggerClassName}>
|
|
121
144
|
<SelectValue />
|
|
122
145
|
</SelectTrigger>
|
|
123
146
|
<SelectContent>
|
|
124
|
-
<SelectItem value="AM">AM</SelectItem>
|
|
125
|
-
<SelectItem value="PM">PM</SelectItem>
|
|
147
|
+
<SelectItem value="AM" className={itemClassName}>AM</SelectItem>
|
|
148
|
+
<SelectItem value="PM" className={itemClassName}>PM</SelectItem>
|
|
126
149
|
</SelectContent>
|
|
127
150
|
</Select>
|
|
128
151
|
)}
|
|
@@ -57,6 +57,7 @@ export function CronSchedulerProvider({
|
|
|
57
57
|
value,
|
|
58
58
|
onChange,
|
|
59
59
|
defaultType = 'daily',
|
|
60
|
+
size = 'default',
|
|
60
61
|
}: CronSchedulerProviderProps) {
|
|
61
62
|
// Track if this is initial mount to avoid calling onChange on mount
|
|
62
63
|
const isInitialMount = useRef(true);
|
|
@@ -203,6 +204,8 @@ export function CronSchedulerProvider({
|
|
|
203
204
|
() => ({
|
|
204
205
|
// State
|
|
205
206
|
...state,
|
|
207
|
+
// Config
|
|
208
|
+
size,
|
|
206
209
|
// Computed
|
|
207
210
|
cronExpression,
|
|
208
211
|
humanDescription,
|
|
@@ -219,6 +222,7 @@ export function CronSchedulerProvider({
|
|
|
219
222
|
}),
|
|
220
223
|
[
|
|
221
224
|
state,
|
|
225
|
+
size,
|
|
222
226
|
cronExpression,
|
|
223
227
|
humanDescription,
|
|
224
228
|
initialValue,
|
|
@@ -78,6 +78,14 @@ export function useCronPreview() {
|
|
|
78
78
|
);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Hook for the configured visual density (`default` | `sm`).
|
|
83
|
+
* Returns a stable value — never causes re-renders by itself.
|
|
84
|
+
*/
|
|
85
|
+
export function useCronSize() {
|
|
86
|
+
return useCronSchedulerContext().size;
|
|
87
|
+
}
|
|
88
|
+
|
|
81
89
|
/**
|
|
82
90
|
* Full context access
|
|
83
91
|
* Use sparingly - causes re-render on any state change
|