@prsm/mono-components 0.1.0

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.
Files changed (90) hide show
  1. package/.claude/settings.local.json +13 -0
  2. package/.lore +83 -0
  3. package/histoire.config.js +43 -0
  4. package/package.json +39 -0
  5. package/postcss.config.js +6 -0
  6. package/src/components/Badge.vue +36 -0
  7. package/src/components/Button.vue +44 -0
  8. package/src/components/Checkbox.vue +51 -0
  9. package/src/components/CheckboxCards.vue +61 -0
  10. package/src/components/CodeEditor.vue +299 -0
  11. package/src/components/Collapsible.vue +69 -0
  12. package/src/components/CollapsibleGroup.vue +38 -0
  13. package/src/components/Combobox.vue +179 -0
  14. package/src/components/ContextMenu.vue +65 -0
  15. package/src/components/ContextMenuPanel.vue +115 -0
  16. package/src/components/DataTable.vue +326 -0
  17. package/src/components/Dropdown.vue +127 -0
  18. package/src/components/GhostInput.vue +29 -0
  19. package/src/components/Input.vue +23 -0
  20. package/src/components/KeyValue.vue +149 -0
  21. package/src/components/LabeledTextarea.vue +64 -0
  22. package/src/components/LabeledTextareaGroup.vue +14 -0
  23. package/src/components/Mention.vue +79 -0
  24. package/src/components/Modal.vue +109 -0
  25. package/src/components/MultiCombobox.vue +209 -0
  26. package/src/components/NavTree.vue +98 -0
  27. package/src/components/NumberInput.vue +128 -0
  28. package/src/components/PopConfirm.vue +94 -0
  29. package/src/components/Popover.vue +53 -0
  30. package/src/components/RadioCards.vue +37 -0
  31. package/src/components/RadioGroup.vue +57 -0
  32. package/src/components/RangeSlider.vue +165 -0
  33. package/src/components/ScrollBox.vue +78 -0
  34. package/src/components/SectionHeader.vue +18 -0
  35. package/src/components/Select.vue +187 -0
  36. package/src/components/Switch.vue +85 -0
  37. package/src/components/Tabs.vue +34 -0
  38. package/src/components/TagInput.vue +80 -0
  39. package/src/components/Textarea.vue +97 -0
  40. package/src/components/ToastContainer.vue +104 -0
  41. package/src/components/ToggleButtons.vue +45 -0
  42. package/src/components/ToggleGroup.vue +30 -0
  43. package/src/components/Tooltip.vue +56 -0
  44. package/src/components/Tree.vue +188 -0
  45. package/src/composables/toast.js +54 -0
  46. package/src/composables/useClickOutside.js +23 -0
  47. package/src/composables/useMention.js +291 -0
  48. package/src/composables/usePointerDrag.js +39 -0
  49. package/src/histoire-setup.js +1 -0
  50. package/src/index.js +43 -0
  51. package/src/style.css +96 -0
  52. package/stories/Badge.story.vue +24 -0
  53. package/stories/Button.story.vue +45 -0
  54. package/stories/Checkbox.story.vue +31 -0
  55. package/stories/CheckboxCards.story.vue +51 -0
  56. package/stories/CodeEditor.story.vue +71 -0
  57. package/stories/Collapsible.story.vue +84 -0
  58. package/stories/Combobox.story.vue +44 -0
  59. package/stories/ContextMenu.story.vue +59 -0
  60. package/stories/DataTable.story.vue +185 -0
  61. package/stories/Dropdown.story.vue +49 -0
  62. package/stories/GhostInput.story.vue +24 -0
  63. package/stories/Input.story.vue +23 -0
  64. package/stories/KeyValue.story.vue +104 -0
  65. package/stories/LabeledTextarea.story.vue +44 -0
  66. package/stories/Mention.story.vue +166 -0
  67. package/stories/Modal.story.vue +86 -0
  68. package/stories/MultiCombobox.story.vue +76 -0
  69. package/stories/NavTree.story.vue +184 -0
  70. package/stories/NumberInput.story.vue +31 -0
  71. package/stories/Overview.story.vue +85 -0
  72. package/stories/PopConfirm.story.vue +39 -0
  73. package/stories/RadioCards.story.vue +66 -0
  74. package/stories/RadioGroup.story.vue +52 -0
  75. package/stories/RangeSlider.story.vue +75 -0
  76. package/stories/ScrollBox.story.vue +54 -0
  77. package/stories/SectionHeader.story.vue +22 -0
  78. package/stories/Select.story.vue +34 -0
  79. package/stories/Switch.story.vue +42 -0
  80. package/stories/Tabs.story.vue +34 -0
  81. package/stories/TagInput.story.vue +54 -0
  82. package/stories/Textarea.story.vue +28 -0
  83. package/stories/Toast.story.vue +28 -0
  84. package/stories/ToggleButtons.story.vue +57 -0
  85. package/stories/ToggleGroup.story.vue +34 -0
  86. package/stories/Tooltip.story.vue +55 -0
  87. package/stories/Tree.story.vue +115 -0
  88. package/tailwind.config.js +9 -0
  89. package/tailwind.preset.js +79 -0
  90. package/vite.config.js +6 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebSearch",
