pimelon-ui 0.1.31 → 0.1.45

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": "pimelon-ui",
3
- "version": "0.1.31",
3
+ "version": "0.1.45",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -8,9 +8,6 @@
8
8
  "prettier": "npx prettier -w ./src",
9
9
  "prepare": "husky install",
10
10
  "bump-and-release": "git pull --rebase origin main && yarn version --patch && git push && git push --tags",
11
- "docs:dev": "vitepress dev docs",
12
- "docs:build": "vitepress build docs",
13
- "docs:serve": "vitepress serve docs",
14
11
  "dev": "vite",
15
12
  "build": "vite build",
16
13
  "preview": "vite preview",
@@ -62,12 +59,12 @@
62
59
  "vue-router": "^4.1.6"
63
60
  },
64
61
  "devDependencies": {
65
- "@histoire/plugin-vue": "^0.16.1",
62
+ "@histoire/plugin-vue": "^0.17.14",
66
63
  "@vitejs/plugin-vue": "^4.0.0",
67
64
  "autoprefixer": "^10.4.13",
68
65
  "cross-fetch": "^3.1.5",
69
- "histoire": "^0.16.2",
70
- "husky": "^9.0.11",
66
+ "histoire": "^0.17.14",
67
+ "husky": "^9.1.6",
71
68
  "lint-staged": ">=10",
72
69
  "postcss": "^8.4.21",
73
70
  "prettier": "2.7.1",
@@ -75,7 +72,6 @@
75
72
  "tailwindcss": "^3.2.7",
76
73
  "typescript": "^5.0.2",
77
74
  "vite": "^4.1.0",
78
- "vitepress": "^1.0.0-alpha.29",
79
75
  "vue": "^3.2.45",
80
76
  "vue-router": "^4.1.6"
81
77
  },
@@ -19,7 +19,7 @@
19
19
  <span class="truncate text-base leading-5" v-if="selectedValue">
20
20
  {{ displayValue(selectedValue) }}
21
21
  </span>
22
- <span class="text-base leading-5 text-gray-600" v-else>
22
+ <span class="text-base leading-5 text-gray-500" v-else>
23
23
  {{ placeholder || '' }}
24
24
  </span>
25
25
  </div>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { reactive } from 'vue'
2
+ import { reactive, ref } from 'vue'
3
3
  import FormControl from './FormControl.vue'
4
4
  import FeatherIcon from './FeatherIcon.vue'
5
5
  import Avatar from './Avatar.vue'
@@ -10,8 +10,11 @@ const state = reactive({
10
10
  placeholder: 'Placeholder',
11
11
  disabled: false,
12
12
  label: 'Label',
13
- modelValue: '',
14
13
  })
14
+ const inputValue = ref('')
15
+ const selectValue = ref(null)
16
+ const autocompleteValue = ref(null)
17
+ const checkboxValue = ref(false)
15
18
 
16
19
  const inputTypes = [
17
20
  'text',
@@ -34,11 +37,7 @@ const variants = ['subtle', 'outline']
34
37
  :title="inputType"
35
38
  >
36
39
  <div class="p-2">
37
- <FormControl
38
- :type="inputType"
39
- v-bind="state"
40
- v-model="state.modelValue"
41
- />
40
+ <FormControl :type="inputType" v-bind="state" v-model="inputValue" />
42
41
  </div>
43
42
  </Variant>
44
43
  <Variant title="select">
@@ -51,10 +50,11 @@ const variants = ['subtle', 'outline']
51
50
  { label: 'Three', value: '3' },
52
51
  ]"
53
52
  v-bind="state"
53
+ v-model="selectValue"
54
54
  />
55
55
  </div>
56
56
  </Variant>
57
- <Variant title="select">
57
+ <Variant title="autocomplete">
58
58
  <div class="p-2">
59
59
  <FormControl
60
60
  type="autocomplete"
@@ -64,12 +64,13 @@ const variants = ['subtle', 'outline']
64
64
  { label: 'Three', value: '3' },
65
65
  ]"
66
66
  v-bind="state"
67
+ v-model="autocompleteValue"
67
68
  />
68
69
  </div>
69
70
  </Variant>
70
71
  <Variant title="checkbox">
71
72
  <div class="p-2">
72
- <FormControl type="checkbox" v-bind="state" />
73
+ <FormControl type="checkbox" v-bind="state" v-model="checkboxValue" />
73
74
  </div>
74
75
  </Variant>
75
76
 
@@ -114,7 +114,7 @@
114
114
  </template>
115
115
 
116
116
  <script setup>
117
- import { Autocomplete, FeatherIcon, FormControl } from 'pimelon-ui'
117
+ import { Autocomplete, FeatherIcon, FormControl } from '../../index'
118
118
  import { computed, h } from 'vue'
119
119
  import FilterIcon from './FilterIcon.vue'
