webcake-ui-kit 1.0.0 → 1.0.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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +358 -8
  3. package/package.json +68 -5
  4. package/src/components/accordion/Accordion.vue +70 -0
  5. package/src/components/accordion/accordion.css +5 -0
  6. package/src/components/accordion-item/AccordionItem.vue +98 -0
  7. package/src/components/accordion-item/accordion-item.css +143 -0
  8. package/src/components/alert-dialog/AlertDialog.vue +82 -0
  9. package/src/components/alert-dialog/alert-dialog.css +33 -0
  10. package/src/components/badge/Badge.vue +2 -2
  11. package/src/components/badge/badge.css +1 -4
  12. package/src/components/breadcrumb/Breadcrumb.vue +85 -0
  13. package/src/components/breadcrumb/breadcrumb.css +90 -0
  14. package/src/components/button/Button.vue +77 -10
  15. package/src/components/button/button.css +258 -24
  16. package/src/components/button-group/ButtonGroup.vue +25 -0
  17. package/src/components/button-group/button-group.css +30 -0
  18. package/src/components/checkbox/Checkbox.vue +55 -0
  19. package/src/components/checkbox/checkbox.css +86 -0
  20. package/src/components/checkbox-group/CheckboxGroup.vue +50 -0
  21. package/src/components/checkbox-group/checkbox-group.css +35 -0
  22. package/src/components/dialog/Dialog.vue +355 -0
  23. package/src/components/dialog/dialog.css +255 -0
  24. package/src/components/divider/Divider.vue +35 -0
  25. package/src/components/divider/divider.css +38 -0
  26. package/src/components/input/Input.vue +99 -0
  27. package/src/components/input/input.css +123 -0
  28. package/src/components/pagination/Pagination.vue +211 -0
  29. package/src/components/pagination/pagination.css +13 -0
  30. package/src/components/radio/Radio.vue +74 -0
  31. package/src/components/radio/radio.css +89 -0
  32. package/src/components/radio-group/RadioGroup.vue +70 -0
  33. package/src/components/radio-group/radio_group.css +11 -0
  34. package/src/components/rich-checkbox-group/RichCheckboxGroup.vue +59 -0
  35. package/src/components/rich-checkbox-group/rich-checkbox-group.css +54 -0
  36. package/src/components/rich-switch-group/RichSwitchGroup.vue +49 -0
  37. package/src/components/rich-switch-group/rich_switch_group.css +45 -0
  38. package/src/components/select/Select.vue +262 -0
  39. package/src/components/select/select.css +207 -0
  40. package/src/components/select-option/SelectOption.vue +82 -0
  41. package/src/components/select-option/select_option.css +60 -0
  42. package/src/components/sidebar-group-label/SidebarGroupLabel.vue +68 -0
  43. package/src/components/sidebar-group-label/sidebar_group_label.css +61 -0
  44. package/src/components/sidebar-item/SidebarItem.vue +110 -0
  45. package/src/components/sidebar-item/sidebar_item.css +142 -0
  46. package/src/components/slider/Slider.vue +255 -0
  47. package/src/components/slider/slider.css +89 -0
  48. package/src/components/spinner/Spinner.vue +47 -0
  49. package/src/components/spinner/spinner.css +48 -0
  50. package/src/components/switch/Switch.vue +32 -0
  51. package/src/components/switch/switch.css +46 -0
  52. package/src/components/switch-group/SwitchGroup.vue +32 -0
  53. package/src/components/switch-group/switch_group.css +28 -0
  54. package/src/components/tabs/Tabs.vue +57 -0
  55. package/src/components/tabs/tabs.css +118 -0
  56. package/src/components/tag/Tag.vue +47 -0
  57. package/src/components/tag/tag.css +115 -0
  58. package/src/components/toggle/Toggle.vue +112 -0
  59. package/src/components/toggle/toggle.css +174 -0
  60. package/src/components/toggle-group/ToggleGroup.vue +57 -0
  61. package/src/components/toggle-group/toggle-group.css +68 -0
  62. package/src/icons/LoaderIcon.vue +22 -0
  63. package/src/index.js +29 -2
  64. package/src/styles/border_radius.css +3 -3
  65. package/src/styles/color_general.css +21 -14
  66. package/src/styles/shadow.css +2 -2
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <span
3
+ :class="[
4
+ 'ui-input',
5
+ `ui-input--size-${size}`,
6
+ `ui-input--round-${roundness}`,
7
+ error && 'ui-input--error',
8
+ disabled && 'ui-input--disabled'
9
+ ]"
10
+ >
11
+ <span v-if="hasPrefix" class="ui-input__decoration ui-input__prefix" aria-hidden="true">
12
+ <slot name="prefix"></slot>
13
+ </span>
14
+ <input
15
+ ref="input"
16
+ class="ui-input__field"
17
+ :type="type"
18
+ :value="currentValue"
19
+ :placeholder="placeholder"
20
+ :disabled="disabled"
21
+ :readonly="readonly"
22
+ :name="name"
23
+ v-bind="$attrs"
24
+ @input="onInput"
25
+ @focus="onFocus"
26
+ @blur="onBlur"
27
+ @keydown="onKeydown"
28
+ />
29
+ <span v-if="hasSuffix" class="ui-input__decoration ui-input__suffix" aria-hidden="true">
30
+ <slot name="suffix"></slot>
31
+ </span>
32
+ </span>
33
+ </template>
34
+
35
+ <script>
36
+ export default {
37
+ name: 'Input',
38
+ inheritAttrs: false,
39
+ props: {
40
+ size: {
41
+ type: String,
42
+ default: 'md',
43
+ validator: v => ['xs', 'sm', 'md', 'lg'].includes(v)
44
+ },
45
+ roundness: {
46
+ type: String,
47
+ default: 'default',
48
+ validator: v => ['default', 'round'].includes(v)
49
+ },
50
+ value: { type: [String, Number], default: '' },
51
+ modelValue: { type: [String, Number], default: undefined },
52
+ type: { type: String, default: 'text' },
53
+ placeholder: { type: String, default: '' },
54
+ name: { type: String, default: '' },
55
+ error: { type: Boolean, default: false },
56
+ disabled: { type: Boolean, default: false },
57
+ readonly: { type: Boolean, default: false }
58
+ },
59
+ emits: ['input', 'change', 'update:modelValue', 'focus', 'blur', 'pressEnter'],
60
+ computed: {
61
+ currentValue() {
62
+ return this.modelValue !== undefined ? this.modelValue : this.value
63
+ },
64
+ hasPrefix() {
65
+ return !!this.$slots['prefix']
66
+ },
67
+ hasSuffix() {
68
+ return !!this.$slots['suffix']
69
+ }
70
+ },
71
+ methods: {
72
+ focus() {
73
+ if (this.$refs.input) this.$refs.input.focus()
74
+ },
75
+ blur() {
76
+ if (this.$refs.input) this.$refs.input.blur()
77
+ },
78
+ onInput(e) {
79
+ const next = e.target.value
80
+ this.$emit('input', next)
81
+ this.$emit('update:modelValue', next)
82
+ this.$emit('change', next, e)
83
+ },
84
+ onFocus(e) {
85
+ this.$emit('focus', e.target.value, e)
86
+ },
87
+ onBlur(e) {
88
+ this.$emit('blur', e.target.value, e)
89
+ },
90
+ onKeydown(e) {
91
+ if (e.key === 'Enter') {
92
+ this.$emit('pressEnter', e.target.value, e)
93
+ }
94
+ }
95
+ }
96
+ }
97
+ </script>
98
+
99
+ <style src="./input.css" scoped></style>
@@ -0,0 +1,123 @@
1
+ .ui-input {
2
+ position: relative;
3
+ display: inline-flex;
4
+ align-items: center;
5
+ width: 100%;
6
+ max-width: 320px;
7
+ background: var(--input);
8
+ border: 1px solid var(--border-primary);
9
+ box-shadow: var(--shadow-xs);
10
+ font-family: var(--font-family-body);
11
+ color: var(--primary-fg);
12
+ overflow: hidden;
13
+ transition:
14
+ border-color 0.12s ease,
15
+ box-shadow 0.12s ease;
16
+ }
17
+
18
+ /* Size — paddings, gaps, heights, typography. Inner field inherits typography. */
19
+ .ui-input--size-md {
20
+ min-height: 36px;
21
+ gap: var(--spacing-xs);
22
+ padding: var(--spacing-7p5) var(--spacing-sm);
23
+ font-size: var(--paragraph-small-font-size);
24
+ line-height: var(--paragraph-small-line-height);
25
+ letter-spacing: var(--paragraph-small-letter-spacing);
26
+ }
27
+ .ui-input--size-lg {
28
+ min-height: 40px;
29
+ gap: var(--spacing-sm);
30
+ padding: var(--spacing-9p5) var(--spacing-md);
31
+ font-size: var(--paragraph-small-font-size);
32
+ line-height: var(--paragraph-small-line-height);
33
+ letter-spacing: var(--paragraph-small-letter-spacing);
34
+ }
35
+ .ui-input--size-sm {
36
+ min-height: 32px;
37
+ gap: var(--spacing-6);
38
+ padding: var(--spacing-5p5) var(--spacing-xs);
39
+ font-size: var(--paragraph-small-font-size);
40
+ line-height: var(--paragraph-small-line-height);
41
+ letter-spacing: var(--paragraph-small-letter-spacing);
42
+ }
43
+ .ui-input--size-xs {
44
+ min-height: 28px;
45
+ gap: var(--spacing-2xs);
46
+ padding: var(--spacing-6);
47
+ font-size: var(--paragraph-mini-font-size);
48
+ line-height: var(--paragraph-mini-line-height);
49
+ letter-spacing: var(--paragraph-mini-letter-spacing);
50
+ }
51
+
52
+ /* Roundness — Mini uses smaller default radius per Figma. */
53
+ .ui-input--round-default {
54
+ border-radius: var(--rounded-xl);
55
+ }
56
+ .ui-input--size-xs.ui-input--round-default {
57
+ border-radius: var(--rounded-lg);
58
+ }
59
+ .ui-input--round-round {
60
+ border-radius: var(--rounded-full);
61
+ }
62
+
63
+ /* Focus (interactive) — wrapper highlights when inner field is focused. */
64
+ .ui-input:focus-within {
65
+ border-color: var(--border-focus);
66
+ box-shadow: 0 0 0 3px var(--focus-ring);
67
+ }
68
+
69
+ /* Error (logical) — red border, no base shadow. */
70
+ .ui-input--error {
71
+ border-color: var(--destructive-border);
72
+ box-shadow: none;
73
+ }
74
+ .ui-input--error:focus-within {
75
+ border-color: var(--destructive-border);
76
+ box-shadow: 0 0 0 3px var(--focus-ring-error);
77
+ }
78
+
79
+ /* Disabled — visual only; cursor handled by inner field. */
80
+ .ui-input--disabled {
81
+ opacity: 0.5;
82
+ }
83
+ .ui-input--disabled:focus-within {
84
+ border-color: var(--border-primary);
85
+ box-shadow: var(--shadow-xs);
86
+ }
87
+
88
+ /* The native input — strips browser chrome and inherits typography from wrapper. */
89
+ .ui-input__field {
90
+ flex: 1 1 0;
91
+ min-width: 0;
92
+ width: 100%;
93
+ margin: 0;
94
+ padding: 0;
95
+ background: transparent;
96
+ border: 0;
97
+ outline: 0;
98
+ color: inherit;
99
+ font: inherit;
100
+ letter-spacing: inherit;
101
+ }
102
+ .ui-input__field::placeholder {
103
+ color: var(--muted-fg);
104
+ opacity: 1;
105
+ }
106
+ .ui-input__field:disabled {
107
+ cursor: not-allowed;
108
+ }
109
+
110
+ /* Decoration slot wrappers — fixed-size icon containers. */
111
+ .ui-input__decoration {
112
+ display: inline-flex;
113
+ align-items: center;
114
+ justify-content: center;
115
+ flex-shrink: 0;
116
+ width: 20px;
117
+ height: 20px;
118
+ color: var(--muted-fg);
119
+ }
120
+ .ui-input--size-xs .ui-input__decoration {
121
+ width: 16px;
122
+ height: 16px;
123
+ }
@@ -0,0 +1,211 @@
1
+ <template>
2
+ <nav class="ui-pagination" role="navigation" :aria-label="ariaLabel" :class="{ 'ui-pagination--disabled': disabled }">
3
+ <Button variant="ghost" :disabled="isPrevDisabled" @click="goPrev" :label="prevLabel || ''">
4
+ <template v-if="showIcon" #icon-left>
5
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
6
+ <path
7
+ d="M10 4L6 8L10 12"
8
+ stroke="currentColor"
9
+ stroke-width="1.5"
10
+ stroke-linecap="round"
11
+ stroke-linejoin="round"
12
+ />
13
+ </svg>
14
+ </template>
15
+ </Button>
16
+
17
+ <template v-for="(item, idx) in items">
18
+ <Button
19
+ v-if="typeof item === 'number'"
20
+ :key="'page-' + item"
21
+ :variant="currentPage === item ? 'outline' : 'ghost'"
22
+ :disabled="disabled"
23
+ @click="select(item)"
24
+ >
25
+ {{ item }}
26
+ </Button>
27
+ <Button
28
+ v-else
29
+ :key="`ellipsis-${idx}-${item}`"
30
+ variant="ghost"
31
+ :disabled="disabled"
32
+ @click="onEllipsisClick(item)"
33
+ >
34
+ <template #icon>
35
+ <svg
36
+ xmlns="http://www.w3.org/2000/svg"
37
+ width="20"
38
+ height="20"
39
+ viewBox="0 0 24 24"
40
+ fill="none"
41
+ stroke="currentColor"
42
+ stroke-width="1.5"
43
+ stroke-linecap="round"
44
+ stroke-linejoin="round"
45
+ class="lucide lucide-ellipsis-icon lucide-ellipsis"
46
+ >
47
+ <circle cx="12" cy="12" r="1" />
48
+ <circle cx="19" cy="12" r="1" />
49
+ <circle cx="5" cy="12" r="1" />
50
+ </svg>
51
+ </template>
52
+ </Button>
53
+ </template>
54
+
55
+ <Button variant="ghost" :disabled="isNextDisabled" @click="goNext" :label="nextLabel || ''">
56
+ <template v-if="showIcon" #icon-right>
57
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
58
+ <path
59
+ d="M6 4L10 8L6 12"
60
+ stroke="currentColor"
61
+ stroke-width="1.5"
62
+ stroke-linecap="round"
63
+ stroke-linejoin="round"
64
+ />
65
+ </svg>
66
+ </template>
67
+ </Button>
68
+ </nav>
69
+ </template>
70
+
71
+ <script>
72
+ import Button from '../button/Button.vue'
73
+
74
+ function range(start, end) {
75
+ if (end < start) return []
76
+ var r = []
77
+ for (var i = start; i <= end; i++) r.push(i)
78
+ return r
79
+ }
80
+
81
+ export default {
82
+ name: 'Pagination',
83
+ components: { Button },
84
+
85
+ model: {
86
+ prop: 'current',
87
+ event: 'change'
88
+ },
89
+
90
+ props: {
91
+ current: { type: Number, default: 1 },
92
+ total: { type: Number, required: true, validator: v => v >= 0 },
93
+ pageSize: { type: Number, default: 10, validator: v => v >= 1 },
94
+ siblings: { type: Number, default: 1, validator: v => v >= 0 },
95
+ boundary: { type: Number, default: 1, validator: v => v >= 0 },
96
+ showIcon: { type: Boolean, default: false },
97
+ prevLabel: { type: String, default: 'Previous' },
98
+ nextLabel: { type: String, default: 'Next' },
99
+ ariaLabel: { type: String, default: 'Pagination' },
100
+ pageAriaLabel: { type: Function, default: null },
101
+ disabled: { type: Boolean, default: false },
102
+ jumpStep: { type: Number, default: 4 }
103
+ },
104
+
105
+ emits: ['change', 'update:modelValue'],
106
+
107
+ computed: {
108
+ totalPages: function () {
109
+ if (this.total <= 0) return 0
110
+ return Math.max(1, Math.ceil(this.total / this.pageSize))
111
+ },
112
+ currentPage: function () {
113
+ if (this.totalPages <= 0) return 1
114
+ return Math.max(1, Math.min(this.current, this.totalPages))
115
+ },
116
+ items: function () {
117
+ return this.computeItems(this.currentPage, this.totalPages, this.siblings, this.boundary)
118
+ },
119
+ isPrevDisabled: function () {
120
+ return this.disabled || this.currentPage <= 1 || this.totalPages <= 0
121
+ },
122
+ isNextDisabled: function () {
123
+ return this.disabled || this.currentPage >= this.totalPages || this.totalPages <= 0
124
+ },
125
+ effectiveJumpStep: function () {
126
+ return Math.max(0, Math.floor(this.jumpStep))
127
+ }
128
+ },
129
+
130
+ methods: {
131
+ // Smart pagination range — only inserts ellipses when an actual gap > 1 exists,
132
+ // and expands the visible range on the non-elided side when current is near a boundary.
133
+ // Output items are integers (page numbers) or the strings 'ellipsis-start' / 'ellipsis-end'.
134
+ computeItems: function (current, total, siblings, boundary) {
135
+ if (!total || total <= 0) return []
136
+ var s = Math.max(0, siblings)
137
+ var b = Math.max(0, boundary)
138
+
139
+ // Max numbers that can render WITHOUT any ellipsis.
140
+ // Layout: [boundary] [start-ellipsis] [current ± siblings] [end-ellipsis] [boundary]
141
+ // Without ellipses, the visible run would need at least b + (2s + 1) + b = 2b + 2s + 1.
142
+ // We also account for the 2 ellipsis slots (which would otherwise be page numbers): 2b + 2s + 3.
143
+ var noEllipsisThreshold = b * 2 + s * 2 + 3
144
+ if (total <= noEllipsisThreshold) return range(1, total)
145
+
146
+ var leftSibling = Math.max(current - s, b + 1)
147
+ var rightSibling = Math.min(current + s, total - b)
148
+
149
+ // Only emit an ellipsis when the gap it covers is > 1 page (otherwise show that page).
150
+ var showLeftEllipsis = leftSibling > b + 2
151
+ var showRightEllipsis = rightSibling < total - b - 1
152
+
153
+ var startPages = range(1, b)
154
+ var endPages = range(total - b + 1, total)
155
+
156
+ if (!showLeftEllipsis && showRightEllipsis) {
157
+ // Current is near the start — extend the left run to fill the slot a left ellipsis would occupy.
158
+ var leftCount = b + s * 2 + 1
159
+ return range(1, leftCount).concat(['ellipsis-end']).concat(endPages)
160
+ }
161
+
162
+ if (showLeftEllipsis && !showRightEllipsis) {
163
+ // Current is near the end — extend the right run.
164
+ var rightCount = b + s * 2 + 1
165
+ return startPages.concat(['ellipsis-start']).concat(range(total - rightCount + 1, total))
166
+ }
167
+
168
+ // Both ellipses needed (current is in the middle, and gaps on both sides are > 1)
169
+ return startPages
170
+ .concat(['ellipsis-start'])
171
+ .concat(range(leftSibling, rightSibling))
172
+ .concat(['ellipsis-end'])
173
+ .concat(endPages)
174
+ },
175
+
176
+ select: function (page) {
177
+ if (this.disabled) return
178
+ if (this.totalPages <= 0) return
179
+ var clamped = Math.max(1, Math.min(page, this.totalPages))
180
+ if (clamped === this.currentPage) return
181
+ this.$emit('change', clamped)
182
+ this.$emit('update:modelValue', clamped)
183
+ },
184
+
185
+ goPrev: function () {
186
+ if (this.isPrevDisabled) return
187
+ this.select(this.currentPage - 1)
188
+ },
189
+
190
+ goNext: function () {
191
+ if (this.isNextDisabled) return
192
+ this.select(this.currentPage + 1)
193
+ },
194
+
195
+ onEllipsisClick: function (kind) {
196
+ if (this.effectiveJumpStep <= 0 || this.disabled) return
197
+ var delta = kind === 'ellipsis-start' ? -this.effectiveJumpStep : this.effectiveJumpStep
198
+ this.select(this.currentPage + delta)
199
+ },
200
+
201
+ resolvePageAriaLabel: function (page) {
202
+ if (typeof this.pageAriaLabel === 'function') {
203
+ return this.pageAriaLabel(page)
204
+ }
205
+ return page === this.currentPage ? 'Page ' + page + ', current page' : 'Go to page ' + page
206
+ }
207
+ }
208
+ }
209
+ </script>
210
+
211
+ <style src="./pagination.css" scoped></style>
@@ -0,0 +1,13 @@
1
+ .ui-pagination {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ flex-wrap: wrap;
5
+ gap: var(--spacing-xs);
6
+ font-family: var(--font-family-body);
7
+ color: var(--primary-fg);
8
+ }
9
+
10
+ .ui-pagination--disabled {
11
+ opacity: 0.6;
12
+ pointer-events: none;
13
+ }
@@ -0,0 +1,74 @@
1
+ <template>
2
+ <label
3
+ class="ui-radio"
4
+ :class="{
5
+ checked: isChecked,
6
+ disabled: isDisabled,
7
+ error: isError
8
+ }"
9
+ >
10
+ <input type="radio" class="ui-radio__input" :checked="isChecked" :disabled="isDisabled" @change="handleChange" />
11
+
12
+ <span class="ui-radio__inner"></span>
13
+
14
+ <div class="ui-radio__label_wrapper">
15
+ <span class="ui-radio__label">
16
+ <slot>
17
+ {{ label }}
18
+ </slot>
19
+ </span>
20
+ <span class="ui-radio__label_extra">
21
+ <slot name="extraLabel"></slot>
22
+ </span>
23
+ </div>
24
+ </label>
25
+ </template>
26
+
27
+ <script>
28
+ export default {
29
+ name: 'Radio',
30
+ inject: {
31
+ radioGroup: { default: null }
32
+ },
33
+ props: {
34
+ label: {
35
+ type: String,
36
+ default: ''
37
+ },
38
+ value: {
39
+ type: String,
40
+ required: true
41
+ },
42
+ checked: {
43
+ type: Boolean,
44
+ default: false
45
+ },
46
+ disabled: {
47
+ type: Boolean,
48
+ default: false
49
+ },
50
+ error: {
51
+ type: Boolean,
52
+ default: false
53
+ }
54
+ },
55
+ computed: {
56
+ isChecked() {
57
+ return this.radioGroup ? this.radioGroup.value === this.value : this.checked
58
+ },
59
+ isDisabled() {
60
+ return this.disabled || (this.radioGroup ? this.radioGroup.disabled : false)
61
+ },
62
+ isError() {
63
+ return this.error || (this.radioGroup ? this.radioGroup.error : false)
64
+ }
65
+ },
66
+ methods: {
67
+ handleChange() {
68
+ this.radioGroup.select(this.value)
69
+ }
70
+ }
71
+ }
72
+ </script>
73
+
74
+ <style src="./radio.css" scoped></style>
@@ -0,0 +1,89 @@
1
+ .ui-radio {
2
+ --size: var(--spacing-md);
3
+ --label-gap: var(--spacing-xs);
4
+ --label-color: var(--color-neutral-700);
5
+ --border-color: var(--border-3);
6
+ --focus-color: var(--border-primary);
7
+ --error-focus-color: var(--color-red-200);
8
+ --error-color: var(--color-red-500);
9
+ --inner-bg: var(--color-brand-shades-600);
10
+ --disabled-border-color: var(--color-neutral-200);
11
+ --disabled-checked-border-color: var(--color-neutral-950);
12
+ --disabled-inner-bg: var(--color-neutral-950);
13
+
14
+ position: relative;
15
+ display: inline-flex;
16
+ align-items: start;
17
+ gap: var(--label-gap);
18
+ cursor: pointer;
19
+ user-select: none;
20
+ color: var(--label-color);
21
+ transition: all 0.2s;
22
+ }
23
+ .ui-radio__input {
24
+ position: absolute;
25
+ opacity: 0;
26
+ pointer-events: none;
27
+ }
28
+ .ui-radio__inner {
29
+ position: relative;
30
+ width: var(--size);
31
+ height: var(--size);
32
+ border: 1px solid var(--border-color);
33
+ border-radius: 50%;
34
+ background: #fff;
35
+ box-sizing: border-box;
36
+ transition: all 0.2s;
37
+ box-shadow: var(--shadow-xs);
38
+ margin-top: 2px;
39
+ }
40
+ .ui-radio__inner::after {
41
+ content: '';
42
+ position: absolute;
43
+ top: 50%;
44
+ left: 50%;
45
+ width: 8px;
46
+ height: 8px;
47
+ border-radius: 50%;
48
+ transform: translate(-50%, -50%) scale(0);
49
+ transition: transform 0.2s ease;
50
+ background: var(--inner-bg);
51
+ }
52
+ .ui-radio:hover .ui-radio__inner {
53
+ box-shadow: 0 0 0 3px var(--focus-color);
54
+ }
55
+ .ui-radio.disabled {
56
+ cursor: not-allowed;
57
+ opacity: 0.5;
58
+ }
59
+ .ui-radio.disabled.checked {
60
+ opacity: 0.3;
61
+ }
62
+ .ui-radio.disabled .ui-radio__inner {
63
+ border-color: var(--disabled-border-color);
64
+ }
65
+ .ui-radio.disabled.checked .ui-radio__inner {
66
+ border-color: var(--disabled-checked-border-color);
67
+ }
68
+ .ui-radio.disabled.checked .ui-radio__inner::after {
69
+ background: var(--disabled-inner-bg);
70
+ }
71
+
72
+ .ui-radio.error:hover .ui-radio__inner {
73
+ box-shadow: 0 0 0 3px var(--error-focus-color);
74
+ }
75
+ .ui-radio.error .ui-radio__inner {
76
+ border-color: var(--error-color);
77
+ }
78
+ .ui-radio.error .ui-radio__inner::after {
79
+ background: var(--error-color);
80
+ }
81
+
82
+ .ui-radio.checked .ui-radio__inner::after {
83
+ transform: translate(-50%, -50%) scale(1);
84
+ }
85
+
86
+ .ui-radio__label_wrapper {
87
+ display: flex;
88
+ flex-direction: column;
89
+ }
@@ -0,0 +1,70 @@
1
+ <template>
2
+ <div class="ui-radio-group" :class="`ui-radio-group--${direction}`">
3
+ <slot>
4
+ <Radio
5
+ v-for="opt in normalizedOptions"
6
+ :key="opt.value"
7
+ :value="opt.value"
8
+ :label="opt.label"
9
+ :disabled="opt.disabled"
10
+ />
11
+ </slot>
12
+ </div>
13
+ </template>
14
+
15
+ <script>
16
+ import Radio from '../radio/Radio.vue'
17
+
18
+ export default {
19
+ name: 'RadioGroup',
20
+ components: { Radio },
21
+ provide() {
22
+ return { radioGroup: this }
23
+ },
24
+ model: {
25
+ prop: 'value',
26
+ event: 'change'
27
+ },
28
+ emits: ['change'],
29
+
30
+ props: {
31
+ value: {
32
+ type: String,
33
+ default: ''
34
+ },
35
+ options: {
36
+ type: Array,
37
+ default: () => []
38
+ },
39
+ disabled: {
40
+ type: Boolean,
41
+ default: false
42
+ },
43
+ error: {
44
+ type: Boolean,
45
+ default: false
46
+ },
47
+ direction: {
48
+ type: String,
49
+ default: 'vertical',
50
+ validator: v => ['vertical', 'horizontal'].includes(v)
51
+ }
52
+ },
53
+ computed: {
54
+ normalizedOptions() {
55
+ return this.options.map(opt =>
56
+ typeof opt === 'string'
57
+ ? { label: opt, value: opt, disabled: false }
58
+ : { label: opt.label || opt.value, value: opt.value, disabled: !!opt.disabled }
59
+ )
60
+ }
61
+ },
62
+ methods: {
63
+ select(val) {
64
+ this.$emit('change', val)
65
+ }
66
+ }
67
+ }
68
+ </script>
69
+
70
+ <style src="./radio_group.css" scoped></style>
@@ -0,0 +1,11 @@
1
+ .ui-radio-group {
2
+ display: flex;
3
+ gap: var(--spacing-sm, 8px);
4
+ }
5
+ .ui-radio-group--vertical {
6
+ flex-direction: column;
7
+ }
8
+ .ui-radio-group--horizontal {
9
+ flex-direction: row;
10
+ flex-wrap: wrap;
11
+ }