frappe-ui 0.1.178 → 0.1.179

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.178",
3
+ "version": "0.1.179",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.ts",
6
6
  "type": "module",
@@ -3,46 +3,52 @@
3
3
  class="flex max-h-[140px] items-center gap-2 overflow-hidden bg-surface-white text-ink-gray-8 px-6 pt-5"
4
4
  :class="config.delta ? 'pb-6' : 'pb-3'"
5
5
  >
6
- <div class="flex w-full flex-col">
7
- <span class="truncate text-sm font-medium">
8
- {{ config.title }}
9
- </span>
10
- <div
11
- class="flex-1 flex-shrink-0 truncate text-[24px] font-semibold leading-10"
12
- >
13
- {{ config.prefix }}{{ formatValue(config.value, 1, true)
14
- }}{{ config.suffix }}
6
+ <slot name="body">
7
+ <div class="flex w-full flex-col">
8
+ <slot name="title">
9
+ <span class="truncate text-sm font-medium text-ink-gray-5">
10
+ {{ config.title }}
11
+ </span>
12
+ </slot>
13
+ <slot name="subtitle" v-bind="{ formatValue }">
14
+ <div
15
+ class="flex-1 flex-shrink-0 truncate text-[24px] text-ink-gray-6 font-semibold leading-10"
16
+ >
17
+ {{ config.prefix }}{{ formatValue(config.value, 1, true)
18
+ }}{{ config.suffix }}
19
+ </div>
20
+ </slot>
21
+ <slot name="delta" v-bind="{ formatValue }">
22
+ <div
23
+ v-if="config.delta"
24
+ class="flex items-center gap-0.5 text-xs font-medium"
25
+ :class="[
26
+ config.negativeIsBetter
27
+ ? config.delta >= 0
28
+ ? 'text-ink-red-4'
29
+ : 'text-ink-green-3'
30
+ : config.delta >= 0
31
+ ? 'text-ink-green-3'
32
+ : 'text-ink-red-4',
33
+ ]"
34
+ >
35
+ <span class="">
36
+ {{ config.delta >= 0 ? '↑' : '↓' }}
37
+ </span>
38
+ <span>
39
+ {{ config.deltaPrefix }}{{ formatValue(config.delta, 1, true)
40
+ }}{{ config.deltaSuffix }}
41
+ </span>
42
+ </div>
43
+ </slot>
15
44
  </div>
16
- <div
17
- v-if="config.delta"
18
- class="flex items-center gap-0.5 text-xs font-medium"
19
- :class="[
20
- config.negativeIsBetter
21
- ? config.delta >= 0
22
- ? 'text-ink-red-4'
23
- : 'text-ink-green-3'
24
- : config.delta >= 0
25
- ? 'text-ink-green-3'
26
- : 'text-ink-red-4',
27
- ]"
28
- >
29
- <span class="">
30
- {{ config.delta >= 0 ? '↑' : '↓' }}
31
- </span>
32
- <span>
33
- {{ config.deltaPrefix }}{{ formatValue(config.delta, 1, true)
34
- }}{{ config.deltaSuffix }}
35
- </span>
36
- </div>
37
- </div>
45
+ </slot>
38
46
  </div>
39
47
  </template>
40
48
 
41
49
  <script setup lang="ts">
42
- import { ref } from 'vue'
43
50
  import { formatValue } from './helpers'
44
51
  import { NumberChartConfig } from './types'
45
52
 
46
- const props = defineProps<{ config: NumberChartConfig }>()
47
- const error = ref('')
53
+ defineProps<{ config: NumberChartConfig }>()
48
54
  </script>
@@ -2,25 +2,78 @@
2
2
  import { ref } from 'vue'
3
3
  import Dialog from './Dialog.vue'
4
4
  import { Button } from '../Button'
5
+ import { Dropdown } from '../Dropdown'
6
+ import LucideSettings from '~icons/lucide/settings'
7
+ import LucideStar from '~icons/lucide/star'
8
+ import LucideChevronDown from '~icons/lucide/chevron-down'
5
9
 
6
10
  const dialog1 = ref(false)
7
11
  const dialog2 = ref(false)
12
+ const dialog3 = ref(false)
13
+ const dialog4 = ref(false)
14
+ const dialog5 = ref(false)
15
+ const dialog6 = ref(false)
8
16
 
