pimelon-ui 0.1.262 → 0.1.267

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 (61) hide show
  1. package/melon/DataImport/UploadStep.vue +27 -8
  2. package/melon/DataImport/types.ts +6 -0
  3. package/melon/drive/components/RenameDialog.vue +1 -1
  4. package/melon/drive/components/ShareDialog.vue +8 -5
  5. package/melon/drive/components/TagInput/TagInput.vue +33 -20
  6. package/melon/drive/js/resources.js +2 -3
  7. package/melon/drive/js/utils.js +1 -1
  8. package/package.json +4 -2
  9. package/src/components/Alert/Alert.cy.ts +64 -0
  10. package/src/components/Avatar/Avatar.cy.ts +47 -0
  11. package/src/components/Badge/Badge.cy.ts +273 -0
  12. package/src/components/Breadcrumbs/Breadcrumbs.cy.ts +45 -0
  13. package/src/components/Button/Button.cy.ts +128 -0
  14. package/src/components/Button/Button.vue +1 -6
  15. package/src/components/Checkbox/Checkbox.cy.ts +31 -0
  16. package/src/components/Checkbox/Checkbox.vue +1 -1
  17. package/src/components/Combobox/Combobox.cy.ts +93 -0
  18. package/src/components/Combobox/Combobox.vue +1 -1
  19. package/src/components/DatePicker/DatePicker.cy.ts +124 -0
  20. package/src/components/DatePicker/DatePicker.vue +3 -0
  21. package/src/components/DatePicker/DateRangePicker.cy.ts +105 -0
  22. package/src/components/DatePicker/DateRangePicker.vue +3 -0
  23. package/src/components/Dialog/Dialog.cy.ts +90 -0
  24. package/src/components/Dropdown/Dropdown.cy.ts +69 -0
  25. package/src/components/ErrorMessage/ErrorMessage.cy.ts +15 -0
  26. package/src/components/FormControl/FormControl.vue +2 -2
  27. package/src/components/ListFilter/ListFilter.vue +1 -1
  28. package/src/components/ListView/ListGroupHeader.vue +2 -2
  29. package/src/components/ListView/ListHeader.vue +1 -1
  30. package/src/components/ListView/ListHeaderItem.vue +1 -1
  31. package/src/components/ListView/ListRow.vue +2 -2
  32. package/src/components/ListView/ListRowItem.vue +1 -1
  33. package/src/components/ListView/ListSelectBanner.vue +4 -4
  34. package/src/components/MonthPicker/MonthPicker.cy.ts +83 -0
  35. package/src/components/MonthPicker/MonthPicker.vue +7 -5
  36. package/src/components/MultiSelect/MultiSelect.cy.ts +70 -0
  37. package/src/components/Password/Password.cy.ts +35 -0
  38. package/src/components/Popover/Popover.cy.ts +62 -0
  39. package/src/components/Popover/Popover.vue +8 -8
  40. package/src/components/Popover/types.ts +1 -0
  41. package/src/components/Progress/Progress.cy.ts +80 -0
  42. package/src/components/Rating/Rating.cy.ts +67 -0
  43. package/src/components/Select/Select.cy.ts +84 -0
  44. package/src/components/Select/Select.vue +1 -1
  45. package/src/components/Sidebar/Sidebar.cy.ts +89 -0
  46. package/src/components/Switch/Switch.cy.ts +46 -0
  47. package/src/components/Switch/Switch.vue +5 -11
  48. package/src/components/Tabs/Tabs.cy.ts +78 -0
  49. package/src/components/Tabs/Tabs.vue +3 -3
  50. package/src/components/TextEditor/TextEditor.cy.ts +181 -0
  51. package/src/components/TextEditor/commands.js +47 -12
  52. package/src/components/TextEditor/components/MediaNodeView.vue +32 -13
  53. package/src/components/TextEditor/components/Menu.vue +167 -152
  54. package/src/components/TextEditor/extensions/image/image-extension.ts +57 -17
  55. package/src/components/TextEditor/extensions/video-extension.ts +67 -18
  56. package/src/components/TextEditor/index.ts +1 -0
  57. package/src/components/TextInput/TextInput.cy.ts +87 -0
  58. package/src/components/TimePicker/TimePicker.cy.ts +122 -0
  59. package/src/components/Tooltip/Tooltip.cy.ts +66 -0
  60. package/src/components/Tree/tree.cy.ts +144 -0
  61. package/src/resources/resources.js +1 -0
