@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,85 @@
1
+ <script setup>
2
+ defineProps({
3
+ modelValue: { type: Boolean, default: false },
4
+ disabled: { type: Boolean, default: false }
5
+ })
6
+
7
+ const emit = defineEmits(["update:modelValue"])
8
+ </script>
9
+
10
+ <template>
11
+ <button
12
+ type="button"
13
+ class="switch"
14
+ :class="{ checked: modelValue, disabled }"
15
+ :disabled="disabled"
16
+ @click="!disabled && emit('update:modelValue', !modelValue)"
17
+ >
18
+ <span class="thumb" />
19
+ </button>
20
+ </template>
21
+
22
+ <style scoped>
23
+ .switch {
24
+ --track-w: 36px;
25
+ --track-h: 20px;
26
+ --thumb-size: 14px;
27
+ --thumb-margin: 2px;
28
+
29
+ position: relative;
30
+ display: inline-flex;
31
+ align-items: center;
32
+ width: var(--track-w);
33
+ height: var(--track-h);
34
+ padding: 0;
35
+ border: 1px solid var(--border);
36
+ border-radius: 100px;
37
+ background: var(--bg-4);
38
+ cursor: pointer;
39
+ transition: background 0.15s, border-color 0.15s;
40
+ }
41
+
42
+ .switch:hover:not(.disabled) {
43
+ background: var(--bg-5);
44
+ }
45
+
46
+ .switch.checked {
47
+ background: var(--accent);
48
+ border-color: var(--accent);
49
+ }
50
+
51
+ .switch.checked:hover:not(.disabled) {
52
+ background: var(--accent-hover);
53
+ border-color: var(--accent-hover);
54
+ }
55
+
56
+ .switch.disabled {
57
+ opacity: 0.4;
58
+ cursor: not-allowed;
59
+ }
60
+
61
+ .thumb {
62
+ position: absolute;
63
+ top: var(--thumb-margin);
64
+ left: var(--thumb-margin);
65
+ width: var(--thumb-size);
66
+ height: var(--thumb-size);
67
+ background: #fff;
68
+ border-radius: 100px;
69
+ transition: left 0.15s ease, width 0.15s ease;
70
+ pointer-events: none;
71
+ }
72
+
73
+ .switch.checked .thumb {
74
+ left: calc(100% - var(--thumb-size) - var(--thumb-margin));
75
+ }
76
+
77
+ .switch:active:not(.disabled) .thumb {
78
+ width: calc(var(--thumb-size) + 4px);
79
+ }
80
+
81
+ .switch.checked:active:not(.disabled) .thumb {
82
+ left: calc(100% - var(--thumb-size) - var(--thumb-margin) - 4px);
83
+ width: calc(var(--thumb-size) + 4px);
84
+ }
85
+ </style>
@@ -0,0 +1,34 @@
1
+ <script setup>
2
+ import { Icon } from "@iconify/vue"
3
+
4
+ defineProps({
5
+ modelValue: { type: String, required: true },
6
+ tabs: { type: Array, required: true }
7
+ })
8
+
9
+ const emit = defineEmits(["update:modelValue"])
10
+ </script>
11
+
12
+ <template>
13
+ <div class="inline-flex items-center gap-0.5">
14
+ <button
15
+ v-for="tab in tabs"
16
+ :key="tab.key"
17
+ type="button"
18
+ class="inline-flex items-center gap-1 px-2 py-1 rounded-sm font-mono text-base cursor-pointer border-none"
19
+ :class="modelValue === tab.key
20
+ ? 'bg-3 text-fg-0'
21
+ : 'bg-transparent text-fg-2 hover:bg-2 hover:text-fg-1'"
22
+ @click="emit('update:modelValue', tab.key)"
23
+ >
24
+ <Icon v-if="tab.icon" :icon="tab.icon" class="text-base" />
25
+ <span>{{ tab.label }}</span>
26
+ <span
27
+ v-if="tab.count != null"
28
+ class="text-xs text-fg-3 bg-4 px-1 py-px rounded-sm"
29
+ >
30
+ {{ tab.count }}
31
+ </span>
32
+ </button>
33
+ </div>
34
+ </template>
@@ -0,0 +1,80 @@
1
+ <script setup>
2
+ import { ref } from "vue"
3
+ import { Icon } from "@iconify/vue"
4
+
5
+ const props = defineProps({
6
+ modelValue: { type: Array, default: () => [] },
7
+ placeholder: { type: String, default: "type and press enter..." },
8
+ })
9
+
10
+ const emit = defineEmits(["update:modelValue"])
11
+
12
+ const query = ref("")
13
+ const inputRef = ref(null)
14
+
15
+ function add() {
16
+ const value = query.value.trim()
17
+ if (!value) return
18
+ if (!props.modelValue.includes(value)) {
19
+ emit("update:modelValue", [...props.modelValue, value])
20
+ }
21
+ query.value = ""
22
+ }
23
+
24
+ function remove(value) {
25
+ emit("update:modelValue", props.modelValue.filter(v => v !== value))
26
+ }
27
+
28
+ function clearAll() {
29
+ emit("update:modelValue", [])
30
+ }
31
+
32
+ function handleKeydown(e) {
33
+ if (e.key === "Enter") {
34
+ e.preventDefault()
35
+ add()
36
+ } else if (e.key === "Backspace" && !query.value && props.modelValue.length) {
37
+ remove(props.modelValue[props.modelValue.length - 1])
38
+ }
39
+ }
40
+
41
+ function focus() {
42
+ inputRef.value?.focus()
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <div
48
+ class="flex items-center gap-1 p-1 bg-0 border border-line rounded-sm font-mono text-base cursor-text min-w-0 w-full focus-within:border-accent"
49
+ @click="focus"
50
+ >
51
+ <div class="flex-1 flex items-center flex-wrap gap-1 min-w-0">
52
+ <span
53
+ v-for="tag in modelValue"
54
+ :key="tag"
55
+ class="inline-flex items-center gap-0.5 px-1 py-px bg-3 text-fg-0 text-xs rounded-sm whitespace-nowrap"
56
+ >
57
+ {{ tag }}
58
+ <Icon
59
+ icon="material-symbols:close"
60
+ class="text-xs text-fg-2 hover:text-fg-0 cursor-pointer"
61
+ @click.stop="remove(tag)"
62
+ />
63
+ </span>
64
+ <input
65
+ ref="inputRef"
66
+ v-model="query"
67
+ type="text"
68
+ class="flex-1 min-w-[60px] px-1 py-px bg-transparent border-none font-mono text-base text-fg-0 outline-none focus-visible:outline-none"
69
+ :placeholder="modelValue.length ? '' : placeholder"
70
+ @keydown="handleKeydown"
71
+ />
72
+ </div>
73
+ <Icon
74
+ v-if="modelValue.length"
75
+ icon="material-symbols:close"
76
+ class="text-lg text-fg-2 hover:text-fg-0 shrink-0 cursor-pointer"
77
+ @click.stop="clearAll"
78
+ />
79
+ </div>
80
+ </template>
@@ -0,0 +1,97 @@
1
+ <script setup>
2
+ import { ref, watch, onMounted, nextTick } from "vue"
3
+
4
+ const props = defineProps({
5
+ modelValue: { type: String, default: "" },
6
+ placeholder: { type: String, default: "" },
7
+ autoGrow: { type: Boolean, default: false },
8
+ rows: { type: Number, default: 3 }
9
+ })
10
+
11
+ const emit = defineEmits(["update:modelValue"])
12
+ const el = ref(null)
13
+
14
+ function resize() {
15
+ if (!props.autoGrow || !el.value) return
16
+ el.value.style.height = "auto"
17
+ const border = el.value.offsetHeight - el.value.clientHeight
18
+ el.value.style.height = (el.value.scrollHeight + border) + "px"
19
+ }
20
+
21
+ function onInput(e) {
22
+ emit("update:modelValue", e.target.value)
23
+ resize()
24
+ }
25
+
26
+ watch(() => props.modelValue, () => nextTick(resize))
27
+ onMounted(resize)
28
+ </script>
29
+
30
+ <template>
31
+ <textarea
32
+ ref="el"
33
+ :value="modelValue"
34
+ :placeholder="placeholder"
35
+ :rows="autoGrow ? 1 : rows"
36
+ class="mono-textarea px-2 py-1 text-base leading-[1.5] bg-0 border border-line rounded-sm font-mono text-fg-0 placeholder:text-fg-3 focus:border-accent focus:outline-none w-full"
37
+ :class="autoGrow ? 'resize-none overflow-hidden' : 'resize-vertical'"
38
+ @input="onInput"
39
+ />
40
+ </template>
41
+
42
+ <style scoped>
43
+ .mono-textarea {
44
+ scrollbar-width: thin;
45
+ scrollbar-color: transparent transparent;
46
+ transition: scrollbar-color 0.15s;
47
+ }
48
+
49
+ .mono-textarea:hover {
50
+ scrollbar-color: var(--bg-5) transparent;
51
+ }
52
+
53
+ .mono-textarea::-webkit-scrollbar {
54
+ width: 8px;
55
+ }
56
+
57
+ .mono-textarea::-webkit-scrollbar-track {
58
+ background: transparent;
59
+ }
60
+
61
+ .mono-textarea:hover::-webkit-scrollbar-track {
62
+ background: var(--bg-2);
63
+ }
64
+
65
+ .mono-textarea::-webkit-scrollbar-thumb {
66
+ background: transparent;
67
+ border-radius: 8px;
68
+ }
69
+
70
+ .mono-textarea:hover::-webkit-scrollbar-thumb {
71
+ background: var(--bg-5);
72
+ }
73
+
74
+ .mono-textarea::-webkit-scrollbar-thumb:hover {
75
+ background: var(--bg-6);
76
+ }
77
+
78
+ .mono-textarea::-webkit-resizer {
79
+ background: transparent;
80
+ background-image:
81
+ radial-gradient(circle, var(--bg-5) 1px, transparent 0),
82
+ radial-gradient(circle, var(--bg-5) 1px, transparent 0),
83
+ radial-gradient(circle, var(--bg-5) 1px, transparent 0),
84
+ radial-gradient(circle, var(--bg-5) 1px, transparent 0),
85
+ radial-gradient(circle, var(--bg-5) 1px, transparent 0),
86
+ radial-gradient(circle, var(--bg-5) 1px, transparent 0);
87
+ background-size: 2px 2px;
88
+ background-repeat: no-repeat;
89
+ background-position:
90
+ bottom 2px right 2px,
91
+ bottom 2px right 6px,
92
+ bottom 2px right 10px,
93
+ bottom 6px right 2px,
94
+ bottom 6px right 6px,
95
+ bottom 10px right 2px;
96
+ }
97
+ </style>
@@ -0,0 +1,104 @@
1
+ <script setup>
2
+ import { toasts, toast } from "../composables/toast.js"
3
+ import { Icon } from "@iconify/vue"
4
+
5
+ const iconMap = {
6
+ success: "material-symbols:check-circle-outline",
7
+ warning: "material-symbols:warning-outline",
8
+ error: "material-symbols:error-outline",
9
+ info: "material-symbols:info-outline",
10
+ neutral: null
11
+ }
12
+
13
+ const colorMap = {
14
+ success: "var(--success)",
15
+ warning: "var(--warning)",
16
+ error: "var(--error)",
17
+ info: "var(--info)",
18
+ neutral: "var(--fg-2)"
19
+ }
20
+
21
+ function getStyle(variant) {
22
+ const color = colorMap[variant]
23
+ return {
24
+ borderLeft: `2px solid ${color}`
25
+ }
26
+ }
27
+ </script>
28
+
29
+ <template>
30
+ <Teleport to="body">
31
+ <div class="toast-container">
32
+ <TransitionGroup name="toast">
33
+ <div
34
+ v-for="t in toasts"
35
+ :key="t.id"
36
+ class="toast-item"
37
+ :style="getStyle(t.variant)"
38
+ @mouseenter="toast.pause(t.id)"
39
+ @mouseleave="toast.resume(t.id)"
40
+ >
41
+ <Icon
42
+ v-if="iconMap[t.variant]"
43
+ :icon="iconMap[t.variant]"
44
+ class="text-xl shrink-0"
45
+ :style="{ color: colorMap[t.variant] }"
46
+ />
47
+ <span class="flex-1 text-base text-fg-0">{{ t.message }}</span>
48
+ <button
49
+ class="shrink-0 text-fg-3 hover:text-fg-1 bg-transparent border-none cursor-pointer p-0 leading-none"
50
+ @click="toast.remove(t.id)"
51
+ >
52
+ <Icon icon="material-symbols:close" class="text-base" />
53
+ </button>
54
+ </div>
55
+ </TransitionGroup>
56
+ </div>
57
+ </Teleport>
58
+ </template>
59
+
60
+ <style scoped>
61
+ .toast-container {
62
+ position: fixed;
63
+ top: 12px;
64
+ left: 50%;
65
+ transform: translateX(-50%);
66
+ z-index: 2000;
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: 6px;
70
+ pointer-events: none;
71
+ }
72
+
73
+ .toast-item {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 8px;
77
+ padding: 8px 12px;
78
+ background: var(--bg-1);
79
+ border: 1px solid var(--border);
80
+ border-radius: 2px;
81
+ min-width: 240px;
82
+ max-width: 400px;
83
+ pointer-events: auto;
84
+ font-family: "JetBrains Mono", "Fira Code", "SF Mono", monospace;
85
+ }
86
+
87
+ .toast-enter-active {
88
+ transition: all 0.2s ease;
89
+ }
90
+
91
+ .toast-leave-active {
92
+ transition: all 0.15s ease;
93
+ }
94
+
95
+ .toast-enter-from {
96
+ opacity: 0;
97
+ transform: translateY(-8px);
98
+ }
99
+
100
+ .toast-leave-to {
101
+ opacity: 0;
102
+ transform: translateY(-4px);
103
+ }
104
+ </style>
@@ -0,0 +1,45 @@
1
+ <script setup>
2
+ import { computed } from "vue"
3
+ import { Icon } from "@iconify/vue"
4
+
5
+ const props = defineProps({
6
+ modelValue: { type: Array, default: () => [] },
7
+ options: { type: Array, required: true },
8
+ label: { type: String, default: null }
9
+ })
10
+
11
+ const emit = defineEmits(["update:modelValue"])
12
+
13
+ const selectedSet = computed(() => new Set(props.modelValue))
14
+
15
+ function toggle(value) {
16
+ const next = selectedSet.value.has(value)
17
+ ? props.modelValue.filter(v => v !== value)
18
+ : [...props.modelValue, value]
19
+ emit("update:modelValue", next)
20
+ }
21
+ </script>
22
+
23
+ <template>
24
+ <div class="inline-flex items-center gap-1.5">
25
+ <span v-if="label" class="text-base text-fg-2 font-mono">{{ label }}</span>
26
+ <div class="inline-flex h-[26px] border border-line rounded-sm overflow-hidden">
27
+ <button
28
+ v-for="(opt, i) in options"
29
+ :key="opt.value ?? opt"
30
+ type="button"
31
+ class="inline-flex items-center justify-center min-w-[24px] h-full px-1.5 font-mono text-base cursor-pointer"
32
+ :class="[
33
+ selectedSet.has(opt.value ?? opt)
34
+ ? 'bg-accent border-accent text-white'
35
+ : 'bg-0 text-fg-2 hover:bg-2 hover:text-fg-1',
36
+ i > 0 ? 'border-l border-line' : ''
37
+ ]"
38
+ @click="toggle(opt.value ?? opt)"
39
+ >
40
+ <Icon v-if="opt.icon" :icon="opt.icon" class="text-lg" />
41
+ <span v-else>{{ opt.label ?? opt }}</span>
42
+ </button>
43
+ </div>
44
+ </div>
45
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup>
2
+ import { Icon } from "@iconify/vue"
3
+
4
+ defineProps({
5
+ modelValue: { type: String, required: true },
6
+ options: { type: Array, required: true }
7
+ })
8
+
9
+ const emit = defineEmits(["update:modelValue"])
10
+ </script>
11
+
12
+ <template>
13
+ <div class="inline-flex h-[26px] border border-line rounded-sm overflow-hidden">
14
+ <button
15
+ v-for="(opt, i) in options"
16
+ :key="opt.value"
17
+ type="button"
18
+ class="inline-flex items-center justify-center min-w-[24px] h-full px-1.5 font-mono text-base cursor-pointer"
19
+ :class="[
20
+ modelValue === opt.value
21
+ ? 'bg-3 text-accent'
22
+ : 'bg-0 text-fg-2 hover:bg-2 hover:text-fg-1',
23
+ i > 0 ? 'border-l border-line' : ''
24
+ ]"
25
+ @click="emit('update:modelValue', opt.value)"
26
+ >
27
+ <Icon :icon="opt.icon" class="text-base" />
28
+ </button>
29
+ </div>
30
+ </template>
@@ -0,0 +1,56 @@
1
+ <script setup>
2
+ import { ref, computed } from "vue"
3
+ import { useFloating, offset, flip, shift } from "@floating-ui/vue"
4
+
5
+ const props = defineProps({
6
+ text: { type: String, required: true },
7
+ placement: { type: String, default: "top" },
8
+ delay: { type: Number, default: 400 }
9
+ })
10
+
11
+ const open = ref(false)
12
+ const triggerRef = ref(null)
13
+ const floatingRef = ref(null)
14
+ let showTimeout = null
15
+
16
+ const { floatingStyles } = useFloating(triggerRef, floatingRef, {
17
+ placement: computed(() => props.placement),
18
+ strategy: "fixed",
19
+ middleware: [offset(6), flip(), shift({ padding: 8 })],
20
+ open
21
+ })
22
+
23
+ function show() {
24
+ showTimeout = setTimeout(() => { open.value = true }, props.delay)
25
+ }
26
+
27
+ function hide() {
28
+ clearTimeout(showTimeout)
29
+ open.value = false
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <span
35
+ ref="triggerRef"
36
+ class="inline-flex"
37
+ @mouseenter="show"
38
+ @mouseleave="hide"
39
+ @focus="show"
40
+ @blur="hide"
41
+ >
42
+ <slot />
43
+ </span>
44
+ <Teleport to="body">
45
+ <Transition name="tooltip">
46
+ <div
47
+ v-if="open"
48
+ ref="floatingRef"
49
+ :style="floatingStyles"
50
+ class="tooltip-content fixed z-[1100] px-1.5 py-0.5 text-xs bg-3 border rounded-sm text-fg-0 pointer-events-none whitespace-nowrap"
51
+ >
52
+ {{ text }}
53
+ </div>
54
+ </Transition>
55
+ </Teleport>
56
+ </template>