@maestro-js/components 1.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/commands/add.ts +41 -0
  2. package/commands/index.ts +7 -0
  3. package/commands/list.ts +9 -0
  4. package/dist/components.json +1 -0
  5. package/dist/index.d.ts +14 -0
  6. package/dist/index.js +48 -0
  7. package/package.json +49 -0
  8. package/registry.json +1445 -0
  9. package/scripts/build.ts +44 -0
  10. package/src/components/alert-dialog.tsx +150 -0
  11. package/src/components/autocomplete.tsx +288 -0
  12. package/src/components/autosuggest.tsx +314 -0
  13. package/src/components/avatar.tsx +54 -0
  14. package/src/components/boolean-select.tsx +48 -0
  15. package/src/components/button-group.tsx +75 -0
  16. package/src/components/button-link.tsx +71 -0
  17. package/src/components/button.tsx +132 -0
  18. package/src/components/checkbox-group.tsx +172 -0
  19. package/src/components/checkbox.tsx +207 -0
  20. package/src/components/chip.tsx +158 -0
  21. package/src/components/container.tsx +82 -0
  22. package/src/components/currency-field.tsx +183 -0
  23. package/src/components/date-input.tsx +189 -0
  24. package/src/components/date-picker.tsx +211 -0
  25. package/src/components/date-range-picker.tsx +290 -0
  26. package/src/components/date-time-picker.tsx +196 -0
  27. package/src/components/dialog.tsx +97 -0
  28. package/src/components/disclosure.tsx +114 -0
  29. package/src/components/drawer.tsx +78 -0
  30. package/src/components/enum-chip.tsx +30 -0
  31. package/src/components/file-input.tsx +245 -0
  32. package/src/components/form.tsx +82 -0
  33. package/src/components/headless-file-input.tsx +362 -0
  34. package/src/components/helpers/animated-popover.tsx +24 -0
  35. package/src/components/helpers/button-context.ts +10 -0
  36. package/src/components/helpers/calendar-month-year-picker.tsx +280 -0
  37. package/src/components/helpers/form-field.tsx +229 -0
  38. package/src/components/helpers/get-button-classes.ts +138 -0
  39. package/src/components/helpers/headless-button.tsx +36 -0
  40. package/src/components/helpers/pdf-dist.client.ts +6 -0
  41. package/src/components/icon.tsx +26 -0
  42. package/src/components/image-input.tsx +265 -0
  43. package/src/components/img.tsx +46 -0
  44. package/src/components/inline-alert.tsx +54 -0
  45. package/src/components/labeled-value.tsx +480 -0
  46. package/src/components/link-tabs.tsx +118 -0
  47. package/src/components/menu.tsx +152 -0
  48. package/src/components/month-day-input.tsx +176 -0
  49. package/src/components/multi-file-input.tsx +244 -0
  50. package/src/components/multi-image-input.tsx +389 -0
  51. package/src/components/multicomplete.tsx +322 -0
  52. package/src/components/multiselect.tsx +325 -0
  53. package/src/components/multisuggest.tsx +357 -0
  54. package/src/components/number-field.tsx +143 -0
  55. package/src/components/numeric-tag-field.tsx +271 -0
  56. package/src/components/pdf-input.tsx +249 -0
  57. package/src/components/pdf.tsx +86 -0
  58. package/src/components/percentage-field.tsx +187 -0
  59. package/src/components/phone-number-field.tsx +166 -0
  60. package/src/components/radio-group.tsx +112 -0
  61. package/src/components/radio.tsx +91 -0
  62. package/src/components/select.tsx +215 -0
  63. package/src/components/spinner.tsx +16 -0
  64. package/src/components/stepper.tsx +181 -0
  65. package/src/components/switch.tsx +186 -0
  66. package/src/components/tabs.tsx +151 -0
  67. package/src/components/tag-field.tsx +250 -0
  68. package/src/components/text-area.tsx +148 -0
  69. package/src/components/text-field.tsx +144 -0
  70. package/src/components/time-input.tsx +198 -0
  71. package/src/components/toast.tsx +176 -0
  72. package/src/components/toggle-button-group.tsx +94 -0
  73. package/src/components/year-month-input.tsx +187 -0
  74. package/src/utils/colors.ts +44 -0
  75. package/src/utils/compose-refs.ts +32 -0
  76. package/src/utils/enum-colors.ts +1 -0
  77. package/src/utils/file-input.ts +49 -0
  78. package/src/utils/icons.d.ts +20 -0
  79. package/src/utils/tw.ts +13 -0
  80. package/src/utils/use-element-size.ts +35 -0
  81. package/src/utils/use-number-input.ts +143 -0
  82. package/src/utils/use-pagination.ts +38 -0
  83. package/src/utils/use-prevent-default.ts +27 -0
  84. package/src/utils/use-render-props.ts +39 -0
  85. package/src/utils/use-spin-delay.ts +24 -0
  86. package/src/utils/use-stable-accessor.ts +11 -0
  87. package/src/utils/use-tab-indicator.ts +106 -0
  88. package/tests/commands.test.ts +81 -0
  89. package/tsconfig.json +27 -0