@@ -153,7 +153,7 @@
153
153
  <script setup lang="ts">
154
154
  import { computed, nextTick, ref, watch } from 'vue'
155
155
  import { useRouter } from 'vue-router'
156
- import type { DataImports, DataImport, DocField } from './types'
156
+ import type { DataImports, DataImport, DocField, DocType } from './types'
157
157
  import { toast } from "../../src/components/Toast/index"
158
158
  import { fieldsToIgnore, getChildTableName, getBadgeColor } from './dataImport'
159
159
  import Badge from '../../src/components/Badge/Badge.vue'
@@ -327,20 +327,39 @@ const getExportFields = (type: 'mandatory' | 'all') => {
327
327
  }
328
328
 
329
329
  const getMandatoryFields = () => {
330
- let parentDoctype = props.fields.data?.docs.find((doc: any) => doc.name == props.doctype)
331
- let exportableFields = parentDoctype.fields.filter((field: DocField) => {
330
+ let exportableFields: Record<string, string[]> = {}
331
+ let docs = props.fields.data?.docs || []
332
+ let referenceDoctype = props.doctype || props.data?.reference_doctype as string
333
+ let parentDoctype = docs.find((doc: DocType) => doc.name == referenceDoctype)
334
+
335
+ let parentFields = parentDoctype.fields.filter((field: DocField) => {
332
336
  return !fieldsToIgnore.includes(field.fieldtype) && field.reqd
333
337
  }).map((field: DocField) => field.fieldname)
334
- exportableFields.unshift('name')
335
- return {
336
- [props.doctype || props.data?.reference_doctype as string]: exportableFields
337
- }
338
+ parentFields.unshift('name')
339
+ exportableFields[referenceDoctype] = parentFields
340
+
341
+ let childDoctypes = parentDoctype.fields.filter((field: DocField) => {
342
+ return (field.fieldtype === 'Table' || field.fieldtype === 'Table MultiSelect') && field.reqd
343
+ })
344
+
345
+ childDoctypes.forEach((field: DocField) => {
346
+ let childDoctype = docs.find((doc: DocType) => doc.name == field.options)
347
+ if (childDoctype) {
348
+ let childFields = childDoctype.fields.filter((f: DocField) => {
349
+ return !fieldsToIgnore.includes(f.fieldtype) && f.reqd
350
+ }).map((f: DocField) => f.fieldname)
351
+ childFields.unshift('name')
352
+ exportableFields[field.fieldname] = childFields
353
+ }
354
+ })
355
+
356
+ return exportableFields
338
357
  }
339
358
 
340
359
  const getAllFields = () => {
341
360
  let doctypeMap: Record<string, string[]> = {}
342
361
  let docs = props.fields.data?.docs || []
343
- docs.forEach((doc: any) => {
362
+ docs.forEach((doc: DocType) => {
344
363
  let exportableFields = doc.fields.filter((field: DocField) => {
345
364
  return !fieldsToIgnore.includes(field.fieldtype)
346
365
  }).map((field: DocField) => field.fieldname)
@@ -35,6 +35,12 @@ export interface DocField {
35
35
  fieldname: string
36
36
  reqd: 0 | 1
37
37
  fieldtype: string
38
+ options?: string
39
+ }
40
+
41
+ export interface DocType {
42
+ name: string
43
+ fields: DocField[]
38
44
  }
39
45
 
40
46
  export interface File {
@@ -49,7 +49,7 @@ const open = ref(true)
49
49
  const newTitle = ref('')
50
50
  const file_ext = ref('')
51
51
 
52
- if (props.entity.is_group || props.entity.document) {
52
+ if (props.entity.is_group || props.entity.doc) {
53
53
  newTitle.value = props.entity.title
54
54
  } else {
55
55
  const parts = props.entity.title.split('.')
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <Dialog v-model="open" :options="{ size: 'lg' }">
3
3
  <template #body-main>
4
- <div class="p-4 sm:px-6">
4
+ <div class="p-4">
5
5
  <!-- Header -->
6
6
  <div class="flex w-full justify-between gap-x-2 mb-4">
7
7
  <div class="font-semibold text-2xl flex text-nowrap overflow-hidden">
@@ -180,7 +180,7 @@
180
180
  v-if="usersToAdd.length"
181
181
  label="Invite"
182
182
  variant="solid"
183
- @click="addPermissions"
183
+ @click="inviteUsers"
184
184
  />
185
185
  </div>
186
186
  </div>
@@ -338,17 +338,20 @@ watch(
338
338
  { immediate: true },
339
339
  )
340
340
 
341
- const addPermissions = () => {
341
+ const inviteUsers = () => {
342
342
  const access = getAccess(accessToAdd.value)
343
- for (const user of usersToAdd.value) {
343
+ for (let user of usersToAdd.value) {
344
344
  const r = {
345
345
  entity_name: props.entity.name,
346
346
  user,
347
347
  ...access,
348
348
  }
349
349
  props.updateAccess.submit(r)
350
+ const userObj = filteredUsers.value.find((k) => k.value === user)
351
+ // For new records
352
+ if (!userObj.email) userObj.email = userObj.label
350
353
  props.usersWithAccess.data.push({
351
- ...filteredUsers.value.find((k) => k.value === user),
354
+ ...userObj,
352
355
  ...access,
353
356
  })
354
357
  }
@@ -1,7 +1,8 @@
1
1
  <script setup lang="ts">
2
- import { ref, computed } from 'vue'
2
+ import { computed, ref } from 'vue'
3
3
  import {
4
4
  TagsInputRoot,
5
+ TagsInputInput,
5
6
  TagsInputItem,
6
7
  TagsInputItemText,
7
8
  TagsInputItemDelete,
@@ -10,11 +11,13 @@ import Combobox from '../../../../src/components/Combobox/Combobox.vue'
10
11
  import { getLabel, getIcon, RenderIcon, getValue } from './utils'
11
12
  import { type SimpleOption } from '../../../../src/components/Combobox/types'
12
13
  import { TagInputProps } from './types'
14
+ import LucideX from '~icons/lucide/x'
13
15
 
14
16
  const props = defineProps<TagInputProps>()
15
- const search = ref('')
16
17
  const options = defineModel<SimpleOption[]>('options', { default: [] })
17
18
  const modelValue = defineModel<SimpleOption[]>({ default: [] })
19
+ const rerenderCombobox = ref(0)
20
+
18
21
  const optionsWithIcons = computed(() => {
19
22
  if (!props.renderIcon) return options.value
20
23
  return options.value.map((k) =>
@@ -64,7 +67,7 @@ const filteredOptions = computed(() => {
64
67
  function addTag(tag: string) {
65
68
  if (!tag) return
66
69
  if (!modelValue.value.includes(tag)) modelValue.value.push(tag)
67
- search.value = ''
70
+ rerenderCombobox.value += 1
68
71
  }
69
72
 
70
73
  function removeTag(tag: string) {
@@ -81,33 +84,43 @@ function removeTag(tag: string) {
81
84
  v-for="item in selectedTags"
82
85
  :key="getValue(item)"
83
86
  :value="getValue(item)"
84
- class="shadow-sm m-0.25 mr-0 p-1.5 text-sm bg-white flex items-center justify-center gap-1.5 rounded p-0.5 ring-1 ring-outline-gray-1 shadow-xs"
87
+ class="shadow-sm m-0.25 mr-0 p-1.5 text-sm bg-white flex items-center justify-center gap-1.5 rounded p-0.5 ring-1 ring-outline-gray-2 shadow-xs"
85
88
  >
86
89
  <RenderIcon :icon="getIcon(item)" />
87
90
  <TagsInputItemText class="text-xs text-ink-gray-8">{{
88
91
  getLabel(item)
89
92
  }}</TagsInputItemText>
90
93
  <TagsInputItemDelete
91
- class="p-0.5 rounded bg-transparent hover:bg-blackA4"
94
+ class="p-0.5 rounded-sm bg-transparent hover:bg-surface-gray-1"
92
95
  @click="removeTag(getValue(item))"
93
96
  >
94
97
  <LucideX class="size-3 text-ink-gray-6" />
95
98
  </TagsInputItemDelete>
96
99
  </TagsInputItem>
97
- <!-- fix: keyboard navigation doesn't work -->
98
- <Combobox
99
- :options="filteredOptions"
100
- v-model="search"
101
- :placeholder
102
- class="flex-1 min-w-[100px] text-xs focus:outline-none"
103
- @update:modelValue="addTag"
104
- :open-on-click="true"
105
- variant="ghost"
106
- :hide-trigger="true"
107
- >
108
- <template #add-email="{ searchTerm }">
109
- {{ searchTerm }}
110
- </template>
111
- </Combobox>
100
+ <TagsInputInput :as-child="true">
101
+ <!-- Explicitly set unset type as TagInput passes it in -->
102
+ <Combobox
103
+ :key="rerenderCombobox"
104
+ :options="filteredOptions"
105
+ type=""
106
+ :placeholder
107
+ class="flex-1 min-w-[100px] text-xs focus:outline-none"
108
+ @update:modelValue="addTag"
109
+ :open-on-click="true"
110
+ variant="ghost"
111
+ >
112
+ <template #add-email="{ searchTerm }">
113
+ {{ searchTerm }}
114
+ </template>
115
+ </Combobox>
116
+ </TagsInputInput>
112
117
  </TagsInputRoot>
113
118
  </template>
119
+ <style>
120
+ [data-state='active'] {
121
+ background: var(--surface-gray-1);
122
+ }
123
+ [aria-label='Show popup'] {
124
+ display: none;
125
+ }
126
+ </style>
@@ -68,7 +68,7 @@ export const usersWithAccess = createResource({
68
68
  })
69
69
 
70
70
  export const updateAccess = createResource({
71
- url: 'drive.api.files.call_controller_method',
71
+ url: 'drive.api.files.update_access',
72
72
  makeParams: (params) => ({ ...params, method: params.method || 'share' }),
73
73
  onError: (error) => toast.error(error.messages[0]),
74
74
  })
@@ -96,11 +96,10 @@ export const getTeam = createResource({
96
96
  })
97
97
 
98
98
  export const rename = createResource({
99
- url: 'drive.api.files.call_controller_method',
99
+ url: 'drive.api.files.rename',
100
100
  method: 'POST',
101
101
  makeParams: (data) => {
102
102
  return {
103
- method: 'rename',
104
103
  ...data,
105
104
  }
106
105
  },
@@ -52,7 +52,7 @@ function slugger(title) {
52
52
  })
53
53
  }
54
54
 
55
- const copyToClipboard = (str) => {
55
+ export const copyToClipboard = (str) => {
56
56
  if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
57
57
  return navigator.clipboard.writeText(str)
58
58
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pimelon-ui",
3
- "version": "0.1.262",
3
+ "version": "0.1.267",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -111,7 +111,7 @@
111
111
  "husky": "^9.1.7",
112
112
  "idb-keyval": "^6.2.0",
113
113
  "lowlight": "^3.3.0",
114
- "lucide-static": "^0.543.0",
114
+ "lucide-static": "^0.545.0",
115
115
  "marked": "^15.0.12",
116
116
  "ora": "5.4.1",
117
117
  "prettier": "^3.3.2",
@@ -133,6 +133,8 @@
133
133
  "devDependencies": {
134
134
  "@vitejs/plugin-vue": "^4.0.0",
135
135
  "autoprefixer": "^10.4.13",
136
+ "cypress": "^15.8.2",
137
+ "cypress-split": "^1.24.28",
136
138
  "jsdom": "^27.4.0",
137
139
  "lint-staged": ">=10",
138
140
  "msw": "^2.7.0",
@@ -0,0 +1,64 @@
1
+ import Alert from './Alert.vue'
2
+ import { h } from 'vue'
3
+
4
+ const titleTxt = 'some title'
5
+ const description = 'some description'
6
+ const el = '[role="alert"]'
7
+ const themes = ['blue', 'red', 'green']
8
+
9
+ const TestIcon = {
10
+ render() {
11
+ return h('svg', { 'data-cy': 'prefix-icon' })
12
+ },
13
+ }
14
+
15
+ describe('Alert', () => {
16
+ it('Test text', () => {
17
+ cy.mount(Alert, {
18
+ props: {
19
+ title: titleTxt,
20
+ description: description,
21
+ },
22
+ })
23
+
24
+ cy.get(`${el} span`).should('have.text', titleTxt)
25
+ cy.get(`${el} p`).should('have.text', description)
26
+ })
27
+
28
+ it('Themes', () => {
29
+ themes.forEach((x) => {
30
+ cy.mount(Alert, {
31
+ props: { theme: x, title: titleTxt, description: description },
32
+ })
33
+
34
+ cy.get(el).should('have.class', `bg-surface-${x}-2`)
35
+ })
36
+ })
37
+
38
+ it('Dismiss', () => {
39
+ cy.mount(Alert)
40
+ cy.get(el).should('exist')
41
+ cy.get(`${el} button`).click()
42
+ cy.get(el).should('not.exist')
43
+ })
44
+
45
+ it('Non Dismissable', () => {
46
+ cy.mount(Alert, { props: { dismissable: false } })
47
+ cy.get(`${el} button`).should('not.exist')
48
+ })
49
+
50
+ it('Icon slot', () => {
51
+ cy.mount(Alert, {
52
+ slots: { icon: TestIcon },
53
+ })
54
+ cy.get('[data-cy="prefix-icon"]').should('exist')
55
+ })
56
+
57
+ it('Footer slot', () => {
58
+ cy.mount(Alert, {
59
+ slots: { footer: h('div', { id: 'footer' }, 'some footer') },
60
+ })
61
+
62
+ cy.get(`${el} #footer`).should('exist')
63
+ })
64
+ })
@@ -0,0 +1,47 @@
1
+ import Avatar from './Avatar.vue'
2
+
3
+ const sizes = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl']
4
+
5
+ const sizeHeights = {
6
+ xs: '4',
7
+ sm: '5',
8
+ md: '6',
9
+ lg: '7',
10
+ xl: '8',
11
+ '2xl': '10',
12
+ '3xl': '11.5',
13
+ }
14
+
15
+ const defaultProps = {
16
+ 'data-cy': 'avatar',
17
+ image: 'https://avatars.githubusercontent.com/u/499550',
18
+ label: 'Abc',
19
+ }
20
+
21
+ describe('Avatar', () => {
22
+ it('Renders', () => {
23
+ cy.mount(Avatar, {
24
+ props: defaultProps,
25
+ })
26
+
27
+ cy.get('[data-cy="avatar"]').should('exist')
28
+ })
29
+
30
+ it('Sizes', () => {
31
+ sizes.forEach((x) => {
32
+ cy.mount(Avatar, {
33
+ props: { ...defaultProps, size: x },
34
+ })
35
+
36
+ cy.get('[data-cy="avatar"]').should('have.class', 'h-' + sizeHeights[x])
37
+ })
38
+ })
39
+
40
+ it('Name', () => {
41
+ cy.mount(Avatar, {
42
+ props: { 'data-cy': 'avatar', label: 'Abc' },
43
+ })
44
+
45
+ cy.get('[data-cy="avatar"]').should('have.text', 'A')
46
+ })
47
+ })
@@ -0,0 +1,273 @@
1
+ import Badge from './Badge.vue'
2
+ import { h } from 'vue'
3
+
4
+ describe('<Badge />', () => {
5
+ it('renders default badge', () => {
6
+ cy.mount(Badge, {
7
+ slots: {
8
+ default: 'Default',
9
+ },
10
+ })
11
+ cy.get('.inline-flex.rounded-full').should('contain.text', 'Default')
12
+ // Default theme is gray, variant is subtle
13
+ cy.get('.inline-flex.rounded-full').should('have.class', 'text-ink-gray-6')
14
+ cy.get('.inline-flex.rounded-full').should('have.class', 'bg-surface-gray-2')
15
+ })
16
+
17
+ it('renders label prop', () => {
18
+ cy.mount(Badge, {
19
+ props: {
20
+ label: 'Badge Label',
21
+ },
22
+ })
23
+ cy.get('.inline-flex.rounded-full').should('have.text', 'Badge Label')
24
+ })
25
+
26
+ it('renders different themes with subtle variant', () => {
27
+ // Gray (default)
28
+ cy.mount(Badge, {
29
+ props: {
30
+ theme: 'gray',
31
+ label: 'Gray',
32
+ },
33
+ })
34
+ cy.get('.inline-flex.rounded-full').should('have.class', 'text-ink-gray-6')
35
+ cy.get('.inline-flex.rounded-full').should('have.class', 'bg-surface-gray-2')
36
+
37
+ // Blue
38
+ cy.mount(Badge, {
39
+ props: {
40
+ theme: 'blue',
41
+ label: 'Blue',
42
+ },
43
+ })
44
+ cy.get('.inline-flex.rounded-full').should('have.class', 'text-ink-blue-2')
45
+ cy.get('.inline-flex.rounded-full').should('have.class', 'bg-surface-blue-2')
46
+
47
+ // Green
48
+ cy.mount(Badge, {
49
+ props: {
50
+ theme: 'green',
51
+ label: 'Green',
52
+ },
53
+ })
54
+ cy.get('.inline-flex.rounded-full').should('have.class', 'text-ink-green-3')
55
+ cy.get('.inline-flex.rounded-full').should('have.class', 'bg-surface-green-2')
56
+
57
+ // Orange
58
+ cy.mount(Badge, {
59
+ props: {
60
+ theme: 'orange',
61
+ label: 'Orange',
62
+ },
63
+ })
64
+ cy.get('.inline-flex.rounded-full').should('have.class', 'text-ink-amber-3')
65
+ cy.get('.inline-flex.rounded-full').should('have.class', 'bg-surface-amber-1')
66
+
67
+ // Red
68
+ cy.mount(Badge, {
69
+ props: {
70
+ theme: 'red',
71
+ label: 'Red',
72
+ },
73
+ })
74
+ cy.get('.inline-flex.rounded-full').should('have.class', 'text-ink-red-4')
75
+ cy.get('.inline-flex.rounded-full').should('have.class', 'bg-surface-red-2')
76
+ })
77
+
78
+ it('renders different variants with gray theme', () => {
79
+ // Solid
80
+ cy.mount(Badge, {
81
+ props: {
82
+ variant: 'solid',
83
+ label: 'Solid',
84
+ },
85
+ })
86
+ cy.get('.inline-flex.rounded-full').should('have.class', 'text-ink-white')
87
+ cy.get('.inline-flex.rounded-full').should('have.class', 'bg-surface-gray-7')
88
+
89
+ // Subtle (default)
90
+ cy.mount(Badge, {
91
+ props: {
92
+ variant: 'subtle',
93
+ label: 'Subtle',
94
+ },
95
+ })
96
+ cy.get('.inline-flex.rounded-full').should('have.class', 'text-ink-gray-6')
97
+ cy.get('.inline-flex.rounded-full').should('have.class', 'bg-surface-gray-2')
98
+
99
+ // Outline
100
+ cy.mount(Badge, {
101
+ props: {
102
+ variant: 'outline',
103
+ label: 'Outline',
104
+ },
105
+ })
106
+ cy.get('.inline-flex.rounded-full').should('have.class', 'text-ink-gray-6')
107
+ cy.get('.inline-flex.rounded-full').should('have.class', 'bg-transparent')
108
+ cy.get('.inline-flex.rounded-full').should('have.class', 'border-outline-gray-1')
109
+
110
+ // Ghost
111
+ cy.mount(Badge, {
112
+ props: {
113
+ variant: 'ghost',
114
+ label: 'Ghost',
115
+ },
116
+ })
117
+ cy.get('.inline-flex.rounded-full').should('have.class', 'text-ink-gray-6')
118
+ cy.get('.inline-flex.rounded-full').should('have.class', 'bg-transparent')
119
+ })
120
+
121
+ it('renders different sizes', () => {
122
+ // Small
123
+ cy.mount(Badge, {
124
+ props: {
125
+ size: 'sm',
126
+ label: 'Small',
127
+ },
128
+ })
129
+ cy.get('.inline-flex.rounded-full').should('have.class', 'h-4')
130
+ cy.get('.inline-flex.rounded-full').should('have.class', 'text-xs')
131
+ cy.get('.inline-flex.rounded-full').should('have.class', 'px-1.5')
132
+
133
+ // Medium (default)
134
+ cy.mount(Badge, {
135
+ props: {
136
+ size: 'md',
137
+ label: 'Medium',
138
+ },
139
+ })
140
+ cy.get('.inline-flex.rounded-full').should('have.class', 'h-5')
141
+ cy.get('.inline-flex.rounded-full').should('have.class', 'text-xs')
142
+ cy.get('.inline-flex.rounded-full').should('have.class', 'px-1.5')
143
+
144
+ // Large
145
+ cy.mount(Badge, {
146
+ props: {
147
+ size: 'lg',
148
+ label: 'Large',
149
+ },
150
+ })
151
+ cy.get('.inline-flex.rounded-full').should('have.class', 'h-6')
152
+ cy.get('.inline-flex.rounded-full').should('have.class', 'text-sm')
153
+ cy.get('.inline-flex.rounded-full').should('have.class', 'px-2')
154
+ })
155
+
156
+ it('renders prefix slot', () => {
157
+ const TestIcon = {
158
+ render() {
159
+ return h('svg', { 'data-cy': 'prefix-icon' })
160
+ },
161
+ }
162
+
163
+ cy.mount(Badge, {
164
+ props: { label: 'With Icon' },
165
+ slots: {
166
+ prefix: () => h(TestIcon),
167
+ },
168
+ })
169
+
170
+ cy.get('[data-cy="prefix-icon"]').should('exist')
171
+ cy.get('.inline-flex.rounded-full').should('contain.text', 'With Icon')
172
+ })
173
+
174
+ it('renders suffix slot', () => {
175
+ const TestIcon = {
176
+ render() {
177
+ return h('svg', { 'data-cy': 'suffix-icon' })
178
+ },
179
+ }
180
+
181
+ cy.mount(Badge, {
182
+ props: { label: 'With Icon' },
183
+ slots: {
184
+ suffix: () => h(TestIcon),
185
+ },
186
+ })
187
+
188
+ cy.get('[data-cy="suffix-icon"]').should('exist')
189
+ cy.get('.inline-flex.rounded-full').should('contain.text', 'With Icon')
190
+ })
191
+
192
+ it('renders both prefix and suffix slots', () => {
193
+ const PrefixIcon = {
194
+ render() {
195
+ return h('svg', { 'data-cy': 'prefix-icon' })
196
+ },
197
+ }
198
+
199
+ const SuffixIcon = {
200
+ render() {
201
+ return h('svg', { 'data-cy': 'suffix-icon' })
202
+ },
203
+ }
204
+
205
+ cy.mount(Badge, {
206
+ props: { label: 'With Icons' },
207
+ slots: {
208
+ prefix: () => h(PrefixIcon),
209
+ suffix: () => h(SuffixIcon),
210
+ },
211
+ })
212
+
213
+ cy.get('[data-cy="prefix-icon"]').should('exist')
214
+ cy.get('[data-cy="suffix-icon"]').should('exist')
215
+ cy.get('.inline-flex.rounded-full').should('contain.text', 'With Icons')
216
+ })
217
+
218
+ it('supports numeric label', () => {
219
+ cy.mount(Badge, {
220
+ props: {
221
+ label: 42,
222
+ },
223
+ })
224
+ cy.get('.inline-flex.rounded-full').should('have.text', '42')
225
+ })
226
+
227
+ it('has correct layout classes', () => {
228
+ cy.mount(Badge, {
229
+ props: {
230
+ label: 'Test',
231
+ },
232
+ })
233
+ cy.get('.inline-flex.rounded-full').should('have.class', 'inline-flex')
234
+ cy.get('.inline-flex.rounded-full').should('have.class', 'items-center')
235
+ cy.get('.inline-flex.rounded-full').should('have.class', 'rounded-full')
236
+ cy.get('.inline-flex.rounded-full').should('have.class', 'whitespace-nowrap')
237
+ })
238
+
239
+ it('renders prefix slot with correct size constraints', () => {
240
+ const TestIcon = {
241
+ render() {
242
+ return h('svg', { 'data-cy': 'prefix-icon', class: 'w-4 h-4' })
243
+ },
244
+ }
245
+
246
+ // Test with sm size
247
+ cy.mount(Badge, {
248
+ props: { label: 'SM', size: 'sm' },
249
+ slots: {
250
+ prefix: () => h(TestIcon),
251
+ },
252
+ })
253
+ cy.get('[data-cy="prefix-icon"]').parent().should('have.class', 'max-h-4')
254
+
255
+ // Test with md size (default)
256
+ cy.mount(Badge, {
257
+ props: { label: 'MD', size: 'md' },
258
+ slots: {
259
+ prefix: () => h(TestIcon),
260
+ },
261
+ })
262
+ cy.get('[data-cy="prefix-icon"]').parent().should('have.class', 'max-h-4')
263
+
264
+ // Test with lg size
265
+ cy.mount(Badge, {
266
+ props: { label: 'LG', size: 'lg' },
267
+ slots: {
268
+ prefix: () => h(TestIcon),
269
+ },
270
+ })
271
+ cy.get('[data-cy="prefix-icon"]').parent().should('have.class', 'max-h-6')
272
+ })
273
+ })