@softize/opus 30.0.0 → 31.1.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 (64) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/bin/lib/check.mjs +109 -101
  3. package/bin/lib/copy.mjs +2973 -1801
  4. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +52 -0
  5. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +27 -2
  6. package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +142 -0
  7. package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
  8. package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
  9. package/docs/adr/0066-civil-date-input-and-calendar-list.md +28 -0
  10. package/docs/adr/README.md +6 -0
  11. package/package.json +1 -1
  12. package/registry/skills/build-opus-ui/SKILL.md +21 -19
  13. package/registry/skills/build-opus-ui/references/evaluations.md +13 -11
  14. package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
  15. package/src/core/presentation.ts +938 -32
  16. package/src/core/types.ts +2 -0
  17. package/src/ui/components/patterns/form.tsx +22 -1
  18. package/src/ui/components/patterns/grouped-list.ts +2 -2
  19. package/src/ui/components/patterns/list-calendar.tsx +129 -0
  20. package/src/ui/components/patterns/list.tsx +612 -382
  21. package/src/ui/components/patterns/page-heading-context.tsx +12 -5
  22. package/src/ui/components/patterns/page.tsx +227 -197
  23. package/src/ui/components/patterns/presentation-fields.tsx +246 -0
  24. package/src/ui/components/patterns/presentation-sections.tsx +494 -0
  25. package/src/ui/components/patterns/presentation.tsx +1142 -229
  26. package/src/ui/components/patterns/structural-header.tsx +23 -32
  27. package/src/ui/components/patterns/surface-anatomy.ts +27 -217
  28. package/src/ui/components/patterns/surface.tsx +19 -29
  29. package/src/ui/components/patterns/trigger.tsx +10 -2
  30. package/src/ui/components/primitives/calendar.tsx +39 -18
  31. package/src/ui/components/primitives/close-button.tsx +11 -3
  32. package/src/ui/components/primitives/command.tsx +2 -8
  33. package/src/ui/components/primitives/date-input.tsx +276 -0
  34. package/src/ui/components/primitives/detail.tsx +1 -1
  35. package/src/ui/components/primitives/dialog.tsx +19 -81
  36. package/src/ui/components/primitives/drawer.tsx +17 -79
  37. package/src/ui/components/primitives/field.tsx +1 -1
  38. package/src/ui/components/primitives/pagination.tsx +3 -2
  39. package/src/ui/components/primitives/table.tsx +1 -1
  40. package/src/ui/components/primitives/tabs.tsx +13 -2
  41. package/src/ui/docs/content/action-form.md +6 -1
  42. package/src/ui/docs/content/action-list.md +156 -68
  43. package/src/ui/docs/content/action-trigger.md +5 -0
  44. package/src/ui/docs/content/button.md +5 -6
  45. package/src/ui/docs/content/calendar-month-view.md +50 -0
  46. package/src/ui/docs/content/calendar.md +6 -1
  47. package/src/ui/docs/content/command.md +62 -50
  48. package/src/ui/docs/content/compositions.md +129 -459
  49. package/src/ui/docs/content/date-input.md +67 -0
  50. package/src/ui/docs/content/dialog.md +44 -112
  51. package/src/ui/docs/content/drawer.md +44 -103
  52. package/src/ui/docs/content/field.md +2 -1
  53. package/src/ui/docs/content/input.md +3 -0
  54. package/src/ui/docs/content/page.md +204 -412
  55. package/src/ui/docs/content/presentation.md +371 -23
  56. package/src/ui/docs/content/separator.md +1 -1
  57. package/src/ui/docs/content/surface.md +65 -120
  58. package/src/ui/docs/content/tokens.md +18 -4
  59. package/src/ui/docs/registry.tsx +38 -9
  60. package/src/ui/drivers/context.tsx +11 -4
  61. package/src/ui/drivers/react.tsx +27 -7
  62. package/src/ui/meta.ts +17 -6
  63. package/src/ui/react.tsx +41 -8
  64. package/src/ui/theme.css +44 -33
