frappe-ui 0.1.138 → 0.1.140
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 +2 -1
- package/src/components/Button/Button.vue +1 -1
- package/src/components/Combobox/Combobox.vue +9 -8
- package/src/components/TextEditor/EditLink.vue +8 -21
- package/src/components/TextEditor/link-extension.ts +179 -58
- package/src/components/TextEditor/linkPasteHandler.ts +51 -0
- package/src/tailwind/colorPalette.js +3 -7
- package/src/tailwind/figma-variables-to-colors.js +3 -3
- package/src/tailwind/plugin.js +6 -6
- package/src/tailwind/preset.js +6 -7
- package/src/tailwind/update-tailwind-classes.js +3 -3
- package/src/utils/tailwind.config.js +2 -2
- package/src/utils/url-validation.ts +39 -0
- package/vite/buildConfig.js +3 -5
- package/vite/doctypeInterfaceGenerator.js +3 -5
- package/vite/frappeProxy.js +2 -4
- package/vite/frappeTypes.js +8 -5
- package/vite/generateTypes.js +6 -4
- package/vite/index.js +6 -6
- package/vite/jinjaBootData.js +1 -3
- package/vite/lucideIcons.js +8 -10
- package/vite/utils.js +5 -11
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.140",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
|
+
"type": "module",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"test": "vitest",
|
|
8
9
|
"prettier": "yarn prettier -w ./src",
|
|
@@ -85,7 +85,6 @@ const props = withDefaults(defineProps<ButtonProps>(), {
|
|
|
85
85
|
})
|
|
86
86
|
|
|
87
87
|
const slots = useSlots()
|
|
88
|
-
const router = useRouter()
|
|
89
88
|
|
|
90
89
|
const buttonClasses = computed(() => {
|
|
91
90
|
let solidClasses = {
|
|
@@ -216,6 +215,7 @@ const isIconButton = computed(() => {
|
|
|
216
215
|
|
|
217
216
|
const handleClick = () => {
|
|
218
217
|
if (props.route) {
|
|
218
|
+
const router = useRouter()
|
|
219
219
|
return router.push(props.route)
|
|
220
220
|
} else if (props.link) {
|
|
221
221
|
return window.open(props.link, '_blank')
|
|
@@ -46,7 +46,7 @@ interface ComboboxProps {
|
|
|
46
46
|
const props = defineProps<ComboboxProps>()
|
|
47
47
|
const emit = defineEmits(['update:modelValue', 'update:selectedOption'])
|
|
48
48
|
|
|
49
|
-
const searchTerm = ref(
|
|
49
|
+
const searchTerm = ref(getDisplayValue(props.modelValue))
|
|
50
50
|
const internalModelValue = ref(props.modelValue)
|
|
51
51
|
const isOpen = ref(false)
|
|
52
52
|
const userHasTyped = ref(false)
|
|
@@ -71,23 +71,23 @@ const onUpdateModelValue = (value: string | null) => {
|
|
|
71
71
|
emit('update:selectedOption', selectedOpt)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
function isGroup(option: ComboboxOption): option is GroupedOption {
|
|
75
75
|
return typeof option === 'object' && 'group' in option
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
function getLabel(option: SimpleOption): string {
|
|
79
79
|
return typeof option === 'string' ? option : option.label
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
function getValue(option: SimpleOption): string {
|
|
83
83
|
return typeof option === 'string' ? option : option.value
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
function isDisabled(option: SimpleOption): boolean {
|
|
87
87
|
return typeof option === 'object' && !!option.disabled
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
function getIcon(option: SimpleOption): string | Component | undefined {
|
|
91
91
|
return typeof option === 'object' ? option.icon : undefined
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -105,9 +105,10 @@ const allOptionsFlat = computed(() => {
|
|
|
105
105
|
|
|
106
106
|
function getDisplayValue(selectedValue: string | null | undefined): string {
|
|
107
107
|
if (!selectedValue) return ''
|
|
108
|
-
const
|
|
109
|
-
(opt)
|
|
108
|
+
const options = props.options.flatMap((opt) =>
|
|
109
|
+
isGroup(opt) ? opt.options : opt,
|
|
110
110
|
)
|
|
111
|
+
const selectedOption = options.find((opt) => getValue(opt) === selectedValue)
|
|
111
112
|
return selectedOption ? getLabel(selectedOption) : selectedValue || ''
|
|
112
113
|
}
|
|
113
114
|
|
|
@@ -27,18 +27,16 @@
|
|
|
27
27
|
</template>
|
|
28
28
|
|
|
29
29
|
<script setup lang="ts">
|
|
30
|
-
import { onMounted, ref, useTemplateRef } from 'vue'
|
|
30
|
+
import { onMounted, ref, useTemplateRef, nextTick } from 'vue'
|
|
31
31
|
import Button from '../Button/Button.vue'
|
|
32
32
|
import TextInput from '../TextInput.vue'
|
|
33
33
|
import Tooltip from '../Tooltip/Tooltip.vue'
|
|
34
34
|
import LucideCheck from '~icons/lucide/check'
|
|
35
35
|
import LucideX from '~icons/lucide/x'
|
|
36
|
+
import { isValidUrl } from '../../utils/url-validation'
|
|
36
37
|
|
|
37
38
|
const props = defineProps<{
|
|
38
|
-
show: boolean
|
|
39
39
|
href: string
|
|
40
|
-
onClose: () => void
|
|
41
|
-
onUpdateHref: (href: string) => void
|
|
42
40
|
}>()
|
|
43
41
|
|
|
44
42
|
const emit = defineEmits<{
|
|
@@ -49,28 +47,17 @@ const emit = defineEmits<{
|
|
|
49
47
|
const _href = ref(props.href)
|
|
50
48
|
const input = useTemplateRef('input')
|
|
51
49
|
|
|
52
|
-
// Simple URL validation regex
|
|
53
|
-
const isValidUrl = (url: string) => {
|
|
54
|
-
if (!url) return true
|
|
55
|
-
const regex =
|
|
56
|
-
/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/i
|
|
57
|
-
return regex.test(url)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
50
|
const submitLink = () => {
|
|
61
|
-
if (isValidUrl(_href.value)) {
|
|
51
|
+
if (_href.value === '' || isValidUrl(_href.value)) {
|
|
62
52
|
emit('updateHref', _href.value)
|
|
63
53
|
}
|
|
64
54
|
}
|
|
65
55
|
|
|
66
|
-
onMounted(() => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
input.value.el.select()
|
|
72
|
-
}
|
|
73
|
-
}, 0)
|
|
56
|
+
onMounted(async () => {
|
|
57
|
+
await nextTick()
|
|
58
|
+
if (input.value?.el) {
|
|
59
|
+
input.value.el.focus()
|
|
60
|
+
input.value.el.select()
|
|
74
61
|
}
|
|
75
62
|
})
|
|
76
63
|
</script>
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import Link from '@tiptap/extension-link'
|
|
2
1
|
import { createApp, h } from 'vue'
|
|
2
|
+
import Link from '@tiptap/extension-link'
|
|
3
|
+
import tippy, { type Instance as TippyInstance } from 'tippy.js'
|
|
4
|
+
import { getMarkRange, Range, Editor } from '@tiptap/core'
|
|
5
|
+
import { MarkType, Mark as ProseMirrorMark } from 'prosemirror-model'
|
|
6
|
+
import { Plugin, PluginKey } from 'prosemirror-state'
|
|
3
7
|
import EditLink from './EditLink.vue'
|
|
4
|
-
import
|
|
5
|
-
import { getMarkRange, Mark, Range, Editor } from '@tiptap/core'
|
|
8
|
+
import { linkPasteHandler } from './linkPasteHandler'
|
|
6
9
|
|
|
7
10
|
declare module '@tiptap/core' {
|
|
8
11
|
interface Commands<ReturnType> {
|
|
@@ -22,6 +25,7 @@ export const LinkExtension = Link.extend({
|
|
|
22
25
|
openOnClick: false,
|
|
23
26
|
autolink: true,
|
|
24
27
|
defaultProtocol: 'https',
|
|
28
|
+
linkOnPaste: false,
|
|
25
29
|
}
|
|
26
30
|
},
|
|
27
31
|
|
|
@@ -36,7 +40,8 @@ export const LinkExtension = Link.extend({
|
|
|
36
40
|
const { doc } = state
|
|
37
41
|
|
|
38
42
|
let range: Range | undefined = undefined
|
|
39
|
-
let mark:
|
|
43
|
+
let mark: ProseMirrorMark | undefined = undefined
|
|
44
|
+
let shouldDelayPopover = false
|
|
40
45
|
|
|
41
46
|
// Check if cursor is within a link or if there's a selection
|
|
42
47
|
if (from === to) {
|
|
@@ -49,8 +54,15 @@ export const LinkExtension = Link.extend({
|
|
|
49
54
|
.resolve(markRange.from)
|
|
50
55
|
.marks()
|
|
51
56
|
.find((m) => m.type === this.type)
|
|
57
|
+
|
|
58
|
+
// Select the link text
|
|
59
|
+
editor
|
|
60
|
+
.chain()
|
|
61
|
+
.setTextSelection({ from: markRange.from, to: markRange.to })
|
|
62
|
+
.run()
|
|
63
|
+
shouldDelayPopover = true
|
|
52
64
|
} else {
|
|
53
|
-
// No selection and not within a link
|
|
65
|
+
// No selection and not within a link, and cursor not in link
|
|
54
66
|
return false
|
|
55
67
|
}
|
|
56
68
|
} else {
|
|
@@ -69,49 +81,58 @@ export const LinkExtension = Link.extend({
|
|
|
69
81
|
const selectionFrom = range.from
|
|
70
82
|
const selectionTo = range.to
|
|
71
83
|
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
const showPopover = () => {
|
|
85
|
+
openLinkEditor(existingHref, editor.view.dom)
|
|
86
|
+
.then((href) => {
|
|
87
|
+
if (href === null) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
77
90
|
|
|
78
|
-
|
|
91
|
+
let chain = editor
|
|
92
|
+
.chain()
|
|
93
|
+
.focus(null, { scrollIntoView: false })
|
|
79
94
|
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
if (href === '') {
|
|
96
|
+
chain
|
|
97
|
+
.setTextSelection({ from: selectionFrom, to: selectionTo })
|
|
98
|
+
.unsetLink()
|
|
99
|
+
.command(({ tr }) => {
|
|
100
|
+
tr.setStoredMarks([])
|
|
101
|
+
return true
|
|
102
|
+
})
|
|
103
|
+
.run()
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
chain = chain
|
|
82
108
|
.setTextSelection({ from: selectionFrom, to: selectionTo })
|
|
83
|
-
.
|
|
109
|
+
.setLink({ href })
|
|
110
|
+
.setTextSelection(selectionTo)
|
|
84
111
|
.command(({ tr }) => {
|
|
85
112
|
tr.setStoredMarks([])
|
|
86
113
|
return true
|
|
87
114
|
})
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
chain = chain.insertContent(' ')
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
chain.run()
|
|
113
|
-
})
|
|
114
|
-
.catch(() => {}) // Ignore cancellation
|
|
115
|
+
|
|
116
|
+
const posAfterLink = selectionTo
|
|
117
|
+
const charAfter =
|
|
118
|
+
posAfterLink < doc.content.size
|
|
119
|
+
? doc.textBetween(posAfterLink, posAfterLink + 1)
|
|
120
|
+
: null
|
|
121
|
+
|
|
122
|
+
if (charAfter === null || charAfter !== ' ') {
|
|
123
|
+
chain = chain.insertContent(' ')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
chain.run()
|
|
127
|
+
})
|
|
128
|
+
.catch(() => {})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (shouldDelayPopover) {
|
|
132
|
+
requestAnimationFrame(showPopover)
|
|
133
|
+
} else {
|
|
134
|
+
showPopover()
|
|
135
|
+
}
|
|
115
136
|
|
|
116
137
|
return true
|
|
117
138
|
},
|
|
@@ -123,6 +144,27 @@ export const LinkExtension = Link.extend({
|
|
|
123
144
|
'Mod-k': () => this.editor.commands.openLinkEditor(),
|
|
124
145
|
}
|
|
125
146
|
},
|
|
147
|
+
|
|
148
|
+
addProseMirrorPlugins() {
|
|
149
|
+
let plugins = this.parent?.() || []
|
|
150
|
+
|
|
151
|
+
plugins.push(
|
|
152
|
+
linkPasteHandler({
|
|
153
|
+
editor: this.editor,
|
|
154
|
+
defaultProtocol: this.options.defaultProtocol,
|
|
155
|
+
type: this.type,
|
|
156
|
+
}),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
plugins.push(
|
|
160
|
+
clearLinkOnBoundaryPlugin({
|
|
161
|
+
editor: this.editor,
|
|
162
|
+
type: this.type,
|
|
163
|
+
}),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return plugins
|
|
167
|
+
},
|
|
126
168
|
})
|
|
127
169
|
|
|
128
170
|
function openLinkEditor(href: string, anchor: HTMLElement): Promise<string> {
|
|
@@ -138,16 +180,17 @@ function openLinkEditor(href: string, anchor: HTMLElement): Promise<string> {
|
|
|
138
180
|
if (selection && selection.rangeCount > 0) {
|
|
139
181
|
const range = selection.getRangeAt(0)
|
|
140
182
|
const rect = range.getBoundingClientRect()
|
|
183
|
+
const isCollapsed = range.collapsed
|
|
141
184
|
|
|
142
185
|
virtualReference = {
|
|
143
186
|
getBoundingClientRect: () => ({
|
|
144
187
|
width: 0,
|
|
145
|
-
height:
|
|
188
|
+
height: rect.height,
|
|
146
189
|
top: rect.top,
|
|
147
|
-
right: rect.left
|
|
148
|
-
bottom: rect.
|
|
149
|
-
left: rect.left
|
|
150
|
-
x: rect.left
|
|
190
|
+
right: isCollapsed ? rect.left : rect.right,
|
|
191
|
+
bottom: rect.bottom,
|
|
192
|
+
left: rect.left,
|
|
193
|
+
x: rect.left,
|
|
151
194
|
y: rect.top,
|
|
152
195
|
toJSON: () => {},
|
|
153
196
|
}),
|
|
@@ -158,18 +201,47 @@ function openLinkEditor(href: string, anchor: HTMLElement): Promise<string> {
|
|
|
158
201
|
}
|
|
159
202
|
}
|
|
160
203
|
|
|
161
|
-
|
|
204
|
+
let app: ReturnType<typeof createApp> | null = null
|
|
205
|
+
let tippyInstance: TippyInstance | null = null
|
|
206
|
+
let isDestroyed = false
|
|
207
|
+
let promiseSettled = false
|
|
208
|
+
|
|
209
|
+
const settlePromise = (action: 'resolve' | 'reject', value?: any) => {
|
|
210
|
+
if (promiseSettled) return
|
|
211
|
+
promiseSettled = true
|
|
212
|
+
if (action === 'resolve') {
|
|
213
|
+
resolve(value)
|
|
214
|
+
} else {
|
|
215
|
+
reject(value)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const destroy = () => {
|
|
220
|
+
if (isDestroyed) return
|
|
221
|
+
isDestroyed = true
|
|
222
|
+
|
|
223
|
+
settlePromise('reject', 'Link editing cancelled or destroyed')
|
|
224
|
+
|
|
225
|
+
requestAnimationFrame(() => {
|
|
226
|
+
tippyInstance?.destroy()
|
|
227
|
+
app?.unmount()
|
|
228
|
+
container?.remove()
|
|
229
|
+
app = null
|
|
230
|
+
tippyInstance = null
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
app = createApp({
|
|
162
235
|
render() {
|
|
163
236
|
return h(EditLink, {
|
|
164
|
-
show: true,
|
|
165
237
|
href,
|
|
166
238
|
onClose: () => {
|
|
239
|
+
settlePromise('reject', 'Link editing cancelled')
|
|
167
240
|
destroy()
|
|
168
|
-
reject('Link editing cancelled')
|
|
169
241
|
},
|
|
170
242
|
onUpdateHref: (newHref: string) => {
|
|
243
|
+
settlePromise('resolve', newHref)
|
|
171
244
|
destroy()
|
|
172
|
-
resolve(newHref)
|
|
173
245
|
},
|
|
174
246
|
})
|
|
175
247
|
},
|
|
@@ -177,7 +249,7 @@ function openLinkEditor(href: string, anchor: HTMLElement): Promise<string> {
|
|
|
177
249
|
|
|
178
250
|
app.mount(container)
|
|
179
251
|
|
|
180
|
-
|
|
252
|
+
tippyInstance = tippy(anchor, {
|
|
181
253
|
getReferenceClientRect: () => virtualReference.getBoundingClientRect(),
|
|
182
254
|
content: container,
|
|
183
255
|
trigger: 'manual',
|
|
@@ -189,20 +261,69 @@ function openLinkEditor(href: string, anchor: HTMLElement): Promise<string> {
|
|
|
189
261
|
maxWidth: 'none',
|
|
190
262
|
onHidden() {
|
|
191
263
|
destroy()
|
|
192
|
-
reject('Link editing cancelled')
|
|
193
264
|
},
|
|
265
|
+
hideOnClick: true,
|
|
266
|
+
interactiveDebounce: 75,
|
|
194
267
|
})
|
|
195
268
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
container.remove()
|
|
201
|
-
}, 0)
|
|
269
|
+
if (!tippyInstance) {
|
|
270
|
+
container.remove()
|
|
271
|
+
settlePromise('reject', 'Failed to initialize link editor tooltip')
|
|
272
|
+
return
|
|
202
273
|
}
|
|
203
274
|
|
|
204
275
|
tippyInstance.show()
|
|
205
276
|
})
|
|
206
277
|
}
|
|
207
278
|
|
|
279
|
+
function clearLinkOnBoundaryPlugin(options: {
|
|
280
|
+
editor: Editor
|
|
281
|
+
type: MarkType
|
|
282
|
+
}) {
|
|
283
|
+
return new Plugin({
|
|
284
|
+
key: new PluginKey('clearLinkMarkOnBoundary'),
|
|
285
|
+
appendTransaction: (transactions, oldState, newState) => {
|
|
286
|
+
if (!options.editor.isEditable) {
|
|
287
|
+
return null
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const { tr, doc, selection, storedMarks } = newState
|
|
291
|
+
const { $from, empty } = selection
|
|
292
|
+
|
|
293
|
+
if (!empty || !storedMarks || storedMarks.length === 0) {
|
|
294
|
+
// Only apply for cursor selections and if there are stored marks
|
|
295
|
+
return null
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const linkMarkType = options.type
|
|
299
|
+
const hasStoredLinkMark = storedMarks.some(
|
|
300
|
+
(mark) => mark.type === linkMarkType,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if (!hasStoredLinkMark) {
|
|
304
|
+
return null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check if the cursor position itself has an active link mark in the document
|
|
308
|
+
const marksAtCursor = $from.marks()
|
|
309
|
+
const activeLinkAtCursor = marksAtCursor.some(
|
|
310
|
+
(mark) => mark.type === linkMarkType,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
if (activeLinkAtCursor) {
|
|
314
|
+
// If there's an actual link mark active in the document at the cursor,
|
|
315
|
+
// then it's correct for the stored mark to be there.
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// If we are here, it means:
|
|
320
|
+
// 1. Selection is a cursor (empty).
|
|
321
|
+
// 2. There's a stored link mark.
|
|
322
|
+
// 3. There's no active link mark in the document at the cursor position.
|
|
323
|
+
// This indicates the stored link mark should be cleared.
|
|
324
|
+
return tr.setStoredMarks([])
|
|
325
|
+
},
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
208
329
|
export default LinkExtension
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Editor } from '@tiptap/core'
|
|
2
|
+
import { MarkType } from '@tiptap/pm/model'
|
|
3
|
+
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
|
4
|
+
import { isValidUrl } from '../../utils/url-validation'
|
|
5
|
+
|
|
6
|
+
type PasteHandlerOptions = {
|
|
7
|
+
editor: Editor
|
|
8
|
+
defaultProtocol: string
|
|
9
|
+
type: MarkType
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function linkPasteHandler(options: PasteHandlerOptions): Plugin {
|
|
13
|
+
return new Plugin({
|
|
14
|
+
key: new PluginKey('handlePasteLink'),
|
|
15
|
+
props: {
|
|
16
|
+
handlePaste: (view, event, slice) => {
|
|
17
|
+
const { state } = view
|
|
18
|
+
const { selection } = state
|
|
19
|
+
const { empty } = selection
|
|
20
|
+
|
|
21
|
+
if (empty) {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let textContent = ''
|
|
26
|
+
slice.content.forEach((node) => {
|
|
27
|
+
textContent += node.textContent
|
|
28
|
+
})
|
|
29
|
+
if (!textContent) {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let link = isValidUrl(textContent) ? textContent : null
|
|
34
|
+
if (!link) {
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return options.editor
|
|
39
|
+
.chain()
|
|
40
|
+
.setTextSelection({ from: selection.from, to: selection.to })
|
|
41
|
+
.setLink({ href: link })
|
|
42
|
+
.setTextSelection(selection.to)
|
|
43
|
+
.command(({ tr }) => {
|
|
44
|
+
tr.setStoredMarks([])
|
|
45
|
+
return true
|
|
46
|
+
})
|
|
47
|
+
.run()
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import tailwindColors from 'tailwindcss/colors'
|
|
2
|
+
import colorsData from './colors.json' assert { type: 'json' }
|
|
3
3
|
|
|
4
4
|
function generateColorPalette() {
|
|
5
5
|
const colorPalette = {
|
|
@@ -108,8 +108,4 @@ function generateSemanticColors() {
|
|
|
108
108
|
return output
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
generateColorPalette,
|
|
113
|
-
generateCSSVariables,
|
|
114
|
-
generateSemanticColors,
|
|
115
|
-
}
|
|
111
|
+
export { generateColorPalette, generateCSSVariables, generateSemanticColors }
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* to colors JSON object that can be used in the Tailwind config.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import fs from 'fs'
|
|
7
|
+
import path from 'path'
|
|
8
8
|
|
|
9
9
|
function main() {
|
|
10
10
|
const variables = getVariables()
|
|
@@ -144,7 +144,7 @@ function getVariables() {
|
|
|
144
144
|
console.log('Please provide path to variables.json file')
|
|
145
145
|
process.exit(1)
|
|
146
146
|
}
|
|
147
|
-
return
|
|
147
|
+
return JSON.parse(fs.readFileSync(variablesJSONPath, 'utf-8'))
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
main()
|
package/src/tailwind/plugin.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import plugin from 'tailwindcss/plugin'
|
|
2
|
+
import {
|
|
3
3
|
generateColorPalette,
|
|
4
4
|
generateSemanticColors,
|
|
5
5
|
generateCSSVariables,
|
|
6
|
-
}
|
|
6
|
+
} from './colorPalette.js'
|
|
7
7
|
|
|
8
8
|
let colorPalette = generateColorPalette()
|
|
9
9
|
let semanticColors = generateSemanticColors()
|
|
@@ -49,7 +49,7 @@ let componentStyles = {
|
|
|
49
49
|
},
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
export default plugin(
|
|
53
53
|
function ({ addBase, addComponents, theme }) {
|
|
54
54
|
addBase({ ...globalStyles(theme), ...cssVariables })
|
|
55
55
|
addComponents(componentStyles)
|
|
@@ -342,7 +342,7 @@ module.exports = plugin(
|
|
|
342
342
|
css: {
|
|
343
343
|
fontSize: '14px',
|
|
344
344
|
fontWeight: 420,
|
|
345
|
-
lineHeight: 1.
|
|
345
|
+
lineHeight: 1.5,
|
|
346
346
|
letterSpacing: '0.02em',
|
|
347
347
|
h1: {
|
|
348
348
|
fontSize: em(20, 14),
|
|
@@ -361,7 +361,7 @@ module.exports = plugin(
|
|
|
361
361
|
},
|
|
362
362
|
p: {
|
|
363
363
|
marginTop: '0.5rem',
|
|
364
|
-
marginBottom: '
|
|
364
|
+
marginBottom: '0.5rem',
|
|
365
365
|
},
|
|
366
366
|
'ul > li': {
|
|
367
367
|
margin: '0.5rem 0',
|
package/src/tailwind/preset.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import themePlugin from './plugin.js'
|
|
2
|
+
import forms from '@tailwindcss/forms'
|
|
3
|
+
import typography from '@tailwindcss/typography'
|
|
4
|
+
|
|
2
5
|
/** @type {import('tailwindcss').Config} */
|
|
3
|
-
|
|
6
|
+
export default {
|
|
4
7
|
darkMode: ['selector', '[data-theme="dark"]'],
|
|
5
|
-
plugins: [
|
|
6
|
-
require('@tailwindcss/forms'),
|
|
7
|
-
require('@tailwindcss/typography'),
|
|
8
|
-
themePlugin,
|
|
9
|
-
],
|
|
8
|
+
plugins: [forms, typography, themePlugin],
|
|
10
9
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import preset from '../tailwind/preset.js'
|
|
2
2
|
console.warn(
|
|
3
3
|
'`frappe-ui/src/utils/tailwind.config.js` is deprecated. Use `frappe-ui/tailwind/preset.js` instead.',
|
|
4
4
|
)
|
|
5
5
|
// keep backwards compatible for now
|
|
6
|
-
|
|
6
|
+
export default preset
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates if the given string is a valid URL, including common protocols,
|
|
3
|
+
* relative paths, and anchor links. It also allows an empty string.
|
|
4
|
+
*
|
|
5
|
+
* @param url The URL string to validate.
|
|
6
|
+
* @returns True if the URL is considered valid, false otherwise.
|
|
7
|
+
*/
|
|
8
|
+
export function isValidUrl(url: string): boolean {
|
|
9
|
+
if (url === '') {
|
|
10
|
+
return true // Allows empty string as per documentation
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Regex for absolute URLs with common schemes (http, https, mailto, tel, //)
|
|
14
|
+
// Allows for paths, query strings, and fragments.
|
|
15
|
+
if (/^(https?:\/\/|mailto:|tel:|\/\/)[^\s]+$/i.test(url)) {
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Regex for:
|
|
20
|
+
// 1. Relative paths (starting with / or .)
|
|
21
|
+
// 2. Anchor links (starting with #)
|
|
22
|
+
// These allow for most characters except whitespace, as the initial characters
|
|
23
|
+
// prevent misinterpretation as a scheme like 'javascript:'.
|
|
24
|
+
if (/^([./#][^\s]*)$/i.test(url)) {
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Regex for "Schemeless" paths (e.g., page.html, my-document, slug-name)
|
|
29
|
+
// Must start with a word character or hyphen.
|
|
30
|
+
// Subsequent characters are restricted to common URL path/query/fragment characters
|
|
31
|
+
// (alphanumeric, -, _, ., /, #, ?, =, &, %).
|
|
32
|
+
// This prevents colons (avoiding 'javascript:' like schemes) and other problematic
|
|
33
|
+
// characters like spaces, (, ), !, {, }.
|
|
34
|
+
if (/^[\w\-]([\w\-./#?=&%]*)$/i.test(url)) {
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false
|
|
39
|
+
}
|
package/vite/buildConfig.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import fs from 'fs'
|
|
3
3
|
|
|
4
|
-
function buildConfig(options = {}) {
|
|
4
|
+
export function buildConfig(options = {}) {
|
|
5
5
|
let outDir = options.outDir || findOutputDir()
|
|
6
6
|
if (!outDir) {
|
|
7
7
|
console.error(
|
|
@@ -165,5 +165,3 @@ function findAppDir() {
|
|
|
165
165
|
|
|
166
166
|
return null
|
|
167
167
|
}
|
|
168
|
-
|
|
169
|
-
module.exports = { buildConfig }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
import path from 'path'
|
|
3
3
|
|
|
4
|
-
class DocTypeInterfaceGenerator {
|
|
4
|
+
export class DocTypeInterfaceGenerator {
|
|
5
5
|
constructor(appsPath, appDoctypeMap, outputPath) {
|
|
6
6
|
this.appsPath = appsPath
|
|
7
7
|
this.appDoctypeMap = appDoctypeMap
|
|
@@ -244,5 +244,3 @@ class DocTypeInterfaceGenerator {
|
|
|
244
244
|
`
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
|
-
|
|
248
|
-
exports.DocTypeInterfaceGenerator = DocTypeInterfaceGenerator
|
package/vite/frappeProxy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { getCommonSiteConfig } from './utils.js'
|
|
2
2
|
|
|
3
|
-
function frappeProxy({
|
|
3
|
+
export function frappeProxy({
|
|
4
4
|
port = 8080,
|
|
5
5
|
source = '^/(app|login|api|assets|files|private)',
|
|
6
6
|
} = {}) {
|
|
@@ -32,5 +32,3 @@ function frappeProxy({
|
|
|
32
32
|
}),
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
exports.frappeProxy = frappeProxy
|
package/vite/frappeTypes.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { spawn } from 'child_process'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
// Get __dirname equivalent in ES modules
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
7
|
+
const __dirname = path.dirname(__filename)
|
|
8
|
+
|
|
9
|
+
export function frappeTypes(options = {}) {
|
|
5
10
|
let childProcess = null
|
|
6
11
|
|
|
7
12
|
return {
|
|
@@ -63,5 +68,3 @@ function frappeTypes(options = {}) {
|
|
|
63
68
|
},
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
|
-
|
|
67
|
-
exports.frappeTypes = frappeTypes
|
package/vite/generateTypes.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { fileURLToPath } from 'url'
|
|
3
|
+
import { findAppsFolder } from './utils.js'
|
|
4
|
+
import { DocTypeInterfaceGenerator } from './doctypeInterfaceGenerator.js'
|
|
4
5
|
|
|
5
6
|
// Handle termination signals to exit cleanly
|
|
6
7
|
process.on('SIGINT', () => process.exit(0))
|
|
@@ -49,7 +50,8 @@ async function main() {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
// Execute if run directly
|
|
52
|
-
|
|
53
|
+
const currentFilePath = fileURLToPath(import.meta.url)
|
|
54
|
+
if (process.argv[1] === currentFilePath) {
|
|
53
55
|
main()
|
|
54
56
|
.then(() => process.exit(0))
|
|
55
57
|
.catch((err) => {
|
package/vite/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { lucideIcons } from './lucideIcons.js'
|
|
2
|
+
import { frappeProxy } from './frappeProxy.js'
|
|
3
|
+
import { frappeTypes } from './frappeTypes.js'
|
|
4
|
+
import { jinjaBootData } from './jinjaBootData.js'
|
|
5
|
+
import { buildConfig } from './buildConfig.js'
|
|
6
6
|
|
|
7
7
|
function frappeuiPlugin(
|
|
8
8
|
options = {
|
|
@@ -32,4 +32,4 @@ function frappeuiPlugin(
|
|
|
32
32
|
return plugins
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
export default frappeuiPlugin
|
package/vite/jinjaBootData.js
CHANGED
package/vite/lucideIcons.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import * as LucideIcons from 'lucide-static'
|
|
2
|
+
import Icons from 'unplugin-icons/vite'
|
|
3
|
+
import Components from 'unplugin-vue-components/vite'
|
|
4
|
+
import IconsResolver from 'unplugin-icons/resolver'
|
|
5
5
|
|
|
6
|
-
function
|
|
6
|
+
export function lucideIcons() {
|
|
7
7
|
return [
|
|
8
|
-
Components
|
|
8
|
+
Components({
|
|
9
9
|
resolvers: [
|
|
10
|
-
IconsResolver
|
|
10
|
+
IconsResolver({
|
|
11
11
|
prefix: false,
|
|
12
12
|
enabledCollections: ['lucide'],
|
|
13
13
|
}),
|
|
14
14
|
],
|
|
15
15
|
}),
|
|
16
|
-
Icons
|
|
16
|
+
Icons({
|
|
17
17
|
customCollections: {
|
|
18
18
|
lucide: getIcons(),
|
|
19
19
|
},
|
|
@@ -63,5 +63,3 @@ function camelToDash(key) {
|
|
|
63
63
|
}
|
|
64
64
|
return [withNumber]
|
|
65
65
|
}
|
|
66
|
-
|
|
67
|
-
exports.lucideIcons = lucideIconsPlugin
|
package/vite/utils.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import fs from 'fs'
|
|
3
3
|
|
|
4
|
-
function getConfig() {
|
|
4
|
+
export function getConfig() {
|
|
5
5
|
let configPath = path.join(process.cwd(), 'frappeui.json')
|
|
6
6
|
if (fs.existsSync(configPath)) {
|
|
7
7
|
return JSON.parse(fs.readFileSync(configPath))
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
function getCommonSiteConfig() {
|
|
11
|
+
export function getCommonSiteConfig() {
|
|
12
12
|
let currentDir = path.resolve('.')
|
|
13
13
|
// traverse up till we find frappe-bench with sites directory
|
|
14
14
|
while (currentDir !== '/') {
|
|
@@ -27,7 +27,7 @@ function getCommonSiteConfig() {
|
|
|
27
27
|
return null
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function findAppsFolder() {
|
|
30
|
+
export function findAppsFolder() {
|
|
31
31
|
let currentDir = process.cwd()
|
|
32
32
|
while (currentDir !== '/') {
|
|
33
33
|
if (
|
|
@@ -40,9 +40,3 @@ function findAppsFolder() {
|
|
|
40
40
|
}
|
|
41
41
|
return null
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
module.exports = {
|
|
45
|
-
getConfig,
|
|
46
|
-
getCommonSiteConfig,
|
|
47
|
-
findAppsFolder,
|
|
48
|
-
}
|