@milaboratories/uikit 2.2.67 → 2.2.69

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/uikit",
3
- "version": "2.2.67",
3
+ "version": "2.2.69",
4
4
  "type": "module",
5
5
  "main": "dist/pl-uikit.umd.js",
6
6
  "module": "dist/pl-uikit.js",
@@ -34,9 +34,9 @@
34
34
  "yarpm": "^1.2.0",
35
35
  "svgo": "^3.3.2",
36
36
  "@types/d3": "^7.4.3",
37
- "@milaboratories/eslint-config": "^1.0.4",
38
37
  "@milaboratories/helpers": "^1.6.11",
39
- "@platforma-sdk/model": "^1.29.2"
38
+ "@milaboratories/eslint-config": "^1.0.4",
39
+ "@platforma-sdk/model": "^1.29.16"
40
40
  },
41
41
  "scripts": {
42
42
  "dev": "vite",
@@ -47,7 +47,6 @@
47
47
 
48
48
  --btn-shape-shadow: inset 0px -3px 0px rgba(36, 34, 61, 0.12), inset 0px 3px 0px rgba(255, 255, 255, 0.4);
49
49
  --btn-group-shape-shadow: inset 0px -3px 0px rgba(36, 34, 61, 0.12), inset 0px 3px 0px rgba(255, 255, 255, 0.4);
50
- --btn-sec-hover-grey: rgba(155, 171, 204, 0.16);
51
50
 
52
51
  --btn-switcher-bg: linear-gradient(180deg, #a1e69c 0%, #d0f5b0 100%);
53
52
  --btn-switcher-option-color: var(--txt-01);
@@ -109,7 +108,6 @@
109
108
 
110
109
  --btn-shape-shadow: none;
111
110
  --btn-group-shape-shadow: inset 0px -3px 0px rgba(13, 13, 15, 0.24), inset 0px 3px 0px rgba(255, 255, 255, 0.12);
112
- --btn-sec-hover-grey: var(--color-sec-hover-white);
113
111
 
114
112
  --btn-switcher-bg: #5e5e70;
115
113
  --btn-switcher-option-color: var(--txt-03);
@@ -1,6 +1,6 @@
1
1
  :root {
2
2
  --txt-00: #ffffff;
3
- --txt-01: #110529;
3
+ --txt-01: light-dark(#110529, #ffffff);
4
4
  --txt-02: #231842;
5
5
  --txt-03: #9d9eae;
6
6
  --txt-mask: #cfd1db;
@@ -27,13 +27,13 @@
27
27
  --btn-primary-hover: #231842;
28
28
  --btn-primary-press: #080214;
29
29
  --btn-sec-hover-white: rgba(255, 255, 255, 0.5);
30
- --btn-sec-hover-grey: rgba(155, 171, 204, 0.16);
30
+ --btn-sec-hover-grey: light-dark(rgba(155, 171, 204, 0.16), rgba(131, 131, 163, 0.16));
31
31
  --btn-sec-press-grey: rgba(155, 171, 204, 0.24);
32
32
  --btn-active-select: rgba(99, 224, 36, 0.24);
33
33
  --btn-switcher: linear-gradient(180deg, #a1e59c 0%, #d0f5b0 100%);
34
34
  //disable
35
35
  --dis-00: #ffffff;
36
- --dis-01: #cfd1db;
36
+ --dis-01: light-dark(#cfd1db, #3d3d42);
37
37
  //background
38
38
  --bg-base-dark: #110529;
39
39
  --bg-base-light: #f7f8fa;
@@ -27,7 +27,7 @@ function closeAlert() {
27
27
  <slot />
28
28
  </div>
29
29
  <div v-if="closable" class="pl-notification-alert__close">
30
- <PlBtnGhost icon="close" @click="closeAlert" />
30
+ <PlBtnGhost icon="close" @click.stop="closeAlert" />
31
31
  </div>
32
32
  </div>
33
33
 
@@ -0,0 +1,92 @@
1
+ <script setup lang="ts" generic="M">
2
+ import { inject } from 'vue';
3
+ import { radioGroupModelKey, radioGroupNameKey } from './keys';
4
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- used by the props documentation
5
+ import type PlRadioGroup from './PlRadioGroup.vue';
6
+
7
+ const standaloneModel = defineModel<M>();
8
+
9
+ const { name: standaloneName, ...props } = defineProps<{
10
+ /** Used to group multiple radio controls. Will be ignored if this component is a descendant of a {@link PlRadioGroup}. */
11
+ name?: string;
12
+ /** Value that goes into `v-model`. */
13
+ value?: M;
14
+ /** Whether the radio control is disabled. */
15
+ disabled?: boolean;
16
+ }>();
17
+
18
+ defineSlots<{
19
+ /** Label of the radio control. */
20
+ default?(): unknown;
21
+ }>();
22
+
23
+ const name = inject(radioGroupNameKey, standaloneName);
24
+ const model = inject<typeof standaloneModel>(radioGroupModelKey, standaloneModel);
25
+ </script>
26
+
27
+ <template>
28
+ <label :class="$style.container">
29
+ <input v-model="model" :class="$style.input" type="radio" :name v-bind="props" />
30
+ <span :class="$style.label"><slot /></span>
31
+ </label>
32
+ </template>
33
+
34
+ <style module>
35
+ .container {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 4px;
39
+ padding: 4px;
40
+ border-radius: 6px;
41
+ transition: all 200ms ease-in-out;
42
+ color: var(--txt-01);
43
+ user-select: none;
44
+ &:hover:not(:has(:disabled)) {
45
+ background: var(--btn-sec-hover-grey);
46
+ }
47
+ &:has(:disabled) {
48
+ color: var(--dis-01);
49
+ }
50
+ }
51
+
52
+ .input {
53
+ appearance: none;
54
+ position: relative;
55
+ block-size: 24px;
56
+ aspect-ratio: 1;
57
+ margin: 0;
58
+ border-radius: 50%;
59
+ outline: 2px solid transparent;
60
+ color: inherit;
61
+ transition: inherit;
62
+ &:focus {
63
+ outline-color: var(--border-color-focus);
64
+ }
65
+ &::before {
66
+ content: "";
67
+ position: absolute;
68
+ inset: 2px;
69
+ border-radius: 50%;
70
+ border: 2px solid;
71
+ }
72
+ &::after {
73
+ content: "";
74
+ position: absolute;
75
+ inset: 7.5px;
76
+ border-radius: 50%;
77
+ background-color: currentColor;
78
+ scale: 0;
79
+ transition: inherit;
80
+ }
81
+ &:checked::after {
82
+ scale: 1;
83
+ }
84
+ }
85
+
86
+ .label {
87
+ padding-inline: 4px;
88
+ line-height: calc(20 / 14);
89
+ font-weight: 500;
90
+ text-box: trim-both cap alphabetic;
91
+ }
92
+ </style>
@@ -0,0 +1,74 @@
1
+ <script setup lang="ts" generic="M">
2
+ import { provide } from 'vue';
3
+ import { radioGroupModelKey, radioGroupNameKey } from './keys';
4
+ import PlRadio from './PlRadio.vue';
5
+
6
+ type RadioGroupOption = {
7
+ label: string;
8
+ value: M;
9
+ disabled?: boolean;
10
+ };
11
+
12
+ const model = defineModel<M>();
13
+
14
+ const props = defineProps<{
15
+ /** Name of the radio group. */
16
+ name?: string;
17
+ /**
18
+ * List of available options.
19
+ * Renders a list of {@link PlRadio} components before the {@link slots.default | default} slot.
20
+ */
21
+ options?: Readonly<RadioGroupOption[]>;
22
+ /** Function to get option's unique key. Use if default mechanism (key = index) is unstable. */
23
+ keyExtractor?: (value: M, index: number) => PropertyKey;
24
+ }>();
25
+
26
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- used by the props documentation
27
+ const slots = defineSlots<{
28
+ /**
29
+ * Can be anything, but usually an array of {@link PlRadio} components.
30
+ * If {@link props.options|options} are provided, they will be rendered before this slot.
31
+ */
32
+ default?(): unknown;
33
+ /** Label of the radio group. */
34
+ label?(): unknown;
35
+ }>();
36
+
37
+ const keyExtractor = props.keyExtractor ?? ((_, i) => i);
38
+
39
+ provide(radioGroupNameKey, props.name);
40
+ provide(radioGroupModelKey, model);
41
+ </script>
42
+
43
+ <template>
44
+ <fieldset :class="$style.container">
45
+ <legend :class="$style.label">
46
+ <slot name="label" />
47
+ </legend>
48
+ <PlRadio
49
+ v-for="(option, i) in options"
50
+ :key="keyExtractor(option.value, i)"
51
+ :value="option.value"
52
+ :disabled="option.disabled"
53
+ >
54
+ {{ option.label }}
55
+ </PlRadio>
56
+ <slot />
57
+ </fieldset>
58
+ </template>
59
+
60
+ <style module>
61
+ .container {
62
+ margin: 0;
63
+ padding: 0;
64
+ border: none;
65
+ }
66
+ .label {
67
+ margin-block-end: 12px;
68
+ padding: 0;
69
+ color: var(--txt-01);
70
+ line-height: calc(20 / 14);
71
+ font-weight: 500;
72
+ text-box: trim-both cap alphabetic;
73
+ }
74
+ </style>
@@ -0,0 +1,168 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { VueWrapper } from '@vue/test-utils';
3
+ import { mount } from '@vue/test-utils';
4
+ import PlRadio from '../PlRadio.vue';
5
+ import PlRadioGroup from '../PlRadioGroup.vue';
6
+ import { h } from 'vue';
7
+
8
+ // --- Use objects as values ---
9
+ const VALUE_1 = { id: 1, name: 'one' };
10
+ const VALUE_2 = { id: 2, name: 'two' };
11
+ const VALUE_3 = { id: 3, name: 'three' };
12
+ const VALUE_A = { id: 'a', name: 'A' };
13
+ const VALUE_B = { id: 'b', name: 'B' };
14
+ const VALUE_4 = { id: 4, name: 'four' };
15
+
16
+ const OPTIONS = [
17
+ { label: 'Option 1', value: VALUE_1 },
18
+ { label: 'Option 2', value: VALUE_2 },
19
+ { label: 'Option 3', value: VALUE_3, disabled: true },
20
+ ];
21
+ // --- ---
22
+
23
+ describe('PlRadioGroup', () => {
24
+ it('renders options correctly', () => {
25
+ const wrapper = mount(PlRadioGroup, {
26
+ props: {
27
+ options: OPTIONS,
28
+ },
29
+ });
30
+
31
+ // Simplify type casting for now, as InstanceType was problematic
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ const radios = wrapper.findAllComponents(PlRadio) as unknown as VueWrapper<any>[];
34
+
35
+ expect(radios.length).toBe(OPTIONS.length);
36
+
37
+ radios.forEach((radioWrapper, index) => {
38
+ expect(radioWrapper.text()).toBe(OPTIONS[index].label);
39
+ // Use toEqual for object comparison
40
+ expect(radioWrapper.props('value')).toEqual(OPTIONS[index].value);
41
+ expect(radioWrapper.props('disabled') ?? false).toBe(OPTIONS[index].disabled ?? false);
42
+ });
43
+ });
44
+
45
+ it('handles v-model with options prop', async () => {
46
+ const wrapper = mount(PlRadioGroup, {
47
+ props: {
48
+ 'modelValue': VALUE_1, // Initial value is an object
49
+ 'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }),
50
+ 'options': OPTIONS,
51
+ },
52
+ });
53
+
54
+ const radioInputs = wrapper.findAll('input[type="radio"]');
55
+ expect(radioInputs.length).toBe(OPTIONS.length);
56
+
57
+ // Check initial state
58
+ expect((radioInputs[0].element as HTMLInputElement).checked).toBe(true);
59
+ expect((radioInputs[1].element as HTMLInputElement).checked).toBe(false);
60
+
61
+ // Click the second option
62
+ await radioInputs[1].setValue(true); // Use setValue for radio inputs
63
+
64
+ // Check updated state - use toEqual for objects
65
+ expect(wrapper.props('modelValue')).toEqual(VALUE_2);
66
+ expect((radioInputs[0].element as HTMLInputElement).checked).toBe(false);
67
+ expect((radioInputs[1].element as HTMLInputElement).checked).toBe(true);
68
+ });
69
+
70
+ // TODO: fix this test
71
+ it.skip('respects disabled options', async () => {
72
+ const wrapper = mount(PlRadioGroup, {
73
+ props: {
74
+ 'modelValue': VALUE_1, // Initial value is an object
75
+ 'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }),
76
+ 'options': OPTIONS,
77
+ },
78
+ });
79
+
80
+ const radioInputs = wrapper.findAll<HTMLInputElement>('input[type="radio"]');
81
+ expect((radioInputs[2].element as HTMLInputElement).disabled).toBe(true);
82
+
83
+ // Try clicking the disabled option
84
+ await radioInputs[2].setValue(true);
85
+
86
+ // Model value should not change - use toEqual
87
+ expect(wrapper.props('modelValue')).toEqual(VALUE_1);
88
+ expect((radioInputs[0].element).checked ?? false).toBe(true);
89
+ expect((radioInputs[2].element).checked ?? false).toBe(false);
90
+ });
91
+
92
+ it('assigns the name attribute correctly', () => {
93
+ const groupName = 'test-group';
94
+ const wrapper = mount(PlRadioGroup, {
95
+ props: {
96
+ name: groupName,
97
+ options: OPTIONS,
98
+ },
99
+ });
100
+
101
+ const radioInputs = wrapper.findAll('input[type="radio"]');
102
+ radioInputs.forEach((input) => {
103
+ expect(input.attributes('name')).toBe(groupName);
104
+ });
105
+ });
106
+
107
+ it('renders default slot content', async () => {
108
+ const wrapper = mount(PlRadioGroup, {
109
+ props: {
110
+ 'modelValue': VALUE_A, // Initial value is an object
111
+ 'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }),
112
+ },
113
+ slots: {
114
+ default: () => [
115
+ // Use object values in slots
116
+ h(PlRadio, { value: VALUE_A }, { default: () => 'Slot Option A' }),
117
+ h(PlRadio, { value: VALUE_B }, { default: () => 'Slot Option B' }),
118
+ ],
119
+ },
120
+ });
121
+
122
+ const radios = wrapper.findAllComponents(PlRadio);
123
+ expect(radios.length).toBe(2);
124
+ expect(radios[0].text()).toBe('Slot Option A');
125
+ expect(radios[1].text()).toBe('Slot Option B');
126
+
127
+ const radioInputs = wrapper.findAll('input[type="radio"]');
128
+ expect((radioInputs[0].element as HTMLInputElement).checked).toBe(true);
129
+
130
+ await radioInputs[1].setValue(true);
131
+ // Use toEqual for object comparison
132
+ expect(wrapper.props('modelValue')).toEqual(VALUE_B);
133
+ expect((radioInputs[1].element as HTMLInputElement).checked).toBe(true);
134
+ });
135
+
136
+ it('renders label slot content', () => {
137
+ const labelText = 'My Radio Group Label';
138
+ const wrapper = mount(PlRadioGroup, {
139
+ slots: {
140
+ label: () => labelText,
141
+ },
142
+ });
143
+
144
+ const legend = wrapper.find('legend');
145
+ expect(legend.exists()).toBe(true);
146
+ expect(legend.text()).toBe(labelText);
147
+ });
148
+
149
+ it('combines options prop and default slot', () => {
150
+ const wrapper = mount(PlRadioGroup, {
151
+ props: {
152
+ options: OPTIONS,
153
+ },
154
+ slots: {
155
+ // Use object value in slot
156
+ default: () => h(PlRadio, { value: VALUE_4 }, { default: () => 'Slot Option 4' }),
157
+ },
158
+ });
159
+
160
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
161
+ const radiosCombined = wrapper.findAllComponents(PlRadio) as unknown as VueWrapper<any>[];
162
+ expect(radiosCombined.length).toBe(OPTIONS.length + 1);
163
+ // Use toEqual for object comparison
164
+ expect(radiosCombined[0].props('value')).toEqual(OPTIONS[0].value);
165
+ expect(radiosCombined[OPTIONS.length].props('value')).toEqual(VALUE_4);
166
+ expect(radiosCombined[OPTIONS.length].text()).toBe('Slot Option 4');
167
+ });
168
+ });
@@ -0,0 +1,2 @@
1
+ export { default as PlRadio } from './PlRadio.vue';
2
+ export { default as PlRadioGroup } from './PlRadioGroup.vue';
@@ -0,0 +1,4 @@
1
+ import { type InjectionKey, type ModelRef } from 'vue';
2
+
3
+ export const radioGroupNameKey: InjectionKey<string | undefined> = Symbol();
4
+ export const radioGroupModelKey: InjectionKey<ModelRef<unknown>> = Symbol();
package/src/index.ts CHANGED
@@ -67,6 +67,8 @@ export * from './components/PlIcon24';
67
67
  export * from './components/PlChartStackedBar';
68
68
  export * from './components/PlChartHistogram';
69
69
 
70
+ export * from './components/PlRadio';
71
+
70
72
  export * from './colors';
71
73
 
72
74
  // @TODO review (may be private)