@aleph-alpha/ui-library 1.16.0 → 1.17.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.
- package/dist/system/index.d.ts +585 -402
- package/dist/system/lib.js +13063 -12825
- package/package.json +3 -4
- package/src/components/UiChip/UiChip.stories.ts +239 -0
- package/src/components/UiChip/UiChip.vue +128 -0
- package/src/components/UiChip/__tests__/UiChip.test.ts +102 -0
- package/src/components/UiChip/index.ts +2 -0
- package/src/components/UiChip/types.ts +50 -0
- package/src/components/UiDropdownMenu/UiDropdownMenu.stories.ts +259 -1
- package/src/components/UiField/UiField.stories.ts +589 -249
- package/src/components/UiField/UiField.vue +87 -2
- package/src/components/UiField/UiFieldDescription.vue +22 -1
- package/src/components/UiField/UiFieldLabel.vue +2 -0
- package/src/components/UiField/UiFieldLabelInfo.vue +22 -0
- package/src/components/UiField/index.ts +1 -0
- package/src/components/UiField/keys.ts +5 -0
- package/src/components/UiField/types.ts +86 -1
- package/src/components/UiSelect/__tests__/UiSelectTrigger.test.ts +47 -2
- package/src/components/UiToggle/UiToggle.stories.ts +54 -1
- package/src/components/UiToggle/__tests__/UiToggle.test.ts +15 -0
- package/src/components/UiToggle/types.ts +1 -1
- package/src/components/UiToggleGroup/__tests__/UiToggleGroup.test.ts +21 -0
- package/src/components/UiToggleGroup/types.ts +2 -2
- package/src/components/core/button/index.ts +8 -3
- package/src/components/core/field/FieldLabel.vue +1 -1
- package/src/components/core/field/index.ts +5 -5
- package/src/components/core/select/SelectTrigger.vue +1 -1
- package/src/components/core/toggle/Toggle.vue +3 -3
- package/src/components/core/toggle/index.ts +6 -3
- package/src/components/core/toggle-group/index.ts +6 -3
- package/src/components/index.ts +1 -0
|
@@ -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
|
|
16
|
-
|
|
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';
|
|
@@ -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' | '
|
|
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' },
|
|
@@ -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
|
*/
|
|
@@ -4,18 +4,18 @@ import { cva } from 'class-variance-authority';
|
|
|
4
4
|
export { default as Button } from './Button.vue';
|
|
5
5
|
|
|
6
6
|
export const buttonVariants = cva(
|
|
7
|
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
7
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md border border-transparent text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
11
11
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
12
12
|
destructive:
|
|
13
|
-
'bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
|
|
13
|
+
'bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:border-transparent focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
|
|
14
14
|
outline:
|
|
15
15
|
'border border-input bg-transparent text-foreground shadow-xs hover:bg-hover-default',
|
|
16
16
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
17
17
|
ghost: 'text-foreground hover:bg-hover-default',
|
|
18
|
-
link: 'text-link underline-offset-4 hover:underline
|
|
18
|
+
link: 'text-link underline-offset-4 hover:underline',
|
|
19
19
|
},
|
|
20
20
|
size: {
|
|
21
21
|
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
|
@@ -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-
|
|
11
|
+
'flex-row items-start',
|
|
12
12
|
'[&>[data-slot=field-label]]:flex-auto',
|
|
13
|
-
'has-[>[data-slot=field-content]]:
|
|
13
|
+
'has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
|
|
14
14
|
],
|
|
15
15
|
responsive: [
|
|
16
|
-
'flex-
|
|
17
|
-
'
|
|
18
|
-
'
|
|
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-
|
|
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?: '
|
|
11
|
-
size?: '
|
|
10
|
+
variant?: ToggleVariants['variant'];
|
|
11
|
+
size?: ToggleVariants['size'];
|
|
12
12
|
}
|
|
13
13
|
>(),
|
|
14
14
|
{
|