@@ -7,8 +7,10 @@ import {
7
7
  import { ptBR } from "date-fns/locale";
8
8
  import {
9
9
  DayPicker,
10
+ DateLib,
10
11
  getDefaultClassNames,
11
12
  type DayButton,
13
+ type RootProps,
12
14
  } from "react-day-picker";
13
15
 
14
16
  import { cn } from "../../lib/cn.ts";
@@ -16,7 +18,9 @@ import { Button, buttonVariants } from "./button.tsx";
16
18
 
17
19
  /** Rótulos acessíveis da navegação em pt-BR — o react-day-picker só traduz nomes de mês e
18
20
  * dia pelo `locale`; os aria-labels dos controles continuariam em inglês. */
19
- const LABELS_PT_BR: NonNullable<React.ComponentProps<typeof DayPicker>["labels"]> = {
21
+ const LABELS_PT_BR: NonNullable<
22
+ React.ComponentProps<typeof DayPicker>["labels"]
23
+ > = {
20
24
  labelPrevious: () => "Mês anterior",
21
25
  labelNext: () => "Próximo mês",
22
26
  labelMonthDropdown: () => "Escolher o mês",
@@ -34,6 +38,17 @@ const LABELS_PT_BR: NonNullable<React.ComponentProps<typeof DayPicker>["labels"]
34
38
  },
35
39
  };
36
40
 
41
+ function CalendarRoot({ className, rootRef, ...props }: RootProps) {
42
+ return (
43
+ <div
44
+ data-slot="calendar"
45
+ ref={rootRef}
46
+ className={cn(className)}
47
+ {...props}
48
+ />
49
+ );
50
+ }
51
+
37
52
  function Calendar({
38
53
  className,
39
54
  classNames,
@@ -63,17 +78,27 @@ function Calendar({
63
78
  )}
64
79
  captionLayout={captionLayout}
65
80
  formatters={{
81
+ formatCaption: (date, options, dateLib) => {
82
+ const caption = (
83
+ dateLib ??
84
+ new DateLib({
85
+ ...options,
86
+ locale: { ...ptBR, ...locale, ...options?.locale },
87
+ })
88
+ ).format(date, "LLLL/yyyy");
89
+ return (
90
+ caption.charAt(0).toLocaleUpperCase(locale.code) + caption.slice(1)
91
+ );
92
+ },
66
93
  // O dateLib já carrega o `locale` em vigor: o dropdown fala a mesma língua da legenda.
67
94
  formatMonthDropdown: (date, dateLib) =>
68
- dateLib?.format(date, "LLL") ?? date.toLocaleString("pt-BR", { month: "short" }),
95
+ dateLib?.format(date, "LLL") ??
96
+ date.toLocaleString("pt-BR", { month: "short" }),
69
97
  ...formatters,
70
98
  }}
71
99
  classNames={{
72
100
  root: cn("w-fit", defaultClassNames.root),
73
- months: cn(
74
- "relative flex flex-row gap-4",
75
- defaultClassNames.months,
76
- ),
101
+ months: cn("relative flex flex-row gap-4", defaultClassNames.months),
77
102
  month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
78
103
  nav: cn(
79
104
  "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
@@ -156,16 +181,7 @@ function Calendar({
156
181
  ...classNames,
157
182
  }}
158
183
  components={{
159
- Root: ({ className, rootRef, ...props }) => {
160
- return (
161
- <div
162
- data-slot="calendar"
163
- ref={rootRef}
164
- className={cn(className)}
165
- {...props}
166
- />
167
- );
168
- },
184
+ Root: CalendarRoot,
169
185
  Chevron: ({ className, orientation, ...props }) => {
170
186
  if (orientation === "left") {
171
187
  return (
@@ -219,7 +235,12 @@ function CalendarDayButton({
219
235
  return (
220
236
  <Button
221
237
  ref={ref}
222
- variant="ghost"
238
+ context={
239
+ modifiers.selected && !modifiers.range_middle ? "primary" : "neutral"
240
+ }
241
+ variant={
242
+ modifiers.selected && !modifiers.range_middle ? "solid" : "ghost"
243
+ }
223
244
  size="icon"
224
245
  data-day={day.date.toLocaleDateString()}
225
246
  data-selected-single={
@@ -232,7 +253,7 @@ function CalendarDayButton({
232
253
  data-range-end={modifiers.range_end}
233
254
  data-range-middle={modifiers.range_middle}
234
255
  className={cn(
235
- "flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[0.1875rem] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70",
256
+ "flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[0.1875rem] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
236
257
  defaultClassNames.day,
237
258
  className,
238
259
  )}
@@ -3,22 +3,30 @@ import { XIcon } from "lucide-react";
3
3
 
4
4
  import { Button, type ButtonProps } from "./button.tsx";
5
5
 
6
- export type CloseButtonSize = "xs" | "sm";
6
+ export type CloseButtonSize = "xs" | "sm" | "default";
7
7
 
8
8
  const buttonSizeByCloseSize = {
9
+ default: "icon",
9
10
  xs: "icon-xs",
10
11
  sm: "icon-sm",
11
12
  } as const;
12
13
 
13
14
  export interface CloseButtonProps extends Omit<
14
15
  ButtonProps,
15
- "busy" | "children" | "context" | "icon" | "shape" | "size" | "type" | "variant"
16
+ | "busy"
17
+ | "children"
18
+ | "context"
19
+ | "icon"
20
+ | "shape"
21
+ | "size"
22
+ | "type"
23
+ | "variant"
16
24
  > {
17
25
  size?: CloseButtonSize;
18
26
  }
19
27
 
20
28
  export function CloseButton({
21
- size = "sm",
29
+ size = "xs",
22
30
  disabled = false,
23
31
  "aria-label": ariaLabel = "Fechar",
24
32
  ...props
@@ -38,9 +38,6 @@ function CommandDialog({
38
38
  return (
39
39
  <Dialog {...props}>
40
40
  <DialogContent
41
- // A paleta é uma superfície contínua: a lista já separa os grupos, e uma faixa
42
- // sob o cabeçalho competiria com ela.
43
- layout="continuous"
44
41
  className={cn("overflow-hidden p-0", className)}
45
42
  showCloseButton={showCloseButton}
46
43
  aria-describedby={descriptionId}
@@ -50,11 +47,8 @@ function CommandDialog({
50
47
  <p id={descriptionId} className="sr-only">
51
48
  {description}
52
49
  </p>
53
- {/* O recuo e a escala são os da paleta, não os da família: a caixa de busca
54
- manda na densidade, e o título aqui é um rótulo acima dela. É a única
55
- superfície do SDK que diverge nos dois, e por isso está escrito aqui. */}
56
- <DialogHeader className="px-3 py-2">
57
- <DialogTitle className="text-sm">{title}</DialogTitle>
50
+ <DialogHeader>
51
+ <DialogTitle>{title}</DialogTitle>
58
52
  </DialogHeader>
59
53
  <Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
60
54
  {children}
@@ -0,0 +1,276 @@
1
+ import * as React from "react";
2
+ import { CalendarDays } from "lucide-react";
3
+ import { Input, type InputProps } from "./input.tsx";
4
+ import { Calendar } from "./calendar.tsx";
5
+ import { Popover, PopoverAnchor, PopoverContent } from "./popover.tsx";
6
+
7
+ export interface DateInputProps extends Omit<
8
+ InputProps,
9
+ | "type"
10
+ | "value"
11
+ | "defaultValue"
12
+ | "onChange"
13
+ | "icon"
14
+ | "trailing"
15
+ | "min"
16
+ | "max"
17
+ | "step"
18
+ > {
19
+ /** Data civil ISO; durante a digitação, pode conter texto ainda inválido. */
20
+ value?: string;
21
+ defaultValue?: string;
22
+ onValueChange?: (value: string) => void;
23
+ }
24
+
25
+ function parseDate(value: string): Date | undefined {
26
+ const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
27
+ if (!parts) return undefined;
28
+ const [, year, month, day] = parts.map(Number);
29
+ if (year < 1 || month < 1 || month > 12 || day < 1 || day > 31)
30
+ return undefined;
31
+ const date = new Date(0);
32
+ date.setHours(0, 0, 0, 0);
33
+ date.setFullYear(year, month - 1, day);
34
+ return date.getFullYear() === year &&
35
+ date.getMonth() === month - 1 &&
36
+ date.getDate() === day
37
+ ? date
38
+ : undefined;
39
+ }
40
+
41
+ function serializeDate(date: Date): string {
42
+ return `${String(date.getFullYear()).padStart(4, "0")}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
43
+ }
44
+
45
+ function valueFromText(text: string): string {
46
+ if (parseDate(text)) return text;
47
+ const parts =
48
+ /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(text) ??
49
+ /^(\d{2})(\d{2})(\d{4})$/.exec(text);
50
+ if (!parts) return text;
51
+ const candidate = `${parts[3]}-${parts[2]}-${parts[1]}`;
52
+ return parseDate(candidate) ? candidate : text;
53
+ }
54
+
55
+ function maskDate(text: string): string {
56
+ const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
57
+ const digits = (
58
+ iso ? `${iso[3]}${iso[2]}${iso[1]}` : text.replace(/\D/g, "")
59
+ ).slice(0, 8);
60
+ return (
61
+ digits.slice(0, 2) +
62
+ (digits.length >= 2 ? "/" : "") +
63
+ digits.slice(2, 4) +
64
+ (digits.length >= 4 ? "/" : "") +
65
+ digits.slice(4)
66
+ );
67
+ }
68
+
69
+ function positionAfterDigits(count: number): number {
70
+ return count + (count >= 2 ? 1 : 0) + (count >= 4 ? 1 : 0);
71
+ }
72
+
73
+ export const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
74
+ function DateInput(
75
+ {
76
+ value,
77
+ defaultValue = "",
78
+ onValueChange,
79
+ disabled,
80
+ readOnly,
81
+ name,
82
+ form,
83
+ placeholder = "DD/MM/AAAA",
84
+ onKeyDown,
85
+ ...props
86
+ },
87
+ ref,
88
+ ) {
89
+ const inputRef = React.useRef<HTMLInputElement>(null);
90
+ const calendarId = React.useId();
91
+ const fieldRef = React.useRef<HTMLDivElement>(null);
92
+ const restoreInputFocus = React.useRef(false);
93
+ React.useImperativeHandle(ref, () => inputRef.current!);
94
+ const [internalValue, setInternalValue] = React.useState(defaultValue);
95
+ const currentValue = value ?? internalValue;
96
+ const selected = parseDate(currentValue);
97
+ const text = selected
98
+ ? currentValue.split("-").reverse().join("/")
99
+ : currentValue;
100
+ const [open, setOpen] = React.useState(false);
101
+ const openRef = React.useRef(open);
102
+ openRef.current = open;
103
+ const [focusCalendar, setFocusCalendar] = React.useState(false);
104
+ const [calendarSession, setCalendarSession] = React.useState(0);
105
+ const [caret, setCaret] = React.useState<{ position: number }>();
106
+ const [month, setMonth] = React.useState(() => selected ?? new Date());
107
+
108
+ React.useLayoutEffect(() => {
109
+ const input = inputRef.current;
110
+ if (caret && input && document.activeElement === input) {
111
+ input.setSelectionRange(caret.position, caret.position);
112
+ }
113
+ }, [caret]);
114
+
115
+ React.useEffect(() => {
116
+ const ownerForm = inputRef.current?.form;
117
+ if (value !== undefined || !ownerForm) return;
118
+ const reset = () => setInternalValue(defaultValue);
119
+ ownerForm.addEventListener("reset", reset);
120
+ return () => ownerForm.removeEventListener("reset", reset);
121
+ }, [value, defaultValue, form]);
122
+
123
+ const change = (next: string) => {
124
+ if (value === undefined) setInternalValue(next);
125
+ onValueChange?.(next);
126
+ };
127
+ const changeOpen = (next: boolean) => {
128
+ if (
129
+ next &&
130
+ (disabled || readOnly || inputRef.current?.matches(":disabled"))
131
+ )
132
+ return;
133
+ if (next) {
134
+ setMonth(selected ?? new Date());
135
+ if (!open) setCalendarSession((session) => session + 1);
136
+ }
137
+ setOpen(next);
138
+ };
139
+
140
+ return (
141
+ <Popover open={open && !disabled && !readOnly} onOpenChange={changeOpen}>
142
+ {name && (
143
+ <input
144
+ type="hidden"
145
+ name={name}
146
+ form={form}
147
+ value={currentValue}
148
+ disabled={disabled}
149
+ />
150
+ )}
151
+ <PopoverAnchor asChild>
152
+ <div
153
+ ref={fieldRef}
154
+ onClick={(event) => {
155
+ if (event.defaultPrevented) return;
156
+ if (
157
+ disabled ||
158
+ readOnly ||
159
+ inputRef.current?.matches(":disabled")
160
+ )
161
+ return;
162
+ inputRef.current?.focus();
163
+ setFocusCalendar(false);
164
+ changeOpen(true);
165
+ }}
166
+ >
167
+ <Input
168
+ {...props}
169
+ ref={inputRef}
170
+ form={form}
171
+ type="text"
172
+ inputMode="numeric"
173
+ value={text}
174
+ placeholder={placeholder}
175
+ disabled={disabled}
176
+ readOnly={readOnly}
177
+ role="combobox"
178
+ aria-expanded={open}
179
+ aria-haspopup="dialog"
180
+ aria-controls={open ? calendarId : undefined}
181
+ aria-keyshortcuts="Alt+ArrowDown"
182
+ onChange={(event) => {
183
+ const input = event.target;
184
+ let raw = input.value;
185
+ let cursor = input.selectionStart ?? raw.length;
186
+ const inputType = (event.nativeEvent as InputEvent).inputType;
187
+ // A barra pertence à máscara: apagar junto dela também remove o dígito adjacente,
188
+ // inclusive em teclados virtuais que não emitem keydown de Backspace ou Delete.
189
+ if (
190
+ raw.length === text.length - 1 &&
191
+ raw.replace(/\D/g, "") === text.replace(/\D/g, "")
192
+ ) {
193
+ if (inputType === "deleteContentBackward" && cursor > 0) {
194
+ raw = raw.slice(0, cursor - 1) + raw.slice(cursor);
195
+ cursor -= 1;
196
+ } else if (inputType === "deleteContentForward") {
197
+ raw = raw.slice(0, cursor) + raw.slice(cursor + 1);
198
+ }
199
+ }
200
+ const masked = maskDate(raw);
201
+ const digitCount = raw
202
+ .slice(0, cursor)
203
+ .replace(/\D/g, "").length;
204
+ setCaret({
205
+ position: Math.min(
206
+ masked.length,
207
+ positionAfterDigits(digitCount),
208
+ ),
209
+ });
210
+ change(valueFromText(masked));
211
+ }}
212
+ onKeyDown={(event) => {
213
+ onKeyDown?.(event);
214
+ if (event.defaultPrevented) return;
215
+ if (event.altKey && event.key === "ArrowDown") {
216
+ event.preventDefault();
217
+ setFocusCalendar(true);
218
+ changeOpen(true);
219
+ }
220
+ }}
221
+ trailing={<CalendarDays aria-hidden="true" className="size-4" />}
222
+ />
223
+ </div>
224
+ </PopoverAnchor>
225
+ <PopoverContent
226
+ id={calendarId}
227
+ className="w-auto p-0"
228
+ align="start"
229
+ aria-label="Escolher data"
230
+ onOpenAutoFocus={(event) => {
231
+ event.preventDefault();
232
+ if (focusCalendar) {
233
+ (event.target as HTMLElement)
234
+ .querySelector<HTMLButtonElement>('[data-day][tabindex="0"]')
235
+ ?.focus();
236
+ }
237
+ }}
238
+ onInteractOutside={(event) => {
239
+ if (fieldRef.current?.contains(event.target as Node))
240
+ event.preventDefault();
241
+ }}
242
+ onEscapeKeyDown={() => {
243
+ restoreInputFocus.current = true;
244
+ }}
245
+ onCloseAutoFocus={(event) => {
246
+ event.preventDefault();
247
+ // O fechamento anterior pode terminar depois de uma reabertura rápida.
248
+ if (restoreInputFocus.current && !openRef.current) {
249
+ inputRef.current?.focus();
250
+ }
251
+ restoreInputFocus.current = false;
252
+ }}
253
+ >
254
+ <Calendar
255
+ // DayPicker aplica autoFocus apenas na montagem, inclusive em reaberturas durante a animação.
256
+ key={`${calendarSession}-${focusCalendar}`}
257
+ mode="single"
258
+ required
259
+ autoFocus={focusCalendar}
260
+ startMonth={parseDate("0001-01-01")}
261
+ endMonth={parseDate("9999-12-01")}
262
+ month={month}
263
+ onMonthChange={setMonth}
264
+ selected={selected}
265
+ onSelect={(date) => {
266
+ if (inputRef.current?.matches(":disabled")) return;
267
+ change(serializeDate(date));
268
+ restoreInputFocus.current = true;
269
+ setOpen(false);
270
+ }}
271
+ />
272
+ </PopoverContent>
273
+ </Popover>
274
+ );
275
+ },
276
+ );
@@ -122,7 +122,7 @@ function DetailField({
122
122
  !dividedHorizontalField &&
123
123
  "group-data-[orientation=horizontal]/detail-group:grid-cols-[minmax(0,var(--detail-label-width))_minmax(0,1fr)] group-data-[orientation=horizontal]/detail-group:items-baseline group-data-[orientation=horizontal]/detail-group:gap-x-3",
124
124
  dividedHorizontalField && "items-stretch gap-x-0",
125
- "group-data-[orientation=vertical]/detail-group:gap-y-1",
125
+ "group-data-[orientation=vertical]/detail-group:gap-y-1.5",
126
126
  "group-data-[dividers=true]/detail-group:outline group-data-[dividers=true]/detail-group:outline-1 group-data-[dividers=true]/detail-group:outline-border",
127
127
  "group-data-[variant=framed]/detail-group:bg-transparent",
128
128
  className,