Unify switch controls
This commit is contained in:
1 parent
b9a4b53109
commit
9b314ad817
7 files changed
+160
-257
No files matched your search
@@ -33,7 +33,7 @@ import {
|
|||||||
setClipDragSource,
|
setClipDragSource,
|
||||||
} from '@/composables/useClipDragSource'
|
} from '@/composables/useClipDragSource'
|
||||||
import type { Asset } from '@/types/scene'
|
import type { Asset } from '@/types/scene'
|
||||||
import { confirmDialog, promptDialog } from '@/components/ui'
|
import { confirmDialog, promptDialog, ToggleSwitch } from '@/components/ui'
|
||||||
import { loadSceneImages, renderButtonThumbnail, renderSvgThumbnail } from '@/lib/render'
|
import { loadSceneImages, renderButtonThumbnail, renderSvgThumbnail } from '@/lib/render'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
|
||||||
@@ -624,21 +624,11 @@ function onAssetDragStart(e: DragEvent, asset: ImageOrSvgAsset) {
|
|||||||
>
|
>
|
||||||
<div class="assets-header">
|
<div class="assets-header">
|
||||||
<h3>Assets</h3>
|
<h3>Assets</h3>
|
||||||
<label
|
<ToggleSwitch
|
||||||
class="thumbs-switch"
|
v-model="showThumbs"
|
||||||
|
:label="t('sidebar.thumbsToggle')"
|
||||||
v-tooltip="showThumbs ? t('sidebar.thumbnailsHide') : t('sidebar.thumbnailsShow')"
|
v-tooltip="showThumbs ? t('sidebar.thumbnailsHide') : t('sidebar.thumbnailsShow')"
|
||||||
>
|
/>
|
||||||
<span class="thumbs-switch-label">{{ t('sidebar.thumbsToggle') }}</span>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="thumbs-switch-input"
|
|
||||||
:checked="showThumbs"
|
|
||||||
@change="showThumbs = ($event.target as HTMLInputElement).checked"
|
|
||||||
/>
|
|
||||||
<span class="thumbs-switch-track">
|
|
||||||
<span class="thumbs-switch-knob"></span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="upload-btn" @click="triggerUpload">
|
<button class="upload-btn" @click="triggerUpload">
|
||||||
@@ -988,73 +978,6 @@ h4 {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compact slide switch labelled "Thumbs" — native checkbox stays hidden
|
|
||||||
* for a11y (keyboard + screen readers), the visible track + knob is
|
|
||||||
* driven purely by the `:checked` state. */
|
|
||||||
.thumbs-switch {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbs-switch-label {
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbs-switch-input {
|
|
||||||
/* Keep focus behaviour via the native input; the visual sits next to it. */
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbs-switch-track {
|
|
||||||
position: relative;
|
|
||||||
width: 26px;
|
|
||||||
height: 14px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--border);
|
|
||||||
transition: background 140ms var(--ease);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbs-switch-knob {
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
left: 2px;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--panel);
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
|
|
||||||
transition: left 140ms var(--ease), background 140ms var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbs-switch-input:checked + .thumbs-switch-track {
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbs-switch-input:checked + .thumbs-switch-track .thumbs-switch-knob {
|
|
||||||
left: 14px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbs-switch-input:focus-visible + .thumbs-switch-track {
|
|
||||||
outline: 2px solid var(--accent);
|
|
||||||
outline-offset: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbs-switch:hover .thumbs-switch-track {
|
|
||||||
filter: brightness(1.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section rows: unified layout — [caret + title][action?][count]. The
|
/* Section rows: unified layout — [caret + title][action?][count]. The
|
||||||
* count always sits at the far right so the right column is visually
|
* count always sits at the far right so the right column is visually
|
||||||
* stable regardless of whether a row has a create-action (+) beside it. */
|
* stable regardless of whether a row has a create-action (+) beside it. */
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { useTooltipPref } from '@/composables/useTooltipPref'
|
|||||||
import { useAssetThumbsPref } from '@/composables/useAssetThumbsPref'
|
import { useAssetThumbsPref } from '@/composables/useAssetThumbsPref'
|
||||||
import { useShowWelcomeOnStart } from '@/composables/useShowWelcomeOnStart'
|
import { useShowWelcomeOnStart } from '@/composables/useShowWelcomeOnStart'
|
||||||
import { showLocked } from '@/composables/useShowLocked'
|
import { showLocked } from '@/composables/useShowLocked'
|
||||||
import { Modal } from '@/components/ui'
|
import { Modal, ToggleSwitch } from '@/components/ui'
|
||||||
import { setLocale, t, useI18n } from '@/i18n'
|
import { setLocale, t, useI18n } from '@/i18n'
|
||||||
import type { Locale } from '@/i18n'
|
import type { Locale } from '@/i18n'
|
||||||
|
|
||||||
@@ -245,9 +245,10 @@ function applyTheme(t: Theme) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="row-toggle">
|
<label class="row-toggle">
|
||||||
<span class="switch" :class="{ on: tooltipsEnabled }">
|
<ToggleSwitch
|
||||||
<input type="checkbox" v-model="tooltipsEnabled" />
|
v-model="tooltipsEnabled"
|
||||||
</span>
|
:aria-label="t('settings.tooltipsLabel')"
|
||||||
|
/>
|
||||||
<div class="row-toggle-text">
|
<div class="row-toggle-text">
|
||||||
<div class="row-toggle-label">{{ t('settings.tooltipsLabel') }}</div>
|
<div class="row-toggle-label">{{ t('settings.tooltipsLabel') }}</div>
|
||||||
<div class="row-toggle-desc">{{ t('settings.tooltipsDesc') }}</div>
|
<div class="row-toggle-desc">{{ t('settings.tooltipsDesc') }}</div>
|
||||||
@@ -255,9 +256,10 @@ function applyTheme(t: Theme) {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="row-toggle">
|
<label class="row-toggle">
|
||||||
<span class="switch" :class="{ on: showLocked }">
|
<ToggleSwitch
|
||||||
<input type="checkbox" v-model="showLocked" />
|
v-model="showLocked"
|
||||||
</span>
|
:aria-label="t('settings.showLockedLabel')"
|
||||||
|
/>
|
||||||
<div class="row-toggle-text">
|
<div class="row-toggle-text">
|
||||||
<div class="row-toggle-label">{{ t('settings.showLockedLabel') }}</div>
|
<div class="row-toggle-label">{{ t('settings.showLockedLabel') }}</div>
|
||||||
<div class="row-toggle-desc">{{ t('settings.showLockedDesc') }}</div>
|
<div class="row-toggle-desc">{{ t('settings.showLockedDesc') }}</div>
|
||||||
@@ -265,9 +267,10 @@ function applyTheme(t: Theme) {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="row-toggle">
|
<label class="row-toggle">
|
||||||
<span class="switch" :class="{ on: thumbsEnabled }">
|
<ToggleSwitch
|
||||||
<input type="checkbox" v-model="thumbsEnabled" />
|
v-model="thumbsEnabled"
|
||||||
</span>
|
:aria-label="t('settings.assetThumbsLabel')"
|
||||||
|
/>
|
||||||
<div class="row-toggle-text">
|
<div class="row-toggle-text">
|
||||||
<div class="row-toggle-label">{{ t('settings.assetThumbsLabel') }}</div>
|
<div class="row-toggle-label">{{ t('settings.assetThumbsLabel') }}</div>
|
||||||
<div class="row-toggle-desc">{{ t('settings.assetThumbsDesc') }}</div>
|
<div class="row-toggle-desc">{{ t('settings.assetThumbsDesc') }}</div>
|
||||||
@@ -275,9 +278,10 @@ function applyTheme(t: Theme) {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="row-toggle">
|
<label class="row-toggle">
|
||||||
<span class="switch" :class="{ on: welcomeOnStart }">
|
<ToggleSwitch
|
||||||
<input type="checkbox" v-model="welcomeOnStart" />
|
v-model="welcomeOnStart"
|
||||||
</span>
|
:aria-label="t('settings.welcomeOnStartLabel')"
|
||||||
|
/>
|
||||||
<div class="row-toggle-text">
|
<div class="row-toggle-text">
|
||||||
<div class="row-toggle-label">{{ t('settings.welcomeOnStartLabel') }}</div>
|
<div class="row-toggle-label">{{ t('settings.welcomeOnStartLabel') }}</div>
|
||||||
<div class="row-toggle-desc">{{ t('settings.welcomeOnStartDesc') }}</div>
|
<div class="row-toggle-desc">{{ t('settings.welcomeOnStartDesc') }}</div>
|
||||||
@@ -550,10 +554,10 @@ input[type='number'] {
|
|||||||
letter-spacing: normal;
|
letter-spacing: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Switch + two-line label. Defensive resets defeat the scoped
|
/* Switch row + two-line label. Defensive resets defeat the scoped
|
||||||
blanket `label { flex-direction: column; text-transform: uppercase;
|
blanket `label { flex-direction: column; text-transform: uppercase;
|
||||||
font-size: 10px; flex: 1 1 0; }` that form-row labels in this file
|
font-size: 10px; flex: 1 1 0; }` that form-row labels in this file
|
||||||
depend on — without the reset that rule would stack the switch on
|
depend on — without the reset that rule would stack the control on
|
||||||
top of the text and uppercase everything inside. */
|
top of the text and uppercase everything inside. */
|
||||||
.row-toggle {
|
.row-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -599,47 +603,6 @@ input[type='number'] {
|
|||||||
text-transform: none;
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-toggle .switch {
|
|
||||||
position: relative;
|
|
||||||
width: 32px;
|
|
||||||
height: 18px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: var(--border);
|
|
||||||
border-radius: 999px;
|
|
||||||
transition: background 140ms var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-toggle .switch.on {
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-toggle .switch input {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
opacity: 0;
|
|
||||||
cursor: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-toggle .switch::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
left: 2px;
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
||||||
transition: transform 140ms var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-toggle .switch.on::after {
|
|
||||||
transform: translateX(14px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.clipart-row {
|
.clipart-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
import { useSceneStore } from '@/stores/scene'
|
import { useSceneStore } from '@/stores/scene'
|
||||||
import { useProjectName } from '@/composables/useProjectName'
|
import { useProjectName } from '@/composables/useProjectName'
|
||||||
import type { ButtonAsset, Layer } from '@/types/scene'
|
import type { ButtonAsset, Layer } from '@/types/scene'
|
||||||
import { Modal } from '@/components/ui'
|
import { Modal, ToggleSwitch } from '@/components/ui'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
import { hasDesktopRender } from '@/lib/desktopRenderExport'
|
import { hasDesktopRender } from '@/lib/desktopRenderExport'
|
||||||
|
|
||||||
@@ -454,13 +454,12 @@ function onUpdate(v: boolean) {
|
|||||||
:class="{ disabled: !hasHiddenLayers }"
|
:class="{ disabled: !hasHiddenLayers }"
|
||||||
v-tooltip="hasHiddenLayers ? undefined : t('export.skipHiddenNone')"
|
v-tooltip="hasHiddenLayers ? undefined : t('export.skipHiddenNone')"
|
||||||
>
|
>
|
||||||
<span class="switch" :class="{ on: skipHidden && hasHiddenLayers }">
|
<ToggleSwitch
|
||||||
<input
|
:model-value="skipHidden && hasHiddenLayers"
|
||||||
type="checkbox"
|
:aria-label="t('export.skipHidden')"
|
||||||
v-model="skipHidden"
|
:disabled="!hasHiddenLayers"
|
||||||
:disabled="!hasHiddenLayers"
|
@update:model-value="skipHidden = $event"
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
<div class="export-toggle-text">
|
<div class="export-toggle-text">
|
||||||
<div class="export-toggle-label">{{ t('export.skipHidden') }}</div>
|
<div class="export-toggle-label">{{ t('export.skipHidden') }}</div>
|
||||||
<div class="export-toggle-desc">{{ t('export.skipHiddenDesc') }}</div>
|
<div class="export-toggle-desc">{{ t('export.skipHiddenDesc') }}</div>
|
||||||
@@ -784,50 +783,6 @@ function onUpdate(v: boolean) {
|
|||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* iOS-style switch. The <input> is a transparent overlay that still
|
|
||||||
handles keyboard focus and click targeting; the visual is drawn by
|
|
||||||
the span + ::after pill. */
|
|
||||||
.switch {
|
|
||||||
position: relative;
|
|
||||||
width: 32px;
|
|
||||||
height: 18px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: var(--border);
|
|
||||||
border-radius: 999px;
|
|
||||||
transition: background 140ms var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch.on {
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch input {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
cursor: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
left: 2px;
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
||||||
transition: transform 140ms var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch.on::after {
|
|
||||||
transform: translateX(14px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.target-size {
|
.target-size {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import type { Action, AnimatableProps, CameraProps, Layer } from '@/types/scene'
|
|||||||
import RangeSlider from './RangeSlider.vue'
|
import RangeSlider from './RangeSlider.vue'
|
||||||
import ActionEditor from './ActionEditor.vue'
|
import ActionEditor from './ActionEditor.vue'
|
||||||
import FontPicker from './FontPicker.vue'
|
import FontPicker from './FontPicker.vue'
|
||||||
|
import { ToggleSwitch } from '@/components/ui'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
import { useTimeFormat } from '@/composables/useTimeFormat'
|
import { useTimeFormat } from '@/composables/useTimeFormat'
|
||||||
import { useEyeDropper } from '@/composables/useEyeDropper'
|
import { useEyeDropper } from '@/composables/useEyeDropper'
|
||||||
@@ -685,33 +686,24 @@ function deleteSelection() {
|
|||||||
<div v-if="sectionOpen.transform" class="group-body">
|
<div v-if="sectionOpen.transform" class="group-body">
|
||||||
<div class="toggle-row">
|
<div class="toggle-row">
|
||||||
<span class="toggle-label">{{ t('properties.camera.enabled') }}</span>
|
<span class="toggle-label">{{ t('properties.camera.enabled') }}</span>
|
||||||
<button
|
<ToggleSwitch
|
||||||
class="toggle-btn"
|
v-model="cameraEnabled"
|
||||||
:class="{ active: cameraEnabled }"
|
:aria-label="t('properties.camera.enabled')"
|
||||||
@click="cameraEnabled = !cameraEnabled"
|
/>
|
||||||
>
|
|
||||||
{{ cameraEnabled ? 'On' : 'Off' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="toggle-row">
|
<div class="toggle-row">
|
||||||
<span class="toggle-label">{{ t('properties.camera.overlay') }}</span>
|
<span class="toggle-label">{{ t('properties.camera.overlay') }}</span>
|
||||||
<button
|
<ToggleSwitch
|
||||||
class="toggle-btn"
|
v-model="cameraOverlayVisible"
|
||||||
:class="{ active: cameraOverlayVisible }"
|
:aria-label="t('properties.camera.overlay')"
|
||||||
@click="cameraOverlayVisible = !cameraOverlayVisible"
|
/>
|
||||||
>
|
|
||||||
{{ cameraOverlayVisible ? 'On' : 'Off' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="toggle-row">
|
<div class="toggle-row">
|
||||||
<span class="toggle-label">{{ t('properties.camera.locked') }}</span>
|
<span class="toggle-label">{{ t('properties.camera.locked') }}</span>
|
||||||
<button
|
<ToggleSwitch
|
||||||
class="toggle-btn"
|
v-model="cameraLocked"
|
||||||
:class="{ active: cameraLocked }"
|
:aria-label="t('properties.camera.locked')"
|
||||||
@click="cameraLocked = !cameraLocked"
|
/>
|
||||||
>
|
|
||||||
{{ cameraLocked ? 'On' : 'Off' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>X<input type="number" v-model.number="cameraX" :disabled="cameraLocked" /></label>
|
<label>X<input type="number" v-model.number="cameraX" :disabled="cameraLocked" /></label>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
pickBackupFile,
|
pickBackupFile,
|
||||||
readBackupFile,
|
readBackupFile,
|
||||||
} from '@/lib/backupFile'
|
} from '@/lib/backupFile'
|
||||||
import { confirmDialog, Modal } from '@/components/ui'
|
import { confirmDialog, Modal, ToggleSwitch } from '@/components/ui'
|
||||||
import { showWelcomeOnStart } from '@/composables/useShowWelcomeOnStart'
|
import { showWelcomeOnStart } from '@/composables/useShowWelcomeOnStart'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
|
||||||
@@ -366,9 +366,10 @@ watch(
|
|||||||
<div v-if="backupError" class="backup-error">{{ backupError }}</div>
|
<div v-if="backupError" class="backup-error">{{ backupError }}</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<label class="ftr-toggle" v-tooltip="t('welcome.showOnStartHelp')">
|
<label class="ftr-toggle" v-tooltip="t('welcome.showOnStartHelp')">
|
||||||
<span class="switch sm" :class="{ on: showWelcomeOnStart }">
|
<ToggleSwitch
|
||||||
<input type="checkbox" v-model="showWelcomeOnStart" />
|
v-model="showWelcomeOnStart"
|
||||||
</span>
|
:aria-label="t('welcome.showOnStart')"
|
||||||
|
/>
|
||||||
<span class="ftr-toggle-label">{{ t('welcome.showOnStart') }}</span>
|
<span class="ftr-toggle-label">{{ t('welcome.showOnStart') }}</span>
|
||||||
</label>
|
</label>
|
||||||
<span class="ftr-sep"></span>
|
<span class="ftr-sep"></span>
|
||||||
@@ -676,8 +677,6 @@ watch(
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compact footer toggle — same visual language as the other toggles
|
|
||||||
in the app but sized for the row. */
|
|
||||||
.ftr-toggle {
|
.ftr-toggle {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -691,44 +690,4 @@ watch(
|
|||||||
.ftr-toggle-label {
|
.ftr-toggle-label {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch.sm {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
width: 28px;
|
|
||||||
height: 16px;
|
|
||||||
background: var(--border);
|
|
||||||
border-radius: 999px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
transition: background 140ms var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch.sm input {
|
|
||||||
opacity: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch.sm::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
left: 2px;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
||||||
transition: transform 140ms var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch.sm.on {
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch.sm.on::after {
|
|
||||||
transform: translateX(12px);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
label?: string
|
||||||
|
ariaLabel?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||||
|
|
||||||
|
function onChange(event: Event) {
|
||||||
|
emit('update:modelValue', (event.target as HTMLInputElement).checked)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span class="ui-switch thumbs-switch" :class="{ disabled }">
|
||||||
|
<span v-if="$slots.default || label" class="ui-switch-label thumbs-switch-label">
|
||||||
|
<slot>{{ label }}</slot>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="ui-switch-input thumbs-switch-input"
|
||||||
|
:checked="modelValue"
|
||||||
|
:disabled="disabled"
|
||||||
|
:aria-label="ariaLabel || label"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
<span class="ui-switch-track">
|
||||||
|
<span class="ui-switch-knob"></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ui-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--text-dim);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-switch.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-switch-label {
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-switch-input {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-switch-track {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 26px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--border);
|
||||||
|
transition: background 140ms var(--ease);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-switch-knob {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
|
||||||
|
transition: left 140ms var(--ease), background 140ms var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-switch-input:checked + .ui-switch-track {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-switch-input:checked + .ui-switch-track .ui-switch-knob {
|
||||||
|
left: 14px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-switch-input:focus-visible + .ui-switch-track {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-switch:hover:not(.disabled) .ui-switch-track {
|
||||||
|
filter: brightness(1.08);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,5 +2,6 @@ export { default as DialogsHost } from './DialogsHost.vue'
|
|||||||
export { default as IconButton } from './IconButton.vue'
|
export { default as IconButton } from './IconButton.vue'
|
||||||
export { default as Modal } from './Modal.vue'
|
export { default as Modal } from './Modal.vue'
|
||||||
export { default as Toast } from './Toast.vue'
|
export { default as Toast } from './Toast.vue'
|
||||||
|
export { default as ToggleSwitch } from './ToggleSwitch.vue'
|
||||||
export { confirmDialog, dialogState, promptDialog } from './dialogs'
|
export { confirmDialog, dialogState, promptDialog } from './dialogs'
|
||||||
export type { ConfirmOptions, PromptOptions } from './dialogs'
|
export type { ConfirmOptions, PromptOptions } from './dialogs'
|
||||||
Reference in new issue
Block a user