frappe-ui 0.1.60 → 0.1.61

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": "frappe-ui",
3
- "version": "0.1.60",
3
+ "version": "0.1.61",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -50,6 +50,7 @@
50
50
  "@vueuse/core": "^10.4.1",
51
51
  "feather-icons": "^4.28.0",
52
52
  "idb-keyval": "^6.2.0",
53
+ "prettier": "^3.3.2",
53
54
  "radix-vue": "^1.5.3",
54
55
  "showdown": "^2.1.0",
55
56
  "socket.io-client": "^4.5.1",
@@ -68,7 +69,7 @@
68
69
  "husky": ">=6",
69
70
  "lint-staged": ">=10",
70
71
  "postcss": "^8.4.21",
71
- "prettier": "2.7.1",
72
+ "prettier": "3.3.2",
72
73
  "prettier-plugin-tailwindcss": "^0.1.13",
73
74
  "tailwindcss": "^3.2.7",
74
75
  "typescript": "^5.0.2",
@@ -15,7 +15,10 @@
15
15
  :disabled="disabled"
16
16
  :id="htmlId"
17
17
  :checked="Boolean(modelValue)"
18
- @change="(e) => $emit('update:modelValue', (e.target as HTMLInputElement).checked)"
18
+ @change="
19
+ (e) =>
20
+ $emit('update:modelValue', (e.target as HTMLInputElement).checked)
21
+ "
19
22
  v-bind="attrs"
20
23
  />
21
24
  <label class="block" :class="labelClasses" v-if="label" :for="htmlId">