120
120
  import NestedPopover from './NestedPopover.vue'
@@ -9,7 +9,7 @@
9
9
  </template>
10
10
 
11
11
  <script setup>
12
- import { Autocomplete, createListResource } from 'pimelon-ui'
12
+ import { Autocomplete, createListResource } from '../../index'
13
13
  import { computed, ref, watch } from 'vue'
14
14
 
15
15
  const props = defineProps({
@@ -0,0 +1,24 @@
1
+ <template>
2
+ <div
3
+ class="flex h-full w-full flex-col items-center justify-center text-base"
4
+ >
5
+ <slot>
6
+ <div class="text-xl font-medium">{{ list.options.emptyState.title }}</div>
7
+ <div class="mt-1 text-base text-gray-600">
8
+ {{ list.options.emptyState.description }}
9
+ </div>
10
+ <Button
11
+ v-if="list.options.emptyState.button"
12
+ v-bind="list.options.emptyState.button"
13
+ class="mt-4"
14
+ ></Button>
15
+ </slot>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup>
20
+ import { inject } from 'vue'
21
+ import Button from '../Button.vue'
22
+
23
+ const list = inject('list')
24
+ </script>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <div class="flex items-center">
3
+ <button
4
+ @click="toggleGroup"
5
+ class="ml-[3px] mr-[11px] rounded p-1 hover:bg-gray-100"
6
+ >
7
+ <DownSolid
8
+ class="h-4 w-4 text-gray-900 transition-transform duration-200"
9
+ :class="[group.collapsed ? '-rotate-90' : '']"
10
+ />
11
+ </button>
12
+ <div class="w-full py-1.5 pr-2">
13
+ <component
14
+ v-if="list.slots['group-header']"
15
+ :is="list.slots['group-header']"
16
+ v-bind="{ group }"
17
+ />
18
+ <span v-else class="text-base font-medium leading-6">
19
+ {{ group.group }}
20
+ </span>
21
+ </div>
22
+ </div>
23
+ <div class="mx-2 h-px border-t border-gray-200"></div>
24
+ </template>
25
+ <script setup>
26
+ import { inject } from 'vue'
27
+ import DownSolid from '../../icons/DownSolid.vue'
28
+
29
+ const props = defineProps({
30
+ group: {
31
+ type: Object,
32
+ required: true,
33
+ },
34
+ })
35
+
36
+ const list = inject('list')
37
+
38
+ function toggleGroup() {
39
+ if (props.group.collapsed == null) {
40
+ props.group.collapsed = false
41
+ }
42
+ props.group.collapsed = !props.group.collapsed
43
+ }
44
+ </script>
@@ -0,0 +1,17 @@
1
+ <template>
2
+ <div class="mb-5 mt-2" v-if="!group.collapsed">
3
+ <ListRow v-for="row in group.rows" :key="row[list.rowKey]" :row="row" />
4
+ </div>
5
+ </template>
6
+ <script setup>
7
+ import ListRow from './ListRow.vue'
8
+ import { inject } from 'vue'
9
+
10
+ const props = defineProps({
11
+ group: {
12
+ type: Object,
13
+ required: true,
14
+ },
15
+ })
16
+ const list = inject('list')
17
+ </script>
@@ -0,0 +1,22 @@
1
+ <template>
2
+ <div class="h-full overflow-y-auto">
3
+ <div v-for="group in list.rows" :key="group.group">
4
+ <ListGroupHeader :group="group">
5
+ <slot
6
+ name="group-header"
7
+ v-if="$slots['group-header']"
8
+ v-bind="{ group }"
9
+ />
10
+ </ListGroupHeader>
11
+ <ListGroupRows :group="group" />
12
+ </div>
13
+ </div>
14
+ </template>
15
+
16
+ <script setup>
17
+ import ListGroupHeader from './ListGroupHeader.vue'
18
+ import ListGroupRows from './ListGroupRows.vue'
19
+ import { inject } from 'vue'
20
+
21
+ const list = inject('list')
22
+ </script>
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <div
3
3
  ref="columnRef"
4
- class="group flex items-center justify-between"
5
- :class="alignmentMap[item.align]"
4
+ class="group flex items-center"
5
+ :class="item.align ? alignmentMap[item.align] : 'justify-between'"
6
6
  >
7
7
  <div
8
8
  class="flex items-center space-x-2 truncate text-sm text-gray-600"
@@ -14,13 +14,14 @@
14
14
  class="[all:unset] hover:[all:unset]"
15
15
  >
16
16
  <div
17
- class="grid items-center space-x-4 rounded px-2 py-2.5"
17
+ class="grid items-center space-x-4 rounded px-2"
18
18
  :class="
19
19
  list.selections.has(row[list.rowKey])
20
20
  ? 'bg-gray-100 hover:bg-gray-200'
21
21
  : 'hover:bg-gray-50'
22
22
  "
23
23
  :style="{
24
+ height: rowHeight,
24
25
  gridTemplateColumns: getGridTemplateColumns(
25
26
  list.columns,
26
27
  list.options.selectable
@@ -42,7 +43,23 @@
42
43
  ]"
43
44
  >
44
45
  <slot v-bind="{ idx: i, column, item: row[column.key] }">
45
- <ListRowItem :item="row[column.key]" :align="column.align" />
46
+ <component
47
+ v-if="list.slots.cell"
48
+ :is="list.slots.cell"
49
+ v-bind="{
50
+ column,
51
+ row,
52
+ item: row[column.key],
53
+ align: column.align,
54
+ }"
55
+ />
56
+ <ListRowItem
57
+ v-else
58
+ :column="column"
59
+ :row="row"
60
+ :item="row[column.key]"
61
+ :align="column.align"
62
+ />
46
63
  </slot>
47
64
  </div>
48
65
  </div>
@@ -73,4 +90,11 @@ const isLastRow = computed(() => {
73
90
  props.row[list.value.rowKey]
74
91
  )
75
92
  })
