@djangocfg/ui-tools 2.1.129 → 2.1.131

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 (38) hide show
  1. package/README.md +57 -1
  2. package/dist/CronScheduler.client-5UEBG4EY.mjs +67 -0
  3. package/dist/CronScheduler.client-5UEBG4EY.mjs.map +1 -0
  4. package/dist/CronScheduler.client-ZDNFXYWJ.cjs +72 -0
  5. package/dist/CronScheduler.client-ZDNFXYWJ.cjs.map +1 -0
  6. package/dist/chunk-JFGLA6DT.cjs +1013 -0
  7. package/dist/chunk-JFGLA6DT.cjs.map +1 -0
  8. package/dist/chunk-MQDWUBVX.mjs +993 -0
  9. package/dist/chunk-MQDWUBVX.mjs.map +1 -0
  10. package/dist/index.cjs +109 -0
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +280 -1
  13. package/dist/index.d.ts +280 -1
  14. package/dist/index.mjs +32 -1
  15. package/dist/index.mjs.map +1 -1
  16. package/package.json +6 -6
  17. package/src/index.ts +5 -0
  18. package/src/tools/CronScheduler/CronScheduler.client.tsx +140 -0
  19. package/src/tools/CronScheduler/CronScheduler.story.tsx +220 -0
  20. package/src/tools/CronScheduler/components/CronCheatsheet.tsx +101 -0
  21. package/src/tools/CronScheduler/components/CustomInput.tsx +67 -0
  22. package/src/tools/CronScheduler/components/DayChips.tsx +130 -0
  23. package/src/tools/CronScheduler/components/MonthDayGrid.tsx +143 -0
  24. package/src/tools/CronScheduler/components/SchedulePreview.tsx +103 -0
  25. package/src/tools/CronScheduler/components/ScheduleTypeSelector.tsx +57 -0
  26. package/src/tools/CronScheduler/components/TimeSelector.tsx +132 -0
  27. package/src/tools/CronScheduler/components/index.ts +24 -0
  28. package/src/tools/CronScheduler/context/CronSchedulerContext.tsx +237 -0
  29. package/src/tools/CronScheduler/context/hooks.ts +86 -0
  30. package/src/tools/CronScheduler/context/index.ts +18 -0
  31. package/src/tools/CronScheduler/index.tsx +91 -0
  32. package/src/tools/CronScheduler/lazy.tsx +67 -0
  33. package/src/tools/CronScheduler/types/index.ts +112 -0
  34. package/src/tools/CronScheduler/utils/cron-builder.ts +100 -0
  35. package/src/tools/CronScheduler/utils/cron-humanize.ts +218 -0
  36. package/src/tools/CronScheduler/utils/cron-parser.ts +188 -0
  37. package/src/tools/CronScheduler/utils/index.ts +12 -0
  38. package/src/tools/index.ts +36 -0
