@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.
- package/.claude/settings.local.json +13 -0
- package/.lore +83 -0
- package/histoire.config.js +43 -0
- package/package.json +39 -0
- package/postcss.config.js +6 -0
- package/src/components/Badge.vue +36 -0
- package/src/components/Button.vue +44 -0
- package/src/components/Checkbox.vue +51 -0
- package/src/components/CheckboxCards.vue +61 -0
- package/src/components/CodeEditor.vue +299 -0
- package/src/components/Collapsible.vue +69 -0
- package/src/components/CollapsibleGroup.vue +38 -0
- package/src/components/Combobox.vue +179 -0
- package/src/components/ContextMenu.vue +65 -0
- package/src/components/ContextMenuPanel.vue +115 -0
- package/src/components/DataTable.vue +326 -0
- package/src/components/Dropdown.vue +127 -0
- package/src/components/GhostInput.vue +29 -0
- package/src/components/Input.vue +23 -0
- package/src/components/KeyValue.vue +149 -0
- package/src/components/LabeledTextarea.vue +64 -0
- package/src/components/LabeledTextareaGroup.vue +14 -0
- package/src/components/Mention.vue +79 -0
- package/src/components/Modal.vue +109 -0
- package/src/components/MultiCombobox.vue +209 -0
- package/src/components/NavTree.vue +98 -0
- package/src/components/NumberInput.vue +128 -0
- package/src/components/PopConfirm.vue +94 -0
- package/src/components/Popover.vue +53 -0
- package/src/components/RadioCards.vue +37 -0
- package/src/components/RadioGroup.vue +57 -0
- package/src/components/RangeSlider.vue +165 -0
- package/src/components/ScrollBox.vue +78 -0
- package/src/components/SectionHeader.vue +18 -0
- package/src/components/Select.vue +187 -0
- package/src/components/Switch.vue +85 -0
- package/src/components/Tabs.vue +34 -0
- package/src/components/TagInput.vue +80 -0
- package/src/components/Textarea.vue +97 -0
- package/src/components/ToastContainer.vue +104 -0
- package/src/components/ToggleButtons.vue +45 -0
- package/src/components/ToggleGroup.vue +30 -0
- package/src/components/Tooltip.vue +56 -0
- package/src/components/Tree.vue +188 -0
- package/src/composables/toast.js +54 -0
- package/src/composables/useClickOutside.js +23 -0
- package/src/composables/useMention.js +291 -0
- package/src/composables/usePointerDrag.js +39 -0
- package/src/histoire-setup.js +1 -0
- package/src/index.js +43 -0
- package/src/style.css +96 -0
- package/stories/Badge.story.vue +24 -0
- package/stories/Button.story.vue +45 -0
- package/stories/Checkbox.story.vue +31 -0
- package/stories/CheckboxCards.story.vue +51 -0
- package/stories/CodeEditor.story.vue +71 -0
- package/stories/Collapsible.story.vue +84 -0
- package/stories/Combobox.story.vue +44 -0
- package/stories/ContextMenu.story.vue +59 -0
- package/stories/DataTable.story.vue +185 -0
- package/stories/Dropdown.story.vue +49 -0
- package/stories/GhostInput.story.vue +24 -0
- package/stories/Input.story.vue +23 -0
- package/stories/KeyValue.story.vue +104 -0
- package/stories/LabeledTextarea.story.vue +44 -0
- package/stories/Mention.story.vue +166 -0
- package/stories/Modal.story.vue +86 -0
- package/stories/MultiCombobox.story.vue +76 -0
- package/stories/NavTree.story.vue +184 -0
- package/stories/NumberInput.story.vue +31 -0
- package/stories/Overview.story.vue +85 -0
- package/stories/PopConfirm.story.vue +39 -0
- package/stories/RadioCards.story.vue +66 -0
- package/stories/RadioGroup.story.vue +52 -0
- package/stories/RangeSlider.story.vue +75 -0
- package/stories/ScrollBox.story.vue +54 -0
- package/stories/SectionHeader.story.vue +22 -0
- package/stories/Select.story.vue +34 -0
- package/stories/Switch.story.vue +42 -0
- package/stories/Tabs.story.vue +34 -0
- package/stories/TagInput.story.vue +54 -0
- package/stories/Textarea.story.vue +28 -0
- package/stories/Toast.story.vue +28 -0
- package/stories/ToggleButtons.story.vue +57 -0
- package/stories/ToggleGroup.story.vue +34 -0
- package/stories/Tooltip.story.vue +55 -0
- package/stories/Tree.story.vue +115 -0
- package/tailwind.config.js +9 -0
- package/tailwind.preset.js +79 -0
- 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>
|