@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.
Files changed (83) hide show
  1. package/package.json +13 -13
  2. package/src/{tools/Chat/highlight → lib/browser-bridge}/README.md +46 -18
  3. package/src/lib/browser-bridge/commands/chat.ts +42 -0
  4. package/src/lib/browser-bridge/commands/highlight.ts +70 -0
  5. package/src/lib/browser-bridge/commands/index.ts +15 -0
  6. package/src/lib/browser-bridge/commands/inspect.ts +31 -0
  7. package/src/lib/browser-bridge/commands/scroll.ts +31 -0
  8. package/src/lib/browser-bridge/commands/write.ts +45 -0
  9. package/src/lib/browser-bridge/directive-bus.ts +120 -0
  10. package/src/lib/browser-bridge/index.ts +56 -0
  11. package/src/lib/browser-bridge/logger.ts +27 -0
  12. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/HighlightOverlay.tsx +14 -0
  13. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/__tests__/HighlightOverlay.test.tsx +52 -0
  14. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/__tests__/resolveRef.test.ts +39 -0
  15. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/index.ts +8 -5
  16. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/resolveRef.ts +5 -0
  17. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/useHighlightTargets.ts +58 -27
  18. package/src/lib/browser-bridge/overlay/waitForVisible.ts +70 -0
  19. package/src/lib/browser-bridge/registry.ts +41 -0
  20. package/src/lib/browser-bridge/setBridgeResolver.ts +42 -0
  21. package/src/lib/browser-bridge/window.ts +76 -0
  22. package/src/lib/page-snapshot/capture/walk.ts +13 -5
  23. package/src/lib/page-snapshot/engine.ts +9 -4
  24. package/src/lib/page-snapshot/index.ts +5 -0
  25. package/src/lib/page-snapshot/react/provider.tsx +70 -3
  26. package/src/lib/page-snapshot/react/use-page-snapshot.ts +10 -0
  27. package/src/lib/page-snapshot/refs/__tests__/locator.test.ts +94 -0
  28. package/src/lib/page-snapshot/refs/__tests__/registry.test.ts +59 -3
  29. package/src/lib/page-snapshot/refs/locator.ts +218 -0
  30. package/src/lib/page-snapshot/refs/registry.ts +29 -14
  31. package/src/tools/Chat/README.md +1 -1
  32. package/src/tools/Chat/composer/AttachContext.tsx +22 -0
  33. package/src/tools/Chat/composer/Composer.tsx +108 -6
  34. package/src/tools/Chat/composer/ComposerMenuButton.tsx +39 -2
  35. package/src/tools/Chat/composer/fileToAttachment.ts +53 -0
  36. package/src/tools/Chat/composer/index.ts +16 -1
  37. package/src/tools/Chat/composer/types.ts +71 -0
  38. package/src/tools/Chat/composer/useComposerAttach.tsx +218 -0
  39. package/src/tools/Chat/constants.ts +24 -1
  40. package/src/tools/Chat/context/ChatProvider.tsx +17 -2
  41. package/src/tools/Chat/core/logger.ts +15 -2
  42. package/src/tools/Chat/hooks/useChat.ts +32 -0
  43. package/src/tools/Chat/hooks/useChatComposer.ts +13 -0
  44. package/src/tools/Chat/index.ts +34 -2
  45. package/src/tools/Chat/launcher/ChatDock.tsx +13 -3
  46. package/src/tools/Chat/launcher/ChatFAB.tsx +4 -2
  47. package/src/tools/Chat/launcher/ChatGreeting.tsx +3 -2
  48. package/src/tools/Chat/launcher/ChatLauncher.tsx +42 -7
  49. package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +3 -2
  50. package/src/tools/Chat/launcher/header/ChatHeader.tsx +2 -0
  51. package/src/tools/Chat/launcher/header/ChatHeaderActionButton.tsx +2 -0
  52. package/src/tools/Chat/launcher/header/ChatHeaderLanguageButton.tsx +2 -2
  53. package/src/tools/Chat/launcher/header/HeaderSlots.tsx +16 -9
  54. package/src/tools/Chat/lazy.tsx +34 -2
  55. package/src/tools/Chat/messages/MessageBubble.tsx +1 -1
  56. package/src/tools/Chat/public.ts +17 -0
  57. package/src/tools/Chat/settings/README.md +87 -0
  58. package/src/tools/Chat/settings/__tests__/useChatSettings.test.tsx +84 -0
  59. package/src/tools/Chat/settings/__tests__/useLocalStorage.test.tsx +138 -0
  60. package/src/tools/Chat/settings/index.ts +23 -0
  61. package/src/tools/Chat/settings/types.ts +108 -0
  62. package/src/tools/Chat/settings/useChatSettings.ts +168 -0
  63. package/src/tools/Chat/types/events.ts +50 -0
  64. package/src/tools/Chat/types/index.ts +1 -1
  65. package/src/tools/Chat/types/message.ts +5 -0
  66. package/src/tools/CronScheduler/CronScheduler.client.tsx +42 -15
  67. package/src/tools/CronScheduler/components/CustomInput.tsx +26 -7
  68. package/src/tools/CronScheduler/components/DayChips.tsx +20 -7
  69. package/src/tools/CronScheduler/components/MonthDayGrid.tsx +35 -10
  70. package/src/tools/CronScheduler/components/SchedulePreview.tsx +8 -5
  71. package/src/tools/CronScheduler/components/ScheduleTypeSelector.tsx +12 -3
  72. package/src/tools/CronScheduler/components/TimeSelector.tsx +36 -13
  73. package/src/tools/CronScheduler/context/CronSchedulerContext.tsx +4 -0
  74. package/src/tools/CronScheduler/context/hooks.ts +8 -0
  75. package/src/tools/CronScheduler/context/index.ts +1 -0
  76. package/src/tools/CronScheduler/index.tsx +2 -0
  77. package/src/tools/CronScheduler/lazy.tsx +1 -0
  78. package/src/tools/CronScheduler/types/index.ts +18 -1
  79. package/src/tools/Map/lazy.tsx +11 -4
  80. package/src/tools/Uploader/hooks/useClipboardPaste.ts +3 -1
  81. package/src/tools/index.ts +2 -0
  82. /package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/SpotlightCanvas.tsx +0 -0
  83. /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="space-y-3">
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 className="font-mono text-xs text-muted-foreground">
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 h-9 w-full items-center gap-2 rounded-md border px-3',
146
- 'border-input bg-transparent text-sm shadow-xs transition-colors',
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
- 'h-4 w-4 shrink-0',
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="h-4 w-4 shrink-0 opacity-50"
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="w-[var(--radix-popover-trigger-width)] min-w-[320px] p-3"
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 px-3 py-2',
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="flex min-w-0 items-center gap-2">
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
- 'h-4 w-4 shrink-0',
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 className={cn('truncate text-sm', !isValid && 'text-destructive')}>
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 htmlFor="cron-custom-input" className="text-sm text-muted-foreground">
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 text-base pr-10 h-11',
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 className="absolute right-3 top-1/2 -translate-y-1/2">
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 aria-hidden="true" className="h-5 w-5 text-success" />
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 aria-hidden="true" className="h-5 w-5 text-destructive" />
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
- 'py-2.5 rounded-lg text-xs font-medium',
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 className="grid grid-cols-7 gap-1" role="group" aria-label="Days of week">
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="flex gap-2">
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 px-3 py-1.5 rounded-md text-xs font-medium',
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
- 'aspect-square flex items-center justify-center',
65
- 'rounded-lg text-sm font-medium',
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="flex gap-2">
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 className="grid grid-cols-7 gap-1" role="group" aria-label="Days of month">
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 <div key={cell.key} aria-hidden="true" className="aspect-square" />;
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 aria-live="polite" className="text-xs text-muted-foreground text-center">
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 px-3 py-1.5 rounded-md text-xs font-medium',
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
- 'px-3 py-2.5 rounded-lg border',
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="flex items-center gap-2 min-w-0">
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
- 'h-4 w-4 shrink-0',
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
- 'text-xs font-medium px-2 py-1.5',
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 className="grid w-full grid-cols-4 h-9 p-0.5">
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 className={cn('flex items-center gap-3', className)}>
65
- <div className="flex items-center gap-2 text-muted-foreground">
66
- <Clock aria-hidden="true" className="h-4 w-4" />
67
- <span className="text-sm">Run at</span>
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 className="flex items-center gap-1.5 flex-1">
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="w-[70px] h-9">
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="w-[70px] h-9">
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="w-[70px] h-9">
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
@@ -14,5 +14,6 @@ export {
14
14
  useCronMonthDays,
15
15
  useCronCustom,
16
16
  useCronPreview,
17
+ useCronSize,
17
18
  useCronScheduler,
18
19
  } from './hooks';