picocrank 1.21.5 → 1.22.1

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.
@@ -0,0 +1,191 @@
1
+ <template>
2
+ <div
3
+ :class="rootClasses"
4
+ role="radiogroup"
5
+ :aria-labelledby="ariaLabelledby || undefined"
6
+ :aria-label="ariaLabel || undefined"
7
+ :aria-disabled="disabled ? 'true' : undefined"
8
+ >
9
+ <label
10
+ v-for="option in normalizedOptions"
11
+ :key="option.key"
12
+ :class="{ disabled: disabled || option.disabled }"
13
+ >
14
+ <input
15
+ type="radio"
16
+ :name="groupName"
17
+ :value="option.key"
18
+ :checked="isSelected(option)"
19
+ :disabled="disabled || option.disabled"
20
+ @change="select(option)"
21
+ />
22
+ <span>{{ option.label }}</span>
23
+ </label>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup>
28
+ import { computed, useId } from 'vue'
29
+
30
+ const props = defineProps({
31
+ modelValue: {
32
+ type: [String, Number, Boolean, null],
33
+ default: null,
34
+ },
35
+ options: {
36
+ type: Array,
37
+ required: true,
38
+ validator: (options) => Array.isArray(options) && options.every((option) => {
39
+ if (typeof option === 'string' || typeof option === 'number') {
40
+ return true
41
+ }
42
+ return option
43
+ && typeof option === 'object'
44
+ && 'value' in option
45
+ && typeof option.label === 'string'
46
+ }),
47
+ },
48
+ name: {
49
+ type: String,
50
+ default: '',
51
+ },
52
+ variant: {
53
+ type: String,
54
+ default: 'default',
55
+ validator: (value) => ['default', 'boolean', 'list'].includes(value),
56
+ },
57
+ disabled: {
58
+ type: Boolean,
59
+ default: false,
60
+ },
61
+ ariaLabelledby: {
62
+ type: String,
63
+ default: '',
64
+ },
65
+ ariaLabel: {
66
+ type: String,
67
+ default: '',
68
+ },
69
+ })
70
+
71
+ const emit = defineEmits(['update:modelValue', 'change'])
72
+
73
+ const generatedName = useId()
74
+
75
+ const groupName = computed(() => props.name || generatedName)
76
+
77
+ const rootClasses = computed(() => ({
78
+ 'radio-group': true,
79
+ 'radio-boolean': props.variant === 'boolean',
80
+ 'radio-list': props.variant === 'list',
81
+ disabled: props.disabled,
82
+ }))
83
+
84
+ const normalizedOptions = computed(() => props.options.map((option, index) => {
85
+ if (typeof option === 'string' || typeof option === 'number') {
86
+ return {
87
+ key: String(index),
88
+ label: String(option),
89
+ value: option,
90
+ disabled: false,
91
+ }
92
+ }
93
+
94
+ return {
95
+ key: String(index),
96
+ label: option.label,
97
+ value: option.value,
98
+ disabled: Boolean(option.disabled),
99
+ }
100
+ }))
101
+
102
+ function isSelected(option) {
103
+ return props.modelValue === option.value
104
+ }
105
+
106
+ function select(option) {
107
+ if (props.disabled || option.disabled) {
108
+ return
109
+ }
110
+ emit('update:modelValue', option.value)
111
+ emit('change', option.value)
112
+ }
113
+ </script>
114
+
115
+ <style scoped>
116
+ /*
117
+ * Default and boolean variants: hide the native radio chrome; keep the input
118
+ * for semantics/keyboard. List (stacked) keeps the visible radio icon.
119
+ */
120
+ .radio-group label {
121
+ display: flex;
122
+ align-items: center;
123
+ box-sizing: border-box;
124
+ padding-inline: 0.75em;
125
+ gap: 0.5em;
126
+ }
127
+
128
+ .radio-group:not(.radio-list) label {
129
+ position: relative;
130
+ justify-content: center;
131
+ }
132
+
133
+ .radio-group input[type="radio"] {
134
+ margin: 0;
135
+ flex-shrink: 0;
136
+ }
137
+
138
+ .radio-group:not(.radio-list) input[type="radio"] {
139
+ position: absolute;
140
+ inset: 0;
141
+ width: 100%;
142
+ height: 100%;
143
+ padding: 0;
144
+ opacity: 0;
145
+ cursor: inherit;
146
+ z-index: 1;
147
+ }
148
+
149
+ .radio-group label:has(input:focus-visible) {
150
+ outline: 2px solid black;
151
+ outline-offset: 2px;
152
+ z-index: 2;
153
+ }
154
+
155
+ /* Beat Femtocrank's gray .radio-group focus/hover when the option is selected. */
156
+ .radio-group label:has(input:checked):hover,
157
+ .radio-group label:has(input:checked:focus-visible) {
158
+ background-color: #3a6f3a;
159
+ color: white;
160
+ }
161
+
162
+ .radio-group.disabled {
163
+ opacity: 0.7;
164
+ cursor: not-allowed;
165
+ }
166
+
167
+ .radio-group label.disabled,
168
+ .radio-group.disabled label {
169
+ background-color: var(--disabled-background-color);
170
+ color: var(--disabled-text-color);
171
+ cursor: not-allowed;
172
+ }
173
+
174
+ .radio-group label.disabled:hover,
175
+ .radio-group label.disabled:has(input:focus-visible),
176
+ .radio-group label.disabled:has(input:checked),
177
+ .radio-group label.disabled:has(input:checked):hover,
178
+ .radio-group.disabled label:hover,
179
+ .radio-group.disabled label:has(input:focus-visible),
180
+ .radio-group.disabled label:has(input:checked),
181
+ .radio-group.disabled label:has(input:checked):hover {
182
+ background-color: var(--disabled-background-color);
183
+ color: var(--disabled-text-color);
184
+ }
185
+
186
+ .radio-group label.disabled:has(input:focus-visible),
187
+ .radio-group.disabled label:has(input:focus-visible) {
188
+ outline: 2px solid black;
189
+ outline-offset: 2px;
190
+ }
191
+ </style>