frappe-ui 1.0.0-beta.73 → 1.0.0-beta.75
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/experimental/ListView/ListFooter.vue +7 -3
- package/experimental.ts +0 -11
- package/package.json +74 -19
- package/src/components/Badge/Badge.vue +1 -1
- package/src/molecules/code-editor/CodeEditor.vue +82 -0
- package/src/molecules/code-editor/CodeEditorContent.vue +138 -0
- package/src/molecules/code-editor/code-editor-context.ts +46 -0
- package/src/molecules/code-editor/extensions.ts +133 -0
- package/src/molecules/code-editor/index.ts +32 -0
- package/src/molecules/code-editor/kit.ts +198 -0
- package/src/molecules/code-editor/languages.ts +99 -0
- package/src/molecules/code-editor/style.css +252 -0
- package/src/molecules/code-editor/types.ts +19 -0
- package/src/molecules/code-editor/useCodeEditor.ts +222 -0
- package/tailwind/colorPalette.js +61 -166
- package/tailwind/content.js +0 -1
- package/tailwind/index.d.ts +4 -0
- package/tailwind/lucideIconsPlugin.js +10 -2
- package/tailwind/migrate-tokens-v2.js +2 -2
- package/tailwind/plugin.js +67 -88
- package/tailwind/preset.js +3 -20
- package/tailwind/tokens/build-types.js +281 -0
- package/tailwind/{figma-tokens-to-theme.js → tokens/build.js} +134 -37
- package/tailwind/{colors.json → tokens/colors.js} +4 -1
- package/tailwind/{generated/effects.json → tokens/effects.js} +4 -1
- package/tailwind/tokens/provenance.json +11 -0
- package/tailwind/{generated/radius.json → tokens/radius.js} +4 -1
- package/tailwind/{generated/typography.json → tokens/typography.js} +22 -19
- package/tailwind/tokens.d.ts +714 -0
- package/tailwind/tokens.js +335 -0
- package/vite/README.md +43 -0
- package/vite/codeLanguages.js +206 -0
- package/vite/index.d.ts +10 -0
- package/vite/index.js +8 -0
- package/vitepress/plugins/componentTransformer.ts +6 -0
- package/vitepress/plugins/preventCircularChunks.ts +15 -0
- package/experimental/CodeEditor/CodeEditor.api.md +0 -140
- package/experimental/CodeEditor/CodeEditor.md +0 -106
- package/experimental/CodeEditor/CodeEditor.vue +0 -548
- package/experimental/CodeEditor/CodePreview.vue +0 -45
- package/experimental/CodeEditor/index.ts +0 -18
- package/experimental/CodeEditor/languages.ts +0 -63
- package/experimental/CodeEditor/theme.ts +0 -272
- package/experimental/CodeEditor/types.ts +0 -69
- package/tailwind/generated/colors.json +0 -1173
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
<slot name="left">
|
|
5
5
|
<TabButtons
|
|
6
6
|
v-model="pageLengthCount"
|
|
7
|
-
:
|
|
7
|
+
:options="
|
|
8
|
+
pageLengthOptions.map((o) => ({ label: String(o), value: o }))
|
|
9
|
+
"
|
|
8
10
|
/>
|
|
9
11
|
</slot>
|
|
10
12
|
<slot name="right">
|
|
@@ -27,7 +29,7 @@
|
|
|
27
29
|
</template>
|
|
28
30
|
<script setup>
|
|
29
31
|
import TabButtons from '#components/TabButtons/TabButtons.vue'
|
|
30
|
-
import {
|
|
32
|
+
import { computed } from 'vue'
|
|
31
33
|
|
|
32
34
|
const props = defineProps({
|
|
33
35
|
modelValue: {
|
|
@@ -51,7 +53,9 @@ const pageLengthCount = computed({
|
|
|
51
53
|
set: (value) => emit('update:modelValue', value),
|
|
52
54
|
})
|
|
53
55
|
|
|
54
|
-
const pageLengthOptions =
|
|
56
|
+
const pageLengthOptions = computed(
|
|
57
|
+
() => props.options.pageLengthOptions || [20, 50, 100],
|
|
58
|
+
)
|
|
55
59
|
|
|
56
60
|
const showLoadMore = computed(() => {
|
|
57
61
|
return (
|
package/experimental.ts
CHANGED
|
@@ -10,17 +10,6 @@ export {
|
|
|
10
10
|
InputError,
|
|
11
11
|
} from './src/components/InputLabeling'
|
|
12
12
|
export { useInputLabeling } from './src/composables/useInputLabeling'
|
|
13
|
-
export {
|
|
14
|
-
CodeEditor,
|
|
15
|
-
CodePreview,
|
|
16
|
-
loadLanguage,
|
|
17
|
-
} from './experimental/CodeEditor'
|
|
18
|
-
export type {
|
|
19
|
-
CodeLanguage,
|
|
20
|
-
CodeEditorProps,
|
|
21
|
-
CodeEditorEmits,
|
|
22
|
-
CodePreviewProps,
|
|
23
|
-
} from './experimental/CodeEditor'
|
|
24
13
|
// Calendar family. Moved out of root (#1020, redirect of #989) — parked
|
|
25
14
|
// here, unstable, with its public API unchanged, until a redesigned
|
|
26
15
|
// calendar family replaces it. Migration is the import-path change only.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.75",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.19.0"
|
|
@@ -69,7 +69,8 @@
|
|
|
69
69
|
"docs:gen": "tsx docs/scripts/propsgen.ts",
|
|
70
70
|
"docs:gen:all": "yarn docs:gen --all",
|
|
71
71
|
"docs:check": "tsx docs/scripts/propsgen.ts --check",
|
|
72
|
-
"sync-tokens": "node tailwind/
|
|
72
|
+
"sync-tokens": "node tailwind/tokens/build.js",
|
|
73
|
+
"sync-token-types": "node tailwind/tokens/build-types.js"
|
|
73
74
|
},
|
|
74
75
|
"exports": {
|
|
75
76
|
".": {
|
|
@@ -84,6 +85,10 @@
|
|
|
84
85
|
"import": "./src/molecules/editor/index.ts",
|
|
85
86
|
"types": "./src/molecules/editor/index.ts"
|
|
86
87
|
},
|
|
88
|
+
"./code-editor": {
|
|
89
|
+
"import": "./src/molecules/code-editor/index.ts",
|
|
90
|
+
"types": "./src/molecules/code-editor/index.ts"
|
|
91
|
+
},
|
|
87
92
|
"./list": {
|
|
88
93
|
"import": "./src/molecules/list/index.ts",
|
|
89
94
|
"types": "./src/molecules/list/index.ts"
|
|
@@ -101,6 +106,11 @@
|
|
|
101
106
|
"import": "./tailwind/index.js",
|
|
102
107
|
"default": "./tailwind/index.js"
|
|
103
108
|
},
|
|
109
|
+
"./tailwind/tokens": {
|
|
110
|
+
"types": "./tailwind/tokens.d.ts",
|
|
111
|
+
"import": "./tailwind/tokens.js",
|
|
112
|
+
"default": "./tailwind/tokens.js"
|
|
113
|
+
},
|
|
104
114
|
"./vite": {
|
|
105
115
|
"types": "./vite/index.d.ts",
|
|
106
116
|
"import": "./vite/index.js"
|
|
@@ -158,25 +168,15 @@
|
|
|
158
168
|
"author": "Frappe Technologies Pvt. Ltd.",
|
|
159
169
|
"license": "MIT",
|
|
160
170
|
"dependencies": {
|
|
161
|
-
"@
|
|
162
|
-
"@codemirror/lang-css": "^6.3.0",
|
|
163
|
-
"@codemirror/lang-html": "^6.4.9",
|
|
164
|
-
"@codemirror/lang-javascript": "^6.2.2",
|
|
165
|
-
"@codemirror/lang-json": "^6.0.1",
|
|
166
|
-
"@codemirror/lang-markdown": "^6.3.1",
|
|
167
|
-
"@codemirror/lang-python": "^6.1.6",
|
|
168
|
-
"@codemirror/lang-sass": "^6.0.2",
|
|
169
|
-
"@codemirror/lang-sql": "^6.8.0",
|
|
170
|
-
"@codemirror/lang-xml": "^6.1.0",
|
|
171
|
-
"@codemirror/lang-yaml": "^6.1.3",
|
|
172
|
-
"@codemirror/language": "^6.0.0",
|
|
173
|
-
"@codemirror/lint": "^6.0.0",
|
|
171
|
+
"@codemirror/autocomplete": "^6.0.0",
|
|
174
172
|
"@codemirror/commands": "^6.0.0",
|
|
173
|
+
"@codemirror/language": "^6.0.0",
|
|
174
|
+
"@codemirror/search": "^6.0.0",
|
|
175
175
|
"@codemirror/state": "^6.0.0",
|
|
176
176
|
"@codemirror/view": "^6.0.0",
|
|
177
|
-
"@lezer/highlight": "^1.0.0",
|
|
178
177
|
"@floating-ui/dom": "^1.7.4",
|
|
179
178
|
"@floating-ui/vue": "^1.1.9",
|
|
179
|
+
"@lezer/highlight": "^1.0.0",
|
|
180
180
|
"@tailwindcss/forms": "^0.5.3",
|
|
181
181
|
"@tailwindcss/typography": "^0.5.16",
|
|
182
182
|
"@tiptap/core": "^3.26.0",
|
|
@@ -209,8 +209,8 @@
|
|
|
209
209
|
"@tiptap/starter-kit": "^3.26.0",
|
|
210
210
|
"@tiptap/suggestion": "^3.26.0",
|
|
211
211
|
"@tiptap/vue-3": "^3.26.0",
|
|
212
|
+
"@vue/compiler-sfc": "^3.5.13",
|
|
212
213
|
"@vueuse/core": "^14.1.0",
|
|
213
|
-
"codemirror": "^6.0.1",
|
|
214
214
|
"dayjs": "^1.11.13",
|
|
215
215
|
"dompurify": "^3.4.0",
|
|
216
216
|
"echarts": "^6.1.0",
|
|
@@ -232,16 +232,60 @@
|
|
|
232
232
|
"vue-sonner": "^2.0.9"
|
|
233
233
|
},
|
|
234
234
|
"peerDependencies": {
|
|
235
|
+
"@codemirror/lang-css": "^6.3.0",
|
|
236
|
+
"@codemirror/lang-html": "^6.4.9",
|
|
237
|
+
"@codemirror/lang-javascript": "^6.2.2",
|
|
238
|
+
"@codemirror/lang-json": "^6.0.1",
|
|
239
|
+
"@codemirror/lang-markdown": "^6.3.1",
|
|
240
|
+
"@codemirror/lang-python": "^6.1.6",
|
|
241
|
+
"@codemirror/lang-sass": "^6.0.2",
|
|
242
|
+
"@codemirror/lang-sql": "^6.8.0",
|
|
243
|
+
"@codemirror/lang-xml": "^6.1.0",
|
|
244
|
+
"@codemirror/lang-yaml": "^6.1.3",
|
|
245
|
+
"@codemirror/lint": "^6.0.0",
|
|
235
246
|
"@shikijs/transformers": ">=3.15.0",
|
|
236
247
|
"@vue/compiler-dom": ">=3.5.0",
|
|
237
248
|
"shiki": ">=3.15.0",
|
|
238
|
-
"tailwindcss": ">=3.4.
|
|
249
|
+
"tailwindcss": ">=3.4.2 <4",
|
|
239
250
|
"vite": ">=5",
|
|
240
251
|
"vitepress": ">=2.0.0-alpha.15",
|
|
241
252
|
"vue": ">=3.5.0",
|
|
242
253
|
"vue-router": "^4.1.6"
|
|
243
254
|
},
|
|
244
255
|
"peerDependenciesMeta": {
|
|
256
|
+
"@codemirror/lang-css": {
|
|
257
|
+
"optional": true
|
|
258
|
+
},
|
|
259
|
+
"@codemirror/lang-html": {
|
|
260
|
+
"optional": true
|
|
261
|
+
},
|
|
262
|
+
"@codemirror/lang-javascript": {
|
|
263
|
+
"optional": true
|
|
264
|
+
},
|
|
265
|
+
"@codemirror/lang-json": {
|
|
266
|
+
"optional": true
|
|
267
|
+
},
|
|
268
|
+
"@codemirror/lang-markdown": {
|
|
269
|
+
"optional": true
|
|
270
|
+
},
|
|
271
|
+
"@codemirror/lang-python": {
|
|
272
|
+
"optional": true
|
|
273
|
+
},
|
|
274
|
+
"@codemirror/lang-sass": {
|
|
275
|
+
"optional": true
|
|
276
|
+
},
|
|
277
|
+
"@codemirror/lang-sql": {
|
|
278
|
+
"optional": true
|
|
279
|
+
},
|
|
280
|
+
"@codemirror/lang-xml": {
|
|
281
|
+
"optional": true
|
|
282
|
+
},
|
|
283
|
+
"@codemirror/lang-yaml": {
|
|
284
|
+
"optional": true
|
|
285
|
+
},
|
|
286
|
+
"@codemirror/lint": {
|
|
287
|
+
"optional": true
|
|
288
|
+
},
|
|
245
289
|
"@shikijs/transformers": {
|
|
246
290
|
"optional": true
|
|
247
291
|
},
|
|
@@ -259,6 +303,17 @@
|
|
|
259
303
|
}
|
|
260
304
|
},
|
|
261
305
|
"devDependencies": {
|
|
306
|
+
"@codemirror/lang-css": "^6.3.0",
|
|
307
|
+
"@codemirror/lang-html": "^6.4.9",
|
|
308
|
+
"@codemirror/lang-javascript": "^6.2.2",
|
|
309
|
+
"@codemirror/lang-json": "^6.0.1",
|
|
310
|
+
"@codemirror/lang-markdown": "^6.3.1",
|
|
311
|
+
"@codemirror/lang-python": "^6.1.6",
|
|
312
|
+
"@codemirror/lang-sass": "^6.0.2",
|
|
313
|
+
"@codemirror/lang-sql": "^6.8.0",
|
|
314
|
+
"@codemirror/lang-xml": "^6.1.0",
|
|
315
|
+
"@codemirror/lang-yaml": "^6.1.3",
|
|
316
|
+
"@codemirror/lint": "^6.0.0",
|
|
262
317
|
"@cypress/code-coverage": "^3.13.10",
|
|
263
318
|
"@shikijs/transformers": "^3.15.0",
|
|
264
319
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
@@ -277,7 +332,7 @@
|
|
|
277
332
|
"prettier": "^3.3.2",
|
|
278
333
|
"prettier-plugin-tailwindcss": "^0.1.13",
|
|
279
334
|
"shiki": "^3.15.0",
|
|
280
|
-
"tailwindcss": "^3.4.
|
|
335
|
+
"tailwindcss": "^3.4.2",
|
|
281
336
|
"tsx": "^4.20.6",
|
|
282
337
|
"vite": "^7.3.2",
|
|
283
338
|
"vite-plugin-istanbul": "^9.0.0",
|
|
@@ -32,7 +32,7 @@ const props = withDefaults(defineProps<BadgeProps>(), {
|
|
|
32
32
|
variant: 'subtle',
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
// The semantic scale (Figma export → tailwind/
|
|
35
|
+
// The semantic scale (Figma export → tailwind/tokens/colors.js) is
|
|
36
36
|
// consistent across all themes — gray is the only exception (`-7` is its
|
|
37
37
|
// saturated step, since gray needs more headroom). Tailwind's JIT needs
|
|
38
38
|
// literal class names so the per-theme strings are inlined below.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Extension } from '@codemirror/state'
|
|
3
|
+
import type { EditorView } from '@codemirror/view'
|
|
4
|
+
import { useCodeEditor } from './useCodeEditor'
|
|
5
|
+
import { provideCodeEditor } from './code-editor-context'
|
|
6
|
+
import type { CodeEditorExposed } from './types'
|
|
7
|
+
|
|
8
|
+
const model = defineModel<string>({ default: '' })
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(
|
|
11
|
+
defineProps<{
|
|
12
|
+
/**
|
|
13
|
+
* Every capability the editor has: languages, keymaps, completion sources,
|
|
14
|
+
* themes. Required, and reactive — a new array is re-applied without
|
|
15
|
+
* recreating the view, so the document, the selection and the undo history
|
|
16
|
+
* survive a language change.
|
|
17
|
+
*/
|
|
18
|
+
extensions: Extension[]
|
|
19
|
+
editable?: boolean
|
|
20
|
+
autofocus?: boolean
|
|
21
|
+
}>(),
|
|
22
|
+
{
|
|
23
|
+
editable: true,
|
|
24
|
+
autofocus: false,
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits<{
|
|
29
|
+
/** Blur: the commit point. */
|
|
30
|
+
change: [value: string]
|
|
31
|
+
focus: [event: FocusEvent]
|
|
32
|
+
blur: [event: FocusEvent]
|
|
33
|
+
}>()
|
|
34
|
+
|
|
35
|
+
defineSlots<{
|
|
36
|
+
// The consumer owns the layout: render CodeEditorContent, and whatever chrome
|
|
37
|
+
// the app draws around it.
|
|
38
|
+
default?(props: { editor: EditorView | null }): any
|
|
39
|
+
}>()
|
|
40
|
+
|
|
41
|
+
const editor = useCodeEditor({
|
|
42
|
+
content: model,
|
|
43
|
+
extensions: () => props.extensions,
|
|
44
|
+
editable: () => props.editable,
|
|
45
|
+
autofocus: props.autofocus,
|
|
46
|
+
onChange(view) {
|
|
47
|
+
// Read straight off the view rather than off `model`: `change` is a
|
|
48
|
+
// side-event and must not depend on v-model timing.
|
|
49
|
+
emit('change', view.state.doc.toString())
|
|
50
|
+
},
|
|
51
|
+
onFocus(_view, event) {
|
|
52
|
+
emit('focus', event)
|
|
53
|
+
},
|
|
54
|
+
onBlur(_view, event) {
|
|
55
|
+
emit('blur', event)
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Publish the view to the parts in the slot, so they need no explicit `:editor`.
|
|
60
|
+
provideCodeEditor(editor)
|
|
61
|
+
|
|
62
|
+
// Sanctioned template-ref escape hatch (ADR-0012): a parent's script reaches the
|
|
63
|
+
// live view — to focus it, insert at the cursor, or open the search panel — with
|
|
64
|
+
// no other surface that gets there. The exposed member is the view itself, so no
|
|
65
|
+
// new verbs join the imperative surface.
|
|
66
|
+
//
|
|
67
|
+
// A getter, and typed: Vue unwraps a handed-back ref at the proxy boundary, so
|
|
68
|
+
// `defineExpose({ editor })` would document a ref the caller never sees and
|
|
69
|
+
// leave that ref writable from outside. Same shape as `TextInput.vue`.
|
|
70
|
+
defineExpose<CodeEditorExposed>({
|
|
71
|
+
get editor() {
|
|
72
|
+
return editor.value
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<template>
|
|
78
|
+
<!-- Renderless. <CodeEditor> owns the view lifecycle and the content v-model,
|
|
79
|
+
and draws nothing: there is no label, no box and no toolbar here. The box
|
|
80
|
+
is <CodeEditorContent>, and the label belongs to the app's own field. -->
|
|
81
|
+
<slot :editor="editor" />
|
|
82
|
+
</template>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
nextTick,
|
|
4
|
+
onBeforeUnmount,
|
|
5
|
+
onMounted,
|
|
6
|
+
ref,
|
|
7
|
+
useTemplateRef,
|
|
8
|
+
watch,
|
|
9
|
+
} from 'vue'
|
|
10
|
+
import type { EditorView } from '@codemirror/view'
|
|
11
|
+
import { useResolvedCodeEditor } from './code-editor-context'
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
/** Optional inside <CodeEditor> — falls back to the provided view. */
|
|
15
|
+
editor?: EditorView | null
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
/**
|
|
20
|
+
* The content crossed the height cap, or stopped crossing it. Only
|
|
21
|
+
* transitions are emitted.
|
|
22
|
+
*
|
|
23
|
+
* CSS cannot measure the crossing: a cap clips silently, and no selector
|
|
24
|
+
* matches "this element is scrolling". Three consumers draw an expand
|
|
25
|
+
* affordance off exactly this fact. frappe-ui draws none of them — it emits
|
|
26
|
+
* the boolean and sets `data-overflowing`.
|
|
27
|
+
*/
|
|
28
|
+
overflow: [overflowing: boolean]
|
|
29
|
+
}>()
|
|
30
|
+
|
|
31
|
+
const resolved = useResolvedCodeEditor(() => props.editor)
|
|
32
|
+
const rootEl = useTemplateRef<HTMLElement>('rootEl')
|
|
33
|
+
const overflowing = ref(false)
|
|
34
|
+
// Both flags live on the part's own root, never on `view.dom`. CodeMirror owns
|
|
35
|
+
// that element's `class` attribute: it builds one string from `cm-editor`,
|
|
36
|
+
// `cm-focused`, the theme classes and every `editorAttributes` value, then
|
|
37
|
+
// writes it with `setAttribute`, which drops anything added from outside. A
|
|
38
|
+
// class set here would survive until the next focus, blur or theme change.
|
|
39
|
+
const scrolledX = ref(false)
|
|
40
|
+
|
|
41
|
+
let mounted: EditorView | null = null
|
|
42
|
+
let observer: ResizeObserver | null = null
|
|
43
|
+
// Last value written to the DOM. `measure` runs inside a ResizeObserver
|
|
44
|
+
// callback, so writing a style it already holds is how an observer loop starts.
|
|
45
|
+
let textHeight = ''
|
|
46
|
+
|
|
47
|
+
function measure() {
|
|
48
|
+
const view = mounted
|
|
49
|
+
if (!view) return
|
|
50
|
+
|
|
51
|
+
const scroller = view.scrollDOM
|
|
52
|
+
const next = scroller.scrollHeight > scroller.clientHeight + 1
|
|
53
|
+
if (next !== overflowing.value) {
|
|
54
|
+
overflowing.value = next
|
|
55
|
+
emit('overflow', next)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Drives the gutter's horizontal-scroll shadow (see style.css).
|
|
59
|
+
scrolledX.value = scroller.scrollLeft > 0
|
|
60
|
+
|
|
61
|
+
// The shadow is capped to the bottom of the last line rather than the full
|
|
62
|
+
// gutter height: CodeMirror stretches the gutter past the code, and a shadow
|
|
63
|
+
// running down into the empty area below reads as one floating in space.
|
|
64
|
+
const nextTextHeight = `${view.lineBlockAt(view.state.doc.length).bottom}px`
|
|
65
|
+
if (nextTextHeight !== textHeight) {
|
|
66
|
+
textHeight = nextTextHeight
|
|
67
|
+
view.dom.style.setProperty('--_code-text-height', nextTextHeight)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function unmountView() {
|
|
72
|
+
if (!mounted) return
|
|
73
|
+
mounted.scrollDOM.removeEventListener('scroll', measure)
|
|
74
|
+
mounted.dom.remove()
|
|
75
|
+
mounted = null
|
|
76
|
+
// The observer holds the departed view's `contentDOM` and `scrollDOM`. It has
|
|
77
|
+
// to go here rather than next to the re-observe below, because the part can
|
|
78
|
+
// lose its view without gaining another one (`:editor="null"`, or the engine
|
|
79
|
+
// destroying the view on unmount).
|
|
80
|
+
observer?.disconnect()
|
|
81
|
+
observer = null
|
|
82
|
+
// The cached value belongs to the view that just left, not to the next one.
|
|
83
|
+
textHeight = ''
|
|
84
|
+
// An empty box scrolls nowhere.
|
|
85
|
+
scrolledX.value = false
|
|
86
|
+
// An empty box overflows nothing. Without this the part keeps
|
|
87
|
+
// `data-overflowing` set, and the consumer that drew an expand affordance off
|
|
88
|
+
// the last `true` never hears it go away.
|
|
89
|
+
if (overflowing.value) {
|
|
90
|
+
overflowing.value = false
|
|
91
|
+
emit('overflow', false)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function mountView(view: EditorView | null) {
|
|
96
|
+
const el = rootEl.value
|
|
97
|
+
if (!el || mounted === view) return
|
|
98
|
+
unmountView()
|
|
99
|
+
if (!view) return
|
|
100
|
+
|
|
101
|
+
// The view is created with no `parent`, so `view.dom` is detached until a
|
|
102
|
+
// part appends it. This is the append. The part never destroys the view: the
|
|
103
|
+
// engine owns that, and a part that outlived its editor would take it down.
|
|
104
|
+
el.appendChild(view.dom)
|
|
105
|
+
mounted = view
|
|
106
|
+
view.scrollDOM.addEventListener('scroll', measure, { passive: true })
|
|
107
|
+
|
|
108
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
109
|
+
// `contentDOM` catches doc edits and folds; `scrollDOM` catches the cap
|
|
110
|
+
// itself changing and wrapping reflow on a width change. Neither alone
|
|
111
|
+
// covers both: a capped scroller keeps its box size while the content
|
|
112
|
+
// height shifts under it.
|
|
113
|
+
observer = new ResizeObserver(() => measure())
|
|
114
|
+
observer.observe(view.contentDOM)
|
|
115
|
+
observer.observe(view.scrollDOM)
|
|
116
|
+
}
|
|
117
|
+
measure()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
onMounted(() => mountView(resolved.value))
|
|
121
|
+
watch(resolved, async (view) => {
|
|
122
|
+
await nextTick()
|
|
123
|
+
mountView(view)
|
|
124
|
+
})
|
|
125
|
+
onBeforeUnmount(() => unmountView())
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<template>
|
|
129
|
+
<!-- The box. `class` falls through the normal way (P10: no class-name prop) —
|
|
130
|
+
unlike the editor family's content part, this root is ours, not the
|
|
131
|
+
engine's, so nothing else writes to its class attribute. -->
|
|
132
|
+
<div
|
|
133
|
+
ref="rootEl"
|
|
134
|
+
data-slot="code-editor-content"
|
|
135
|
+
:data-overflowing="overflowing ? 'true' : undefined"
|
|
136
|
+
:data-scrolled-x="scrolledX ? 'true' : undefined"
|
|
137
|
+
/>
|
|
138
|
+
</template>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
inject,
|
|
4
|
+
provide,
|
|
5
|
+
type ComputedRef,
|
|
6
|
+
type InjectionKey,
|
|
7
|
+
type Ref,
|
|
8
|
+
} from 'vue'
|
|
9
|
+
import type { EditorView } from '@codemirror/view'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Code editor context.
|
|
13
|
+
*
|
|
14
|
+
* `<CodeEditor>` provides its live `EditorView` ref through this key so the
|
|
15
|
+
* parts rendered in its slot (`CodeEditorContent`) can pull it via `inject`
|
|
16
|
+
* instead of having `:editor` threaded down by hand. An explicit `:editor`
|
|
17
|
+
* prop always wins, which is what keeps the parts usable WITHOUT
|
|
18
|
+
* `<CodeEditor>` (composed directly on a `useCodeEditor` ref).
|
|
19
|
+
*
|
|
20
|
+
* The shape mirrors `frappe-ui/editor`'s `editor-context.ts`. The two families
|
|
21
|
+
* keep separate keys: a code editor nested inside a rich-text editor must not
|
|
22
|
+
* inject the TipTap instance, and vice versa.
|
|
23
|
+
*/
|
|
24
|
+
export const CodeEditorContextKey: InjectionKey<Ref<EditorView | null>> =
|
|
25
|
+
Symbol('frappe-ui-code-editor-context')
|
|
26
|
+
|
|
27
|
+
/** Publish a view ref to descendant parts. Called by `<CodeEditor>`. */
|
|
28
|
+
export function provideCodeEditor(editor: Ref<EditorView | null>): void {
|
|
29
|
+
provide(CodeEditorContextKey, editor)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the view a part should use: the explicit `:editor` prop when one was
|
|
34
|
+
* passed (even `null`), otherwise the injected context. Returns a reactive
|
|
35
|
+
* `ComputedRef` so the part tracks the view coming online.
|
|
36
|
+
*/
|
|
37
|
+
export function useResolvedCodeEditor(
|
|
38
|
+
getProp: () => EditorView | null | undefined,
|
|
39
|
+
): ComputedRef<EditorView | null> {
|
|
40
|
+
const injected = inject(CodeEditorContextKey, null)
|
|
41
|
+
return computed(() => {
|
|
42
|
+
const fromProp = getProp()
|
|
43
|
+
if (fromProp !== undefined) return fromProp
|
|
44
|
+
return injected?.value ?? null
|
|
45
|
+
})
|
|
46
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'
|
|
2
|
+
import { indentWithTab } from '@codemirror/commands'
|
|
3
|
+
import { searchPanelOpen } from '@codemirror/search'
|
|
4
|
+
import { Prec, type Extension } from '@codemirror/state'
|
|
5
|
+
import { EditorView, keymap } from '@codemirror/view'
|
|
6
|
+
import { tags } from '@lezer/highlight'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The class every frappe rule in `style.css` is scoped under. Nothing about the
|
|
10
|
+
* frappe look is ambient: without `codeChrome` in the extension array the class
|
|
11
|
+
* is absent and none of those rules match.
|
|
12
|
+
*/
|
|
13
|
+
export const CODE_CHROME_CLASS = 'frappe-code'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Layer 2 of the styling model: the frappe box. Adds one class to the editor's
|
|
17
|
+
* root, and the package stylesheet does the rest.
|
|
18
|
+
*
|
|
19
|
+
* The styling lives in plain CSS rather than in an `EditorView.theme`, because
|
|
20
|
+
* a theme is a StyleModule whose injection order decides equal-specificity
|
|
21
|
+
* fights. The old field lost that fight often enough to leave a comment about
|
|
22
|
+
* it. A CSS custom property read at one use site has no ordering problem, so
|
|
23
|
+
* every knob here is a `--code-*` variable a consumer sets on the editor or on
|
|
24
|
+
* any ancestor.
|
|
25
|
+
*/
|
|
26
|
+
export const codeChrome: Extension = EditorView.editorAttributes.of({
|
|
27
|
+
class: CODE_CHROME_CLASS,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Layer 3: frappe syntax colours.
|
|
32
|
+
*
|
|
33
|
+
* Syntax highlighting needs an extension in any case. CodeMirror maps Lezer
|
|
34
|
+
* tags to classes through `syntaxHighlighting(HighlightStyle.define(...))`, and
|
|
35
|
+
* plain CSS cannot do that mapping.
|
|
36
|
+
*
|
|
37
|
+
* Every colour is an `--ink-*` token, and those tokens re-map under the app's
|
|
38
|
+
* colour scheme — so the palette follows dark mode with no second theme and no
|
|
39
|
+
* `Compartment`. `Prec.highest` makes these win over a default highlight style
|
|
40
|
+
* a consumer also loaded, which then only fills the tags this one omits.
|
|
41
|
+
*/
|
|
42
|
+
export const codeHighlight: Extension = Prec.highest(
|
|
43
|
+
syntaxHighlighting(
|
|
44
|
+
HighlightStyle.define([
|
|
45
|
+
{
|
|
46
|
+
tag: [tags.keyword, tags.moduleKeyword, tags.controlKeyword],
|
|
47
|
+
color: 'var(--ink-blue-5)',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
tag: [tags.string, tags.special(tags.string), tags.regexp],
|
|
51
|
+
color: 'var(--ink-green-5)',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
tag: [tags.comment, tags.lineComment, tags.blockComment],
|
|
55
|
+
color: 'var(--ink-gray-5)',
|
|
56
|
+
fontStyle: 'italic',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
tag: [tags.number, tags.bool, tags.null, tags.atom],
|
|
60
|
+
color: 'var(--ink-amber-5)',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
tag: [tags.typeName, tags.className, tags.namespace],
|
|
64
|
+
color: 'var(--ink-amber-5)',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
tag: [
|
|
68
|
+
tags.function(tags.variableName),
|
|
69
|
+
tags.function(tags.propertyName),
|
|
70
|
+
tags.labelName,
|
|
71
|
+
],
|
|
72
|
+
color: 'var(--ink-blue-5)',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
tag: [tags.propertyName, tags.attributeName],
|
|
76
|
+
color: 'var(--ink-red-5)',
|
|
77
|
+
},
|
|
78
|
+
{ tag: [tags.tagName, tags.angleBracket], color: 'var(--ink-red-5)' },
|
|
79
|
+
{
|
|
80
|
+
tag: [tags.operator, tags.punctuation, tags.separator, tags.bracket],
|
|
81
|
+
color: 'var(--ink-gray-6)',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
tag: [tags.variableName, tags.definition(tags.variableName)],
|
|
85
|
+
color: 'var(--ink-gray-8)',
|
|
86
|
+
},
|
|
87
|
+
{ tag: tags.meta, color: 'var(--ink-gray-5)' },
|
|
88
|
+
{
|
|
89
|
+
tag: [tags.link, tags.url],
|
|
90
|
+
color: 'var(--ink-blue-5)',
|
|
91
|
+
textDecoration: 'underline',
|
|
92
|
+
},
|
|
93
|
+
{ tag: tags.heading, color: 'var(--ink-blue-5)', fontWeight: 'bold' },
|
|
94
|
+
{ tag: tags.strong, fontWeight: 'bold' },
|
|
95
|
+
{ tag: tags.emphasis, fontStyle: 'italic' },
|
|
96
|
+
{ tag: tags.strikethrough, textDecoration: 'line-through' },
|
|
97
|
+
{ tag: tags.invalid, color: 'var(--ink-red-7)' },
|
|
98
|
+
]),
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The frappe keymap: Tab and Shift-Tab indent and dedent, Escape blurs.
|
|
104
|
+
*
|
|
105
|
+
* Escape is a WCAG 2.1.2 obligation, not a nicety. `indentWithTab` takes Tab
|
|
106
|
+
* away from the browser's focus-move, so without an escape hatch a keyboard
|
|
107
|
+
* user who tabs into the editor cannot tab out. Esc-then-Tab is CodeMirror's
|
|
108
|
+
* standard way out of that trap.
|
|
109
|
+
*
|
|
110
|
+
* `Prec.high` puts both bindings above the default keymaps, so a kit member can
|
|
111
|
+
* never shadow them by landing earlier in the array.
|
|
112
|
+
*
|
|
113
|
+
* That precedence is why the blur is conditional. `searchKeymap` declares
|
|
114
|
+
* `{key: 'Escape', run: closeSearchPanel, scope: 'editor search-panel'}` —
|
|
115
|
+
* two scopes, and CodeMirror registers the binding under both. In the `editor`
|
|
116
|
+
* scope this one runs first, so returning `true` with a panel open would blur
|
|
117
|
+
* the editor, leave the panel up, and fire the blur commit as a side effect.
|
|
118
|
+
* Falling through hands Escape back to `closeSearchPanel`, and the next Escape
|
|
119
|
+
* blurs.
|
|
120
|
+
*/
|
|
121
|
+
export const codeKeymap: Extension = Prec.high(
|
|
122
|
+
keymap.of([
|
|
123
|
+
indentWithTab,
|
|
124
|
+
{
|
|
125
|
+
key: 'Escape',
|
|
126
|
+
run: (view) => {
|
|
127
|
+
if (searchPanelOpen(view.state)) return false
|
|
128
|
+
view.contentDOM.blur()
|
|
129
|
+
return true
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
]),
|
|
133
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Code editor styles ship with the package: importing from
|
|
2
|
+
// `frappe-ui/code-editor` pulls in the frappe chrome rules so the family is
|
|
3
|
+
// self-contained. The rules are scoped under the class `codeChrome` adds, so
|
|
4
|
+
// loading this stylesheet still styles nothing until that extension is in the
|
|
5
|
+
// extension array.
|
|
6
|
+
import './style.css'
|
|
7
|
+
|
|
8
|
+
// Engine
|
|
9
|
+
export { useCodeEditor, type CodeEditorOptions } from './useCodeEditor'
|
|
10
|
+
|
|
11
|
+
// The component, and the one part. There are no menu parts: CodeMirror renders
|
|
12
|
+
// its own gutters, panels, tooltips and completion popups inside `view.dom`.
|
|
13
|
+
export { default as CodeEditor } from './CodeEditor.vue'
|
|
14
|
+
export { default as CodeEditorContent } from './CodeEditorContent.vue'
|
|
15
|
+
|
|
16
|
+
// Kit — one configurable extension bundle
|
|
17
|
+
export { CodeKit, type CodeKitOptions, type CodeKitExtension } from './kit'
|
|
18
|
+
|
|
19
|
+
// Individual extensions (each also a CodeKit member). No placeholder wrapper:
|
|
20
|
+
// `@codemirror/view` already exports `placeholder()`, so one would add a name
|
|
21
|
+
// frappe-ui owns until 2.0.0 and nothing else.
|
|
22
|
+
//
|
|
23
|
+
// The list is written out rather than re-exported with `export *`: a wildcard
|
|
24
|
+
// from an implementation module publishes whatever that file exports next
|
|
25
|
+
// (PHILOSOPHY.md, P15).
|
|
26
|
+
export { codeChrome, codeHighlight, codeKeymap } from './extensions'
|
|
27
|
+
|
|
28
|
+
// Languages
|
|
29
|
+
export { loadLanguage, type LanguageKey } from './languages'
|
|
30
|
+
|
|
31
|
+
// Types
|
|
32
|
+
export type { CodeEditorExposed } from './types'
|