@@ -0,0 +1,172 @@
1
+ import * as React from 'react'
2
+ import { HeadlessForm } from '@maestro-js/form'
3
+ import type { ValidationFunction, ValidationResult } from '@maestro-js/form'
4
+ import { tw } from '../utils/tw'
5
+ import { FormFieldComponents } from './helpers/form-field'
6
+ import { LabeledCheckbox } from './checkbox'
7
+
8
+ export type CheckboxGroupOption = {
9
+ value: string
10
+ label: React.ReactNode
11
+ description?: React.ReactNode
12
+ isDisabled?: boolean
13
+ }
14
+
15
+ export type CheckboxGroupProps = {
16
+ name?: string
17
+ form?: string
18
+
19
+ label?: React.ReactNode
20
+ cornerHint?: React.ReactNode
21
+ helpText?: React.ReactNode
22
+ warningMessage?: React.ReactNode
23
+
24
+ isRequired?: boolean
25
+ isDisabled?: boolean
26
+ isInvalid?: boolean
27
+ validate?: ValidationFunction<string[]>
28
+ errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)
29
+
30
+ value?: string[]
31
+ defaultValue?: string[]
32
+ onChange?: (value: string[]) => void
33
+
34
+ options: CheckboxGroupOption[]
35
+ orientation?: 'horizontal' | 'vertical'
36
+ showSelectAll?: boolean
37
+ minSelection?: number
38
+ maxSelection?: number
39
+ }
40
+
41
+ export function CheckboxGroup(props: CheckboxGroupProps) {
42
+ const hiddenInputRef = React.useRef<HTMLInputElement>(null)
43
+ const helpTextId = React.useId()
44
+
45
+ const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? [], props.onChange)
46
+
47
+ const {
48
+ validationMessage,
49
+ isInvalid: validationInvalid,
50
+ commitValidation
51
+ } = HeadlessForm.useValidation(
52
+ {
53
+ validate: props.validate,
54
+ value,
55
+ name: props.name,
56
+ isRequired: props.isRequired,
57
+ form: props.form,
58
+ focus() {
59
+ // Focus is handled by individual checkboxes
60
+ }
61
+ },
62
+ hiddenInputRef
63
+ )
64
+
65
+ const isInvalid = props.isInvalid || validationInvalid
66
+ const hasWarning = Boolean(props.warningMessage) && !isInvalid
67
+ const orientation = props.orientation ?? 'vertical'
68
+
69
+ const enabledOptions = props.options.filter((o) => !o.isDisabled)
70
+ const allSelected = value.length === enabledOptions.length
71
+ const someSelected = value.length > 0 && !allSelected
72
+
73
+ const handleOptionToggle = (optionValue: string) => {
74
+ const newValue = value.includes(optionValue) ? value.filter((v) => v !== optionValue) : [...value, optionValue]
75
+
76
+ if (props.minSelection && newValue.length < props.minSelection) {
77
+ return
78
+ }
79
+ if (props.maxSelection && newValue.length > props.maxSelection) {
80
+ return
81
+ }
82
+
83
+ setValue(newValue)
84
+ commitValidation()
85
+ }
86
+
87
+ const handleSelectAll = () => {
88
+ if (allSelected) {
89
+ setValue([])
90
+ } else {
91
+ setValue(enabledOptions.map((o) => o.value))
92
+ }
93
+ commitValidation()
94
+ }
95
+
96
+ return (
97
+ <div
98
+ className="relative"
99
+ role="group"
100
+ aria-labelledby={props.label ? `${helpTextId}-label` : undefined}
101
+ aria-describedby={helpTextId}
102
+ >
103
+ {value.map((v, i) => (
104
+ <HeadlessForm.HiddenInput
105
+ key={i}
106
+ ref={i === 0 ? hiddenInputRef : undefined}
107
+ name={props.name}
108
+ value={v}
109
+ form={props.form}
110
+ />
111
+ ))}
112
+ {value.length === 0 && <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value="" form={props.form} />}
113
+
114
+ {props.label && (
115
+ <div className="flex justify-between items-center mb-2">
116
+ <FormFieldComponents.Label as="span" id={`${helpTextId}-label`}>
117
+ {props.label}
118
+ </FormFieldComponents.Label>
119
+ {props.cornerHint && <div className="text-sm text-neutral-500">{props.cornerHint}</div>}
120
+ </div>
121
+ )}
122
+
123
+ {props.showSelectAll && (
124
+ <div className="mb-2 pb-2 border-b border-neutral-200">
125
+ <LabeledCheckbox
126
+ checked={allSelected}
127
+ isIndeterminate={someSelected}
128
+ onChange={handleSelectAll}
129
+ isDisabled={props.isDisabled}
130
+ label={<span className="text-sm font-medium">Select All</span>}
131
+ />
132
+ </div>
133
+ )}
134
+
135
+ <div className={tw('flex', orientation === 'vertical' ? 'flex-col space-y-1.5' : 'flex-row flex-wrap gap-4')}>
136
+ {props.options.map((option) => (
137
+ <LabeledCheckbox
138
+ key={option.value}
139
+ label={option.label}
140
+ helpText={option.description ?? null}
141
+ checked={value.includes(option.value)}
142
+ onChange={() => handleOptionToggle(option.value)}
143
+ isDisabled={option.isDisabled || props.isDisabled}
144
+ />
145
+ ))}
146
+ </div>
147
+
148
+ {(props.minSelection || props.maxSelection) && (
149
+ <div className="mt-2 text-xs text-neutral-500">
150
+ {props.minSelection && props.maxSelection
151
+ ? `Select between ${props.minSelection} and ${props.maxSelection} options`
152
+ : props.minSelection
153
+ ? `Select at least ${props.minSelection} option${props.minSelection > 1 ? 's' : ''}`
154
+ : `Select up to ${props.maxSelection} option${props.maxSelection! > 1 ? 's' : ''}`}
155
+ {' · '}
156
+ {value.length} selected
157
+ </div>
158
+ )}
159
+
160
+ <FormFieldComponents.FormFieldHelpText
161
+ id={helpTextId}
162
+ isInvalid={isInvalid}
163
+ validationMessage={validationMessage}
164
+ hasWarning={hasWarning}
165
+ warningMessage={props.warningMessage}
166
+ helpText={props.helpText}
167
+ />
168
+ </div>
169
+ )
170
+ }
171
+
172
+ CheckboxGroup.displayName = 'CheckboxGroup'
@@ -0,0 +1,207 @@
1
+ import * as React from 'react'
2
+ import { Checkbox as AriaCheckbox } from 'react-aria-components'
3
+ import { HeadlessForm } from '@maestro-js/form'
4
+ import type { ValidationFunction, ValidationResult } from '@maestro-js/form'
5
+ import { tw } from '../utils/tw'
6
+ import { FormFieldComponents } from './helpers/form-field'
7
+ import { Icon } from './icon'
8
+
9
+ // ── Props ─────────────────────────────────────────────────────────────────────
10
+
11
+ export type PlainCheckboxProps = {
12
+ name?: string
13
+ form?: string
14
+
15
+ isDisabled?: boolean
16
+ isRequired?: boolean
17
+ isInvalid?: boolean
18
+ validate?: ValidationFunction<boolean>
19
+
20
+ checked?: boolean
21
+ defaultChecked?: boolean
22
+ onChange?: (checked: boolean) => void
23
+
24
+ 'aria-label'?: string
25
+ }
26
+
27
+ export type LabeledCheckboxProps = {
28
+ name?: string
29
+ form?: string
30
+
31
+ label?: React.ReactNode
32
+ cornerHint?: React.ReactNode
33
+ helpText?: React.ReactNode
34
+ warningMessage?: React.ReactNode
35
+
36
+ isRequired?: boolean
37
+ isDisabled?: boolean
38
+ isIndeterminate?: boolean
39
+ isInvalid?: boolean
40
+ validate?: ValidationFunction<boolean>
41
+ errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)
42
+
43
+ checked?: boolean
44
+ defaultChecked?: boolean
45
+ onChange?: (checked: boolean) => void
46
+
47
+ value?: string
48
+ }
49
+
50
+ // ── Plain Checkbox ─────────────────────────────────────────────────────────────
51
+
52
+ export function PlainCheckbox(props: PlainCheckboxProps) {
53
+ const hiddenInputRef = React.useRef<HTMLInputElement>(null)
54
+
55
+ const [checked, setChecked] = HeadlessForm.useControlledState(props.checked, props.defaultChecked ?? false, props.onChange)
56
+
57
+ const { isInvalid: validationInvalid, commitValidation } = HeadlessForm.useValidation(
58
+ {
59
+ validate: props.validate,
60
+ value: checked,
61
+ name: props.name,
62
+ isRequired: false,
63
+ form: props.form,
64
+ focus() {
65
+ hiddenInputRef.current?.closest('label')?.focus()
66
+ }
67
+ },
68
+ hiddenInputRef
69
+ )
70
+
71
+ return (
72
+ <AriaCheckbox
73
+ isSelected={checked}
74
+ onChange={(v) => {
75
+ setChecked(v)
76
+ commitValidation()
77
+ }}
78
+ isDisabled={props.isDisabled}
79
+ isInvalid={props.isInvalid || validationInvalid}
80
+ aria-label={props['aria-label']}
81
+ className="group inline-flex items-center focus:outline-none [&>div]:focus-visible:ring-2 [&>div]:focus-visible:ring-primary-500/20 [&>div]:focus-visible:ring-offset-2"
82
+ >
83
+ {({ isSelected, isIndeterminate, isDisabled }) => (
84
+ <>
85
+ {props.isDisabled || !props.name ? null : (
86
+ <HeadlessForm.HiddenInput
87
+ ref={hiddenInputRef}
88
+ name={props.name}
89
+ form={props.form}
90
+ value={checked ? 'true' : 'false'}
91
+ />
92
+ )}
93
+ <CheckboxBox isSelected={isSelected} isIndeterminate={isIndeterminate} isDisabled={isDisabled} />
94
+ </>
95
+ )}
96
+ </AriaCheckbox>
97
+ )
98
+ }
99
+
100
+ PlainCheckbox.displayName = 'PlainCheckbox'
101
+
102
+ // ── Labeled Checkbox ───────────────────────────────────────────────────────────
103
+
104
+ export function LabeledCheckbox(props: LabeledCheckboxProps) {
105
+ const hiddenInputRef = React.useRef<HTMLInputElement>(null)
106
+ const helpTextId = React.useId()
107
+
108
+ const [checked, setChecked] = HeadlessForm.useControlledState(props.checked, props.defaultChecked ?? false, props.onChange)
109
+
110
+ const {
111
+ validationMessage,
112
+ isInvalid: validationInvalid,
113
+ commitValidation
114
+ } = HeadlessForm.useValidation(
115
+ {
116
+ validate: props.validate,
117
+ value: checked,
118
+ name: props.name,
119
+ isRequired: props.isRequired,
120
+ form: props.form,
121
+ focus() {
122
+ hiddenInputRef.current?.closest('label')?.focus()
123
+ }
124
+ },
125
+ hiddenInputRef
126
+ )
127
+
128
+ const isInvalid = props.isInvalid || validationInvalid
129
+ const hasWarning = Boolean(props.warningMessage) && !isInvalid
130
+
131
+ return (
132
+ <AriaCheckbox
133
+ isSelected={checked}
134
+ isIndeterminate={props.isIndeterminate}
135
+ onChange={(v) => {
136
+ setChecked(v)
137
+ commitValidation()
138
+ }}
139
+ isDisabled={props.isDisabled}
140
+ isInvalid={isInvalid}
141
+ aria-describedby={helpTextId}
142
+ className="group relative flex items-start gap-3 select-none focus:outline-none [&>div:first-child>div]:focus-visible:ring-2 [&>div:first-child>div]:focus-visible:ring-primary-500/20 [&>div:first-child>div]:focus-visible:ring-offset-2"
143
+ >
144
+ {({ isSelected, isIndeterminate, isDisabled }) => (
145
+ <>
146
+ {props.isDisabled || !props.name ? null : (
147
+ <HeadlessForm.HiddenInput
148
+ ref={hiddenInputRef}
149
+ name={props.name}
150
+ form={props.form}
151
+ value={checked ? 'true' : 'false'}
152
+ />
153
+ )}
154
+ <div className="shrink-0">
155
+ <CheckboxBox isSelected={isSelected} isIndeterminate={isIndeterminate} isDisabled={isDisabled} />
156
+ </div>
157
+ <div className="flex flex-col justify-center">
158
+ <div className="flex items-center gap-2 justify-between">
159
+ <FormFieldComponents.Label as="span">{props.label}</FormFieldComponents.Label>
160
+ {props.cornerHint && <div className="text-sm text-neutral-500">{props.cornerHint}</div>}
161
+ </div>
162
+ <FormFieldComponents.FormFieldHelpText
163
+ id={helpTextId}
164
+ isInvalid={isInvalid}
165
+ validationMessage={validationMessage}
166
+ hasWarning={hasWarning}
167
+ warningMessage={props.warningMessage}
168
+ helpText={props.helpText}
169
+ UNSAFE_className="mt-0.5"
170
+ />
171
+ </div>
172
+ </>
173
+ )}
174
+ </AriaCheckbox>
175
+ )
176
+ }
177
+
178
+ LabeledCheckbox.displayName = 'LabeledCheckbox'
179
+
180
+ // ── Helper components ──────────────────────────────────────────────────────────
181
+
182
+ function CheckboxBox({
183
+ isSelected,
184
+ isIndeterminate,
185
+ isDisabled
186
+ }: {
187
+ isSelected: boolean
188
+ isIndeterminate: boolean
189
+ isDisabled: boolean
190
+ }) {
191
+ return (
192
+ <div
193
+ className={tw(
194
+ 'relative h-5 w-5 rounded border transition-all duration-50',
195
+ isSelected || isIndeterminate ? 'bg-primary-500 border-primary-500' : 'bg-white border-neutral-300',
196
+ !isDisabled && !(isSelected || isIndeterminate) && 'group-hovered:border-neutral-400',
197
+ isDisabled && 'opacity-50 cursor-not-allowed'
198
+ )}
199
+ >
200
+ {isIndeterminate ? (
201
+ <Icon name="minus" className="h-5/6 absolute inset-0 m-auto aspect-square text-white" />
202
+ ) : (
203
+ <Icon name="check" className="h-5/6 absolute inset-0 m-auto aspect-square text-white" />
204
+ )}
205
+ </div>
206
+ )
207
+ }
@@ -0,0 +1,158 @@
1
+ import * as React from 'react'
2
+ import { tw } from '../utils/tw'
3
+ import { Icon } from './icon'
4
+
5
+ // ── Props ─────────────────────────────────────────────────────────────────────
6
+
7
+ export type ChipProps = {
8
+ children: React.ReactNode
9
+ onRemove?: () => void
10
+ isDisabled?: boolean
11
+ size?: 'sm' | 'md' | 'lg'
12
+ color?: 'stone' | 'info' | 'green' | 'yellow' | 'orange' | 'red'
13
+ colorMode?: 'light' | 'dark'
14
+ className?: string
15
+ style?: React.CSSProperties
16
+ }
17
+
18
+ export type ChipButtonProps = {
19
+ children: React.ReactNode
20
+ disabled?: boolean
21
+ size?: 'sm' | 'md' | 'lg'
22
+ color?: 'stone' | 'info' | 'green' | 'yellow' | 'orange' | 'red'
23
+ colorMode?: 'light' | 'dark'
24
+ className?: string
25
+ } & Omit<React.ComponentProps<'button'>, 'disabled' | 'size' | 'color'>
26
+
27
+ // ── Chip ──────────────────────────────────────────────────────────────────────
28
+
29
+ export function Chip(props: ChipProps) {
30
+ const {
31
+ children,
32
+ onRemove,
33
+ isDisabled = false,
34
+ size = 'md',
35
+ color = 'stone',
36
+ colorMode = 'light',
37
+ className,
38
+ style
39
+ } = props
40
+
41
+ const colorClasses = colorMode === 'dark' ? darkColorClasses : lightColorClasses
42
+
43
+ return (
44
+ <div
45
+ className={tw(
46
+ 'inline-flex items-center rounded-full font-normal transition-colors max-w-full',
47
+ sizeClasses[size],
48
+ !isDisabled && !style && colorClasses[color],
49
+ isDisabled && 'opacity-50 cursor-not-allowed',
50
+ className
51
+ )}
52
+ style={style}
53
+ >
54
+ <span className="truncate">{children}</span>
55
+ {onRemove && (
56
+ <button
57
+ type="button"
58
+ onClick={onRemove}
59
+ disabled={isDisabled}
60
+ className={tw(
61
+ 'inline-flex items-center justify-center rounded-full transition-colors translate-x-1',
62
+ removeBtnSizeClasses[size],
63
+ !isDisabled && 'hover:bg-black/10',
64
+ isDisabled && 'cursor-not-allowed'
65
+ )}
66
+ aria-label="Remove"
67
+ tabIndex={-1}
68
+ >
69
+ <Icon name="x-mark" className={removeIconSizeClasses[size]} aria-hidden="true" />
70
+ </button>
71
+ )}
72
+ </div>
73
+ )
74
+ }
75
+
76
+ // ── ChipButton ────────────────────────────────────────────────────────────────
77
+
78
+ export const ChipButton = React.forwardRef<HTMLButtonElement, ChipButtonProps>((props, ref) => {
79
+ const { children, size = 'md', color = 'stone', colorMode = 'light', className, disabled, ...rest } = props
80
+
81
+ const colorClasses = colorMode === 'dark' ? darkHoverColorClasses : lightHoverColorClasses
82
+
83
+ return (
84
+ <button
85
+ ref={ref}
86
+ type="button"
87
+ disabled={disabled}
88
+ className={tw(
89
+ 'inline-flex items-center rounded-full font-normal transition-colors',
90
+ sizeClasses[size],
91
+ !disabled && colorClasses[color],
92
+ disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
93
+ className
94
+ )}
95
+ {...rest}
96
+ >
97
+ {children}
98
+ </button>
99
+ )
100
+ })
101
+
102
+ ChipButton.displayName = 'ChipButton'
103
+
104
+ // ── Helpers ───────────────────────────────────────────────────────────────────
105
+
106
+ const sizeClasses = {
107
+ sm: 'px-2 py-0.5 text-xs',
108
+ md: 'px-2.5 py-1 text-sm',
109
+ lg: 'px-3 py-1.5 text-base'
110
+ }
111
+
112
+ const lightColorClasses = {
113
+ stone: 'bg-stone-100 text-stone-700',
114
+ info: 'bg-info-100 text-info-700',
115
+ green: 'bg-green-100 text-green-700',
116
+ yellow: 'bg-yellow-100 text-yellow-700',
117
+ orange: 'bg-orange-100 text-orange-700',
118
+ red: 'bg-red-100 text-red-700'
119
+ }
120
+
121
+ const darkColorClasses = {
122
+ stone: 'bg-stone-600 text-white',
123
+ info: 'bg-info-600 text-white',
124
+ green: 'bg-green-600 text-white',
125
+ yellow: 'bg-yellow-600 text-white',
126
+ orange: 'bg-orange-600 text-white',
127
+ red: 'bg-red-600 text-white'
128
+ }
129
+
130
+ const lightHoverColorClasses = {
131
+ stone: 'bg-stone-100 text-stone-700 hover:bg-stone-200',
132
+ info: 'bg-info-100 text-info-700 hover:bg-info-200',
133
+ green: 'bg-green-100 text-green-700 hover:bg-green-200',
134
+ yellow: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200',
135
+ orange: 'bg-orange-100 text-orange-700 hover:bg-orange-200',
136
+ red: 'bg-red-100 text-red-700 hover:bg-red-200'
137
+ }
138
+
139
+ const darkHoverColorClasses = {
140
+ stone: 'bg-stone-600 text-white hover:bg-stone-700',
141
+ info: 'bg-info-600 text-white hover:bg-info-700',
142
+ green: 'bg-green-600 text-white hover:bg-green-700',
143
+ yellow: 'bg-yellow-600 text-white hover:bg-yellow-700',
144
+ orange: 'bg-orange-600 text-white hover:bg-orange-700',
145
+ red: 'bg-red-600 text-white hover:bg-red-700'
146
+ }
147
+
148
+ const removeBtnSizeClasses = {
149
+ sm: 'h-4 w-4 -m-0.5 p-0.5',
150
+ md: 'h-5 w-5 -m-1 p-1',
151
+ lg: 'h-6 w-6 -m-1.5 p-1.5'
152
+ }
153
+
154
+ const removeIconSizeClasses = {
155
+ sm: 'h-2 w-2',
156
+ md: 'h-3 w-3',
157
+ lg: 'h-4 w-4'
158
+ }
@@ -0,0 +1,82 @@
1
+ import * as React from 'react'
2
+ import { tw } from '../utils/tw'
3
+
4
+ // ── Props ────────────────────────────────────────────────────────────────────
5
+
6
+ export type ContainerSize = 'lg' | 'md' | 'sm'
7
+ export type ContainerElevation = 0 | -1 | -2 | 'inherit'
8
+
9
+ export type ContainerProps = React.ComponentProps<'div'> & {
10
+ size?: ContainerSize
11
+ elevation?: ContainerElevation
12
+ }
13
+
14
+ export type ContainerSectionProps = ContainerProps & {
15
+ label?: React.ReactNode
16
+ actions?: React.ReactNode
17
+ }
18
+
19
+ export type SectionProps = {
20
+ label?: React.ReactNode
21
+ actions?: React.ReactNode
22
+ children?: React.ReactNode
23
+ }
24
+
25
+ // ── Container ────────────────────────────────────────────────────────────────
26
+
27
+ export function Container({ size = 'lg', elevation = 0, className, ...props }: ContainerProps) {
28
+ return <div {...props} className={tw(elevationClasses[String(elevation)], sizeClasses[size], className)} />
29
+ }
30
+
31
+ Container.displayName = 'Container'
32
+
33
+ // ── ContainerSection ─────────────────────────────────────────────────────────
34
+
35
+ export function ContainerSection({ label, actions, ...props }: ContainerSectionProps) {
36
+ return (
37
+ <div className="flex flex-col gap-1">
38
+ <div className="flex items-start justify-between">
39
+ <h3 className="text-lg font-semibold">{label}</h3>
40
+ {actions}
41
+ </div>
42
+ <Container {...props} />
43
+ </div>
44
+ )
45
+ }
46
+
47
+ ContainerSection.displayName = 'ContainerSection'
48
+
49
+ // ── Section ──────────────────────────────────────────────────────────────────
50
+
51
+ export function Section({ label, actions, children }: SectionProps) {
52
+ return (
53
+ <div className="flex flex-col gap-1">
54
+ <div className="flex items-start justify-between">
55
+ <h3 className="text-lg font-semibold">{label}</h3>
56
+ {actions}
57
+ </div>
58
+ <div>{children}</div>
59
+ </div>
60
+ )
61
+ }
62
+
63
+ Section.displayName = 'Section'
64
+
65
+ // ── Helpers ──────────────────────────────────────────────────────────────────
66
+
67
+ const sizeClasses: Record<ContainerSize, string> = {
68
+ lg: 'p-6 rounded-lg',
69
+ md: 'p-3 rounded',
70
+ sm: 'p-2 rounded-sm'
71
+ }
72
+
73
+ const elevationClasses: Record<string, string> = {
74
+ '0': 'bg-white border border-neutral-200',
75
+ '-1': 'bg-neutral-50 border border-neutral-200',
76
+ '-2': 'bg-neutral-100 border border-neutral-300',
77
+ inherit: ''
78
+ }
79
+
80
+ export function getSizeClassesForContainer(size: ContainerSize) {
81
+ return sizeClasses[size]
82
+ }