9
- const createPromise = () => {
17
+ // Dropdown state
18
+ const selectedOption = ref('Option 1')
19
+
20
+ const dropdownOptions = [
21
+ {
22
+ label: 'Option 1',
23
+ onClick: () => {
24
+ selectedOption.value = 'Option 1'
25
+ },
26
+ },
27
+ {
28
+ label: 'Option 2',
29
+ onClick: () => {
30
+ selectedOption.value = 'Option 2'
31
+ },
32
+ },
33
+ {
34
+ label: 'Option 3',
35
+ onClick: () => {
36
+ selectedOption.value = 'Option 3'
37
+ },
38
+ },
39
+ {
40
+ group: 'Advanced Options',
41
+ items: [
42
+ {
43
+ label: 'Advanced Option A',
44
+ icon: LucideSettings,
45
+ onClick: () => {
46
+ selectedOption.value = 'Advanced Option A'
47
+ },
48
+ },
49
+ {
50
+ label: 'Advanced Option B',
51
+ icon: LucideStar,
52
+ onClick: () => {
53
+ selectedOption.value = 'Advanced Option B'
54
+ },
55
+ },
56
+ ],
57
+ },
58
+ ]
59
+
60
+ const createPromise = (): Promise<void> => {
10
61
  return new Promise((resolve) => {
11
62
  setTimeout(resolve, 2000)
12
63
  })
13
64
  }
14
65
  </script>
66
+
15
67
  <template>
16
68
  <Story :layout="{ width: 500, type: 'grid' }">
17
- <Variant title="With options" autoPropsDisabled>
18
- <Button @click="dialog1 = true">Show Dialog</Button>
69
+ <!-- 1. Basic Dialog with Actions -->
70
+ <Variant title="Basic Dialog with Actions" autoPropsDisabled>
71
+ <Button @click="dialog1 = true">Show Confirmation Dialog</Button>
19
72
  <Dialog
20
73
  :options="{
21
- title: 'Confirm',
22
- message: 'Are you sure you want to confirm this action?',
23
- size: 'xl',
74
+ title: 'Confirm Action',
75
+ message: 'Are you sure you want to proceed with this action?',
76
+ size: 'lg',
24
77
  icon: {
25
78
  name: 'alert-triangle',
26
79
  appearance: 'warning',
@@ -29,27 +82,133 @@ const createPromise = () => {
29
82
  {
30
83
  label: 'Confirm',
31
84
  variant: 'solid',
32
- onClick: () => {
33
- return createPromise()
34
- },
85
+ onClick: () => createPromise(),
35
86
  },
36
87
  ],
37
88
  }"
38
89
  v-model="dialog1"
39
90
  />
40
91
  </Variant>
41
- <Variant title="With slots" autoPropsDisabled>
42
- <Button @click="dialog2 = true">Show Dialog</Button>
92
+
93
+ <!-- 2. Custom Content with Slots -->
94
+ <Variant title="Custom Content with Slots" autoPropsDisabled>
95
+ <Button @click="dialog2 = true">Show Custom Dialog</Button>
43
96
  <Dialog v-model="dialog2">
44
97
  <template #body-title>
45
- <h3>Custom Title</h3>
98
+ <h3 class="text-2xl font-semibold text-blue-600">
99
+ Custom Title with Styling
100
+ </h3>
46
101
  </template>
47
102
  <template #body-content>
48
- <p>Custom Body</p>
103
+ <div class="space-y-4">
104
+ <p class="text-gray-700">
105
+ This dialog uses custom slots for flexible content layout.
106
+ </p>
107
+ <div class="bg-blue-50 p-4 rounded-lg">
108
+ <p class="text-blue-800">
109
+ You can put any content here including forms, lists, or other
110
+ components.
111
+ </p>
112
+ </div>
113
+ </div>
114
+ </template>
115
+ <template #actions="{ close }">
116
+ <div class="flex justify-start flex-row-reverse gap-2">
117
+ <Button variant="solid" @click="close">Save Changes</Button>
118
+ <Button variant="outline" @click="close">Cancel</Button>
119
+ </div>
120
+ </template>
121
+ </Dialog>
122
+ </Variant>
123
+
124
+ <!-- 3. Different Sizes -->
125
+ <Variant title="Different Sizes" autoPropsDisabled>
126
+ <div class="space-x-2">
127
+ <Button @click="dialog3 = true">Small Dialog</Button>
128
+ <Button @click="dialog4 = true">Large Dialog</Button>
129
+ </div>
130
+
131
+ <!-- Small Dialog -->
132
+ <Dialog
133
+ :options="{
134
+ title: 'Small Dialog',
135
+ message: 'This is a small dialog.',
136
+ size: 'sm',
137
+ actions: [{ label: 'OK', variant: 'solid' }],
138
+ }"
139
+ v-model="dialog3"
140
+ />
141
+
142
+ <!-- Large Dialog -->
143
+ <Dialog
144
+ :options="{
145
+ title: 'Large Dialog',
146
+ message: 'This is a large dialog with more space for content.',
147
+ size: '4xl',
148
+ actions: [{ label: 'OK', variant: 'solid' }],
149
+ }"
150
+ v-model="dialog4"
151
+ />
152
+ </Variant>
153
+
154
+ <!-- 4. Disable Outside Click -->
155
+ <Variant title="Disable Outside Click to Close" autoPropsDisabled>
156
+ <Button @click="dialog5 = true">Show Modal Dialog</Button>
157
+ <Dialog
158
+ :options="{
159
+ title: 'Modal Dialog',
160
+ message:
161
+ 'This dialog cannot be closed by clicking outside. Use the buttons or ESC key.',
162
+ actions: [{ label: 'Close', variant: 'solid' }],
163
+ }"
164
+ :disable-outside-click-to-close="true"
165
+ v-model="dialog5"
166
+ />
167
+ </Variant>
168
+
169
+ <!-- 5. Dialog with Interactive Components -->
170
+ <Variant title="Dialog with Interactive Components" autoPropsDisabled>
171
+ <Button @click="dialog6 = true">Show Settings Dialog</Button>
172
+ <Dialog v-model="dialog6">
173
+ <template #body-title>
174
+ <h3 class="text-2xl font-semibold text-ink-gray-9">
175
+ Settings Dialog
176
+ </h3>
177
+ </template>
178
+ <template #body-content>
179
+ <div class="space-y-6 text-base">
180
+ <p class="text-gray-700">
181
+ This dialog contains interactive elements to test proper layering.
182
+ </p>
183
+
184
+ <div class="space-y-3">
185
+ <label class="block text-sm font-medium text-gray-700">
186
+ Select an option:
187
+ </label>
188
+ <Dropdown :options="dropdownOptions" placement="left">
189
+ <Button variant="outline">
190
+ {{ selectedOption }}
191
+
192
+ <template #suffix>
193
+ <LucideChevronDown class="h-4 w-4 text-gray-500" />
194
+ </template>
195
+ </Button>
196
+ </Dropdown>
197
+ </div>
198
+
199
+ <div class="bg-gray-50 text-p-sm p-4 text-ink-gray-6 rounded-lg">
200
+ <p><strong>Selected value:</strong> {{ selectedOption }}</p>
201
+ <p class="mt-1">
202
+ Interactive components should work properly within dialogs.
203
+ </p>
204
+ </div>
205
+ </div>
49
206
  </template>