@@ -0,0 +1,140 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * CronScheduler Client Component
5
+ *
6
+ * Compact cron expression builder following Apple HIG principles.
7
+ * Uses context-based architecture for state management.
8
+ */
9
+
10
+ import { cn } from '@djangocfg/ui-core/lib';
11
+ import { CronSchedulerProvider } from './context/CronSchedulerContext';
12
+ import { useCronType } from './context/hooks';
13
+ import {
14
+ ScheduleTypeSelector,
15
+ TimeSelector,
16
+ DayChips,
17
+ MonthDayGrid,
18
+ CustomInput,
19
+ SchedulePreview,
20
+ } from './components';
21
+ import type { CronSchedulerProps } from './types';
22
+
23
+ // ============================================================================
24
+ // Inner Component (uses context)
25
+ // ============================================================================
26
+
27
+ interface CronSchedulerInnerProps {
28
+ showPreview: boolean;
29
+ showCronExpression: boolean;
30
+ allowCopy: boolean;
31
+ timeFormat: '12h' | '24h';
32
+ disabled: boolean;
33
+ className?: string;
34
+ }
35
+
36
+ function CronSchedulerInner({
37
+ showPreview,
38
+ showCronExpression,
39
+ allowCopy,
40
+ timeFormat,
41
+ disabled,
42
+ className,
43
+ }: CronSchedulerInnerProps) {
44
+ const { type } = useCronType();
45
+
46
+ return (
47
+ <div className={cn('space-y-3', className)}>
48
+ {/* Schedule Type Selector */}
49
+ <ScheduleTypeSelector disabled={disabled} />
50
+
51
+ {/* Time Selector (shown for daily, weekly, monthly) */}
52
+ {type !== 'custom' && (
53
+ <TimeSelector format={timeFormat} disabled={disabled} />
54
+ )}
55
+
56
+ {/* Day Chips (weekly only) */}
57
+ {type === 'weekly' && (
58
+ <DayChips disabled={disabled} showPresets />
59
+ )}
60
+
61
+ {/* Month Day Grid (monthly only) */}
62
+ {type === 'monthly' && (
63
+ <MonthDayGrid disabled={disabled} showPresets />
64
+ )}
65
+
66
+ {/* Custom Input (custom only) */}
67
+ {type === 'custom' && (
68
+ <CustomInput disabled={disabled} />
69
+ )}
70
+
71
+ {/* Preview - always show cron expression */}
72
+ {showPreview && (
73
+ <SchedulePreview
74
+ showCronExpression
75
+ allowCopy={allowCopy}
76
+ />
77
+ )}
78
+ </div>
79
+ );
80
+ }
81
+
82
+ // ============================================================================
83
+ // Main Component (with Provider)
84
+ // ============================================================================
85
+
86
+ /**
87
+ * CronScheduler - Compact cron expression builder
88
+ *
89
+ * A user-friendly interface for creating cron schedules without
90
+ * needing to know cron syntax. Follows Apple HIG design principles.
91
+ *
92
+ * @example
93
+ * // Basic usage
94
+ * <CronScheduler
95
+ * value={cronExpression}
96
+ * onChange={setCronExpression}
97
+ * />
98
+ *
99
+ * @example
100
+ * // With all options
101
+ * <CronScheduler
102
+ * value="0 9 * * 1-5"
103
+ * onChange={handleChange}
104
+ * defaultType="weekly"
105
+ * showPreview
106
+ * showCronExpression
107
+ * allowCopy
108
+ * timeFormat="24h"
109
+ * />
110
+ */
111
+ export function CronScheduler({
112
+ value,
113
+ onChange,
114
+ defaultType = 'daily',
115
+ showPreview = true,
116
+ showCronExpression = false,
117
+ allowCopy = false,
118
+ timeFormat = '24h',
119
+ disabled = false,
120
+ className,
121
+ }: CronSchedulerProps) {
122
+ return (
123
+ <CronSchedulerProvider
124
+ value={value}
125
+ onChange={onChange}
126
+ defaultType={defaultType}
127
+ >
128
+ <CronSchedulerInner
129
+ showPreview={showPreview}
130
+ showCronExpression={showCronExpression}
131
+ allowCopy={allowCopy}
132
+ timeFormat={timeFormat}
133
+ disabled={disabled}
134
+ className={className}
135
+ />
136
+ </CronSchedulerProvider>
137
+ );
138
+ }
139
+
140
+ export default CronScheduler;
@@ -0,0 +1,220 @@
1
+ import { useState } from 'react';
2
+ import { defineStory, useBoolean, useSelect } from '@djangocfg/playground';
3
+ import { CronScheduler } from './index';
4
+
5
+ export default defineStory({
6
+ title: 'Tools/Cron Scheduler',
7
+ component: CronScheduler,
8
+ description: 'Compact cron expression builder with Apple HIG-style UI.',
9
+ });
10
+
11
+ export const Interactive = () => {
12
+ const [cron, setCron] = useState('0 9 * * 1-5');
13
+
14
+ const [defaultType] = useSelect('defaultType', {
15
+ options: ['daily', 'weekly', 'monthly', 'custom'] as const,
16
+ defaultValue: 'weekly',
17
+ label: 'Default Type',
18
+ description: 'Initial schedule type',
19
+ });
20
+
21
+ const [showPreview] = useBoolean('showPreview', {
22
+ defaultValue: true,
23
+ label: 'Show Preview',
24
+ description: 'Show human-readable preview',
25
+ });
26
+
27
+ const [showCronExpression] = useBoolean('showCronExpression', {
28
+ defaultValue: true,
29
+ label: 'Show Cron Expression',
30
+ description: 'Show raw cron expression in preview',
31
+ });
32
+
33
+ const [allowCopy] = useBoolean('allowCopy', {
34
+ defaultValue: true,
35
+ label: 'Allow Copy',
36
+ description: 'Enable copy to clipboard',
37
+ });
38
+
39
+ const [timeFormat] = useSelect('timeFormat', {
40
+ options: ['24h', '12h'] as const,
41
+ defaultValue: '24h',
42
+ label: 'Time Format',
43
+ description: 'Time display format',
44
+ });
45
+
46
+ const [disabled] = useBoolean('disabled', {
47
+ defaultValue: false,
48
+ label: 'Disabled',
49
+ description: 'Disable all interactions',
50
+ });
51
+
52
+ return (
53
+ <div className="w-[400px]">
54
+ <CronScheduler
55
+ value={cron}
56
+ onChange={setCron}
57
+ defaultType={defaultType}
58
+ showPreview={showPreview}
59
+ showCronExpression={showCronExpression}
60
+ allowCopy={allowCopy}
61
+ timeFormat={timeFormat}
62
+ disabled={disabled}
63
+ />
64
+ </div>
65
+ );
66
+ };
67
+
68
+ export const Daily = () => {
69
+ const [cron, setCron] = useState('0 9 * * *');
70
+
71
+ return (
72
+ <div className="w-[400px]">
73
+ <CronScheduler
74
+ value={cron}
75
+ onChange={setCron}
76
+ defaultType="daily"
77
+ showPreview
78
+ />
79
+ </div>
80
+ );
81
+ };
82
+
83
+ export const Weekly = () => {
84
+ const [cron, setCron] = useState('30 14 * * 1,3,5');
85
+
86
+ return (
87
+ <div className="w-[400px]">
88
+ <CronScheduler
89
+ value={cron}
90
+ onChange={setCron}
91
+ defaultType="weekly"
92
+ showPreview
93
+ />
94
+ </div>
95
+ );
96
+ };
97
+
98
+ export const Monthly = () => {
99
+ const [cron, setCron] = useState('0 9 1,15 * *');
100
+
101
+ return (
102
+ <div className="w-[400px]">
103
+ <CronScheduler
104
+ value={cron}
105
+ onChange={setCron}
106
+ defaultType="monthly"
107
+ showPreview
108
+ />
109
+ </div>
110
+ );
111
+ };
112
+
113
+ export const Custom = () => {
114
+ const [cron, setCron] = useState('*/15 9-17 * * 1-5');
115
+
116
+ return (
117
+ <div className="w-[400px]">
118
+ <CronScheduler
119
+ value={cron}
120
+ onChange={setCron}
121
+ defaultType="custom"
122
+ showPreview
123
+ showCronExpression
124
+ />
125
+ </div>
126
+ );
127
+ };
128
+
129
+ export const WithCronExpression = () => {
130
+ const [cron, setCron] = useState('0 9 * * 1-5');
131
+
132
+ return (
133
+ <div className="w-[400px]">
134
+ <CronScheduler
135
+ value={cron}
136
+ onChange={setCron}
137
+ showPreview
138
+ showCronExpression
139
+ allowCopy
140
+ />
141
+ </div>
142
+ );
143
+ };
144
+
145
+ export const TwelveHourFormat = () => {
146
+ const [cron, setCron] = useState('0 14 * * *');
147
+
148
+ return (
149
+ <div className="w-[400px]">
150
+ <CronScheduler
151
+ value={cron}
152
+ onChange={setCron}
153
+ showPreview
154
+ timeFormat="12h"
155
+ />
156
+ </div>
157
+ );
158
+ };
159
+
160
+ export const Disabled = () => (
161
+ <div className="w-[400px]">
162
+ <CronScheduler
163
+ value="0 9 * * 1-5"
164
+ showPreview
165
+ disabled
166
+ />
167
+ </div>
168
+ );
169
+
170
+ export const Compact = () => {
171
+ const [cron, setCron] = useState('0 9 * * 1-5');
172
+
173
+ return (
174
+ <div className="w-[400px]">
175
+ <CronScheduler
176
+ value={cron}
177
+ onChange={setCron}
178
+ defaultType="weekly"
179
+ showPreview={false}
180
+ />
181
+ </div>
182
+ );
183
+ };
184
+
185
+ export const Controlled = () => {
186
+ const [cron, setCron] = useState('0 9 * * *');
187
+
188
+ const presets = [
189
+ { label: 'Every day at 9am', value: '0 9 * * *' },
190
+ { label: 'Weekdays at 6pm', value: '0 18 * * 1-5' },
191
+ { label: 'Every Monday', value: '0 0 * * 1' },
192
+ { label: '1st of month', value: '0 0 1 * *' },
193
+ ];
194
+
195
+ return (
196
+ <div className="w-[400px] space-y-4">
197
+ <div className="flex flex-wrap gap-2">
198
+ {presets.map(({ label, value }) => (
199
+ <button
200
+ key={value}
201
+ onClick={() => setCron(value)}
202
+ className={`px-2 py-1 text-xs rounded-md transition-colors ${
203
+ cron === value
204
+ ? 'bg-primary text-primary-foreground'
205
+ : 'bg-muted hover:bg-muted/80'
206
+ }`}
207
+ >
208
+ {label}
209
+ </button>
210
+ ))}
211
+ </div>
212
+ <CronScheduler
213
+ value={cron}
214
+ onChange={setCron}
215
+ showPreview
216
+ allowCopy
217
+ />
218
+ </div>
219
+ );
220
+ };
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * CronCheatsheet
5
+ *
6
+ * Popover with cron syntax reference.
7
+ */
8
+
9
+ import {
10
+ Popover,
11
+ PopoverTrigger,
12
+ PopoverContent,
13
+ } from '@djangocfg/ui-core/components';
14
+ import { cn } from '@djangocfg/ui-core/lib';
15
+ import { HelpCircle } from 'lucide-react';
16
+
17
+ export interface CronCheatsheetProps {
18
+ className?: string;
19
+ }
20
+
21
+ export function CronCheatsheet({ className }: CronCheatsheetProps) {
22
+ return (
23
+ <Popover>
24
+ <PopoverTrigger asChild>
25
+ <button
26
+ type="button"
27
+ className={cn(
28
+ 'p-1 rounded hover:bg-muted/50 transition-colors',
29
+ className
30
+ )}
31
+ aria-label="Cron syntax help"
32
+ >
33
+ <HelpCircle className="h-4 w-4 text-muted-foreground" />
34
+ </button>
35
+ </PopoverTrigger>
36
+ <PopoverContent className="w-72 p-3" align="end">
37
+ <div className="space-y-3">
38
+ <p className="font-medium text-sm">Cron Format</p>
39
+ <code className="block text-xs bg-muted px-2 py-1.5 rounded font-mono text-center">
40
+ min hour day month weekday
41
+ </code>
42
+
43
+ <div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
44
+ <div className="flex justify-between">
45
+ <span className="text-muted-foreground">minute</span>
46
+ <span>0-59</span>
47
+ </div>
48
+ <div className="flex justify-between">
49
+ <span className="text-muted-foreground">hour</span>
50
+ <span>0-23</span>
51
+ </div>
52
+ <div className="flex justify-between">
53
+ <span className="text-muted-foreground">day</span>
54
+ <span>1-31</span>
55
+ </div>
56
+ <div className="flex justify-between">
57
+ <span className="text-muted-foreground">month</span>
58
+ <span>1-12</span>
59
+ </div>
60
+ <div className="flex justify-between col-span-2">
61
+ <span className="text-muted-foreground">weekday</span>
62
+ <span>0-6 (Sun-Sat)</span>
63
+ </div>
64
+ </div>
65
+
66
+ <div className="pt-2 border-t border-border space-y-1.5">
67
+ <p className="text-xs font-medium">Special characters</p>
68
+ <div className="grid grid-cols-2 gap-1.5 text-xs">
69
+ <span><code className="bg-muted px-1 rounded">*</code> any value</span>
70
+ <span><code className="bg-muted px-1 rounded">,</code> list (1,3,5)</span>
71
+ <span><code className="bg-muted px-1 rounded">-</code> range (1-5)</span>
72
+ <span><code className="bg-muted px-1 rounded">/</code> step (*/15)</span>
73
+ </div>
74
+ </div>
75
+
76
+ <div className="pt-2 border-t border-border space-y-1.5">
77
+ <p className="text-xs font-medium">Examples</p>
78
+ <div className="space-y-1 text-xs">
79
+ <div className="flex justify-between font-mono">
80
+ <span className="text-muted-foreground">0 9 * * *</span>
81
+ <span>daily at 9am</span>
82
+ </div>
83
+ <div className="flex justify-between font-mono">
84
+ <span className="text-muted-foreground">0 9 * * 1-5</span>
85
+ <span>weekdays 9am</span>
86
+ </div>
87
+ <div className="flex justify-between font-mono">
88
+ <span className="text-muted-foreground">*/15 * * * *</span>
89
+ <span>every 15 min</span>
90
+ </div>
91
+ <div className="flex justify-between font-mono">
92
+ <span className="text-muted-foreground">0 0 1 * *</span>
93
+ <span>1st of month</span>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </PopoverContent>
99
+ </Popover>
100
+ );
101
+ }
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * CustomInput
5
+ *
6
+ * Raw cron expression input with validation and help popover.
7
+ */
8
+
9
+ import { useState, useEffect } from 'react';
10
+ import { Input } from '@djangocfg/ui-core/components';
11
+ import { cn } from '@djangocfg/ui-core/lib';
12
+ import { AlertCircle, CheckCircle2 } from 'lucide-react';
13
+ import { useCronCustom } from '../context/hooks';
14
+ import { isValidCron } from '../utils/cron-parser';
15
+
16
+ export interface CustomInputProps {
17
+ disabled?: boolean;
18
+ className?: string;
19
+ }
20
+
21
+ export function CustomInput({ disabled, className }: CustomInputProps) {
22
+ const { customCron, isValid, setCustomCron } = useCronCustom();
23
+ const [localValue, setLocalValue] = useState(customCron);
24
+ const [localValid, setLocalValid] = useState(isValid);
25
+
26
+ useEffect(() => {
27
+ setLocalValue(customCron);
28
+ setLocalValid(isValid);
29
+ }, [customCron, isValid]);
30
+
31
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
32
+ const value = e.target.value;
33
+ setLocalValue(value);
34
+ const valid = isValidCron(value);
35
+ setLocalValid(valid);
36
+ setCustomCron(value);
37
+ };
38
+
39
+ return (
40
+ <div className={cn('space-y-2', className)}>
41
+ <span className="text-sm text-muted-foreground">Cron expression</span>
42
+
43
+ <div className="relative">
44
+ <Input
45
+ type="text"
46
+ value={localValue}
47
+ onChange={handleChange}
48
+ disabled={disabled}
49
+ placeholder="* * * * *"
50
+ className={cn(
51
+ 'font-mono text-base pr-10 h-11',
52
+ !localValid && localValue.trim() && 'border-destructive focus-visible:ring-destructive/50'
53
+ )}
54
+ />
55
+ <div className="absolute right-3 top-1/2 -translate-y-1/2">
56
+ {localValue.trim() && (
57
+ localValid ? (
58
+ <CheckCircle2 className="h-5 w-5 text-green-500" />
59
+ ) : (
60
+ <AlertCircle className="h-5 w-5 text-destructive" />
61
+ )
62
+ )}
63
+ </div>
64
+ </div>
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,130 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * DayChips
5
+ *
6
+ * Full-width day of week selector with clear labels.
7
+ * Shows abbreviated day names for clarity.
8
+ */
9
+
10
+ import { cn } from '@djangocfg/ui-core/lib';
11
+ import { useCronWeekDays } from '../context/hooks';
12
+ import type { WeekDay } from '../types';
13
+
14
+ const DAYS: { value: WeekDay; label: string }[] = [
15
+ { value: 1, label: 'Mon' },
16
+ { value: 2, label: 'Tue' },
17
+ { value: 3, label: 'Wed' },
18
+ { value: 4, label: 'Thu' },
19
+ { value: 5, label: 'Fri' },
20
+ { value: 6, label: 'Sat' },
21
+ { value: 0, label: 'Sun' },
22
+ ];
23
+
24
+ export interface DayChipsProps {
25
+ disabled?: boolean;
26
+ showPresets?: boolean;
27
+ className?: string;
28
+ }
29
+
30
+ export function DayChips({
31
+ disabled,
32
+ showPresets = true,
33
+ className,
34
+ }: DayChipsProps) {
35
+ const { weekDays, toggleWeekDay, setWeekDays } = useCronWeekDays();
36
+
37
+ const isWeekdays = weekDays.length === 5 && [1,2,3,4,5].every(d => weekDays.includes(d as WeekDay));
38
+ const isWeekend = weekDays.length === 2 && [0,6].every(d => weekDays.includes(d as WeekDay));
39
+ const isEveryday = weekDays.length === 7;
40
+
41
+ return (
42
+ <div className={cn('space-y-3', className)}>
43
+ {/* Day Grid - full width */}
44
+ <div className="grid grid-cols-7 gap-1">
45
+ {DAYS.map(({ value, label }) => {
46
+ const isSelected = weekDays.includes(value);
47
+ const isWeekend = value === 0 || value === 6;
48
+
49
+ return (
50
+ <button
51
+ key={value}
52
+ type="button"
53
+ disabled={disabled}
54
+ onClick={() => toggleWeekDay(value)}
55
+ aria-pressed={isSelected}
56
+ className={cn(
57
+ 'flex flex-col items-center justify-center',
58
+ 'py-2.5 rounded-lg text-xs font-medium',
59
+ 'transition-all duration-150',
60
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
61
+ 'active:scale-[0.97]',
62
+ isSelected
63
+ ? 'bg-primary text-primary-foreground shadow-sm'
64
+ : cn(
65
+ 'bg-muted/50 hover:bg-muted',
66
+ isWeekend ? 'text-muted-foreground/70' : 'text-muted-foreground'
67
+ ),
68
+ disabled && 'opacity-50 cursor-not-allowed pointer-events-none'
69
+ )}
70
+ >
71
+ <span>{label}</span>
72
+ </button>
73
+ );
74
+ })}
75
+ </div>
76
+
77
+ {/* Quick Presets */}
78
+ {showPresets && (
79
+ <div className="flex gap-2">
80
+ <PresetButton
81
+ label="Weekdays"
82
+ isActive={isWeekdays}
83
+ onClick={() => setWeekDays([1, 2, 3, 4, 5] as WeekDay[])}
84
+ disabled={disabled}
85
+ />
86
+ <PresetButton
87
+ label="Weekends"
88
+ isActive={isWeekend}
89
+ onClick={() => setWeekDays([0, 6] as WeekDay[])}
90
+ disabled={disabled}
91
+ />
92
+ <PresetButton
93
+ label="Every day"
94
+ isActive={isEveryday}
95
+ onClick={() => setWeekDays([0, 1, 2, 3, 4, 5, 6] as WeekDay[])}
96
+ disabled={disabled}
97
+ />
98
+ </div>
99
+ )}
100
+ </div>
101
+ );
102
+ }
103
+
104
+ interface PresetButtonProps {
105
+ label: string;
106
+ isActive: boolean;
107
+ onClick: () => void;
108
+ disabled?: boolean;
109
+ }
110
+
111
+ function PresetButton({ label, isActive, onClick, disabled }: PresetButtonProps) {
112
+ return (
113
+ <button
114
+ type="button"
115
+ disabled={disabled}
116
+ onClick={onClick}
117
+ className={cn(
118
+ 'flex-1 px-3 py-1.5 rounded-md text-xs font-medium',
119
+ 'transition-colors duration-150',
120
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
121
+ isActive
122
+ ? 'bg-primary/15 text-primary border border-primary/30'
123
+ : 'bg-muted/30 text-muted-foreground hover:bg-muted/50 border border-transparent',
124
+ disabled && 'opacity-50 cursor-not-allowed'
125
+ )}
126
+ >
127
+ {label}
128
+ </button>
129
+ );
130
+ }