@pienter/ui 0.2.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/CHANGELOG.md +64 -0
- package/CONVENTIONS.md +1499 -0
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/components/action/button/Button.astro +95 -0
- package/components/action/button/Button.vue +92 -0
- package/components/action/button/IconButton.astro +86 -0
- package/components/action/button/IconButton.vue +85 -0
- package/components/action/button/button.css +194 -0
- package/components/action/toggle/Toggle.vue +38 -0
- package/components/action/toggle/toggle.css +77 -0
- package/components/action/toggle-group/ToggleGroup.vue +67 -0
- package/components/action/toggle-group/toggle-group.css +78 -0
- package/components/display/avatar/Avatar.astro +17 -0
- package/components/display/avatar/Avatar.vue +30 -0
- package/components/display/avatar/AvatarStack.astro +9 -0
- package/components/display/avatar/AvatarStack.vue +11 -0
- package/components/display/avatar/avatar.css +58 -0
- package/components/display/badge/Badge.astro +15 -0
- package/components/display/badge/Badge.vue +23 -0
- package/components/display/badge/badge.css +56 -0
- package/components/display/empty/Empty.astro +9 -0
- package/components/display/empty/Empty.vue +11 -0
- package/components/display/empty/empty.css +39 -0
- package/components/display/icon/Icon.astro +52 -0
- package/components/display/icon/Icon.vue +57 -0
- package/components/display/icon/icon.css +47 -0
- package/components/feedback/alert/Alert.astro +52 -0
- package/components/feedback/alert/Alert.vue +60 -0
- package/components/feedback/alert/alert.css +78 -0
- package/components/feedback/progress/Progress.astro +68 -0
- package/components/feedback/progress/Progress.vue +82 -0
- package/components/feedback/progress/progress.css +68 -0
- package/components/feedback/skeleton/Skeleton.astro +32 -0
- package/components/feedback/skeleton/Skeleton.vue +39 -0
- package/components/feedback/skeleton/skeleton.css +56 -0
- package/components/feedback/spinner/Spinner.astro +25 -0
- package/components/feedback/spinner/Spinner.vue +36 -0
- package/components/feedback/spinner/spinner.css +91 -0
- package/components/feedback/toast/Toast.astro +50 -0
- package/components/feedback/toast/Toast.vue +74 -0
- package/components/feedback/toast/toast.css +128 -0
- package/components/form/checkbox/Checkbox.astro +79 -0
- package/components/form/checkbox/Checkbox.vue +95 -0
- package/components/form/checkbox/checkbox.css +59 -0
- package/components/form/combobox/Combobox.vue +508 -0
- package/components/form/combobox/combobox.css +110 -0
- package/components/form/date-input/DateInput.astro +105 -0
- package/components/form/date-input/DateInput.vue +121 -0
- package/components/form/date-input/date-input.css +19 -0
- package/components/form/form/Form.astro +106 -0
- package/components/form/form/Form.vue +181 -0
- package/components/form/form/form.css +46 -0
- package/components/form/input-otp/InputOTP.astro +147 -0
- package/components/form/input-otp/InputOTP.vue +209 -0
- package/components/form/input-otp/input-otp.css +52 -0
- package/components/form/label/Label.astro +13 -0
- package/components/form/label/Label.vue +20 -0
- package/components/form/label/label.css +11 -0
- package/components/form/number-field/NumberField.astro +142 -0
- package/components/form/number-field/NumberField.vue +155 -0
- package/components/form/number-field/number-field.css +115 -0
- package/components/form/radio-group/RadioGroup.astro +105 -0
- package/components/form/radio-group/RadioGroup.vue +110 -0
- package/components/form/radio-group/radio-group.css +114 -0
- package/components/form/radio-group/types.ts +14 -0
- package/components/form/select/Segmented.vue +36 -0
- package/components/form/select/Select.astro +105 -0
- package/components/form/select/Select.vue +109 -0
- package/components/form/select/select.css +96 -0
- package/components/form/slider/Slider.astro +205 -0
- package/components/form/slider/Slider.vue +321 -0
- package/components/form/slider/slider.css +115 -0
- package/components/form/switch/Switch.astro +75 -0
- package/components/form/switch/Switch.vue +89 -0
- package/components/form/switch/switch.css +64 -0
- package/components/form/tags-input/TagsInput.astro +153 -0
- package/components/form/tags-input/TagsInput.vue +207 -0
- package/components/form/tags-input/tags-input.css +128 -0
- package/components/form/text-input/TextInput.astro +84 -0
- package/components/form/text-input/TextInput.vue +99 -0
- package/components/form/text-input/text-input.css +165 -0
- package/components/form/textarea/Textarea.astro +86 -0
- package/components/form/textarea/Textarea.vue +102 -0
- package/components/form/textarea/textarea.css +25 -0
- package/components/layout/accordion/Accordion.vue +59 -0
- package/components/layout/accordion/accordion.css +87 -0
- package/components/layout/card/Card.astro +13 -0
- package/components/layout/card/Card.vue +20 -0
- package/components/layout/card/card.css +55 -0
- package/components/layout/collapsible/Collapsible.vue +77 -0
- package/components/layout/collapsible/collapsible.css +76 -0
- package/components/layout/separator/Separator.astro +31 -0
- package/components/layout/separator/Separator.vue +33 -0
- package/components/layout/separator/separator.css +27 -0
- package/components/layout/table/DataTable.vue +127 -0
- package/components/layout/table/Table.astro +116 -0
- package/components/layout/table/Table.vue +146 -0
- package/components/layout/table/TableRow.vue +59 -0
- package/components/layout/table/table.css +201 -0
- package/components/layout/table/types.ts +35 -0
- package/components/layout/table/useTable.ts +7 -0
- package/components/navigation/breadcrumb/Breadcrumb.astro +36 -0
- package/components/navigation/breadcrumb/Breadcrumb.vue +36 -0
- package/components/navigation/breadcrumb/breadcrumb.css +37 -0
- package/components/navigation/navbar/Navbar.astro +62 -0
- package/components/navigation/navbar/Navbar.vue +50 -0
- package/components/navigation/navbar/navbar.css +77 -0
- package/components/navigation/pagination/Pagination.vue +107 -0
- package/components/navigation/pagination/pagination.css +53 -0
- package/components/navigation/sidebar/Sidebar.astro +132 -0
- package/components/navigation/sidebar/Sidebar.vue +174 -0
- package/components/navigation/sidebar/SidebarItemRender.astro +83 -0
- package/components/navigation/sidebar/SidebarItemRender.vue +98 -0
- package/components/navigation/sidebar/sidebar.css +303 -0
- package/components/navigation/sidebar/types.ts +72 -0
- package/components/navigation/tabs/Tabs.vue +84 -0
- package/components/navigation/tabs/tabs.css +39 -0
- package/components/overlay/alert-dialog/AlertDialog.astro +112 -0
- package/components/overlay/alert-dialog/AlertDialog.vue +117 -0
- package/components/overlay/alert-dialog/alert-dialog.css +57 -0
- package/components/overlay/command/Command.vue +356 -0
- package/components/overlay/command/command.css +179 -0
- package/components/overlay/dropdown-menu/DropdownMenu.vue +143 -0
- package/components/overlay/dropdown-menu/dropdown-menu.css +120 -0
- package/components/overlay/modal/Modal.astro +66 -0
- package/components/overlay/modal/Modal.vue +85 -0
- package/components/overlay/modal/modal.css +60 -0
- package/components/overlay/popover/Popover.vue +113 -0
- package/components/overlay/popover/popover.css +53 -0
- package/components/overlay/sheet/Sheet.vue +88 -0
- package/components/overlay/sheet/sheet.css +108 -0
- package/components/overlay/tooltip/Tooltip.vue +210 -0
- package/components/overlay/tooltip/tooltip.css +50 -0
- package/composables/useUrlSort.ts +48 -0
- package/icons/alert-triangle.ts +1 -0
- package/icons/arrow-down.ts +1 -0
- package/icons/arrow-up-down.ts +5 -0
- package/icons/arrow-up.ts +1 -0
- package/icons/bell.ts +1 -0
- package/icons/check.ts +1 -0
- package/icons/chevron-down.ts +1 -0
- package/icons/chevron-left.ts +1 -0
- package/icons/chevron-right.ts +1 -0
- package/icons/chevron-up-down.ts +5 -0
- package/icons/chevron-up.ts +1 -0
- package/icons/circle-alert.ts +1 -0
- package/icons/circle-check.ts +1 -0
- package/icons/clipboard.ts +1 -0
- package/icons/download.ts +1 -0
- package/icons/edit.ts +1 -0
- package/icons/external-link.ts +1 -0
- package/icons/eye.ts +1 -0
- package/icons/file.ts +1 -0
- package/icons/filter.ts +1 -0
- package/icons/folder.ts +1 -0
- package/icons/image.ts +1 -0
- package/icons/inbox.ts +1 -0
- package/icons/index.ts +91 -0
- package/icons/info.ts +1 -0
- package/icons/layers.ts +1 -0
- package/icons/link-2.ts +1 -0
- package/icons/list.ts +1 -0
- package/icons/loader.ts +3 -0
- package/icons/menu.ts +1 -0
- package/icons/more-horizontal.ts +1 -0
- package/icons/more-vertical.ts +1 -0
- package/icons/plus-circle.ts +1 -0
- package/icons/plus.ts +1 -0
- package/icons/save.ts +1 -0
- package/icons/search.ts +1 -0
- package/icons/send.ts +1 -0
- package/icons/settings.ts +1 -0
- package/icons/tool.ts +1 -0
- package/icons/trash-2.ts +1 -0
- package/icons/trash.ts +1 -0
- package/icons/upload-cloud.ts +1 -0
- package/icons/upload.ts +1 -0
- package/icons/x.ts +1 -0
- package/package.json +150 -0
- package/styles/0-settings/colors.css +241 -0
- package/styles/0-settings/index.css +5 -0
- package/styles/0-settings/layout.css +52 -0
- package/styles/0-settings/motion.css +11 -0
- package/styles/0-settings/spacing.css +15 -0
- package/styles/0-settings/typography.css +37 -0
- package/styles/0-utils/index.css +1 -0
- package/styles/1-reset/index.css +1 -0
- package/styles/1-reset/reset.css +26 -0
- package/styles/2-base/base.css +42 -0
- package/styles/2-base/forms.css +23 -0
- package/styles/2-base/index.css +2 -0
- package/styles/3-layout/container.css +57 -0
- package/styles/3-layout/index.css +2 -0
- package/styles/3-layout/section.css +17 -0
- package/styles/5-utilities/accessibility.css +13 -0
- package/styles/5-utilities/index.css +2 -0
- package/styles/5-utilities/text.css +5 -0
- package/styles/main.css +8 -0
- package/styles/styles.d.ts +6 -0
- package/utils/a11y/focus.ts +68 -0
- package/utils/a11y/id.ts +10 -0
- package/utils/a11y/index.ts +9 -0
- package/utils/a11y/keyboard.ts +32 -0
- package/utils/a11y/live-region.ts +36 -0
- package/utils/controllers/dialog.ts +205 -0
- package/utils/controllers/disclosure.ts +117 -0
- package/utils/controllers/form.ts +524 -0
- package/utils/controllers/index.ts +39 -0
- package/utils/controllers/menu.ts +255 -0
- package/utils/controllers/number-field.ts +103 -0
- package/utils/controllers/otp.ts +252 -0
- package/utils/controllers/popover.ts +434 -0
- package/utils/controllers/sidebar.ts +610 -0
- package/utils/controllers/slider.ts +336 -0
- package/utils/controllers/tags-input.ts +255 -0
- package/utils/controllers/toast.ts +426 -0
- package/utils/dom/index.ts +1 -0
- package/utils/dom/scroll-lock.ts +48 -0
- package/utils/index.ts +3 -0
- package/utils/sort/index.ts +3 -0
- package/utils/sort/serialize.ts +19 -0
- package/utils/sort/state.ts +11 -0
- package/utils/sort/types.ts +15 -0
- package/utils/validation/form.ts +93 -0
- package/utils/validation/index.ts +13 -0
- package/utils/validation/rules.ts +31 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
import './toast.css';
|
|
3
|
+
import Icon from '../../display/icon/Icon.astro';
|
|
4
|
+
import type { IconName } from '../../../icons/index.js';
|
|
5
|
+
|
|
6
|
+
type Tone = 'brand' | 'success' | 'warning' | 'danger';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
tone?: Tone;
|
|
10
|
+
title: string;
|
|
11
|
+
message?: string;
|
|
12
|
+
dismissible?: boolean;
|
|
13
|
+
icon?: IconName;
|
|
14
|
+
action?: { label: string };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const toneIconMap: Record<Tone, IconName> = {
|
|
18
|
+
brand: 'info',
|
|
19
|
+
success: 'circle-check',
|
|
20
|
+
warning: 'alert-triangle',
|
|
21
|
+
danger: 'circle-alert',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const { tone, title, message, dismissible = true, icon, action } = Astro.props;
|
|
25
|
+
const resolvedIcon: IconName = icon ?? (tone ? toneIconMap[tone] : 'info');
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<div class="pui-toast" data-tone={tone}>
|
|
29
|
+
<span class="pui-toast__icon" aria-hidden="true">
|
|
30
|
+
<Icon name={resolvedIcon} size="sm" />
|
|
31
|
+
</span>
|
|
32
|
+
<div class="pui-toast__body">
|
|
33
|
+
<p class="pui-toast__title">{title}</p>
|
|
34
|
+
{message && <p class="pui-toast__msg">{message}</p>}
|
|
35
|
+
</div>
|
|
36
|
+
{
|
|
37
|
+
action && (
|
|
38
|
+
<button class="pui-toast__action" type="button">
|
|
39
|
+
{action.label}
|
|
40
|
+
</button>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
{
|
|
44
|
+
dismissible && (
|
|
45
|
+
<button class="pui-toast__close" type="button" aria-label="Close">
|
|
46
|
+
<Icon name="x" size="sm" />
|
|
47
|
+
</button>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
</div>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="pui-toast" :data-tone="tone">
|
|
3
|
+
<span class="pui-toast__icon" aria-hidden="true">
|
|
4
|
+
<Icon :name="resolvedIcon" size="sm" />
|
|
5
|
+
</span>
|
|
6
|
+
<div class="pui-toast__body">
|
|
7
|
+
<p class="pui-toast__title">{{ title }}</p>
|
|
8
|
+
<p v-if="message" class="pui-toast__msg">{{ message }}</p>
|
|
9
|
+
</div>
|
|
10
|
+
<button
|
|
11
|
+
v-if="action"
|
|
12
|
+
class="pui-toast__action"
|
|
13
|
+
type="button"
|
|
14
|
+
@click="$emit('action')"
|
|
15
|
+
>
|
|
16
|
+
{{ action.label }}
|
|
17
|
+
</button>
|
|
18
|
+
<button
|
|
19
|
+
v-if="dismissible"
|
|
20
|
+
class="pui-toast__close"
|
|
21
|
+
type="button"
|
|
22
|
+
aria-label="Close"
|
|
23
|
+
@click="$emit('dismiss')"
|
|
24
|
+
>
|
|
25
|
+
<Icon name="x" size="sm" />
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import { computed } from 'vue';
|
|
32
|
+
import Icon from '../../display/icon/Icon.vue';
|
|
33
|
+
import type { IconName } from '../../../icons/index.js';
|
|
34
|
+
|
|
35
|
+
type Tone = 'brand' | 'success' | 'warning' | 'danger';
|
|
36
|
+
|
|
37
|
+
const toneIconMap: Record<Tone, IconName> = {
|
|
38
|
+
brand: 'info',
|
|
39
|
+
success: 'circle-check',
|
|
40
|
+
warning: 'alert-triangle',
|
|
41
|
+
danger: 'circle-alert',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const props = withDefaults(
|
|
45
|
+
defineProps<{
|
|
46
|
+
tone?: Tone;
|
|
47
|
+
title: string;
|
|
48
|
+
message?: string;
|
|
49
|
+
dismissible?: boolean;
|
|
50
|
+
icon?: IconName;
|
|
51
|
+
action?: { label: string };
|
|
52
|
+
}>(),
|
|
53
|
+
{
|
|
54
|
+
tone: undefined,
|
|
55
|
+
message: undefined,
|
|
56
|
+
dismissible: true,
|
|
57
|
+
icon: undefined,
|
|
58
|
+
action: undefined,
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
defineEmits<{
|
|
63
|
+
dismiss: [];
|
|
64
|
+
action: [];
|
|
65
|
+
}>();
|
|
66
|
+
|
|
67
|
+
const resolvedIcon = computed<IconName>(
|
|
68
|
+
() => props.icon ?? (props.tone ? toneIconMap[props.tone] : 'info'),
|
|
69
|
+
);
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<style>
|
|
73
|
+
@import './toast.css';
|
|
74
|
+
</style>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
/* The global region container — appended once on first toast.show().
|
|
3
|
+
Position fixed bottom-right; flex column with gap so stacked toasts
|
|
4
|
+
space cleanly. v1 only ships bottom-right; the data-position attr
|
|
5
|
+
is reserved for future positions. */
|
|
6
|
+
.pui-toast-region {
|
|
7
|
+
position: fixed;
|
|
8
|
+
inset: auto var(--space-s) var(--space-s) auto;
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: var(--space-2xs);
|
|
12
|
+
z-index: 1000;
|
|
13
|
+
pointer-events: none;
|
|
14
|
+
max-width: calc(100vw - var(--space-s) * 2);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Reactivate pointer events on the toasts themselves so the
|
|
18
|
+
region's `pointer-events: none` lets clicks pass through the
|
|
19
|
+
gaps between toasts. */
|
|
20
|
+
.pui-toast-region > .pui-toast {
|
|
21
|
+
pointer-events: auto;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.pui-toast {
|
|
25
|
+
display: grid;
|
|
26
|
+
grid-template-columns: auto 1fr auto auto;
|
|
27
|
+
align-items: start;
|
|
28
|
+
gap: var(--space-xs);
|
|
29
|
+
padding: var(--space-xs) var(--space-s);
|
|
30
|
+
background: var(--bg-clr-surface);
|
|
31
|
+
border: var(--stroke-md) solid var(--border-clr-strong);
|
|
32
|
+
border-radius: var(--radius-sm);
|
|
33
|
+
box-shadow: var(--shadow-spark);
|
|
34
|
+
max-width: 28rem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.pui-toast__icon {
|
|
38
|
+
width: 1.6rem;
|
|
39
|
+
height: 1.6rem;
|
|
40
|
+
border-radius: 50%;
|
|
41
|
+
background: var(--bg-clr-dark);
|
|
42
|
+
color: var(--text-clr-inverse);
|
|
43
|
+
display: grid;
|
|
44
|
+
place-items: center;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.pui-toast__body {
|
|
48
|
+
display: grid;
|
|
49
|
+
gap: 2px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.pui-toast__title {
|
|
53
|
+
font-weight: var(--fw-bold);
|
|
54
|
+
margin: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.pui-toast__msg {
|
|
58
|
+
margin: 0;
|
|
59
|
+
color: var(--text-clr-muted);
|
|
60
|
+
font-size: var(--step--1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Action button — same baseline as the close button (no chrome) but
|
|
64
|
+
the label is visible. Clicking invokes the action callback and
|
|
65
|
+
dismisses the toast. */
|
|
66
|
+
.pui-toast__action {
|
|
67
|
+
appearance: none;
|
|
68
|
+
border: 0;
|
|
69
|
+
background: transparent;
|
|
70
|
+
color: var(--text-clr-link);
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
padding: 0;
|
|
73
|
+
line-height: 1;
|
|
74
|
+
font: inherit;
|
|
75
|
+
font-weight: var(--fw-bold);
|
|
76
|
+
align-self: center;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.pui-toast__action:hover {
|
|
80
|
+
color: var(--text-clr-link-hover);
|
|
81
|
+
text-decoration: underline;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.pui-toast__action:focus-visible {
|
|
85
|
+
outline: var(--stroke-md) solid var(--outline-clr-base);
|
|
86
|
+
outline-offset: 2px;
|
|
87
|
+
border-radius: var(--radius-xs);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.pui-toast__close {
|
|
91
|
+
appearance: none;
|
|
92
|
+
border: 0;
|
|
93
|
+
background: transparent;
|
|
94
|
+
color: var(--text-clr-muted);
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
padding: 0;
|
|
97
|
+
line-height: 1;
|
|
98
|
+
display: grid;
|
|
99
|
+
place-items: center;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.pui-toast__close:hover {
|
|
103
|
+
color: var(--text-clr-base);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.pui-toast__close:focus-visible {
|
|
107
|
+
outline: var(--stroke-md) solid var(--outline-clr-base);
|
|
108
|
+
outline-offset: 2px;
|
|
109
|
+
border-radius: var(--radius-xs);
|
|
110
|
+
color: var(--text-clr-base);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.pui-toast[data-tone='success'] .pui-toast__icon {
|
|
114
|
+
background: var(--bg-clr-success);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.pui-toast[data-tone='warning'] .pui-toast__icon {
|
|
118
|
+
background: var(--bg-clr-warning);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.pui-toast[data-tone='danger'] .pui-toast__icon {
|
|
122
|
+
background: var(--bg-clr-danger);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.pui-toast[data-tone='brand'] .pui-toast__icon {
|
|
126
|
+
background: var(--bg-clr-brand);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
import './checkbox.css';
|
|
3
|
+
import { generateId } from '../../../utils/a11y/id.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
id?: string;
|
|
7
|
+
label: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
checked?: boolean;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
hint?: string;
|
|
13
|
+
errors?: string[];
|
|
14
|
+
status?: 'error' | 'success';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
id,
|
|
19
|
+
label,
|
|
20
|
+
name,
|
|
21
|
+
checked = false,
|
|
22
|
+
required = false,
|
|
23
|
+
disabled = false,
|
|
24
|
+
hint,
|
|
25
|
+
errors = [],
|
|
26
|
+
status,
|
|
27
|
+
...rest
|
|
28
|
+
} = Astro.props;
|
|
29
|
+
|
|
30
|
+
const inputId = id ?? generateId('checkbox');
|
|
31
|
+
const hintId = `${inputId}-hint`;
|
|
32
|
+
const errorsId = `${inputId}-errors`;
|
|
33
|
+
|
|
34
|
+
const hasErrors = errors.length > 0;
|
|
35
|
+
const computedStatus = status ?? (hasErrors ? 'error' : undefined);
|
|
36
|
+
|
|
37
|
+
const describedByIds: string[] = [];
|
|
38
|
+
if (hint) describedByIds.push(hintId);
|
|
39
|
+
if (hasErrors) describedByIds.push(errorsId);
|
|
40
|
+
const describedBy =
|
|
41
|
+
describedByIds.length > 0 ? describedByIds.join(' ') : undefined;
|
|
42
|
+
|
|
43
|
+
const ariaChecked = checked ? 'true' : 'false';
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<div class="pui-field" data-layout="inline" data-status={computedStatus}>
|
|
47
|
+
<input
|
|
48
|
+
class="pui-checkbox"
|
|
49
|
+
type="checkbox"
|
|
50
|
+
id={inputId}
|
|
51
|
+
name={name}
|
|
52
|
+
checked={checked}
|
|
53
|
+
required={required}
|
|
54
|
+
disabled={disabled}
|
|
55
|
+
aria-checked={ariaChecked}
|
|
56
|
+
aria-invalid={hasErrors ? 'true' : undefined}
|
|
57
|
+
aria-describedby={describedBy}
|
|
58
|
+
{...rest}
|
|
59
|
+
/>
|
|
60
|
+
<label class="pui-field__label" for={inputId}>
|
|
61
|
+
{label}
|
|
62
|
+
</label>
|
|
63
|
+
{
|
|
64
|
+
hint && (
|
|
65
|
+
<p id={hintId} class="pui-field__hint">
|
|
66
|
+
{hint}
|
|
67
|
+
</p>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
{
|
|
71
|
+
hasErrors && (
|
|
72
|
+
<ul id={errorsId} class="pui-field__hint" role="alert">
|
|
73
|
+
{errors.map((error) => (
|
|
74
|
+
<li>{error}</li>
|
|
75
|
+
))}
|
|
76
|
+
</ul>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
</div>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="pui-field" data-layout="inline" :data-status="computedStatus">
|
|
3
|
+
<input
|
|
4
|
+
v-bind="$attrs"
|
|
5
|
+
:id="inputId"
|
|
6
|
+
class="pui-checkbox"
|
|
7
|
+
type="checkbox"
|
|
8
|
+
:name="name"
|
|
9
|
+
:checked="modelValue"
|
|
10
|
+
:required="required"
|
|
11
|
+
:disabled="disabled"
|
|
12
|
+
:aria-checked="ariaCheckedValue"
|
|
13
|
+
:aria-invalid="hasErrors ? 'true' : undefined"
|
|
14
|
+
:aria-describedby="describedBy"
|
|
15
|
+
@change="
|
|
16
|
+
emit(
|
|
17
|
+
'update:modelValue',
|
|
18
|
+
($event.target as HTMLInputElement).checked,
|
|
19
|
+
)
|
|
20
|
+
"
|
|
21
|
+
/>
|
|
22
|
+
<label class="pui-field__label" :for="inputId">{{ label }}</label>
|
|
23
|
+
<p v-if="hint" :id="hintId" class="pui-field__hint">{{ hint }}</p>
|
|
24
|
+
<ul
|
|
25
|
+
v-if="errors.length"
|
|
26
|
+
:id="errorsId"
|
|
27
|
+
class="pui-field__hint"
|
|
28
|
+
role="alert"
|
|
29
|
+
>
|
|
30
|
+
<li v-for="error in errors" :key="error">{{ error }}</li>
|
|
31
|
+
</ul>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup lang="ts">
|
|
36
|
+
import { computed } from 'vue';
|
|
37
|
+
import { generateId } from '../../../utils/a11y/id.js';
|
|
38
|
+
|
|
39
|
+
defineOptions({ inheritAttrs: false });
|
|
40
|
+
|
|
41
|
+
const props = withDefaults(
|
|
42
|
+
defineProps<{
|
|
43
|
+
id?: string;
|
|
44
|
+
label: string;
|
|
45
|
+
modelValue?: boolean;
|
|
46
|
+
name?: string;
|
|
47
|
+
required?: boolean;
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
hint?: string;
|
|
50
|
+
errors?: string[];
|
|
51
|
+
status?: 'error' | 'success';
|
|
52
|
+
}>(),
|
|
53
|
+
{
|
|
54
|
+
id: undefined,
|
|
55
|
+
modelValue: false,
|
|
56
|
+
name: undefined,
|
|
57
|
+
required: false,
|
|
58
|
+
disabled: false,
|
|
59
|
+
hint: undefined,
|
|
60
|
+
errors: () => [],
|
|
61
|
+
status: undefined,
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const emit = defineEmits<{
|
|
66
|
+
'update:modelValue': [value: boolean];
|
|
67
|
+
}>();
|
|
68
|
+
|
|
69
|
+
const inputId = props.id ?? generateId('checkbox');
|
|
70
|
+
const hintId = `${inputId}-hint`;
|
|
71
|
+
const errorsId = `${inputId}-errors`;
|
|
72
|
+
|
|
73
|
+
const hasErrors = computed(() => props.errors.length > 0);
|
|
74
|
+
|
|
75
|
+
const computedStatus = computed(() => {
|
|
76
|
+
if (props.status) return props.status;
|
|
77
|
+
if (hasErrors.value) return 'error';
|
|
78
|
+
return undefined;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const describedBy = computed(() => {
|
|
82
|
+
const ids: string[] = [];
|
|
83
|
+
if (props.hint) ids.push(hintId);
|
|
84
|
+
if (hasErrors.value) ids.push(errorsId);
|
|
85
|
+
return ids.length > 0 ? ids.join(' ') : undefined;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const ariaCheckedValue = computed<'true' | 'false'>(() =>
|
|
89
|
+
props.modelValue ? 'true' : 'false',
|
|
90
|
+
);
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<style>
|
|
94
|
+
@import './checkbox.css';
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.pui-checkbox {
|
|
3
|
+
appearance: none;
|
|
4
|
+
flex: none;
|
|
5
|
+
margin: 0;
|
|
6
|
+
width: 1.15em;
|
|
7
|
+
height: 1.15em;
|
|
8
|
+
border: var(--stroke-md) solid var(--border-clr-strong);
|
|
9
|
+
border-radius: var(--radius-xs);
|
|
10
|
+
background: var(--bg-clr-surface);
|
|
11
|
+
display: grid;
|
|
12
|
+
place-items: center;
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
transition:
|
|
15
|
+
background 0.12s ease,
|
|
16
|
+
border-color 0.12s ease;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.pui-checkbox:checked {
|
|
20
|
+
background: var(--bg-clr-brand);
|
|
21
|
+
border-color: var(--border-clr-brand);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Checkmark for the binary-checked state. */
|
|
25
|
+
.pui-checkbox:checked::after {
|
|
26
|
+
content: '';
|
|
27
|
+
width: 0.3em;
|
|
28
|
+
height: 0.55em;
|
|
29
|
+
border: solid var(--bg-clr-brand-ink);
|
|
30
|
+
border-width: 0 2px 2px 0;
|
|
31
|
+
transform: rotate(45deg);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.pui-checkbox:focus-visible {
|
|
35
|
+
outline: 3px solid var(--outline-clr-base);
|
|
36
|
+
outline-offset: 2px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.pui-checkbox:disabled {
|
|
40
|
+
opacity: 0.5;
|
|
41
|
+
cursor: not-allowed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* Status-driven recolour from the parent pui-field wrapper. */
|
|
45
|
+
.pui-field[data-status='error'] .pui-checkbox {
|
|
46
|
+
border-color: var(--border-clr-danger);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.pui-field[data-status='success'] .pui-checkbox:checked {
|
|
50
|
+
background: var(--bg-clr-success, var(--bg-clr-brand));
|
|
51
|
+
border-color: var(--bg-clr-success, var(--border-clr-brand));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Mute the label + cursor when the input is disabled. */
|
|
55
|
+
.pui-field:has(.pui-checkbox:disabled) .pui-field__label {
|
|
56
|
+
color: var(--text-clr-muted);
|
|
57
|
+
cursor: not-allowed;
|
|
58
|
+
}
|
|
59
|
+
}
|