50
- <template #actions>
51
- <Button variant="solid">Confirm</Button>
52
- <Button class="ml-2" @click="dialog2 = false">Close</Button>
207
+ <template #actions="{ close }">
208
+ <div class="flex space-x-2">
209
+ <Button variant="solid" @click="close">Save Settings</Button>
210
+ <Button variant="outline" @click="close">Cancel</Button>
211
+ </div>
53
212
  </template>
54
213
  </Dialog>
55
214
  </Variant>
@@ -1,44 +1,17 @@
1
1
  <template>
2
- <TransitionRoot
3
- as="template"
4
- :show="isOpen"
5
- @after-leave="$emit('after-leave')"
6
- >
7
- <HDialog
8
- as="div"
9
- class="fixed inset-0 z-10 overflow-y-auto"
10
- @close="!disableOutsideClickToClose && close()"
11
- >
12
- <div
13
- class="flex min-h-screen flex-col items-center px-4 py-4 text-center"
14
- :class="dialogPositionClasses"
2
+ <DialogRoot v-model:open="isOpen" @update:open="handleOpenChange">
3
+ <DialogPortal>
4
+ <DialogOverlay
5
+ class="fixed inset-0 bg-black-overlay-200 backdrop-filter backdrop-blur-[12px] overflow-y-auto dialog-overlay"
6
+ :data-dialog="options.title"
7
+ @after-leave="$emit('after-leave')"
15
8
  >