93
+
94
+ const rowHeight = computed(() => {
95
+ if (typeof list.value.options.rowHeight === 'number') {
96
+ return `${list.value.options.rowHeight}px`
97
+ }
98
+ return list.value.options.rowHeight
99
+ })
76
100
  </script>
@@ -5,21 +5,38 @@
5
5
  class="flex items-center space-x-2"
6
6
  :class="alignmentMap[align]"
7
7
  >
8
- <slot name="prefix" />
8
+ <slot name="prefix">
9
+ <component
10
+ v-if="column.prefix"
11
+ :is="
12
+ typeof column.prefix === 'function'
13
+ ? column.prefix({ row })
14
+ : column.prefix
15
+ "
16
+ />
17
+ </slot>
9
18
  <slot v-bind="{ label }">
10
19
  <div class="truncate text-base">
11
- {{ label }}
20
+ {{ column?.getLabel ? column.getLabel({ row }) : label }}
12
21
  </div>
13
22
  </slot>
14
23
  <slot name="suffix" />
15
24
  </component>
16
25
  </template>
17
26
  <script setup>
18
- import { alignmentMap } from './utils'
19
- import Tooltip from '../Tooltip.vue'
20
27
  import { computed, inject } from 'vue'
28
+ import Tooltip from '../Tooltip.vue'
29
+ import { alignmentMap } from './utils'
21
30
 
