frappe-ui 0.1.244 → 0.1.245

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/icons/Icon.vue ADDED
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import { onMounted } from 'vue';
3
+
4
+ const props = defineProps<{ name: string }>()
5
+
6
+ onMounted(() => {
7
+ if (!document.getElementById('lucide-sprite')) {
8
+ console.warn(
9
+ 'Lucide sprite not found! Make sure to use the spritePlugin.'
10
+ )
11
+ }
12
+ })
13
+ </script>
14
+
15
+ <template>
16
+ <svg
17
+ width="24"
18
+ height="24"
19
+ viewBox="0 0 24 24"
20
+ fill="none"
21
+ stroke="currentColor"
22
+ stroke-width="1.5"
23
+ stroke-linecap="round"
24
+ stroke-linejoin="round"
25
+ >
26
+ <use :href="`#${props.name}`" />
27
+ </svg>
28
+ </template>
@@ -0,0 +1,126 @@
1
+ <script setup lang="ts">
2
+ import { ref, reactive } from 'vue'
3
+ import IconPicker from './IconPicker.vue'
4
+
5
+ const basicValue = ref(null)
6
+ const preselectedValue = ref('star')
7
+ const disabledValue = ref('')
8
+
9
+ const state = reactive({
10
+ disabled: false,
11
+ placeholder: 'Select an icon...',
12
+ openOnClick: true,
13
+ openOnFocus: true,
14
+ placement: 'start',
15
+ })
16
+ </script>
17
+
18
+ <template>
19
+ <Story title="IconPicker" :layout="{ type: 'grid', width: 400 }">
20
+ <Variant title="Basic Usage">
21
+ <div class="p-4">
22
+ <label class="block text-sm font-medium mb-2">Basic Icon Picker</label>
23
+ <IconPicker
24
+ v-model="basicValue"
25
+ :placeholder="state.placeholder"
26
+ :disabled="state.disabled"
27
+ :open-on-click="state.openOnClick"
28
+ :open-on-focus="state.openOnFocus"
29
+ :placement="state.placement"
30
+ />
31
+ <div class="mt-2 text-sm text-gray-600">
32
+ Selected: {{ basicValue || 'None' }}
33
+ </div>
34
+ </div>
35
+ </Variant>
36
+
37
+ <Variant title="Subtle Variant (Default)">
38
+ <div class="p-4">
39
+ <label class="block text-sm font-medium mb-2">Subtle Variant</label>
40
+ <IconPicker
41
+ variant="subtle"
42
+ v-model="basicValue"
43
+ :placeholder="state.placeholder"
44
+ :disabled="state.disabled"
45
+ />
46
+ <div class="mt-2 text-sm text-gray-600">
47
+ Selected: {{ basicValue || 'None' }}
48
+ </div>
49
+ </div>
50
+ </Variant>
51
+
52
+ <Variant title="Outline Variant">
53
+ <div class="p-4">
54
+ <label class="block text-sm font-medium mb-2">Outline Variant</label>
55
+ <IconPicker
56
+ variant="outline"
57
+ v-model="basicValue"
58
+ :placeholder="state.placeholder"
59
+ :disabled="state.disabled"
60
+ />
61
+ <div class="mt-2 text-sm text-gray-600">
62
+ Selected: {{ basicValue || 'None' }}
63
+ </div>
64
+ </div>
65
+ </Variant>
66
+
67
+ <Variant title="Ghost Variant">
68
+ <div class="p-4">
69
+ <label class="block text-sm font-medium mb-2">Ghost Variant</label>
70
+ <IconPicker
71
+ variant="ghost"
72
+ v-model="basicValue"
73
+ :placeholder="state.placeholder"
74
+ :disabled="state.disabled"
75
+ />
76
+ <div class="mt-2 text-sm text-gray-600">
77
+ Selected: {{ basicValue || 'None' }}
78
+ </div>
79
+ </div>
80
+ </Variant>
81
+
82
+ <Variant title="Pre-selected Icon">
83
+ <div class="p-4">
84
+ <label class="block text-sm font-medium mb-2">Pre-selected Icon</label>
85
+ <IconPicker
86
+ v-model="preselectedValue"
87
+ :placeholder="state.placeholder"
88
+ :disabled="state.disabled"
89
+ />
90
+ <div class="mt-2 text-sm text-gray-600">
91
+ Selected: {{ preselectedValue || 'None' }}
92
+ </div>
93
+ </div>
94
+ </Variant>
95
+
96
+ <Variant title="Disabled State">
97
+ <div class="p-4">
98
+ <label class="block text-sm font-medium mb-2">Disabled Icon Picker</label>
99
+ <IconPicker
100
+ v-model="disabledValue"
101
+ placeholder="This is disabled"
102
+ :disabled="true"
103
+ />
104
+ <div class="mt-2 text-sm text-gray-600">
105
+ Icon picker is disabled
106
+ </div>
107
+ </div>
108
+ </Variant>
109
+
110
+ <template #controls>
111
+ <HstText v-model="state.placeholder" title="Placeholder" />
112
+ <HstCheckbox v-model="state.disabled" title="Disabled" />
113
+ <HstCheckbox v-model="state.openOnClick" title="Open on Click" />
114
+ <HstCheckbox v-model="state.openOnFocus" title="Open on Focus" />
115
+ <HstSelect
116
+ v-model="state.placement"
117
+ title="Placement"
118
+ :options="[
119
+ { value: 'start', label: 'Start' },
120
+ { value: 'center', label: 'Center' },
121
+ { value: 'end', label: 'End' }
122
+ ]"
123
+ />
124
+ </template>
125
+ </Story>
126
+ </template>
@@ -0,0 +1,221 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ ComboboxAnchor,
4
+ ComboboxContent,
5
+ ComboboxEmpty,
6
+ ComboboxInput,
7
+ ComboboxPortal,
8
+ ComboboxRoot,
9
+ ComboboxTrigger,
10
+ ComboboxViewport,
11
+ } from 'reka-ui'
12
+ import { computed, onMounted, ref, watch } from 'vue'
13
+ import Icon from './Icon.vue'
14
+
15
+ export interface IconPickerProps {
16
+ variant?: 'subtle' | 'outline' | 'ghost'
17
+ modelValue?: string | null
18
+ placeholder?: string
19
+ disabled?: boolean
20
+ openOnFocus?: boolean
21
+ openOnClick?: boolean
22
+ placement?: 'start' | 'center' | 'end'
23
+ maxIcons?: number
24
+ }
25
+
26
+ const props = withDefaults(defineProps<IconPickerProps>(), {
27
+ variant: 'subtle',
28
+ openOnClick: true,
29
+ openOnFocus: true,
30
+ maxIcons: 100,
31
+ })
32
+
33
+ const emit = defineEmits(['update:modelValue', 'focus', 'blur', 'input'])
34
+
35
+ const searchTerm = ref(getLabel(props.modelValue || ''))
36
+ const internalModelValue = ref(props.modelValue)
37
+ const isOpen = ref(false)
38
+ const iconNames = ref<string[]>([])
39
+
40
+ watch(
41
+ () => props.modelValue,
42
+ (newValue) => {
43
+ internalModelValue.value = newValue
44
+ searchTerm.value = newValue ? getLabel(newValue) : ''
45
+ },
46
+ )
47
+
48
+ onMounted(() => {
49
+ const spriteContainer = document.getElementById('lucide-sprite')
50
+ if (!spriteContainer) {
51
+ console.warn('Lucide sprite not found! Make sure to use the spritePlugin.')
52
+ return
53
+ }
54
+
55
+ const symbols = spriteContainer.getElementsByTagName('symbol')
56
+ const names: string[] = []
57
+ for (let i = 0; i < symbols.length; i++) {
58
+ const symbol = symbols[i]
59
+ names.push(symbol.id)
60
+ }
61
+ iconNames.value = names
62
+ })
63
+
64
+ function getLabel(name: string) {
65
+ return name.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
66
+ }
67
+
68
+ const filteredIcons = computed(() => {
69
+ if (!searchTerm.value) return iconNames.value
70
+ const lowerSearch = searchTerm.value.toLowerCase()
71
+ return iconNames.value.filter((name) =>
72
+ name.replace(/-/g, ' ').toLowerCase().includes(lowerSearch),
73
+ )
74
+ })
75
+
76
+ const onUpdateModelValue = (value: string | null) => {
77
+ internalModelValue.value = value
78
+ emit('update:modelValue', value)
79
+ searchTerm.value = value ? getLabel(value) : ''
80
+ isOpen.value = false
81
+ }
82
+
83
+ const handleInputChange = (event: Event) => {
84
+ const target = event.target as HTMLInputElement
85
+ searchTerm.value = target.value
86
+
87
+ if (searchTerm.value === '') {
88
+ internalModelValue.value = null
89
+ emit('update:modelValue', null)
90
+ }
91
+ emit('input', searchTerm.value)
92
+ }
93
+
94
+ const handleOpenChange = (open: boolean) => {
95
+ isOpen.value = open
96
+ if (!open) {
97
+ searchTerm.value = internalModelValue.value
98
+ ? getLabel(internalModelValue.value)
99
+ : ''
100
+ }
101
+ }
102
+
103
+ const handleClick = (event: MouseEvent) => {
104
+ if (props.openOnClick) {
105
+ isOpen.value = true
106
+ }
107
+ }
108
+
109
+ const handleFocus = (event: FocusEvent) => {
110
+ if (props.openOnFocus) {
111
+ isOpen.value = true
112
+ }
113
+ emit('focus', event)
114
+ }
115
+
116
+ const handleBlur = (event: FocusEvent) => {
117
+ emit('blur', event)
118
+ }
119
+
120
+ const handleIconClick = (iconName: string) => {
121
+ onUpdateModelValue(iconName)
122
+ }
123
+
124
+ const reset = () => {
125
+ searchTerm.value = ''
126
+ internalModelValue.value = null
127
+ emit('update:modelValue', null)
128
+ }
129
+
130
+ const variantClasses = computed(() => {
131
+ const borderCss =
132
+ 'border focus-within:border-outline-gray-4 focus-within:ring-2 focus-within:ring-outline-gray-3'
133
+
134
+ return {
135
+ subtle: `${borderCss} bg-surface-gray-2 hover:bg-surface-gray-3 border-transparent`,
136
+ outline: `${borderCss} border-outline-gray-2`,
137
+ ghost: '',
138
+ }[props.variant]
139
+ })
140
+
141
+ defineExpose({
142
+ reset,
143
+ })
144
+ </script>
145
+
146
+ <template>
147
+ <div class="relative">
148
+ <ComboboxRoot
149
+ :model-value="internalModelValue"
150
+ @update:modelValue="onUpdateModelValue"
151
+ @update:open="handleOpenChange"
152
+ :ignore-filter="true"
153
+ :open="isOpen"
154
+ >
155
+ <ComboboxAnchor
156
+ class="flex h-7 w-full items-center justify-between gap-2 rounded px-2 py-1 transition-colors"
157
+ :class="{
158
+ 'opacity-50 pointer-events-none': disabled,
159
+ [variantClasses]: true,
160
+ }"
161
+ @click="handleClick"
162
+ >
163
+ <div class="flex items-center gap-2 flex-1 overflow-hidden">
164
+ <Icon
165
+ :name="internalModelValue || 'circle-dashed'"
166
+ class="w-4 h-4 flex-shrink-0"
167
+ />
168
+ <ComboboxInput
169
+ :value="searchTerm"
170
+ @input="handleInputChange"
171
+ @focus="handleFocus"
172
+ @blur="handleBlur"
173
+ class="bg-transparent p-0 focus:outline-0 border-0 focus:border-0 focus:ring-0 text-base text-ink-gray-8 h-full placeholder:text-ink-gray-4 w-full"
174
+ :placeholder="placeholder || 'Select an icon...'"
175
+ :disabled="disabled"
176
+ autocomplete="off"
177
+ />
178
+ </div>
179
+ <ComboboxTrigger :disabled="disabled">
180
+ <Icon name="chevron-down" class="h-4 w-4 text-ink-gray-5" />
181
+ </ComboboxTrigger>
182
+ </ComboboxAnchor>
183
+ <ComboboxPortal>
184
+ <ComboboxContent
185
+ class="z-10 w-60 mt-1 bg-surface-modal overflow-hidden rounded-lg shadow-2xl"
186
+ position="popper"
187
+ @openAutoFocus.prevent
188
+ @closeAutoFocus.prevent
189
+ :align="props.placement || 'start'"
190
+ >
191
+ <ComboboxViewport class="max-h-60 overflow-auto p-2">
192
+ <ComboboxEmpty
193
+ v-if="filteredIcons.length === 0"
194
+ class="text-ink-gray-5 text-base text-center py-1.5 px-2.5"
195
+ >
196
+ <template v-if="searchTerm">
197
+ No icons found for "{{ searchTerm }}"
198
+ </template>
199
+ <template v-else> No icons available. </template>
200
+ </ComboboxEmpty>
201
+ <div v-if="filteredIcons.length > 0" class="flex flex-wrap">
202
+ <button
203
+ v-for="iconName in filteredIcons.slice(0, props.maxIcons)"
204
+ :key="iconName"
205
+ @click="handleIconClick(iconName)"
206
+ type="button"
207
+ class="w-8 h-8 flex items-center justify-center rounded hover:bg-surface-gray-3 transition-colors"
208
+ :class="{
209
+ 'bg-surface-gray-3': internalModelValue === iconName,
210
+ }"
211
+ :title="getLabel(iconName)"
212
+ >
213
+ <Icon :name="iconName" class="w-4 h-4" />
214
+ </button>
215
+ </div>
216
+ </ComboboxViewport>
217
+ </ComboboxContent>
218
+ </ComboboxPortal>
219
+ </ComboboxRoot>
220
+ </div>
221
+ </template>
package/icons/index.ts CHANGED
@@ -8,3 +8,8 @@ export { default as LightningIcon } from './LightningIcon.vue'
8
8
  export { default as MaximizeIcon } from './MaximizeIcon.vue'