16
- <TransitionChild
17
- as="template"
18
- enter="ease-out duration-150"
19
- enter-from="opacity-0"
20
- enter-to="opacity-100"
21
- leave="ease-in duration-150"
22
- leave-from="opacity-100"
23
- leave-to="opacity-0"
9
+ <div
10
+ class="flex min-h-screen flex-col items-center px-4 py-4 text-center"
11
+ :class="dialogPositionClasses"
24
12
  >
25
- <div
26
- class="fixed inset-0 bg-black-overlay-200 transition-opacity dark:backdrop-filter dark:backdrop-blur-[1px]"
27
- :data-dialog="options.title"
28
- />
29
- </TransitionChild>
30
-
31
- <TransitionChild
32
- as="template"
33
- enter="ease-out duration-150"
34
- enter-from="opacity-50 translate-y-2 scale-95"
35
- enter-to="opacity-100 translate-y-0 scale-100"
36
- leave="ease-in duration-150"
37
- leave-from="opacity-100 translate-y-0 scale-100"
38
- leave-to="opacity-50 translate-y-4 translate-y-4 scale-95"
39
- >
40
- <DialogPanel
41
- class="my-8 inline-block w-full transform overflow-hidden rounded-xl bg-surface-modal text-left align-middle shadow-xl transition-all"
13
+ <DialogContent
14
+ class="my-8 inline-block w-full transform overflow-hidden rounded-xl bg-surface-modal text-left align-middle shadow-xl dialog-content"
42
15
  :class="{
43
16
  'max-w-7xl': options.size === '7xl',
44
17
  'max-w-6xl': options.size === '6xl',
@@ -52,6 +25,14 @@
52
25
  'max-w-sm': options.size === 'sm',
53
26
  'max-w-xs': options.size === 'xs',
54
27
  }"
28
+ @escape-key-down="close()"
29
+ @interact-outside="
30
+ (e: Event) => {
31
+ if (props.disableOutsideClickToClose) {
32
+ e.preventDefault()
33
+ }
34
+ }
35
+ "
55
36
  >
56
37
  <slot name="body">
57
38
  <slot name="body-main">
@@ -83,35 +64,22 @@
83
64
  </slot>
84
65
  </DialogTitle>
85
66
  </div>
86
- <Button variant="ghost" @click="close">
87
- <template #icon>
88
- <svg
89
- width="16"
90
- height="16"
91
- viewBox="0 0 16 16"
92
- fill="none"
93
- xmlns="http://www.w3.org/2000/svg"
94
- class="text-ink-gray-9"
95
- >
96
- <path
97
- fill-rule="evenodd"
98
- clip-rule="evenodd"
99
- d="M12.8567 3.85355C13.052 3.65829 13.052 3.34171 12.8567 3.14645C12.6615 2.95118 12.3449 2.95118 12.1496 3.14645L8.00201 7.29405L3.85441 3.14645C3.65914 2.95118 3.34256 2.95118 3.1473 3.14645C2.95204 3.34171 2.95204 3.65829 3.1473 3.85355L7.29491 8.00116L3.14645 12.1496C2.95118 12.3449 2.95118 12.6615 3.14645 12.8567C3.34171 13.052 3.65829 13.052 3.85355 12.8567L8.00201 8.70827L12.1505 12.8567C12.3457 13.052 12.6623 13.052 12.8576 12.8567C13.0528 12.6615 13.0528 12.3449 12.8576 12.1496L8.70912 8.00116L12.8567 3.85355Z"
100
- fill="currentColor"
101
- />
102
- </svg>
103
- </template>
104
- </Button>
67
+ <DialogClose as-child>
68
+ <Button variant="ghost" @click="close">
69
+ <template #icon>
70
+ <LucideX class="h-4 w-4 text-ink-gray-9" />
71
+ </template>
72
+ </Button>
73
+ </DialogClose>
105
74
  </div>
106
75
  </slot>
107
76
 
108
77
  <slot name="body-content">
109
- <p
110
- class="text-p-base text-ink-gray-7"
111
- v-if="options.message"
112
- >
113
- {{ options.message }}
114
- </p>
78
+ <DialogDescription as-child v-if="options.message">
79
+ <p class="text-p-base text-ink-gray-7">
80
+ {{ options.message }}
81
+ </p>
82
+ </DialogDescription>
115
83
  </slot>
