vueless 1.3.5-beta.0 → 1.3.5-beta.2

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 (34) hide show
  1. package/README.md +1 -1
  2. package/components.d.ts +2 -1
  3. package/components.ts +2 -1
  4. package/constants.d.ts +2 -1
  5. package/constants.js +2 -1
  6. package/package.json +2 -2
  7. package/types.ts +1 -1
  8. package/ui.container-col/config.ts +8 -0
  9. package/ui.container-col/tests/UCol.test.ts +26 -0
  10. package/ui.container-col/types.ts +10 -0
  11. package/{ui.text-empty → ui.container-empty}/storybook/stories.ts +7 -4
  12. package/ui.container-placeholder/UPlaceholder.vue +50 -0
  13. package/ui.container-placeholder/config.ts +40 -0
  14. package/ui.container-placeholder/constants.ts +5 -0
  15. package/ui.container-placeholder/storybook/docs.mdx +15 -0
  16. package/ui.container-placeholder/storybook/stories.ts +126 -0
  17. package/ui.container-placeholder/tests/UPlaceholder.test.ts +173 -0
  18. package/ui.container-placeholder/types.ts +56 -0
  19. package/ui.container-row/config.ts +8 -0
  20. package/ui.container-row/tests/URow.test.ts +26 -0
  21. package/ui.container-row/types.ts +10 -0
  22. package/ui.data-list/UDataList.vue +1 -1
  23. package/ui.data-list/tests/UDataList.test.ts +1 -1
  24. package/ui.data-table/UTable.vue +1 -1
  25. package/ui.data-table/tests/UTable.test.ts +1 -1
  26. package/ui.form-calendar/UCalendar.vue +10 -5
  27. package/ui.form-date-picker-range/UDatePickerRange.vue +1 -1
  28. /package/{ui.text-empty → ui.container-empty}/UEmpty.vue +0 -0
  29. /package/{ui.text-empty → ui.container-empty}/config.ts +0 -0
  30. /package/{ui.text-empty → ui.container-empty}/constants.ts +0 -0
  31. /package/{ui.text-empty → ui.container-empty}/storybook/assets/empty-inbox.png +0 -0
  32. /package/{ui.text-empty → ui.container-empty}/storybook/docs.mdx +0 -0
  33. /package/{ui.text-empty → ui.container-empty}/tests/UEmpty.test.ts +0 -0
  34. /package/{ui.text-empty → ui.container-empty}/types.ts +0 -0