5
+ "WebFetch(domain:raw.githubusercontent.com)",
6
+ "WebFetch(domain:github.com)",
7
+ "WebFetch(domain:histoire.dev)",
8
+ "mcp__github__search_code",
9
+ "mcp__github__get_file_contents",
10
+ "Bash(ls:*)"
11
+ ]
12
+ }
13
+ }
package/.lore ADDED
@@ -0,0 +1,83 @@
1
+ -- philosophy --
2
+
3
+ use these components when they fit naturally. if a component is close but not exactly what you need, do not force it. read the component source for patterns and inspiration, then build something custom in the project. a well-built one-off is better than a shoehorned library component.
4
+
5
+ - exact fit: use it directly
6
+ - close fit: read source, consider wrapper or slot. if it feels forced, build custom
7
+ - not a fit: build custom, follow same design system conventions (monospace, grayscale, amber accent, tight spacing)
8
+
9
+ -- installation --
10
+
11
+ not published to npm. add via file dependency:
12
+
13
+ ```json
14
+ { "dependencies": { "mono-components": "file:/Users/jonathanpyers/code/mono-components" } }
15
+ ```
16
+
17
+ peer dependency: vue ^3.4
18
+
19
+ -- project setup --
20
+
21
+ ships a tailwind v3 preset. use it instead of defining theme colors manually:
22
+
23
+ ```js
24
+ // tailwind.config.js
25
+ import preset from "mono-components/tailwind"
26
+ export default {
27
+ presets: [preset],
28
+ content: [
29
+ "./index.html",
30
+ "./src/**/*.{vue,js}",
31
+ "./node_modules/mono-components/src/**/*.vue"
32
+ ]
33
+ }
34
+ ```
35
+
36
+ import base styles in main.js:
37
+ ```js
38
+ import "mono-components/style.css"
39
+ ```
40
+
41
+ all exports are named. no default export:
42
+ ```js
43
+ import { Button, Select, DataTable, toast } from "mono-components"
44
+ ```
45
+
46
+ -- decision guide --
47
+
48
+ "i need a select":
49
+ - few options, no search needed: Select
50
+ - many options or search by name: Combobox
51
+ - multi-select: MultiCombobox
52
+ - action menu (options do things, not set values): Dropdown
53
+
54
+ "i need a toggle":
55
+ - on/off setting: Switch
56
+ - yes/no with label: Checkbox
57
+ - pick one from icon options: ToggleGroup
58
+ - pick many from options: ToggleButtons
59
+ - pick one with labels: RadioGroup
60
+ - pick one with descriptions: RadioCards (single select, radio circles)
61
+ - pick many with descriptions: CheckboxCards (multi select, square checkmarks)
62
+
63
+ "i need to show data":
64
+ - tabular, possibly editable: DataTable
65
+ - key-value pairs, detail view: KeyValue
66
+ - status label: Badge
67
+
68
+ "i need floating content":
69
+ - generic anchored panel: Popover
70
+ - confirmation before action: PopConfirm
71
+ - info on hover: Tooltip
72
+ - blocking dialog: Modal
73
+ - right-click actions: ContextMenu
74
+
75
+ "i need hierarchy/navigation":
76
+ - data tree with search: Tree
77
+ - sidebar nav: NavTree
78
+ - tab switching: Tabs
79
+ - collapsible sections: Collapsible + CollapsibleGroup
80
+
81
+ -- story files --
82
+
83
+ each component has a story file in stories/ showing usage examples and prop combinations. when adapting a component or building custom, read the story for real usage patterns.
@@ -0,0 +1,43 @@
1
+ import { defineConfig } from "histoire"
2
+ import { HstVue } from "@histoire/plugin-vue"
3
+
4
+ export default defineConfig({
5
+ plugins: [HstVue()],
6
+ setupFile: "./src/histoire-setup.js",
7
+ theme: {
8
+ title: "mono-components",
9
+ defaultColorScheme: "auto",
10
+ colors: {
11
+ gray: {
12
+ 50: "#fcfcfe",
13
+ 100: "#f3f4f7",
14
+ 200: "#e8eaee",
15
+ 300: "#dbdde3",
16
+ 400: "#b8bdc7",
17
+ 500: "#a0a6b2",
18
+ 600: "#616872",
19
+ 700: "#191a1e",
20
+ 750: "#141518",
21
+ 800: "#111214",
22
+ 850: "#0d0e10",
23
+ 900: "#0a0b0c",
24
+ 950: "#060607"
25
+ },
26
+ primary: {
27
+ 50: "#fef4ec",
28
+ 100: "#fde4c8",
29
+ 200: "#fbc890",
30
+ 300: "#f09848",
31
+ 400: "#cf6510",
32
+ 500: "#b65400",
33
+ 600: "#9a4700",
34
+ 700: "#7d3900",
35
+ 800: "#632d00",
36
+ 900: "#4a2100"
37
+ }
38
+ }
39
+ },
40
+ vite: {
41
+ base: "/"
42
+ }
43
+ })
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@prsm/mono-components",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./src/index.js",
6
+ "exports": {
7
+ ".": "./src/index.js",
8
+ "./style.css": "./src/style.css",
9
+ "./tailwind": "./tailwind.preset.js"
10
+ },
11
+ "scripts": {
12
+ "story:dev": "histoire dev",
13
+ "story:build": "histoire build"
14
+ },
15
+ "peerDependencies": {
16
+ "vue": "^3.4"
17
+ },
18
+ "dependencies": {
19
+ "@codemirror/autocomplete": "^6.20.0",
20
+ "@codemirror/commands": "^6.10.2",
21
+ "@codemirror/lang-sql": "^6.10.0",
22
+ "@codemirror/language": "^6.12.1",
23
+ "@codemirror/state": "^6.5.4",
24
+ "@codemirror/view": "^6.39.14",
25
+ "@floating-ui/vue": "^1.1.6",
26
+ "@iconify/vue": "^4.3.0",
27
+ "@lezer/highlight": "^1.2.3"
28
+ },
29
+ "devDependencies": {
30
+ "@histoire/plugin-vue": "^0.17.17",
31
+ "@vitejs/plugin-vue": "^5.2.1",
32
+ "autoprefixer": "^10.4.20",
33
+ "histoire": "^0.17.17",
34
+ "postcss": "^8.5.1",
35
+ "tailwindcss": "^3.4.17",
36
+ "vite": "^5.4.0",
37
+ "vue": "^3.5.13"
38
+ }
39
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {}
5
+ }
6
+ }
@@ -0,0 +1,36 @@
1
+ <script setup>
2
+ import { computed } from "vue"
3
+
4
+ const props = defineProps({
5
+ variant: {
6
+ type: String,
7
+ default: "neutral",
8
+ validator: v => ["success", "warning", "error", "info", "neutral"].includes(v)
9
+ }
10
+ })
11
+
12
+ const colorMap = {
13
+ success: "var(--success)",
14
+ warning: "var(--warning)",
15
+ error: "var(--error)",
16
+ info: "var(--info)",
17
+ neutral: "var(--fg-2)"
18
+ }
19
+
20
+ const badgeStyle = computed(() => {
21
+ const color = colorMap[props.variant]
22
+ return {
23
+ color,
24
+ backgroundColor: `color-mix(in srgb, ${color} 10%, transparent)`
25
+ }
26
+ })
27
+ </script>
28
+
29
+ <template>
30
+ <span
31
+ class="inline-flex items-center gap-1 text-sm py-1 px-1.5 rounded-sm font-mono"
32
+ :style="badgeStyle"
33
+ >
34
+ <slot />
35
+ </span>
36
+ </template>
@@ -0,0 +1,44 @@
1
+ <script setup>
2
+ defineProps({
3
+ variant: {
4
+ type: String,
5
+ default: "default",
6
+ validator: v => ["default", "primary", "outline", "icon", "danger", "ghost"].includes(v)
7
+ },
8
+ size: {
9
+ type: String,
10
+ default: "default",
11
+ validator: v => ["default", "small"].includes(v)
12
+ },
13
+ type: {
14
+ type: String,
15
+ default: "button"
16
+ }
17
+ })
18
+
19
+ const variantClasses = {
20
+ default: "bg-2 border border-line text-fg-0 hover:bg-3 active:bg-5",
21
+ primary: "bg-accent border border-accent text-white hover:bg-accent-hover active:brightness-75",
22
+ outline: "bg-transparent border border-line text-fg-0 hover:bg-2 active:bg-4",
23
+ icon: "bg-transparent border border-transparent text-fg-1 hover:bg-2 active:bg-4",
24
+ danger: "bg-error border border-error text-white hover:brightness-110 active:brightness-75",
25
+ ghost: "bg-transparent border border-transparent text-fg-0 hover:bg-2 active:bg-4"
26
+ }
27
+
28
+ const sizeClasses = {
29
+ default: "px-2 py-1 text-base",
30
+ small: "px-1.5 py-0.5 text-xs"
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <button
36
+ :type="type"
37
+ class="inline-flex items-center justify-center gap-1 rounded-sm font-mono cursor-pointer whitespace-nowrap"
38
+ :class="[variantClasses[variant], sizeClasses[size]]"
39
+ >
40
+ <span class="inline-flex items-center justify-center gap-1 min-h-[1.5em]">
41
+ <slot />
42
+ </span>
43
+ </button>
44
+ </template>
@@ -0,0 +1,51 @@
1
+ <script setup>
2
+ import { computed } from "vue"
3
+
4
+ const props = defineProps({
5
+ modelValue: { type: Boolean, default: false },
6
+ indeterminate: { type: Boolean, default: false },
7
+ label: { type: String, default: null }
8
+ })
9
+
10
+ const emit = defineEmits(["update:modelValue"])
11
+
12
+ const isChecked = computed(() => props.modelValue)
13
+
14
+ function toggle() {
15
+ emit("update:modelValue", !props.modelValue)
16
+ }
17
+ </script>
18
+
19
+ <template>
20
+ <label
21
+ v-if="label"
22
+ class="inline-flex items-center gap-1.5 px-1.5 py-1 rounded-sm cursor-pointer hover:bg-2"
23
+ @click.prevent="toggle"
24
+ >
25
+ <span
26
+ class="w-[14px] h-[14px] rounded-sm border flex items-center justify-center shrink-0"
27
+ :class="isChecked || indeterminate ? 'bg-accent border-accent' : 'bg-0 border-line'"
28
+ >
29
+ <svg v-if="isChecked && !indeterminate" width="10" height="10" viewBox="0 0 10 10" fill="none">
30
+ <path d="M2 5L4 7L8 3" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
31
+ </svg>
32
+ <svg v-else-if="indeterminate" width="10" height="10" viewBox="0 0 10 10" fill="none">
33
+ <path d="M2.5 5H7.5" stroke="white" stroke-width="1.5" stroke-linecap="round" />
34
+ </svg>
35
+ </span>
36
+ <span class="text-base font-mono text-fg-0 select-none">{{ label }}</span>
37
+ </label>
38
+ <span
39
+ v-else
40
+ class="inline-flex items-center justify-center w-[14px] h-[14px] rounded-sm border cursor-pointer"
41
+ :class="isChecked || indeterminate ? 'bg-accent border-accent' : 'bg-0 border-line'"
42
+ @click="toggle"
43
+ >
44
+ <svg v-if="isChecked && !indeterminate" width="10" height="10" viewBox="0 0 10 10" fill="none">
45
+ <path d="M2 5L4 7L8 3" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
46
+ </svg>
47
+ <svg v-else-if="indeterminate" width="10" height="10" viewBox="0 0 10 10" fill="none">
48
+ <path d="M2.5 5H7.5" stroke="white" stroke-width="1.5" stroke-linecap="round" />
49
+ </svg>
50
+ </span>
51
+ </template>
@@ -0,0 +1,61 @@
1
+ <script setup>
2
+ defineProps({
3
+ modelValue: { type: Array, default: () => [] },
4
+ options: { type: Array, required: true }
5
+ })
6
+
7
+ const emit = defineEmits(["update:modelValue"])
8
+
9
+ function toggle(value, current) {
10
+ const next = current.includes(value)
11
+ ? current.filter(v => v !== value)
12
+ : [...current, value]
13
+ emit("update:modelValue", next)
14
+ }
15
+
16
+ function isChecked(opt, modelValue) {
17
+ return opt.disabled || modelValue.includes(opt.value)
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <div class="flex flex-col gap-1">
23
+ <button
24
+ v-for="opt in options"
25
+ :key="opt.value"
26
+ type="button"
27
+ class="flex items-start gap-2 px-2 py-1.5 rounded-sm border text-left font-mono"
28
+ :class="[
29
+ opt.disabled
30
+ ? 'border-line bg-0 opacity-50 cursor-default'
31
+ : isChecked(opt, modelValue)
32
+ ? 'border-accent bg-3 cursor-pointer'
33
+ : 'border-line bg-0 hover:bg-2 cursor-pointer'
34
+ ]"
35
+ :disabled="opt.disabled"
36
+ @click="!opt.disabled && toggle(opt.value, modelValue)"
37
+ >
38
+ <span
39
+ class="mt-0.5 w-[14px] h-[14px] rounded-sm border shrink-0 flex items-center justify-center"
40
+ :class="isChecked(opt, modelValue) ? 'border-accent bg-accent' : 'border-line'"
41
+ >
42
+ <svg
43
+ v-if="isChecked(opt, modelValue)"
44
+ class="w-[10px] h-[10px] text-bg-0"
45
+ viewBox="0 0 10 10"
46
+ fill="none"
47
+ stroke="currentColor"
48
+ stroke-width="1.5"
49
+ stroke-linecap="round"
50
+ stroke-linejoin="round"
51
+ >
52
+ <path d="M2 5.5L4 7.5L8 3" />
53
+ </svg>
54
+ </span>
55
+ <div class="flex flex-col gap-px">
56
+ <span class="text-base text-fg-0">{{ opt.label }}</span>
57
+ <span v-if="opt.description" class="text-xs text-fg-2">{{ opt.description }}</span>
58
+ </div>
59
+ </button>
60
+ </div>
61
+ </template>
@@ -0,0 +1,299 @@
1
+ <script setup>
2
+ import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue"
3
+ import { EditorView, keymap, placeholder as phPlugin } from "@codemirror/view"
4
+ import { EditorState, Compartment } from "@codemirror/state"
5
+ import { sql, StandardSQL } from "@codemirror/lang-sql"
6
+ import { autocompletion, completionKeymap } from "@codemirror/autocomplete"
7
+ import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"
8
+ import { syntaxHighlighting, HighlightStyle } from "@codemirror/language"
9
+ import { tags } from "@lezer/highlight"
10
+
11
+ const props = defineProps({
12
+ modelValue: { type: String, default: "" },
13
+ placeholder: { type: String, default: "" },
14
+ schema: { type: Object, default: () => ({}) },
15
+ language: { type: String, default: "sql" },
16
+ readonly: { type: Boolean, default: false },
17
+ })
18
+
19
+ const emit = defineEmits(["update:modelValue"])
20
+
21
+ const container = ref(null)
22
+ let view = null
23
+ const langCompartment = new Compartment()
24
+ const schemaCompartment = new Compartment()
25
+ const readonlyCompartment = new Compartment()
26
+
27
+ const monoTheme = EditorView.theme({
28
+ "&": {
29
+ fontSize: "12px",
30
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', monospace",
31
+ backgroundColor: "var(--bg-0)",
32
+ color: "var(--fg-0)",
33
+ },
34
+ "&.cm-focused": {
35
+ outline: "none",
36
+ },
37
+ ".cm-content": {
38
+ padding: "4px 0",
39
+ caretColor: "var(--fg-0)",
40
+ },
41
+ ".cm-cursor": {
42
+ borderLeftColor: "var(--fg-0)",
43
+ },
44
+ ".cm-selectionBackground": {
45
+ backgroundColor: "var(--bg-3) !important",
46
+ },
47
+ "&.cm-focused .cm-selectionBackground": {
48
+ backgroundColor: "var(--bg-3) !important",
49
+ },
50
+ ".cm-activeLine": {
51
+ backgroundColor: "transparent",
52
+ },
53
+ ".cm-gutters": {
54
+ display: "none",
55
+ },
56
+ ".cm-line": {
57
+ padding: "0 8px",
58
+ },
59
+ ".cm-placeholder": {
60
+ color: "var(--fg-3)",
61
+ fontStyle: "normal",
62
+ },
63
+ ".cm-tooltip": {
64
+ backgroundColor: "var(--bg-1)",
65
+ border: "1px solid var(--border)",
66
+ borderRadius: "3px",
67
+ boxShadow: "0 2px 8px rgba(0,0,0,.15)",
68
+ },
69
+ ".cm-tooltip-autocomplete": {
70
+ "& > ul": {
71
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', monospace",
72
+ fontSize: "11px",
73
+ },
74
+ "& > ul > li": {
75
+ padding: "2px 8px",
76
+ color: "var(--fg-0)",
77
+ },
78
+ "& > ul > li[aria-selected]": {
79
+ backgroundColor: "var(--bg-3)",
80
+ color: "var(--fg-0)",
81
+ },
82
+ },
83
+ ".cm-completionLabel": {
84
+ color: "var(--fg-0)",
85
+ },
86
+ ".cm-completionDetail": {
87
+ color: "var(--fg-3)",
88
+ fontStyle: "normal",
89
+ marginLeft: "8px",
90
+ },
91
+ ".cm-completionMatchedText": {
92
+ color: "var(--accent)",
93
+ textDecoration: "none",
94
+ },
95
+ })
96
+
97
+ // hardcoded because HighlightStyle injects into a static sheet where
98
+ // css vars can be unreliable depending on host context
99
+ const hl = {
100
+ keyword: "#c4813a",
101
+ string: "#7a9e6e",
102
+ number: "#d19a66",
103
+ comment: "#666666",
104
+ fn: "#6eb2d6",
105
+ punctuation: "#888888",
106
+ foreground: "#b0b0b0",
107
+ }
108
+
109
+ const monoHighlight = HighlightStyle.define([
110
+ { tag: tags.keyword, color: hl.keyword, fontWeight: "600" },
111
+ { tag: tags.operator, color: hl.punctuation },
112
+ { tag: tags.string, color: hl.string },
113
+ { tag: tags.number, color: hl.number },
114
+ { tag: tags.bool, color: hl.keyword, fontWeight: "600" },
115
+ { tag: tags.null, color: hl.punctuation },
116
+ { tag: tags.comment, color: hl.comment, fontStyle: "italic" },
117
+ { tag: tags.typeName, color: hl.fn },
118
+ { tag: tags.function(tags.variableName), color: hl.fn },
119
+ { tag: tags.definition(tags.variableName), color: "#e5e5e5" },
120
+ { tag: tags.variableName, color: "#e5e5e5" },
121
+ { tag: tags.punctuation, color: hl.punctuation },
122
+ { tag: tags.paren, color: hl.punctuation },
123
+ { tag: tags.squareBracket, color: hl.punctuation },
124
+ { tag: tags.brace, color: hl.punctuation },
125
+ { tag: tags.separator, color: hl.punctuation },
126
+ { tag: tags.special(tags.string), color: hl.string },
127
+ ])
128
+
129
+ const autoHeight = EditorView.theme({
130
+ "&": { height: "auto" },
131
+ ".cm-scroller": { overflow: "visible" },
132
+ })
133
+
134
+ function buildSchema() {
135
+ if (!props.schema || Object.keys(props.schema).length === 0) return {}
136
+ const tables = {}
137
+ for (const [table, cols] of Object.entries(props.schema)) {
138
+ tables[table] = cols.map(c => typeof c === "string" ? c : c.name || c)
139
+ }
140
+ return tables
141
+ }
142
+
143
+ function schemaCompletions(context) {
144
+ const tables = buildSchema()
145
+ const word = context.matchBefore(/\w+/)
146
+ if (!word || word.from === word.to) return null
147
+ const cols = new Set()
148
+ for (const colList of Object.values(tables)) {
149
+ for (const c of colList) cols.add(c)
150
+ }
151
+ if (cols.size === 0) return null
152
+ return {
153
+ from: word.from,
154
+ options: [...cols].map(c => ({ label: c, type: "property" })),
155
+ }
156
+ }
157
+
158
+ function buildLangExtension() {
159
+ const tables = buildSchema()
160
+ return sql({
161
+ dialect: StandardSQL,
162
+ schema: Object.keys(tables).length ? tables : undefined,
163
+ upperCaseKeywords: false,
164
+ })
165
+ }
166
+
167
+ const updateListener = EditorView.updateListener.of((update) => {
168
+ if (update.docChanged) {
169
+ const value = update.state.doc.toString()
170
+ emit("update:modelValue", value)
171
+ }
172
+ })
173
+
174
+ function focusedClass() {
175
+ return EditorView.theme({
176
+ "&.cm-editor": {
177
+ borderColor: "var(--border)",
178
+ borderWidth: "1px",
179
+ borderStyle: "solid",
180
+ borderRadius: "3px",
181
+ transition: "border-color 0.1s",
182
+ },
183
+ "&.cm-editor.cm-focused": {
184
+ borderColor: "var(--accent)",
185
+ },
186
+ })
187
+ }
188
+
189
+ onMounted(() => {
190
+ const extensions = [
191
+ monoTheme,
192
+ focusedClass(),
193
+ syntaxHighlighting(monoHighlight),
194
+ autoHeight,
195
+ keymap.of([...defaultKeymap, ...historyKeymap]),
196
+ history(),
197
+ autocompletion({ activateOnTyping: true }),
198
+ keymap.of(completionKeymap),
199
+ schemaCompartment.of(EditorState.languageData.of(() => [{ autocomplete: schemaCompletions }])),
200
+ langCompartment.of(buildLangExtension()),
201
+ readonlyCompartment.of(EditorState.readOnly.of(props.readonly)),
202
+ updateListener,
203
+ ]
204
+
205
+ if (props.placeholder) {
206
+ extensions.push(phPlugin(props.placeholder))
207
+ }
208
+
209
+ const state = EditorState.create({
210
+ doc: props.modelValue,
211
+ extensions,
212
+ })
213
+
214
+ view = new EditorView({
215
+ state,
216
+ parent: container.value,
217
+ })
218
+ })
219
+
220
+ onBeforeUnmount(() => {
221
+ if (view) {
222
+ view.destroy()
223
+ view = null
224
+ }
225
+ })
226
+
227
+ watch(() => props.modelValue, (val) => {
228
+ if (!view) return
229
+ const current = view.state.doc.toString()
230
+ if (val !== current) {
231
+ view.dispatch({
232
+ changes: { from: 0, to: view.state.doc.length, insert: val },
233
+ })
234
+ }
235
+ })
236
+
237
+ watch(() => props.schema, () => {
238
+ if (!view) return
239
+ view.dispatch({
240
+ effects: [
241
+ langCompartment.reconfigure(buildLangExtension()),
242
+ schemaCompartment.reconfigure(EditorState.languageData.of(() => [{ autocomplete: schemaCompletions }])),
243
+ ],
244
+ })
245
+ }, { deep: true })
246
+
247
+ watch(() => props.readonly, (val) => {
248
+ if (!view) return
249
+ view.dispatch({
250
+ effects: readonlyCompartment.reconfigure(EditorState.readOnly.of(val)),
251
+ })
252
+ })
253
+ </script>
254
+
255
+ <template>
256
+ <div ref="container" class="mono-code-editor" />
257
+ </template>
258
+
259
+ <style scoped>
260
+ .mono-code-editor {
261
+ min-height: 24px;
262
+ }
263
+
264
+ .mono-code-editor :deep(.cm-editor) {
265
+ scrollbar-width: thin;
266
+ scrollbar-color: transparent transparent;
267
+ transition: scrollbar-color 0.15s;
268
+ }
269
+
270
+ .mono-code-editor :deep(.cm-editor:hover) {
271
+ scrollbar-color: var(--bg-5) transparent;
272
+ }
273
+
274
+ .mono-code-editor :deep(.cm-editor)::-webkit-scrollbar {
275
+ width: 8px;
276
+ height: 8px;
277
+ }
278
+
279
+ .mono-code-editor :deep(.cm-editor)::-webkit-scrollbar-track {
280
+ background: transparent;
281
+ }
282
+
283
+ .mono-code-editor :deep(.cm-editor:hover)::-webkit-scrollbar-track {
284
+ background: var(--bg-2);
285
+ }
286
+
287
+ .mono-code-editor :deep(.cm-editor)::-webkit-scrollbar-thumb {
288
+ background: transparent;
289
+ border-radius: 8px;
290
+ }
291
+
292
+ .mono-code-editor :deep(.cm-editor:hover)::-webkit-scrollbar-thumb {
293
+ background: var(--bg-5);
294
+ }
295
+
296
+ .mono-code-editor :deep(.cm-editor)::-webkit-scrollbar-thumb:hover {
297
+ background: var(--bg-6);
298
+ }
299
+ </style>