116
84
  </div>
117
85
  </div>
@@ -136,25 +104,37 @@
136
104
  </slot>
137
105
  </div>
138
106
  </slot>
139
- </DialogPanel>
140
- </TransitionChild>
141
- </div>
142
- </HDialog>
143
- </TransitionRoot>
107
+ </DialogContent>
108
+ </div>
109
+ </DialogOverlay>
110
+ </DialogPortal>
111
+ </DialogRoot>
144
112
  </template>
145
113
 
146
114
  <script setup lang="ts">
147
115
  import {
148
- DialogPanel,
116
+ DialogRoot,
117
+ DialogPortal,
118
+ DialogOverlay,
119
+ DialogContent,
149
120
  DialogTitle,
150
- Dialog as HDialog,
151
- TransitionChild,
152
- TransitionRoot,
153
- } from '@headlessui/vue'
121
+ DialogDescription,
122
+ DialogClose,
123
+ } from 'reka-ui'
154
124
  import { computed, reactive } from 'vue'
155
125
  import { Button } from '../Button'
156
126
  import FeatherIcon from '../FeatherIcon.vue'
157
- import type { DialogProps, DialogIcon } from './types'
127
+ import type {
128
+ DialogProps,
129
+ DialogIcon,
130
+ DialogAction,
131
+ DialogActionContext,
132
+ } from './types'
133
+
134
+ // Type for dialog action with reactive loading state
135
+ type ReactiveDialogAction = DialogAction & {
136
+ loading: boolean
137
+ }
158
138
 