package/README.md CHANGED
@@ -28,7 +28,7 @@ A UI library with Open Architecture for Vue.js 3 and Nuxt.js 3 / 4, powered by [
28
28
  - 🖼️ Inline SVG icons
29
29
  - 🪄 Auto component imports (as you use them)
30
30
  - 🧿 Uncompiled source in npm for better DX
31
- - 🧪️ 1300+ unit tests ensuring consistent logic
31
+ - 🧪️ 1400+ unit tests ensuring consistent logic
32
32
  - 🛡️ Full TypeScript support with type safety
33
33
 
34
34
  ## Built-In Storybook
package/components.d.ts CHANGED
@@ -36,7 +36,6 @@ export { default as UNotify } from "./ui.text-notify/UNotify.vue";
36
36
  export { default as UNumber } from "./ui.text-number/UNumber.vue";
37
37
  export { default as UFile } from "./ui.text-file/UFile.vue";
38
38
  export { default as UFiles } from "./ui.text-files/UFiles.vue";
39
- export { default as UEmpty } from "./ui.text-empty/UEmpty.vue";
40
39
  export { default as UBadge } from "./ui.text-badge/UBadge.vue";
41
40
  /* Containers */
42
41
  export { default as UDivider } from "./ui.container-divider/UDivider.vue";
@@ -46,6 +45,8 @@ export { default as UGroup } from "./ui.container-group/UGroup.vue";
46
45
  export { default as UGroups } from "./ui.container-groups/UGroups.vue";
47
46
  export { default as UAccordion } from "./ui.container-accordion/UAccordion.vue";
48
47
  export { default as UAccordionItem } from "./ui.container-accordion-item/UAccordionItem.vue";
48
+ export { default as UEmpty } from "./ui.container-empty/UEmpty.vue";
49
+ export { default as UPlaceholder } from "./ui.container-placeholder/UPlaceholder.vue";
49
50
  export { default as UCard } from "./ui.container-card/UCard.vue";
50
51
  export { default as UModal } from "./ui.container-modal/UModal.vue";
51
52
  export { default as UModalConfirm } from "./ui.container-modal-confirm/UModalConfirm.vue";
package/components.ts CHANGED
@@ -36,7 +36,6 @@ export { default as UNotify } from "./ui.text-notify/UNotify.vue";
36
36
  export { default as UNumber } from "./ui.text-number/UNumber.vue";
37
37
  export { default as UFile } from "./ui.text-file/UFile.vue";
38
38
  export { default as UFiles } from "./ui.text-files/UFiles.vue";
39
- export { default as UEmpty } from "./ui.text-empty/UEmpty.vue";
40
39
  export { default as UBadge } from "./ui.text-badge/UBadge.vue";
41
40
  /* Containers */
42
41
  export { default as UDivider } from "./ui.container-divider/UDivider.vue";
@@ -46,6 +45,8 @@ export { default as UGroup } from "./ui.container-group/UGroup.vue";
46
45
  export { default as UGroups } from "./ui.container-groups/UGroups.vue";
47
46
  export { default as UAccordion } from "./ui.container-accordion/UAccordion.vue";
48
47
  export { default as UAccordionItem } from "./ui.container-accordion-item/UAccordionItem.vue";
48
+ export { default as UEmpty } from "./ui.container-empty/UEmpty.vue";
49
+ export { default as UPlaceholder } from "./ui.container-placeholder/UPlaceholder.vue";
49
50
  export { default as UCard } from "./ui.container-card/UCard.vue";
50
51
  export { default as UModal } from "./ui.container-modal/UModal.vue";
51
52
  export { default as UModalConfirm } from "./ui.container-modal-confirm/UModalConfirm.vue";
package/constants.d.ts CHANGED
@@ -176,7 +176,6 @@ export namespace COMPONENTS {
176
176
  let UNumber: string;
177
177
  let UFile: string;
178
178
  let UFiles: string;
179
- let UEmpty: string;
180
179
  let UBadge: string;
181
180
  let UDivider: string;
182
181
  let UCol: string;
@@ -185,6 +184,8 @@ export namespace COMPONENTS {
185
184
  let UGroups: string;
186
185
  let UAccordion: string;
187
186
  let UAccordionItem: string;
187
+ let UEmpty: string;
188
+ let UPlaceholder: string;
188
189
  let UCard: string;
189
190
  let UModal: string;
190
191
  let UModalConfirm: string;
package/constants.js CHANGED
@@ -284,7 +284,6 @@ export const COMPONENTS = {
284
284
  UNumber: "ui.text-number",
285
285
  UFile: "ui.text-file",
286
286
  UFiles: "ui.text-files",
287
- UEmpty: "ui.text-empty",
288
287
  UBadge: "ui.text-badge",
289
288
 
290
289
  /* Containers */
@@ -295,6 +294,8 @@ export const COMPONENTS = {
295
294
  UGroups: "ui.container-groups",
296
295
  UAccordion: "ui.container-accordion",
297
296
  UAccordionItem: "ui.container-accordion-item",
297
+ UEmpty: "ui.container-empty",
298
+ UPlaceholder: "ui.container-placeholder",
298
299
  UCard: "ui.container-card",
299
300
  UModal: "ui.container-modal",
300
301
  UModalConfirm: "ui.container-modal-confirm",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "1.3.5-beta.0",
3
+ "version": "1.3.5-beta.2",
4
4
  "description": "Vue Styleless UI Component Library, powered by Tailwind CSS.",
5
5
  "author": "Johnny Grid <hello@vueless.com> (https://vueless.com)",
6
6
  "homepage": "https://vueless.com",
@@ -57,7 +57,7 @@
57
57
  "@vue/eslint-config-typescript": "^14.6.0",
58
58
  "@vue/test-utils": "^2.4.6",
59
59
  "@vue/tsconfig": "^0.7.0",
60
- "@vueless/storybook": "^1.2.13",
60
+ "@vueless/storybook": "^1.3.1",
61
61
  "eslint": "^9.32.0",
62
62
  "eslint-plugin-storybook": "^10.0.2",
63
63
  "eslint-plugin-vue": "^10.3.0",
package/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import UTextDefaultConfig from "./ui.text-block/config";
2
2
  import UAlertDefaultConfig from "./ui.text-alert/config";
3
- import UEmptyDefaultConfig from "./ui.text-empty/config";
3
+ import UEmptyDefaultConfig from "./ui.container-empty/config";
4
4
  import UFileDefaultConfig from "./ui.text-file/config";
5
5
  import UFilesDefaultConfig from "./ui.text-files/config";
6
6
  import UHeaderDefaultConfig from "./ui.text-header/config";
@@ -12,6 +12,12 @@ export default /*tw*/ {
12
12
  block: {
13
13
  true: "w-full",
14
14
  },
15
+ grow: {
16
+ true: "flex-grow",
17
+ },
18
+ shrink: {
19
+ true: "flex-shrink",
20
+ },
15
21
  gap: {
16
22
  none: "gap-0",
17
23
  "3xs": "gap-0.5",
@@ -61,5 +67,7 @@ export default /*tw*/ {
61
67
  wrap: false,
62
68
  reverse: false,
63
69
  block: false,
70
+ grow: false,
71
+ shrink: false,
64
72
  },
65
73
  };
@@ -140,6 +140,32 @@ describe("UCol.vue", () => {
140
140
  expect(component.attributes("class")).toContain(expectedClasses);
141
141
  });
142
142
 
143
+ it("Grow – applies the correct grow class", () => {
144
+ const grow = true;
145
+ const expectedClasses = "flex-grow";
146
+
147
+ const component = mount(UCol, {
148
+ props: {
149
+ grow,
150
+ },
151
+ });
152
+
153
+ expect(component.attributes("class")).toContain(expectedClasses);
154
+ });
155
+
156
+ it("Shrink – applies the correct shrink class", () => {
157
+ const shrink = true;
158
+ const expectedClasses = "flex-shrink";
159
+
160
+ const component = mount(UCol, {
161
+ props: {
162
+ shrink,
163
+ },
164
+ });
165
+
166
+ expect(component.attributes("class")).toContain(expectedClasses);
167
+ });
168
+
143
169
  it("Tag – renders the correct HTML tag", () => {
144
170
  const tags = ["div", "section", "article", "main", "aside", "nav", "span"];
145
171
 
@@ -49,6 +49,16 @@ export interface Props {
49
49
  */
50
50
  block?: boolean;
51
51
 
52
+ /**
53
+ * Allow flex item to grow to fill available space (flex-grow).
54
+ */
55
+ grow?: boolean;
56
+
57
+ /**
58
+ * Allow flex item to shrink if necessary (flex-shrink).
59
+ */
60
+ shrink?: boolean;
61
+
52
62
  /**
53
63
  * Allows changing HTML tag.
54
64
  */
@@ -6,7 +6,7 @@ import {
6
6
  getDocsDescription,
7
7
  } from "../../utils/storybook";
8
8
 
9
- import UEmpty from "../../ui.text-empty/UEmpty.vue";
9
+ import UEmpty from "../../ui.container-empty/UEmpty.vue";
10
10
  import UButton from "../../ui.button/UButton.vue";
11
11
  import UIcon from "../../ui.image-icon/UIcon.vue";
12
12
  import URow from "../../ui.container-row/URow.vue";
@@ -25,8 +25,8 @@ interface UEmptyArgs extends Props {
25
25
  }
26
26
 
27
27
  export default {
28
- id: "4090",
29
- title: "Text & Content / Empty",
28
+ id: "5055",
29
+ title: "Containers / Empty",
30
30
  component: UEmpty,
31
31
  args: {
32
32
  title: "No contacts",
@@ -95,12 +95,15 @@ DefaultSlot.args = {
95
95
  `,
96
96
  };
97
97
 
98
+ export const PlaceholderIcon = DefaultTemplate.bind({});
99
+ PlaceholderIcon.args = { placeholderIcon: "inbox", title: "No messages" };
100
+
98
101
  export const FooterSlot = DefaultTemplate.bind({});
99
102
  FooterSlot.args = {
100
103
  slotTemplate: `
101
104
  <template #footer>
102
105
  <UButton
103
- label="Add new one"
106
+ label="Add"
104
107
  size="sm"
105
108
  color="grayscale"
106
109
  variant="soft"
@@ -0,0 +1,50 @@
1
+ <script setup lang="ts">
2
+ import { useTemplateRef } from "vue";
3
+
4
+ import { useUI } from "../composables/useUI";
5
+ import { getDefaults } from "../utils/ui";
6
+
7
+ import { COMPONENT_NAME } from "./constants";
8
+ import defaultConfig from "./config";
9
+
10
+ import type { Props, Config } from "./types";
11
+
12
+ defineOptions({ inheritAttrs: false });
13
+
14
+ withDefaults(defineProps<Props>(), {
15
+ ...getDefaults<Props, Config>(defaultConfig, COMPONENT_NAME),
16
+ label: "",
17
+ });
18
+
19
+ const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
20
+
21
+ defineExpose({
22
+ /**
23
+ * A reference to the component's wrapper element for direct DOM manipulation.
24
+ * @property {HTMLDivElement}
25
+ */
26
+ wrapperRef,
27
+ });
28
+
29
+ const { getDataTest, wrapperAttrs, contentAttrs, labelAttrs } = useUI<Config>(defaultConfig);
30
+ </script>
31
+
32
+ <template>
33
+ <div
34
+ ref="wrapper"
35
+ role="region"
36
+ :aria-label="label || 'Placeholder area'"
37
+ v-bind="wrapperAttrs"
38
+ :data-test="getDataTest()"
39
+ >
40
+ <div v-bind="contentAttrs">
41
+ <!--
42
+ @slot Use it to add custom content inside the placeholder.
43
+ @binding {string} label
44
+ -->
45
+ <slot :label="label">
46
+ <span v-if="label" v-bind="labelAttrs">{{ label }}</span>
47
+ </slot>
48
+ </div>
49
+ </div>
50
+ </template>
@@ -0,0 +1,40 @@
1
+ export default /*tw*/ {
2
+ wrapper: {
3
+ base: `
4
+ flex items-center justify-center w-full h-full
5
+ border-solid border-2 border-{color}/15
6
+ `,
7
+ variants: {
8
+ rounded: {
9
+ sm: "rounded-small",
10
+ md: "rounded-medium",
11
+ lg: "rounded-large",
12
+ none: "rounded-none",
13
+ },
14
+ dashed: {
15
+ true: "border-dashed",
16
+ },
17
+ dotted: {
18
+ true: "border-dotted",
19
+ },
20
+ },
21
+ },
22
+ content: "flex items-center justify-center",
23
+ label: {
24
+ base: "text-{color} select-none",
25
+ variants: {
26
+ size: {
27
+ sm: "text-small",
28
+ md: "text-medium",
29
+ lg: "text-large",
30
+ },
31
+ },
32
+ },
33
+ defaults: {
34
+ color: "neutral",
35
+ size: "md",
36
+ rounded: "md",
37
+ dashed: false,
38
+ dotted: false,
39
+ },
40
+ };
@@ -0,0 +1,5 @@
1
+ /*
2
+ This const is needed to prevent the issue in script setup:
3
+ `defineProps` is referencing locally declared variables. (vue/valid-define-props)
4
+ */
5
+ export const COMPONENT_NAME = "UPlaceholder";
@@ -0,0 +1,15 @@
1
+ import { Meta, Title, Description, Primary, Controls, Stories } from "@storybook/addon-docs/blocks";
2
+ import * as PlaceholderStories from "./stories";
3
+
4
+ <Meta of={PlaceholderStories} />
5
+
6
+ <Title />
7
+
8
+ <Description />
9
+
10
+ <Primary />
11
+
12
+ <Controls />
13
+
14
+ <Stories />
15
+
@@ -0,0 +1,126 @@
1
+ import type { Meta, StoryFn } from "@storybook/vue3-vite";
2
+ import {
3
+ getArgs,
4
+ getArgTypes,
5
+ getSlotNames,
6
+ getSlotsFragment,
7
+ getDocsDescription,
8
+ } from "../../utils/storybook";
9
+
10
+ import UPlaceholder from "../UPlaceholder.vue";
11
+ import UCol from "../../ui.container-col/UCol.vue";
12
+ import URow from "../../ui.container-row/URow.vue";
13
+ import UText from "../../ui.text-block/UText.vue";
14
+ import UHeader from "../../ui.text-header/UHeader.vue";
15
+
16
+ import type { Props } from "../types";
17
+
18
+ interface PlaceholderArgs extends Props {
19
+ slotTemplate?: string;
20
+ enum?: string;
21
+ }
22
+
23
+ export default {
24
+ id: "5052",
25
+ title: "Containers / Placeholder",
26
+ args: {
27
+ label: "Placeholder label.",
28
+ dashed: true,
29
+ },
30
+ argTypes: {
31
+ ...getArgTypes(UPlaceholder.__name),
32
+ },
33
+ parameters: {
34
+ docs: {
35
+ ...getDocsDescription(UPlaceholder.__name),
36
+ },
37
+ },
38
+ } as Meta;
39
+
40
+ const DefaultTemplate: StoryFn<PlaceholderArgs> = (args: PlaceholderArgs) => ({
41
+ components: { UPlaceholder, URow, UText },
42
+ setup: () => ({ args, slots: getSlotNames(UPlaceholder.__name) }),
43
+ template: `
44
+ <UPlaceholder v-bind="args" class="h-32">
45
+ ${args.slotTemplate || getSlotsFragment("")}
46
+ </UPlaceholder>
47
+ `,
48
+ });
49
+
50
+ const EnumTemplate: StoryFn<PlaceholderArgs> = (args: PlaceholderArgs, { argTypes }) => ({
51
+ components: { UPlaceholder, URow },
52
+ setup: () => ({ args, argTypes, getArgs }),
53
+ template: `
54
+ <URow>
55
+ <UPlaceholder
56
+ v-for="option in argTypes?.[args.enum]?.options"
57
+ v-bind="getArgs(args, option)"
58
+ :key="option"
59
+ class="h-32"
60
+ />
61
+ </URow>
62
+ `,
63
+ });
64
+
65
+ export const Default = DefaultTemplate.bind({});
66
+ Default.args = {};
67
+
68
+ export const NoLabel = DefaultTemplate.bind({});
69
+ NoLabel.args = { label: "" };
70
+
71
+ export const Sizes = EnumTemplate.bind({});
72
+ Sizes.args = { enum: "size", label: "{enumValue}" };
73
+
74
+ export const Rounded = EnumTemplate.bind({});
75
+ Rounded.args = { enum: "rounded", label: "{enumValue}" };
76
+
77
+ export const BorderStyle: StoryFn<PlaceholderArgs> = (args: PlaceholderArgs) => ({
78
+ components: { UPlaceholder, URow },
79
+ setup: () => ({ args }),
80
+ template: `
81
+ <URow>
82
+ <UPlaceholder label="Solid" class="h-32" />
83
+ <UPlaceholder label="Dashed" dashed class="h-32" />
84
+ <UPlaceholder label="Dotted" dotted class="h-32" />
85
+ </URow>
86
+ `,
87
+ });
88
+
89
+ export const Colors = EnumTemplate.bind({});
90
+ Colors.args = { enum: "color", label: "{enumValue}" };
91
+
92
+ export const WithSlot: StoryFn<PlaceholderArgs> = (args: PlaceholderArgs) => ({
93
+ components: { UPlaceholder, UCol, UText, UHeader },
94
+ setup: () => ({ args }),
95
+ template: `
96
+ <UPlaceholder v-bind="args" class="h-32">
97
+ <UCol align="center" gap="2xs">
98
+ <UHeader size="lg">📦</UHeader>
99
+ <UText color="neutral">Custom slot content</UText>
100
+ </UCol>
101
+ </UPlaceholder>
102
+ `,
103
+ });
104
+
105
+ export const FixedSize: StoryFn<PlaceholderArgs> = (args: PlaceholderArgs) => ({
106
+ components: { UPlaceholder },
107
+ setup: () => ({ args }),
108
+ template: `
109
+ <UPlaceholder label="Fixed size: 300x150" class="w-[300px] h-[150px]" />
110
+ `,
111
+ });
112
+
113
+ export const LayoutExample: StoryFn<PlaceholderArgs> = (args: PlaceholderArgs) => ({
114
+ components: { UPlaceholder, URow, UCol },
115
+ setup: () => ({ args }),
116
+ template: `
117
+ <URow align="stretch" class="h-96">
118
+ <UPlaceholder label="Sidebar" class="w-64" />
119
+ <UCol align="stretch" grow>
120
+ <UPlaceholder label="Header" class="h-16" />
121
+ <UPlaceholder label="Main Content" />
122
+ <UPlaceholder label="Footer" class="h-12" />
123
+ </UCol>
124
+ </URow>
125
+ `,
126
+ });
@@ -0,0 +1,173 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, it, expect } from "vitest";
3
+
4
+ import UPlaceholder from "../UPlaceholder.vue";
5
+
6
+ import type { Props } from "../types";
7
+
8
+ describe("UPlaceholder.vue", () => {
9
+ describe("Props", () => {
10
+ it("Size – applies the correct size class", () => {
11
+ const sizeClasses = {
12
+ sm: "text-small",
13
+ md: "text-medium",
14
+ lg: "text-large",
15
+ };
16
+
17
+ Object.entries(sizeClasses).forEach(([size, classes]) => {
18
+ const component = mount(UPlaceholder, {
19
+ props: {
20
+ size: size as Props["size"],
21
+ label: "Test",
22
+ },
23
+ });
24
+
25
+ const labelElement = component.find("span");
26
+
27
+ expect(labelElement.attributes("class")).toContain(classes);
28
+ });
29
+ });
30
+
31
+ it("Rounded – applies the correct rounded class", () => {
32
+ const roundedClasses = {
33
+ sm: "rounded-small",
34
+ md: "rounded-medium",
35
+ lg: "rounded-large",
36
+ none: "rounded-none",
37
+ };
38
+
39
+ Object.entries(roundedClasses).forEach(([rounded, classes]) => {
40
+ const component = mount(UPlaceholder, {
41
+ props: {
42
+ rounded: rounded as Props["rounded"],
43
+ },
44
+ });
45
+
46
+ expect(component.attributes("class")).toContain(classes);
47
+ });
48
+ });
49
+
50
+ it("Dashed – applies the correct border class", () => {
51
+ const dashed = true;
52
+ const expectedClass = "border-dashed";
53
+
54
+ const component = mount(UPlaceholder, {
55
+ props: {
56
+ dashed,
57
+ },
58
+ });
59
+
60
+ expect(component.attributes("class")).toContain(expectedClass);
61
+ });
62
+
63
+ it("Dotted – applies the correct border class", () => {
64
+ const dotted = true;
65
+ const expectedClass = "border-dotted";
66
+
67
+ const component = mount(UPlaceholder, {
68
+ props: {
69
+ dotted,
70
+ },
71
+ });
72
+
73
+ expect(component.attributes("class")).toContain(expectedClass);
74
+ });
75
+
76
+ it("Border – applies solid border class by default", () => {
77
+ const expectedClass = "border-solid";
78
+
79
+ const component = mount(UPlaceholder, {
80
+ props: {
81
+ dashed: false,
82
+ dotted: false,
83
+ },
84
+ });
85
+
86
+ expect(component.attributes("class")).toContain(expectedClass);
87
+ });
88
+
89
+ it("Color – applies the correct color class", () => {
90
+ const color = "primary";
91
+
92
+ const component = mount(UPlaceholder, {
93
+ props: {
94
+ color,
95
+ },
96
+ });
97
+
98
+ expect(component.attributes("class")).toContain(`border-${color}`);
99
+ });
100
+
101
+ it("Label – renders the correct label text", () => {
102
+ const label = "Test Label";
103
+
104
+ const component = mount(UPlaceholder, {
105
+ props: {
106
+ label,
107
+ },
108
+ });
109
+
110
+ expect(component.text()).toContain(label);
111
+ });
112
+
113
+ it("Label – sets aria-label from label prop", () => {
114
+ const label = "Test Area";
115
+
116
+ const component = mount(UPlaceholder, {
117
+ props: {
118
+ label,
119
+ },
120
+ });
121
+
122
+ expect(component.attributes("aria-label")).toBe(label);
123
+ });
124
+
125
+ it("Label – sets default aria-label when no label provided", () => {
126
+ const component = mount(UPlaceholder);
127
+
128
+ expect(component.attributes("aria-label")).toBe("Placeholder area");
129
+ });
130
+
131
+ it("Role – applies the correct role attribute", () => {
132
+ const component = mount(UPlaceholder);
133
+
134
+ expect(component.attributes("role")).toBe("region");
135
+ });
136
+
137
+ it("Data Test – applies the correct data-test attribute", () => {
138
+ const dataTest = "custom-test-id";
139
+
140
+ const component = mount(UPlaceholder, {
141
+ props: {
142
+ dataTest,
143
+ },
144
+ });
145
+
146
+ expect(component.attributes("data-test")).toBe(dataTest);
147
+ });
148
+ });
149
+
150
+ describe("Slots", () => {
151
+ it("Default – renders content from default slot", () => {
152
+ const slotContent = "Custom Content";
153
+ const slotClass = "custom-content";
154
+
155
+ const component = mount(UPlaceholder, {
156
+ slots: {
157
+ default: `<div class='${slotClass}'>${slotContent}</div>`,
158
+ },
159
+ });
160
+
161
+ expect(component.find(`.${slotClass}`).exists()).toBe(true);
162
+ expect(component.text()).toContain(slotContent);
163
+ });
164
+ });
165
+
166
+ describe("Exposed refs", () => {
167
+ it("wrapperRef – exposes wrapperRef", () => {
168
+ const component = mount(UPlaceholder);
169
+
170
+ expect(component.vm.wrapperRef).toBeDefined();
171
+ });
172
+ });
173
+ });
@@ -0,0 +1,56 @@
1
+ import defaultConfig from "./config";
2
+
3
+ import type { ComponentConfig } from "../types";
4
+
5
+ export type Config = typeof defaultConfig;
6
+
7
+ export interface Props {
8
+ /**
9
+ * Placeholder label text.
10
+ */
11
+ label?: string;
12
+
13
+ /**
14
+ * Label text size.
15
+ */
16
+ size?: "sm" | "md" | "lg";
17
+
18
+ /**
19
+ * Border radius size.
20
+ */
21
+ rounded?: "sm" | "md" | "lg" | "none";
22
+
23
+ /**
24
+ * Use dashed border style.
25
+ */
26
+ dashed?: boolean;
27
+
28
+ /**
29
+ * Use dotted border style.
30
+ */
31
+ dotted?: boolean;
32
+
33
+ /**
34
+ * Border color.
35
+ */
36
+ color?:
37
+ | "primary"
38
+ | "secondary"
39
+ | "error"
40
+ | "warning"
41
+ | "success"
42
+ | "info"
43
+ | "notice"
44
+ | "neutral"
45
+ | "grayscale";
46
+
47
+ /**
48
+ * Component config object.
49
+ */
50
+ config?: ComponentConfig<Config>;
51
+
52
+ /**
53
+ * Data-test attribute for automated testing.
54
+ */
55
+ dataTest?: string | null;
56
+ }
@@ -12,6 +12,12 @@ export default /*tw*/ {
12
12
  block: {
13
13
  true: "w-full",
14
14
  },
15
+ grow: {
16
+ true: "flex-grow",
17
+ },
18
+ shrink: {
19
+ true: "flex-shrink",
20
+ },
15
21
  gap: {
16
22
  none: "gap-0",
17
23
  "2xs": "gap-1",
@@ -59,5 +65,7 @@ export default /*tw*/ {
59
65
  wrap: false,
60
66
  block: false,
61
67
  reverse: false,
68
+ grow: false,
69
+ shrink: false,
62
70
  },
63
71
  };
@@ -138,6 +138,32 @@ describe("URow.vue", () => {
138
138
  expect(component.attributes("class")).toContain(expectedClasses);
139
139
  });
140
140
 
141
+ it("Grow – applies the correct grow class", () => {
142
+ const grow = true;
143
+ const expectedClasses = "flex-grow";
144
+
145
+ const component = mount(URow, {
146
+ props: {
147
+ grow,
148
+ },
149
+ });
150
+
151
+ expect(component.attributes("class")).toContain(expectedClasses);
152
+ });
153
+
154
+ it("Shrink – applies the correct shrink class", () => {
155
+ const shrink = true;
156
+ const expectedClasses = "flex-shrink";
157
+
158
+ const component = mount(URow, {
159
+ props: {
160
+ shrink,
161
+ },
162
+ });
163
+
164
+ expect(component.attributes("class")).toContain(expectedClasses);
165
+ });
166
+
141
167
  it("Tag – renders the correct HTML tag", () => {
142
168
  const tags = ["div", "section", "article", "main", "aside", "nav", "span"];
143
169
 
@@ -49,6 +49,16 @@ export interface Props {
49
49
  */
50
50
  block?: boolean;
51
51
 
52
+ /**
53
+ * Allow flex item to grow to fill available space (flex-grow).
54
+ */
55
+ grow?: boolean;
56
+
57
+ /**
58
+ * Allow flex item to shrink if necessary (flex-shrink).
59
+ */
60
+ shrink?: boolean;
61
+
52
62
  /**
53
63
  * Allows changing HTML tag.
54
64
  */
@@ -7,7 +7,7 @@ import { getDefaults } from "../utils/ui";
7
7
  import { hasSlotContent } from "../utils/helper";
8
8
 
9
9
  import UIcon from "../ui.image-icon/UIcon.vue";
10
- import UEmpty from "../ui.text-empty/UEmpty.vue";
10
+ import UEmpty from "../ui.container-empty/UEmpty.vue";
11
11
 
12
12
  import { COMPONENT_NAME } from "./constants";
13
13
  import defaultConfig from "./config";
@@ -2,7 +2,7 @@ import { mount, VueWrapper } from "@vue/test-utils";
2
2
  import { describe, it, expect } from "vitest";
3
3
 
4
4
  import UDataList from "../UDataList.vue";
5
- import UEmpty from "../../ui.text-empty/UEmpty.vue";
5
+ import UEmpty from "../../ui.container-empty/UEmpty.vue";
6
6
  import UIcon from "../../ui.image-icon/UIcon.vue";
7
7
  import draggable from "vuedraggable";
8
8
 
@@ -12,7 +12,7 @@ import {
12
12
  } from "vue";
13
13
  import { isEqual } from "lodash-es";
14
14
 
15
- import UEmpty from "../ui.text-empty/UEmpty.vue";
15
+ import UEmpty from "../ui.container-empty/UEmpty.vue";
16
16
  import UCheckbox from "../ui.form-checkbox/UCheckbox.vue";
17
17
  import ULoaderProgress from "../ui.loader-progress/ULoaderProgress.vue";
18
18
  import UTableRow from "./UTableRow.vue";
@@ -4,7 +4,7 @@ import { nextTick } from "vue";
4
4
 
5
5
  import UTable from "../UTable.vue";
6
6
  import UTableRow from "../UTableRow.vue";
7
- import UEmpty from "../../ui.text-empty/UEmpty.vue";
7
+ import UEmpty from "../../ui.container-empty/UEmpty.vue";
8
8
  import ULoaderProgress from "../../ui.loader-progress/ULoaderProgress.vue";
9
9
  import UDivider from "../../ui.container-divider/UDivider.vue";
10
10
  import {
@@ -90,6 +90,12 @@ const emit = defineEmits([
90
90
  * @property {string} value
91
91
  */
92
92
  "userDateChange",
93
+
94
+ /**
95
+ * Triggers when range date values (from or to) change.
96
+ * @property {object} value
97
+ */
98
+ "change-range",
93
99
  ]);
94
100
 
95
101
  const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
@@ -253,6 +259,7 @@ const localValue = computed({
253
259
  if (newDate && newDateTo) {
254
260
  tempRangeValue.value = null;
255
261
  emit("update:modelValue", newRangeDate);
262
+ emit("change-range", newRangeDate);
256
263
  }
257
264
  } else {
258
265
  emit("update:modelValue", newDate);
@@ -453,15 +460,11 @@ function onInputDate(newDate: Date | null) {
453
460
  }
454
461
 
455
462
  if (props.range && isRangeDate(localValue.value)) {
456
- const localFromTime =
457
- localValue.value?.from instanceof Date ? localValue.value?.from.getTime() : 0;
458
-
459
- const isSameAsFrom = newDate.getTime() === localFromTime;
460
463
  const isNewDateLessFromDate = localValue.value.from && newDate < localValue.value.from;
461
464
  const areToAndFromDateExists = localValue.value.to && localValue.value.from;
462
465
  const hasFrom = localValue.value.from;
463
466
 
464
- const isFullReset = isSameAsFrom || isNewDateLessFromDate || areToAndFromDateExists || !hasFrom;
467
+ const isFullReset = isNewDateLessFromDate || areToAndFromDateExists || !hasFrom;
465
468
 
466
469
  const updatedValue = isFullReset
467
470
  ? { from: newDate, to: null }
@@ -472,8 +475,10 @@ function onInputDate(newDate: Date | null) {
472
475
  localValue.value = updatedValue;
473
476
 
474
477
  emit("input", updatedValue);
478
+ emit("change-range", updatedValue);
475
479
  } else {
476
480
  tempRangeValue.value = updatedValue;
481
+ emit("change-range", updatedValue);
477
482
  }
478
483
  } else {
479
484
  localValue.value = newDate;
@@ -764,7 +764,7 @@ watchEffect(() => {
764
764
  v-bind="datepickerCalendarAttrs as KeyAttrsWithConfig<UCalendarConfig>"
765
765
  range
766
766
  :data-test="getDataTest('calendar')"
767
- @input="onInputCalendar"
767
+ @change-range="onInputCalendar"
768
768
  />
769
769
  </div>
770
770
  </Transition>
File without changes
File without changes