|
| 1 | +<script setup lang="ts"> |
| 2 | +import type { SelectModelValue } from "@v1/types/model"; |
| 3 | +import type { SelectOption } from "@v1/types/option"; |
| 4 | +
|
| 5 | +import { Select } from "@v1/index"; |
| 6 | +import { FlaskConical, Github } from "lucide-vue-next"; |
| 7 | +import { computed, ref, watch } from "vue"; |
| 8 | +
|
| 9 | +import DemoHeader from "../playground/components/DemoHeader.vue"; |
| 10 | +import DemoPanel from "../playground/components/DemoPanel.vue"; |
| 11 | +import DemoPill from "../playground/components/DemoPill.vue"; |
| 12 | +import DemoSection from "../playground/components/DemoSection.vue"; |
| 13 | +import DemoValue from "../playground/components/DemoValue.vue"; |
| 14 | +
|
| 15 | +type BooleanControl = { |
| 16 | + key: "multiple" | "searchable" | "clearable" | "disabled" | "loading"; |
| 17 | + label: string; |
| 18 | + description: string; |
| 19 | +}; |
| 20 | +
|
| 21 | +const booleanControls: BooleanControl[] = [ |
| 22 | + { key: "multiple", label: "Multiple", description: "Allow selecting more than one option." }, |
| 23 | + { key: "searchable", label: "Searchable", description: "Filter options with a search input." }, |
| 24 | + { key: "clearable", label: "Clearable", description: "Show a clear button when a value is set." }, |
| 25 | + { key: "disabled", label: "Disabled", description: "Prevent all interaction." }, |
| 26 | + { key: "loading", label: "Loading", description: "Show a loading indicator on the trigger." }, |
| 27 | +]; |
| 28 | +
|
| 29 | +const options = ref<SelectOption<string>[]>([ |
| 30 | + { label: "JavaScript", value: "js" }, |
| 31 | + { label: "TypeScript", value: "ts" }, |
| 32 | + { label: "Vue", value: "vue" }, |
| 33 | + { label: "React", value: "react", disabled: true }, |
| 34 | + { label: "Svelte", value: "svelte" }, |
| 35 | +]); |
| 36 | +
|
| 37 | +const model = ref<SelectModelValue<string>>(null); |
| 38 | +const multiple = ref(false); |
| 39 | +const searchable = ref(false); |
| 40 | +const clearable = ref(false); |
| 41 | +const disabled = ref(false); |
| 42 | +const loading = ref(false); |
| 43 | +const placeholder = ref("Pick a framework"); |
| 44 | +
|
| 45 | +const modelDisplay = computed(() => JSON.stringify(model.value, null, 2)); |
| 46 | +
|
| 47 | +const controlValues = computed(() => ({ |
| 48 | + multiple: multiple.value, |
| 49 | + searchable: searchable.value, |
| 50 | + clearable: clearable.value, |
| 51 | + disabled: disabled.value, |
| 52 | + loading: loading.value, |
| 53 | +})); |
| 54 | +
|
| 55 | +const emptyModel = (): SelectModelValue<string> => (multiple.value ? [] : null); |
| 56 | +
|
| 57 | +watch(multiple, () => { |
| 58 | + model.value = emptyModel(); |
| 59 | +}); |
| 60 | +
|
| 61 | +function toggleBooleanControl(key: BooleanControl["key"]) { |
| 62 | + const refs = { multiple, searchable, clearable, disabled, loading } as const; |
| 63 | + refs[key].value = !refs[key].value; |
| 64 | +} |
| 65 | +
|
| 66 | +function toggleOptionDisabled(value: string) { |
| 67 | + const option = options.value.find((item) => item.value === value); |
| 68 | +
|
| 69 | + if (!option) { |
| 70 | + return; |
| 71 | + } |
| 72 | +
|
| 73 | + option.disabled = !option.disabled; |
| 74 | +} |
| 75 | +</script> |
| 76 | + |
| 77 | +<template> |
| 78 | + <div class="min-h-screen"> |
| 79 | + <header class="border-b border-neutral-800/80 bg-[var(--playground-panel)]/80"> |
| 80 | + <div class="mx-auto flex w-full max-w-6xl flex-col gap-4 px-6 py-6 lg:flex-row lg:items-center lg:justify-between"> |
| 81 | + <div class="space-y-2"> |
| 82 | + <div class="flex items-center gap-2"> |
| 83 | + <FlaskConical class="h-5 w-5 text-green-400/80" /> |
| 84 | + <h1 class="text-2xl font-semibold text-neutral-100"> |
| 85 | + v1 Dev Playground |
| 86 | + </h1> |
| 87 | + <DemoPill>src-v1</DemoPill> |
| 88 | + </div> |
| 89 | + |
| 90 | + <p class="text-xs font-semibold text-neutral-500"> |
| 91 | + Live preview with HMR β edits to <code class="font-mono text-neutral-400">src-v1/</code> reload instantly. |
| 92 | + </p> |
| 93 | + </div> |
| 94 | + |
| 95 | + <div class="flex flex-wrap items-center gap-3 text-sm text-neutral-400"> |
| 96 | + <a |
| 97 | + class="inline-flex items-center gap-2 rounded-full border border-neutral-800/80 px-3 py-1.5 transition hover:border-neutral-700 hover:text-neutral-200" |
| 98 | + href="https://github.com/TotomInc/vue3-select-component" |
| 99 | + rel="noreferrer" |
| 100 | + target="_blank" |
| 101 | + > |
| 102 | + <Github class="h-4 w-4" /> |
| 103 | + GitHub |
| 104 | + </a> |
| 105 | + </div> |
| 106 | + </div> |
| 107 | + </header> |
| 108 | + |
| 109 | + <div class="mx-auto flex w-full max-w-6xl flex-col gap-8 px-6 py-8 lg:flex-row"> |
| 110 | + <aside class="flex w-full flex-col gap-6 lg:w-72 lg:shrink-0"> |
| 111 | + <DemoSection |
| 112 | + title="Props" |
| 113 | + description="Toggle assembled Select props in real time." |
| 114 | + > |
| 115 | + <div class="flex flex-col gap-2"> |
| 116 | + <label |
| 117 | + v-for="control in booleanControls" |
| 118 | + :key="control.key" |
| 119 | + class="flex cursor-pointer items-start gap-3 rounded-lg border border-neutral-800/80 bg-[var(--playground-panel)] px-3 py-2.5 transition hover:border-neutral-700" |
| 120 | + > |
| 121 | + <input |
| 122 | + type="checkbox" |
| 123 | + class="mt-0.5 accent-green-500" |
| 124 | + :checked="controlValues[control.key]" |
| 125 | + @change="toggleBooleanControl(control.key)" |
| 126 | + > |
| 127 | + <span class="grid gap-0.5"> |
| 128 | + <span class="text-sm font-medium text-neutral-200">{{ control.label }}</span> |
| 129 | + <span class="text-xs text-neutral-500">{{ control.description }}</span> |
| 130 | + </span> |
| 131 | + </label> |
| 132 | + </div> |
| 133 | + </DemoSection> |
| 134 | + |
| 135 | + <DemoSection |
| 136 | + title="Placeholder" |
| 137 | + description="Customize the empty-state label." |
| 138 | + > |
| 139 | + <input |
| 140 | + v-model="placeholder" |
| 141 | + type="text" |
| 142 | + class="w-full rounded-lg border border-neutral-800/80 bg-[var(--playground-panel)] px-3 py-2 text-sm text-neutral-200 outline-none transition focus:border-green-500/50" |
| 143 | + > |
| 144 | + </DemoSection> |
| 145 | + |
| 146 | + <DemoSection |
| 147 | + title="Options" |
| 148 | + description="Toggle disabled state on individual options." |
| 149 | + > |
| 150 | + <div class="flex flex-col gap-1.5"> |
| 151 | + <label |
| 152 | + v-for="option in options" |
| 153 | + :key="option.value" |
| 154 | + class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm transition hover:bg-neutral-800/50" |
| 155 | + > |
| 156 | + <input |
| 157 | + type="checkbox" |
| 158 | + class="accent-green-500" |
| 159 | + :checked="option.disabled" |
| 160 | + @change="toggleOptionDisabled(option.value)" |
| 161 | + > |
| 162 | + <span :class="option.disabled ? 'text-neutral-500 line-through' : 'text-neutral-300'"> |
| 163 | + {{ option.label }} |
| 164 | + </span> |
| 165 | + </label> |
| 166 | + </div> |
| 167 | + </DemoSection> |
| 168 | + </aside> |
| 169 | + |
| 170 | + <main class="flex min-w-0 flex-1 flex-col gap-6"> |
| 171 | + <DemoHeader |
| 172 | + eyebrow="Assembled Select" |
| 173 | + title="Interactive preview" |
| 174 | + description="The batteries-included v1 Select composes primitives into a ready-to-use control. Use the sidebar to explore prop combinations." |
| 175 | + /> |
| 176 | + |
| 177 | + <DemoPanel accent> |
| 178 | + <Select |
| 179 | + v-model="model" |
| 180 | + :options="options" |
| 181 | + :multiple="multiple" |
| 182 | + :searchable="searchable" |
| 183 | + :clearable="clearable" |
| 184 | + :disabled="disabled" |
| 185 | + :loading="loading" |
| 186 | + :placeholder="placeholder" |
| 187 | + /> |
| 188 | + |
| 189 | + <DemoValue label="modelValue"> |
| 190 | + <code class="font-mono text-xs">{{ modelDisplay }}</code> |
| 191 | + </DemoValue> |
| 192 | + </DemoPanel> |
| 193 | + |
| 194 | + <DemoSection title="Quick reference"> |
| 195 | + <div class="grid gap-3 text-sm text-neutral-400"> |
| 196 | + <p> |
| 197 | + Run <code class="rounded bg-neutral-800 px-1.5 py-0.5 font-mono text-xs text-neutral-300">npm run dev:v1-playground</code> |
| 198 | + to start this environment. |
| 199 | + </p> |
| 200 | + <p> |
| 201 | + Import path: <code class="rounded bg-neutral-800 px-1.5 py-0.5 font-mono text-xs text-neutral-300">@v1/assembled/Select.vue</code> |
| 202 | + or primitives from <code class="rounded bg-neutral-800 px-1.5 py-0.5 font-mono text-xs text-neutral-300">@v1/primitives</code>. |
| 203 | + </p> |
| 204 | + </div> |
| 205 | + </DemoSection> |
| 206 | + </main> |
| 207 | + </div> |
| 208 | + </div> |
| 209 | +</template> |
0 commit comments