initial commit

This commit is contained in:
Zoe
2023-05-30 15:04:51 -05:00
commit 539805dacb
20 changed files with 14788 additions and 0 deletions

42
README.md Normal file
View File

@@ -0,0 +1,42 @@
# Nuxt 3 Minimal Starter
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install the dependencies:
```bash
# yarn
yarn install
# npm
npm install
# pnpm
pnpm install
```
## Development Server
Start the development server on `http://localhost:3000`
```bash
npm run dev
```
## Production
Build the application for production:
```bash
npm run build
```
Locally preview production build:
```bash
npm run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

5
app.vue Normal file
View File

@@ -0,0 +1,5 @@
<template>
<div>
<NuxtPage />
</div>
</template>

125
assets/styles.css Normal file
View File

@@ -0,0 +1,125 @@
body {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
color: white;
margin: 0;
padding: 0;
margin: 12px;
background: #070809;
}
.vl-button,
.vl-toggle-button {
border: solid 1px #1d1e1f;
background: #1a1b1c;
}
.vl-button,
.vl-toggle-button,
.vl-dropdown button {
display: flex;
justify-content: center;
align-items: center;
padding: 0;
margin: 0;
color: #efefef;
padding-left: 8px;
padding-right: 8px;
padding-top: 6px;
padding-bottom: 6px;
border-radius: 8px;
transition-property: background-color, border-color;
transition-duration: 250ms;
transition-timing-function: ease-in;
cursor: pointer;
min-width: 65px;
}
.vl-button svg {
margin-left: 4px;
}
.vl-toggle-group {
display: flex;
flex-direction: row;
margin: 0;
padding: 0;
border: 0;
}
.vl-toggle-button {
min-width: fit-content;
}
.vl-toggle-button svg {
margin-left: 0;
}
.vl-button:hover,
button:hover {
background: #1d1e1f;
border-color: #202122;
}
.vl-button:focus, button:focus {
outline: 2px solid #4e367e;
background: #1d1e1f;
border-color: #202122;
}
.vl-button:active,
button:active {
background: #191a1b;
border-color: #1c1d1e;
}
.vl-dropdown {
box-shadow: 0 0 12px #00000071;
}
.vl-dropdown-enter-active,
.vl-dropdown-leave-active {
transition: all 300ms ease;
}
.vl-dropdown-enter-from,
.vl-dropdown-leave-to {
transform: translateY(-8px) scale(0.85);
opacity: 0;
}
.vl-toggle-button {
color: #bcbcbc;
}
.vl-toggle-group button {
border-radius: 0px;
border-left: 0px;
}
.vl-toggle-group button:first-child {
border-radius: 8px 0px 0px 8px;
}
.vl-toggle-group button:last-child {
border-radius: 0px 8px 8px 0px;
border-left: 0px;
}
/*
.vl-select-left:active, .vl-select-middle:active, .vl-select-right:active, .vl-select-left:hover, .vl-select-middle:hover, .vl-select-right:hover {
border-color: #1d1e1f;
}*/
.vl-toggle-button[data-state="checked"],
.vl-toggle-button[data-state="on"] {
background: #1f2021;
color: #efefef;
}
.vl-toggle-button:focus {
position: relative;
z-index: 1;
box-shadow: #4e367e 0px 0px 0px 2px
}

View File

@@ -0,0 +1,24 @@
<script setup>
const item = inject('accordionItem');
const contentHeight = ref(0);
const content = ref(null);
watch(item.hidden, () => {
if (!item.hidden.value) {
setTimeout(() => {
contentHeight.value = content.value.offsetHeight;
})
}
})
</script>
<template>
<div :id="`vueless-${item.index}`" role="region" :aria-labelledby="`vueless-${item.index}`" class="vl-accordion-content"
:style="`--vueless-accordion-content-height: ${contentHeight}px`"
:data-state="(item.isOpen.value) ? 'open' : 'closed'"
@animationend="(!item.isOpen.value) ? item.hidden.value = true : ''" :hidden="item.hidden.value">
<div ref="content">
<slot />
</div>
</div>
</template>

View File

@@ -0,0 +1,194 @@
<script setup>
const props = defineProps({
type: {
type: String,
default: "multiple"
},
defaultValue: {
type: String
}
})
const accordion = ref(null);
const accordionItems = ref([])
function toggleAccordion(index) {
const item = accordionItems.value[index];
if (props.type === "single") {
// close everything but the one we just opened
accordionItems.value.forEach((item, i) => {
if (i === index) return;
item.isOpen = false;
})
}
if (item.hidden) {
item.hidden = false;
}
item.isOpen = !item.isOpen;
}
const registerAccordionItem = (value) => {
const item = { isOpen: ref(false), hidden: ref(true), value }
accordionItems.value.push(item);
return { index: accordionItems.value.indexOf(item), isOpen: item.isOpen, hidden: item.hidden, value };
};
const unregisterAccordionItem = (index) => {
accordionItems.value.splice(index, 1);
};
provide('accordion', { registerAccordionItem, unregisterAccordionItem, toggleAccordion })
function keydown(event) {
const headers = Array.from(accordion.value.querySelectorAll(".vl-accordion-header"));
if (event.key === "ArrowUp") {
event.preventDefault();
const focusedElement = document.activeElement;
const currentIndex = headers.indexOf(focusedElement.parentElement);
const nextIndex = currentIndex > 0 ? currentIndex - 1 : headers.length - 1;
const nextButton = headers[nextIndex].querySelector("button");
nextButton.focus();
return;
}
if (event.key === "ArrowDown") {
event.preventDefault();
const focusedElement = document.activeElement;
const currentIndex = headers.indexOf(focusedElement.parentElement);
const nextIndex = currentIndex < headers.length - 1 ? currentIndex + 1 : 0;
const nextButton = headers[nextIndex].querySelector("button");
nextButton.focus();
return;
}
if (event.key === "End") {
event.preventDefault();
return headers[headers.length - 1].querySelector("button").focus();
}
if (event.key === "Home") {
event.preventDefault();
return headers[0].querySelector("button").focus();
}
}
onMounted(() => {
if (props.defaultValue) {
const item = accordionItems.value.filter(item => item.value === props.defaultValue)[0];
item.isOpen = true;
item.hidden = false;
}
})
</script>
<template>
<div class="vl-accordion" ref="accordion" @keydown="keydown($event)">
<slot />
</div>
</template>
<style>
.vl-accordion {
border-radius: 6px;
background-color: #1D1E1F;
box-shadow: 0 0 16px 0 #07070738;
width: 70%;
user-select: none;
cursor: default;
}
.vl-accordion-content {
width: var(--vueless-accordion-content-width);
background-color: #0B0C0D;
overflow: hidden;
transform-origin: top center;
height: 0;
}
.vl-accordion-item:focus-within {
position: relative;
z-index: 1;
box-shadow: #4e367e 0px 0px 0px 2px
}
.vl-accordion-content div {
padding: 15px 20px;
}
.vl-accordion-content[data-state="closed"] {
animation: 300ms cubic-bezier(0.87, 0, 0.13, 1) 0s 1 normal forwards running closeAccordion;
}
.vl-accordion-content[data-state="open"] {
animation: 300ms cubic-bezier(0.87, 0, 0.13, 1) 0s 1 normal forwards running openAccordion;
}
.vl-accordion-item h3 button svg {
transition: transform 300ms ease;
}
.vl-accordion-item[data-state="open"] h3 button svg {
transform: rotate(180deg);
}
.vl-accordion-item {
background-color: #161718;
height: 100%;
margin-top: 1px;
overflow: hidden;
}
.vl-accordion-item:first-child {
margin-top: 0px;
border-radius: 6px 6px 0 0;
}
.vl-accordion-item:last-child {
border-radius: 0 0 6px 6px;
}
.vl-accordion-header:hover {
background-color: #131415;
}
.vl-accordion-header button {
all: unset;
background-color: transparent;
height: 45px;
display: flex;
align-items: center;
justify-content: space-between;
line-height: 1;
padding: 0px 20px;
flex: 1 1 0%;
box-shadow: #1D1E1F 0px 1px 0px;
}
.vl-accordion-header {
all: unset;
display: flex;
}
@keyframes closeAccordion {
0% {
height: var(--vueless-accordion-content-height);
}
100% {
height: 0px;
}
}
@keyframes openAccordion {
0% {
height: 0px;
}
100% {
height: var(--vueless-accordion-content-height);
}
}
</style>

View File

@@ -0,0 +1,19 @@
<script setup>
const props = defineProps({
value: {
type: String,
default: "",
}
})
const accordion = inject('accordion')
const item = accordion.registerAccordionItem(props.value);
provide('accordionItem', item);
</script>
<template>
<div class="vl-accordion-item" :data-state="(item.isOpen.value) ? 'open' : 'closed'">
<slot />
</div>
</template>

View File

@@ -0,0 +1,13 @@
<script setup>
const item = inject('accordionItem');
const { toggleAccordion } = inject('accordion');
</script>
<template>
<h3 class="vl-accordion-header">
<button :id="`vueless-${item.index}`" :aria-controls="`vueless-${item.index}`" :aria-expanded="item.isOpen.value"
@click="toggleAccordion(item.index)">
<slot />
</button>
</h3>
</template>

151
components/vlDropdown.vue Normal file
View File

@@ -0,0 +1,151 @@
<script setup>
const hex = ref((Math.random() * 0xfffff * 1000000).toString(16).slice(0, 6).toString());
const dropdownOpen = ref(false);
const dropdownButton = ref(null);
const dropdown = ref(null);
function dropdownButtonKeypress(event) {
if (event.key !== "Enter" && event.key !== " " && event.key !== "ArrowUp" && event.key !== "ArrowDown" || dropdownOpen.value === true) return;
event.preventDefault();
if (event.key === "ArrowUp") {
dropdownOpen.value = true;
setTimeout(() => {
dropdown.value.children[dropdown.value.children.length - 1].focus();
})
return;
}
if (event.key === "ArrowDown") {
dropdownOpen.value = true;
setTimeout(() => {
dropdown.value.children[1].focus();
})
return;
}
if (event.key === "Enter" || event.key === " ") {
dropdownOpen.value = !dropdownOpen.value;
setTimeout(() => {
dropdown.value.children[1].focus();
})
}
}
function dropdownKeypress(event) {
if (event.key === "ArrowUp") {
event.preventDefault();
const focusedElement = document.activeElement;
const elements = Array.from(dropdown.value.children).filter(
(el) => el.tabIndex === 0
);
const currentIndex = elements.indexOf(focusedElement);
const nextIndex = currentIndex > 0 ? currentIndex - 1 : elements.length - 1;
elements[nextIndex].focus();
}
if (event.key === "ArrowDown") {
event.preventDefault();
const focusedElement = document.activeElement;
const elements = Array.from(dropdown.value.children).filter(
(el) => el.tabIndex === 0
);
const currentIndex = elements.indexOf(focusedElement);
const nextIndex = currentIndex < elements.length - 1 ? currentIndex + 1 : 0;
elements[nextIndex].focus();
}
if (event.key === "Escape" || event.key === "Esc") {
event.preventDefault();
dropdownButton.value.focus();
dropdownOpen.value = false;
}
}
</script>
<template>
<div class="vl-dropdown-button">
<button :id="`menubutton-${hex}`" :aria-controls="`menu-${hex}`" class="vl-button"
@click.exact="dropdownOpen = !dropdownOpen;" @keydown="dropdownButtonKeypress($event)" ref="dropdownButton"
aria-haspopup="menu" :aria-expanded="dropdownOpen" :data-state="(dropdownOpen) ? 'open' : 'closed'">
Dropdown
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m6 9l6 6l6-6" />
</svg>
</button>
<Transition name="vl-dropdown">
<div class="vl-dropdown" :id="`menu-${hex}`" tabindex="-1" :aria-labelledby="`menubutton-${hex}`"
:data-state="(dropdownOpen) ? 'open' : 'closed'" :key="dropdownOpen" :hidden="!dropdownOpen" role="menu"
@keydown="dropdownKeypress($event)" :aria-hidden="!dropdownOpen" ref="dropdown">
<span class="vl-diamond"><svg style="display:block" width="10" height="5" viewBox="0 0 30 10"
preserveAspectRatio="none">
<polygon points="0,0 30,0 15,10" style=""></polygon>
</svg></span>
<slot />
</div>
</Transition>
</div>
</template>
<style>
.vl-dropdown-button .vl-button[data-state="open"] svg {
transform: rotate(180deg);
}
.vl-dropdown-button .vl-button svg {
transition: transform 300ms ease;
}
.vl-dropdown-button {
position: relative;
}
.vl-diamond {
position: absolute;
top: 0px;
transform-origin: center 0px 0px;
transform: rotate(180deg) translateX(50%);
left: 50%;
}
.vl-diamond svg {
fill: #070809;
}
.vl-dropdown[data-state="open"] {
display: flex;
}
.vl-dropdown {
position: absolute;
padding-top: 4px;
padding-bottom: 4px;
padding-right: 4px;
padding-left: 4px;
margin-top: 8px;
left: -25%;
width: 150%;
background: #070809;
border-radius: 6px;
flex-direction: column;
gap: 2px 0;
}
.vl-dropdown button {
width: 100%;
background: unset;
border: unset;
justify-content: start;
border-radius: 6px;
}
.vl-dropdown button:hover {
background: #090a0b;
}
.vl-dropdown button:focus {
outline: 0;
background-color: hsl(226, 79%, 37.5%);
}
</style>

227
components/vlSlider.vue Normal file
View File

@@ -0,0 +1,227 @@
<script setup>
const props = defineProps({
min: {
type: Number,
default: 0
},
max: {
type: Number,
default: 1
},
step: {
type: Number,
default: 0.01
},
value: {
type: Number,
default: 0
},
id: {
type: String,
},
label: {
type: String,
}
})
const emit = defineEmits(['update:modelValue'])
const modelValue = ref(props.value);
watch(modelValue, (newValue) => {
emit('update:modelValue', newValue);
});
emit('update:modelValue', props.value);
function roundValueToStep(number) {
const clampedNumber = Math.min(Math.max(number, props.min), props.max);
const quotient = Math.floor((clampedNumber - props.min) / props.step);
const lowerValue = props.min + quotient * props.step;
const upperValue = lowerValue + props.step;
if (clampedNumber - lowerValue < upperValue - clampedNumber) {
return lowerValue; // Round down
} else {
return upperValue; // Round up
}
}
const rangeSlider = ref(null);
const rangeThumb = ref(null);
const rangeValue = ref({
real: ref(roundValueToStep(props.value)),
stepped: ref(roundValueToStep(props.value))
});
const sliderPercentage = ref((rangeValue.value.real / props.max) * 100);
const steppedSliderPercentage = ref((rangeValue.value.stepped / props.max) * 100)
const steps = (props.max / props.step)
function snapToStep(percentage) {
const pixelsPerStep = rangeSlider.value.clientWidth / steps;
let step = roundValueToStep((((percentage / 100) * rangeSlider.value.clientWidth) / pixelsPerStep) * props.step);
const split = step.toString().split('.');
// fix float imprecisions by making sure the number only has at most 3 digits after the decimal point
if (split.length > 1) {
split[1] = split[1].slice(0,3);
step = parseFloat(split.join('.'));
}
rangeValue.value.stepped = step;
modelValue.value = step;
return (rangeValue.value.stepped / props.max) * 100;
}
function calculateSliderPosition(pointerEvent) {
if (!rangeSlider.value) return;
let pointerPosFromRangeStart = pointerEvent.clientX - rangeSlider.value.getClientRects()[0].x;
if (pointerPosFromRangeStart < 0) {
pointerPosFromRangeStart = 0;
}
if (pointerPosFromRangeStart > rangeSlider.value.clientWidth) {
pointerPosFromRangeStart = rangeSlider.value.clientWidth;
}
const value = (pointerPosFromRangeStart / rangeSlider.value.clientWidth);
const newSliderPercentage = value * 100;
if (newSliderPercentage === sliderPercentage.value) return;
updateSliderPosition(newSliderPercentage);
}
function updateSliderPosition(newSliderPercentage) {
sliderPercentage.value = newSliderPercentage;
steppedSliderPercentage.value = snapToStep(newSliderPercentage);
}
function pointerDown(pointerEvent) {
document.body.addEventListener('pointermove', calculateSliderPosition)
calculateSliderPosition(pointerEvent);
document.body.addEventListener('pointerup', () => {
rangeThumb.value.focus();
document.body.removeEventListener('pointermove', calculateSliderPosition)
}, { once: true })
}
function keydown(keyboardEvent) {
if (keyboardEvent.keyCode >= 37 && keyboardEvent.keyCode <= 40) {
keyboardEvent.preventDefault();
const direction = (keyboardEvent.key === "ArrowRight" || keyboardEvent.key === "ArrowUp") ? 'forward' : 'backward';
if (direction === 'forward') {
if (rangeValue.value.stepped + props.step > props.max) return;
rangeValue.value.stepped += props.step;
} else {
if (rangeValue.value.stepped - props.step < props.min) return;
rangeValue.value.stepped -= props.step;
}
return updateSliderPosition((rangeValue.value.stepped / props.max) * 100);
}
if (keyboardEvent.keyCode === 36 || keyboardEvent.keyCode === 35) {
keyboardEvent.preventDefault();
if (keyboardEvent.key === "Home") {
rangeValue.value.stepped = props.min;
} else {
rangeValue.value.stepped = props.max;
}
return updateSliderPosition((rangeValue.value.stepped / props.max) * 100);
}
if (keyboardEvent.keyCode === 33 || keyboardEvent.keyCode === 34) {
keyboardEvent.preventDefault();
const direction = (keyboardEvent.key === "PageUp") ? 'forward' : 'backward';
let multiplier = 2;
if (direction === 'forward') {
if (rangeValue.value.stepped + (props.step * multiplier) > props.max) multiplier = 1;
if (rangeValue.value.stepped + (props.step * multiplier) > props.max) return;
rangeValue.value.stepped += (props.step * multiplier);
} else {
if (rangeValue.value.stepped - (props.step * multiplier) > props.max) multiplier = 1;
if (rangeValue.value.stepped - (props.step * multiplier) > props.max) return;
rangeValue.value.stepped -= (props.step * multiplier);
}
return updateSliderPosition((rangeValue.value.stepped / props.max) * 100);
}
}
onMounted(() => {
if (props.id) {
document.body.querySelector(`[for="${props.id}"]`).addEventListener("click", () => {
rangeThumb.value.focus();
})
}
})
onUnmounted(() => {
if (props.id) {
document.body.querySelector(`[for="${props.id}"]`).removeEventListener("click", () => {
rangeThumb.value.focus();
})
}
})
</script>
<template>
<div class="vl-range-slider" ref="rangeSlider" @pointerdown="pointerDown($event)">
<span class="vl-range-track">
<span class="vl-range-track-filled" :style="'width: ' + steppedSliderPercentage + '%;'">
</span>
</span>
<span class="vl-range-thumb" :aria-label="props.label" :aria-labelledby="props.id" role="slider" ref="rangeThumb"
:aria-valuemin="props.min" :aria-valuemax="props.max" aria-orientation="horizontal" :aria-valuenow="rangeValue.stepped" @keydown="keydown($event)"
tabindex="0" :style="'left: ' + steppedSliderPercentage + '%;'"></span>
</div>
</template>
<style>
.vl-range-slider {
position: relative;
display: flex;
align-items: center;
width: 200px;
height: 20px;
margin: 4px 0;
}
.vl-range-track,
.vl-range-track-filled {
display: block;
height: 4px;
width: 100%;
background: #313234;
border-radius: 99999px;
}
.vl-range-track-filled {
background: hsl(226, 79%, 47.5%);
width: 0%;
}
.vl-range-thumb {
position: absolute;
display: block;
height: 20px;
width: 20px;
background: #f7f7f7;
border-radius: 99999px;
transform: translateX(-50%);
}
.vl-range-thumb:focus {
outline: 4px solid #b7b7b74a;
}
</style>

19
components/vlToggle.vue Normal file
View File

@@ -0,0 +1,19 @@
<script setup>
const emit = defineEmits(['update:modelValue'])
const checked = ref(false);
watch(checked, (newValue) => {
emit('update:modelValue', newValue);
});
emit('update:modelValue', checked.value);
function toggleChecked() {
checked.value = !checked.value;
}
</script>
<template>
<button class="vl-toggle-button" @click="toggleChecked()" :aria-pressed="checked" :data-state="(checked) ? 'on' : 'off'">
<slot />
</button>
</template>

View File

@@ -0,0 +1,95 @@
<script setup>
const props = defineProps({
type: {
type: String,
default: "single"
},
defaultValue: {
type: String || Array,
}
})
const emit = defineEmits(['update:modelValue'])
const toggleGroup = ref(null);
const selected = ref((props.type === "single") ? props.defaultValue : [].concat(props.defaultValue));
watch(selected, (newValue) => {
emit('update:modelValue', newValue);
})
emit('update:modelValue', selected.value);
const items = ref([]);
function registerToggleItem(value) {
const item = { value, tabIndex: (props.defaultValue.includes(value)) ? ref('0') : ref('-1') }
const itemIndex = (items.value.push(item)) - 1
return { item, index: itemIndex };
}
function toggleItem(index) {
if (props.type === "single") {
selected.value = items.value[index].value;
return;
}
if (selected.value.indexOf(items.value[index].value) > -1) {
selected.value = selected.value.filter((e) => e !== items.value[index].value)
return;
}
selected.value.push(items.value[index].value);
}
function keydown(event) {
const elements = Array.from(toggleGroup.value.children).filter(
(el) => el.nodeName.toLowerCase() === 'button'
);
if (event.key === "Home") {
event.preventDefault();
items.value.forEach((el, i) => {
if (i === 0) return items.value[i].tabIndex = '0';
return items.value[i].tabIndex = '-1';
});
elements[0].focus();
}
if (event.key === "End") {
event.preventDefault();
items.value.forEach((el, i) => {
if (i === items.value.length - 1) return items.value[i].tabIndex = '0';
return items.value[i].tabIndex = '-1';
});
elements[items.value.length-1].focus();
}
const focusedElement = document.activeElement;
const currentIndex = elements.indexOf(focusedElement);
if (event.key === "ArrowRight" || event.key === "ArrowDown") {
event.preventDefault();
const nextIndex = currentIndex < elements.length - 1 ? currentIndex + 1 : 0;
items.value.forEach((el, i) => {
if (i === nextIndex) return items.value[i].tabIndex = '0';
return items.value[i].tabIndex = '-1';
});
elements[nextIndex].focus();
}
if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
event.preventDefault();
const nextIndex = currentIndex > 0 ? currentIndex - 1 : elements.length - 1;
items.value.forEach((el, i) => {
if (i === nextIndex) return items.value[i].tabIndex = '0';
return items.value[i].tabIndex = '-1';
});
elements[nextIndex].focus();
}
}
provide('toggleGroup', { registerToggleItem, toggleItem, selected })
</script>
<template>
<div role="group" ref="toggleGroup" class="vl-toggle-group" @keydown="keydown($event)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup>
const props = defineProps({
value: {
type: String,
default: ""
},
})
const toggleGroup = inject('toggleGroup');
const item = toggleGroup.registerToggleItem(props.value);
</script>
<template>
<button role="radio" type="button" @click="toggleGroup.toggleItem(item.index)" :aria-checked="toggleGroup.selected.value === props.value || toggleGroup.selected.value.indexOf(props.value) > -1" :tabindex="item.item.tabIndex.value" :data-state="(toggleGroup.selected.value === props.value || toggleGroup.selected.value.indexOf(props.value) > -1) ? 'checked' : 'unchecked'" class="vl-toggle-button">
<slot />
</button>
</template>

View File

@@ -0,0 +1,87 @@
<script setup>
const props = defineProps({
checked: {
type: Boolean,
default: false,
},
id: {
type: String
},
label: {
type: String
},
disabled: {
type: Boolean
}
})
const emit = defineEmits(['update:modelValue'])
const active = ref(props.checked);
watch(() => props.checked, (newValue) => {
active.value = newValue;
})
watch(active, (newValue) => {
emit('update:modelValue', newValue);
});
emit('update:modelValue', props.checked)
onMounted(() => {
document.querySelector(`[for='${props.id}']`).addEventListener('click', () => {
if (props.disabled) return;
active.value = !active.value
});
})
</script>
<template>
<button role="switch" class="vl-toggle-switch" :aria-disabled="(props.disabled === true) ? 'true' : 'false'"
:aria-label="label" :aria-labelledby="id" :tabindex="(disabled) ? '-1' : '0'"
@click="(!disabled) ? active = !active : ''" :aria-checked="active" :data-state="(active) ? 'checked' : 'unchecked'">
<div></div>
</button>
</template>
<style scoped> .vl-toggle-switch {
font-size: inherit;
border: 0;
cursor: pointer;
width: 2.5em;
height: 1.4em;
background: #1a1b1c;
border-radius: 100px;
padding: 0.125rem 0.25rem;
position: relative;
transition: background-color 0.3s ease;
}
.vl-toggle-switch[aria-disabled="true"] div {
background: #919191;
}
.vl-toggle-switch div {
position: relative;
left: 0;
width: 1em;
height: 1em;
background: #f7f7f7;
border-radius: 90px;
pointer-events: none;
transition: all 0.3s;
}
.vl-toggle-switch[data-state="checked"] {
background: hsl(226, 79%, 47.5%);
}
.vl-toggle-switch[data-state="checked"] div {
left: 100%;
transform: translateX(-100%);
}
.vl-toggle-switch:active div {
width: 1.3em;
}
</style>

6
nuxt.config.ts Normal file
View File

@@ -0,0 +1,6 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
css: [
'~/assets/styles.css',
]
})

13517
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

15
package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "nuxt-app",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"devDependencies": {
"@types/node": "^18",
"nuxt": "^3.5.1"
}
}

203
pages/index.vue Normal file
View File

@@ -0,0 +1,203 @@
<script setup>
const volume = ref(25);
const airplaneMode = ref(false);
const toggle = ref(false);
const group = ref('center');
const burgerIngredients = ref(['tomato', 'cheese']);
</script>
<template>
<div class="presentation">
<div class="container">
<button class="vl-button">
Button
</button>
</div>
<div class="container">
<Transition>
<VlDropdown>
<button>
Button
</button>
<button>
Button
</button>
<button>
Button
</button>
</VlDropdown>
</Transition>
</div>
<div class="container">
<div>
{{ group }}
<VlToggleGroup v-model="group" type="single" :defaultValue="group" aria-label="Text alignment">
<VlToggleGroupItem aria-label="Left aligned" value="left">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h10M4 18h14" />
</svg>
</VlToggleGroupItem>
<VlToggleGroupItem aria-label="Center aligned" value="center">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M8 12h8M6 18h12" />
</svg>
</VlToggleGroupItem>
<VlToggleGroupItem aria-label="Right aligned" value="right">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16m-10 6h10M6 18h14" />
</svg>
</VlToggleGroupItem>
</VlToggleGroup>
</div>
<div>
{{ burgerIngredients }}
<VlToggleGroup v-model="burgerIngredients" type="multiple" :defaultValue="burgerIngredients" aria-label="Burger ingredients">
<VlToggleGroupItem aria-label="Cheese" value="cheese">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M4.519 20.008L21 20v-3.5a2 2 0 1 1 0-4V9H4.278" />
<path
d="m21 9l-9.385-4.992c-2.512.12-4.758 1.42-6.327 3.425C3.865 9.253 3 11.654 3 14.287c0 2.117.56 4.085 1.519 5.721M15 13v.01M8 13v.01M11 16v.01" />
</g>
</svg>
</VlToggleGroupItem>
<VlToggleGroupItem aria-label="Tomato" value="tomato">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 48 48">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4">
<path
d="M24 44c11.046 0 20-7.387 20-16.5c0-6.442-4.475-11.799-11-14.516L29.5 14.5L30 20l-6.5-2l-6.5 2v-5.5l-3-1.516C8.022 15.837 4 21.393 4 27.5C4 36.613 12.954 44 24 44Z" />
<path
d="m23.5 4l3.809 5.117L36 9.91l-6.337 4.573L31.5 21l-8-3l-8 3l1.837-6.517L11 9.91l8.691-.793L23.5 4Z" />
</g>
</svg>
</VlToggleGroupItem>
<VlToggleGroupItem aria-label="Onions" value="onion">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 32 32">
<path fill="currentColor"
d="M12.74 2.458c0-1.333 1.658-1.981 2.553-.959l1.182 1.354L17.657 1.5c.895-1.023 2.553-.375 2.553.958v1.83c0 .738.44 1.405 1.136 1.712c4.464 1.892 7.604 6.325 7.604 11.488c0 6.076-4.349 11.146-10.11 12.25v.01c0 .69-.56 1.25-1.25 1.25h-2.16c-.69 0-1.25-.56-1.25-1.25v-.005C8.458 28.632 4 23.438 4 17.478c0-5.166 3.143-9.589 7.608-11.48a1.86 1.86 0 0 0 1.132-1.7v-1.84Zm2 1.449v.39a3.86 3.86 0 0 1-2.346 3.54l-.004.002c-.184.077-.364.16-.542.248c-1.96 2.085-3.118 5.341-3.118 8.84c0 5.31 2.645 9.753 6.153 10.805c-1.346-2.014-2.063-6.514-2.063-10.814c0-3.76.5-7.21 1.37-9.44l.75.29c-.82 2.11-1.31 5.53-1.31 9.15c0 3.03.33 5.88.94 8.02c.552 1.949 1.2 2.82 1.69 3.022c.147.003.295.003.441 0c.489-.203 1.137-1.074 1.689-3.022c.6-2.14.94-4.98.94-8.02c0-3.61-.49-7.03-1.31-9.15l.75-.29c.87 2.24 1.37 5.68 1.37 9.44c-.007 4.3-.72 8.801-2.064 10.814c3.508-1.05 6.154-5.494 6.154-10.804c0-3.495-1.155-6.746-3.109-8.832a10.358 10.358 0 0 0-.56-.257l-.012-.005a3.874 3.874 0 0 1-2.339-3.546v-.381l-.635.726a1.456 1.456 0 0 1-2.186.016l-.006-.007l-.643-.735ZM9.67 9.52A10.437 10.437 0 0 0 6 17.478c0 3.797 2.18 7.24 5.335 9.08c-.365-.372-.71-.786-1.035-1.24c-1.61-2.25-2.49-5.23-2.49-8.39c0-2.728.67-5.326 1.86-7.407Zm11.817 17.167a10.474 10.474 0 0 0 5.463-9.2c0-3.176-1.416-6.025-3.649-7.947c1.184 2.078 1.849 4.668 1.849 7.387c0 3.16-.88 6.14-2.49 8.39a10.42 10.42 0 0 1-1.173 1.37Z" />
</svg>
</VlToggleGroupItem>
</VlToggleGroup>
</div>
</div>
<div class="container">
<div style="display: flex; gap: 6px 0; flex-direction: column;">
<div style="display: flex;">
<label for="airplane-mode" style=" margin-right: 8px;">Airplane mode</label>
<VlToggleSwitch :checked="airplaneMode" v-model="airplaneMode" id="airplane-mode" />
</div>
<div style="display: flex;">
<label for="wifi" style=" margin-right: 8px;">Wifi</label>
<VlToggleSwitch :checked="!airplaneMode" :disabled="(airplaneMode) ? true : false" id="wifi" />
</div>
</div>
</div>
<div class="container">
<div>
<label for="Volume">Volume: {{ volume }}</label>
<VlSlider :value="volume" v-model="volume" id="Volume" :step="1" :max="100" />
</div>
<VlSlider label="Volume" :step="1" :min="0" :max="10" />
<VlSlider label="Volume" :value="1" :min="0" :max="9" :step="3" />
</div>
<div class="container">
<VlAccordion type="single" defaultValue="item-1">
<VlAccordionItem value="item-1">
<VlAccordionTrigger>
Section 1
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m6 9l6 6l6-6" />
</svg>
</VlAccordionTrigger>
<VlAccordionContent>
Content for section 1
</VlAccordionContent>
</VlAccordionItem>
<VlAccordionItem value="item-2">
<VlAccordionTrigger>
Section 2
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m6 9l6 6l6-6" />
</svg>
</VlAccordionTrigger>
<VlAccordionContent>
Content for section 2
</VlAccordionContent>
</VlAccordionItem>
<VlAccordionItem value="item-3">
<VlAccordionTrigger>
Section 3
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m6 9l6 6l6-6" />
</svg>
</VlAccordionTrigger>
<VlAccordionContent>
Content for section 3
</VlAccordionContent>
</VlAccordionItem>
<VlAccordionItem value="item-4">
<VlAccordionTrigger>
Section 4
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m6 9l6 6l6-6" />
</svg>
</VlAccordionTrigger>
<VlAccordionContent>
Content for section 4
</VlAccordionContent>
</VlAccordionItem>
</VlAccordion>
</div>
<div class="container">
<div>
{{ toggle }}
<VlToggle v-model="toggle" aria-label="Toggle italic">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M11 5h6M7 19h6m1-14l-4 14" />
</svg>
</VlToggle>
</div>
</div>
</div>
</template>
<style scoped>
.presentation {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 12px;
min-height: calc(100vh - 24px);
}
.container {
position: relative;
overflow: hidden;
display: grid;
place-items: center;
width: 420px;
height: 275px;
background: #121314;
border-radius: 12px;
border: 1px solid #232425;
box-shadow: 0px 0px 12px #00000012;
}</style>

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

3
server/tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

26
tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": [
"ESNext",
"ESNext.AsyncIterable",
"DOM"
],
"strict": true,
"sourceMap": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true,
"jsx": "preserve",
"noUncheckedIndexedAccess": true,
"noImplicitAny": true,
"allowJs": true
},
"extends": "./.nuxt/tsconfig.json"
}