@aleph-alpha/ui-library 1.16.1 → 1.18.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 (36) hide show
  1. package/dist/system/index-DWkO1UL8.js +8 -0
  2. package/dist/system/index-gkRbLfbP.js +8 -0
  3. package/dist/system/index.d.ts +585 -402
  4. package/dist/system/lib.js +14146 -13574
  5. package/package.json +2 -2
  6. package/src/components/UiChip/UiChip.stories.ts +239 -0
  7. package/src/components/UiChip/UiChip.vue +128 -0
  8. package/src/components/UiChip/__tests__/UiChip.test.ts +102 -0
  9. package/src/components/UiChip/index.ts +2 -0
  10. package/src/components/UiChip/types.ts +50 -0
  11. package/src/components/UiDropdownMenu/UiDropdownMenu.stories.ts +259 -1
  12. package/src/components/UiDropdownMenu/UiDropdownMenuRadioGroup.vue +1 -1
  13. package/src/components/UiField/UiField.stories.ts +589 -249
  14. package/src/components/UiField/UiField.vue +87 -2
  15. package/src/components/UiField/UiFieldDescription.vue +22 -1
  16. package/src/components/UiField/UiFieldLabel.vue +2 -0
  17. package/src/components/UiField/UiFieldLabelInfo.vue +22 -0
  18. package/src/components/UiField/index.ts +1 -0
  19. package/src/components/UiField/keys.ts +5 -0
  20. package/src/components/UiField/types.ts +86 -1
  21. package/src/components/UiSelect/__tests__/UiSelectTrigger.test.ts +47 -2
  22. package/src/components/UiToggle/UiToggle.stories.ts +54 -1
  23. package/src/components/UiToggle/__tests__/UiToggle.test.ts +15 -0
  24. package/src/components/UiToggle/types.ts +1 -1
  25. package/src/components/UiToggleGroup/__tests__/UiToggleGroup.test.ts +21 -0
  26. package/src/components/UiToggleGroup/types.ts +2 -2
  27. package/src/components/core/button/index.ts +5 -0
  28. package/src/components/core/field/FieldLabel.vue +1 -1
  29. package/src/components/core/field/index.ts +5 -5
  30. package/src/components/core/select/SelectTrigger.vue +1 -1
  31. package/src/components/core/toggle/Toggle.vue +3 -3
  32. package/src/components/core/toggle/index.ts +6 -3
  33. package/src/components/core/toggle-group/index.ts +6 -3
  34. package/src/components/index.ts +1 -0
  35. package/dist/system/index-CkH7HQaa.js +0 -7
  36. package/dist/system/index-CuHwEAQ_.js +0 -7
@@ -1,6 +1,8 @@
1
1
  <script setup lang="ts">
2
+ import { computed, provide, toRef, useSlots, type VNode } from 'vue';
2
3
  import { Field as ShadcnField } from '@/components/core/field';
3
4
  import type { UiFieldProps } from './types';
5
+ import { FIELD_DESCRIPTION_CHECKBOX_KEY } from './keys';
4
6
 
