frappe-ui 1.0.0-beta.8 → 1.0.0-beta.9
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 +16 -1
- package/src/components/CodeEditor/CodeEditor.api.md +140 -0
- package/src/components/CodeEditor/CodeEditor.cy.ts +156 -0
- package/src/components/CodeEditor/CodeEditor.md +104 -0
- package/src/components/CodeEditor/CodeEditor.vue +514 -0
- package/src/components/CodeEditor/CodePreview.test.ts +49 -0
- package/src/components/CodeEditor/CodePreview.vue +45 -0
- package/src/components/CodeEditor/index.ts +17 -0
- package/src/components/CodeEditor/languages.ts +63 -0
- package/src/components/CodeEditor/stories/Default.vue +16 -0
- package/src/components/CodeEditor/stories/Labeling.vue +25 -0
- package/src/components/CodeEditor/stories/MaxHeight.vue +24 -0
- package/src/components/CodeEditor/stories/Preview.vue +36 -0
- package/src/components/CodeEditor/stories/Sizes.vue +15 -0
- package/src/components/CodeEditor/stories/Variants.vue +13 -0
- package/src/components/CodeEditor/theme.ts +218 -0
- package/src/components/CodeEditor/types.ts +69 -0
- package/src/components/Combobox/Combobox.api.md +21 -21
- package/src/components/Combobox/Combobox.cy.ts +54 -14
- package/src/components/Combobox/Combobox.vue +230 -241
- package/src/components/Combobox/stories/Footer.vue +10 -2
- package/src/components/Combobox/types.ts +8 -29
- package/src/components/Duration/Duration.api.md +92 -0
- package/src/components/Duration/Duration.cy.ts +183 -0
- package/src/components/Duration/Duration.md +98 -0
- package/src/components/Duration/Duration.vue +114 -0
- package/src/components/Duration/duration.ts +185 -0
- package/src/components/Duration/index.ts +8 -0
- package/src/components/Duration/stories/Default.vue +10 -0
- package/src/components/Duration/stories/Formats.vue +16 -0
- package/src/components/Duration/stories/RealWorld.vue +50 -0
- package/src/components/Duration/stories/Sizes.vue +15 -0
- package/src/components/Duration/stories/States.vue +21 -0
- package/src/components/Duration/types.ts +52 -0
- package/src/components/MultiSelect/MultiSelect.api.md +9 -9
- package/src/components/MultiSelect/MultiSelect.md +38 -10
- package/src/components/MultiSelect/MultiSelect.vue +199 -226
- package/src/components/MultiSelect/stories/TagsTrigger.vue +2 -2
- package/src/components/MultiSelect/types.ts +3 -3
- package/src/components/TimePicker/TimePicker.api.md +59 -13
- package/src/components/TimePicker/TimePicker.md +6 -0
- package/src/components/TimePicker/TimePicker.vue +16 -0
- package/src/components/TimePicker/stories/Labeling.vue +21 -0
- package/src/components/TimePicker/stories/SizesAndVariants.vue +23 -0
- package/src/components/TimePicker/types.ts +7 -1
- package/src/index.ts +1 -0
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.9",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -56,6 +56,10 @@
|
|
|
56
56
|
"import": "./src/molecules/editor/index.ts",
|
|
57
57
|
"types": "./src/molecules/editor/index.ts"
|
|
58
58
|
},
|
|
59
|
+
"./code-editor": {
|
|
60
|
+
"import": "./src/components/CodeEditor/index.ts",
|
|
61
|
+
"types": "./src/components/CodeEditor/index.ts"
|
|
62
|
+
},
|
|
59
63
|
"./drive": {
|
|
60
64
|
"import": "./frappe/drive/index.js"
|
|
61
65
|
},
|
|
@@ -104,6 +108,16 @@
|
|
|
104
108
|
"author": "Frappe Technologies Pvt. Ltd.",
|
|
105
109
|
"license": "MIT",
|
|
106
110
|
"dependencies": {
|
|
111
|
+
"@codemirror/lang-css": "^6.3.0",
|
|
112
|
+
"@codemirror/lang-html": "^6.4.9",
|
|
113
|
+
"@codemirror/lang-javascript": "^6.2.2",
|
|
114
|
+
"@codemirror/lang-json": "^6.0.1",
|
|
115
|
+
"@codemirror/lang-markdown": "^6.3.1",
|
|
116
|
+
"@codemirror/lang-python": "^6.1.6",
|
|
117
|
+
"@codemirror/lang-sass": "^6.0.2",
|
|
118
|
+
"@codemirror/lang-sql": "^6.8.0",
|
|
119
|
+
"@codemirror/lang-xml": "^6.1.0",
|
|
120
|
+
"@codemirror/lang-yaml": "^6.1.3",
|
|
107
121
|
"@floating-ui/dom": "^1.7.4",
|
|
108
122
|
"@headlessui/vue": "^1.7.14",
|
|
109
123
|
"@popperjs/core": "^2.11.2",
|
|
@@ -146,6 +160,7 @@
|
|
|
146
160
|
"@tiptap/suggestion": "^3.26.0",
|
|
147
161
|
"@tiptap/vue-3": "^3.26.0",
|
|
148
162
|
"@vueuse/core": "^10.4.1",
|
|
163
|
+
"codemirror": "^6.0.1",
|
|
149
164
|
"dayjs": "^1.11.13",
|
|
150
165
|
"dompurify": "^3.4.0",
|
|
151
166
|
"echarts": "^5.6.0",
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<!-- Auto Generated by scripts/propsgen.ts -->
|
|
2
|
+
<script setup>
|
|
3
|
+
import PropsTable from '@/components/Docs/PropsTable.vue'
|
|
4
|
+
import SlotsTable from '@/components/Docs/SlotsTable.vue'
|
|
5
|
+
import EmitsTable from '@/components/Docs/EmitsTable.vue'
|
|
6
|
+
|
|
7
|
+
const codeEditorProps = [
|
|
8
|
+
{
|
|
9
|
+
name: 'modelValue',
|
|
10
|
+
description: 'The editor\'s text content (controlled, two-way via `v-model`).',
|
|
11
|
+
required: true,
|
|
12
|
+
type: 'string'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'language',
|
|
16
|
+
description: 'CodeMirror language key; falls through to plain text when unset/unknown.',
|
|
17
|
+
required: false,
|
|
18
|
+
type: 'CodeLanguage | (string & {})',
|
|
19
|
+
default: '"plain"'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'disabled',
|
|
23
|
+
description: 'If true, the editor is disabled: greyed out and not editable.',
|
|
24
|
+
required: false,
|
|
25
|
+
type: 'boolean',
|
|
26
|
+
default: 'false'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'placeholder',
|
|
30
|
+
description: 'Placeholder shown when the editor is empty.',
|
|
31
|
+
required: false,
|
|
32
|
+
type: 'string',
|
|
33
|
+
default: '""'
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'variant',
|
|
37
|
+
description: 'Surface style; mirrors frappe-ui inputs. Defaults to `subtle`.',
|
|
38
|
+
required: false,
|
|
39
|
+
type: "Exclude<InputVariant, 'ghost'>",
|
|
40
|
+
default: '"subtle"'
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'size',
|
|
44
|
+
description: 'Size token, mirroring frappe-ui inputs. Scales the editor\'s font size and\nminimum height (`sm | md | lg | xl`). Defaults to `md`.',
|
|
45
|
+
required: false,
|
|
46
|
+
type: 'InputSize',
|
|
47
|
+
default: '"md"'
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'label',
|
|
51
|
+
description: 'Label rendered above (or beside, for binary controls) the input.',
|
|
52
|
+
required: false,
|
|
53
|
+
type: 'string'
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'description',
|
|
57
|
+
description: 'Helper text rendered below the input.\nHidden when `error` is set.',
|
|
58
|
+
required: false,
|
|
59
|
+
type: 'string'
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'error',
|
|
63
|
+
description: 'Error message rendered below the input. When set, the control receives\n`aria-invalid="true"` and `data-state="invalid"`. May be either a string\nor an `Error` object whose `messages?: string[]` is rendered as stacked\nlines (with `Error.message` as the fallback).',
|
|
64
|
+
required: false,
|
|
65
|
+
type: 'string | FrappeUIError'
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'required',
|
|
69
|
+
description: 'Marks the field as required. Renders an asterisk next to the label and\nforwards `required` / `aria-required` to the underlying control.',
|
|
70
|
+
required: false,
|
|
71
|
+
type: 'boolean'
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'id',
|
|
75
|
+
description: 'HTML id of the underlying control. Auto-generated via `useId()` if omitted.',
|
|
76
|
+
required: false,
|
|
77
|
+
type: 'string'
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
const codeEditorSlots = [
|
|
82
|
+
{
|
|
83
|
+
name: 'label',
|
|
84
|
+
description: 'Overrides the rendered label content. Receives `{ required }`.',
|
|
85
|
+
type: '{ required: boolean; }'
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'description',
|
|
89
|
+
description: 'Overrides the rendered description content.',
|
|
90
|
+
type: 'any'
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
const codeEditorEmits = [
|
|
95
|
+
{
|
|
96
|
+
name: 'update:modelValue',
|
|
97
|
+
description: 'Fired when the model value changes.',
|
|
98
|
+
type: '[value: string]'
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'change',
|
|
102
|
+
description: 'Fired after the value is committed.',
|
|
103
|
+
type: '[value: string]'
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'overflow',
|
|
107
|
+
description: 'Fired when content crosses the `--cm-max-height` cap (transitions only). The cap is a CSS hook, not a prop — set `--cm-max-height` on the root.',
|
|
108
|
+
type: '[overflowing: boolean]'
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
const codePreviewProps = [
|
|
113
|
+
{
|
|
114
|
+
name: 'modelValue',
|
|
115
|
+
description: 'The source text to render as sanitized preview output.',
|
|
116
|
+
required: true,
|
|
117
|
+
type: 'string'
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'language',
|
|
121
|
+
description: 'Only `markdown` / `html` render a preview; anything else renders nothing.',
|
|
122
|
+
required: false,
|
|
123
|
+
type: 'CodeLanguage | (string & {})'
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
</script>
|
|
127
|
+
## API Reference
|
|
128
|
+
|
|
129
|
+
### CodeEditor
|
|
130
|
+
|
|
131
|
+
<PropsTable name="CodeEditor" :data="codeEditorProps"/>
|
|
132
|
+
|
|
133
|
+
<SlotsTable :data="codeEditorSlots"/>
|
|
134
|
+
|
|
135
|
+
<EmitsTable :data="codeEditorEmits"/>
|
|
136
|
+
|
|
137
|
+
### CodePreview
|
|
138
|
+
|
|
139
|
+
<PropsTable folder="CodeEditor" name="CodePreview" :data="codePreviewProps"/>
|
|
140
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import CodeEditor from './CodeEditor.vue'
|
|
2
|
+
|
|
3
|
+
// CodeMirror lazy-loads in `onMounted` (dynamic imports) and renders into a
|
|
4
|
+
// `.cm-editor` with a contenteditable `.cm-content`. Cypress retries `cy.get`
|
|
5
|
+
// until that async mount settles, so no explicit wait is needed.
|
|
6
|
+
|
|
7
|
+
describe('CodeEditor', () => {
|
|
8
|
+
it('mounts the editor with the initial document', () => {
|
|
9
|
+
cy.mount(CodeEditor, { props: { modelValue: 'const a = 1' } })
|
|
10
|
+
cy.get('.cm-editor').should('exist')
|
|
11
|
+
cy.get('.cm-content').should('contain.text', 'const a = 1')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('emits update:modelValue live while typing', () => {
|
|
15
|
+
cy.mount(CodeEditor, {
|
|
16
|
+
props: {
|
|
17
|
+
modelValue: '',
|
|
18
|
+
'onUpdate:modelValue': cy.stub().as('update'),
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
cy.get('.cm-content').type('hello')
|
|
22
|
+
cy.get('@update').should('have.been.called')
|
|
23
|
+
// The last emit carries the full document.
|
|
24
|
+
cy.get('@update')
|
|
25
|
+
.its('lastCall.args.0')
|
|
26
|
+
.should('eq', 'hello')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('emits change only on blur (commit), not on every keystroke', () => {
|
|
30
|
+
cy.mount(CodeEditor, {
|
|
31
|
+
props: {
|
|
32
|
+
modelValue: '',
|
|
33
|
+
onChange: cy.stub().as('change'),
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
cy.get('.cm-content').type('draft')
|
|
37
|
+
// No commit while focused.
|
|
38
|
+
cy.get('@change').should('not.have.been.called')
|
|
39
|
+
cy.get('.cm-content').blur()
|
|
40
|
+
cy.get('@change').should('have.been.calledOnceWith', 'draft')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('Escape blurs the editor so the keyboard trap has an exit (commits)', () => {
|
|
44
|
+
cy.mount(CodeEditor, {
|
|
45
|
+
props: { modelValue: '', onChange: cy.stub().as('change') },
|
|
46
|
+
})
|
|
47
|
+
cy.get('.cm-content').type('x')
|
|
48
|
+
cy.get('.cm-content').trigger('keydown', { key: 'Escape' })
|
|
49
|
+
cy.get('@change').should('have.been.calledOnceWith', 'x')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('does not emit update:modelValue when synced from the prop', () => {
|
|
53
|
+
// Updating modelValue externally must not echo back out as fresh input.
|
|
54
|
+
cy.mount(CodeEditor, {
|
|
55
|
+
props: {
|
|
56
|
+
modelValue: 'one',
|
|
57
|
+
'onUpdate:modelValue': cy.stub().as('update'),
|
|
58
|
+
},
|
|
59
|
+
}).then(({ wrapper }) => {
|
|
60
|
+
wrapper.setProps({ modelValue: 'one two three' })
|
|
61
|
+
cy.get('.cm-content').should('contain.text', 'one two three')
|
|
62
|
+
cy.get('@update').should('not.have.been.called')
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('is not editable when disabled', () => {
|
|
67
|
+
cy.mount(CodeEditor, {
|
|
68
|
+
props: { modelValue: 'frozen', disabled: true },
|
|
69
|
+
})
|
|
70
|
+
cy.get('.cm-content').should('have.attr', 'contenteditable', 'false')
|
|
71
|
+
cy.get('.code-editor').should('have.attr', 'data-disabled', 'true')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('swaps disabled at runtime without recreating the view', () => {
|
|
75
|
+
cy.mount(CodeEditor, {
|
|
76
|
+
props: { modelValue: 'frozen', disabled: true },
|
|
77
|
+
}).then(({ wrapper }) => {
|
|
78
|
+
cy.get('.cm-content').should('have.attr', 'contenteditable', 'false')
|
|
79
|
+
wrapper.setProps({ disabled: false })
|
|
80
|
+
cy.get('.cm-content').should('have.attr', 'contenteditable', 'true')
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('exposes the data-* styling hooks on the control', () => {
|
|
85
|
+
cy.mount(CodeEditor, {
|
|
86
|
+
props: { modelValue: '', variant: 'outline', size: 'lg' },
|
|
87
|
+
})
|
|
88
|
+
cy.get('.code-editor').should('have.attr', 'data-slot', 'control')
|
|
89
|
+
cy.get('.code-editor').should('have.attr', 'data-variant', 'outline')
|
|
90
|
+
cy.get('.code-editor').should('have.attr', 'data-size', 'lg')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('labeling contract', () => {
|
|
94
|
+
it('renders the label and wires aria onto the contentDOM', () => {
|
|
95
|
+
cy.mount(CodeEditor, {
|
|
96
|
+
props: {
|
|
97
|
+
modelValue: '',
|
|
98
|
+
label: 'Config',
|
|
99
|
+
description: 'JSON config',
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
cy.contains('label', 'Config').should('exist')
|
|
103
|
+
cy.get('.cm-content').then(($el) => {
|
|
104
|
+
const describedBy = $el.attr('aria-describedby')
|
|
105
|
+
expect(describedBy).to.be.ok
|
|
106
|
+
cy.get(`#${describedBy}`).should('contain.text', 'JSON config')
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('marks the contentDOM invalid and shows the error', () => {
|
|
111
|
+
cy.mount(CodeEditor, {
|
|
112
|
+
props: { modelValue: '', error: 'Invalid JSON' },
|
|
113
|
+
})
|
|
114
|
+
cy.get('.cm-content').should('have.attr', 'aria-invalid', 'true')
|
|
115
|
+
cy.get('.code-editor').should('have.attr', 'data-state', 'invalid')
|
|
116
|
+
cy.contains('Invalid JSON').should('exist')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('forwards required onto aria-required', () => {
|
|
120
|
+
cy.mount(CodeEditor, {
|
|
121
|
+
props: { modelValue: '', label: 'Body', required: true },
|
|
122
|
+
})
|
|
123
|
+
cy.get('.cm-content').should('have.attr', 'aria-required', 'true')
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('overflow', () => {
|
|
128
|
+
// The cap is a CSS hook (`--cm-max-height` on the root), not a prop — set it
|
|
129
|
+
// via attribute fallthrough, the same way a consumer would.
|
|
130
|
+
const longDoc = Array.from({ length: 40 }, (_, i) => `line ${i}`).join('\n')
|
|
131
|
+
|
|
132
|
+
it('emits overflow=true when content exceeds the --cm-max-height cap', () => {
|
|
133
|
+
cy.mount(CodeEditor, {
|
|
134
|
+
props: {
|
|
135
|
+
modelValue: longDoc,
|
|
136
|
+
onOverflow: cy.stub().as('overflow'),
|
|
137
|
+
},
|
|
138
|
+
attrs: { style: '--cm-max-height: 6rem' },
|
|
139
|
+
})
|
|
140
|
+
cy.get('@overflow').should('have.been.calledWith', true)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('does not emit overflow when content fits under the cap', () => {
|
|
144
|
+
cy.mount(CodeEditor, {
|
|
145
|
+
props: {
|
|
146
|
+
modelValue: 'short',
|
|
147
|
+
onOverflow: cy.stub().as('overflow'),
|
|
148
|
+
},
|
|
149
|
+
attrs: { style: '--cm-max-height: 20rem' },
|
|
150
|
+
})
|
|
151
|
+
// Give the ResizeObserver/mount a beat, then assert it stayed quiet.
|
|
152
|
+
cy.get('.cm-editor').should('exist')
|
|
153
|
+
cy.get('@overflow').should('not.have.been.calledWith', true)
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# CodeEditor
|
|
2
|
+
|
|
3
|
+
A CodeMirror 6 code field with syntax highlighting, theming that follows the
|
|
4
|
+
app's light/dark scheme, and an optional sanitized preview. CodeMirror is
|
|
5
|
+
lazy-loaded, so apps that never render a code field pay no runtime cost.
|
|
6
|
+
|
|
7
|
+
CodeEditor ships from its own subpath so the CodeMirror dependency only loads
|
|
8
|
+
for apps that opt in:
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
import { CodeEditor, CodePreview } from 'frappe-ui/code-editor'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Playground
|
|
15
|
+
|
|
16
|
+
<ClientOnly><CodeEditorBuilder /></ClientOnly>
|
|
17
|
+
|
|
18
|
+
<ComponentPreview name="CodeEditor-Default" layout="stacked" class="mt-4" />
|
|
19
|
+
|
|
20
|
+
## Languages
|
|
21
|
+
|
|
22
|
+
Pass a `language` key to enable highlighting. Each language tree-shakes into
|
|
23
|
+
its own async chunk, loaded on demand. Supported keys: `json`, `html`,
|
|
24
|
+
`javascript`, `python`, `sql`, `markdown`, `css`, `scss`, `yaml`, `xml`, and
|
|
25
|
+
`plain` (the default — no highlighting). Unknown keys fall through to plain
|
|
26
|
+
text. `json` additionally wires an inline lint gutter so invalid JSON is
|
|
27
|
+
flagged at its position.
|
|
28
|
+
|
|
29
|
+
## Variants
|
|
30
|
+
|
|
31
|
+
The surface mirrors frappe-ui's input convention so a code field sits flush
|
|
32
|
+
with the TextInput/Textarea fields around it. `subtle` is the filled default and
|
|
33
|
+
`outline` is a bordered box on white. The borderless `ghost` variant is
|
|
34
|
+
intentionally not offered — a code surface without a border reads as plain text
|
|
35
|
+
and loses the affordance that it's an editable field.
|
|
36
|
+
|
|
37
|
+
<ComponentPreview name="CodeEditor-Variants" />
|
|
38
|
+
|
|
39
|
+
## Sizes
|
|
40
|
+
|
|
41
|
+
`size` mirrors the frappe-ui input sizes (`sm | md | lg | xl`, default `md`),
|
|
42
|
+
scaling the editor's font size and minimum height.
|
|
43
|
+
|
|
44
|
+
<ComponentPreview name="CodeEditor-Sizes" />
|
|
45
|
+
|
|
46
|
+
## Max height
|
|
47
|
+
|
|
48
|
+
The height cap is a CSS hook, not a prop — set the `--cm-max-height` custom
|
|
49
|
+
property on the root to any CSS length and content past the cap scrolls
|
|
50
|
+
internally:
|
|
51
|
+
|
|
52
|
+
```vue
|
|
53
|
+
<CodeEditor v-model="code" style="--cm-max-height: 12rem" />
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Pair it with the `overflow` emit (fired only when the content crosses the cap)
|
|
57
|
+
to drive an expand/collapse affordance — that's the one piece you can't measure
|
|
58
|
+
from CSS yourself.
|
|
59
|
+
|
|
60
|
+
<ComponentPreview name="CodeEditor-MaxHeight" />
|
|
61
|
+
|
|
62
|
+
## Preview
|
|
63
|
+
|
|
64
|
+
`CodePreview` is a separate, display-only primitive that renders sanitized
|
|
65
|
+
output for the two languages with a meaningful preview — `markdown` (rendered
|
|
66
|
+
via `marked`) and `html`. Both are passed through DOMPurify before rendering;
|
|
67
|
+
any other language renders nothing. The writer and preview are independent, so
|
|
68
|
+
how you compose them — editor-only, a Write/Preview toggle, or a side-by-side
|
|
69
|
+
split — is up to the consumer.
|
|
70
|
+
|
|
71
|
+
<ComponentPreview name="CodeEditor-Preview" />
|
|
72
|
+
|
|
73
|
+
## Labeling
|
|
74
|
+
|
|
75
|
+
CodeEditor implements the shared input labeling contract (`label`,
|
|
76
|
+
`description`, `error`, `required`). When something needs the chrome it renders
|
|
77
|
+
a wrapping field; otherwise the editor mounts bare, so the primitive's
|
|
78
|
+
footprint is unchanged. Setting `error` flips the field to its invalid state
|
|
79
|
+
(`data-state="invalid"`, `aria-invalid`) and renders the message below; the
|
|
80
|
+
ARIA wiring is applied to the editor's internal content element so it reaches
|
|
81
|
+
the focusable control.
|
|
82
|
+
|
|
83
|
+
<ComponentPreview name="CodeEditor-Labeling" />
|
|
84
|
+
|
|
85
|
+
## Keyboard
|
|
86
|
+
|
|
87
|
+
| Keys | Action |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `Tab` / `Shift`+`Tab` | Indent / dedent the current line or selection |
|
|
90
|
+
| `Escape` | Release focus so `Tab` moves to the next field |
|
|
91
|
+
|
|
92
|
+
`Tab` is rebound to indent (the editor default is a focus-move). `Escape`
|
|
93
|
+
blurs the editor so keyboard users can still tab on — the standard CodeMirror
|
|
94
|
+
keyboard-trap escape hatch (WCAG 2.1.2).
|
|
95
|
+
|
|
96
|
+
## Commit contract
|
|
97
|
+
|
|
98
|
+
`update:modelValue` fires live on every change (mirrors the textarea field
|
|
99
|
+
contract); `change` fires on blur, the commit point where a wrapper would
|
|
100
|
+
normalize the value (e.g. JSON pretty-print). External `modelValue` changes
|
|
101
|
+
are diffed into the view so the caret, selection, and scroll position survive
|
|
102
|
+
a live transform.
|
|
103
|
+
|
|
104
|
+
<!-- @include: ./CodeEditor.api.md -->
|