@@ -64,8 +67,8 @@ const inputClasses = computed(() => {
64
67
  let interactionClasses = props.disabled
65
68
  ? ''
66
69
  : props.padding
67
- ? 'focus:ring-0'
68
- : 'hover:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400 active:bg-gray-100'
70
+ ? 'focus:ring-0'
71
+ : 'hover:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400 active:bg-gray-100'
69
72
 
70
73
  let sizeClasses = {
71
74
  sm: 'w-3.5 h-3.5',
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ import { reactive, ref } from 'vue'
3
+ import DatePicker from './DatePicker.vue'
4
+ import DateTimePicker from './DateTimePicker.vue'
5
+ import DateRangePicker from './DateRangePicker.vue'
6
+
7
+ const state = reactive({
8
+ variant: 'subtle',
9
+ placeholder: 'Placeholder',
10
+ disabled: false,
11
+ label: 'Label',
12
+ })
13
+ const dateValue = ref('')
14
+ const dateTimeValue = ref('')
15
+ const dateRangeValue = ref('')
16
+ </script>
17
+
18
+ <template>
19
+ <Story :layout="{ type: 'grid', width: 500 }">
20
+ <Variant title="Date">
21
+ <div class="p-2">
22
+ <DatePicker v-model="dateValue" v-bind="state" />
23
+ </div>
24
+ </Variant>
25
+ <Variant title="Date Time">
26
+ <div class="p-2">
27
+ <DateTimePicker v-model="dateTimeValue" v-bind="state" />
28
+ </div>
29
+ </Variant>
30
+ <Variant title="Date Range">
31
+ <div class="p-2">
32
+ <DateRangePicker v-model="dateRangeValue" v-bind="state" />
33
+ </div>
34
+ </Variant>
35
+ </Story>
36
+ </template>
@@ -1,91 +1,109 @@
1
1
  <template>
2
- <Popover @open="selectCurrentMonthYear" transition="default">
2
+ <Popover
3
+ @open="selectCurrentMonthYear"
4
+ class="flex w-full [&>div:first-child]:w-full"
5
+ >
3
6
  <template #target="{ togglePopover }">
4
7
  <Input
8
+ readonly
5
9
  type="text"
6
10
  icon-left="calendar"
7
- :class="inputClass"
8
- :value="
9
- modelValue && formatValue ? formatValue(modelValue) : modelValue || ''
10
- "
11
11
  :placeholder="placeholder"
12
+ :value="dateValue && formatter ? formatter(dateValue) : dateValue"
12
13
  @focus="!readonly ? togglePopover() : null"
13
- readonly
14
+ class="w-full"
15
+ :class="inputClass"
16
+ v-bind="$attrs"
14
17
  />
15
18
  </template>
16
- <template #body-main="{ togglePopover }">
17
- <div class="mt-1 select-none p-3 text-left">
18
- <div class="flex items-center justify-between">
19
- <span class="text-base font-medium text-blue-500">
19
+ <template #body="{ togglePopover }">
20
+ <div
21
+ class="mt-2 w-fit select-none divide-y rounded-lg bg-white text-base shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
22
+ >
23
+ <div class="flex items-center p-1 text-gray-500">
24
+ <Button variant="ghost" class="h-7 w-7" @click="prevMonth">
25
+ <FeatherIcon
26
+ :stroke-width="2"
27
+ name="chevron-left"
28
+ class="h-4 w-4"
29
+ />
30
+ </Button>
31
+ <div class="flex-1 text-center text-base font-medium text-gray-700">
20
32
  {{ formatMonth }}
21
- </span>
22
- <span class="flex">
23
- <div
24
- class="grid h-5 w-5 cursor-pointer place-items-center rounded-md hover:bg-gray-100"
25
- >
26
- <FeatherIcon
27
- @click="prevMonth"
28
- name="chevron-left"
29
- class="h-4 w-4"
30
- />
31
- </div>
32
- <div
33
- class="ml-2 grid h-5 w-5 cursor-pointer place-items-center rounded-md hover:bg-gray-100"
34
- >
35
- <FeatherIcon
36
- @click="nextMonth"
37
- name="chevron-right"
38
- class="h-4 w-4"
39
- />
40
- </div>
41
- </span>
33
+ </div>
34
+ <Button variant="ghost" class="h-7 w-7" @click="nextMonth">
35
+ <FeatherIcon
36
+ :stroke-width="2"
37
+ name="chevron-right"
38
+ class="h-4 w-4"
39
+ />
40
+ </Button>
42
41
  </div>
43
- <div class="mt-2 text-sm">
44
- <div class="grid w-full grid-cols-7 place-items-center text-gray-600">
42
+ <div class="flex items-center justify-center gap-1 p-1">
43
+ <TextInput
44
+ class="text-sm"
45
+ type="text"
46
+ :value="dateValue"
47
+ @change="
48
+ selectDate(getDate($event.target.value)) || togglePopover()
49
+ "
50
+ />
51
+ <Button
52
+ :label="'Today'"
53
+ class="text-sm"
54
+ @click="selectDate(getDate()) || togglePopover()"
55
+ />
56
+ </div>
57
+ <div
58
+ class="flex flex-col items-center justify-center p-1 text-gray-800"
59
+ >
60
+ <div class="flex items-center text-xs uppercase">
45
61
  <div
46
- class="grid h-6 w-6 place-items-center gap-1 text-center"
47
- v-for="(d, i) in ['S', 'M', 'T', 'W', 'T', 'F', 'S']"
62
+ class="flex h-6 w-8 items-center justify-center text-center"
63
+ v-for="(d, i) in ['su', 'mo', 'tu', 'we', 'th', 'fr', 'sa']"
48
64
  :key="i"
49
65
  >
50
66
  {{ d }}
51
67
  </div>
52
68
  </div>
53
- <div v-for="(week, i) in datesAsWeeks" :key="i" class="mt-1">
54
- <div class="grid w-full grid-cols-7 place-items-center gap-1">
55
- <div
56
- v-for="date in week"
57
- :key="toValue(date)"
58
- class="grid h-6 w-6 cursor-pointer place-items-center rounded-md hover:bg-blue-100 hover:text-blue-700"
59
- :class="{
60
- 'text-gray-600': date.getMonth() !== currentMonth - 1,
61
- 'text-blue-500': toValue(date) === toValue(today),
62
- 'bg-blue-100 font-semibold text-blue-500':
63
- toValue(date) === modelValue,
64
- }"
65
- @click="
66
- () => {
67
- selectDate(date)
68
- togglePopover()
69
- }
70
- "
71
- >
72
- {{ date.getDate() }}
73
- </div>
69
+ <div
70
+ class="flex items-center"
71
+ v-for="(week, i) in datesAsWeeks"
72
+ :key="i"
73
+ >
74
+ <div
75
+ v-for="date in week"
76
+ :key="toValue(date)"
77
+ class="flex h-8 w-8 cursor-pointer items-center justify-center rounded hover:bg-gray-50"
78
+ :class="{
79
+ 'text-gray-400': date.getMonth() !== currentMonth - 1,
80
+ 'font-extrabold text-gray-900':
81
+ toValue(date) === toValue(today),
82
+ 'bg-gray-800 text-white hover:bg-gray-800':
83
+ toValue(date) === dateValue,
84
+ }"
85
+ @click="
86
+ () => {
87
+ selectDate(date)
88
+ togglePopover()
89
+ }
90
+ "
91
+ >
92
+ {{ date.getDate() }}
74
93
  </div>
75
94
  </div>
76
95
  </div>
77
- <div class="mt-2 flex w-full justify-end">
78
- <div
79
- class="cursor-pointer rounded-md px-2 py-1 text-sm hover:bg-gray-100"
96
+ <div class="flex justify-end p-1">
97
+ <Button
98
+ :label="'Clear'"
99
+ class="text-sm"
80
100
  @click="
81
101
  () => {
82
102
  selectDate('')
83
103
  togglePopover()
84
104
  }
85
105
  "
86
- >
87
- Clear
88
- </div>
106
+ />
89
107
  </div>
90
108
  </div>
91
109
  </template>
@@ -94,17 +112,27 @@
94
112
 
95
113
  <script>
96
114
  import Input from './Input.vue'
97
- import FeatherIcon from './FeatherIcon.vue'
115
+ import Button from './Button.vue'
98
116
  import Popover from './Popover.vue'
99
-
117
+ import FeatherIcon from './FeatherIcon.vue'
118
+ import TextInput from './TextInput.vue'
100
119
  export default {
101
120
  name: 'DatePicker',
102
- props: ['modelValue', 'placeholder', 'readonly', 'formatValue', 'inputClass'],
103
- emits: ['update:modelValue'],
121
+ props: [
122
+ 'value',
123
+ 'modelValue',
124
+ 'placeholder',
125
+ 'formatter',
126
+ 'readonly',
127
+ 'inputClass',
128
+ ],
129
+ emits: ['update:modelValue', 'change'],
104
130
  components: {
131
+ Popover,
105
132
  Input,
133
+ Button,
106
134
  FeatherIcon,
107
- Popover,
135
+ TextInput,
108
136
  },
109
137
  data() {
110
138
  return {
@@ -159,17 +187,25 @@ export default {
159
187
  },
160
188
  formatMonth() {
161
189
  let date = this.getDate(this.currentYear, this.currentMonth - 1, 1)
162
- return date.toLocaleString('en-US', { month: 'short', year: 'numeric' })
190
+ let month = date.toLocaleString('en-US', {
191
+ month: 'long',
192
+ })
193
+ return `${month}, ${date.getFullYear()}`
194
+ },
195
+ dateValue() {
196
+ return this.value ? this.value : this.modelValue
163
197
  },
164
198
  },
165
199
  methods: {
166
200
  selectDate(date) {
201
+ this.$emit('change', this.toValue(date))
167
202
  this.$emit('update:modelValue', this.toValue(date))
168
203
  },
169
204
  selectCurrentMonthYear() {
170
- let date = this.modelValue
171
- ? this.getDate(this.modelValue)
172
- : this.getDate()
205
+ let date = this.dateValue ? this.getDate(this.dateValue) : this.getDate()
206
+ if (date === 'Invalid Date') {
207
+ date = this.getDate()
208
+ }
173
209
  this.currentYear = date.getFullYear()
174
210
  this.currentMonth = date.getMonth() + 1
175
211
  },
@@ -201,7 +237,7 @@ export default {
201
237
  date = this.getDate(
202
238
  date.getFullYear(),
203
239
  date.getMonth(),
204
- date.getDate() + incrementer
240
+ date.getDate() + incrementer,
205
241
  )
206
242
  dates.push(date)
207
243
  count--
@@ -0,0 +1,326 @@
1
+ <template>
2
+ <Popover
3
+ @open="selectCurrentMonthYear"
4
+ class="flex w-full [&>div:first-child]:w-full"
5
+ >
6
+ <template #target="{ togglePopover }">
7
+ <Input
8
+ readonly
9
+ type="text"
10
+ icon-left="calendar"
11
+ :placeholder="placeholder"
12
+ :value="dateValue && formatter ? formatDates(dateValue) : dateValue"
13
+ @focus="!readonly ? togglePopover() : null"
14
+ class="w-full"
15
+ :class="inputClass"
16
+ v-bind="$attrs"
17
+ />
18
+ </template>
19
+ <template #body="{ togglePopover }">
20
+ <div
21
+ class="mt-2 w-fit select-none divide-y rounded-lg bg-white text-base shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
22
+ >
23
+ <div class="flex items-center p-1 text-gray-500">
24
+ <Button variant="ghost" class="h-7 w-7" @click="prevMonth">
25
+ <FeatherIcon
26
+ :stroke-width="2"
27
+ name="chevron-left"
28
+ class="h-4 w-4"
29
+ />
30
+ </Button>
31
+ <div class="flex-1 text-center text-base font-medium text-gray-700">
32
+ {{ formatMonth }}
33
+ </div>
34
+ <Button variant="ghost" class="h-7 w-7" @click="nextMonth">
35
+ <FeatherIcon
36
+ :stroke-width="2"
37
+ name="chevron-right"
38
+ class="h-4 w-4"
39
+ />
40
+ </Button>
41
+ </div>
42
+ <div class="flex items-center justify-center gap-1 p-1">
43
+ <TextInput class="w-28 text-sm" type="text" v-model="fromDate" />
44
+ <TextInput class="w-28 text-sm" type="text" v-model="toDate" />
45
+ </div>
46
+ <div
47
+ class="flex flex-col items-center justify-center p-1 text-gray-800"
48
+ >
49
+ <div class="flex items-center text-xs uppercase">
50
+ <div
51
+ class="flex h-6 w-8 items-center justify-center text-center"
52
+ v-for="(d, i) in ['su', 'mo', 'tu', 'we', 'th', 'fr', 'sa']"
53
+ :key="i"
54
+ >
55
+ {{ d }}
56
+ </div>
57
+ </div>
58
+ <div
59
+ class="flex items-center"
60
+ v-for="(week, i) in datesAsWeeks"
61
+ :key="i"
62
+ >
63
+ <div
64
+ v-for="date in week"
65
+ :key="toValue(date)"
66
+ class="flex h-8 w-8 cursor-pointer items-center justify-center rounded hover:bg-gray-50"
67
+ :class="{
68
+ 'text-gray-400': date.getMonth() !== currentMonth - 1,
69
+ 'text-gray-900': date.getMonth() === currentMonth - 1,
70
+ 'font-extrabold text-gray-900':
71
+ toValue(date) === toValue(today),
72
+ 'rounded-none bg-gray-100': isInRange(date),
73
+ 'rounded-l-md rounded-r-none bg-gray-800 text-white hover:bg-gray-800':
74
+ fromDate && toValue(date) === toValue(fromDate),
75
+ 'rounded-r-md bg-gray-800 text-white hover:bg-gray-800':
76
+ toDate && toValue(date) === toValue(toDate),
77
+ }"
78
+ @click="() => handleDateClick(date)"
79
+ >
80
+ {{ date.getDate() }}
81
+ </div>
82
+ </div>
83
+ </div>
84
+ <div class="flex justify-end space-x-1 p-1">
85
+ <Button
86
+ :label="'Clear'"
87
+ @click="() => clearDates() | togglePopover()"
88
+ :disabled="!fromDate || !toDate"
89
+ />
90
+ <Button
91
+ variant="solid"
92
+ :label="'Apply'"
93
+ :disabled="!fromDate || !toDate"
94
+ @click="() => selectDates() | togglePopover()"
95
+ />
96
+ </div>
97
+ </div>
98
+ </template>
99
+ </Popover>
100
+ </template>
101
+
102
+ <script>
103
+ import Input from './Input.vue'
104
+ import Button from './Button.vue'
105
+ import Popover from './Popover.vue'
106
+ import FeatherIcon from './FeatherIcon.vue'
107
+ import TextInput from './TextInput.vue'
108
+ export default {
109
+ name: 'DateRangePicker',
110
+ props: ['modelValue', 'placeholder', 'formatter', 'readonly', 'inputClass'],
111
+ emits: ['update:modelValue', 'change'],
112
+ components: {
113
+ Popover,
114
+ Input,
115
+ Button,
116
+ FeatherIcon,
117
+ TextInput,
118
+ },
119
+ data() {
120
+ const fromDate = this.dateValue ? this.dateValue[0] : ''
121
+ const toDate = this.dateValue ? this.dateValue[1] : ''
122
+ return {
123
+ currentYear: null,
124
+ currentMonth: null,
125
+ fromDate,
126
+ toDate,
127
+ }
128
+ },
129
+ created() {
130
+ this.selectCurrentMonthYear()
131
+ },
132
+ computed: {
133
+ today() {
134
+ return this.getDate()
135
+ },
136
+ datesAsWeeks() {
137
+ let datesAsWeeks = []
138
+ let dates = this.dates.slice()
139
+ while (dates.length) {
140
+ let week = dates.splice(0, 7)
141
+ datesAsWeeks.push(week)
142
+ }
143
+ return datesAsWeeks
144
+ },
145
+ dates() {
146
+ if (!(this.currentYear && this.currentMonth)) {
147
+ return []
148
+ }
149
+ let monthIndex = this.currentMonth - 1
150
+ let year = this.currentYear
151
+
152
+ let firstDayOfMonth = this.getDate(year, monthIndex, 1)
153
+ let lastDayOfMonth = this.getDate(year, monthIndex + 1, 0)
154
+ let leftPaddingCount = firstDayOfMonth.getDay()
155
+ let rightPaddingCount = 6 - lastDayOfMonth.getDay()
156
+
157
+ let leftPadding = this.getDatesAfter(firstDayOfMonth, -leftPaddingCount)
158
+ let rightPadding = this.getDatesAfter(lastDayOfMonth, rightPaddingCount)
159
+ let daysInMonth = this.getDaysInMonth(monthIndex, year)
160
+ let datesInMonth = this.getDatesAfter(firstDayOfMonth, daysInMonth - 1)
161
+
162
+ let dates = [
163
+ ...leftPadding,
164
+ firstDayOfMonth,
165
+ ...datesInMonth,
166
+ ...rightPadding,
167
+ ]
168
+ if (dates.length < 42) {
169
+ const finalPadding = this.getDatesAfter(dates.at(-1), 42 - dates.length)
170
+ dates = dates.concat(...finalPadding)
171
+ }
172
+ return dates
173
+ },
174
+ formatMonth() {
175
+ let date = this.getDate(this.currentYear, this.currentMonth - 1, 1)
176
+ let month = date.toLocaleString('en-US', {
177
+ month: 'long',
178
+ })
179
+ return `${month}, ${date.getFullYear()}`
180
+ },
181
+ dateValue() {
182
+ return this.value ? this.value : this.modelValue
183
+ },
184
+ },
185
+ methods: {
186
+ handleDateClick(date) {
187
+ if (this.fromDate && this.toDate) {
188
+ this.fromDate = this.toValue(date)
189
+ this.toDate = ''
190
+ } else if (this.fromDate && !this.toDate) {
191
+ this.toDate = this.toValue(date)
192
+ } else {
193
+ this.fromDate = this.toValue(date)
194
+ }
195
+ this.swapDatesIfNecessary()
196
+ },
197
+ selectDates() {
198
+ let val = `${this.fromDate},${this.toDate}`
199
+ if (!this.fromDate && !this.toDate) {
200
+ val = ''
201
+ }
202
+ this.$emit('change', val)
203
+ this.$emit('update:modelValue', val)
204
+ },
205
+ swapDatesIfNecessary() {
206
+ if (!this.fromDate || !this.toDate) {
207
+ return
208
+ }
209
+ // if fromDate is greater than toDate, swap them
210
+ let fromDate = this.getDate(this.fromDate)
211
+ let toDate = this.getDate(this.toDate)
212
+ if (fromDate > toDate) {
213
+ let temp = fromDate
214
+ fromDate = toDate
215
+ toDate = temp
216
+ }
217
+ this.fromDate = this.toValue(fromDate)
218
+ this.toDate = this.toValue(toDate)
219
+ },
220
+ selectCurrentMonthYear() {
221
+ let date = this.toDate ? this.getDate(this.toDate) : this.today
222
+ this.currentYear = date.getFullYear()
223
+ this.currentMonth = date.getMonth() + 1
224
+ },
225
+ prevMonth() {
226
+ this.changeMonth(-1)
227
+ },
228
+ nextMonth() {
229
+ this.changeMonth(1)
230
+ },
231
+ changeMonth(adder) {
232
+ this.currentMonth = this.currentMonth + adder
233
+ if (this.currentMonth < 1) {
234
+ this.currentMonth = 12
235
+ this.currentYear = this.currentYear - 1
236
+ }
237
+ if (this.currentMonth > 12) {
238
+ this.currentMonth = 1
239
+ this.currentYear = this.currentYear + 1
240
+ }
241
+ },
242
+ getDatesAfter(date, count) {
243
+ let incrementer = 1
244
+ if (count < 0) {
245
+ incrementer = -1
246
+ count = Math.abs(count)
247
+ }
248
+ let dates = []
249
+ while (count) {
250
+ date = this.getDate(
251
+ date.getFullYear(),
252
+ date.getMonth(),
253
+ date.getDate() + incrementer,
254
+ )
255
+ dates.push(date)
256
+ count--
257
+ }
258
+ if (incrementer === -1) {
259
+ return dates.reverse()
260
+ }
261
+ return dates
262
+ },
263
+
264
+ getDaysInMonth(monthIndex, year) {
265
+ let daysInMonthMap = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
266
+ let daysInMonth = daysInMonthMap[monthIndex]
267
+ if (monthIndex === 1 && this.isLeapYear(year)) {
268
+ return 29
269
+ }
270
+ return daysInMonth
271
+ },
272
+
273
+ isLeapYear(year) {
274
+ if (year % 400 === 0) return true
275
+ if (year % 100 === 0) return false
276
+ if (year % 4 === 0) return true
277
+ return false
278
+ },
279
+
280
+ toValue(date) {
281
+ if (!date) {
282
+ return ''
283
+ }
284
+ if (typeof date === 'string') {
285
+ return date
286
+ }
287
+
288
+ // toISOString is buggy and reduces the day by one
289
+ // this is because it considers the UTC timestamp
290
+ // in order to circumvent that we need to use luxon/moment
291
+ // but that refactor could take some time, so fixing the time difference
292
+ // as suggested in this answer.
293
+ // https://stackoverflow.com/a/16084846/3541205
294
+ date.setHours(0, -date.getTimezoneOffset(), 0, 0)
295
+ return date.toISOString().slice(0, 10)
296
+ },
297
+
298
+ getDate(...args) {
299
+ let d = new Date(...args)
300
+ return d
301
+ },
302
+
303
+ isInRange(date) {
304
+ if (!this.fromDate || !this.toDate) {
305
+ return false
306
+ }
307
+ return (
308
+ date >= this.getDate(this.fromDate) && date <= this.getDate(this.toDate)
309
+ )
310
+ },
311
+
312
+ formatDates(value) {
313
+ if (!value) {
314
+ return ''
315
+ }
316
+ const values = value.split(',')
317
+ return this.formatter(values[0]) + ' to ' + this.formatter(values[1])
318
+ },
319
+ clearDates() {
320
+ this.fromDate = ''
321
+ this.toDate = ''
322
+ this.selectDates()
323
+ },
324
+ },
325
+ }
326
+ </script>