9
9
  export { default as MinimizeIcon } from './MinimizeIcon.vue'
10
10
  export { default as StepsIcon } from './StepsIcon.vue'
11
+
12
+ // Lucide Icons
13
+ export { default as Icon } from './Icon.vue'
14
+ export { default as IconPicker } from './IconPicker.vue'
15
+ export { default as spritePlugin } from './spritePlugin'
@@ -0,0 +1,13 @@
1
+ // @ts-ignore
2
+ import sprite from 'lucide-static/sprite.svg?raw'
3
+ import type { App } from 'vue'
4
+
5
+ export default {
6
+ install(app: App) {
7
+ const div = document.createElement('div')
8
+ div.id = 'lucide-sprite'
9
+ div.style.display = 'none'
10
+ div.innerHTML = sprite
11
+ document.body.prepend(div)
12
+ },
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.1.244",
3
+ "version": "0.1.245",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -4,6 +4,7 @@
4
4
  <div
5
5
  ref="anchorRef"
6
6
  :class="['flex', $attrs.class]"
7
+ :style="($attrs.style as StyleValue)"
7
8
  @mouseover="onMouseover"
8
9
  @mouseleave="onMouseleave"
9
10
  >
@@ -62,7 +63,7 @@
62
63
  </template>
63
64
 
64
65
  <script setup lang="ts">
65
- import { computed, ref, onUnmounted } from 'vue'
66
+ import { computed, ref, onUnmounted, type StyleValue } from 'vue'
66
67
  import {
67
68
  PopoverAnchor,
68
69
  PopoverContent,
@@ -2,10 +2,12 @@
2
2
  import { computed } from 'vue'
3
3
  import type { SelectProps } from './types'
4
4
  import LucideChevronDown from '~icons/lucide/chevron-down'
5
+ import LucideCheck from '~icons/lucide/check'
5
6
 
6
7
  import {
7
8
  SelectContent,
8
9
  SelectItem,
10
+ SelectItemIndicator,
9
11
  SelectItemText,
10
12
  SelectPortal,
11
13
  SelectRoot,
@@ -90,7 +92,7 @@ const selectOptions = computed(() => {
90
92
  :disabled="disabled"
91
93
  >
92
94
  <slot name="prefix" />
93
- <SelectValue :placeholder="placeholder" class='truncate' />
95
+ <SelectValue :placeholder="placeholder" class="truncate" />
94
96
  <slot name="suffix">
95
97
  <LucideChevronDown class="size-4 text-ink-gray-4 ml-auto shrink-0" />
96
98
  </slot>
@@ -107,15 +109,12 @@ const selectOptions = computed(() => {
107
109
  :key="option.value"
108
110
  :value="option.value"
109
111
  :class="[sizeClasses, paddingClasses, fontSizeClasses]"
110
- class="
111
- text-base text-ink-gray-9 flex items-center relative
112
- data-[highlighted]:bg-surface-gray-2 border-0 [data-state=checked]:bg-surface-gray-2
113
- data-[disabled]:text-ink-gray-4 select-none
114
- "
112
+ class="text-base text-ink-gray-9 flex items-center data-[highlighted]:bg-surface-gray-2 border-0 data-[state=checked]:bg-surface-gray-2 data-[disabled]:text-ink-gray-4 select-none"
115
113
  >
116
114
  <SelectItemText>
117
115
  <slot name="option" v-bind="{ option }">{{ option.label }}</slot>
118
116
  </SelectItemText>
117
+ <SelectItemIndicator :as="LucideCheck" class="size-4 ml-auto" />
119
118
  </SelectItem>
120
119
  <slot name="footer" />
121
120
  </SelectViewport>
@@ -135,7 +134,7 @@ const selectOptions = computed(() => {
135
134
  @keyframes fadeInScale {
136
135
  from {
137
136
  opacity: 0;
138
- transform: scale(0.90);
137
+ transform: scale(0.9);
139
138
  }
140
139
  to {
141
140
  opacity: 1;