5
7
  defineOptions({
6
8
  name: 'UiField',
@@ -8,11 +10,94 @@
8
10
 
9
11
  const props = withDefaults(defineProps<UiFieldProps>(), {
10
12
  orientation: 'vertical',
13
+ descriptionPlacement: 'under-input',
14
+ descriptionCheckbox: false,
11
15
  });
16
+
17
+ provide(FIELD_DESCRIPTION_CHECKBOX_KEY, toRef(props, 'descriptionCheckbox'));
18
+
19
+ const slots = useSlots();
20
+
21
+ const isResponsive = computed(() => props.orientation === 'responsive');
22
+
23
+ const shouldGroupLabel = computed(
24
+ () => isResponsive.value && props.descriptionPlacement === 'under-label',
25
+ );
26
+
27
+ const shouldGroupInput = computed(
28
+ () => isResponsive.value && props.descriptionPlacement === 'under-input',
29
+ );
30
+
31
+ const placementClass = computed(() => {
32
+ if (isResponsive.value) return '';
33
+ return props.descriptionPlacement === 'under-label'
34
+ ? '[&>[data-slot=field-description]]:order-1 [&>[data-slot=field-content]]:order-2'
35
+ : '[&>[data-slot=field-content]]:order-1 [&>[data-slot=field-description]]:order-2';
36
+ });
37
+
38
+ function getSlotName(vnode: VNode): string | undefined {
39
+ return (vnode.type as Record<string, unknown>)?.name as string | undefined;
40
+ }
41
+
42
+ function partitionSlots(groupNames: string[]) {
43
+ const children = slots.default?.() ?? [];
44
+ const group = children.filter((v) => groupNames.includes(getSlotName(v) ?? ''));
45
+ const rest = children.filter((v) => !groupNames.includes(getSlotName(v) ?? ''));
46
+ return { group, rest };
47
+ }
48
+
49
+ const labelPartition = computed(() => partitionSlots(['UiFieldLabel', 'UiFieldDescription']));
50
+
51
+ const inputPartition = computed(() => {
52
+ const children = slots.default?.() ?? [];
53
+ const content = children.filter((v) => getSlotName(v) === 'UiFieldContent');
54
+ const desc = children.filter((v) => getSlotName(v) === 'UiFieldDescription');
55
+ const rest = children.filter((v) => {
56
+ const n = getSlotName(v);
57
+ return n !== 'UiFieldContent' && n !== 'UiFieldDescription';
58
+ });
59
+ return { group: [...content, ...desc], rest };
60
+ });
61
+
62
+ // Stable function refs to avoid unmount/remount on every render.
63
+ // Inline arrows in <component :is="() => ..."> create new references each
64
+ // render cycle, causing Vue to treat them as different components.
65
+ function renderLabelGroup() {
66
+ return labelPartition.value.group;
67
+ }
68
+ function renderLabelRest() {
69
+ return labelPartition.value.rest;
70
+ }
71
+ function renderInputRest() {
72
+ return inputPartition.value.rest;
73
+ }
74
+ function renderInputGroup() {
75
+ return inputPartition.value.group;
76
+ }
12
77
  </script>
13
78
 
14
79
  <template>
15
- <ShadcnField :orientation="props.orientation">
16
- <slot />
80
+ <ShadcnField
81
+ :orientation="props.orientation"
82
+ :class="placementClass"
83
+ :data-description-placement="props.descriptionPlacement"
84
+ >
85
+ <template v-if="shouldGroupLabel">
86
+ <div class="flex flex-col gap-1.5">
87
+ <component :is="renderLabelGroup" />
88
+ </div>
89
+ <component :is="renderLabelRest" />
90
+ </template>
91
+
92
+ <template v-else-if="shouldGroupInput">
93
+ <component :is="renderInputRest" />
94
+ <div class="flex flex-1 flex-col gap-1.5">
95
+ <component :is="renderInputGroup" />
96
+ </div>
97
+ </template>
98
+
99
+ <template v-else>
100
+ <slot />
101
+ </template>
17
102
  </ShadcnField>
18
103
  </template>
@@ -1,13 +1,34 @@
1
1
  <script setup lang="ts">
2
+ import { computed, inject, ref } from 'vue';
2
3
  import { FieldDescription as ShadcnFieldDescription } from '@/components/core/field';
4
+ import { UiCheckbox } from '@/components/UiCheckbox';
5
+ import type { UiFieldDescriptionProps } from './types';
6
+ import type { UiCheckboxModelValue } from '@/components/UiCheckbox/types';
7
+ import { FIELD_DESCRIPTION_CHECKBOX_KEY } from './keys';
3
8
 
4
9
  defineOptions({
5
10
  name: 'UiFieldDescription',
6
11
  });
12
+
13
+ const props = withDefaults(defineProps<UiFieldDescriptionProps>(), {
14
+ variant: 'default',
15
+ disabled: false,
16
+ });
17
+
18
+ const parentCheckbox = inject(FIELD_DESCRIPTION_CHECKBOX_KEY, ref(false));
19
+
20
+ const showCheckbox = computed(() => props.variant === 'input' || parentCheckbox.value);
21
+
22
+ const checked = defineModel<UiCheckboxModelValue>();
7
23
  </script>
8
24
 
9
25
  <template>
10
- <ShadcnFieldDescription>
26
+ <ShadcnFieldDescription v-if="!showCheckbox">
11
27
  <slot />
12
28
  </ShadcnFieldDescription>
29
+
30
+ <ShadcnFieldDescription v-else class="flex items-center gap-2">
31
+ <UiCheckbox v-model="checked" :name="props.name ?? ''" :disabled="props.disabled" />
32
+ <span><slot /></span>
33
+ </ShadcnFieldDescription>
13
34
  </template>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { FieldLabel as ShadcnFieldLabel } from '@/components/core/field';
3
+ import UiFieldLabelInfo from './UiFieldLabelInfo.vue';
3
4
  import type { UiFieldLabelProps } from './types';
4
5
 
5
6
  defineOptions({
@@ -12,5 +13,6 @@
12
13
  <template>
13
14
  <ShadcnFieldLabel :for="props.for">
14
15
  <slot />
16
+ <UiFieldLabelInfo v-if="props.tooltip" :description="props.tooltip" :icon="props.tooltipIcon" />
15
17
  </ShadcnFieldLabel>
16
18
  </template>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import { UiTooltip } from '@/components/UiTooltip';
3
+ import { UiIconButton } from '@/components/UiIconButton';
4
+ import { UiIcon } from '@/components/UiIcon';
5
+ import type { UiFieldLabelInfoProps } from './types';
6
+
7
+ defineOptions({
8
+ name: 'UiFieldLabelInfo',
9
+ });
10
+
11
+ const props = withDefaults(defineProps<UiFieldLabelInfoProps>(), {
12
+ icon: 'circle-help',
13
+ });
14
+ </script>
15
+
16
+ <template>
17
+ <UiTooltip :content="props.description">
18
+ <UiIconButton variant="ghost" size="icon-sm" :ariaLabel="props.description">
19
+ <UiIcon :name="props.icon" :size="14" class="text-content-on-surface-primary" />
20
+ </UiIconButton>
21
+ </UiTooltip>
22
+ </template>
@@ -4,6 +4,7 @@ export { default as UiFieldDescription } from './UiFieldDescription.vue';
4
4
  export { default as UiFieldError } from './UiFieldError.vue';
5
5
  export { default as UiFieldGroup } from './UiFieldGroup.vue';
6
6
  export { default as UiFieldLabel } from './UiFieldLabel.vue';
7
+ export { default as UiFieldLabelInfo } from './UiFieldLabelInfo.vue';
7
8
  export { default as UiFieldLegend } from './UiFieldLegend.vue';
8
9
  export { default as UiFieldSeparator } from './UiFieldSeparator.vue';
9
10
  export { default as UiFieldSet } from './UiFieldSet.vue';
@@ -0,0 +1,5 @@
1
+ import type { InjectionKey, Ref } from 'vue';
2
+
3
+ export const FIELD_DESCRIPTION_CHECKBOX_KEY: InjectionKey<Ref<boolean | undefined>> = Symbol(
4
+ 'UiFieldDescriptionCheckbox',
5
+ );
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Layout orientation supported by `UiField`.
3
+ * - `vertical`: stacked layout (label above input).
4
+ * - `responsive`: horizontal by default, collapses to vertical on small screens.
3
5
  */
4
- export type UiFieldOrientation = 'vertical' | 'horizontal' | 'responsive';
6
+ export type UiFieldOrientation = 'vertical' | 'responsive';
5
7
 
6
8
  /**
7
9
  * Error shape supported by `UiFieldError`.
@@ -14,6 +16,11 @@ export type UiFieldErrorLike =
14
16
  }
15
17
  | undefined;
16
18
 
19
+ /**
20
+ * Where the description text is rendered relative to the field.
21
+ */
22
+ export type UiFieldDescriptionPlacement = 'under-label' | 'under-input';
23
+
17
24
  /**
18
25
  * A form field wrapper that groups label, input, description, and error messages.
19
26
  * Provides consistent layout and accessibility for form controls.
@@ -28,6 +35,21 @@ export interface UiFieldProps {
28
35
  * @default 'vertical'
29
36
  */
30
37
  orientation?: UiFieldOrientation;
38
+
39
+ /**
40
+ * Where the description is placed relative to the field layout.
41
+ * - `under-label`: description renders between the label and the input.
42
+ * - `under-input`: description renders after the input.
43
+ * @default 'under-input'
44
+ */
45
+ descriptionPlacement?: UiFieldDescriptionPlacement;
46
+
47
+ /**
48
+ * When `true`, the child `UiFieldDescription` renders with a checkbox
49
+ * next to the description text (unless the description sets its own `variant` prop).
50
+ * @default false
51
+ */
52
+ descriptionCheckbox?: boolean;
31
53
  }
32
54
 
33
55
  /**
@@ -40,6 +62,34 @@ export interface UiFieldErrorProps {
40
62
  errors?: UiFieldErrorLike[];
41
63
  }
42
64
 
65
+ /**
66
+ * Visual variant for `UiFieldDescription`.
67
+ */
68
+ export type UiFieldDescriptionVariant = 'default' | 'input';
69
+
70
+ /**
71
+ * Props for `UiFieldDescription` component.
72
+ */
73
+ export interface UiFieldDescriptionProps {
74
+ /**
75
+ * Visual variant of the description.
76
+ * Use `input` to render a checkbox next to the description text.
77
+ * @default 'default'
78
+ */
79
+ variant?: UiFieldDescriptionVariant;
80
+
81
+ /**
82
+ * Name attribute for the checkbox (required when `variant="input"`).
83
+ */
84
+ name?: string;
85
+
86
+ /**
87
+ * Disables the checkbox.
88
+ * @default false
89
+ */
90
+ disabled?: boolean;
91
+ }
92
+
43
93
  /**
44
94
  * Props for `UiFieldLabel` component.
45
95
  */
@@ -49,4 +99,39 @@ export interface UiFieldLabelProps {
49
99
  * Use together with `id` on an input.
50
100
  */
51
101
  for?: string;
102
+
103
+ /**
104
+ * When provided, renders a `UiFieldLabelInfo` icon next to the label
105
+ * that shows this text as a tooltip on hover/focus.
106
+ */
107
+ tooltip?: string;
108
+
109
+ /**
110
+ * Lucide icon name for the tooltip trigger when `tooltip` is set.
111
+ * @default 'circle-help'
112
+ * @see https://lucide.dev/icons
113
+ */
114
+ tooltipIcon?: string;
115
+ }
116
+
117
+ /**
118
+ * An interactive info icon placed next to a field label that reveals a tooltip on hover or focus.
119
+ * Uses a ghost icon button as the trigger for keyboard and pointer accessibility.
120
+ * @category Form Inputs
121
+ * @useCases field hint, label help, input context
122
+ * @keywords field, label, info, tooltip, help, hint
123
+ * @related UiFieldLabel, UiTooltip, UiIconButton
124
+ */
125
+ export interface UiFieldLabelInfoProps {
126
+ /**
127
+ * Tooltip text shown on hover / focus.
128
+ */
129
+ description: string;
130
+
131
+ /**
132
+ * Lucide icon name in kebab-case or PascalCase.
133
+ * @default 'circle-help'
134
+ * @see https://lucide.dev/icons
135
+ */
136
+ icon?: string;
52
137
  }
@@ -1,7 +1,7 @@
1
1
  import '@testing-library/jest-dom/vitest';
2
- import { render } from '@testing-library/vue';
2
+ import { render, waitFor } from '@testing-library/vue';
3
3
  import { describe, expect, test } from 'vitest';
4
- import { UiSelect, UiSelectTrigger, UiSelectContent, UiSelectValue } from '../index';
4
+ import { UiSelect, UiSelectTrigger, UiSelectContent, UiSelectItem, UiSelectValue } from '../index';
5
5
 
6
6
  describe('UiSelectTrigger', () => {
7
7
  test('applies custom class', () => {
@@ -51,4 +51,49 @@ describe('UiSelectTrigger', () => {
51
51
 
52
52
  expect(getByRole('combobox')).toHaveAttribute('aria-label', 'Select your preference');
53
53
  });
54
+
55
+ test('has data-placeholder when no value is selected', () => {
56
+ const { getByRole } = render({
57
+ components: { UiSelect, UiSelectTrigger, UiSelectContent, UiSelectValue },
58
+ template: `
59
+ <UiSelect>
60
+ <UiSelectTrigger>
61
+ <UiSelectValue placeholder="Select" />
62
+ </UiSelectTrigger>
63
+ <UiSelectContent />
64
+ </UiSelect>
65
+ `,
66
+ });
67
+
68
+ const trigger = getByRole('combobox');
69
+ expect(trigger).toHaveAttribute('data-placeholder');
70
+ });
71
+
72
+ test('removes data-placeholder when a value is selected', async () => {
73
+ const { getByRole } = render({
74
+ components: {
75
+ UiSelect,
76
+ UiSelectTrigger,
77
+ UiSelectContent,
78
+ UiSelectItem,
79
+ UiSelectValue,
80
+ },
81
+ template: `
82
+ <UiSelect default-value="option1">
83
+ <UiSelectTrigger>
84
+ <UiSelectValue placeholder="Select" />
85
+ </UiSelectTrigger>
86
+ <UiSelectContent>
87
+ <UiSelectItem value="option1">Option 1</UiSelectItem>
88
+ </UiSelectContent>
89
+ </UiSelect>
90
+ `,
91
+ });
92
+
93
+ await waitFor(() => {
94
+ const trigger = getByRole('combobox');
95
+ expect(trigger).not.toHaveAttribute('data-placeholder');
96
+ expect(trigger).toHaveTextContent('Option 1');
97
+ });
98
+ });
54
99
  });
@@ -9,7 +9,7 @@ const meta: Meta<typeof UiToggle> = {
9
9
  argTypes: {
10
10
  variant: {
11
11
  control: 'select',
12
- options: ['default', 'outline'],
12
+ options: ['default', 'outline', 'soft'],
13
13
  description: 'The visual style variant',
14
14
  },
15
15
  size: {
@@ -94,6 +94,35 @@ export const Outline: Story = {
94
94
  },
95
95
  };
96
96
 
97
+ const softTemplateSource = `<script setup lang="ts">
98
+ import { UiToggle, UiIcon } from '@aleph-alpha/ui-library'
99
+ </script>
100
+
101
+ <template>
102
+ <UiToggle variant="soft" aria-label="Toggle bold">
103
+ <UiIcon name="bold" />
104
+ </UiToggle>
105
+ </template>`;
106
+
107
+ /**
108
+ * Soft variant toggle with a subtle accent background when pressed.
109
+ */
110
+ export const Soft: Story = {
111
+ render: () => ({
112
+ components: { UiToggle, UiIcon },
113
+ template: `<UiToggle variant="soft" aria-label="Toggle bold">
114
+ <UiIcon name="bold" />
115
+ </UiToggle>`,
116
+ }),
117
+ parameters: {
118
+ docs: {
119
+ source: {
120
+ code: softTemplateSource,
121
+ },
122
+ },
123
+ },
124
+ };
125
+
97
126
  const withTextTemplateSource = `<script setup lang="ts">
98
127
  import { UiToggle, UiIcon } from '@aleph-alpha/ui-library'
99
128
  </script>
@@ -234,6 +263,12 @@ import { UiToggle, UiIcon } from '@aleph-alpha/ui-library'
234
263
  <UiToggle variant="outline" default-value aria-label="Toggle italic (pressed)">
235
264
  <UiIcon name="italic" />
236
265
  </UiToggle>
266
+ <UiToggle variant="soft" aria-label="Toggle underline">
267
+ <UiIcon name="underline" />
268
+ </UiToggle>
269
+ <UiToggle variant="soft" default-value aria-label="Toggle underline (pressed)">
270
+ <UiIcon name="underline" />
271
+ </UiToggle>
237
272
  </div>
238
273
  </div>
239
274
 
@@ -280,6 +315,12 @@ import { UiToggle, UiIcon } from '@aleph-alpha/ui-library'
280
315
  <UiToggle disabled variant="outline" default-value aria-label="Toggle underline (pressed)">
281
316
  <UiIcon name="underline" />
282
317
  </UiToggle>
318
+ <UiToggle disabled variant="soft" aria-label="Toggle underline">
319
+ <UiIcon name="underline" />
320
+ </UiToggle>
321
+ <UiToggle disabled variant="soft" default-value aria-label="Toggle underline (pressed)">
322
+ <UiIcon name="underline" />
323
+ </UiToggle>
283
324
  </div>
284
325
  </div>
285
326
  </div>
@@ -309,6 +350,12 @@ export const AllStates: Story = {
309
350
  <UiToggle variant="outline" default-value aria-label="Toggle italic (pressed)">
310
351
  <UiIcon name="italic" />
311
352
  </UiToggle>
353
+ <UiToggle variant="soft" aria-label="Toggle underline">
354
+ <UiIcon name="underline" />
355
+ </UiToggle>
356
+ <UiToggle variant="soft" default-value aria-label="Toggle underline (pressed)">
357
+ <UiIcon name="underline" />
358
+ </UiToggle>
312
359
  </div>
313
360
  </div>
314
361
 
@@ -355,6 +402,12 @@ export const AllStates: Story = {
355
402
  <UiToggle disabled variant="outline" default-value aria-label="Toggle underline (pressed)">
356
403
  <UiIcon name="underline" />
357
404
  </UiToggle>
405
+ <UiToggle disabled variant="soft" aria-label="Toggle underline">
406
+ <UiIcon name="underline" />
407
+ </UiToggle>
408
+ <UiToggle disabled variant="soft" default-value aria-label="Toggle underline (pressed)">
409
+ <UiIcon name="underline" />
410
+ </UiToggle>
358
411
  </div>
359
412
  </div>
360
413
  </div>
@@ -42,6 +42,21 @@ describe('UiToggle', () => {
42
42
  expect(toggle).toHaveClass('border');
43
43
  });
44
44
 
45
+ test('applies soft variant classes', () => {
46
+ const { container } = render(UiToggle, {
47
+ props: { variant: 'soft' },
48
+ slots: { default: 'Toggle' },
49
+ });
50
+ const toggle = container.querySelector('button');
51
+ expect(toggle).toHaveClass('bg-transparent');
52
+ expect(toggle).toHaveClass('data-[state=on]:bg-accent');
53
+ expect(toggle).toHaveClass('data-[state=on]:border-accent-default');
54
+ expect(toggle).toHaveClass('data-[state=open]:bg-accent');
55
+ expect(toggle).toHaveClass('data-[state=open]:border-accent-default');
56
+ expect(toggle).toHaveClass('aria-pressed:bg-accent');
57
+ expect(toggle).toHaveClass('aria-pressed:border-accent-default');
58
+ });
59
+
45
60
  test('applies sm size classes', () => {
46
61
  const { container } = render(UiToggle, {
47
62
  props: { size: 'sm' },
@@ -25,7 +25,7 @@ export interface UiToggleProps {
25
25
  * The visual style variant.
26
26
  * @default 'default'
27
27
  */
28
- variant?: 'default' | 'outline';
28
+ variant?: 'default' | 'outline' | 'soft';
29
29
  /**
30
30
  * The size of the toggle.
31
31
  * @default 'default'
@@ -390,5 +390,26 @@ describe('UiToggleGroup', () => {
390
390
  const items = container.querySelectorAll('[data-slot="toggle-group-item"]');
391
391
  expect(items).toHaveLength(2);
392
392
  });
393
+
394
+ test('applies soft variant classes to items', () => {
395
+ const { container } = render(UiToggleGroup, {
396
+ props: { type: 'single', variant: 'soft' },
397
+ slots: {
398
+ default: {
399
+ components: { UiToggleGroupItem },
400
+ template: '<UiToggleGroupItem value="a">A</UiToggleGroupItem>',
401
+ },
402
+ },
403
+ global: {
404
+ components: { UiToggleGroupItem },
405
+ },
406
+ });
407
+ const item = container.querySelector('[data-slot="toggle-group-item"]');
408
+ expect(item).toHaveClass('bg-transparent');
409
+ expect(item).toHaveClass('data-[state=on]:bg-accent');
410
+ expect(item).toHaveClass('data-[state=on]:border-accent-default');
411
+ expect(item).toHaveClass('aria-pressed:bg-accent');
412
+ expect(item).toHaveClass('aria-pressed:border-accent-default');
413
+ });
393
414
  });
394
415
  });
@@ -11,7 +11,7 @@ interface UiToggleGroupBaseProps {
11
11
  * The visual style variant applied to all items.
12
12
  * @default 'default'
13
13
  */
14
- variant?: 'default' | 'outline';
14
+ variant?: 'default' | 'outline' | 'soft';
15
15
  /**
16
16
  * The size applied to all items.
17
17
  * @default 'default'
@@ -122,7 +122,7 @@ export interface UiToggleGroupItemProps {
122
122
  /**
123
123
  * Override the variant for this specific item.
124
124
  */
125
- variant?: 'default' | 'outline';
125
+ variant?: 'default' | 'outline' | 'soft';
126
126
  /**
127
127
  * Override the size for this specific item.
128
128
  */
@@ -27,6 +27,11 @@ export const buttonVariants = cva(
27
27
  'icon-lg': 'size-10',
28
28
  },
29
29
  },
30
+ compoundVariants: [
31
+ { variant: 'ghost', size: 'icon', class: 'size-auto' },
32
+ { variant: 'ghost', size: 'icon-sm', class: 'size-auto' },
33
+ { variant: 'ghost', size: 'icon-lg', class: 'size-auto' },
34
+ ],
30
35
  defaultVariants: {
31
36
  variant: 'default',
32
37
  size: 'default',
@@ -16,7 +16,7 @@
16
16
  :for="props.for"
17
17
  :class="
18
18
  cn(
19
- 'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
19
+ 'group/field-label peer/field-label flex w-fit items-center gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
20
20
  'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
21
21
  'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
22
22
  props.class,
@@ -8,14 +8,14 @@ export const fieldVariants = cva(
8
8
  orientation: {
9
9
  vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],
10
10
  horizontal: [
11
- 'flex-row items-center',
11
+ 'flex-row items-start',
12
12
  '[&>[data-slot=field-label]]:flex-auto',
13
- 'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
13
+ 'has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
14
14
  ],
15
15
  responsive: [
16
- 'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto',
17
- '@md/field-group:[&>[data-slot=field-label]]:flex-auto',
18
- '@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
16
+ 'flex-row items-start @max-md/field-group:flex-col @max-md/field-group:[&>*]:w-full @max-md/field-group:[&>.sr-only]:w-auto',
17
+ '[&>[data-slot=field-label]]:flex-auto',
18
+ 'has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
19
19
  ],
20
20
  },
21
21
  },
@@ -24,7 +24,7 @@
24
24
  v-bind="forwardedProps"
25
25
  :class="
26
26
  cn(
27
- 'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-md border bg-background-input-default px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
27
+ 'border-input data-[placeholder]:text-muted-foreground data-[placeholder]:bg-background-input-default [&_svg:not([class*=\'text-\'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-md border bg-accent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
28
28
  props.class,
29
29
  )
30
30
  "
@@ -2,13 +2,13 @@
2
2
  import type { ToggleEmits, ToggleProps } from 'reka-ui';
3
3
  import { Toggle, useForwardPropsEmits } from 'reka-ui';
4
4
  import { cn } from '@/lib/utils';
5
- import { toggleVariants } from '.';
5
+ import { ToggleVariants, toggleVariants } from '.';
6
6
 
7
7
  const props = withDefaults(
8
8
  defineProps<
9
9
  ToggleProps & {
10
- variant?: 'default' | 'outline';
11
- size?: 'default' | 'sm' | 'lg';
10
+ variant?: ToggleVariants['variant'];
11
+ size?: ToggleVariants['size'];
12
12
  }
13
13
  >(),
14
14
  {
@@ -3,12 +3,15 @@ import { cva, type VariantProps } from 'class-variance-authority';
3
3
  export { default as Toggle } from './Toggle.vue';
4
4
 
5
5
  export const toggleVariants = cva(
6
- 'inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-hover-default outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent-default data-[state=on]:text-accent-default-foreground data-[state=on]:hover:bg-accent-default data-[state=on]:hover:text-accent-default-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
6
+ 'inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-hover-default outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
7
7
  {
8
8
  variants: {
9
9
  variant: {
10
- default: 'bg-transparent',
11
- outline: 'border border-input bg-transparent shadow-sm focus-visible:border-ring',
10
+ default:
11
+ 'bg-transparent data-[state=on]:bg-accent-default data-[state=on]:text-accent-default-foreground data-[state=on]:hover:bg-accent-default data-[state=on]:hover:text-accent-default-foreground data-[state=open]:bg-accent-default data-[state=open]:text-accent-default-foreground data-[state=open]:hover:bg-accent-default data-[state=open]:hover:text-accent-default-foreground aria-pressed:bg-accent-default aria-pressed:text-accent-default-foreground aria-pressed:hover:bg-accent-default aria-pressed:hover:text-accent-default-foreground',
12
+ outline:
13
+ 'border border-input bg-transparent shadow-sm focus-visible:border-ring data-[state=on]:bg-accent-default data-[state=on]:text-accent-default-foreground data-[state=on]:hover:bg-accent-default data-[state=on]:hover:text-accent-default-foreground data-[state=open]:bg-accent-default data-[state=open]:text-accent-default-foreground data-[state=open]:hover:bg-accent-default data-[state=open]:hover:text-accent-default-foreground aria-pressed:bg-accent-default aria-pressed:text-accent-default-foreground aria-pressed:hover:bg-accent-default aria-pressed:hover:text-accent-default-foreground',
14
+ soft: 'bg-transparent data-[state=on]:bg-accent data-[state=on]:text-accent-foreground data-[state=on]:border data-[state=on]:border-accent-default data-[state=on]:hover:bg-accent data-[state=on]:hover:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:border data-[state=open]:border-accent-default data-[state=open]:hover:bg-accent data-[state=open]:hover:text-accent-foreground aria-pressed:bg-accent aria-pressed:text-accent-foreground aria-pressed:border aria-pressed:border-accent-default aria-pressed:hover:bg-accent aria-pressed:hover:text-accent-foreground',
12
15
  },
13
16
  size: {
14
17
  default: 'h-9 px-2 min-w-9',