22
31
  const props = defineProps({
32
+ column: {
33
+ type: Object,
34
+ default: {},
35
+ },
36
+ row: {
37
+ type: Object,
38
+ default: {},
39
+ },
23
40
  item: {
24
41
  type: [String, Number, Object],
25
42
  default: '',
@@ -4,19 +4,25 @@
4
4
  class="flex w-max min-w-full flex-col overflow-y-hidden"
5
5
  :class="$attrs.class"
6
6
  >
7
- <slot>
7
+ <slot v-bind="{ showGroupedRows, selectable }">
8
8
  <ListHeader />
9
- <ListRows />
10
- <ListSelectBanner v-if="_options.selectable" />
9
+ <template v-if="props.rows.length">
10
+ <ListGroups v-if="showGroupedRows" />
11
+ <ListRows v-else />
12
+ </template>
13
+ <ListEmptyState v-else />
14
+ <ListSelectBanner v-if="selectable" />
11
15
  </slot>
12
16
  </div>
13
17
  </div>
14
18
  </template>
15
19
  <script setup>
20
+ import ListEmptyState from './ListEmptyState.vue'
16
21
  import ListHeader from './ListHeader.vue'
17
22
  import ListRows from './ListRows.vue'
23
+ import ListGroups from './ListGroups.vue'
18
24
  import ListSelectBanner from './ListSelectBanner.vue'
19
- import { reactive, computed, provide, watch } from 'vue'
25
+ import { reactive, computed, provide, watch, useSlots } from 'vue'
20
26
 
21
27
  defineOptions({
22
28
  inheritAttrs: false,
@@ -37,16 +43,23 @@ const props = defineProps({
37
43
  },
38
44
  options: {
39
45
  type: Object,
40
- default: {
46
+ default: () => ({
41
47
  getRowRoute: null,
42
48
  onRowClick: null,
43
49
  showTooltip: true,
44
50
  selectable: true,
45
- resizeColumn: true,
46
- },
51
+ resizeColumn: false,
52
+ rowHeight: 40,
53
+ emptyState: {
54
+ title: 'No Data',
55
+ description: 'No data available',
56
+ },
57
+ }),
47
58
  },
48
59
  })
49
60
 
61
+ const slots = useSlots()
62
+
50
63
  let selections = reactive(new Set())
51
64
 
52
65
  const emit = defineEmits(['update:selections'])
@@ -60,12 +73,18 @@ let _options = computed(() => {
60
73
  return value === undefined ? true : value
61
74
  }
62
75
 
76
+ function defaultFalse(value) {
77
+ return value === undefined ? false : value
78
+ }
79
+
63
80
  return {
64
81
  getRowRoute: props.options.getRowRoute || null,
65
82
  onRowClick: props.options.onRowClick || null,
66
83
  showTooltip: defaultTrue(props.options.showTooltip),
67
84
  selectable: defaultTrue(props.options.selectable),
68
- resizeColumn: defaultTrue(props.options.resizeColumn),
85
+ resizeColumn: defaultFalse(props.options.resizeColumn),
86
+ rowHeight: props.options.rowHeight || 40,
87
+ emptyState: props.options.emptyState,
69
88
  }
70
89
  })
71
90
 
@@ -74,6 +93,16 @@ const allRowsSelected = computed(() => {
74
93
  return selections.size === props.rows.length
75
94
  })
76
95
 
96
+ const selectable = computed(() => {
97
+ return _options.value.selectable
98
+ })
99
+
100
+ let showGroupedRows = computed(() => {
101
+ return props.rows.every(
102
+ (row) => row.group && row.rows && Array.isArray(row.rows)
103
+ )
104
+ })
105
+
77
106
  function toggleRow(row) {
78
107
  if (!selections.delete(row)) {
79
108
  selections.add(row)
@@ -97,6 +126,7 @@ provide(
97
126
  options: _options.value,
98
127
  selections: selections,
99
128
  allRowsSelected: allRowsSelected.value,
129
+ slots: slots,
100
130
  toggleRow,
101
131
  toggleAllRows,
102
132
  }))
@@ -62,6 +62,31 @@ required to be passed in the `row` object.
62
62
  }
63
63
  ```
64
64
 
65
+ ### Grouped Rows
66
+
67
+ To render grouped rows, you must provide `rows` in the following format:
68
+
69
+ ```
70
+ [
71
+ {
72
+ group: 'Group Title 1',
73
+ collapsed: false,
74
+ rows: [
75
+ {id: 1, key1: value1, key2: value2, ...},
76
+ {id: 2, key1: value1, key2: value2, ...},
77
+ ]
78
+ },
79
+ {
80
+ group: 'Group Title 2',
81
+ collapsed: false,
82
+ rows: [
83
+ {id: 3, key1: value1, key2: value2, ...},
84
+ {id: 4, key1: value1, key2: value2, ...},
85
+ ]
86
+ },
87
+ ]
88
+ ```
89
+
65
90
  ### Options
66
91
 
67
92
  1. If you want to route using router-link just add a `getRowRoute` function
@@ -78,7 +103,7 @@ required to be passed in the `row` object.
78
103
  4. showTooltip (Boolean) - if true, tooltip will be shown on hover of row -
79
104
  default is true
80
105
  5. resizeColumn (Boolean) - if true, column can be resized by dragging the
81
- resizer on the right side of the column header - default is true
106
+ resizer on the right side of the column header - default is false
82
107
 
83
108
  ---
84
109
 
@@ -1,21 +1,31 @@
1
1
  <script setup>
2
- import ListView from './ListView/ListView.vue'
2
+ import { reactive, h, ref } from 'vue'
3
+ import Avatar from './Avatar.vue'
4
+ import Badge from './Badge.vue'
5
+ import Button from './Button.vue'
6
+ import FeatherIcon from './FeatherIcon.vue'
3
7
  import ListHeader from './ListView/ListHeader.vue'
4
8
  import ListHeaderItem from './ListView/ListHeaderItem.vue'
5
- import ListRows from './ListView/ListRows.vue'
6
9
  import ListRow from './ListView/ListRow.vue'
7
10
  import ListRowItem from './ListView/ListRowItem.vue'
11
+ import ListRows from './ListView/ListRows.vue'
12
+ import ListGroups from './ListView/ListGroups.vue'
8
13
  import ListSelectBanner from './ListView/ListSelectBanner.vue'
9
- import FeatherIcon from './FeatherIcon.vue'
10
- import Badge from './Badge.vue'
11
- import Button from './Button.vue'
12
- import Avatar from './Avatar.vue'
13
- import { reactive } from 'vue'
14
+ import ListView from './ListView/ListView.vue'
14
15
 
15
16
  const state = reactive({
16
17
  selectable: true,
17
18
  showTooltip: true,
18
19
  resizeColumn: true,
20
+ emptyState: {
21
+ title: 'No records found',
22
+ description: 'Create a new record to get started',
23
+ button: {
24
+ label: 'New Record',
25
+ variant: 'solid',
26
+ onClick: () => console.log('New Record'),
27
+ },
28
+ },
19
29
  })
20
30
 
21
31
  const simple_columns = reactive([
@@ -23,6 +33,14 @@ const simple_columns = reactive([
23
33
  label: 'Name',
24
34
  key: 'name',
25
35
  width: 3,
36
+ getLabel: ({ row }) => row.name,
37
+ prefix: ({ row }) => {
38
+ return h(Avatar, {
39
+ shape: 'circle',
40
+ image: row.user_image,
41
+ size: 'sm',
42
+ })
43
+ },
26
44
  },
27
45
  {
28
46
  label: 'Email',
@@ -46,6 +64,7 @@ const simple_rows = [
46
64
  email: 'john@doe.com',
47
65
  status: 'Active',
48
66
  role: 'Developer',
67
+ user_image: 'https://avatars.githubusercontent.com/u/499550',
49
68
  },
50
69
  {
51
70
  id: 2,
@@ -53,9 +72,148 @@ const simple_rows = [
53
72
  email: 'jane@doe.com',
54
73
  status: 'Inactive',
55
74
  role: 'HR',
75
+ user_image: 'https://avatars.githubusercontent.com/u/499120',
56
76
  },
57
77
  ]
58
78
 
79
+ const group_columns = reactive([
80
+ {
81
+ label: 'Name',
82
+ key: 'name',
83
+ width: 3,
84
+ },
85
+ {
86
+ label: 'Email',
87
+ key: 'email',
88
+ width: '200px',
89
+ },
90
+ {
91
+ label: 'Role',
92
+ key: 'role',
93
+ },
94
+ {
95
+ label: 'Status',
96
+ key: 'status',
97
+ },
98
+ ])
99
+
100
+ const grouped_rows = ref([
101
+ {
102
+ group: 'Developer',
103
+ collapsed: false,
104
+ rows: [
105
+ {
106
+ id: 2,
107
+ name: 'Gary Fox',
108
+ email: 'gary@fox.com',
109
+ status: 'Inactive',
110
+ role: 'Developer',
111
+ },
112
+ {
113
+ id: 6,
114
+ name: 'Emily Davis',
115
+ email: 'emily@davis.com',
116
+ status: 'Active',
117
+ role: 'Developer',
118
+ },
119
+ {
120
+ id: 9,
121
+ name: 'David Lee',
122
+ email: 'david@lee.com',
123
+ status: 'Inactive',
124
+ role: 'Developer',
125
+ },
126
+ ],
127
+ },
128
+ {
129
+ group: 'Manager',
130
+ collapsed: false,
131
+ rows: [
132
+ {
133
+ id: 3,
134
+ name: 'John Doe',
135
+ email: 'john@doe.com',
136
+ status: 'Active',
137
+ role: 'Manager',
138
+ },
139
+ {
140
+ id: 8,
141
+ name: 'Sarah Wilson',
142
+ email: 'sarah@wilson.com',
143
+ status: 'Active',
144
+ role: 'Manager',
145
+ },
146
+ ],
147
+ },
148
+ {
149
+ group: 'Designer',
150
+ collapsed: false,
151
+ rows: [
152
+ {
153
+ id: 4,
154
+ name: 'Alice Smith',
155
+ email: 'alice@smith.com',
156
+ status: 'Active',
157
+ role: 'Designer',
158
+ },
159
+ {
160
+ id: 10,
161
+ name: 'Olivia Taylor',
162
+ email: 'olivia@taylor.com',
163
+ status: 'Active',
164
+ role: 'Designer',
165
+ },
166
+ ],
167
+ },
168
+ {
169
+ group: 'HR',
170
+ collapsed: false,
171
+ rows: [
172
+ {
173
+ id: 1,
174
+ name: 'Jane Mary',
175
+ email: 'jane@doe.com',
176
+ status: 'Inactive',
177
+ role: 'HR',
178
+ },
179
+ {
180
+ id: 7,
181
+ name: 'Michael Brown',
182
+ email: 'michael@brown.com',
183
+ status: 'Inactive',
184
+ role: 'HR',
185
+ },
186
+ {
187
+ id: 12,
188
+ name: 'Sophia Martinez',
189
+ email: 'sophia@martinez.com',
190
+ status: 'Active',
191
+ role: 'HR',
192
+ },
193
+ ],
194
+ },
195
+ {
196
+ group: 'Tester',
197
+ collapsed: false,
198
+ rows: [
199
+ {
200
+ id: 5,
201
+ name: 'Bob Johnson',
202
+ email: 'bob@johnson.com',
203
+ status: 'Inactive',
204
+ role: 'Tester',
205
+ },
206
+ {
207
+ id: 11,
208
+ name: 'James Anderson',
209
+ email: 'james@anderson.com',
210
+ status: 'Inactive',
211
+ role: 'Tester',
212
+ },
213
+ ],
214
+ },
215
+ ])
216
+
59
217
  const custom_columns = reactive([
60
218
  {
61
219
  label: 'Name',
@@ -121,7 +279,7 @@ const custom_rows = [
121
279
  <Story :layout="{ type: 'grid', width: '95%' }">
122
280
  <Variant title="Simple List">
123
281
  <ListView
124
- class="h-[250px]"
282
+ class="h-[150px]"
125
283
  :columns="simple_columns"
126
284
  :rows="simple_rows"
127
285
  :options="{
@@ -135,7 +293,7 @@ const custom_rows = [
135
293
  </Variant>
136
294
  <Variant title="Custom List">
137
295
  <ListView
138
- class="h-[250px]"
296
+ class="h-[150px]"
139
297
  :columns="custom_columns"
140
298
  :rows="custom_rows"
141
299
  :options="{
@@ -202,11 +360,79 @@ const custom_rows = [
202
360
  </ListSelectBanner>
203
361
  </ListView>
204
362
  </Variant>
363
+ <Variant title="Grouped Rows">
364
+ <ListView
365
+ class="h-[250px]"
366
+ :columns="group_columns"
367
+ :rows="grouped_rows"
368
+ :options="{
369
+ getRowRoute: (row) => ({ name: 'User', params: { userId: row.id } }),
370
+ selectable: state.selectable,
371
+ showTooltip: state.showTooltip,
372
+ resizeColumn: state.resizeColumn,
373
+ }"
374
+ row-key="id"
375
+ >
376
+ <template #group-header="{ group }">
377
+ <span class="text-base font-medium leading-6 text-gray-900">
378
+ {{ group.group }} ({{ group.rows.length }})
379
+ </span>
380
+ </template>
381
+ </ListView>
382
+ </Variant>
383
+ <Variant title="Cell Slot">
384
+ <div>
385
+ <ListView
386
+ class="h-[250px]"
387
+ :columns="simple_columns"
388
+ :rows="simple_rows"
389
+ :options="{
390
+ selectable: state.selectable,
391
+ showTooltip: state.showTooltip,
392
+ resizeColumn: state.resizeColumn,
393
+ emptyState: state.emptyState,
394
+ }"
395
+ row-key="id"
396
+ >
397
+ <template #cell="{ item, row, column }">
398
+ <Badge v-if="column.key == 'status'">{{ item }}</Badge>
399
+ <span class="font-medium text-gray-700" v-else>{{ item }}</span>
400
+ </template>
401
+ </ListView>
402
+ </div>
403
+ </Variant>
404
+ <Variant title="Empty List">
405
+ <div>
406
+ <ListView
407
+ class="h-[250px]"
408
+ :columns="simple_columns"
409
+ :rows="[]"
410
+ :options="{
411
+ selectable: state.selectable,
412
+ showTooltip: state.showTooltip,
413
+ resizeColumn: state.resizeColumn,
414
+ emptyState: state.emptyState,
415
+ }"
416
+ row-key="id"
417
+ />
418
+ </div>
419
+ </Variant>
205
420
 
206
421
  <template #controls>
207
422
  <HstCheckbox v-model="state.selectable" title="Selectable" />
208
423
  <HstCheckbox v-model="state.showTooltip" title="Show tooltip" />
209
424
  <HstCheckbox v-model="state.resizeColumn" title="Resize Column" />
425
+ <!-- empty state config -->
426
+ <HstText
427
+ v-model="state.emptyState.title"
428
+ title="Empty Title"
429
+ placeholder="No records found"
430
+ />
431
+ <HstText
432
+ v-model="state.emptyState.description"
433
+ title="Empty Description"
434
+ placeholder="Create a new record to get started"
435
+ />
210
436
  </template>
211
437
  </Story>
212
438
  </template>
@@ -17,8 +17,8 @@
17
17
  <teleport to="#melonui-popper-root">
18
18
  <div
19
19
  ref="popover"
20
- :class="popoverClass"
21
- class="popover-container relative z-[100]"
20
+ class="relative z-[100]"
21
+ :class="[popoverContainerClass, popoverClass]"
22
22
  :style="{ minWidth: targetWidth ? targetWidth + 'px' : null }"
23
23
  @mouseover="pointerOverTargetOrPopup = true"
24
24
  @mouseleave="onMouseleave"
@@ -87,6 +87,7 @@ export default {
87
87
  expose: ['open', 'close'],
88
88
  data() {
89
89
  return {
90
+ popoverContainerClass: 'body-container',
90
91
  showPopup: false,
91
92
  targetWidth: null,
92
93
  pointerOverTargetOrPopup: false,
@@ -111,14 +112,35 @@ export default {
111
112
  },
112
113
  mounted() {
113
114
  this.listener = (e) => {
114
- let $els = [this.$refs.reference, this.$refs.popover]
115
- let insideClick = $els.some(
116
- ($el) => $el && (e.target === $el || $el.contains(e.target))
117
- )
115
+ const clickedElement = e.target
116
+ const reference = this.$refs.reference
117
+ const popoverBody = this.$refs.popover
118
+ const insideClick =
119
+ clickedElement === reference ||
120
+ clickedElement === popoverBody ||
121
+ reference?.contains(clickedElement) ||
122
+ popoverBody?.contains(clickedElement)
118
123
  if (insideClick) {
119
124
  return
120
125
  }
121
- this.close()
126
+
127
+ const root = document.getElementById('melonui-popper-root')
128
+ const insidePopoverRoot = root.contains(clickedElement)
129
+ if (!insidePopoverRoot) {
130
+ return this.close()
131
+ }
132
+
133
+ const bodyClass = `.${this.popoverContainerClass}`
134
+ const clickedElementBody = clickedElement?.closest(bodyClass)
135
+ const currentPopoverBody = reference?.closest(bodyClass)
136
+ const isSiblingClicked =
137
+ clickedElementBody &&
138
+ currentPopoverBody &&
139
+ clickedElementBody === currentPopoverBody
140
+
141
+ if (isSiblingClicked) {
142
+ this.close()
143
+ }
122
144
  }
123
145
  if (this.hideOnBlur) {
124
146
  document.addEventListener('click', this.listener)
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="min-w-50 space-y-[10px]">
2
+ <div class="w-full space-y-[10px]">
3
3
  <div
4
4
  v-if="props.label || props.hint"
5
5
  class="flex items-baseline justify-between"
@@ -10,6 +10,14 @@
10
10
  >
11
11
  <slot name="prefix"> </slot>
12
12
  </div>
13
+ <div
14
+ v-if="placeholder"
15
+ v-show="!modelValue"
16
+ class="pointer-events-none absolute text-gray-500"
17
+ :class="[fontSizeClasses, paddingClasses]"
18
+ >
19
+ {{ placeholder }}
20
+ </div>
13
21
  <select
14
22
  :class="selectClasses"
15
23
  :disabled="disabled"
@@ -33,7 +41,10 @@
33
41
 
34
42
  <script setup lang="ts">
35
43
  import { computed, useSlots, useAttrs } from 'vue'
36
- import debounce from '../utils/debounce'
44
+
45
+ defineOptions({
46
+ inheritAttrs: false,
47
+ })
37
48
 
38
49
  type SelectOption =
39
50
  | string
@@ -86,19 +97,30 @@ const textColor = computed(() => {
86
97
  return props.disabled ? 'text-gray-500' : 'text-gray-800'
87
98
  })
88
99
 
89
- const selectClasses = computed(() => {
90
- let sizeClasses = {
91
- sm: 'text-base rounded h-7',
92
- md: 'text-base rounded h-8',
93
- lg: 'text-lg rounded-md h-10',
94
- xl: 'text-xl rounded-md h-10',
100
+ const fontSizeClasses = computed(() => {
101
+ return {
102
+ sm: 'text-base',
103
+ md: 'text-base',
104
+ lg: 'text-lg',
105
+ xl: 'text-xl',
95
106
  }[props.size]
107
+ })
96
108
 
97
- let paddingClasses = {
98
- sm: ['py-0', slots.prefix ? 'pl-8' : 'pl-2'],
99
- md: ['py-0', slots.prefix ? 'pl-9' : 'pl-2.5'],
100
- lg: ['py-0', slots.prefix ? 'pl-10' : 'pl-3'],
101
- xl: ['py-0', slots.prefix ? 'pl-10' : 'pl-3'],
109
+ const paddingClasses = computed(() => {
110
+ return {
111
+ sm: 'px-2',
112
+ md: 'px-2.5',
113
+ lg: 'px-3',
114
+ xl: 'px-3',
115
+ }[props.size]
116
+ })
117
+
118
+ const selectClasses = computed(() => {
119
+ let sizeClasses = {
120
+ sm: 'rounded h-7',
121
+ md: 'rounded h-8',
122
+ lg: 'rounded-md h-10',
123
+ xl: 'rounded-md h-10',
102
124
  }[props.size]
103
125
 
104
126
  let variant = props.disabled ? 'disabled' : props.variant
@@ -118,10 +140,11 @@ const selectClasses = computed(() => {
118
140
 
119
141
  return [
120
142
  sizeClasses,
121
- paddingClasses,
143
+ fontSizeClasses.value,
144
+ paddingClasses.value,
122
145
  variantClasses,
123
146
  textColor.value,
124
- 'transition-colors w-full',
147
+ 'transition-colors w-full py-0',
125
148
  ]
126
149
  })
127
150
 
@@ -0,0 +1,8 @@
1
+ <template>
2
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16">
3
+ <path
4
+ fill="currentColor"
5
+ d="M4.293 5.28h7.413a.5.5 0 0 1 .41.787l-3.707 5.295a.5.5 0 0 1-.82 0L3.884 6.067a.5.5 0 0 1 .41-.787Z"
6
+ />
7
+ </svg>
8
+ </template>
package/src/index.js CHANGED
@@ -41,9 +41,13 @@ export {
41
41
  export { default as ListView } from './components/ListView/ListView.vue'
42
42
  export { default as ListHeader } from './components/ListView/ListHeader.vue'
43
43
  export { default as ListHeaderItem } from './components/ListView/ListHeaderItem.vue'
44
+ export { default as ListEmptyState } from './components/ListView/ListEmptyState.vue'
44
45
  export { default as ListRows } from './components/ListView/ListRows.vue'
45
46
  export { default as ListRow } from './components/ListView/ListRow.vue'
46
47
  export { default as ListRowItem } from './components/ListView/ListRowItem.vue'
48
+ export { default as ListGroups } from './components/ListView/ListGroups.vue'
49
+ export { default as ListGroupHeader } from './components/ListView/ListGroupHeader.vue'
50
+ export { default as ListGroupRows } from './components/ListView/ListGroupRows.vue'
47
51
  export { default as ListSelectBanner } from './components/ListView/ListSelectBanner.vue'
48
52
  export { default as ListFooter } from './components/ListView/ListFooter.vue'
49
53
  export { default as Toast } from './components/Toast.vue'
@@ -37,6 +37,7 @@ export function createListResource(options, vm) {
37
37
  doctype: options.doctype,
38
38
  fields: options.fields,
39
39
  filters: options.filters,
40
+ orFilters: options.orFilters,
40
41
  orderBy: options.orderBy,
41
42
  start: options.start || 0,
42
43
  pageLength: options.pageLength || 20,
@@ -59,6 +60,7 @@ export function createListResource(options, vm) {
59
60
  doctype: out.doctype,
60
61
  fields: out.fields,
61
62
  filters: out.filters,
63
+ or_filters: out.orFilters,
62
64
  order_by: out.orderBy,
63
65
  start: out.start,
64
66
  limit: out.pageLength,
@@ -71,9 +73,7 @@ export function createListResource(options, vm) {
71
73
  },
72
74
  onSuccess(data) {
73
75
  out.hasPreviousPage = !!out.start
74
- if (data.length < out.pageLength) {
75
- out.hasNextPage = false
76
- }
76
+ out.hasNextPage = data.length < out.pageLength ? false : true
77
77
  let pagedData
78
78
  if (!out.start || out.start == 0) {
79
79
  pagedData = data
@@ -23,11 +23,9 @@ let createMixin = (mixinOptions) => ({
23
23
  console.warn('Failed to get resource options\n\n', error)
24
24
  out = null
25
25
  }
26
- return JSON.stringify(out)
26
+ return out
27
27
  },
28
- (_options, _oldOptions) => {
29
- let options = _options ? JSON.parse(_options) : null
30
- let oldOptions = _oldOptions ? JSON.parse(_oldOptions) : null
28
+ (options, oldOptions) => {
31
29
  if (!options) {
32
30
  return
33
31
  }
@@ -31,7 +31,7 @@ export function melonRequest(options) {
31
31
  let url = options.url
32
32
  if (response.ok) {
33
33
  const data = await response.json()
34
- if (data.docs || url === 'login') {
34
+ if (data.docs || url === '/api/method/login') {
35
35
  return data
36
36
  }
37
37
  if (data.exc) {
package/vite.js CHANGED
@@ -1,27 +1,30 @@
1
1
  const path = require('path')
2
2
  const fs = require('fs')
3
3
 
4
- module.exports = function proxyOptions({ port = 8080 } = {}) {
4
+ module.exports = function proxyOptions({
5
+ port = 8080,
6
+ source = '^/(app|login|api|assets|files)',
7
+ } = {}) {
5
8
  const config = getCommonSiteConfig()
6
9
  const webserver_port = config ? config.webserver_port : 8000
7
10
  if (!config) {
8
11
  console.log('No common_site_config.json found, using default port 8000')
9
12
  }
13
+ let proxy = {}
14
+ proxy[source] = {
15
+ target: `http://127.0.0.1:${webserver_port}`,
16
+ ws: true,
17
+ router: function (req) {
18
+ const site_name = req.headers.host.split(':')[0]
19
+ return `http://${site_name}:${webserver_port}`
20
+ }
21
+ }
10
22
  return {
11
23
  name: 'melonui-vite-plugin',
12
24
  config: () => ({
13
25
  server: {
14
26
  port: port,
15
- proxy: {
16
- '^/(app|login|api|assets|files)': {
17
- target: `http://127.0.0.1:${webserver_port}`,
18
- ws: true,
19
- router: function (req) {
20
- const site_name = req.headers.host.split(':')[0]
21
- return `http://${site_name}:${webserver_port}`
22
- },
23
- },
24
- },
27
+ proxy: proxy,
25
28
  },
26
29
  }),
27
30
  }