159
139
  const props = withDefaults(defineProps<DialogProps>(), {
160
140
  options: () => ({}),
@@ -167,7 +147,7 @@ const emit = defineEmits<{
167
147
  (event: 'after-leave'): void
168
148
  }>()
169
149
 
170
- const actions = computed(() => {
150
+ const actions = computed((): ReactiveDialogAction[] => {
171
151
  let actions = props.options.actions
172
152
  if (!actions?.length) return []
173
153
 
@@ -183,12 +163,15 @@ const actions = computed(() => {
183
163
  if (action.onClick) {
184
164
  // deprecated: uncomment this when we remove the backwards compatibility
185
165
  // let context: DialogActionContext = { close }
186
- let backwardsCompatibleContext = function () {
166
+ type BackwardsCompatibleDialogActionContext = (() => void) &
167
+ DialogActionContext
168
+
169
+ let backwardsCompatibleContext = (() => {
187
170
  console.warn(
188
- 'Value passed to onClick is a context object. Please use context.close() instead of context() to close the dialog.'
171
+ 'Value passed to onClick is a context object. Please use context.close() instead of context() to close the dialog.',
189
172
  )
190
173
  close()
191
- }
174
+ }) as BackwardsCompatibleDialogActionContext
192
175
  backwardsCompatibleContext.close = close
193
176
  await action.onClick(backwardsCompatibleContext)
194
177
  }
@@ -205,7 +188,7 @@ const isOpen = computed({
205
188
  get() {
206
189
  return props.modelValue
207
190
  },
208
- set(val) {
191
+ set(val: boolean) {
209
192
  emit('update:modelValue', val)
210
193
  if (!val) {
211
194
  emit('close')
@@ -213,6 +196,10 @@ const isOpen = computed({
213
196
  },
214
197
  })
215
198
 
199
+ function handleOpenChange(open: boolean) {
200
+ isOpen.value = open
201
+ }
202
+
216
203
  function close() {
217
204
  isOpen.value = false
218
205
  }
@@ -229,31 +216,92 @@ const icon = computed(() => {
229
216
 
230
217
  const dialogPositionClasses = computed(() => {
231
218
  const position = props.options?.position || 'center'
232
- return {
219
+ const classMap: Record<string, string> = {
233
220
  center: 'justify-center',
234
221
  top: 'pt-[20vh]',
235
- }[position]
222
+ }
223
+ return classMap[position]
236
224
  })
237
225
 
238
226
  const dialogIconBgClasses = computed(() => {
239
227
  const appearance = icon.value?.appearance
240
228
  if (!appearance) return 'bg-surface-gray-2'
241
- return {
229
+ const classMap: Record<string, string> = {
242
230
  warning: 'bg-surface-amber-2',
243
231
  info: 'bg-surface-blue-2',
244
232
  danger: 'bg-surface-red-2',
245
233
  success: 'bg-surface-green-2',
246
- }[appearance]
234
+ }
235
+ return classMap[appearance]
247
236
  })
248
237
 
249
238
  const dialogIconClasses = computed(() => {
250
239
  const appearance = icon.value?.appearance
251
240
  if (!appearance) return 'text-ink-gray-5'
252
- return {
241
+ const classMap: Record<string, string> = {
253
242
  warning: 'text-ink-amber-3',
254
243
  info: 'text-ink-blue-3',
255
244
  danger: 'text-ink-red-4',
256
245
  success: 'text-ink-green-3',
257
- }[appearance]
246
+ }
247
+ return classMap[appearance]
258
248
  })
259
249
  </script>
250
+
251
+ <style scoped>
252
+ @keyframes dialog-overlay-in {
253
+ from {
254
+ opacity: 0;
255
+ }
256
+ to {
257
+ opacity: 1;
258
+ }
259
+ }
260
+
261
+ @keyframes dialog-overlay-out {
262
+ from {
263
+ opacity: 1;
264
+ }
265
+ to {
266
+ opacity: 0;
267
+ }
268
+ }
269
+
270
+ @keyframes dialog-content-in {
271
+ from {
272
+ opacity: 0.5;
273
+ transform: scale(0.98);
274
+ }
275
+ to {
276
+ opacity: 1;
277
+ transform: scale(1);
278
+ }
279
+ }
280
+
281
+ @keyframes dialog-content-out {
282
+ from {
283
+ opacity: 1;
284
+ transform: scale(1);
285
+ }
286
+ to {
287
+ opacity: 0.5;
288
+ transform: scale(0.98);
289
+ }
290
+ }
291
+
292
+ :global(.dialog-overlay[data-state='open']) {
293
+ animation: dialog-overlay-in 100ms ease-out;
294
+ }
295
+
296
+ :global(.dialog-overlay[data-state='closed']) {
297
+ animation: dialog-overlay-out 150ms ease-in;
298
+ }
299
+
300
+ :global(.dialog-content[data-state='open']) {
301
+ animation: dialog-content-in 100ms ease-out;
302
+ }
303
+
304
+ :global(.dialog-content[data-state='closed']) {
305
+ animation: dialog-content-out 150ms ease-in;
306
+ }
307
+ </style>
@@ -1,2 +1,14 @@
1
- export { default as Dialog } from './Dialog.vue'
2
- export type { DialogProps } from './types'
1
+ import DialogMain from './Dialog.vue'
2
+ import { DialogTitle, DialogDescription } from 'reka-ui'
3
+ export type { DialogProps } from './types'
4
+
5
+ type DialogExport = typeof DialogMain & {
6
+ Title: typeof DialogTitle
7
+ Description: typeof DialogDescription
8
+ }
9
+
10
+ const Dialog = DialogMain as DialogExport
11
+ Dialog.Title = DialogTitle
12
+ Dialog.Description = DialogDescription
13
+
14
+ export { Dialog }
@@ -27,10 +27,10 @@ type DialogOptions = {
27
27
  position?: 'top' | 'center'
28
28
  }
29
29
 
30
- type DialogActionContext = {
30
+ export type DialogActionContext = {
31
31
  close: () => void
32
32
  }
33
- type DialogAction = ButtonProps & {
33
+ export type DialogAction = ButtonProps & {
34
34
  onClick?: (context: DialogActionContext) => void | Promise<void>
35
35
  }
36
36
 
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <div
3
+ class="inline-flex items-center gap-0.5 text-sm"
4
+ :class="{
5
+ 'bg-surface-gray-2 rounded-sm text-ink-gray-5 py-0.5 px-1': bg,
6
+ 'text-ink-gray-4': !bg,
7
+ }"
8
+ >
9
+ <span v-if="ctrl || meta">
10
+ <LucideCommand v-if="isMac" class="w-3 h-3" />
11
+ <span v-else>Ctrl</span>
12
+ </span>
13
+ <span v-if="shift"><LucideShift class="w-3 h-3" /></span>
14
+ <span v-if="alt"><LucideAlt class="w-3 h-3" /></span>
15
+ <slot></slot>
16
+ </div>
17
+ </template>
18
+ <script setup lang="ts">
19
+ import LucideCommand from '~icons/lucide/command'
20
+ import LucideShift from '~icons/lucide/arrow-big-up'
21
+ import LucideAlt from '~icons/lucide/option'
22
+
23
+ const isMac = navigator.userAgent.includes('Mac')
24
+
25
+ defineProps({
26
+ meta: Boolean,
27
+ ctrl: Boolean,
28
+ shift: Boolean,
29
+ alt: Boolean,
30
+ shortcut: String,
31
+ bg: Boolean,
32
+ })
33
+ </script>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import { Password } from './index'
3
+ import { ref } from 'vue'
4
+
5
+ const value = ref('')
6
+ </script>
7
+
8
+ <template>
9
+ <Story title="Password" :layout="{ width: 500, type: 'grid' }">
10
+ <div class="p-2">
11
+ <Password v-model="value" />
12
+ </div>
13
+ </Story>
14
+ </template>
@@ -0,0 +1,58 @@
1
+ <template>
2
+ <FormControl
3
+ :type="show ? 'text' : 'password'"
4
+ :value="modelValue || value"
5
+ v-bind="$attrs"
6
+ @keydown.meta.i.prevent="show = !show"
7
+ @keydown.ctrl.i.prevent="show = !show"
8
+ >
9
+ <template #prefix v-if="$slots.prefix">
10
+ <slot name="prefix" />
11
+ </template>
12
+ <template #suffix>
13
+ <Tooltip>
14
+ <template #body>
15
+ <div
16
+ class="rounded bg-surface-gray-7 py-1.5 px-2 text-xs text-ink-white shadow-xl"
17
+ >
18
+ <span class="flex items-center gap-1">
19
+ {{ show ? 'Hide Password' : 'Show Password' }}
20
+ <KeyboardShortcut
21
+ bg
22
+ ctrl
23
+ class="!bg-surface-gray-5 !text-ink-gray-2 px-1"
24
+ >
25
+ <span class="font-mono leading-none tracking-widest">+I</span>
26
+ </KeyboardShortcut>
27
+ </span>
28
+ </div>
29
+ </template>
30
+ <div>
31
+ <component
32
+ v-show="showEye"
33
+ :is="show ? LucideEyeOff : LucideEye"
34
+ class="h-3 cursor-pointer mr-1"
35
+ @click="show = !show"
36
+ />
37
+ </div>
38
+ </Tooltip>
39
+ </template>
40
+ </FormControl>
41
+ </template>
42
+ <script setup lang="ts">
43
+ import LucideEye from '~icons/lucide/eye'
44
+ import LucideEyeOff from '~icons/lucide/eye-off'
45
+ import KeyboardShortcut from '../KeyboardShortcut.vue'
46
+ import FormControl from '../FormControl/FormControl.vue'
47
+ import Tooltip from '../Tooltip/Tooltip.vue'
48
+ import type { PasswordProps } from './types'
49
+ import { ref, computed } from 'vue'
50
+
51
+ const props = defineProps<PasswordProps>()
52
+
53
+ const show = ref(false)
54
+ const showEye = computed(() => {
55
+ let v = props.modelValue || props.value
56
+ return !v?.includes('*')
57
+ })
58
+ </script>
@@ -0,0 +1,2 @@
1
+ export { default as Password } from './Password.vue'
2
+ export type { PasswordProps } from './types'
@@ -0,0 +1,4 @@
1
+ export interface PasswordProps {
2
+ modelValue?: string | null
3
+ value?: string | null
4
+ }
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ export * from './components/Popover'
31
31
  export * from './components/Rating'
32
32
  export { default as Resource } from './components/Resource.vue'
33
33
  export * from './components/Select'
34
+ export * from './components/Password'
34
35
  export * from './components/Spinner'
35
36
  export * from './components/Switch'
36
37
  export * from './components/TabButtons'
@@ -67,6 +68,7 @@ export { default as CommandPaletteItem } from './components/CommandPalette/Comma
67
68
  export { default as ListFilter } from './components/ListFilter/ListFilter.vue'
68
69
  export { default as Calendar } from './components/Calendar/Calendar.vue'
69
70
  export { default as NestedPopover } from './components/ListFilter/NestedPopover.vue'
71
+ export { default as KeyboardShortcut } from './components/KeyboardShortcut.vue'
70
72
  export * from './components/CircularProgressBar'
71
73
  export * from './components/Tree'
72
74
  export { default as FrappeUIProvider } from './components/Provider/FrappeUIProvider.vue'