mirror of
https://git.hmsn.ink/kospo/svcm/dmz.git
synced 2026-03-20 02:52:18 +09:00
first
This commit is contained in:
133
src/VueroApp.vue
Normal file
133
src/VueroApp.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
// This is the global app setup function
|
||||
const { locale } = useI18n()
|
||||
|
||||
const rtlCodes = [
|
||||
'ar',
|
||||
'arc',
|
||||
'dv',
|
||||
'fa',
|
||||
'ha',
|
||||
'he',
|
||||
'khw',
|
||||
'ks',
|
||||
'ku',
|
||||
'ps',
|
||||
'ur',
|
||||
'yi',
|
||||
]
|
||||
|
||||
useHead(() => ({
|
||||
title: '소액계약관리',
|
||||
link: [
|
||||
{
|
||||
rel: 'icon',
|
||||
href: '/favicon.svg',
|
||||
type: 'image/svg+xml',
|
||||
},
|
||||
{
|
||||
rel: 'alternate icon',
|
||||
href: '/favicon.ico',
|
||||
type: 'image/x-icon',
|
||||
sizes: '16x16',
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
href: '/apple-touch-icon.png',
|
||||
type: 'image/png',
|
||||
sizes: '180x180',
|
||||
},
|
||||
{
|
||||
rel: 'mask-icon',
|
||||
href: '/favicon.svg',
|
||||
type: 'image/svg+xml',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
],
|
||||
meta: [
|
||||
// Critical Tags
|
||||
{ charset: 'utf-8' },
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width, initial-scale=1.5, shrink-to-fit=no',
|
||||
},
|
||||
// PWA theme color
|
||||
{
|
||||
name: 'theme-color',
|
||||
content: '#ffffff',
|
||||
},
|
||||
{
|
||||
name: 'msapplication-TileColor',
|
||||
content: '#232326',
|
||||
},
|
||||
// SEO
|
||||
{
|
||||
name: 'robots',
|
||||
content: 'index,follow,max-image-preview:large',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
content:
|
||||
'Vuero is more than a dashboard template, it is design system built with Vue, Bulma and Typescript. It ships with more than 200+ demo pages and a components.',
|
||||
},
|
||||
// Open Graph
|
||||
{
|
||||
property: 'og:site_name',
|
||||
content: 'Vuero by Css Ninja',
|
||||
},
|
||||
{
|
||||
property: 'og:locale',
|
||||
content: 'en_US',
|
||||
},
|
||||
{
|
||||
property: 'og:type',
|
||||
content: 'article',
|
||||
},
|
||||
{
|
||||
property: 'og:url',
|
||||
content: 'https://vuero.cssninja.io/',
|
||||
},
|
||||
{
|
||||
property: 'og:image:type',
|
||||
content: 'image/png',
|
||||
},
|
||||
{
|
||||
property: 'og:image:width',
|
||||
content: '1200',
|
||||
},
|
||||
{
|
||||
property: 'og:image:height',
|
||||
content: '630',
|
||||
},
|
||||
{
|
||||
property: 'og:image',
|
||||
content:
|
||||
'https://media.cssninja.io/embed/marketplace/product/wide.png?headline=Vue%203%20Admin%20and%20Webapp%20UI%20Kit&url=https%3A%2F%2Fcdn.schema.io%2Fcssninja%2F62671c0f1bfb2b0019e066aa%2F5badfba048f6ec5250e5e1a4e35cc5d2&previewUrl=https%3A%2F%2Fcdn.schema.io%2Fcssninja%2F62509cb6c752bf01326ee966%2Fa0c4a94938866331845447c595aacef2',
|
||||
},
|
||||
// Twitter
|
||||
{
|
||||
name: 'twitter:site',
|
||||
content: '@cssninjaStudio',
|
||||
},
|
||||
],
|
||||
htmlAttrs: {
|
||||
lang: locale.value,
|
||||
dir: rtlCodes.includes(locale.value) ? 'rtl' : 'ltr',
|
||||
},
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Suspense>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition
|
||||
name="fade-slow"
|
||||
mode="out-in"
|
||||
>
|
||||
<component :is="Component" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</Suspense>
|
||||
</div>
|
||||
</template>
|
||||
65
src/app.ts
Normal file
65
src/app.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { createApp as createClientApp, createSSRApp } from 'vue'
|
||||
|
||||
import type { H3Event } from 'h3'
|
||||
import { createHead } from '@unhead/vue'
|
||||
import { InferSeoMetaPlugin } from '@unhead/addons'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import type { VueroPlugin, VueroAppContext } from '/@src/utils/plugins'
|
||||
import { createRouter } from '/@src/router'
|
||||
import VueroApp from '/@src/VueroApp.vue'
|
||||
import '/@src/styles'
|
||||
|
||||
const plugins = import.meta.glob<{ default?: VueroPlugin }>('./plugins/*.ts', {
|
||||
eager: true,
|
||||
})
|
||||
|
||||
export async function createApp(event?: H3Event) {
|
||||
const app = __VUERO_SSR_BUILD__
|
||||
? createSSRApp(VueroApp)
|
||||
: createClientApp(VueroApp)
|
||||
|
||||
const router = createRouter()
|
||||
|
||||
const head = createHead({
|
||||
plugins: [InferSeoMetaPlugin()],
|
||||
})
|
||||
app.use(head)
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
// restore pinia state from SSR if any, before loading plugins
|
||||
if (__VUERO_SSR_BUILD__ && !import.meta.env.SSR) {
|
||||
const initialState = window.__vuero__
|
||||
if (typeof initialState?.pinia === 'object') {
|
||||
pinia.state.value = { ...initialState.pinia }
|
||||
}
|
||||
}
|
||||
|
||||
app.use(pinia)
|
||||
|
||||
const vuero: VueroAppContext = {
|
||||
app,
|
||||
router,
|
||||
head,
|
||||
pinia,
|
||||
event,
|
||||
}
|
||||
|
||||
for (const path in plugins) {
|
||||
try {
|
||||
const plugin = plugins[path]?.default
|
||||
if (!plugin) throw new Error(`Plugin does not have a default export.`)
|
||||
await plugin(vuero)
|
||||
}
|
||||
catch (error) {
|
||||
console.log(`Error while loading plugin "${path}"`)
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// use router after plugin registration, so we can register navigation guards in plugins
|
||||
app.use(vuero.router)
|
||||
|
||||
return vuero
|
||||
}
|
||||
18
src/components/ClientOnly.vue
Normal file
18
src/components/ClientOnly.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { SlotsType } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ClientOnly',
|
||||
slots: Object as SlotsType<{
|
||||
default: void
|
||||
}>,
|
||||
setup(_, { slots }) {
|
||||
const show = ref(false)
|
||||
onMounted(() => {
|
||||
show.value = true
|
||||
})
|
||||
|
||||
return () => (show.value && slots.default ? slots.default() : null)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
167
src/components/base-addons/VBillboardJS.vue
Normal file
167
src/components/base-addons/VBillboardJS.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChartOptions, Chart } from 'billboard.js'
|
||||
|
||||
import 'billboard.js/dist/billboard.min.css'
|
||||
|
||||
export interface VBillboardJSEmits {
|
||||
(e: 'ready', billboard: Chart): void
|
||||
}
|
||||
export interface VBillboardJSProps {
|
||||
options: ChartOptions
|
||||
}
|
||||
|
||||
const emit = defineEmits<VBillboardJSEmits>()
|
||||
const props = defineProps<VBillboardJSProps>()
|
||||
|
||||
const element = ref<HTMLElement>()
|
||||
|
||||
onMounted(async () => {
|
||||
if (!element.value) return
|
||||
|
||||
try {
|
||||
const bb = await import('billboard.js').then(m => m.default || m)
|
||||
const billboard = bb.generate({
|
||||
...props.options,
|
||||
bindto: element.value,
|
||||
})
|
||||
emit('ready', billboard)
|
||||
|
||||
nextTick(() => {
|
||||
billboard.resize()
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="element" />
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.bb-title {
|
||||
font-family: var(--font-alt) !important;
|
||||
font-size: 1rem !important;
|
||||
font-weight: 600 !important;
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
.bb-legend-background,
|
||||
.bb-chart-arcs-background {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.bb-axis line,
|
||||
.bb-axis .domain {
|
||||
color: color-mix(in oklab, var(--fade-grey), black 4%);
|
||||
stroke: color-mix(in oklab, var(--fade-grey), black 4%);
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.tick {
|
||||
text tspan {
|
||||
fill: color-mix(in oklab, var(--light-text), black 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.bb-title {
|
||||
fill: var(--dark-dark-text) !important;
|
||||
}
|
||||
|
||||
.bb-axis line,
|
||||
.bb-axis .domain {
|
||||
color: color-mix(in oklab, var(--dark-sidebar), white 20%) !important;
|
||||
stroke: color-mix(in oklab, var(--dark-sidebar), white 20%) !important;
|
||||
}
|
||||
|
||||
.bb-legend {
|
||||
.bb-legend-background rect {
|
||||
fill: color-mix(in oklab, var(--dark-sidebar), black 2%) !important;
|
||||
color: color-mix(in oklab, var(--dark-sidebar), white 12%) !important;
|
||||
stroke: color-mix(in oklab, var(--dark-sidebar), white 12%) !important;
|
||||
}
|
||||
|
||||
.bb-legend-item {
|
||||
text {
|
||||
fill: var(--dark-dark-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bb-chart-arc path {
|
||||
color: color-mix(in oklab, var(--dark-sidebar), white 12%) !important;
|
||||
stroke: color-mix(in oklab, var(--dark-sidebar), white 12%) !important;
|
||||
}
|
||||
|
||||
.bb-chart-arc .bb-gauge-value {
|
||||
fill: var(--light-text) !important;
|
||||
}
|
||||
|
||||
.bb-chart-arcs .bb-chart-arcs-background {
|
||||
color: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
|
||||
fill: color-mix(in oklab, var(--dark-sidebar), black 2%) !important;
|
||||
stroke: color-mix(in oklab, var(--dark-sidebar), black 2%) !important;
|
||||
}
|
||||
|
||||
.bb-chart-arcs-title,
|
||||
.bb-gauge-value,
|
||||
.bb-axis text {
|
||||
fill: var(--dark-dark-text);
|
||||
}
|
||||
|
||||
.bb-tooltip {
|
||||
border: 1px solid color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
|
||||
|
||||
// background-color: var(--white);
|
||||
|
||||
th {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
|
||||
background-color: color-mix(in oklab, var(--dark-sidebar), black 2%) !important;
|
||||
color: #fffdfd !important;
|
||||
font-family: var(--font) !important;
|
||||
font-weight: 400 !important;
|
||||
|
||||
span {
|
||||
font-family: var(--font) !important;
|
||||
font-weight: 400 !important;
|
||||
color: #fffdfd !important;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
|
||||
}
|
||||
|
||||
td {
|
||||
background-color: color-mix(in oklab, var(--dark-sidebar), black 2%) !important;
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
|
||||
color: var(--light-text) !important;
|
||||
|
||||
> span,
|
||||
> .iconify {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
|
||||
fill: var(--white) !important;
|
||||
color: var(--white) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bb-tooltip-title {
|
||||
color: #fffdfd !important;
|
||||
}
|
||||
|
||||
.bb-tooltip-detail {
|
||||
.bb-tooltip-name,
|
||||
.bb-tooltip-value {
|
||||
color: #fffdfd !important;
|
||||
|
||||
span {
|
||||
color: #fffdfd !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
313
src/components/base-addons/VCreditCard.vue
Normal file
313
src/components/base-addons/VCreditCard.vue
Normal file
@@ -0,0 +1,313 @@
|
||||
<script setup lang="ts">
|
||||
import type { VCreditCardColor } from '/@src/composables/credit-card'
|
||||
|
||||
export interface VCreditCardEmits {
|
||||
(e: 'flip'): void
|
||||
}
|
||||
export interface VCreditCardProps {
|
||||
number?: string
|
||||
name?: string
|
||||
expiry?: string
|
||||
cvc?: string | number
|
||||
color?: VCreditCardColor
|
||||
flipped?: boolean
|
||||
}
|
||||
|
||||
const emit = defineEmits<VCreditCardEmits>()
|
||||
const props = withDefaults(defineProps<VCreditCardProps>(), {
|
||||
color: 'grey',
|
||||
name: 'John Doe',
|
||||
number: '1234 1234 1234 1234',
|
||||
cvc: '123',
|
||||
expiry: '01/30',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const nameUppercase = computed(() => props.name?.toUpperCase() ?? '')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card-container">
|
||||
<div
|
||||
:class="[props.flipped && 'flipped']"
|
||||
class="creditcard"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@keydown.enter.prevent="emit('flip')"
|
||||
@click="emit('flip')"
|
||||
>
|
||||
<div class="front">
|
||||
<slot />
|
||||
|
||||
<svg
|
||||
id="cardfront"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 750 471"
|
||||
style="enable-background: new 0 0 750 471"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<g id="Front2">
|
||||
<g id="CardBackground">
|
||||
<g id="Page-1_1_">
|
||||
<g id="amex_1_">
|
||||
<path
|
||||
id="Rectangle-1_1_"
|
||||
class="lightcolor"
|
||||
:class="props.color"
|
||||
d="M40,0h670c22.1,0,40,17.9,40,40v391c0,22.1-17.9,40-40,40H40c-22.1,0-40-17.9-40-40V40
|
||||
C0,17.9,17.9,0,40,0z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
class="darkcolor"
|
||||
:class="`${props.color}dark`"
|
||||
d="M750,431V193.2c-217.6-57.5-556.4-13.5-750,24.9V431c0,22.1,17.9,40,40,40h670C732.1,471,750,453.1,750,431z"
|
||||
/>
|
||||
</g>
|
||||
<text
|
||||
id="svgnumber"
|
||||
transform="matrix(1 0 0 1 60.106 295.0121)"
|
||||
class="st2 st3 st4"
|
||||
>
|
||||
{{ props.number }}
|
||||
</text>
|
||||
<text
|
||||
id="svgname"
|
||||
transform="matrix(1 0 0 1 54.1064 428.1723)"
|
||||
class="st2 st5 st6"
|
||||
>
|
||||
{{ nameUppercase }}
|
||||
</text>
|
||||
<text
|
||||
transform="matrix(1 0 0 1 54.1074 389.8793)"
|
||||
class="st7 st5 st8"
|
||||
>
|
||||
{{ t('components.v-credit-card.holder-label') }}
|
||||
</text>
|
||||
<text
|
||||
transform="matrix(1 0 0 1 479.7754 388.8793)"
|
||||
class="st7 st5 st8"
|
||||
>
|
||||
{{ t('components.v-credit-card.expiration-label') }}
|
||||
</text>
|
||||
<text
|
||||
transform="matrix(1 0 0 1 65.1054 241.5)"
|
||||
class="st7 st5 st8"
|
||||
>
|
||||
{{ t('components.v-credit-card.number-label') }}
|
||||
</text>
|
||||
<g>
|
||||
<text
|
||||
id="svgexpire"
|
||||
transform="matrix(1 0 0 1 574.4219 433.8095)"
|
||||
class="st2 st5 st9"
|
||||
>
|
||||
{{ props.expiry }}
|
||||
</text>
|
||||
<text
|
||||
transform="matrix(1 0 0 1 479.3848 417.0097)"
|
||||
class="st2 st10 st11"
|
||||
>
|
||||
{{ t('components.v-credit-card.valid-label') }}
|
||||
</text>
|
||||
<text
|
||||
transform="matrix(1 0 0 1 479.3848 435.6762)"
|
||||
class="st2 st10 st11"
|
||||
>
|
||||
{{ t('components.v-credit-card.valid-thru-label') }}
|
||||
</text>
|
||||
<polygon
|
||||
class="st2"
|
||||
points="554.5,421 540.4,414.2 540.4,427.9"
|
||||
/>
|
||||
</g>
|
||||
<g id="cchip">
|
||||
<g>
|
||||
<path
|
||||
class="st2"
|
||||
d="M168.1,143.6H82.9c-10.2,0-18.5-8.3-18.5-18.5V74.9c0-10.2,8.3-18.5,18.5-18.5h85.3
|
||||
c10.2,0,18.5,8.3,18.5,18.5v50.2C186.6,135.3,178.3,143.6,168.1,143.6z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<rect
|
||||
x="82"
|
||||
y="70"
|
||||
class="st12"
|
||||
width="1.5"
|
||||
height="60"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<rect
|
||||
x="167.4"
|
||||
y="70"
|
||||
class="st12"
|
||||
width="1.5"
|
||||
height="60"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
class="st12"
|
||||
d="M125.5,130.8c-10.2,0-18.5-8.3-18.5-18.5c0-4.6,1.7-8.9,4.7-12.3c-3-3.4-4.7-7.7-4.7-12.3
|
||||
c0-10.2,8.3-18.5,18.5-18.5s18.5,8.3,18.5,18.5c0,4.6-1.7,8.9-4.7,12.3c3,3.4,4.7,7.7,4.7,12.3
|
||||
C143.9,122.5,135.7,130.8,125.5,130.8z M125.5,70.8c-9.3,0-16.9,7.6-16.9,16.9c0,4.4,1.7,8.6,4.8,11.8l0.5,0.5l-0.5,0.5
|
||||
c-3.1,3.2-4.8,7.4-4.8,11.8c0,9.3,7.6,16.9,16.9,16.9s16.9-7.6,16.9-16.9c0-4.4-1.7-8.6-4.8-11.8l-0.5-0.5l0.5-0.5
|
||||
c3.1-3.2,4.8-7.4,4.8-11.8C142.4,78.4,134.8,70.8,125.5,70.8z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<rect
|
||||
x="82.8"
|
||||
y="82.1"
|
||||
class="st12"
|
||||
width="25.8"
|
||||
height="1.5"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<rect
|
||||
x="82.8"
|
||||
y="117.9"
|
||||
class="st12"
|
||||
width="26.1"
|
||||
height="1.5"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<rect
|
||||
x="142.4"
|
||||
y="82.1"
|
||||
class="st12"
|
||||
width="25.8"
|
||||
height="1.5"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<rect
|
||||
x="142"
|
||||
y="117.9"
|
||||
class="st12"
|
||||
width="26.2"
|
||||
height="1.5"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Back" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="back">
|
||||
<svg
|
||||
id="cardback"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 750 471"
|
||||
style="enable-background: new 0 0 750 471"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<g id="Front">
|
||||
<line
|
||||
class="st0"
|
||||
x1="35.3"
|
||||
y1="10.4"
|
||||
x2="36.7"
|
||||
y2="11"
|
||||
/>
|
||||
</g>
|
||||
<g id="Back2">
|
||||
<g id="Page-1_2_">
|
||||
<g id="amex_2_">
|
||||
<path
|
||||
id="Rectangle-1_2_"
|
||||
class="darkcolor"
|
||||
:class="`${props.color}dark`"
|
||||
d="M40,0h670c22.1,0,40,17.9,40,40v391c0,22.1-17.9,40-40,40H40c-22.1,0-40-17.9-40-40V40
|
||||
C0,17.9,17.9,0,40,0z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<rect
|
||||
y="61.6"
|
||||
class="st2"
|
||||
width="750"
|
||||
height="78"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
class="st3"
|
||||
d="M701.1,249.1H48.9c-3.3,0-6-2.7-6-6v-52.5c0-3.3,2.7-6,6-6h652.1c3.3,0,6,2.7,6,6v52.5
|
||||
C707.1,246.4,704.4,249.1,701.1,249.1z"
|
||||
/>
|
||||
<rect
|
||||
x="42.9"
|
||||
y="198.6"
|
||||
class="st4"
|
||||
width="664.1"
|
||||
height="10.5"
|
||||
/>
|
||||
<rect
|
||||
x="42.9"
|
||||
y="224.5"
|
||||
class="st4"
|
||||
width="664.1"
|
||||
height="10.5"
|
||||
/>
|
||||
<path
|
||||
class="st5"
|
||||
d="M701.1,184.6H618h-8h-10v64.5h10h8h83.1c3.3,0,6-2.7,6-6v-52.5C707.1,187.3,704.4,184.6,701.1,184.6z"
|
||||
/>
|
||||
</g>
|
||||
<text
|
||||
id="svgsecurity"
|
||||
transform="matrix(1 0 0 1 621.999 227.2734)"
|
||||
class="st6 st7"
|
||||
>
|
||||
{{ props.cvc }}
|
||||
</text>
|
||||
<g class="st8">
|
||||
<text
|
||||
transform="matrix(1 0 0 1 518.083 280.0879)"
|
||||
class="st9 st6 st10"
|
||||
>
|
||||
{{ t('components.v-credit-card.cvc-label') }}
|
||||
</text>
|
||||
</g>
|
||||
<rect
|
||||
x="58.1"
|
||||
y="378.6"
|
||||
class="st11"
|
||||
width="375.5"
|
||||
height="13.5"
|
||||
/>
|
||||
<rect
|
||||
x="58.1"
|
||||
y="405.6"
|
||||
class="st11"
|
||||
width="421.7"
|
||||
height="13.5"
|
||||
/>
|
||||
<text
|
||||
id="svgnameback"
|
||||
transform="matrix(1 0 0 1 59.5073 228.6099)"
|
||||
class="st12 st13"
|
||||
>
|
||||
{{ props.name }}
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
182
src/components/base-addons/VFilePond.vue
Normal file
182
src/components/base-addons/VFilePond.vue
Normal file
@@ -0,0 +1,182 @@
|
||||
<script lang="ts">
|
||||
import type { FilePondEvent, FilePondOptions } from 'filepond'
|
||||
|
||||
import * as FilePond from 'filepond'
|
||||
import FilePondPluginFileValidateSize from 'filepond-plugin-file-validate-size'
|
||||
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type'
|
||||
import FilePondPluginImageExitOrientation from 'filepond-plugin-image-exif-orientation'
|
||||
import FilePondPluginImageCrop from 'filepond-plugin-image-crop'
|
||||
import FilePondPluginImageEdit from 'filepond-plugin-image-edit'
|
||||
import FilePondPluginImagePreview from 'filepond-plugin-image-preview'
|
||||
import FilePondPluginImageResize from 'filepond-plugin-image-resize'
|
||||
import FilePondPluginImageTransform from 'filepond-plugin-image-transform'
|
||||
|
||||
import 'filepond/dist/filepond.min.css'
|
||||
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css'
|
||||
import 'filepond-plugin-image-edit/dist/filepond-plugin-image-edit.min.css'
|
||||
import { type PropType, type ComponentObjectPropsOptions, type EmitsOptions } from 'vue'
|
||||
|
||||
type FilePondSize = undefined | 'small' | 'tiny'
|
||||
|
||||
const plugins = [
|
||||
FilePondPluginFileValidateSize,
|
||||
FilePondPluginFileValidateType,
|
||||
FilePondPluginImageExitOrientation,
|
||||
FilePondPluginImageCrop,
|
||||
FilePondPluginImageEdit,
|
||||
FilePondPluginImagePreview,
|
||||
FilePondPluginImageResize,
|
||||
FilePondPluginImageTransform,
|
||||
]
|
||||
|
||||
FilePond.registerPlugin(...plugins)
|
||||
|
||||
const types = {
|
||||
boolean: Boolean,
|
||||
int: Number,
|
||||
number: Number,
|
||||
string: String,
|
||||
array: Array,
|
||||
object: Object,
|
||||
function: Function,
|
||||
// action: Function, not used
|
||||
serverapi: Object,
|
||||
// regex: String, not used
|
||||
}
|
||||
|
||||
// Setup initial prop types and update when plugins are added
|
||||
const getNativeConstructorFromType = (type: keyof typeof types) => {
|
||||
if (!type) {
|
||||
return String
|
||||
}
|
||||
|
||||
return types[type]
|
||||
}
|
||||
|
||||
const _OptionTypes = FilePond.OptionTypes as Record<string, keyof typeof types>
|
||||
|
||||
// Activated props
|
||||
const propsOptions: ComponentObjectPropsOptions = {}
|
||||
|
||||
// Events that need to be mapped to emitters
|
||||
const eventNames: EmitsOptions = []
|
||||
|
||||
const defaultOptions = FilePond.getOptions() as Record<string, any>
|
||||
|
||||
for (const prop in _OptionTypes) {
|
||||
// don't add events to the props array
|
||||
if (/^on/.test(prop)) {
|
||||
eventNames.push(prop.replace('on', ''))
|
||||
continue
|
||||
}
|
||||
|
||||
// get property type ( can be either a String or the type defined within FilePond )
|
||||
propsOptions[prop] = {
|
||||
type: getNativeConstructorFromType(_OptionTypes[prop]),
|
||||
default: () => defaultOptions[prop],
|
||||
}
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VFilePond',
|
||||
props: {
|
||||
...propsOptions,
|
||||
size: {
|
||||
type: String as PropType<FilePondSize>,
|
||||
default: undefined,
|
||||
validator: (value: FilePondSize) => {
|
||||
// The value must match one of these strings
|
||||
if ([undefined, 'small', 'tiny'].indexOf(value) === -1) {
|
||||
console.warn(
|
||||
`VFilePond: invalid "${value}" size. Should be small, tiny or undefined`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: ['input', ...eventNames],
|
||||
setup(props, { emit, expose }) {
|
||||
const pond = ref<FilePond.FilePond>()
|
||||
const inputElement = ref<HTMLInputElement>()
|
||||
const pondOptions = Object.assign({}, { ...props }) as FilePondOptions
|
||||
|
||||
expose({
|
||||
pond,
|
||||
inputElement,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (inputElement.value && FilePond.supported()) {
|
||||
pond.value = FilePond.create(inputElement.value, {
|
||||
...pondOptions,
|
||||
fileValidateTypeDetectType: (source, type) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (pondOptions.acceptedFileTypes) {
|
||||
const index = pondOptions.acceptedFileTypes.findIndex(
|
||||
allowedType => allowedType === type,
|
||||
)
|
||||
if (index > -1) {
|
||||
resolve(type)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
reject()
|
||||
}),
|
||||
})
|
||||
|
||||
for (const eventName of eventNames) {
|
||||
const event = eventName as FilePondEvent
|
||||
if (event) {
|
||||
pond.value.on(event, (...event) => {
|
||||
emit('input', pond.value ? pond.value.getFiles() : [])
|
||||
emit(eventName, ...event)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
if (pond.value) {
|
||||
for (const eventName of eventNames) {
|
||||
const event = eventName as FilePondEvent
|
||||
if (event) {
|
||||
pond.value.off(event, (event) => {
|
||||
emit(eventName, event)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pond.value.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
const input = h('input', {
|
||||
type: 'file',
|
||||
ref: inputElement,
|
||||
id: pondOptions.id,
|
||||
name: pondOptions.name,
|
||||
class: pondOptions.className,
|
||||
required: pondOptions.required,
|
||||
accept: pondOptions.acceptedFileTypes,
|
||||
multiple: pondOptions.allowMultiple,
|
||||
capture: pondOptions.captureMethod,
|
||||
})
|
||||
|
||||
const wrapper = h('div', { class: 'filepond--wrapper' }, [input])
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: ['filepond-profile-wrap', props.size && `is-${props.size}`],
|
||||
},
|
||||
[wrapper],
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
76
src/components/base-addons/VIMaskInput.vue
Normal file
76
src/components/base-addons/VIMaskInput.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts" setup generic="Opts extends FactoryArg">
|
||||
import type { InputMask, FactoryArg, UpdateOpts } from 'imask'
|
||||
|
||||
import IMask from 'imask'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
options: Opts
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
'accept': [value: InputMask<Opts>, event?: InputEvent]
|
||||
'complete': [value: InputMask<Opts>, event?: InputEvent]
|
||||
}>()
|
||||
|
||||
const inputElement = ref<HTMLElement>()
|
||||
const inputMask = shallowRef<InputMask<Opts>>()
|
||||
|
||||
watch([inputElement, () => props.options, () => props.modelValue], () => {
|
||||
if (inputElement.value && props.options) {
|
||||
try {
|
||||
if (inputMask.value) {
|
||||
inputMask.value.updateOptions(props.options as UpdateOpts<Opts>)
|
||||
inputMask.value.unmaskedValue = props.modelValue
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
inputMask.value = IMask(inputElement.value, props.options ?? {})
|
||||
|
||||
if (props.modelValue) {
|
||||
inputMask.value.unmaskedValue = props.modelValue
|
||||
inputMask.value.updateValue()
|
||||
emit('accept', inputMask.value, undefined)
|
||||
}
|
||||
|
||||
inputMask.value.on('accept', (inputEvent) => {
|
||||
if (!inputMask.value) return
|
||||
emit('update:modelValue', inputMask.value?.value || '')
|
||||
emit('accept', inputMask.value, inputEvent)
|
||||
})
|
||||
|
||||
inputMask.value.on('complete', (inputEvent) => {
|
||||
if (!inputMask.value) return
|
||||
emit('complete', inputMask.value, inputEvent)
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error(
|
||||
'VIMaskInput: bad imask options, see https://imask.js.org/ for available parameters',
|
||||
)
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (inputMask.value) {
|
||||
inputMask.value.destroy()
|
||||
inputMask.value = undefined
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
inputMask,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
ref="inputElement"
|
||||
type="text"
|
||||
:value="props.modelValue"
|
||||
>
|
||||
</template>
|
||||
497
src/components/base-addons/VMarkdownEditor.vue
Normal file
497
src/components/base-addons/VMarkdownEditor.vue
Normal file
@@ -0,0 +1,497 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
BUILT_IN_COMMANDS,
|
||||
CommandTrigger,
|
||||
TextareaMarkdownOptions,
|
||||
Command,
|
||||
} from 'textarea-markdown-editor/dist/esm/types'
|
||||
import type { Cursor } from 'textarea-markdown-editor/dist/esm/Cursor.new'
|
||||
import { bootstrapTextareaMarkdown } from 'textarea-markdown-editor/dist/esm/bootstrap'
|
||||
|
||||
type VMarkdownEditorAction = (typeof BUILT_IN_COMMANDS)[number]
|
||||
type VMarkdownEditorContext = {
|
||||
textarea: HTMLTextAreaElement
|
||||
cursor: Cursor
|
||||
trigger: CommandTrigger
|
||||
value: string
|
||||
}
|
||||
type VMarkdownEditorCommandAction = {
|
||||
icon: string
|
||||
tooltip?: string
|
||||
label?: string
|
||||
action: VMarkdownEditorAction | ((ctx: VMarkdownEditorContext) => void | Promise<void>)
|
||||
}
|
||||
type VMarkdownEditorCommandGroup = {
|
||||
icon: string
|
||||
tooltip?: string
|
||||
vertical?: boolean
|
||||
label?: string
|
||||
children: Record<string, VMarkdownEditorCommandAction>
|
||||
}
|
||||
|
||||
type VMarkdownEditorToolbar = Record<
|
||||
string,
|
||||
VMarkdownEditorCommandAction | VMarkdownEditorCommandGroup
|
||||
>
|
||||
|
||||
const modelValue = defineModel<string>({
|
||||
default: '',
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
autogrow?: boolean
|
||||
options?: Partial<TextareaMarkdownOptions>
|
||||
commands?: Command[]
|
||||
toolbar?: VMarkdownEditorToolbar
|
||||
}>(),
|
||||
{
|
||||
options: () => ({
|
||||
enableIndentExtension: true,
|
||||
enableLinkPasteExtension: true,
|
||||
enableOrderedListAutoCorrectExtension: true,
|
||||
enablePrefixWrappingExtension: true,
|
||||
enableProperLineRemoveBehaviorExtension: true,
|
||||
}),
|
||||
commands: () => [],
|
||||
toolbar: () => ({
|
||||
'bold': {
|
||||
icon: 'ci:bold',
|
||||
tooltip: 'Bold (Ctrl + B)',
|
||||
action: 'bold',
|
||||
},
|
||||
'italic': {
|
||||
icon: 'ci:italic',
|
||||
tooltip: 'Italic (Ctrl + I)',
|
||||
action: 'italic',
|
||||
},
|
||||
'strike-through': {
|
||||
icon: 'ci:strikethrough',
|
||||
tooltip: 'Strike Through (Ctrl + Shift + X)',
|
||||
action: 'strike-through',
|
||||
},
|
||||
'headings': {
|
||||
icon: 'ci:heading',
|
||||
tooltip: 'Headings',
|
||||
children: {
|
||||
h1: {
|
||||
icon: 'ci:heading-h1',
|
||||
tooltip: 'H1',
|
||||
action: 'h1',
|
||||
},
|
||||
h2: {
|
||||
icon: 'ci:heading-h2',
|
||||
tooltip: 'H2',
|
||||
action: 'h2',
|
||||
},
|
||||
h3: {
|
||||
icon: 'ci:heading-h3',
|
||||
tooltip: 'H3',
|
||||
action: 'h3',
|
||||
},
|
||||
h4: {
|
||||
icon: 'ci:heading-h4',
|
||||
tooltip: 'H4',
|
||||
action: 'h4',
|
||||
},
|
||||
h5: {
|
||||
icon: 'ci:heading-h5',
|
||||
tooltip: 'H5',
|
||||
action: 'h5',
|
||||
},
|
||||
h6: {
|
||||
icon: 'ci:heading-h6',
|
||||
tooltip: 'H6',
|
||||
action: 'h6',
|
||||
},
|
||||
},
|
||||
},
|
||||
'unordered-list': {
|
||||
icon: 'ci:list-ul',
|
||||
tooltip: 'Unordered List',
|
||||
action: 'unordered-list',
|
||||
},
|
||||
'ordered-list': {
|
||||
icon: 'ci:list-ol',
|
||||
tooltip: 'Ordered List',
|
||||
action: 'ordered-list',
|
||||
},
|
||||
'code-block': {
|
||||
icon: 'ci:terminal',
|
||||
tooltip: 'Code Block',
|
||||
action: 'code-block',
|
||||
},
|
||||
'code-inline': {
|
||||
icon: 'ci:code',
|
||||
tooltip: 'Code Inline',
|
||||
action: 'code-inline',
|
||||
},
|
||||
// code: {
|
||||
// icon: 'ci:code',
|
||||
// tooltip: 'Code',
|
||||
// },
|
||||
'link': {
|
||||
icon: 'ci:link',
|
||||
tooltip: 'Link',
|
||||
action: 'link',
|
||||
},
|
||||
'image': {
|
||||
icon: 'ci:image',
|
||||
tooltip: 'Image',
|
||||
action: 'image',
|
||||
},
|
||||
'block-quotes': {
|
||||
icon: 'ci:double-quotes-l',
|
||||
tooltip: 'Block Quotes',
|
||||
action: 'block-quotes',
|
||||
},
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
const { field, id } = useVFieldContext({
|
||||
help: 'VMarkdownEditor',
|
||||
})
|
||||
|
||||
const textareaRef = ref<HTMLTextAreaElement>()
|
||||
const mode = ref<'write' | 'preview'>('write')
|
||||
const trigger = shallowRef<CommandTrigger>()
|
||||
const cursor = shallowRef<Cursor>()
|
||||
|
||||
const internal = computed({
|
||||
get() {
|
||||
if (field?.value) {
|
||||
return String(field.value.value)
|
||||
}
|
||||
else {
|
||||
return modelValue.value
|
||||
}
|
||||
},
|
||||
set(value: string) {
|
||||
if (field?.value) {
|
||||
field.value.setValue(value)
|
||||
}
|
||||
modelValue.value = value
|
||||
},
|
||||
})
|
||||
|
||||
function fitSize() {
|
||||
if (!textareaRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (props.autogrow) {
|
||||
textareaRef.value.style.height = 'auto'
|
||||
textareaRef.value.style.height = textareaRef.value.scrollHeight + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
function triggerAction(
|
||||
action: VMarkdownEditorAction | ((ctx: VMarkdownEditorContext) => void | Promise<void>),
|
||||
) {
|
||||
if (typeof action === 'function') {
|
||||
action({
|
||||
textarea: textareaRef.value!,
|
||||
cursor: cursor.value!,
|
||||
trigger: trigger.value!,
|
||||
value: internal.value,
|
||||
})
|
||||
}
|
||||
else {
|
||||
trigger.value?.(action)
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect((cleanup) => {
|
||||
if (textareaRef.value) {
|
||||
const mde = bootstrapTextareaMarkdown(textareaRef.value, {
|
||||
options: props.options, // optional options config
|
||||
commands: [], // optional commands configs
|
||||
})
|
||||
|
||||
trigger.value = mde.trigger
|
||||
cursor.value = mde.cursor
|
||||
|
||||
fitSize()
|
||||
|
||||
cleanup(mde.dispose)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="markdown-editor">
|
||||
<VFlex
|
||||
justify-content="space-between"
|
||||
class="toolbar"
|
||||
>
|
||||
<VFlexItem class="toolbar-mode">
|
||||
<VButtons addons>
|
||||
<VAction
|
||||
dark="2"
|
||||
:active="mode === 'write'"
|
||||
@click="mode = 'write'"
|
||||
>
|
||||
<VIcon icon="lucide:edit-3" />
|
||||
<span>Write</span>
|
||||
</VAction>
|
||||
<VAction
|
||||
dark="2"
|
||||
:disabled="!internal"
|
||||
:active="mode === 'preview'"
|
||||
@click="mode = 'preview'"
|
||||
>
|
||||
<VIcon icon="lucide:eye" />
|
||||
<span>Preview</span>
|
||||
</VAction>
|
||||
</VButtons>
|
||||
</VFlexItem>
|
||||
<VFlexItem class="toolbar-actions">
|
||||
<!-- toolbar -->
|
||||
<VButtons
|
||||
v-if="mode === 'write'"
|
||||
addons
|
||||
>
|
||||
<div
|
||||
v-for="(command, key) in props.toolbar"
|
||||
:key="key"
|
||||
class="toolbar-item"
|
||||
>
|
||||
<VAction
|
||||
v-if="'action' in command"
|
||||
v-tooltip.rounded="command.tooltip"
|
||||
dark="2"
|
||||
class="toolbar-action"
|
||||
@click.prevent="() => triggerAction(command.action)"
|
||||
>
|
||||
<VIcon
|
||||
v-if="command.icon"
|
||||
:icon="command.icon"
|
||||
/>
|
||||
<span v-if="command.label">{{ command.label }}</span>
|
||||
</VAction>
|
||||
<VDropdown
|
||||
v-else
|
||||
class="toolbar-dropdown"
|
||||
:class="[command.vertical && 'is-vertical']"
|
||||
>
|
||||
<template #button="dropdown">
|
||||
<VAction
|
||||
v-tooltip.rounded="command.tooltip"
|
||||
dark="2"
|
||||
:active="dropdown.isOpen"
|
||||
class="toolbar-dropdown-trigger"
|
||||
@keydown.enter.prevent="dropdown.toggle"
|
||||
@click="dropdown.toggle"
|
||||
>
|
||||
<VIcon
|
||||
v-if="command.icon"
|
||||
:icon="command.icon"
|
||||
/>
|
||||
<span v-if="command.label">{{ command.label }}</span>
|
||||
</VAction>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<VButtons
|
||||
class="mt-1"
|
||||
addons
|
||||
>
|
||||
<VAction
|
||||
v-for="(sub, subkey) in command.children"
|
||||
:key="`action-${subkey}`"
|
||||
v-tooltip.rounded="sub.tooltip"
|
||||
class="toolbar-dropdown-action"
|
||||
dark="2"
|
||||
@click.prevent="() => triggerAction(sub.action)"
|
||||
>
|
||||
<VIcon
|
||||
v-if="sub.icon"
|
||||
:icon="sub.icon"
|
||||
/>
|
||||
<span v-if="sub.label">{{ sub.label }}</span>
|
||||
</VAction>
|
||||
</VButtons>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</div>
|
||||
</VButtons>
|
||||
</VFlexItem>
|
||||
</VFlex>
|
||||
|
||||
<!-- textarea input -->
|
||||
<slot
|
||||
v-if="mode === 'write'"
|
||||
name="before-textarea"
|
||||
/>
|
||||
<textarea
|
||||
v-show="mode === 'write'"
|
||||
:id="id"
|
||||
ref="textareaRef"
|
||||
v-model="internal"
|
||||
v-bind="$attrs"
|
||||
class="textarea mt-0"
|
||||
autocomplete="no"
|
||||
rows="10"
|
||||
@input="fitSize"
|
||||
/>
|
||||
<slot
|
||||
v-if="mode === 'write'"
|
||||
name="after-textarea"
|
||||
/>
|
||||
|
||||
<slot
|
||||
v-if="mode === 'preview'"
|
||||
name="preview"
|
||||
v-bind="{ value: internal }"
|
||||
>
|
||||
<VCard radius="smooth">
|
||||
<VMarkdownPreview :source="internal" />
|
||||
</VCard>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.markdown-editor {
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 0.5rem;
|
||||
--primary-box-shadow: none;
|
||||
|
||||
.buttons {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
.buttons {
|
||||
.button {
|
||||
padding: 0 8px;
|
||||
height: 31px;
|
||||
border-radius: 0;
|
||||
|
||||
:deep(.iconify) {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textarea {
|
||||
max-height: 500px;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
:deep(.dropdown-menu) {
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
|
||||
.dropdown-content {
|
||||
padding: 0;
|
||||
|
||||
.buttons {
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.button {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-dropdown-action.button {
|
||||
&:first-of-type {
|
||||
border-start-start-radius: 3px;
|
||||
border-start-end-radius: 0;
|
||||
border-end-start-radius: 3px;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-start-start-radius: 0;
|
||||
border-start-end-radius: 3px;
|
||||
border-end-start-radius: 0;
|
||||
border-end-end-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-item {
|
||||
&:first-of-type {
|
||||
.toolbar-action {
|
||||
border-start-start-radius: 3px;
|
||||
border-end-start-radius: 3px;
|
||||
}
|
||||
|
||||
.toolbar-dropdown-trigger {
|
||||
border-start-start-radius: 3px;
|
||||
border-end-start-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
.toolbar-action {
|
||||
border-start-end-radius: 3px;
|
||||
border-end-end-radius: 3px;
|
||||
}
|
||||
|
||||
.toolbar-dropdown-trigger {
|
||||
border-start-end-radius: 3px;
|
||||
border-end-end-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
// &:not(:last-of-type) {
|
||||
// margin-inline-start: -1px;
|
||||
// }
|
||||
|
||||
~ .toolbar-item {
|
||||
margin-inline-start: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-dropdown {
|
||||
&.is-vertical {
|
||||
.buttons {
|
||||
align-items: stretch;
|
||||
|
||||
.button {
|
||||
place-content: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-dropdown-action.button {
|
||||
&:first-of-type {
|
||||
border-start-start-radius: 3px;
|
||||
border-start-end-radius: 3px;
|
||||
border-end-start-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-start-start-radius: 0;
|
||||
border-start-end-radius: 0;
|
||||
border-end-start-radius: 3px;
|
||||
border-end-end-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
flex-direction: column;
|
||||
|
||||
.toolbar-dropdown-action {
|
||||
margin-bottom: -1px;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
255
src/components/base-addons/VMarkdownPreview.vue
Normal file
255
src/components/base-addons/VMarkdownPreview.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<script lang="ts">
|
||||
import type { BuiltinLanguage, BuiltinTheme } from 'shiki'
|
||||
import { h, type PropType } from 'vue'
|
||||
|
||||
async function loadModules() {
|
||||
const [
|
||||
rehypeShiki,
|
||||
rehypeExternalLinks,
|
||||
rehypeRaw,
|
||||
[rehypeSanitize, defaultSchema],
|
||||
rehypeStringify,
|
||||
rehypeSlug,
|
||||
rehypeAutolinkHeadings,
|
||||
remarkGfm,
|
||||
remarkParse,
|
||||
remarkRehype,
|
||||
unified,
|
||||
] = await Promise.all([
|
||||
import('@shikijs/rehype').then(m => m.default),
|
||||
import('rehype-external-links').then(m => m.default),
|
||||
import('rehype-raw').then(m => m.default),
|
||||
import('rehype-sanitize').then(m => [m.default, m.defaultSchema] as const),
|
||||
import('rehype-stringify').then(m => m.default),
|
||||
import('rehype-slug').then(m => m.default),
|
||||
import('rehype-autolink-headings').then(m => m.default),
|
||||
import('remark-gfm').then(m => m.default),
|
||||
import('remark-parse').then(m => m.default),
|
||||
import('remark-rehype').then(m => m.default),
|
||||
import('unified').then(m => m.unified),
|
||||
])
|
||||
|
||||
return {
|
||||
rehypeShiki,
|
||||
rehypeExternalLinks,
|
||||
rehypeRaw,
|
||||
rehypeSanitize,
|
||||
defaultSchema,
|
||||
rehypeStringify,
|
||||
rehypeSlug,
|
||||
rehypeAutolinkHeadings,
|
||||
remarkGfm,
|
||||
remarkParse,
|
||||
remarkRehype,
|
||||
unified,
|
||||
}
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VMarkdownPreview',
|
||||
props: {
|
||||
source: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<undefined | 'large' | 'medium' | 'small'>,
|
||||
default: undefined,
|
||||
},
|
||||
maxWidth: {
|
||||
type: String as PropType<undefined | 'fullwidth' | 'medium' | 'small'>,
|
||||
default: undefined,
|
||||
},
|
||||
shiki: {
|
||||
type: Object as PropType<{
|
||||
langs: BuiltinLanguage[]
|
||||
theme:
|
||||
| BuiltinTheme
|
||||
| {
|
||||
light: BuiltinTheme
|
||||
dark: BuiltinTheme
|
||||
}
|
||||
}>,
|
||||
default: () => ({
|
||||
theme: {
|
||||
light: 'min-light',
|
||||
dark: 'github-dark',
|
||||
},
|
||||
langs: ['vue', 'vue-html', 'typescript', 'bash', 'scss'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
async setup(props) {
|
||||
const processor = ref<any>()
|
||||
const html = ref('')
|
||||
|
||||
const loadProcessors = async () => {
|
||||
const langs = props.shiki.langs
|
||||
const themes = {
|
||||
light:
|
||||
typeof props.shiki.theme === 'string'
|
||||
? props.shiki.theme
|
||||
: props.shiki.theme.light,
|
||||
dark:
|
||||
typeof props.shiki.theme === 'string'
|
||||
? props.shiki.theme
|
||||
: props.shiki.theme.dark,
|
||||
}
|
||||
|
||||
const {
|
||||
rehypeShiki,
|
||||
rehypeExternalLinks,
|
||||
rehypeRaw,
|
||||
rehypeSanitize,
|
||||
defaultSchema,
|
||||
rehypeStringify,
|
||||
rehypeSlug,
|
||||
rehypeAutolinkHeadings,
|
||||
remarkGfm,
|
||||
remarkParse,
|
||||
remarkRehype,
|
||||
unified,
|
||||
} = await loadModules()
|
||||
|
||||
processor.value = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkRehype, { allowDangerousHtml: true })
|
||||
.use(rehypeRaw)
|
||||
.use(rehypeSanitize, {
|
||||
...defaultSchema,
|
||||
attributes: {
|
||||
...defaultSchema.attributes,
|
||||
pre: [...(defaultSchema.attributes?.pre || []), ['className'], ['style']],
|
||||
code: [...(defaultSchema.attributes?.code || []), ['className'], ['style']],
|
||||
i: [...(defaultSchema.attributes?.i || []), ['className']],
|
||||
span: [
|
||||
...(defaultSchema.attributes?.span || []),
|
||||
['className'],
|
||||
['style'],
|
||||
['dataHint'],
|
||||
],
|
||||
},
|
||||
})
|
||||
.use(rehypeShiki, {
|
||||
themes,
|
||||
langs,
|
||||
})
|
||||
.use(rehypeExternalLinks, { rel: ['nofollow'], target: '_blank' })
|
||||
.use(rehypeSlug)
|
||||
.use(rehypeAutolinkHeadings, {
|
||||
behavior: 'append',
|
||||
content: {
|
||||
type: 'element',
|
||||
tagName: 'iconify-icon',
|
||||
properties: {
|
||||
className: ['iconify toc-link-anchor'],
|
||||
icon: 'lucide:link',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
})
|
||||
.use(rehypeStringify)
|
||||
}
|
||||
|
||||
const processMd = async () => {
|
||||
const _source = unref(props.source)
|
||||
const _processor = unref(processor)
|
||||
if (!_processor) return
|
||||
if (!_source) return
|
||||
|
||||
const result = (await _processor.process(_source)).toString()
|
||||
|
||||
html.value = result
|
||||
}
|
||||
|
||||
if (import.meta.env.SSR) {
|
||||
await loadProcessors()
|
||||
await processMd()
|
||||
}
|
||||
else {
|
||||
watchEffect(loadProcessors)
|
||||
watchEffect(processMd)
|
||||
}
|
||||
|
||||
const classes = computed(() => {
|
||||
return {
|
||||
'markdown content': true,
|
||||
'is-max-width-fullwidth': props.maxWidth === 'fullwidth',
|
||||
'is-max-width-medium': props.maxWidth === 'medium',
|
||||
'is-max-width-small': props.maxWidth === 'small',
|
||||
'is-small': props.size === 'small',
|
||||
'is-medium': props.size === 'medium',
|
||||
'is-large': props.size === 'large',
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
return h('div', {
|
||||
class: classes.value,
|
||||
innerHTML: html.value,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.is-max-width-full {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.is-max-width-medium {
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.is-max-width-small {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
:deep(a) {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&.is-small {
|
||||
font-size: 0.875rem;
|
||||
|
||||
:deep(pre) {
|
||||
padding: 0.5rem 0.8rem 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.toc-link-anchor) {
|
||||
color: var(--light-text);
|
||||
margin-inline-start: 0.5rem;
|
||||
font-size: 1rem;
|
||||
transition: color 0.2s;
|
||||
outline: none;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.shiki) {
|
||||
border-radius: var(--radius-large);
|
||||
|
||||
// code {
|
||||
// counter-reset: step;
|
||||
// counter-increment: step 0;
|
||||
// }
|
||||
|
||||
// code .line::before {
|
||||
// content: counter(step);
|
||||
// counter-increment: step;
|
||||
// width: 1rem;
|
||||
// margin-inline-end: 1.5rem;
|
||||
// display: inline-block;
|
||||
// text-align: inset-inline-end;
|
||||
// color: #898d98;
|
||||
// }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
105
src/components/base-addons/VPeity.vue
Normal file
105
src/components/base-addons/VPeity.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import type { PeityOptions, PeityType } from '/@src/utils/peity/types'
|
||||
import { drawBar, drawLine, drawPie } from '/@src/utils/peity'
|
||||
|
||||
export interface VPeityProps {
|
||||
values: number[]
|
||||
type: PeityType
|
||||
min?: number
|
||||
max?: number
|
||||
radius?: number
|
||||
innerRadius?: number
|
||||
height?: number
|
||||
width?: number
|
||||
padding?: number
|
||||
stroke?: string
|
||||
strokeWidth?: number
|
||||
fill?: string[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VPeityProps>(), {
|
||||
type: 'line',
|
||||
radius: 8,
|
||||
padding: 0.1,
|
||||
innerRadius: 5,
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
height: 16,
|
||||
width: 16,
|
||||
stroke: undefined,
|
||||
strokeWidth: 1,
|
||||
fill: undefined,
|
||||
values: () => [],
|
||||
})
|
||||
|
||||
const svgElement = ref<HTMLElement>()
|
||||
const svgHeight = computed(() => {
|
||||
const height = props.height || 16
|
||||
|
||||
if (props.type === 'pie' || props.type === 'donut') {
|
||||
const diameter = props.radius * 2
|
||||
return height || diameter
|
||||
}
|
||||
|
||||
return height
|
||||
})
|
||||
const svgWidth = computed(() => {
|
||||
const width = props.width || 16
|
||||
|
||||
if (props.type === 'pie' || props.type === 'donut') {
|
||||
const diameter = props.radius * 2
|
||||
return width || diameter
|
||||
}
|
||||
|
||||
return width
|
||||
})
|
||||
|
||||
watchPostEffect(() => {
|
||||
if (!svgElement.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const element = svgElement.value
|
||||
element.innerHTML = ''
|
||||
|
||||
const opts: PeityOptions = {
|
||||
type: props.type,
|
||||
height: props.height,
|
||||
width: props.width,
|
||||
fill: (idx: number): string => {
|
||||
const f = props.fill ?? []
|
||||
return f[idx % f.length]
|
||||
},
|
||||
}
|
||||
|
||||
switch (props.type) {
|
||||
case 'bar':
|
||||
opts.min = props.min
|
||||
opts.padding = props.padding
|
||||
drawBar(element, props.values, opts)
|
||||
break
|
||||
case 'line':
|
||||
opts.min = props.min
|
||||
opts.stroke = props.stroke
|
||||
opts.strokeWidth = props.strokeWidth
|
||||
drawLine(element, props.values, opts)
|
||||
break
|
||||
case 'pie':
|
||||
case 'donut':
|
||||
opts.radius = props.radius
|
||||
opts.innerRadius = props.innerRadius
|
||||
drawPie(element, props.values, opts)
|
||||
break
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
ref="svgElement"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="peity"
|
||||
:height="svgHeight"
|
||||
:width="svgWidth"
|
||||
/>
|
||||
</template>
|
||||
96
src/components/base-addons/VPhotosSwipe.vue
Normal file
96
src/components/base-addons/VPhotosSwipe.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import PhotoSwipeLightbox, { type PhotoSwipeOptions } from 'photoswipe/lightbox'
|
||||
|
||||
import 'photoswipe/style.css'
|
||||
|
||||
export interface VPhotoSwipeItem {
|
||||
src: string
|
||||
msrc?: string
|
||||
thumbnail?: string
|
||||
alt?: string
|
||||
w?: number
|
||||
h?: number
|
||||
title?: string
|
||||
el?: HTMLElement
|
||||
}
|
||||
export interface VPhotoSwipeProps {
|
||||
items?: VPhotoSwipeItem[]
|
||||
options?: PhotoSwipeOptions
|
||||
singleThumbnail?: boolean
|
||||
thumbnailRadius?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VPhotoSwipeProps>(), {
|
||||
items: () => [],
|
||||
options: () => ({}),
|
||||
thumbnailRadius: undefined,
|
||||
})
|
||||
|
||||
const { onceError } = useImageError()
|
||||
|
||||
let lightbox: PhotoSwipeLightbox | null = null
|
||||
const galleryElement = ref<HTMLElement>()
|
||||
|
||||
onMounted(() => {
|
||||
lightbox = new PhotoSwipeLightbox({
|
||||
gallery: galleryElement.value,
|
||||
pswpModule: () => import('photoswipe'),
|
||||
...props.options,
|
||||
children: 'a',
|
||||
})
|
||||
lightbox.init()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (lightbox) {
|
||||
lightbox.destroy()
|
||||
lightbox = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="galleryElement"
|
||||
class="my-gallery"
|
||||
itemscope
|
||||
itemtype="http://schema.org/ImageGallery"
|
||||
>
|
||||
<figure
|
||||
v-for="(item, index) in items"
|
||||
v-show="index === 0 || !singleThumbnail"
|
||||
:key="index"
|
||||
class="gallery-thumbnail"
|
||||
itemprop="associatedMedia"
|
||||
itemscope
|
||||
itemtype="http://schema.org/ImageObject"
|
||||
:src="item.src"
|
||||
>
|
||||
<a
|
||||
:href="item.src"
|
||||
:title="item.title"
|
||||
itemprop="contentUrl"
|
||||
:data-pswp-width="item.w"
|
||||
:data-pswp-height="item.h"
|
||||
data-cropped="true"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img
|
||||
:class="[thumbnailRadius && `radius-${thumbnailRadius}`]"
|
||||
:src="item.thumbnail"
|
||||
:alt="item.alt"
|
||||
itemprop="thumbnail"
|
||||
@error.once="onceError($event, item.w, item.h)"
|
||||
>
|
||||
</a>
|
||||
</figure>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.gallery-thumbnail {
|
||||
display: inline;
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
221
src/components/base-addons/VPlyr.vue
Normal file
221
src/components/base-addons/VPlyr.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<script setup lang="ts">
|
||||
import type { Options } from 'plyr'
|
||||
import 'plyr/dist/plyr.css'
|
||||
|
||||
export type VPlyrCaptions = {
|
||||
src: string
|
||||
srclang: string
|
||||
default?: boolean
|
||||
}
|
||||
export type VPlyrFormat = '4by3' | '16by9' | 'square'
|
||||
export interface VPlyrProps {
|
||||
source: string
|
||||
title?: string
|
||||
poster?: string
|
||||
captions?: VPlyrCaptions[]
|
||||
reversed?: boolean
|
||||
embed?: boolean
|
||||
ratio?: VPlyrFormat
|
||||
options?: Options
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VPlyrProps>(), {
|
||||
ratio: '16by9',
|
||||
title: '',
|
||||
poster: '',
|
||||
options: () => ({}),
|
||||
captions: () => [],
|
||||
})
|
||||
|
||||
const player = ref()
|
||||
const videoElement = ref<HTMLElement>()
|
||||
|
||||
onMounted(async () => {
|
||||
if (videoElement.value) {
|
||||
const Plyr = await import('plyr').then(mod => mod.default || mod)
|
||||
player.value = new Plyr(videoElement.value, props.options)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (player.value) {
|
||||
player.value.destroy()
|
||||
player.value = undefined
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="video-player-container"
|
||||
:class="[ratio && 'is-' + ratio, reversed && 'reversed-play']"
|
||||
>
|
||||
<!-- video element -->
|
||||
<iframe
|
||||
v-if="embed"
|
||||
:src="`${source}`"
|
||||
:title="props.title"
|
||||
allowfullscreen
|
||||
allowtransparency
|
||||
allow="autoplay"
|
||||
/>
|
||||
|
||||
<video
|
||||
v-else
|
||||
ref="videoElement"
|
||||
controls
|
||||
crossorigin="anonymous"
|
||||
playsinline
|
||||
:data-poster="poster"
|
||||
>
|
||||
<source
|
||||
:src="source"
|
||||
type="video/mp4"
|
||||
>
|
||||
<track
|
||||
v-for="(caption, key) in props.captions"
|
||||
:key="key"
|
||||
:default="caption.default"
|
||||
kind="captions"
|
||||
:srclang="caption.srclang"
|
||||
:src="caption.src"
|
||||
>
|
||||
</video>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.video-player-container-wrapper {
|
||||
max-width: 840px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.video-player-container {
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.is-square {
|
||||
position: relative;
|
||||
height: 440px;
|
||||
width: 480px;
|
||||
|
||||
.plyr {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-4by3 {
|
||||
position: relative;
|
||||
padding-top: 75%;
|
||||
width: 100%;
|
||||
|
||||
.plyr {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-16by9 {
|
||||
position: relative;
|
||||
padding-top: 56.25%;
|
||||
width: 100%;
|
||||
|
||||
.plyr {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.reversed-play {
|
||||
.plyr--full-ui.plyr--video .plyr__control--overlaid {
|
||||
background: var(--white) !important;
|
||||
border: 1px solid var(--primary);
|
||||
color: var(--primary) !important;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: var(--primary) !important;
|
||||
border-color: var(--primary) !important;
|
||||
color: var(--white) !important;
|
||||
|
||||
.iconify {
|
||||
fill: var(--white) !important;
|
||||
stroke: var(--white) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.iconify {
|
||||
fill: none;
|
||||
stroke: var(--primary);
|
||||
stroke-width: 1.6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
video {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.plyr__video-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.plyr--full-ui.plyr--video .plyr__control--overlaid {
|
||||
background: var(--primary) !important;
|
||||
box-shadow: var(--primary-box-shadow);
|
||||
}
|
||||
|
||||
.plyr--video .plyr__control.plyr__tab-focus,
|
||||
.plyr--video .plyr__control:hover,
|
||||
.plyr--video .plyr__control[aria-expanded='true'],
|
||||
.plyr__menu__container .plyr__control[role='menuitemradio'][aria-checked='true']::before {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.plyr--full-ui input[type='range'] {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.plyr__controls {
|
||||
transition: all 0.3s; // transition-all test
|
||||
}
|
||||
|
||||
.plyr--paused,
|
||||
.plyr--stopped {
|
||||
.plyr__controls {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 767px) {
|
||||
.video-player-container {
|
||||
&.is-square {
|
||||
height: 303px;
|
||||
width: 330px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
83
src/components/base-addons/VReloadPrompt.vue
Normal file
83
src/components/base-addons/VReloadPrompt.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { useRegisterSW } from 'virtual:pwa-register/vue'
|
||||
|
||||
export interface VReloadPromptProps {
|
||||
appName: string
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const props = defineProps<VReloadPromptProps>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { offlineReady, needRefresh, updateServiceWorker } = useRegisterSW()
|
||||
|
||||
const close = async () => {
|
||||
loading.value = false
|
||||
offlineReady.value = false
|
||||
needRefresh.value = false
|
||||
}
|
||||
const update = async () => {
|
||||
loading.value = true
|
||||
await updateServiceWorker()
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="from-bottom">
|
||||
<VCard
|
||||
v-if="offlineReady || needRefresh"
|
||||
class="pwa-toast"
|
||||
role="alert"
|
||||
radius="smooth"
|
||||
>
|
||||
<div class="pwa-message">
|
||||
<span v-if="offlineReady">
|
||||
{{ t('components.v-reload-prompt.offline-ready', { appName: props.appName }) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('components.v-reload-prompt.need-refresh', { appName: props.appName }) }}
|
||||
</span>
|
||||
</div>
|
||||
<VButtons align="right">
|
||||
<VButton
|
||||
v-if="needRefresh"
|
||||
color="primary"
|
||||
icon="ion:reload-outline"
|
||||
:loading="loading"
|
||||
@click="() => update()"
|
||||
>
|
||||
{{ t('components.v-reload-prompt.reload-button') }}
|
||||
</VButton>
|
||||
<VButton
|
||||
icon="lucide:x"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('components.v-reload-prompt.close-button') }}
|
||||
</VButton>
|
||||
</VButtons>
|
||||
</VCard>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.pwa-toast {
|
||||
position: fixed;
|
||||
inset-inline-end: 0;
|
||||
bottom: 0;
|
||||
max-width: 350px;
|
||||
margin: 16px;
|
||||
padding: 12px;
|
||||
border: 1px solid #8885;
|
||||
border-radius: 4px;
|
||||
z-index: 10;
|
||||
text-align: inset-inline-start;
|
||||
box-shadow: 3px 4px 5px 0 #8885;
|
||||
}
|
||||
|
||||
.pwa-message {
|
||||
padding: 0.5rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
</style>
|
||||
26
src/components/base-addons/VVivus.vue
Normal file
26
src/components/base-addons/VVivus.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import Vivus from 'vivus'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ['ready'],
|
||||
setup(props, { emit }) {
|
||||
const element = ref<HTMLElement>()
|
||||
|
||||
watchEffect(() => {
|
||||
if (element.value) {
|
||||
new Vivus(element.value, props.options, (vivus: any) => {
|
||||
emit('ready', vivus)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return () => h('div', { ref: element, class: 'vivus-svg' })
|
||||
},
|
||||
})
|
||||
</script>
|
||||
178
src/components/base/VAccordion.vue
Normal file
178
src/components/base/VAccordion.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<script setup lang="ts">
|
||||
interface VAccordionProps {
|
||||
items: {
|
||||
title: string
|
||||
content: string
|
||||
}[]
|
||||
openItems?: number[]
|
||||
exclusive?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VAccordionProps>(), {
|
||||
items: () => [],
|
||||
openItems: () => [],
|
||||
})
|
||||
|
||||
const internalOpenItems = ref(props.openItems)
|
||||
const toggle = (key: number) => {
|
||||
const wasOpen = internalOpenItems.value.includes(key)
|
||||
|
||||
if (props.exclusive) {
|
||||
internalOpenItems.value.splice(0, internalOpenItems.value.length)
|
||||
|
||||
if (!wasOpen) {
|
||||
internalOpenItems.value.push(key)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (wasOpen) {
|
||||
internalOpenItems.value.splice(internalOpenItems.value.indexOf(key), 1)
|
||||
}
|
||||
else {
|
||||
internalOpenItems.value.push(key)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="single-accordion"
|
||||
:class="[exclusive && 'is-exclusive']"
|
||||
>
|
||||
<details
|
||||
v-for="(item, key) in items"
|
||||
:key="key"
|
||||
class="accordion-item"
|
||||
:open="internalOpenItems?.includes(key) ?? undefined"
|
||||
:class="[internalOpenItems?.includes(key) && 'is-active']"
|
||||
>
|
||||
<slot
|
||||
name="accordion-item"
|
||||
:item="item"
|
||||
:index="key"
|
||||
:toggle="toggle"
|
||||
>
|
||||
<summary
|
||||
class="accordion-header"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="() => toggle(key)"
|
||||
@click.prevent="() => toggle(key)"
|
||||
>
|
||||
<slot
|
||||
name="accordion-item-summary"
|
||||
:item="item"
|
||||
:index="key"
|
||||
:toggle="toggle"
|
||||
>
|
||||
{{ item.title }}
|
||||
</slot>
|
||||
</summary>
|
||||
<div class="accordion-content">
|
||||
<slot
|
||||
name="accordion-item-content"
|
||||
:item="item"
|
||||
:index="key"
|
||||
:toggle="toggle"
|
||||
>
|
||||
{{ item.content }}
|
||||
</slot>
|
||||
</div>
|
||||
</slot>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.single-accordion {
|
||||
background: var(--white);
|
||||
margin: 0 auto;
|
||||
box-shadow: var(--light-box-shadow);
|
||||
border-radius: var(--radius-large);
|
||||
overflow: hidden;
|
||||
|
||||
.accordion-item {
|
||||
&.is-active {
|
||||
.accordion-header {
|
||||
&::before {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
border-bottom: 1px solid #dde0e7;
|
||||
color: var(--dark-text);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
font-family: var(--font-alt);
|
||||
padding: 1.5rem;
|
||||
display: block;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: #f6f7f9;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
border-radius: var(--radius-rounded);
|
||||
background-color: #b1b5be;
|
||||
margin-inline-end: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
display: none;
|
||||
border-bottom: 1px solid #dde0e7;
|
||||
background: #f6f7f9;
|
||||
padding: 1.5rem;
|
||||
color: var(--light-text);
|
||||
font-family: var(--font);
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.single-accordion {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 4%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
|
||||
.accordion-header {
|
||||
color: var(--dark-dark-text);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 6%);
|
||||
}
|
||||
|
||||
&::before {
|
||||
background: var(--dark-sidebar);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
&::before {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
background: var(--dark-sidebar);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 8%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
213
src/components/base/VAccordionImage.vue
Normal file
213
src/components/base/VAccordionImage.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<script setup lang="ts">
|
||||
export interface VAccordionImageItem {
|
||||
title: string
|
||||
content: string
|
||||
image: string
|
||||
}
|
||||
export interface VAccordionImageEmits {
|
||||
(e: 'select', key: string | number): void
|
||||
}
|
||||
export interface VAccordionImageProps {
|
||||
items: VAccordionImageItem[]
|
||||
}
|
||||
|
||||
const emit = defineEmits<VAccordionImageEmits>()
|
||||
const props = withDefaults(defineProps<VAccordionImageProps>(), {
|
||||
items: () => [],
|
||||
})
|
||||
|
||||
const toggle = (key: number) => {
|
||||
emit('select', key)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="image-accordion">
|
||||
<ul>
|
||||
<li
|
||||
v-for="(item, key) in props.items"
|
||||
:key="key"
|
||||
class="has-background-image"
|
||||
tabindex="0"
|
||||
:style="{ backgroundImage: `url(${item.image})` }"
|
||||
>
|
||||
<slot
|
||||
name="accordion-item"
|
||||
:item="item"
|
||||
:index="key"
|
||||
:toggle="toggle"
|
||||
>
|
||||
<div>
|
||||
<a
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="toggle(key)"
|
||||
@click="toggle(key)"
|
||||
>
|
||||
<h2>
|
||||
<slot
|
||||
name="accordion-item-summary"
|
||||
:item="item"
|
||||
:index="key"
|
||||
:toggle="toggle"
|
||||
>
|
||||
{{ item.title }}
|
||||
</slot>
|
||||
</h2>
|
||||
<p>
|
||||
<slot
|
||||
name="accordion-item-content"
|
||||
:item="item"
|
||||
:index="key"
|
||||
:toggle="toggle"
|
||||
>
|
||||
{{ item.content }}
|
||||
</slot>
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</slot>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
$a-height: 250px;
|
||||
$text-offset: $a-height - 90;
|
||||
|
||||
.image-accordion {
|
||||
width: 100%;
|
||||
height: $a-height;
|
||||
overflow: hidden;
|
||||
margin: 50px auto;
|
||||
|
||||
ul {
|
||||
width: 100%;
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
display: table-cell;
|
||||
vertical-align: bottom;
|
||||
position: relative;
|
||||
width: 16.666%;
|
||||
height: $a-height;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
transition: all 500ms ease;
|
||||
|
||||
div {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
height: $a-height;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
vertical-align: bottom;
|
||||
padding: 15px 20px;
|
||||
box-sizing: border-box;
|
||||
color: var(--white);
|
||||
text-decoration: none;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
transition: all 200ms ease;
|
||||
|
||||
* {
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
transform: translateX(calc(var(--transform-direction) * -20px));
|
||||
transition: all 400ms ease;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: var(--font-alt);
|
||||
font-weight: 300;
|
||||
text-overflow: clip;
|
||||
font-size: 1.4rem;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0;
|
||||
top: $text-offset;
|
||||
}
|
||||
|
||||
p {
|
||||
top: $text-offset;
|
||||
font-size: 13.5px;
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover li,
|
||||
&:focus-within li {
|
||||
width: 8%;
|
||||
}
|
||||
|
||||
&:hover li:hover,
|
||||
li:focus,
|
||||
&:focus-within li:focus {
|
||||
width: 60%;
|
||||
|
||||
a {
|
||||
background: rgb(0 0 0 / 40%);
|
||||
|
||||
* {
|
||||
opacity: 1;
|
||||
transform: translateX(calc(var(--transform-direction) * 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover li {
|
||||
width: 8% !important;
|
||||
|
||||
a * {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover li:hover {
|
||||
width: 60% !important;
|
||||
|
||||
a {
|
||||
background: rgb(0 0 0 / 40%);
|
||||
|
||||
* {
|
||||
opacity: 1 !important;
|
||||
transform: translateX(calc(var(--transform-direction) * 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (width <= 600px) {
|
||||
.image-accordion {
|
||||
height: auto;
|
||||
|
||||
ul,
|
||||
ul:hover {
|
||||
li,
|
||||
li:hover {
|
||||
position: relative;
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
width: 100% !important;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
144
src/components/base/VAction.vue
Normal file
144
src/components/base/VAction.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import { type RouteLocationAsString } from 'unplugin-vue-router'
|
||||
|
||||
export type VActionDark = '1' | '2' | '3' | '4' | '5' | '6'
|
||||
export interface VActionProps {
|
||||
to?: RouteLocationAsString
|
||||
dark?: VActionDark
|
||||
active?: boolean
|
||||
rounded?: boolean
|
||||
grey?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VActionProps>(), {
|
||||
to: undefined,
|
||||
dark: undefined,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterLink
|
||||
v-if="props.to"
|
||||
:to="props.to"
|
||||
class="button v-action"
|
||||
:class="[
|
||||
props.active && 'is-active',
|
||||
props.rounded && 'is-rounded',
|
||||
props.dark && `is-dark-bg-${props.dark}`,
|
||||
props.grey && 'is-grey',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
</RouterLink>
|
||||
<button
|
||||
v-else
|
||||
class="button v-action"
|
||||
:class="[
|
||||
props.active && 'is-active',
|
||||
props.rounded && 'is-rounded',
|
||||
props.dark && `is-dark-bg-${props.dark}`,
|
||||
props.grey && 'is-grey',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.button {
|
||||
font-family: var(--font);
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&.v-action {
|
||||
padding: 8px 16px;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
line-height: 0;
|
||||
border-radius: 3px;
|
||||
background: var(--white);
|
||||
color: var(--dark-text);
|
||||
border: 1px solid var(--placeholder);
|
||||
transition: border-color 0.3s; // transition-all test
|
||||
cursor: pointer;
|
||||
box-shadow: none !important;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&.is-rounded {
|
||||
border-radius: 500px;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline-offset: var(--accessibility-focus-outline-offset);
|
||||
outline-width: var(--accessibility-focus-outline-width);
|
||||
outline-style: var(--accessibility-focus-outline-style);
|
||||
outline-color: var(--accessibility-focus-outline-color);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: var(--primary-box-shadow);
|
||||
}
|
||||
|
||||
&:not(.is-active) {
|
||||
&:focus {
|
||||
color: var(--dark-text) !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline-color: var(--primary);
|
||||
}
|
||||
|
||||
&.is-grey {
|
||||
background: color-mix(in oklab, var(--fade-grey), white 2%);
|
||||
border-color: color-mix(in oklab, var(--fade-grey), white 2%);
|
||||
color: var(--muted-grey);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: var(--smoke-white);
|
||||
box-shadow: var(--primary-box-shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.button {
|
||||
&.v-action {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
color: var(--dark-dark-text);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: var(--smoke-white);
|
||||
text-shadow: 0 0 1px rgb(0 0 0 / 70%);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: var(--smoke-white) !important;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: var(--primary) !important;
|
||||
border-color: var(--primary) !important;
|
||||
box-shadow: var(--primary-box-shadow) !important;
|
||||
color: color-mix(in oklab, var(--dark-sidebar), black 2%) !important;
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
363
src/components/base/VAnimatedCheckbox.vue
Normal file
363
src/components/base/VAnimatedCheckbox.vue
Normal file
@@ -0,0 +1,363 @@
|
||||
<script setup lang="ts" generic="T extends unknown">
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
export type VAnimatedCheckboxColor =
|
||||
| 'primary'
|
||||
| 'info'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'purple'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const modelValue = defineModel<T[]>({
|
||||
default: () => [],
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: T
|
||||
color?: VAnimatedCheckboxColor
|
||||
}>(),
|
||||
{
|
||||
color: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
const animatedCheckboxId = ref<string | undefined>()
|
||||
const element = ref<HTMLElement>()
|
||||
const innerElement = ref<HTMLElement>()
|
||||
const checked = computed(() =>
|
||||
Boolean(modelValue.value.find(item => toRaw(item) === toRaw(props.value))),
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
animatedCheckboxId.value = `v-animated-checkbox-${uuid4()}`
|
||||
})
|
||||
|
||||
const updateCheckbox = () => {
|
||||
if (element.value && innerElement.value) {
|
||||
if (checked.value) {
|
||||
element.value.classList.add('is-checked')
|
||||
innerElement.value.classList.add('is-opaque')
|
||||
setTimeout(() => {
|
||||
element.value?.classList.remove('is-unchecked')
|
||||
}, 150)
|
||||
}
|
||||
else {
|
||||
element.value.classList.add('is-unchecked')
|
||||
element.value.classList.remove('is-checked')
|
||||
setTimeout(() => {
|
||||
innerElement.value?.classList.remove('is-opaque')
|
||||
}, 150)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function change() {
|
||||
const values = [...modelValue.value]
|
||||
const index = values.findIndex(item => toRaw(item) === toRaw(props.value))
|
||||
|
||||
if (index > -1) {
|
||||
values.splice(index, 1)
|
||||
}
|
||||
else {
|
||||
values.push(toRaw(props.value))
|
||||
}
|
||||
|
||||
modelValue.value = values
|
||||
}
|
||||
|
||||
watchEffect(updateCheckbox)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="element"
|
||||
class="animated-checkbox"
|
||||
:class="[color && 'is-' + color]"
|
||||
>
|
||||
<input
|
||||
:id="animatedCheckboxId"
|
||||
type="checkbox"
|
||||
:value="value"
|
||||
v-bind="$attrs"
|
||||
@change="change"
|
||||
>
|
||||
<label
|
||||
:for="animatedCheckboxId"
|
||||
class="checkmark-wrap"
|
||||
>
|
||||
<div
|
||||
ref="innerElement"
|
||||
class="shadow-circle"
|
||||
/>
|
||||
<svg
|
||||
class="checkmark"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 52 52"
|
||||
>
|
||||
<circle
|
||||
class="checkmark-circle"
|
||||
cx="26"
|
||||
cy="26"
|
||||
r="25"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
class="checkmark-check"
|
||||
fill="none"
|
||||
d="M14.1 27.2l7.1 7.2 16.7-16.8"
|
||||
/>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
$curve: cubic-bezier(0.65, 0, 0.45, 1);
|
||||
|
||||
.animated-checkbox {
|
||||
position: relative;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
|
||||
&:focus-within {
|
||||
border-radius: 50%;
|
||||
outline-offset: var(--accessibility-focus-outline-offset);
|
||||
outline-width: var(--accessibility-focus-outline-width);
|
||||
outline-style: var(--accessibility-focus-outline-style);
|
||||
outline-color: var(--accessibility-focus-outline-color);
|
||||
}
|
||||
|
||||
&.is-purple {
|
||||
.checkmark-circle {
|
||||
color: var(--purple) !important;
|
||||
stroke: var(--purple) !important;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
box-shadow: inset 0 0 0 var(--purple) !important;
|
||||
}
|
||||
|
||||
.checkmark-check {
|
||||
color: var(--purple) !important;
|
||||
stroke: var(--purple) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
.checkmark-circle {
|
||||
color: var(--primary) !important;
|
||||
stroke: var(--primary) !important;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
box-shadow: inset 0 0 0 var(--primary) !important;
|
||||
}
|
||||
|
||||
.checkmark-check {
|
||||
color: var(--primary) !important;
|
||||
stroke: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
.checkmark-circle {
|
||||
color: var(--info) !important;
|
||||
stroke: var(--info) !important;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
box-shadow: inset 0 0 0 var(--info) !important;
|
||||
}
|
||||
|
||||
.checkmark-check {
|
||||
color: var(--info) !important;
|
||||
stroke: var(--info) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
.checkmark-circle {
|
||||
color: var(--success) !important;
|
||||
stroke: var(--success) !important;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
box-shadow: inset 0 0 0 var(--success) !important;
|
||||
}
|
||||
|
||||
.checkmark-check {
|
||||
color: var(--success) !important;
|
||||
stroke: var(--success) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
.checkmark-circle {
|
||||
color: var(--warning) !important;
|
||||
stroke: var(--warning) !important;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
box-shadow: inset 0 0 0 var(--warning) !important;
|
||||
}
|
||||
|
||||
.checkmark-check {
|
||||
color: var(--warning) !important;
|
||||
stroke: var(--warning) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
.checkmark-circle {
|
||||
color: var(--red) !important;
|
||||
stroke: var(--red) !important;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
box-shadow: inset 0 0 0 var(--red) !important;
|
||||
}
|
||||
|
||||
.checkmark-check {
|
||||
color: var(--red) !important;
|
||||
stroke: var(--red) !important;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.checkmark-wrap {
|
||||
position: relative;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
display: inline-block;
|
||||
|
||||
.shadow-circle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: var(--radius-rounded);
|
||||
border: 1px solid var(--placeholder);
|
||||
z-index: 0;
|
||||
opacity: 1;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.is-opaque {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.checkmark-circle {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
stroke-dasharray: 166;
|
||||
stroke-dashoffset: 166;
|
||||
stroke-width: 2;
|
||||
stroke-miterlimit: 10;
|
||||
fill: none;
|
||||
color: var(--primary);
|
||||
stroke: var(--primary);
|
||||
}
|
||||
|
||||
// Checkmark
|
||||
.checkmark {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-rounded);
|
||||
display: block;
|
||||
stroke-width: 2;
|
||||
color: var(--placeholder);
|
||||
stroke: var(--placeholder);
|
||||
stroke-miterlimit: 10;
|
||||
margin: 0 auto;
|
||||
box-shadow: inset 0 0 0 var(--primary);
|
||||
}
|
||||
|
||||
// Check symbol
|
||||
.checkmark-check {
|
||||
transform-origin: 50% 50%;
|
||||
stroke-dasharray: 48;
|
||||
stroke-dashoffset: 48;
|
||||
color: var(--primary);
|
||||
stroke: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
.checkmark-circle {
|
||||
animation: stroke 0.6s $curve both;
|
||||
}
|
||||
|
||||
.checkmark-check {
|
||||
animation: stroke 0.3s $curve 0.8s both;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-unchecked {
|
||||
.checkmark-circle {
|
||||
animation: reverseCircle 0.6s $curve 0.2s both;
|
||||
}
|
||||
|
||||
.checkmark-check {
|
||||
animation: reverseCheck 0.3s $curve 0.1s both;
|
||||
}
|
||||
}
|
||||
|
||||
// Keyframes
|
||||
@keyframes stroke {
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reverseCircle {
|
||||
from {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
stroke-dashoffset: 166;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reverseCheck {
|
||||
from {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
stroke-dashoffset: 48;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.animated-checkbox {
|
||||
.checkmark-wrap {
|
||||
.checkmark-circle,
|
||||
.checkmark-check {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.shadow-circle {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
475
src/components/base/VApproval.vue
Normal file
475
src/components/base/VApproval.vue
Normal file
@@ -0,0 +1,475 @@
|
||||
<script setup lang="ts">
|
||||
import type { iCode, Person } from '/@src/utils/types'
|
||||
|
||||
const userSession = useUserSession()
|
||||
const notyf = useNotyf()
|
||||
|
||||
const apprLine = defineModel<any>({})
|
||||
|
||||
const me = userSession.user
|
||||
const userSelectDisable = ref(false)
|
||||
const selectTab = ref('appr')
|
||||
const apprTabs = ref([
|
||||
{ label: '결재', value: 'appr' },
|
||||
])
|
||||
const gubuns = ref<iCode[]>([])
|
||||
const attends = ref<iCode[]>([])
|
||||
|
||||
api.getCode(7).then((data) => {
|
||||
gubuns.value = data
|
||||
})
|
||||
api.getCode(6).then((data) => {
|
||||
attends.value = data
|
||||
})
|
||||
|
||||
const appendTab = (kind: string) => {
|
||||
notyf.dismissAll()
|
||||
if (kind === 'collabo') {
|
||||
const list = apprTabs.value.filter(e => e.label === '협조')
|
||||
const idx = list.length + 1
|
||||
apprTabs.value.push({
|
||||
label: '협조',
|
||||
value: `${kind}${idx}`,
|
||||
})
|
||||
apprLine.value[`${kind}${idx}`] = []
|
||||
}
|
||||
else {
|
||||
const list = apprTabs.value.filter(e => e.label === '통제')
|
||||
const idx = list.length + 1
|
||||
apprTabs.value.push({
|
||||
label: '통제',
|
||||
value: `${kind}${idx}`,
|
||||
})
|
||||
apprLine.value[`${kind}${idx}`] = []
|
||||
}
|
||||
|
||||
console.log(apprLine.value)
|
||||
}
|
||||
|
||||
const removeTab = () => {
|
||||
const reTab = apprTabs.value.filter(e => e.value !== selectTab.value)
|
||||
delete apprLine.value[selectTab.value]
|
||||
apprTabs.value = reTab
|
||||
selectTab.value = 'appr'
|
||||
}
|
||||
|
||||
const onDelete = (i: number) => {
|
||||
apprLine.value.splice(i, 1)
|
||||
userSelectDisable.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
apprLine.value[selectTab.value] = []
|
||||
})
|
||||
|
||||
watch(apprLine, (user) => {
|
||||
if (user === null) return
|
||||
userSelectDisable.value = true
|
||||
})
|
||||
|
||||
const appendUser = (user: Person) => {
|
||||
apprLine.value[selectTab.value].push(user)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
appendTab,
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="column is-12">
|
||||
<VUserList
|
||||
@appendUser="appendUser"
|
||||
:disabled="userSelectDisable"
|
||||
placeholder="결재자를 선택해주세요."/>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<VTabs
|
||||
type="boxed"
|
||||
v-model:selected='selectTab'
|
||||
:tabs="apprTabs"
|
||||
>
|
||||
<template #tab-link-label="{ activeValue, tab, key}" >
|
||||
<span class="link-label-inner">
|
||||
{{tab.label}}
|
||||
<i
|
||||
v-if="key !== 0"
|
||||
class="lnil lnil-close"
|
||||
@click.prevent="removeTab"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<template #tab="{ activeValue }">
|
||||
<div>
|
||||
<div :key="tab.value" v-for="(tab, idx) in apprTabs">
|
||||
<div class="approval-wrapper" v-if="activeValue === tab.value">
|
||||
<div class="approval-container">
|
||||
<div class="approval-table">
|
||||
<div class="approval-columns">
|
||||
<div class="approval-column" style="width:8%; padding-left:10px">입안</div>
|
||||
<div class="approval-column" style="width:15%"></div>
|
||||
<div class="approval-column" style="width:70%;"></div>
|
||||
<div class="approval-column"></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="approval-columns" v-for="(u, i) in apprLine[selectTab]">
|
||||
<div class="approval-column" style="width:8%">
|
||||
<VField>
|
||||
<VSelect v-model="u.gubunCd">
|
||||
<VOption v-for="gubun in gubuns" :value="gubun.cd">
|
||||
{{ gubun.nm }}
|
||||
</VOption>
|
||||
</VSelect>
|
||||
</VField>
|
||||
</div>
|
||||
<div class="approval-column" style="width:15%;">{{ u.ptsnNm }}</div>
|
||||
<div class="approval-column" style="width:30%;">{{ u.name }} ({{ u.sabun }})</div>
|
||||
<div class="approval-column" style="width:8%;">
|
||||
<VField>
|
||||
<VSelect v-model="u.attendCd">
|
||||
<VOption value="">재중</VOption>
|
||||
<VOption v-for="attend in attends" :value="attend.cd">
|
||||
{{ attend.nm }}
|
||||
</VOption>
|
||||
</VSelect>
|
||||
</VField>
|
||||
</div>
|
||||
<div>
|
||||
<i
|
||||
class="lnil lnil-close mouse-pointer"
|
||||
@click="onDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VTabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.lnil-close {
|
||||
line-height: 2.5em;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
margin-bottom:0px !important;
|
||||
span {
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 10px;
|
||||
}
|
||||
i {
|
||||
display: none;
|
||||
}
|
||||
.link-label-inner {
|
||||
position:relative;
|
||||
width:100%;
|
||||
}
|
||||
.is-active {
|
||||
i {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: -18px;
|
||||
top: 50%;
|
||||
font-size: 10px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.approval-container {
|
||||
.approval-columns {
|
||||
padding: 10px 20px 0 20px !important;
|
||||
|
||||
&:first-child {
|
||||
line-height: 3.5em;
|
||||
height: 3.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.approval-column {
|
||||
|
||||
line-height: 2em;
|
||||
|
||||
.select {
|
||||
line-height: 2em;
|
||||
|
||||
&:not(.is-multiple) {
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
select {
|
||||
&:not([multiple]) {
|
||||
padding-inline-end: 2em;
|
||||
}
|
||||
|
||||
height: 2em;
|
||||
padding-top: 0;
|
||||
padding-right: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
top: 50% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
|
||||
.visible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.is-navbar {
|
||||
.approval-toolbar {
|
||||
padding-top: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.approval-container {
|
||||
border: 1px solid var(--fade-grey);
|
||||
}
|
||||
|
||||
|
||||
.label {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.field {
|
||||
padding-bottom: 10px !important;
|
||||
|
||||
.label {
|
||||
font-size: 1.3em;
|
||||
color: var(--modal-text) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.approval-wrapper {
|
||||
width: 100%;
|
||||
|
||||
.approval-container {
|
||||
background: var(--white);
|
||||
border: none !important;
|
||||
overflow-x: auto;
|
||||
|
||||
.table,
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 8px !important;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px !important;
|
||||
background: rgb(0 0 0 / 20%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.approval-table {
|
||||
border: 1px solid var(--fade-grey);
|
||||
border-collapse: collapse;
|
||||
|
||||
.approval-columns {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid var(--fade-grey);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.approval-column {
|
||||
font-family: var(--font);
|
||||
vertical-align: middle;
|
||||
color: var(--light-text);
|
||||
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.approvals-empty {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.light-text {
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
.flex-media {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.meta {
|
||||
margin-left: 10px;
|
||||
line-height: 1.3;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: var(--light-text);
|
||||
font-family: var(--font);
|
||||
|
||||
&:first-child {
|
||||
font-family: var(--font-alt);
|
||||
color: var(--dark-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row-action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.product-photo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.drinks-icon {
|
||||
display: block;
|
||||
max-width: 48px;
|
||||
border-radius: var(--radius-rounded);
|
||||
border: 1px solid var(--fade-grey);
|
||||
}
|
||||
|
||||
.negative-icon,
|
||||
.positive-icon {
|
||||
.iconify {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.positive-icon {
|
||||
.iconify {
|
||||
color: var(--success);
|
||||
|
||||
* {
|
||||
stroke-width: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.negative-icon {
|
||||
&.is-danger {
|
||||
.iconify {
|
||||
color: var(--danger) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--light-text);
|
||||
|
||||
* {
|
||||
stroke-width: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.price {
|
||||
color: var(--dark-text);
|
||||
font-weight: 500;
|
||||
|
||||
&::before {
|
||||
content: '$';
|
||||
}
|
||||
|
||||
&.price-free {
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.is-available {
|
||||
.iconify {
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-busy {
|
||||
.iconify {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-offline {
|
||||
.iconify {
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
|
||||
.iconify {
|
||||
margin-right: 8px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: var(--font);
|
||||
font-size: 0.9rem;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.approval-wrapper {
|
||||
.approval-container {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 6%);
|
||||
}
|
||||
}
|
||||
|
||||
.approval-table {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
|
||||
th,
|
||||
td {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
|
||||
.drinks-icon {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lnil-close {
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
751
src/components/base/VAvatar.vue
Normal file
751
src/components/base/VAvatar.vue
Normal file
@@ -0,0 +1,751 @@
|
||||
<script setup lang="ts">
|
||||
export type VAvatarSize = 'small' | 'medium' | 'large' | 'big' | 'xl'
|
||||
export type VAvatarColor =
|
||||
| 'primary'
|
||||
| 'success'
|
||||
| 'info'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'h-purple'
|
||||
| 'h-orange'
|
||||
| 'h-blue'
|
||||
| 'h-green'
|
||||
| 'h-red'
|
||||
| 'h-yellow'
|
||||
export type VAvatarDotColor = 'primary' | 'success' | 'info' | 'warning' | 'danger'
|
||||
|
||||
export interface VAvatarProps {
|
||||
picture?: string
|
||||
pictureDark?: string
|
||||
placeholder?: string
|
||||
badge?: string
|
||||
initials?: string
|
||||
size?: VAvatarSize
|
||||
color?: VAvatarColor
|
||||
dotColor?: VAvatarDotColor
|
||||
squared?: boolean
|
||||
dot?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VAvatarProps>(), {
|
||||
picture: undefined,
|
||||
pictureDark: undefined,
|
||||
placeholder: 'https://via.placeholder.com/50x50',
|
||||
initials: '?',
|
||||
badge: undefined,
|
||||
size: undefined,
|
||||
color: undefined,
|
||||
dotColor: undefined,
|
||||
})
|
||||
|
||||
const { onceError } = useImageError()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="v-avatar"
|
||||
:class="[
|
||||
size && `is-${props.size}`,
|
||||
dot && 'has-dot',
|
||||
dotColor && `dot-${props.dotColor}`,
|
||||
squared && dot && 'has-dot-squared',
|
||||
]"
|
||||
>
|
||||
<slot name="avatar">
|
||||
<img
|
||||
v-if="props.picture"
|
||||
class="avatar"
|
||||
:class="[props.squared && 'is-squared', props.pictureDark && 'light-image']"
|
||||
:src="props.picture"
|
||||
alt=""
|
||||
@error.once="onceError($event, props.placeholder)"
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="avatar is-fake"
|
||||
:class="[props.squared && 'is-squared', props.color && `is-${props.color}`]"
|
||||
>
|
||||
<span>{{ props.initials }}</span>
|
||||
</span>
|
||||
<img
|
||||
v-if="props.picture && props.pictureDark"
|
||||
class="avatar dark-image"
|
||||
:class="[props.squared && 'is-squared']"
|
||||
:src="props.pictureDark"
|
||||
alt=""
|
||||
@error.once="onceError($event, props.placeholder)"
|
||||
>
|
||||
</slot>
|
||||
|
||||
<slot name="badge">
|
||||
<img
|
||||
v-if="props.badge"
|
||||
class="badge"
|
||||
:src="props.badge"
|
||||
alt=""
|
||||
@error.once="onceError($event, props.placeholder)"
|
||||
>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.v-avatar {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
|
||||
&.has-dot {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
inset-inline-end: 1px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: var(--radius-rounded);
|
||||
background: var(--success);
|
||||
border: 1.8px solid var(--white);
|
||||
}
|
||||
|
||||
&.has-dot-squared {
|
||||
&::after {
|
||||
top: -3px;
|
||||
inset-inline-end: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.dot-primary {
|
||||
&::after {
|
||||
background: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.dot-info {
|
||||
&::after {
|
||||
background: var(--info);
|
||||
}
|
||||
}
|
||||
|
||||
&.dot-warning {
|
||||
&::after {
|
||||
background: var(--warning);
|
||||
}
|
||||
}
|
||||
|
||||
&.dot-danger {
|
||||
&::after {
|
||||
background: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
&.dot-grey {
|
||||
&::after {
|
||||
background: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
border: 2px solid var(--white);
|
||||
|
||||
&.is-squared {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
&.is-fake {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--fade-grey);
|
||||
border-radius: var(--radius-rounded);
|
||||
|
||||
&.is-primary {
|
||||
background: color-mix(in oklab, var(--primary), white 80%);
|
||||
|
||||
span {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-accent {
|
||||
background: color-mix(in oklab, var(--primary), white 80%);
|
||||
|
||||
span {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
background: color-mix(in oklab, var(--success), white 80%);
|
||||
|
||||
span {
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
background: color-mix(in oklab, var(--info), white 80%);
|
||||
|
||||
span {
|
||||
color: var(--info);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
background: color-mix(in oklab, var(--warning), white 80%);
|
||||
|
||||
span {
|
||||
color: var(--warning);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
background: color-mix(in oklab, var(--danger), white 80%);
|
||||
|
||||
span {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-h-purple {
|
||||
background: color-mix(in oklab, var(--purple), white 80%);
|
||||
|
||||
span {
|
||||
color: var(--purple);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-h-orange {
|
||||
background: color-mix(in oklab, var(--orange), white 80%);
|
||||
|
||||
span {
|
||||
color: var(--orange);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-h-blue {
|
||||
background: color-mix(in oklab, var(--blue), white 80%);
|
||||
|
||||
span {
|
||||
color: var(--blue);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-h-red {
|
||||
background: color-mix(in oklab, var(--red), white 80%);
|
||||
|
||||
span {
|
||||
color: var(--red);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-h-green {
|
||||
background: color-mix(in oklab, var(--green), white 80%);
|
||||
|
||||
span {
|
||||
color: var(--green);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-h-yellow {
|
||||
background: color-mix(in oklab, var(--yellow), white 80%);
|
||||
|
||||
span {
|
||||
color: color-mix(in oklab, var(--yellow), black 8%);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
position: relative;
|
||||
display: block;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted-grey);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: var(--radius-rounded);
|
||||
border-width: 0;
|
||||
|
||||
.inner {
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 2px solid var(--white);
|
||||
border-radius: var(--radius-rounded);
|
||||
background: color-mix(in oklab, var(--fade-grey), white 2%);
|
||||
// border: 1px solid var(--fade-grey);
|
||||
|
||||
span {
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
|
||||
// top: -1px;
|
||||
inset-inline-start: -2px;
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline-end: 0;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border: 1px solid var(--white);
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
border-radius: var(--radius-rounded);
|
||||
}
|
||||
|
||||
&.is-small {
|
||||
max-width: 32px;
|
||||
min-width: 32px;
|
||||
max-height: 32px;
|
||||
|
||||
&.has-dot {
|
||||
&::after {
|
||||
content: '';
|
||||
top: 0;
|
||||
inset-inline-end: 0;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
border-width: 1.4px;
|
||||
}
|
||||
|
||||
&.has-dot-squared {
|
||||
&::after {
|
||||
top: -2px;
|
||||
inset-inline-end: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
|
||||
&.is-squared {
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
&.is-fake,
|
||||
&.is-more {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
|
||||
.inner {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
border-width: 1px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-medium {
|
||||
max-width: 50px;
|
||||
min-width: 50px;
|
||||
|
||||
&.has-dot {
|
||||
&::after {
|
||||
content: '';
|
||||
top: 1px;
|
||||
inset-inline-end: 1px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
&.has-dot-squared {
|
||||
&::after {
|
||||
top: -3px;
|
||||
inset-inline-end: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 50px;
|
||||
min-width: 50px;
|
||||
height: 50px;
|
||||
|
||||
&.is-squared {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
&.is-fake,
|
||||
&.is-more {
|
||||
width: 50px;
|
||||
min-width: 50px;
|
||||
height: 50px;
|
||||
|
||||
.inner {
|
||||
width: 50px;
|
||||
min-width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
border-width: 2px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-large {
|
||||
max-width: 68px;
|
||||
min-width: 68px;
|
||||
|
||||
&.has-dot {
|
||||
&::after {
|
||||
content: '';
|
||||
top: 4px;
|
||||
inset-inline-end: 4px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
border-width: 2.6px;
|
||||
}
|
||||
|
||||
&.has-dot-squared {
|
||||
&::after {
|
||||
top: -4px;
|
||||
inset-inline-end: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 68px;
|
||||
min-width: 68px;
|
||||
height: 68px;
|
||||
|
||||
&.is-squared {
|
||||
border-radius: 16px !important;
|
||||
}
|
||||
|
||||
&.is-fake {
|
||||
width: 68px;
|
||||
min-width: 68px;
|
||||
height: 68px;
|
||||
|
||||
span {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
border-width: 2px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-big {
|
||||
max-width: 80px;
|
||||
min-width: 80px;
|
||||
|
||||
&.has-dot {
|
||||
&::after {
|
||||
content: '';
|
||||
top: 4px;
|
||||
inset-inline-end: 4px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-width: 2.8px;
|
||||
}
|
||||
|
||||
&.has-dot-squared {
|
||||
&::after {
|
||||
top: -4px;
|
||||
inset-inline-end: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
height: 80px;
|
||||
|
||||
&.is-squared {
|
||||
border-radius: 18px !important;
|
||||
}
|
||||
|
||||
&.is-fake {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
height: 80px;
|
||||
|
||||
span {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
border-width: 2.4px;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-xl {
|
||||
max-width: 100px;
|
||||
min-width: 100px;
|
||||
|
||||
&.has-dot {
|
||||
&::after {
|
||||
content: '';
|
||||
top: 6px;
|
||||
inset-inline-end: 5px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
border-width: 2.8px;
|
||||
}
|
||||
|
||||
&.has-dot-squared {
|
||||
&::after {
|
||||
top: -3px;
|
||||
inset-inline-end: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100px;
|
||||
min-width: 100px;
|
||||
height: 100px;
|
||||
|
||||
&.is-squared {
|
||||
border-radius: 22px !important;
|
||||
}
|
||||
|
||||
&.is-fake {
|
||||
width: 100px;
|
||||
min-width: 100px;
|
||||
height: 100px;
|
||||
|
||||
span {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
border-width: 3px;
|
||||
height: 34px;
|
||||
width: 34px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-stack {
|
||||
display: flex;
|
||||
|
||||
.v-avatar {
|
||||
border-radius: var(--radius-rounded);
|
||||
|
||||
&.is-small {
|
||||
border-radius: var(--radius-rounded);
|
||||
|
||||
&:not(:first-child) {
|
||||
$var: 12;
|
||||
|
||||
@for $i from 1 through 99 {
|
||||
&:nth-child(#{$i}) {
|
||||
margin-inline-start: -#{$var}px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-medium {
|
||||
border-radius: var(--radius-rounded);
|
||||
|
||||
&:not(:first-child) {
|
||||
$var: 16;
|
||||
|
||||
@for $i from 1 through 99 {
|
||||
&:nth-child(#{$i}) {
|
||||
margin-inline-start: -#{$var}px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
$var: 14;
|
||||
|
||||
@for $i from 1 through 99 {
|
||||
&:nth-child(#{$i}) {
|
||||
margin-inline-start: -#{$var}px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.v-avatar {
|
||||
&.has-dot {
|
||||
&::after {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 6%);
|
||||
|
||||
&.is-fake {
|
||||
&.is-primary {
|
||||
background: var(--primary);
|
||||
|
||||
span {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-accent {
|
||||
background: var(--primary);
|
||||
|
||||
span {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
background: var(--success);
|
||||
|
||||
span {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
background: var(--info);
|
||||
|
||||
span {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
background: var(--warning);
|
||||
|
||||
span {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
background: var(--danger);
|
||||
|
||||
span {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-h-purple {
|
||||
background: var(--purple);
|
||||
|
||||
span {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-h-orange {
|
||||
background: var(--orange);
|
||||
|
||||
span {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-h-blue {
|
||||
background: var(--blue);
|
||||
|
||||
span {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-h-red {
|
||||
background: var(--red);
|
||||
|
||||
span {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-h-green {
|
||||
background: var(--green);
|
||||
|
||||
span {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-h-yellow {
|
||||
background: var(--yellow);
|
||||
|
||||
span {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-more {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), black 12%);
|
||||
|
||||
.inner {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-fake {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
43
src/components/base/VAvatarStack.vue
Normal file
43
src/components/base/VAvatarStack.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import type { VAvatarProps } from './VAvatar.vue'
|
||||
|
||||
export type VAvatarStackSize = 'small' | 'medium' | 'large' | 'big' | 'xl'
|
||||
|
||||
export interface VAvatarStackProps {
|
||||
limit?: number
|
||||
size?: VAvatarStackSize
|
||||
avatars?: VAvatarProps[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VAvatarStackProps>(), {
|
||||
limit: 5,
|
||||
size: undefined,
|
||||
avatars: () => [],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="avatar-stack">
|
||||
<slot>
|
||||
<VAvatar
|
||||
v-for="(avatar, index) in avatars.slice(0, props.limit)"
|
||||
:key="index"
|
||||
:size="props.size"
|
||||
:picture="avatar.picture"
|
||||
:initials="avatar.initials"
|
||||
:color="avatar.color"
|
||||
/>
|
||||
<div
|
||||
v-if="avatars.length > props.limit"
|
||||
class="v-avatar"
|
||||
:class="[size && 'is-' + props.size]"
|
||||
>
|
||||
<span class="avatar is-more">
|
||||
<span class="inner">
|
||||
<span>+{{ avatars.length - props.limit }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
292
src/components/base/VBlock.vue
Normal file
292
src/components/base/VBlock.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<script setup lang="ts">
|
||||
export interface VBlockProps {
|
||||
title?: string
|
||||
subtitle?: string
|
||||
infratitle?: string
|
||||
center?: boolean
|
||||
lighter?: boolean
|
||||
narrow?: boolean
|
||||
mResponsive?: boolean
|
||||
tResponsive?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VBlockProps>(), {
|
||||
title: undefined,
|
||||
subtitle: undefined,
|
||||
infratitle: undefined,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
!props.center && 'media-flex',
|
||||
props.center && 'media-flex-center',
|
||||
props.narrow && 'no-margin',
|
||||
props.mResponsive && 'is-responsive-mobile',
|
||||
props.tResponsive && 'is-responsive-tablet-p',
|
||||
]"
|
||||
>
|
||||
<slot name="icon" />
|
||||
<div
|
||||
class="flex-meta"
|
||||
:class="[props.lighter && 'is-lighter']"
|
||||
>
|
||||
<slot name="title">
|
||||
<span>{{ props.title }}</span>
|
||||
<span v-if="props.subtitle">{{ props.subtitle }}</span>
|
||||
<span v-if="props.infratitle">{{ props.infratitle }}</span>
|
||||
</slot>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="flex-end">
|
||||
<slot name="action" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.media-flex {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
|
||||
&:last-child,
|
||||
&.no-margin {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.flex-meta {
|
||||
margin-inline-start: 12px;
|
||||
line-height: 1.3;
|
||||
|
||||
&.is-lighter {
|
||||
span,
|
||||
> a {
|
||||
&:first-child {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-light {
|
||||
span,
|
||||
> a {
|
||||
&:first-child {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span,
|
||||
> a {
|
||||
display: block;
|
||||
|
||||
&:first-child {
|
||||
font-family: var(--font-alt);
|
||||
color: var(--dark-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
font-family: var(--font);
|
||||
color: var(--light-text);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
margin-inline-start: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.end-action {
|
||||
margin-inline-start: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
|
||||
&:last-child,
|
||||
&.no-margin {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.flex-meta {
|
||||
margin-inline-start: 12px;
|
||||
line-height: 1.4;
|
||||
|
||||
&.is-lighter {
|
||||
span,
|
||||
> a {
|
||||
&:first-child {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-light {
|
||||
span,
|
||||
> a {
|
||||
&:first-child {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span,
|
||||
> a {
|
||||
display: block;
|
||||
|
||||
&:first-child {
|
||||
font-family: var(--font-alt);
|
||||
font-size: 0.95rem;
|
||||
color: var(--dark-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
font-family: var(--font);
|
||||
color: var(--light-text);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
margin-inline-start: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.end-action {
|
||||
margin-inline-start: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.media-flex-center,
|
||||
.media-flex {
|
||||
.flex-meta {
|
||||
span,
|
||||
a {
|
||||
&:first-child {
|
||||
color: var(--dark-dark-text) !important;
|
||||
}
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 767px) {
|
||||
.media-flex,
|
||||
.media-flex-center {
|
||||
&.is-responsive-mobile {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
|
||||
.v-avatar,
|
||||
.v-icon {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.flex-meta {
|
||||
margin: 10px auto 0;
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
margin: 10px auto;
|
||||
|
||||
.end-action {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: portrait) {
|
||||
.media-flex,
|
||||
.media-flex-center {
|
||||
&.is-responsive-tablet-p {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
|
||||
.v-avatar,
|
||||
.v-icon {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.flex-meta {
|
||||
margin: 10px auto 0;
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
margin: 10px auto;
|
||||
|
||||
.end-action {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: landscape) {
|
||||
.media-flex,
|
||||
.media-flex-center {
|
||||
&.is-responsive-tablet-l {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
|
||||
.v-avatar,
|
||||
.v-icon {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.flex-meta {
|
||||
margin: 10px auto 0;
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
margin: 10px auto;
|
||||
|
||||
.end-action {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
221
src/components/base/VBreadcrumb.vue
Normal file
221
src/components/base/VBreadcrumb.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<script setup lang="ts">
|
||||
export type VBreadcrumbSeparator = 'arrow' | 'bullet' | 'dot' | 'succeeds'
|
||||
export type VBreadcrumbAlign = 'center' | 'right'
|
||||
export interface VBreadcrumbItem {
|
||||
label: string
|
||||
hideLabel?: boolean
|
||||
icon?: string
|
||||
link?: string
|
||||
to?: any
|
||||
}
|
||||
export interface VBreadcrumbsProps {
|
||||
items: VBreadcrumbItem[]
|
||||
separator?: VBreadcrumbSeparator
|
||||
align?: VBreadcrumbAlign
|
||||
withIcons?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VBreadcrumbsProps>(), {
|
||||
separator: undefined,
|
||||
align: undefined,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav
|
||||
role="navigation"
|
||||
class="breadcrumb"
|
||||
aria-label="breadcrumbs"
|
||||
itemscope
|
||||
itemtype="https://schema.org/BreadcrumbList"
|
||||
:class="[`has-${props.separator}-separator`, props.align && `is-${props.align}`]"
|
||||
>
|
||||
<ul>
|
||||
<li
|
||||
v-for="(item, key) in props.items"
|
||||
:key="key"
|
||||
:aria-current="key === items.length - 1 ? 'page' : undefined"
|
||||
itemprop="itemListElement"
|
||||
itemscope
|
||||
itemtype="https://schema.org/ListItem"
|
||||
>
|
||||
<slot
|
||||
name="breadcrumb-item"
|
||||
:item="item"
|
||||
:index="key"
|
||||
>
|
||||
<RouterLink
|
||||
v-if="item.to"
|
||||
class="breadcrumb-item"
|
||||
itemprop="item"
|
||||
:to="item.to"
|
||||
>
|
||||
<span
|
||||
v-if="props.withIcons && !!item.icon"
|
||||
class="icon is-small"
|
||||
:class="[item.hideLabel && props.withIcons && !!item.icon && 'is-solo']"
|
||||
>
|
||||
<VIcon :icon="item.icon" />
|
||||
</span>
|
||||
<meta
|
||||
v-if="item.hideLabel && props.withIcons && !!item.icon"
|
||||
itemprop="name"
|
||||
:content="item.label"
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
itemprop="name"
|
||||
>
|
||||
<slot
|
||||
name="breadcrumb-item-label"
|
||||
:item="item"
|
||||
:index="key"
|
||||
>
|
||||
{{ item.label }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<meta
|
||||
itemprop="position"
|
||||
:content="`${key + 1}`"
|
||||
>
|
||||
</RouterLink>
|
||||
<a
|
||||
v-else-if="item.link"
|
||||
class="breadcrumb-item"
|
||||
itemprop="item"
|
||||
:href="item.link"
|
||||
>
|
||||
<span
|
||||
v-if="props.withIcons && !!item.icon"
|
||||
class="icon is-small"
|
||||
:class="[item.hideLabel && props.withIcons && !!item.icon && 'is-solo']"
|
||||
>
|
||||
<VIcon :icon="item.icon" />
|
||||
</span>
|
||||
<meta
|
||||
v-if="item.hideLabel && props.withIcons && !!item.icon"
|
||||
itemprop="name"
|
||||
:content="item.label"
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
itemprop="name"
|
||||
>
|
||||
<slot
|
||||
name="breadcrumb-item-label"
|
||||
:item="item"
|
||||
:index="key"
|
||||
>
|
||||
{{ item.label }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<meta
|
||||
itemprop="position"
|
||||
:content="`${key + 1}`"
|
||||
>
|
||||
</a>
|
||||
<span
|
||||
v-else
|
||||
class="breadcrumb-item"
|
||||
>
|
||||
<span
|
||||
v-if="props.withIcons && !!item.icon"
|
||||
class="icon is-small"
|
||||
:class="[item.hideLabel && props.withIcons && !!item.icon && 'is-solo']"
|
||||
>
|
||||
<VIcon :icon="item.icon" />
|
||||
</span>
|
||||
<meta
|
||||
v-if="item.hideLabel && props.withIcons && item.icon"
|
||||
itemprop="name"
|
||||
:content="item.label"
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
itemprop="name"
|
||||
>
|
||||
<slot
|
||||
name="breadcrumb-item-label"
|
||||
:item="item"
|
||||
:index="key"
|
||||
>
|
||||
{{ item.label }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<meta
|
||||
itemprop="position"
|
||||
:content="`${key + 1}`"
|
||||
>
|
||||
</span>
|
||||
</slot>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.breadcrumb {
|
||||
&.is-narrow {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
&:first-child {
|
||||
.breadcrumb-item {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
font-family: var(--font);
|
||||
color: var(--light-text);
|
||||
padding: 0 0.75em;
|
||||
|
||||
.icon {
|
||||
&.is-solo {
|
||||
.iconify {
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.iconify {
|
||||
position: relative;
|
||||
top: 0;
|
||||
font-size: 16px;
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
&.breadcrumb-item {
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.breadcrumb {
|
||||
ul {
|
||||
li {
|
||||
a {
|
||||
&.breadcrumb-item {
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
638
src/components/base/VButton.vue
Normal file
638
src/components/base/VButton.vue
Normal file
@@ -0,0 +1,638 @@
|
||||
<script lang="ts">
|
||||
import type { RouteLocationAsString } from 'unplugin-vue-router'
|
||||
import type { SlotsType, PropType } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import VPlaceload from '/@src/components/base/VPlaceload.vue'
|
||||
|
||||
export type VButtonSize = 'medium' | 'big' | 'huge'
|
||||
export type VButtonColor =
|
||||
| 'primary'
|
||||
| 'info'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'white'
|
||||
| 'dark'
|
||||
| 'light'
|
||||
export type VButtonDark = '1' | '2' | '3' | '4' | '5' | '6'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
to: {
|
||||
type: [Object, String] as PropType<RouteLocationAsString>,
|
||||
default: undefined,
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
iconCaret: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
placeload: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
validator: (value: string) => {
|
||||
if (value.match(CssUnitRe) === null) {
|
||||
console.warn(
|
||||
`VButton: invalid "${value}" placeload. Should be a valid css unit value.`,
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
color: {
|
||||
type: String as PropType<VButtonColor>,
|
||||
default: undefined,
|
||||
validator: (value: VButtonColor) => {
|
||||
// The value must match one of these strings
|
||||
if (
|
||||
[
|
||||
undefined,
|
||||
'primary',
|
||||
'info',
|
||||
'success',
|
||||
'warning',
|
||||
'danger',
|
||||
'white',
|
||||
'dark',
|
||||
'light',
|
||||
].indexOf(value) === -1
|
||||
) {
|
||||
console.warn(
|
||||
`VButton: invalid "${value}" color. Should be primary, info, success, warning, danger, dark, light, white or undefined`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<VButtonSize>,
|
||||
default: undefined,
|
||||
validator: (value: VButtonSize) => {
|
||||
// The value must match one of these strings
|
||||
if ([undefined, 'medium', 'big', 'huge'].indexOf(value) === -1) {
|
||||
console.warn(
|
||||
`VButton: invalid "${value}" size. Should be big, huge, medium or undefined`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
type: String as PropType<VButtonDark>,
|
||||
default: undefined,
|
||||
validator: (value: VButtonDark) => {
|
||||
// The value must match one of these strings
|
||||
if ([undefined, '1', '2', '3', '4', '5', '6'].indexOf(value) === -1) {
|
||||
console.warn(
|
||||
`VButton: invalid "${value}" dark. Should be 1, 2, 3, 4, 5, 6 or undefined`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
bold: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
fullwidth: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
light: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
raised: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
elevated: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
outlined: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
darkOutlined: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
lower: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
static: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
slots: Object as SlotsType<{
|
||||
default: void
|
||||
}>,
|
||||
setup(props, { slots, attrs }) {
|
||||
const classes = computed(() => {
|
||||
const defaultClasses = (attrs?.class || []) as string[] | string
|
||||
return [
|
||||
defaultClasses,
|
||||
'button',
|
||||
'v-button',
|
||||
props.disabled && 'is-disabled',
|
||||
props.rounded && 'is-rounded',
|
||||
props.bold && 'is-bold',
|
||||
props.size && `is-${props.size}`,
|
||||
props.lower && 'is-lower',
|
||||
props.fullwidth && 'is-fullwidth',
|
||||
props.outlined && 'is-outlined',
|
||||
props.dark && `is-dark-bg-${props.dark}`,
|
||||
props.darkOutlined && 'is-dark-outlined',
|
||||
props.raised && 'is-raised',
|
||||
props.elevated && 'is-elevated',
|
||||
props.loading && !props.placeload && 'is-loading',
|
||||
props.color && `is-${props.color}`,
|
||||
props.light && 'is-light',
|
||||
props.static && 'is-static',
|
||||
]
|
||||
})
|
||||
const isIconify = computed(() => props.icon && props.icon.indexOf(':') !== -1)
|
||||
const isCaretIconify = computed(
|
||||
() => props.iconCaret && props.iconCaret.indexOf(':') !== -1,
|
||||
)
|
||||
|
||||
const getChildrens = () => {
|
||||
const childrens = []
|
||||
|
||||
let iconWrapper
|
||||
if (isIconify.value) {
|
||||
const icon = h('iconify-icon', {
|
||||
class: 'iconify',
|
||||
icon: props.icon,
|
||||
})
|
||||
iconWrapper = h('span', { class: 'icon' }, icon)
|
||||
}
|
||||
else if (props.icon) {
|
||||
const icon = h('i', { 'aria-hidden': true, 'class': props.icon })
|
||||
iconWrapper = h('span', { class: 'icon rtl-reflect' }, icon)
|
||||
}
|
||||
|
||||
let caretWrapper
|
||||
if (isCaretIconify.value) {
|
||||
const caret = h('iconify-icon', {
|
||||
class: 'iconify',
|
||||
icon: props.iconCaret,
|
||||
})
|
||||
caretWrapper = h('span', { class: 'caret' }, caret)
|
||||
}
|
||||
else if (props.iconCaret) {
|
||||
const caret = h('i', { 'aria-hidden': true, 'class': props.iconCaret })
|
||||
caretWrapper = h('span', { class: 'caret' }, caret)
|
||||
}
|
||||
|
||||
if (iconWrapper) {
|
||||
childrens.push(iconWrapper)
|
||||
}
|
||||
if (props.placeload) {
|
||||
childrens.push(
|
||||
h(VPlaceload, {
|
||||
width: props.placeload,
|
||||
}),
|
||||
)
|
||||
}
|
||||
else {
|
||||
childrens.push(h('span', slots.default?.()))
|
||||
}
|
||||
if (caretWrapper) {
|
||||
childrens.push(caretWrapper)
|
||||
}
|
||||
|
||||
return childrens
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (props.to) {
|
||||
return h(
|
||||
RouterLink,
|
||||
{
|
||||
...attrs,
|
||||
'aria-hidden': !!props.placeload,
|
||||
'to': props.to,
|
||||
'class': ['button', ...classes.value],
|
||||
},
|
||||
{
|
||||
default: getChildrens,
|
||||
},
|
||||
)
|
||||
}
|
||||
else if (props.href) {
|
||||
return h(
|
||||
'a',
|
||||
{
|
||||
...attrs,
|
||||
'aria-hidden': !!props.placeload,
|
||||
'href': props.href,
|
||||
'class': classes.value,
|
||||
},
|
||||
{
|
||||
default: getChildrens,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return h(
|
||||
'button',
|
||||
{
|
||||
'type': 'button',
|
||||
...attrs,
|
||||
'aria-hidden': !!props.placeload,
|
||||
'disabled': props.disabled,
|
||||
'class': ['button', ...classes.value],
|
||||
},
|
||||
{
|
||||
default: getChildrens,
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.button {
|
||||
&.is-circle {
|
||||
border-radius: var(--radius-rounded);
|
||||
}
|
||||
|
||||
&.v-button {
|
||||
padding: 8px 22px;
|
||||
height: 38px;
|
||||
line-height: 1.1;
|
||||
font-size: 0.95rem;
|
||||
font-family: var(--font);
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&:not([disabled]) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
box-shadow: none !important;
|
||||
border-color: color-mix(in oklab, var(--fade-grey), black 2%);
|
||||
}
|
||||
|
||||
&:not(
|
||||
.is-primary,
|
||||
.is-success,
|
||||
.is-info,
|
||||
.is-warning,
|
||||
.is-danger,
|
||||
.is-light,
|
||||
.is-white
|
||||
) {
|
||||
&.is-active {
|
||||
background: var(--primary) !important;
|
||||
border-color: var(--primary) !important;
|
||||
color: var(--white) !important;
|
||||
box-shadow: var(--primary-box-shadow) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline-offset: var(--accessibility-focus-outline-offset);
|
||||
outline-width: var(--accessibility-focus-outline-width);
|
||||
outline-style: var(--accessibility-focus-outline-style);
|
||||
outline-color: var(--accessibility-focus-outline-color);
|
||||
}
|
||||
|
||||
&.is-bold {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
&.is-raised:hover {
|
||||
opacity: 0.9;
|
||||
box-shadow: var(--primary-box-shadow);
|
||||
}
|
||||
|
||||
&.is-elevated {
|
||||
box-shadow: var(--primary-box-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
&.is-raised:hover {
|
||||
opacity: 0.9;
|
||||
box-shadow: var(--success-box-shadow);
|
||||
}
|
||||
|
||||
&.is-elevated {
|
||||
box-shadow: var(--success-box-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
&.is-raised:hover {
|
||||
opacity: 0.9;
|
||||
box-shadow: var(--info-box-shadow);
|
||||
}
|
||||
|
||||
&.is-elevated {
|
||||
box-shadow: var(--info-box-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
&.is-raised:hover {
|
||||
opacity: 0.9;
|
||||
box-shadow: var(--warning-box-shadow);
|
||||
}
|
||||
|
||||
&.is-elevated {
|
||||
box-shadow: var(--warning-box-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
&.is-raised:hover {
|
||||
opacity: 0.9;
|
||||
box-shadow: var(--danger-box-shadow);
|
||||
}
|
||||
|
||||
&.is-elevated {
|
||||
box-shadow: var(--danger-box-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-lower {
|
||||
text-transform: none !important;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
&.is-big {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
&.is-medium {
|
||||
height: 2.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&.is-huge {
|
||||
height: 50px;
|
||||
width: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
&.simple-action {
|
||||
height: 32px;
|
||||
padding: 0 24px;
|
||||
line-height: 0;
|
||||
border-radius: 100px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&.is-purple {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: var(--smoke-white);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
opacity: 0.95;
|
||||
box-shadow: var(--primary-box-shadow);
|
||||
color: var(--smoke-white) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-icon {
|
||||
.iconify {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
.iconify {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.v-button {
|
||||
&:not(
|
||||
.is-primary,
|
||||
.is-success,
|
||||
.is-info,
|
||||
.is-warning,
|
||||
.is-danger,
|
||||
.is-light,
|
||||
.is-white
|
||||
) {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
color: var(--dark-dark-text);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 18%);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-light {
|
||||
border: none;
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
|
||||
color: var(--smoke-white) !important;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
|
||||
&.is-raised:hover {
|
||||
box-shadow: var(--primary-box-shadow) !important;
|
||||
}
|
||||
|
||||
&.is-elevated {
|
||||
box-shadow: var(--primary-box-shadow) !important;
|
||||
}
|
||||
|
||||
&.is-outlined {
|
||||
background: transparent;
|
||||
border-color: var(--primary) !important;
|
||||
color: var(--primary);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: var(--primary) !important;
|
||||
border-color: var(--primary) !important;
|
||||
color: var(--white) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-light {
|
||||
border: none;
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
|
||||
color: color-mix(in oklab, var(--primary), white 20%) !important;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
&.is-light {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
|
||||
color: color-mix(in oklab, var(--info), white 20%) !important;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
&.is-light {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
|
||||
color: color-mix(in oklab, var(--success), white 20%) !important;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
&.is-light {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
|
||||
color: color-mix(in oklab, var(--warning), white 20%) !important;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
&.is-light {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
|
||||
color: color-mix(in oklab, var(--danger), white 20%) !important;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-white {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 6%) !important;
|
||||
border-color: var(--muted-grey) !important;
|
||||
color: var(--muted-grey) !important;
|
||||
}
|
||||
|
||||
&.is-dark-outlined {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
color: var(--dark-dark-text);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: var(--primary) !important;
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
&:not(
|
||||
.is-primary,
|
||||
.is-success,
|
||||
.is-info,
|
||||
.is-warning,
|
||||
.is-danger,
|
||||
.is-light,
|
||||
.is-white
|
||||
) {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
color: var(--dark-dark-text);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 18%);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
&.is-white {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 6%) !important;
|
||||
border-color: var(--muted-grey) !important;
|
||||
color: var(--muted-grey) !important;
|
||||
}
|
||||
|
||||
&.is-dark-outlined {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
color: var(--dark-dark-text);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: var(--primary) !important;
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
20
src/components/base/VButtons.vue
Normal file
20
src/components/base/VButtons.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
export type VButtonsAlign = 'centered' | 'right'
|
||||
export interface VButtonsProps {
|
||||
align?: VButtonsAlign
|
||||
addons?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VButtonsProps>(), {
|
||||
align: undefined,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="buttons"
|
||||
:class="[props.addons && 'has-addons', props.align && `is-${props.align}`]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
38
src/components/base/VCard.vue
Normal file
38
src/components/base/VCard.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
export type VCardRadius = 'regular' | 'smooth' | 'rounded'
|
||||
export type VCardColor =
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'info'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
export interface VCardProps {
|
||||
radius?: VCardRadius
|
||||
color?: VCardColor
|
||||
elevated?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VCardProps>(), {
|
||||
radius: undefined,
|
||||
color: undefined,
|
||||
elevated: false,
|
||||
})
|
||||
|
||||
const cardRadius = computed(() => {
|
||||
if (props.radius === 'smooth') {
|
||||
return 's-card'
|
||||
}
|
||||
else if (props.radius === 'rounded') {
|
||||
return 'l-card'
|
||||
}
|
||||
|
||||
return 'r-card'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[cardRadius, elevated && 'is-raised', props.color && `is-${props.color}`]">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
61
src/components/base/VCardAction.vue
Normal file
61
src/components/base/VCardAction.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
export type VCardActionRadius = 'regular' | 'smooth' | 'rounded'
|
||||
export interface VCardActionProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
avatar?: string
|
||||
badge?: string
|
||||
content?: string
|
||||
radius?: VCardActionRadius
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VCardActionProps>(), {
|
||||
subtitle: undefined,
|
||||
avatar: undefined,
|
||||
badge: undefined,
|
||||
content: undefined,
|
||||
radius: 'regular',
|
||||
})
|
||||
|
||||
const slots = useSlots()
|
||||
const hasDefaultSlot = ref(!!slots.default?.())
|
||||
|
||||
onUpdated(() => {
|
||||
hasDefaultSlot.value = !!slots.default?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="is-raised"
|
||||
:class="[
|
||||
props.radius === 'regular' && 's-card',
|
||||
props.radius === 'smooth' && 'r-card',
|
||||
props.radius === 'rounded' && 'l-card',
|
||||
]"
|
||||
>
|
||||
<div class="card-head">
|
||||
<VBlock
|
||||
:title="props.title"
|
||||
:subtitle="props.subtitle"
|
||||
center
|
||||
>
|
||||
<template #icon>
|
||||
<VAvatar
|
||||
:picture="props.avatar"
|
||||
:badge="props.badge"
|
||||
/>
|
||||
</template>
|
||||
<template #action>
|
||||
<slot name="action" />
|
||||
</template>
|
||||
</VBlock>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasDefaultSlot"
|
||||
class="card-inner"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
40
src/components/base/VCardAdvanced.vue
Normal file
40
src/components/base/VCardAdvanced.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
export type VCardAdvancedRadius = 'regular' | 'smooth' | 'rounded'
|
||||
export interface VCardAdvancedProps {
|
||||
radius?: VCardAdvancedRadius
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VCardAdvancedProps>(), {
|
||||
radius: 'regular',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
props.radius === 'regular' && 's-card-advanced',
|
||||
props.radius === 'smooth' && 'r-card-advanced',
|
||||
props.radius === 'rounded' && 'l-card-advanced',
|
||||
]"
|
||||
>
|
||||
<div class="card-head">
|
||||
<div class="left">
|
||||
<slot name="header-left" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot name="header-right" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
<div class="card-foot">
|
||||
<div class="left">
|
||||
<slot name="footer-left" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot name="footer-right" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
77
src/components/base/VCardMedia.vue
Normal file
77
src/components/base/VCardMedia.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
export type VCardMediaFormat = '4by3' | '16by9'
|
||||
export interface VCardMediaProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
image?: string
|
||||
avatar?: string
|
||||
badge?: string
|
||||
placeholder?: string
|
||||
format?: VCardMediaFormat
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VCardMediaProps>(), {
|
||||
subtitle: undefined,
|
||||
image: undefined,
|
||||
avatar: undefined,
|
||||
badge: undefined,
|
||||
placeholder: 'https://via.placeholder.com/1280x960',
|
||||
format: '4by3',
|
||||
})
|
||||
|
||||
const slots = useSlots()
|
||||
const hasDefaultSlot = ref(!!slots.default?.())
|
||||
|
||||
function placeholderHandler(event: Event) {
|
||||
const target = event.target as HTMLImageElement
|
||||
target.src = props.placeholder
|
||||
}
|
||||
|
||||
onUpdated(() => {
|
||||
hasDefaultSlot.value = !!slots.default?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card v-card">
|
||||
<div
|
||||
v-if="props.image"
|
||||
class="card-image"
|
||||
>
|
||||
<figure
|
||||
class="image is-4by3"
|
||||
:class="[props.format && `is-${props.format}`]"
|
||||
>
|
||||
<img
|
||||
:src="image"
|
||||
alt=""
|
||||
@error.once="placeholderHandler"
|
||||
>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<VBlock
|
||||
:title="props.title"
|
||||
:subtitle="props.subtitle"
|
||||
center
|
||||
narrow
|
||||
>
|
||||
<template #icon>
|
||||
<VAvatar
|
||||
v-if="props.avatar"
|
||||
:picture="props.avatar"
|
||||
:badge="props.badge"
|
||||
size="medium"
|
||||
/>
|
||||
</template>
|
||||
</VBlock>
|
||||
|
||||
<div
|
||||
v-if="hasDefaultSlot"
|
||||
class="inner-content pt-5"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
148
src/components/base/VCardSocial.vue
Normal file
148
src/components/base/VCardSocial.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<script setup lang="ts">
|
||||
export type VCardSocialNetwork =
|
||||
| 'facebook'
|
||||
| 'twitter'
|
||||
| 'linkedin'
|
||||
| 'tumblr'
|
||||
| 'github'
|
||||
| 'dribbble'
|
||||
| 'google-plus'
|
||||
| 'youtube'
|
||||
| 'reddit'
|
||||
| 'invision'
|
||||
| 'amazon'
|
||||
| 'instagram'
|
||||
|
||||
export interface VCardSocialEmits {
|
||||
(e: 'iconClick'): void
|
||||
(e: 'share'): void
|
||||
(e: 'like'): void
|
||||
(e: 'hashtagClick', tag: string): void
|
||||
}
|
||||
export interface VCardSocialProps {
|
||||
title: string
|
||||
network: VCardSocialNetwork
|
||||
hashtags?: string[]
|
||||
avatar?: string
|
||||
username?: string
|
||||
shareLabel?: string
|
||||
likeLabel?: string
|
||||
}
|
||||
|
||||
const emit = defineEmits<VCardSocialEmits>()
|
||||
const props = withDefaults(defineProps<VCardSocialProps>(), {
|
||||
hashtags: () => [],
|
||||
avatar: undefined,
|
||||
username: undefined,
|
||||
shareLabel: 'Share',
|
||||
likeLabel: 'Like',
|
||||
})
|
||||
|
||||
const icon = computed(() => {
|
||||
switch (props.network) {
|
||||
case 'facebook':
|
||||
return 'fa-brands:facebook-f'
|
||||
case 'twitter':
|
||||
return 'fa-brands:twitter'
|
||||
case 'linkedin':
|
||||
return 'fa-brands:linkedin-in'
|
||||
case 'tumblr':
|
||||
return 'fa-brands:tumblr'
|
||||
case 'github':
|
||||
return 'fa-brands:github-alt'
|
||||
case 'dribbble':
|
||||
return 'fa-brands:dribbble'
|
||||
case 'google-plus':
|
||||
return 'fa-brands:google-plus-g'
|
||||
case 'youtube':
|
||||
return 'fa-brands:youtube'
|
||||
case 'reddit':
|
||||
return 'fa-brands:reddit-alien'
|
||||
case 'invision':
|
||||
return 'fa-brands:invision'
|
||||
case 'amazon':
|
||||
return 'fa-brands:amazon'
|
||||
case 'instagram':
|
||||
return 'fa-brands:instagram'
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card v-card">
|
||||
<header class="card-header">
|
||||
<div class="card-header-title">
|
||||
{{ props.title }}
|
||||
</div>
|
||||
<a
|
||||
v-if="icon"
|
||||
class="card-header-icon"
|
||||
:class="[props.network && `text-${props.network}`]"
|
||||
:aria-label="`View on ${props.network}`"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="emit('iconClick')"
|
||||
@click="emit('iconClick')"
|
||||
>
|
||||
<VIcon :icon="icon" />
|
||||
</a>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<VBlock
|
||||
:title="props.username"
|
||||
class="pb-3"
|
||||
>
|
||||
<template #icon>
|
||||
<VAvatar
|
||||
v-if="props.avatar"
|
||||
size="medium"
|
||||
:picture="props.avatar"
|
||||
squared
|
||||
/>
|
||||
</template>
|
||||
<slot />
|
||||
<span v-if="props.hashtags.length">
|
||||
<a
|
||||
v-for="(hashtag, index) in props.hashtags"
|
||||
:key="index"
|
||||
class="px-1"
|
||||
:class="[network && `text-${network}`]"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="emit('hashtagClick', hashtag)"
|
||||
@click="emit('hashtagClick', hashtag)"
|
||||
>
|
||||
{{ hashtag }}
|
||||
</a>
|
||||
</span>
|
||||
</VBlock>
|
||||
</div>
|
||||
|
||||
<footer class="card-footer">
|
||||
<a
|
||||
v-if="props.shareLabel"
|
||||
:class="[network && `hover-bg-${network}`]"
|
||||
class="card-footer-item"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="emit('share')"
|
||||
@click="emit('share')"
|
||||
>
|
||||
{{ props.shareLabel }}
|
||||
</a>
|
||||
<a
|
||||
v-if="props.likeLabel"
|
||||
:class="[network && `hover-text-${network}`]"
|
||||
class="card-footer-item"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="emit('like')"
|
||||
@click="emit('like')"
|
||||
>
|
||||
{{ props.likeLabel }}
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
349
src/components/base/VCheckbox.vue
Normal file
349
src/components/base/VCheckbox.vue
Normal file
@@ -0,0 +1,349 @@
|
||||
<script setup lang="ts">
|
||||
export type VCheckboxColor = 'primary' | 'info' | 'success' | 'warning' | 'danger'
|
||||
|
||||
export interface VCheckboxProps {
|
||||
id?: string
|
||||
raw?: boolean
|
||||
label?: string
|
||||
color?: VCheckboxColor
|
||||
trueValue?: any
|
||||
falseValue?: any
|
||||
value?: any
|
||||
circle?: boolean
|
||||
solid?: boolean
|
||||
paddingless?: boolean
|
||||
wrapperClass?: string
|
||||
}
|
||||
|
||||
const modelValue = defineModel<any>({
|
||||
default: false,
|
||||
})
|
||||
const props = withDefaults(defineProps<VCheckboxProps>(), {
|
||||
id: undefined,
|
||||
label: undefined,
|
||||
color: undefined,
|
||||
trueValue: true,
|
||||
falseValue: false,
|
||||
value: undefined,
|
||||
circle: false,
|
||||
solid: false,
|
||||
paddingless: false,
|
||||
wrapperClass: undefined,
|
||||
})
|
||||
|
||||
const context = useVFieldContext()
|
||||
|
||||
const internal = computed({
|
||||
get() {
|
||||
if (context.field?.value) {
|
||||
return context.field.value.value
|
||||
}
|
||||
else {
|
||||
return modelValue.value
|
||||
}
|
||||
},
|
||||
set(value: any) {
|
||||
if (context.field?.value) {
|
||||
context.field.value.setValue(value)
|
||||
}
|
||||
modelValue.value = value
|
||||
},
|
||||
})
|
||||
|
||||
const classes = computed(() => {
|
||||
if (props.raw) return [props.wrapperClass]
|
||||
|
||||
return [
|
||||
'checkbox',
|
||||
props.wrapperClass,
|
||||
props.solid ? 'is-solid' : 'is-outlined',
|
||||
props.circle && 'is-circle',
|
||||
props.color && `is-${props.color}`,
|
||||
props.paddingless && 'is-paddingless',
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VLabel
|
||||
:id="props.id || context.id.value"
|
||||
raw
|
||||
:class="classes"
|
||||
>
|
||||
<input
|
||||
:id="props.id || context.id.value"
|
||||
v-model="internal"
|
||||
v-bind="$attrs"
|
||||
:true-value="props.trueValue"
|
||||
:false-value="props.falseValue"
|
||||
:value="props.value"
|
||||
type="checkbox"
|
||||
>
|
||||
<span />
|
||||
<slot v-bind="context">
|
||||
{{ props.label }}
|
||||
</slot>
|
||||
</VLabel>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
%controller {
|
||||
position: relative;
|
||||
font-family: var(--font);
|
||||
cursor: pointer;
|
||||
padding: 1em;
|
||||
|
||||
&::selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
input + span {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
background: var(--white);
|
||||
content: '';
|
||||
display: inline-block;
|
||||
margin-inline-end: 0.5rem;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
width: 1.4em;
|
||||
height: 1.4em;
|
||||
border: 1px solid color-mix(in oklab, var(--fade-grey), black 8%);
|
||||
transform: translate3d(0, 0, 0);
|
||||
backface-visibility: hidden;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
transform: scale(0);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (width >= 768px) {
|
||||
&:hover input + span {
|
||||
box-shadow: 0 2px 4px rgba(#000, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
input:active + span {
|
||||
box-shadow: 0 4px 8px rgba(#000, 0.15);
|
||||
}
|
||||
|
||||
input:checked + span::after {
|
||||
transform: translate(calc(var(--transform-direction) * -50%), -50%) scale(1) !important;
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.3s; // transition-all test
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@extend %controller;
|
||||
|
||||
color: var(--light-text);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
&.is-paddingless {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
&.is-circle {
|
||||
input + span {
|
||||
border-radius: var(--radius-rounded);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
input + span {
|
||||
background: color-mix(in oklab, var(--fade-grey), white 3%);
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
input + span {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
|
||||
&::after {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
input + span {
|
||||
border-color: var(--success);
|
||||
background: var(--success);
|
||||
|
||||
&::after {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
input + span {
|
||||
border-color: var(--info);
|
||||
background: var(--info);
|
||||
|
||||
&::after {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
input + span {
|
||||
border-color: var(--warning);
|
||||
background: var(--warning);
|
||||
|
||||
&::after {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
input + span {
|
||||
border-color: var(--danger);
|
||||
background: var(--danger);
|
||||
|
||||
&::after {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-outlined {
|
||||
&.is-primary {
|
||||
input:checked + span {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
input + span {
|
||||
&::after {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
input:checked + span {
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
input + span {
|
||||
&::after {
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
input:checked + span {
|
||||
border-color: var(--info);
|
||||
}
|
||||
|
||||
input + span {
|
||||
&::after {
|
||||
color: var(--info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
input:checked + span {
|
||||
border-color: var(--warning);
|
||||
}
|
||||
|
||||
input + span {
|
||||
&::after {
|
||||
color: var(--warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
input:checked + span {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
input + span {
|
||||
&::after {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input + span {
|
||||
border-radius: var(--radius-small);
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&::after {
|
||||
background-size: contain;
|
||||
position: absolute;
|
||||
top: 48%;
|
||||
inset-inline-start: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
content: '\f00c';
|
||||
font-family: 'Font Awesome\ 5 Free';
|
||||
font-weight: 900;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
input:focus + span,
|
||||
input:active + span {
|
||||
outline-offset: var(--accessibility-focus-outline-offset);
|
||||
outline-width: var(--accessibility-focus-outline-width);
|
||||
outline-color: var(--accessibility-focus-outline-color);
|
||||
outline-style: var(--accessibility-focus-outline-style);
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
%controller {
|
||||
input + span {
|
||||
background-color: color-mix(in oklab, var(--dark-sidebar), white 2%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 4%);
|
||||
|
||||
&::after {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
}
|
||||
|
||||
input + span {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 16%);
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
&.is-solid.is-primary {
|
||||
input + span {
|
||||
background-color: var(--primary) !important;
|
||||
border-color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-outlined.is-primary {
|
||||
input:checked + span {
|
||||
border-color: var(--primary) !important;
|
||||
|
||||
&::after {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
223
src/components/base/VCollapse.vue
Normal file
223
src/components/base/VCollapse.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<script setup lang="ts">
|
||||
export interface VCollapseItem {
|
||||
title: string
|
||||
content: string
|
||||
value?: any
|
||||
url?: string
|
||||
}
|
||||
export interface VCollapseProps {
|
||||
items: VCollapseItem[]
|
||||
itemOpen?: number
|
||||
withChevron?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VCollapseProps>(), {
|
||||
items: () => [],
|
||||
itemOpen: undefined,
|
||||
})
|
||||
|
||||
const internalItemOpen = ref<number | undefined>(props.itemOpen)
|
||||
const toggle = (key: number) => {
|
||||
if (internalItemOpen.value === key) {
|
||||
internalItemOpen.value = undefined
|
||||
return
|
||||
}
|
||||
|
||||
internalItemOpen.value = key
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<details
|
||||
v-for="(item, key) in items"
|
||||
:key="key"
|
||||
:class="[withChevron && 'has-chevron', !withChevron && 'has-plus']"
|
||||
:open="internalItemOpen === key || undefined"
|
||||
class="collapse"
|
||||
>
|
||||
<slot
|
||||
name="collapse-item"
|
||||
:item="item"
|
||||
:index="key"
|
||||
:toggle="toggle"
|
||||
>
|
||||
<summary
|
||||
class="collapse-header"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="() => toggle(key)"
|
||||
@click.prevent="() => toggle(key)"
|
||||
>
|
||||
<h3>
|
||||
<slot
|
||||
name="collapse-item-summary"
|
||||
:item="item"
|
||||
:index="key"
|
||||
:toggle="toggle"
|
||||
>
|
||||
{{ item.title }}
|
||||
</slot>
|
||||
</h3>
|
||||
<div class="collapse-head-info">
|
||||
<slot
|
||||
name="collapse-item-head"
|
||||
:item="item"
|
||||
:index="key"
|
||||
/>
|
||||
<div class="collapse-icon">
|
||||
<VIcon
|
||||
v-if="withChevron"
|
||||
icon="lucide:chevron-down"
|
||||
/>
|
||||
<VIcon
|
||||
v-else-if="!withChevron"
|
||||
icon="lucide:plus"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="collapse-content">
|
||||
<slot
|
||||
name="collapse-item-content"
|
||||
:item="item"
|
||||
:index="key"
|
||||
:toggle="toggle"
|
||||
>
|
||||
<p>
|
||||
{{ item.content }}
|
||||
</p>
|
||||
</slot>
|
||||
</div>
|
||||
</slot>
|
||||
</details>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '/@src/scss/abstracts/all';
|
||||
|
||||
.collapse {
|
||||
@include vuero-s-card;
|
||||
|
||||
padding: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
&.has-plus {
|
||||
&[open] {
|
||||
.collapse-header {
|
||||
.collapse-icon {
|
||||
transform: rotate(calc(var(--transform-direction) * 45deg));
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-content {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-chevron {
|
||||
&[open] {
|
||||
.collapse-header {
|
||||
.collapse-icon {
|
||||
transform: rotate(calc(var(--transform-direction) * 180deg));
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-content {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[open] {
|
||||
.collapse-icon {
|
||||
border-color: color-mix(in oklab, var(--fade-grey), black 3%) !important;
|
||||
box-shadow: var(--light-box-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 60px;
|
||||
padding: 0 20px;
|
||||
cursor: pointer;
|
||||
|
||||
h3 {
|
||||
font-family: var(--font-alt);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
.collapse-head-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-rounded);
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-content {
|
||||
display: none;
|
||||
padding: 0 20px 20px;
|
||||
color: var(--light-text);
|
||||
font-family: var(--font);
|
||||
|
||||
p:not(:last-child) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.collapse {
|
||||
@include vuero-card--dark;
|
||||
|
||||
&[open] {
|
||||
.collapse-header {
|
||||
.collapse-icon {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 2%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 4%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-header {
|
||||
h3 {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 6%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
117
src/components/base/VCollapseLinks.vue
Normal file
117
src/components/base/VCollapseLinks.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script setup lang="ts">
|
||||
import { withoutTrailingSlash } from 'ufo'
|
||||
|
||||
const props = defineProps<{
|
||||
links: {
|
||||
label: string
|
||||
to: string
|
||||
icon?: string
|
||||
tag?: string | number
|
||||
}[]
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const isOpen = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.links.some(link => withoutTrailingSlash(link.to) === withoutTrailingSlash(route.path))) {
|
||||
isOpen.value = true
|
||||
}
|
||||
})
|
||||
|
||||
function toggle() {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="collapse-links has-children" :class="[isOpen && 'active']">
|
||||
<div class="collapse-wrap">
|
||||
<a
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="parent-link"
|
||||
@click.prevent="() => toggle()"
|
||||
@keydown.enter.prevent="() => toggle()"
|
||||
>
|
||||
<slot />
|
||||
<VIcon
|
||||
class="rtl-hidden"
|
||||
icon="lucide:chevron-right"
|
||||
/>
|
||||
<VIcon
|
||||
class="ltr-hidden"
|
||||
icon="lucide:chevron-left"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<Transition
|
||||
name="collapse-links-transition"
|
||||
mode="out-in"
|
||||
>
|
||||
<ul v-if="isOpen" class="collapse-content">
|
||||
<li
|
||||
v-for="child of props.links"
|
||||
:key="child.to"
|
||||
>
|
||||
<VLink
|
||||
class="is-submenu"
|
||||
:to="child.to"
|
||||
>
|
||||
<VIcon
|
||||
v-if="child.icon"
|
||||
:icon="child.icon"
|
||||
/>
|
||||
<span>{{ child.label }}</span>
|
||||
<VTag
|
||||
v-if="child.tag"
|
||||
:label="child.tag"
|
||||
color="primary"
|
||||
outlined
|
||||
curved
|
||||
/>
|
||||
</VLink>
|
||||
</li>
|
||||
</ul>
|
||||
</Transition>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.collapse-links {
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.collapse-links-transition-enter-active,
|
||||
.collapse-links-transition-leave-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scaleY(1);
|
||||
transform-origin: center top;
|
||||
}
|
||||
|
||||
.collapse-links-transition-enter-active {
|
||||
transition:
|
||||
opacity 0.2s ease-in,
|
||||
transform 0.1s ease-in;
|
||||
}
|
||||
|
||||
.collapse-links-transition-leave-active {
|
||||
transition:
|
||||
opacity 0.2s ease-out,
|
||||
transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
.collapse-links-transition-enter-from,
|
||||
.collapse-links-transition-leave-to {
|
||||
transform: translateY(-10px) scaleY(0.2);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.collapse-links-transition-enter-active,
|
||||
.collapse-links-transition-leave-active {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
203
src/components/base/VCollapseModel.vue
Normal file
203
src/components/base/VCollapseModel.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
export interface VCollapseItem {
|
||||
title: string
|
||||
content: string
|
||||
}
|
||||
interface VCollapseProps {
|
||||
items: VCollapseItem[]
|
||||
withChevron?: boolean
|
||||
}
|
||||
|
||||
const modelValue = defineModel<number | undefined>({
|
||||
default: undefined,
|
||||
})
|
||||
const props = defineProps<VCollapseProps>()
|
||||
|
||||
const toggle = (key: number) => {
|
||||
if (modelValue.value === key) {
|
||||
modelValue.value = undefined
|
||||
return
|
||||
}
|
||||
|
||||
modelValue.value = key
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<details
|
||||
v-for="(item, key) in props.items"
|
||||
:key="key"
|
||||
:class="[props.withChevron && 'has-chevron', !props.withChevron && 'has-plus']"
|
||||
:open="modelValue === key || undefined"
|
||||
class="collapse"
|
||||
>
|
||||
<slot
|
||||
name="collapse-item"
|
||||
:item="item"
|
||||
:index="key"
|
||||
:toggle="toggle"
|
||||
>
|
||||
<summary
|
||||
class="collapse-header"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="() => toggle(key)"
|
||||
@click.prevent="() => toggle(key)"
|
||||
>
|
||||
<h3>
|
||||
<slot
|
||||
name="collapse-item-summary"
|
||||
:item="item"
|
||||
:index="key"
|
||||
:toggle="toggle"
|
||||
>
|
||||
{{ item.title }}
|
||||
</slot>
|
||||
</h3>
|
||||
<div class="collapse-icon">
|
||||
<VIcon
|
||||
v-if="props.withChevron"
|
||||
icon="lucide:chevron-down"
|
||||
/>
|
||||
<VIcon
|
||||
v-else-if="!props.withChevron"
|
||||
icon="lucide:plus"
|
||||
/>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="collapse-content">
|
||||
<p>
|
||||
<slot
|
||||
name="collapse-item-content"
|
||||
:item="item"
|
||||
:index="key"
|
||||
:toggle="toggle"
|
||||
>
|
||||
{{ item.content }}
|
||||
</slot>
|
||||
</p>
|
||||
</div>
|
||||
</slot>
|
||||
</details>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.collapse {
|
||||
@include vuero-s-card;
|
||||
|
||||
padding: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
&.has-plus {
|
||||
&[open] {
|
||||
.collapse-header {
|
||||
.collapse-icon {
|
||||
transform: rotate(calc(var(--transform-direction) * 45deg));
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-content {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-chevron {
|
||||
&[open] {
|
||||
.collapse-header {
|
||||
.collapse-icon {
|
||||
transform: rotate(calc(var(--transform-direction) * 180deg));
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-content {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[open] {
|
||||
.collapse-icon {
|
||||
border-color: color-mix(in oklab, var(--fade-grey), black 3%) !important;
|
||||
box-shadow: var(--light-box-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 60px;
|
||||
padding: 0 20px;
|
||||
cursor: pointer;
|
||||
|
||||
h3 {
|
||||
font-family: var(--font-alt);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-rounded);
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-content {
|
||||
display: none;
|
||||
padding: 0 20px 20px;
|
||||
color: var(--light-text);
|
||||
font-family: var(--font);
|
||||
|
||||
p:not(:last-child) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.collapse {
|
||||
@include vuero-card--dark;
|
||||
|
||||
&[open] {
|
||||
.collapse-header {
|
||||
.collapse-icon {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 2%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 4%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-header {
|
||||
h3 {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 6%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
121
src/components/base/VControl.vue
Normal file
121
src/components/base/VControl.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import VLabel from '/@src/components/base/VLabel.vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
isValid: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
hasError: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
expanded: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
fullwidth: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
textaddon: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
nogrow: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
subcontrol: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
raw: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const { field, id } = useVFieldContext({
|
||||
id: props.id,
|
||||
inherit: !props.subcontrol,
|
||||
})
|
||||
|
||||
const isValid = computed(() => props.isValid)
|
||||
const hasError = computed(() =>
|
||||
field?.value ? Boolean(field?.value?.errorMessage?.value) : props.hasError,
|
||||
)
|
||||
|
||||
const controlClasees = computed(() => {
|
||||
if (props.raw) return []
|
||||
|
||||
return [
|
||||
'control',
|
||||
props.icon && 'has-icon',
|
||||
props.loading && 'is-loading',
|
||||
props.expanded && 'is-expanded',
|
||||
props.fullwidth && 'is-fullwidth',
|
||||
props.nogrow && 'is-nogrow',
|
||||
props.textaddon && 'is-textarea-addon',
|
||||
isValid.value && 'has-validation has-success',
|
||||
hasError.value && 'has-validation has-error',
|
||||
props.subcontrol && 'subcontrol',
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="controlClasees">
|
||||
<slot v-bind="{ field, id }" />
|
||||
|
||||
<VIcon
|
||||
v-if="props.icon"
|
||||
:icon="props.icon"
|
||||
class="form-icon"
|
||||
/>
|
||||
|
||||
<VLabel
|
||||
v-if="isValid"
|
||||
class="validation-icon is-success"
|
||||
>
|
||||
<VIcon icon="lucide:check" />
|
||||
</VLabel>
|
||||
<a
|
||||
v-else-if="hasError"
|
||||
class="validation-icon is-error"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click.prevent="() => field?.resetField?.()"
|
||||
@keydown.enter.prevent="() => field?.resetField?.()"
|
||||
>
|
||||
<VIcon icon="lucide:x" />
|
||||
</a>
|
||||
|
||||
<slot
|
||||
v-bind="{ field, id }"
|
||||
name="extra"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.is-nogrow {
|
||||
flex-grow: 0 !important;
|
||||
}
|
||||
|
||||
.is-fullwidth {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
144
src/components/base/VDarkmodeSwitch.vue
Normal file
144
src/components/base/VDarkmodeSwitch.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
const { isDark, onChange } = useDarkmode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label
|
||||
class="theme-toggle"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="(e) => (e.target as HTMLLabelElement).click()"
|
||||
>
|
||||
<ClientOnly>
|
||||
<input
|
||||
:checked="isDark"
|
||||
type="checkbox"
|
||||
@click="onChange"
|
||||
>
|
||||
<span class="toggler">
|
||||
<span class="dark">
|
||||
<VIcon
|
||||
icon="lucide:moon"
|
||||
/>
|
||||
</span>
|
||||
<span class="light">
|
||||
<VIcon
|
||||
icon="lucide:sun"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</ClientOnly>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.theme-toggle {
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
transform: scale(0.9);
|
||||
|
||||
&:focus-within {
|
||||
border-radius: 50px;
|
||||
outline-offset: var(--accessibility-focus-outline-offset);
|
||||
outline-width: var(--accessibility-focus-outline-width);
|
||||
outline-style: var(--accessibility-focus-outline-style);
|
||||
outline-color: var(--accessibility-focus-outline-color);
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:checked ~ .toggler {
|
||||
border-color: var(--primary);
|
||||
|
||||
.dark,
|
||||
.light {
|
||||
transform: translateX(calc(var(--transform-direction) * 100%))
|
||||
rotate(360deg);
|
||||
}
|
||||
|
||||
.dark {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.light {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggler {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 31px;
|
||||
width: 53px;
|
||||
border: 2px solid var(--primary);
|
||||
border-radius: 100px;
|
||||
transition:
|
||||
color 0.3s,
|
||||
background-color 0.3s,
|
||||
border-color 0.3s,
|
||||
height 0.3s,
|
||||
width 0.3s;
|
||||
|
||||
.dark,
|
||||
.light {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
inset-inline-start: 2px;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
border-radius: var(--radius-rounded);
|
||||
background: black;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: translateX(calc(var(--transform-direction) * 0))
|
||||
rotate(calc(var(--transform-direction) * 0));
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.iconify {
|
||||
color: var(--white) !important;
|
||||
height: 14px !important;
|
||||
width: 14px !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.light {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dark {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
opacity: 0;
|
||||
z-index: 0;
|
||||
|
||||
.iconify {
|
||||
color: var(--white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
.theme-toggle {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: portrait) {
|
||||
.theme-toggle {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
21
src/components/base/VDarkmodeToggle.vue
Normal file
21
src/components/base/VDarkmodeToggle.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
const { isDark, onChange } = useDarkmode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label
|
||||
class="dark-mode"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="(e) => (e.target as HTMLLabelElement).click()"
|
||||
>
|
||||
<ClientOnly>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="!isDark"
|
||||
@click="onChange"
|
||||
>
|
||||
<span />
|
||||
</ClientOnly>
|
||||
</label>
|
||||
</template>
|
||||
611
src/components/base/VDropdown.vue
Normal file
611
src/components/base/VDropdown.vue
Normal file
@@ -0,0 +1,611 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownOptions } from '/@src/composables/dropdown'
|
||||
|
||||
export type VDropdownColor = 'primary' | 'info' | 'success' | 'warning' | 'danger'
|
||||
export interface VDropdownProps {
|
||||
title?: string
|
||||
color?: VDropdownColor
|
||||
icon?: string
|
||||
up?: boolean
|
||||
end?: boolean
|
||||
right?: boolean
|
||||
modern?: boolean
|
||||
spaced?: boolean
|
||||
options?: DropdownOptions
|
||||
classes?: {
|
||||
wrapper?: string | string[]
|
||||
content?: string | string[]
|
||||
}
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VDropdownProps>(), {
|
||||
title: undefined,
|
||||
color: undefined,
|
||||
icon: undefined,
|
||||
options: undefined,
|
||||
classes: undefined,
|
||||
})
|
||||
|
||||
const dropdownElement = ref<HTMLElement>()
|
||||
const dropdown = useDropdownContext(dropdownElement, props.options)
|
||||
|
||||
defineExpose({
|
||||
...dropdown,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="dropdownElement"
|
||||
:class="[
|
||||
props.right && 'is-right',
|
||||
props.up && 'is-up',
|
||||
props.end && 'is-end',
|
||||
props.icon && 'is-dots',
|
||||
props.modern && 'is-modern',
|
||||
props.spaced && 'is-spaced',
|
||||
dropdown.isOpen && 'is-active',
|
||||
...(typeof props.classes?.wrapper === 'string'
|
||||
? [props.classes?.wrapper]
|
||||
: props.classes?.wrapper ?? ''),
|
||||
]"
|
||||
class="dropdown"
|
||||
>
|
||||
<slot
|
||||
name="button"
|
||||
v-bind="dropdown"
|
||||
>
|
||||
<a
|
||||
v-if="props.icon"
|
||||
tabindex="0"
|
||||
class="is-trigger dropdown-trigger"
|
||||
aria-label="View more actions"
|
||||
@keydown.enter.prevent="dropdown.toggle"
|
||||
@click="dropdown.toggle"
|
||||
>
|
||||
<VIcon :icon="props.icon" />
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-else
|
||||
tabindex="0"
|
||||
class="is-trigger button dropdown-trigger"
|
||||
:class="[props.color && `is-${props.color}`]"
|
||||
@keydown.enter.prevent="dropdown.toggle"
|
||||
@click="dropdown.toggle"
|
||||
>
|
||||
<span v-if="props.title">{{ props.title }}</span>
|
||||
<span :class="[!props.modern && 'base-caret', props.modern && 'base-caret']">
|
||||
<VIcon
|
||||
v-if="!dropdown.isOpen"
|
||||
icon="fa6-solid:angle-down"
|
||||
/>
|
||||
<VIcon
|
||||
v-else
|
||||
icon="fa6-solid:angle-up"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</slot>
|
||||
|
||||
<div
|
||||
class="dropdown-menu"
|
||||
role="menu"
|
||||
>
|
||||
<div
|
||||
class="dropdown-content"
|
||||
:class="props.classes?.content"
|
||||
>
|
||||
<slot
|
||||
name="content"
|
||||
v-bind="dropdown"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.dropdown {
|
||||
&:not(.is-right) {
|
||||
.dropdown-menu {
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-right {
|
||||
.dropdown-menu {
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-end {
|
||||
.dropdown-menu {
|
||||
bottom: 0;
|
||||
inset-inline-start: 145%;
|
||||
transform: translateY(-100%);
|
||||
height: fit-content;
|
||||
left: 55px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dots {
|
||||
&:hover,
|
||||
&.is-active {
|
||||
.is-trigger {
|
||||
background: color-mix(in oklab, var(--fade-grey), white 2%);
|
||||
|
||||
.iconify {
|
||||
color: color-mix(in oklab, var(--light-text), black 4%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-trigger {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
border-radius: var(--radius-rounded);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
.iconify {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
font-size: 20px;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
margin-top: 6px;
|
||||
padding-bottom: 0;
|
||||
text-align: inset-inline-start;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-modern {
|
||||
&.is-active {
|
||||
.caret {
|
||||
transform: rotate(calc(var(--transform-direction) * 180deg));
|
||||
}
|
||||
}
|
||||
|
||||
.is-trigger {
|
||||
padding-inline-end: 0.75em;
|
||||
|
||||
.caret {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.3s; // transition-all test
|
||||
margin-inline-start: 6px;
|
||||
|
||||
.iconify {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-spaced {
|
||||
.dropdown-menu {
|
||||
box-shadow: 0 5px 16px rgb(0 0 0 / 5%);
|
||||
border-color: var(--fade-grey);
|
||||
padding-top: 0;
|
||||
min-width: 260px;
|
||||
|
||||
&.has-margin {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
border: 1px solid var(--fade-grey);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Item
|
||||
.dropdown-item {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
color: var(--light-text);
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&:not(.is-button):hover,
|
||||
&:not(.is-button).is-active {
|
||||
background: color-mix(in oklab, var(--fade-grey), white 3%);
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
&.no-hover {
|
||||
&:hover {
|
||||
background: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-media {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.is-active {
|
||||
.icon {
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.lnir,
|
||||
.lnil {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
|
||||
.iconify {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
transition: stroke 0.3s;
|
||||
}
|
||||
|
||||
.lnir,
|
||||
.lnil {
|
||||
font-size: 16px;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.item-img {
|
||||
display: block;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: var(--radius-large);
|
||||
|
||||
&.is-rounded {
|
||||
border-radius: var(--radius-rounded);
|
||||
}
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin-inline-start: 10px;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
line-height: 1.3;
|
||||
|
||||
&:first-child {
|
||||
font-family: var(--font-alt);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
font-family: var(--font);
|
||||
color: var(--light-text);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-trigger {
|
||||
&.button {
|
||||
font-family: var(--font);
|
||||
|
||||
&:focus {
|
||||
border-color: color-mix(in oklab, var(--fade-grey), black 4%);
|
||||
box-shadow: var(--light-box-shadow);
|
||||
}
|
||||
|
||||
.base-caret {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
||||
.iconify {
|
||||
font-size: 14px;
|
||||
margin-inline-start: 0.65rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown menu
|
||||
.dropdown-menu {
|
||||
.dropdown-item {
|
||||
text-align: start;
|
||||
color: var(--light-text);
|
||||
font-family: var(--font);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: color-mix(in oklab, var(--fade-grey), white 3%);
|
||||
|
||||
// color: var(--white);
|
||||
}
|
||||
|
||||
// Child dropdown parent
|
||||
&.has-child {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-inline-end: 1rem;
|
||||
|
||||
.iconify {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
color: var(--muted-grey);
|
||||
}
|
||||
|
||||
// Child hover dropdown
|
||||
.child-dropdown {
|
||||
position: absolute;
|
||||
inset-inline-end: -282px;
|
||||
top: 0;
|
||||
width: 280px;
|
||||
transition: all 0.3s; // transition-all test
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
pointer-events: none;
|
||||
|
||||
// Inner
|
||||
.inner {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: var(--white);
|
||||
border: 1px solid var(--primary-grey);
|
||||
border-radius: var(--radius-large);
|
||||
padding: 8px 0;
|
||||
|
||||
// Kanban columns settings
|
||||
.column-setting {
|
||||
padding: 0 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
label {
|
||||
transform: scale(0.7);
|
||||
}
|
||||
|
||||
.text {
|
||||
span {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
|
||||
&:first-child {
|
||||
color: var(--dark-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
color: var(--muted-grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hover state
|
||||
&:hover {
|
||||
.child-dropdown {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
2. Dropdown Dark mode
|
||||
========================================================================== */
|
||||
|
||||
.is-dark {
|
||||
.toolbar-link {
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 2%) !important;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
&.is-spaced,
|
||||
&.is-dots {
|
||||
&:hover,
|
||||
&.is-active {
|
||||
.is-trigger {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 2%) !important;
|
||||
|
||||
.iconify {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
.dropdown-content {
|
||||
background: var(--dark-sidebar) !important;
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 8%) !important;
|
||||
|
||||
.heading {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 8%) !important;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
*:hover {
|
||||
background: var(--dark-sidebar) !important;
|
||||
}
|
||||
|
||||
.heading-right {
|
||||
.notification-link {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
li {
|
||||
.notification-item {
|
||||
&:hover,
|
||||
*:hover {
|
||||
background: var(--dark-sidebar) !important;
|
||||
}
|
||||
|
||||
.user-content {
|
||||
.user-info {
|
||||
color: var(--dark-dark-text) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-media {
|
||||
&:hover {
|
||||
.icon {
|
||||
.iconify {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.lnir,
|
||||
.lnil {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
.icon {
|
||||
.iconify {
|
||||
color: var(--white) !important;
|
||||
}
|
||||
|
||||
.lnir,
|
||||
.lnil {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.meta span {
|
||||
color: var(--white) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
.iconify {
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
.lnir,
|
||||
.lnil {
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
|
||||
.meta {
|
||||
span {
|
||||
&:first-child {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
.dropdown-content {
|
||||
background: var(--dark-sidebar);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 8%) !important;
|
||||
|
||||
.dropdown-item {
|
||||
color: var(--light-text);
|
||||
|
||||
&.is-active {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 2%) !important;
|
||||
|
||||
// color: var(--white) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 5%);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 5%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.child-dropdown {
|
||||
.inner {
|
||||
background: var(--dark-sidebar) !important;
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 4%) !important;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: var(--dark-sidebar) !important;
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 4%) !important;
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
.text {
|
||||
span {
|
||||
&:first-child {
|
||||
color: var(--dark-dark-text) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
83
src/components/base/VField.vue
Normal file
83
src/components/base/VField.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
export type VFieldProps = {
|
||||
id?: string
|
||||
label?: string
|
||||
addons?: boolean
|
||||
textaddon?: boolean
|
||||
grouped?: boolean
|
||||
multiline?: boolean
|
||||
horizontal?: boolean
|
||||
subcontrol?: boolean
|
||||
raw?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VFieldProps>(), {
|
||||
id: undefined,
|
||||
label: undefined,
|
||||
})
|
||||
const { field, id } = useVFieldContext({ id: props.id, inherit: !props.subcontrol })
|
||||
|
||||
const slots = useSlots()
|
||||
const hasLabel = computed(() => Boolean(slots?.label?.() || props.label))
|
||||
const classes = computed(() => {
|
||||
if (props.raw) return []
|
||||
|
||||
return [
|
||||
'field',
|
||||
props.addons && 'has-addons',
|
||||
props.textaddon && 'has-textarea-addon',
|
||||
props.grouped && 'is-grouped',
|
||||
props.grouped && props.multiline && 'is-grouped-multiline',
|
||||
props.horizontal && 'is-horizontal',
|
||||
]
|
||||
})
|
||||
|
||||
defineExpose({ field, id })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<template v-if="props.addons">
|
||||
<div
|
||||
v-if="hasLabel"
|
||||
class="field-addon-label is-normal"
|
||||
>
|
||||
<slot
|
||||
v-bind="{ field, id }"
|
||||
name="label"
|
||||
>
|
||||
<VLabel>{{ props.label }}</VLabel>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="field-addon-body">
|
||||
<slot v-bind="{ field, id }" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="hasLabel && props.horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<slot
|
||||
v-bind="{ field, id }"
|
||||
name="label"
|
||||
>
|
||||
<VLabel>{{ props.label }}</VLabel>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<slot v-bind="{ field, id }" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="hasLabel">
|
||||
<slot
|
||||
v-bind="{ field, id }"
|
||||
name="label"
|
||||
>
|
||||
<VLabel>{{ props.label }}</VLabel>
|
||||
</slot>
|
||||
|
||||
<slot v-bind="{ field, id }" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot v-bind="{ field, id }" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
80
src/components/base/VFlex.vue
Normal file
80
src/components/base/VFlex.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
export type VFlexDirection = 'row' | 'row-reverse' | 'column' | 'column-reverse'
|
||||
export type VFlexWrap = 'nowrap' | 'wrap' | 'wrap-reverse'
|
||||
export type VFlexJustifyContent =
|
||||
| 'flex-start'
|
||||
| 'flex-end'
|
||||
| 'start'
|
||||
| 'end'
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'center'
|
||||
| 'space-between'
|
||||
| 'space-around'
|
||||
| 'space-evenly'
|
||||
| 'normal'
|
||||
export type VFlexAlignItems =
|
||||
| 'flex-start'
|
||||
| 'flex-end'
|
||||
| 'start'
|
||||
| 'end'
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'center'
|
||||
| 'baseline'
|
||||
| 'stretch'
|
||||
| 'normal'
|
||||
export type VFlexAlignContent =
|
||||
| 'flex-start'
|
||||
| 'flex-end'
|
||||
| 'start'
|
||||
| 'end'
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'center'
|
||||
| 'space-between'
|
||||
| 'space-around'
|
||||
| 'space-evenly'
|
||||
| 'normal'
|
||||
|
||||
export interface VFlexProps {
|
||||
inline?: boolean
|
||||
flexDirection?: VFlexDirection
|
||||
flexWrap?: VFlexWrap
|
||||
justifyContent?: VFlexJustifyContent
|
||||
alignItems?: VFlexAlignItems
|
||||
alignContent?: VFlexAlignContent
|
||||
rowGap?: string
|
||||
columnGap?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VFlexProps>(), {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'nowrap',
|
||||
justifyContent: 'normal',
|
||||
alignItems: 'normal',
|
||||
alignContent: 'normal',
|
||||
rowGap: 'normal',
|
||||
columnGap: 'normal',
|
||||
})
|
||||
const display = computed(() => (props.inline ? 'inline-flex' : 'flex'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="v-flex">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.v-flex {
|
||||
display: v-bind(display);
|
||||
flex-direction: v-bind('props.flexDirection');
|
||||
flex-wrap: v-bind('props.flexWrap');
|
||||
justify-content: v-bind('props.justifyContent');
|
||||
align-items: v-bind('props.alignItems');
|
||||
align-content: v-bind('props.alignContent');
|
||||
row-gap: v-bind('props.rowGap');
|
||||
column-gap: v-bind('props.columnGap');
|
||||
}
|
||||
</style>
|
||||
41
src/components/base/VFlexItem.vue
Normal file
41
src/components/base/VFlexItem.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
export type VFlexItemAlignSelf =
|
||||
| 'auto'
|
||||
| 'flex-start'
|
||||
| 'flex-end'
|
||||
| 'center'
|
||||
| 'baseline'
|
||||
| 'stretch'
|
||||
|
||||
export interface VFlexItemProps {
|
||||
order?: string | number
|
||||
flexGrow?: string | number
|
||||
flexShrink?: number
|
||||
flexBasis?: string | 'auto'
|
||||
alignSelf?: VFlexItemAlignSelf
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VFlexItemProps>(), {
|
||||
order: 0,
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
flexBasis: 'auto',
|
||||
alignSelf: 'auto',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="v-flex-item">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.v-flex-item {
|
||||
order: v-bind('props.order');
|
||||
flex-grow: v-bind('props.flexGrow');
|
||||
flex-shrink: v-bind('props.flexShrink');
|
||||
flex-basis: v-bind('props.flexBasis');
|
||||
align-self: v-bind('props.alignSelf');
|
||||
}
|
||||
</style>
|
||||
185
src/components/base/VFlexPagination.vue
Normal file
185
src/components/base/VFlexPagination.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import type { RouteLocationOptions } from 'vue-router/auto'
|
||||
|
||||
export interface VFlexPaginationProps {
|
||||
itemPerPage: number
|
||||
totalItems: number
|
||||
currentPage?: number
|
||||
maxLinksDisplayed?: number
|
||||
noRouter?: boolean
|
||||
routerQueryKey?: string
|
||||
}
|
||||
|
||||
export interface VFlexPaginationEmits {
|
||||
(e: 'update:currentPage', currentPage: number): void
|
||||
}
|
||||
|
||||
const emits = defineEmits<VFlexPaginationEmits>()
|
||||
const props = withDefaults(defineProps<VFlexPaginationProps>(), {
|
||||
currentPage: 1,
|
||||
maxLinksDisplayed: 4,
|
||||
useRouter: true,
|
||||
routerQueryKey: 'page',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const lastPage = computed(() => Math.ceil(props.totalItems / props.itemPerPage) || 1)
|
||||
const totalPageDisplayed = computed(() =>
|
||||
lastPage.value > props.maxLinksDisplayed + 2
|
||||
? props.maxLinksDisplayed + 2
|
||||
: lastPage.value,
|
||||
)
|
||||
const pages = computed(() => {
|
||||
const _pages = []
|
||||
let firstButton = props.currentPage - Math.floor(totalPageDisplayed.value / 2)
|
||||
let lastButton
|
||||
= firstButton + (totalPageDisplayed.value - Math.ceil(totalPageDisplayed.value % 2))
|
||||
|
||||
if (firstButton < 1) {
|
||||
firstButton = 1
|
||||
lastButton = firstButton + (totalPageDisplayed.value - 1)
|
||||
}
|
||||
|
||||
if (lastButton > lastPage.value) {
|
||||
lastButton = lastPage.value
|
||||
firstButton = lastButton - (totalPageDisplayed.value - 1)
|
||||
}
|
||||
|
||||
for (let page = firstButton; page <= lastButton; page += 1) {
|
||||
if (page === firstButton || page === lastButton) {
|
||||
continue
|
||||
}
|
||||
|
||||
_pages.push(page)
|
||||
}
|
||||
|
||||
return _pages
|
||||
})
|
||||
|
||||
const showLastLink = computed(() => lastPage.value > 1)
|
||||
|
||||
const paginatedLink = (page = 1) => {
|
||||
if (props.noRouter) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const _page = Math.max(1, Math.min(page, lastPage.value))
|
||||
const query: any = {
|
||||
...route.query,
|
||||
}
|
||||
|
||||
if (props.routerQueryKey) {
|
||||
query[props.routerQueryKey] = _page <= 1 ? undefined : _page
|
||||
}
|
||||
|
||||
return {
|
||||
name: route.name,
|
||||
params: route.params,
|
||||
query,
|
||||
} as RouteLocationOptions
|
||||
}
|
||||
const handleLinkClick = (e: MouseEvent, page = 1) => {
|
||||
const _page = Math.max(1, Math.min(page, lastPage.value))
|
||||
emits('update:currentPage', _page)
|
||||
|
||||
if (props.noRouter) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VFlex
|
||||
role="navigation"
|
||||
class="flex-pagination pagination is-rounded"
|
||||
aria-label="pagination"
|
||||
justify-content="space-between"
|
||||
>
|
||||
<ul class="pagination-list">
|
||||
<slot name="before-pagination" />
|
||||
<li>
|
||||
<RouterLink
|
||||
:to="paginatedLink(1)"
|
||||
tabindex="0"
|
||||
class="pagination-link"
|
||||
:class="[currentPage === 1 && 'is-current']"
|
||||
@keydown.enter.prevent="
|
||||
(e: MouseEvent) => (e.target as HTMLAnchorElement).click()
|
||||
"
|
||||
@click="(e: MouseEvent) => handleLinkClick(e, 1)"
|
||||
>
|
||||
1
|
||||
</RouterLink>
|
||||
</li>
|
||||
|
||||
<li v-if="showLastLink && (pages.length === 0 || pages[0] > 2)">
|
||||
<span class="pagination-ellipsis">…</span>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-for="page in pages"
|
||||
:key="page"
|
||||
>
|
||||
<RouterLink
|
||||
:to="paginatedLink(page)"
|
||||
tabindex="0"
|
||||
class="pagination-link"
|
||||
:aria-current="currentPage === page ? 'page' : undefined"
|
||||
:class="[currentPage === page && 'is-current']"
|
||||
@keydown.enter.prevent="
|
||||
(e: MouseEvent) => (e.target as HTMLAnchorElement).click()
|
||||
"
|
||||
@click="(e: MouseEvent) => handleLinkClick(e, page)"
|
||||
>
|
||||
{{ page }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
|
||||
<li v-if="showLastLink && pages[pages.length - 1] < lastPage - 1">
|
||||
<span class="pagination-ellipsis">…</span>
|
||||
</li>
|
||||
|
||||
<li v-if="showLastLink">
|
||||
<RouterLink
|
||||
:to="paginatedLink(lastPage)"
|
||||
tabindex="0"
|
||||
class="pagination-link"
|
||||
:class="[currentPage === lastPage && 'is-current']"
|
||||
@keydown.enter.prevent="
|
||||
(e: MouseEvent) => (e.target as HTMLAnchorElement).click()
|
||||
"
|
||||
@click="(e: MouseEvent) => handleLinkClick(e, lastPage)"
|
||||
>
|
||||
{{ lastPage }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
<slot name="after-pagination" />
|
||||
</ul>
|
||||
|
||||
<slot name="before-navigation" />
|
||||
<RouterLink
|
||||
:to="paginatedLink(currentPage - 1)"
|
||||
tabindex="0"
|
||||
class="pagination-previous has-chevron"
|
||||
@keydown.enter.prevent="(e: MouseEvent) => (e.target as HTMLAnchorElement).click()"
|
||||
@click="(e: MouseEvent) => handleLinkClick(e, currentPage - 1)"
|
||||
>
|
||||
<VIcon icon="lucide:chevron-left" class="rtl-hidden" />
|
||||
<VIcon icon="lucide:chevron-right" class="ltr-hidden" />
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
:to="paginatedLink(currentPage + 1)"
|
||||
tabindex="0"
|
||||
class="pagination-next has-chevron"
|
||||
@keydown.enter.prevent="(e: MouseEvent) => (e.target as HTMLAnchorElement).click()"
|
||||
@click="(e: MouseEvent) => handleLinkClick(e, currentPage + 1)"
|
||||
>
|
||||
<VIcon icon="lucide:chevron-left" class="ltr-hidden" />
|
||||
<VIcon icon="lucide:chevron-right" class="rtl-hidden" />
|
||||
</RouterLink>
|
||||
<slot name="after-navigation" />
|
||||
</VFlex>
|
||||
</template>
|
||||
535
src/components/base/VFlexTable.vue
Normal file
535
src/components/base/VFlexTable.vue
Normal file
@@ -0,0 +1,535 @@
|
||||
<script setup lang="ts">
|
||||
import { type VNode } from 'vue'
|
||||
import { flewTableWrapperSymbol } from './VFlexTableWrapper.vue'
|
||||
|
||||
export interface VFlexTableColumn {
|
||||
key: string
|
||||
label: string
|
||||
format: (value: any, row: any, index: number) => any
|
||||
renderHeader?: () => VNode
|
||||
renderRow?: (row: any, column: VFlexTableColumn, index: number) => VNode
|
||||
align?: 'start' | 'center' | 'end'
|
||||
bold?: boolean
|
||||
inverted?: boolean
|
||||
scrollX?: boolean
|
||||
scrollY?: boolean
|
||||
grow?: boolean | 'lg' | 'xl'
|
||||
media?: boolean
|
||||
cellClass?: string
|
||||
}
|
||||
|
||||
export interface VFlexTableProps {
|
||||
data?: any[]
|
||||
columns?: Record<string, string | Partial<VFlexTableColumn>>
|
||||
printObjects?: boolean
|
||||
reactive?: boolean
|
||||
compact?: boolean
|
||||
rounded?: boolean
|
||||
separators?: boolean
|
||||
clickable?: boolean
|
||||
subtable?: boolean
|
||||
noHeader?: boolean
|
||||
}
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'rowClick', row: any, index: number): void
|
||||
}>()
|
||||
const props = withDefaults(defineProps<VFlexTableProps>(), {
|
||||
columns: undefined,
|
||||
data: () => [],
|
||||
})
|
||||
|
||||
const wrapper = inject(flewTableWrapperSymbol, null)
|
||||
|
||||
const data = computed(() => {
|
||||
if (wrapper?.data) return wrapper.data
|
||||
|
||||
if (props.reactive) {
|
||||
if (isReactive(props.data)) {
|
||||
return props.data
|
||||
}
|
||||
else {
|
||||
return reactive(props.data)
|
||||
}
|
||||
}
|
||||
|
||||
return toRaw(props.data)
|
||||
})
|
||||
|
||||
const defaultFormatter = (value: any) => value
|
||||
const columns = computed(() => {
|
||||
const columnsSrc = wrapper?.columns ?? props.columns
|
||||
let columns: VFlexTableColumn[] = []
|
||||
|
||||
if (columnsSrc) {
|
||||
for (const [key, label] of Object.entries(columnsSrc)) {
|
||||
if (typeof label === 'string') {
|
||||
columns.push({
|
||||
format: defaultFormatter,
|
||||
label,
|
||||
key,
|
||||
})
|
||||
}
|
||||
else {
|
||||
columns.push({
|
||||
format: defaultFormatter,
|
||||
label: key,
|
||||
key,
|
||||
...(label as any),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (data.value.length > 0) {
|
||||
for (const [key] of Object.entries(data.value[0])) {
|
||||
columns.push({
|
||||
format: defaultFormatter,
|
||||
label: key,
|
||||
key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return columns
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex-table"
|
||||
:class="[
|
||||
props.compact && 'is-compact',
|
||||
props.rounded && 'is-rounded',
|
||||
props.separators && 'with-separators',
|
||||
props.noHeader && 'no-header',
|
||||
props.clickable && 'is-table-clickable',
|
||||
props.subtable && 'sub-table',
|
||||
]"
|
||||
>
|
||||
<slot name="header">
|
||||
<div
|
||||
v-if="!props.noHeader"
|
||||
class="flex-table-header"
|
||||
>
|
||||
<template
|
||||
v-for="column in columns"
|
||||
:key="'col' + column.key"
|
||||
>
|
||||
<slot
|
||||
name="header-column"
|
||||
:column="column"
|
||||
>
|
||||
<component
|
||||
:is="{ render: column.renderHeader } as any"
|
||||
v-if="column.renderHeader"
|
||||
:class="[
|
||||
column.grow === true && 'is-grow',
|
||||
column.grow === 'lg' && 'is-grow-lg',
|
||||
column.grow === 'xl' && 'is-grow-xl',
|
||||
column.align === 'end' && 'cell-end',
|
||||
column.align === 'center' && 'cell-center',
|
||||
]"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
:class="[
|
||||
column.grow === true && 'is-grow',
|
||||
column.grow === 'lg' && 'is-grow-lg',
|
||||
column.grow === 'xl' && 'is-grow-xl',
|
||||
column.align === 'end' && 'cell-end',
|
||||
column.align === 'center' && 'cell-center',
|
||||
]"
|
||||
>{{ column.label }}</span>
|
||||
</slot>
|
||||
</template>
|
||||
</div>
|
||||
</slot>
|
||||
<slot name="body">
|
||||
<template
|
||||
v-for="(row, index) in data"
|
||||
:key="index"
|
||||
>
|
||||
<slot
|
||||
name="body-row-pre"
|
||||
:row="row"
|
||||
:columns="columns"
|
||||
:index="index"
|
||||
/>
|
||||
<!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -->
|
||||
<div
|
||||
class="flex-table-item"
|
||||
:class="[props.clickable && 'is-clickable']"
|
||||
:tabindex="props.clickable ? 0 : undefined"
|
||||
:role="props.clickable ? 'button' : undefined"
|
||||
@keydown.enter.prevent="
|
||||
() => {
|
||||
props.clickable && emits('rowClick', row, index)
|
||||
}
|
||||
"
|
||||
@click="
|
||||
() => {
|
||||
props.clickable && emits('rowClick', row, index)
|
||||
}
|
||||
"
|
||||
>
|
||||
<slot
|
||||
name="body-row"
|
||||
:row="row"
|
||||
:columns="columns"
|
||||
:index="index"
|
||||
>
|
||||
<template
|
||||
v-for="column in columns"
|
||||
:key="'row' + column.key"
|
||||
>
|
||||
<VFlexTableCell :column="column">
|
||||
<slot
|
||||
name="body-cell"
|
||||
:row="row"
|
||||
:column="column"
|
||||
:index="index"
|
||||
:value="column.format(row[column.key], row, index)"
|
||||
>
|
||||
<component
|
||||
:is="
|
||||
{
|
||||
render: () => column.renderRow?.(row, column, index),
|
||||
} as any
|
||||
"
|
||||
v-if="column.renderRow"
|
||||
/>
|
||||
<span
|
||||
v-else-if="
|
||||
typeof column.format(row[column.key], row, index) === 'object'
|
||||
"
|
||||
:class="[
|
||||
column.cellClass,
|
||||
column.inverted && 'dark-inverted',
|
||||
!column.inverted && (column.bold ? 'dark-text' : 'light-text'),
|
||||
]"
|
||||
>
|
||||
<details v-if="printObjects">
|
||||
<div class="language-json py-4">
|
||||
<pre><code>{{ column.format(row[column.key], row, index) }}</code></pre>
|
||||
</div>
|
||||
</details>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
:class="[
|
||||
column.cellClass,
|
||||
column.inverted && 'dark-inverted',
|
||||
!column.inverted && (column.bold ? 'dark-text' : 'light-text'),
|
||||
]"
|
||||
>
|
||||
{{ column.format(row[column.key], row, index) }}
|
||||
</span>
|
||||
</slot>
|
||||
</VFlexTableCell>
|
||||
</template>
|
||||
</slot>
|
||||
</div>
|
||||
<slot
|
||||
name="body-row-post"
|
||||
:row="row"
|
||||
:columns="columns"
|
||||
:index="index"
|
||||
/>
|
||||
</template>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.flex-table {
|
||||
.flex-table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
|
||||
> span,
|
||||
.text {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted-grey);
|
||||
text-transform: uppercase;
|
||||
padding: 0 10px 10px;
|
||||
|
||||
&.is-checkbox {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
max-width: 30px;
|
||||
|
||||
.checkbox {
|
||||
padding: 0;
|
||||
|
||||
> span {
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.cell-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.cell-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&.is-grow {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
&.is-grow-lg {
|
||||
flex-grow: 3;
|
||||
}
|
||||
|
||||
&.is-grow-xl {
|
||||
flex-grow: 6;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--muted-grey);
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 0;
|
||||
|
||||
> span {
|
||||
min-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flex-table-item {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
background: var(--white);
|
||||
border: 1px solid color-mix(in oklab, var(--fade-grey), black 3%);
|
||||
padding: 8px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&.is-row {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.sub-table {
|
||||
.flex-table-item {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
min-height: 40px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
.table-label {
|
||||
font-family: var(--font);
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8rem;
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
.table-total {
|
||||
font-family: var(--font);
|
||||
color: var(--dark-text);
|
||||
font-weight: 500;
|
||||
|
||||
&.is-bigger {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-compact {
|
||||
.flex-table-item {
|
||||
margin-bottom: 0;
|
||||
border-radius: 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-rounded {
|
||||
&:not(.no-header) {
|
||||
.flex-table-item {
|
||||
&:nth-of-type(2) {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 6px;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.no-header {
|
||||
.flex-table-item {
|
||||
&:first-child {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 6px;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.is-compact) {
|
||||
&.is-rounded {
|
||||
.flex-table-item {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-table-clickable {
|
||||
.flex-table-item {
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
background: var(--widget-grey) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.with-separators {
|
||||
.flex-table-item {
|
||||
.flex-table-cell {
|
||||
&:not(:first-of-type) {
|
||||
border-inline-start: dashed 1px color-mix(in oklab, var(--fade-grey), black 3%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
2. Flex Table Dark mode
|
||||
========================================================================== */
|
||||
|
||||
.is-dark {
|
||||
.flex-table {
|
||||
&:not(.sub-table) {
|
||||
.flex-table-item {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 6%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
}
|
||||
}
|
||||
|
||||
&.with-separators {
|
||||
.flex-table-item {
|
||||
.flex-table-cell {
|
||||
&:not(:first-of-type) {
|
||||
border-inline-start: dashed 1px color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-table-clickable {
|
||||
.flex-table-item {
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 12%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
3. Media Queries
|
||||
========================================================================== */
|
||||
|
||||
@media (width <= 767px) {
|
||||
.flex-table {
|
||||
.flex-table-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flex-table-item {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100% !important;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
> div {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.sub-table) {
|
||||
.flex-table-item {
|
||||
.flex-table-cell {
|
||||
> span,
|
||||
> small,
|
||||
> strong,
|
||||
> p,
|
||||
> div,
|
||||
> .is-pushed-mobile,
|
||||
> .text {
|
||||
margin-inline-start: auto;
|
||||
|
||||
&.no-push {
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
.flex-table-cell {
|
||||
&[data-th] {
|
||||
&::before {
|
||||
content: attr(data-th);
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
color: var(--muted-grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 767px) {
|
||||
.flex-table {
|
||||
&.sub-table {
|
||||
padding-top: 16px;
|
||||
|
||||
.is-vhidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.flex-table-item:not(.is-vhidden) {
|
||||
flex-direction: revert !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
427
src/components/base/VFlexTableCell.vue
Normal file
427
src/components/base/VFlexTableCell.vue
Normal file
@@ -0,0 +1,427 @@
|
||||
<script setup lang="ts">
|
||||
import type { VFlexTableColumn } from './VFlexTable.vue'
|
||||
|
||||
export interface VFlexTableCellProps {
|
||||
column?: Partial<VFlexTableColumn>
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VFlexTableCellProps>(), {
|
||||
column: () => ({}),
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex-table-cell is-relative"
|
||||
:class="[
|
||||
props.column.bold && 'is-bold',
|
||||
props.column.media && 'is-media',
|
||||
props.column.grow === true && 'is-grow',
|
||||
props.column.grow === 'lg' && 'is-grow-lg',
|
||||
props.column.grow === 'xl' && 'is-grow-xl',
|
||||
props.column.scrollX && !props.column.scrollY && 'has-slimscroll-x',
|
||||
!props.column.scrollX && props.column.scrollY && 'has-slimscroll',
|
||||
props.column.scrollX && props.column.scrollY && 'has-slimscroll-all',
|
||||
props.column.align === 'end' && 'cell-end',
|
||||
props.column.align === 'center' && 'cell-center',
|
||||
props.column.cellClass,
|
||||
]"
|
||||
:data-th="props.column.label || undefined"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.flex-table-cell {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
font-family: var(--font);
|
||||
word-break: keep-all;
|
||||
white-space: nowrap;
|
||||
text-align: inset-inline-start;
|
||||
|
||||
&.is-scrollable-x {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
&.is-scrollable-y {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&.is-grow {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
&.cell-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.cell-end {
|
||||
justify-content: flex-end;
|
||||
|
||||
.button {
|
||||
&.has-dot {
|
||||
.dot {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
font-size: 4px;
|
||||
margin: 0 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-link {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-bold {
|
||||
> span {
|
||||
font-family: var(--font-alt);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-checkbox {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
max-width: 30px;
|
||||
|
||||
.checkbox {
|
||||
padding: 0;
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-grow {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
&.is-grow-lg {
|
||||
flex-grow: 3;
|
||||
}
|
||||
|
||||
&.is-grow-xl {
|
||||
flex-grow: 6;
|
||||
}
|
||||
|
||||
&.is-user,
|
||||
&.is-media {
|
||||
padding-inline-start: 0;
|
||||
|
||||
> div span:not(.avatar) {
|
||||
display: block;
|
||||
margin-inline-start: 10px;
|
||||
}
|
||||
|
||||
> div {
|
||||
line-height: 1.2;
|
||||
|
||||
.item-name {
|
||||
font-family: var(--font-alt);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
color: var(--light-text);
|
||||
|
||||
.iconify {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
stroke-width: 1.6px;
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
|
||||
span,
|
||||
.text {
|
||||
display: inline-block;
|
||||
margin-inline-start: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.flex-media {
|
||||
margin-inline-start: 10px;
|
||||
margin-top: 4px;
|
||||
|
||||
.v-avatar {
|
||||
width: 26px !important;
|
||||
min-width: 26px !important;
|
||||
height: 26px !important;
|
||||
|
||||
.avatar {
|
||||
width: 26px !important;
|
||||
min-width: 26px !important;
|
||||
height: 26px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-avatar {
|
||||
margin-inline-start: 0 !important;
|
||||
|
||||
.avatar.is-fake {
|
||||
span,
|
||||
.text {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
+ div {
|
||||
margin-inline-start: 0.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.media {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 130px;
|
||||
min-height: 95px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.cell-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 80px;
|
||||
|
||||
&.is-mini {
|
||||
max-width: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-icon {
|
||||
margin-inline-end: 4px;
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin-bottom: 0 !important;
|
||||
line-height: 1.8;
|
||||
height: 1.8em;
|
||||
}
|
||||
|
||||
.flex-media {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.meta {
|
||||
margin-inline-start: 6px;
|
||||
line-height: 1.3;
|
||||
|
||||
span,
|
||||
.text {
|
||||
display: block !important;
|
||||
font-size: 0.8rem;
|
||||
color: var(--light-text);
|
||||
font-family: var(--font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dot-levels {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.dot {
|
||||
font-size: 8px;
|
||||
color: color-mix(in oklab, var(--light-text), white 6%);
|
||||
margin: 0 6px;
|
||||
|
||||
&.active {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-icon-link {
|
||||
color: var(--light-text);
|
||||
|
||||
.iconify {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
color: var(--primary);
|
||||
|
||||
.iconify {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.flex-table-cell {
|
||||
&.is-user,
|
||||
&.is-media {
|
||||
.v-avatar {
|
||||
.badge {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 6%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.cell-end {
|
||||
.button {
|
||||
&.dark-outlined {
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
border-color: var(--primary) !important;
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark-text {
|
||||
color: var(--dark-dark-text) !important;
|
||||
}
|
||||
|
||||
.avatar-stack {
|
||||
.v-avatar {
|
||||
.avatar {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 6%) !important;
|
||||
}
|
||||
|
||||
.is-more {
|
||||
.inner {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 6%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dot-levels {
|
||||
.dot {
|
||||
&.active {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
.flex-table-cell {
|
||||
position: relative;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&.no-label-mobile {
|
||||
&::before {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.cell-end {
|
||||
justify-content: flex-start !important;
|
||||
|
||||
.btn-group {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-user,
|
||||
&.is-media {
|
||||
padding-inline-start: 10px;
|
||||
|
||||
span,
|
||||
.text {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.media {
|
||||
max-width: 80px;
|
||||
min-height: 80px;
|
||||
|
||||
+ div {
|
||||
margin-inline-start: 10px !important;
|
||||
|
||||
.item-name {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 205px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: portrait) {
|
||||
.flex-table-cell {
|
||||
&.is-user {
|
||||
img {
|
||||
min-width: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-media {
|
||||
.media {
|
||||
max-width: 60px;
|
||||
min-height: 60px;
|
||||
|
||||
+ div {
|
||||
margin-inline-start: 10px !important;
|
||||
|
||||
.item-name {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: landscape) {
|
||||
.flex-table-cell {
|
||||
&.is-media {
|
||||
.media {
|
||||
max-width: 60px;
|
||||
min-height: 60px;
|
||||
|
||||
+ div {
|
||||
margin-inline-start: 10px !important;
|
||||
|
||||
.item-name {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
133
src/components/base/VFlexTableSortColumn.vue
Normal file
133
src/components/base/VFlexTableSortColumn.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<script lang="ts">
|
||||
import type { SlotsType } from 'vue'
|
||||
import type {
|
||||
RouteLocationOptions,
|
||||
LocationQueryValue,
|
||||
} from 'vue-router/auto'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
noRouter: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
routerQueryKey: {
|
||||
type: String,
|
||||
default: 'sort',
|
||||
},
|
||||
},
|
||||
slots: Object as SlotsType<{
|
||||
default: {
|
||||
isDesc: boolean
|
||||
isAsc: boolean
|
||||
nextSort?: string
|
||||
value: string | LocationQueryValue[]
|
||||
}
|
||||
}>,
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, context) {
|
||||
const route = useRoute()
|
||||
const rawSort = computed(
|
||||
() => props.modelValue ?? route.query[props.routerQueryKey] ?? '',
|
||||
)
|
||||
|
||||
const isAsc = computed(() => rawSort.value === `${props.id}:asc`)
|
||||
const isDesc = computed(() => rawSort.value === `${props.id}:desc`)
|
||||
|
||||
const nextSort = computed(() => {
|
||||
return isAsc.value
|
||||
? `${props.id}:desc`
|
||||
: isDesc.value
|
||||
? undefined
|
||||
: `${props.id}:asc`
|
||||
})
|
||||
|
||||
const sortedLink = computed(() => {
|
||||
if (props.noRouter) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const query: any = {
|
||||
...route.query,
|
||||
}
|
||||
|
||||
if (props.routerQueryKey) {
|
||||
query[props.routerQueryKey] = nextSort.value
|
||||
}
|
||||
|
||||
return {
|
||||
name: route.name,
|
||||
params: route.params,
|
||||
query: query,
|
||||
} as RouteLocationOptions
|
||||
})
|
||||
|
||||
const handleLinkClick = (e: MouseEvent) => {
|
||||
context.emit('update:modelValue', nextSort.value)
|
||||
|
||||
if (props.noRouter) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
const slotContent = context.slots?.default?.({
|
||||
isDesc: isDesc.value,
|
||||
isAsc: isAsc.value,
|
||||
nextSort: nextSort.value,
|
||||
value: rawSort.value,
|
||||
})
|
||||
|
||||
const link = h(
|
||||
RouterLink,
|
||||
{
|
||||
to: sortedLink.value,
|
||||
onClick: handleLinkClick,
|
||||
onKeydown(e: KeyboardEvent) {
|
||||
if (e.code === 'Space') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if (e.target instanceof HTMLAnchorElement) {
|
||||
e.target.dispatchEvent(new MouseEvent('click'))
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
default() {
|
||||
const icon = h('iconify-icon', {
|
||||
class: 'ml-3 iconify',
|
||||
icon: isAsc.value
|
||||
? 'fa6-solid:sort-up'
|
||||
: isDesc.value
|
||||
? 'fa6-solid:sort-down'
|
||||
: 'fa6-solid:sort',
|
||||
})
|
||||
|
||||
return [slotContent ?? props.label, icon]
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return h('span', {}, link)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
33
src/components/base/VFlexTableToolbar.vue
Normal file
33
src/components/base/VFlexTableToolbar.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="flex-table-toolbar">
|
||||
<div class="left">
|
||||
<slot name="left" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot name="right" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.flex-table-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
.flex-table-toolbar {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.left {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
469
src/components/base/VFlexTableWrapper.vue
Normal file
469
src/components/base/VFlexTableWrapper.vue
Normal file
@@ -0,0 +1,469 @@
|
||||
<script lang="ts">
|
||||
import type { SlotsType, InjectionKey, PropType } from 'vue'
|
||||
import type { VFlexTableColumn } from './VFlexTable.vue'
|
||||
import VFlexTableSortColumn from './VFlexTableSortColumn.vue'
|
||||
|
||||
export type VFlexTableWrapperDataResolver<T = any> = (parameters: {
|
||||
searchTerm: string
|
||||
start: number
|
||||
limit: number
|
||||
sort?: string
|
||||
controller?: AbortController
|
||||
}) => T[] | Promise<T[]>
|
||||
|
||||
export type VFlexTableWrapperSortFunction<T = any> = (parameters: {
|
||||
key: string
|
||||
column: Partial<VFlexTableWrapperColumn>
|
||||
order: 'asc' | 'desc'
|
||||
a: T
|
||||
b: T
|
||||
}) => number
|
||||
|
||||
export type VFlexTableWrapperFilterFunction<T = any> = (parameters: {
|
||||
searchTerm: string
|
||||
value: any
|
||||
row: T
|
||||
column: Partial<VFlexTableWrapperColumn>
|
||||
index: number
|
||||
}) => boolean
|
||||
|
||||
export interface VFlexTableWrapperColumn extends VFlexTableColumn {
|
||||
searchable?: boolean
|
||||
sortable?: boolean
|
||||
sort?: VFlexTableWrapperSortFunction
|
||||
filter?: VFlexTableWrapperFilterFunction
|
||||
}
|
||||
|
||||
export interface VFlexTableWrapperInjection {
|
||||
data?: any[] | undefined
|
||||
columns?: Record<string, Partial<VFlexTableWrapperColumn>>
|
||||
loading?: boolean
|
||||
searchInput?: string
|
||||
searchTerm?: string
|
||||
start?: number
|
||||
limit?: number
|
||||
sort?: string
|
||||
page?: number
|
||||
total?: number
|
||||
totalPages?: number
|
||||
fetchData: (controller?: AbortController) => Promise<void>
|
||||
}
|
||||
|
||||
export const flewTableWrapperSymbol: InjectionKey<VFlexTableWrapperInjection> = Symbol()
|
||||
|
||||
const defaultFormatter = (value: any) => value
|
||||
const defaultSortFunction: VFlexTableWrapperSortFunction = ({ key, order, a, b }) => {
|
||||
const aValue = a[key]
|
||||
const bValue = b[key]
|
||||
|
||||
if (typeof aValue === 'string') {
|
||||
if (order === 'asc') {
|
||||
return aValue.localeCompare(bValue)
|
||||
}
|
||||
else {
|
||||
return bValue.localeCompare(aValue)
|
||||
}
|
||||
}
|
||||
|
||||
if (aValue > bValue) {
|
||||
return order === 'asc' ? 1 : -1
|
||||
}
|
||||
|
||||
if (aValue < bValue) {
|
||||
return order === 'asc' ? -1 : 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
data: {
|
||||
type: [Array, Function] as PropType<any[] | VFlexTableWrapperDataResolver>,
|
||||
default: undefined,
|
||||
},
|
||||
columns: {
|
||||
type: Object as PropType<Record<string, string | Partial<VFlexTableWrapperColumn>>>,
|
||||
default: undefined,
|
||||
},
|
||||
sort: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
searchTerm: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
debounceSearch: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
},
|
||||
slots: Object as SlotsType<{
|
||||
default: VFlexTableWrapperInjection
|
||||
}>,
|
||||
emits: ['update:sort', 'update:page', 'update:limit', 'update:searchTerm'],
|
||||
setup(props, context) {
|
||||
const rawData = ref<any[]>()
|
||||
const loading = ref(false)
|
||||
|
||||
const defaultSort = ref('')
|
||||
const sort = computed({
|
||||
get: () => props.sort ?? defaultSort.value,
|
||||
set(value) {
|
||||
if (props.sort === undefined) {
|
||||
defaultSort.value = value
|
||||
}
|
||||
else {
|
||||
context.emit('update:sort', value)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const defaultSearchInput = ref('')
|
||||
const searchInput = computed({
|
||||
get: () => props.searchTerm ?? defaultSearchInput.value,
|
||||
set(value) {
|
||||
if (props.searchTerm === undefined) {
|
||||
defaultSearchInput.value = value
|
||||
}
|
||||
else {
|
||||
context.emit('update:searchTerm', value)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const defaultPage = ref(1)
|
||||
const page = computed({
|
||||
get: () => props.page ?? defaultPage.value,
|
||||
set(value) {
|
||||
if (props.page === undefined) {
|
||||
defaultPage.value = value
|
||||
}
|
||||
else {
|
||||
context.emit('update:page', value)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const defaultLimit = ref(10)
|
||||
const limit = computed({
|
||||
get: () => Math.max(1, props.limit ?? defaultLimit.value),
|
||||
set(value) {
|
||||
if (props.limit === undefined) {
|
||||
defaultLimit.value = value
|
||||
}
|
||||
else {
|
||||
context.emit('update:limit', value)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const columns = computed(() => {
|
||||
const columnProps = props.columns
|
||||
if (!columnProps) return columnProps
|
||||
|
||||
const wrapperColumns: Record<string, Partial<VFlexTableWrapperColumn>> = {}
|
||||
|
||||
Object.keys(columnProps).reduce((acc, key) => {
|
||||
const value = columnProps[key]
|
||||
|
||||
if (typeof value === 'string') {
|
||||
acc[key] = {
|
||||
format: defaultFormatter,
|
||||
label: value,
|
||||
key,
|
||||
}
|
||||
}
|
||||
else if (typeof value === 'object') {
|
||||
acc[key] = {
|
||||
format: defaultFormatter,
|
||||
label: key,
|
||||
key,
|
||||
...value,
|
||||
}
|
||||
|
||||
if (value.sortable === true) {
|
||||
if (value.renderHeader) {
|
||||
acc[key].renderHeader = () => {
|
||||
return h(
|
||||
VFlexTableSortColumn,
|
||||
{
|
||||
'id': key,
|
||||
'noRouter': true,
|
||||
'modelValue': sort.value,
|
||||
'onUpdate:modelValue': value => (sort.value = value),
|
||||
},
|
||||
{
|
||||
default: value.renderHeader,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
else {
|
||||
acc[key].renderHeader = () => {
|
||||
return h(VFlexTableSortColumn, {
|
||||
'id': key,
|
||||
'label': value.label ?? key,
|
||||
'noRouter': true,
|
||||
'modelValue': sort.value,
|
||||
'onUpdate:modelValue': value => (sort.value = value),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value.searchable === true && !value.sort) {
|
||||
acc[key].sort = defaultSortFunction
|
||||
}
|
||||
}
|
||||
|
||||
return acc
|
||||
}, wrapperColumns)
|
||||
|
||||
return wrapperColumns
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = rawData.value
|
||||
if (!data) return data
|
||||
if (typeof props.data === 'function') return data
|
||||
|
||||
// filter data
|
||||
if (searchTerm.value) {
|
||||
const searchableColumns = columns.value
|
||||
? (Object.values(columns.value).filter((column) => {
|
||||
if (!column || typeof column === 'string') return false
|
||||
return column.searchable === true
|
||||
}) as Partial<VFlexTableWrapperColumn>[])
|
||||
: []
|
||||
|
||||
if (searchableColumns.length) {
|
||||
const _searchRe = new RegExp(searchTerm.value, 'i')
|
||||
data = data.filter((row, index) => {
|
||||
return searchableColumns.some((column) => {
|
||||
if (!column.key) return false
|
||||
|
||||
const value = row[column.key]
|
||||
|
||||
if (column.filter) {
|
||||
return column.filter({
|
||||
searchTerm: searchTerm.value,
|
||||
value,
|
||||
row,
|
||||
column,
|
||||
index,
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof value === 'string') return value.match(_searchRe)
|
||||
|
||||
return false
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
const sortedData = computed(() => {
|
||||
let data = filteredData.value
|
||||
if (!data) return data
|
||||
if (typeof props.data === 'function') return data
|
||||
|
||||
// sort data
|
||||
if (sort.value && sort.value.includes(':')) {
|
||||
const [sortField, sortOrder] = sort.value.split(':') as [string, 'desc' | 'asc']
|
||||
|
||||
const sortingColumn = columns.value
|
||||
? (Object.values(columns.value).find((column) => {
|
||||
if (!column || typeof column === 'string') return false
|
||||
return column.sortable === true && column.key === sortField
|
||||
}) as Partial<VFlexTableWrapperColumn>)
|
||||
: null
|
||||
|
||||
if (sortingColumn) {
|
||||
const sorted = [...data]
|
||||
sorted.sort((a, b) => {
|
||||
if (!sortingColumn.key) return 0
|
||||
if (!sortingColumn.sort) return 0
|
||||
|
||||
return sortingColumn.sort({
|
||||
order: sortOrder,
|
||||
column: sortingColumn,
|
||||
key: sortingColumn.key,
|
||||
a,
|
||||
b,
|
||||
})
|
||||
})
|
||||
data = sorted
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
const data = computed(() => {
|
||||
if (typeof props.data === 'function') return rawData.value
|
||||
if (!rawData.value) return rawData.value
|
||||
|
||||
let data = sortedData.value
|
||||
|
||||
// paginate data
|
||||
return data?.slice(start.value, start.value + limit.value)
|
||||
})
|
||||
|
||||
const searchTerm = useDebounce(searchInput, props.debounceSearch)
|
||||
const total = computed(() => props.total ?? sortedData.value?.length ?? 0)
|
||||
const start = computed(() => (page.value - 1) * limit.value)
|
||||
const totalPages = computed(() =>
|
||||
total.value ? Math.ceil(total.value / limit.value) : 0,
|
||||
)
|
||||
|
||||
async function fetchData(controller?: AbortController) {
|
||||
if (typeof props.data === 'function') {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
rawData.value = await props.data({
|
||||
searchTerm: searchTerm.value,
|
||||
start: start.value,
|
||||
limit: limit.value,
|
||||
sort: sort.value,
|
||||
controller,
|
||||
})
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch([searchTerm, limit], () => {
|
||||
if (page.value !== 1) {
|
||||
page.value = 1
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(async (onInvalidate) => {
|
||||
let controller: AbortController
|
||||
|
||||
if (typeof props.data === 'function') {
|
||||
controller = new AbortController()
|
||||
await fetchData(controller)
|
||||
}
|
||||
else {
|
||||
rawData.value = props.data
|
||||
}
|
||||
|
||||
onInvalidate(() => {
|
||||
controller?.abort()
|
||||
})
|
||||
})
|
||||
|
||||
const wrapperState = reactive({
|
||||
data,
|
||||
columns,
|
||||
loading,
|
||||
searchInput,
|
||||
searchTerm,
|
||||
start,
|
||||
page,
|
||||
limit,
|
||||
sort,
|
||||
total,
|
||||
totalPages,
|
||||
fetchData,
|
||||
}) as VFlexTableWrapperInjection
|
||||
|
||||
provide(flewTableWrapperSymbol, wrapperState)
|
||||
context.expose(wrapperState)
|
||||
|
||||
return () => {
|
||||
const slotContent = context.slots.default?.(wrapperState)
|
||||
return h('div', { class: 'flex-table-wrapper' }, slotContent)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.flex-table-wrapper {
|
||||
background: var(--white);
|
||||
border: 1px solid color-mix(in oklab, var(--fade-grey), black 3%);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
|
||||
.flex-table {
|
||||
.flex-table-item {
|
||||
margin-bottom: 0;
|
||||
border-radius: 0;
|
||||
border-inline-start: none;
|
||||
border-inline-end: none;
|
||||
border-top: none;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 6px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border-radius: 4px;
|
||||
outline-offset: var(--accessibility-focus-outline-offset);
|
||||
outline-width: var(--accessibility-focus-outline-width);
|
||||
outline-style: var(--accessibility-focus-outline-style);
|
||||
outline-color: var(--accessibility-focus-outline-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
6. Flex Table advanced wrapper Dark mode
|
||||
========================================================================== */
|
||||
|
||||
.is-dark {
|
||||
.flex-table-wrapper {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 6%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
9. Media Queries
|
||||
========================================================================== */
|
||||
|
||||
@media (width <= 767px) {
|
||||
.flex-table-wrapper {
|
||||
.flex-table {
|
||||
.flex-table-header {
|
||||
.is-checkbox {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-table-item {
|
||||
padding-inline-start: 0;
|
||||
padding-inline-end: 0;
|
||||
|
||||
.is-checkbox {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
82
src/components/base/VGrid.vue
Normal file
82
src/components/base/VGrid.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
export type VGridJustifyItems = 'start' | 'end' | 'center' | 'stretch' | 'initial'
|
||||
export type VGridAlignItems = 'start' | 'end' | 'center' | 'stretch' | 'initial'
|
||||
export type VGridJustifyContent =
|
||||
| 'start'
|
||||
| 'end'
|
||||
| 'center'
|
||||
| 'stretch'
|
||||
| 'space-around'
|
||||
| 'space-between'
|
||||
| 'space-evenly'
|
||||
| 'initial'
|
||||
export type VGridAlignContent =
|
||||
| 'start'
|
||||
| 'end'
|
||||
| 'center'
|
||||
| 'stretch'
|
||||
| 'space-around'
|
||||
| 'space-between'
|
||||
| 'space-evenly'
|
||||
| 'initial'
|
||||
export type VGridAutoFlow = 'row' | 'column' | 'row dense' | 'column dense' | 'initial'
|
||||
|
||||
export interface VGridProps {
|
||||
inline?: boolean
|
||||
gridTemplateColumns?: string
|
||||
gridTemplateRows?: string
|
||||
gridTemplateAreas?: string
|
||||
columnGap?: string
|
||||
rowGap?: string
|
||||
justifyItems?: VGridJustifyItems
|
||||
alignItems?: VGridAlignItems
|
||||
justifyContent?: VGridJustifyContent
|
||||
alignContent?: VGridAlignContent
|
||||
placeContent?: string
|
||||
gridAutoColumns?: string
|
||||
gridAutoRows?: string
|
||||
gridAutoFlow?: VGridAutoFlow
|
||||
}
|
||||
const props = withDefaults(defineProps<VGridProps>(), {
|
||||
gridTemplateColumns: 'none',
|
||||
gridTemplateRows: 'none',
|
||||
gridTemplateAreas: 'none',
|
||||
columnGap: 'normal',
|
||||
rowGap: 'normal',
|
||||
justifyItems: 'initial',
|
||||
alignItems: 'initial',
|
||||
justifyContent: 'initial',
|
||||
alignContent: 'initial',
|
||||
placeContent: 'normal',
|
||||
gridAutoColumns: 'auto',
|
||||
gridAutoRows: 'auto',
|
||||
gridAutoFlow: 'row',
|
||||
})
|
||||
const display = computed(() => (props.inline ? 'inline-grid' : 'grid'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="v-grid">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.v-grid {
|
||||
display: v-bind(display);
|
||||
grid-template-columns: v-bind('props.gridTemplateColumns');
|
||||
grid-template-rows: v-bind('props.gridTemplateRows');
|
||||
grid-template-areas: v-bind('props.gridTemplateAreas');
|
||||
column-gap: v-bind('props.columnGap');
|
||||
row-gap: v-bind('props.rowGap');
|
||||
justify-items: v-bind('props.justifyItems');
|
||||
align-items: v-bind('props.alignItems');
|
||||
justify-content: v-bind('props.justifyContent');
|
||||
align-content: v-bind('props.alignContent');
|
||||
/* stylelint-disable-next-line declaration-block-no-shorthand-property-overrides */
|
||||
place-content: v-bind('props.placeContent');
|
||||
grid-auto-columns: v-bind('props.gridAutoColumns');
|
||||
grid-auto-rows: v-bind('props.gridAutoRows');
|
||||
grid-auto-flow: v-bind('props.gridAutoFlow');
|
||||
}
|
||||
</style>
|
||||
43
src/components/base/VGridItem.vue
Normal file
43
src/components/base/VGridItem.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
export type VGridJustifySelf = 'start' | 'end' | 'center' | 'stretch' | 'auto'
|
||||
export type VGridAlignSelf = 'start' | 'end' | 'center' | 'stretch' | 'auto'
|
||||
|
||||
export interface VGridItemProps {
|
||||
gridColumnStart?: string | number
|
||||
gridColumnEnd?: string | number
|
||||
gridRowStart?: string | number
|
||||
gridRowEnd?: string | number
|
||||
justifySelf?: VGridJustifySelf
|
||||
alignSelf?: VGridAlignSelf
|
||||
placeSelf?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VGridItemProps>(), {
|
||||
gridColumnStart: 'auto',
|
||||
gridColumnEnd: 'auto',
|
||||
gridRowStart: 'auto',
|
||||
gridRowEnd: 'auto',
|
||||
justifySelf: 'auto',
|
||||
alignSelf: 'auto',
|
||||
placeSelf: 'auto',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="v-grid-item">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.v-grid-item {
|
||||
grid-column-start: v-bind('props.gridColumnStart');
|
||||
grid-column-end: v-bind('props.gridColumnEnd');
|
||||
grid-row-start: v-bind('props.gridRowStart');
|
||||
grid-row-end: v-bind('props.gridRowEnd');
|
||||
justify-self: v-bind('props.justifySelf');
|
||||
align-self: v-bind('props.alignSelf');
|
||||
/* stylelint-disable-next-line declaration-block-no-shorthand-property-overrides */
|
||||
place-self: v-bind('props.placeSelf');
|
||||
}
|
||||
</style>
|
||||
22
src/components/base/VIcon.vue
Normal file
22
src/components/base/VIcon.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
icon?: string
|
||||
}>()
|
||||
|
||||
const isIconify = computed(() => {
|
||||
return props.icon && props.icon.indexOf(':') !== -1
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<iconify-icon
|
||||
v-if="isIconify"
|
||||
class="iconify"
|
||||
:icon="props.icon"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
aria-hidden="true"
|
||||
:class="props.icon"
|
||||
/>
|
||||
</template>
|
||||
717
src/components/base/VIconBox.vue
Normal file
717
src/components/base/VIconBox.vue
Normal file
@@ -0,0 +1,717 @@
|
||||
<script setup lang="ts">
|
||||
export type VIconBoxSize = 'small' | 'medium' | 'large' | 'big' | 'xl'
|
||||
export type VIconBoxColor =
|
||||
| 'primary'
|
||||
| 'info'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'purple'
|
||||
| 'yellow'
|
||||
| 'orange'
|
||||
| 'green'
|
||||
| 'red'
|
||||
| 'blue'
|
||||
export interface VIconProps {
|
||||
size?: VIconBoxSize
|
||||
color?: VIconBoxColor
|
||||
rounded?: boolean
|
||||
bordered?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VIconProps>(), {
|
||||
size: undefined,
|
||||
color: undefined,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="v-icon"
|
||||
:class="[
|
||||
props.size && 'is-' + props.size,
|
||||
props.color && 'is-' + props.color,
|
||||
props.rounded && 'is-rounded',
|
||||
props.bordered && 'is-bordered',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.icons {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.v-icon {
|
||||
margin: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.v-icon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
border-radius: 8px;
|
||||
background: var(--fade-grey);
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
:deep(.fas),
|
||||
:deep(.fab),
|
||||
:deep(.far),
|
||||
:deep(.fal) {
|
||||
font-size: 1.2rem;
|
||||
color: var(--muted-grey);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
font-size: 1.4rem;
|
||||
color: var(--muted-grey);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
:deep(.iconify) {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&.is-small {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
|
||||
:deep(.fas),
|
||||
:deep(.fab),
|
||||
:deep(.far),
|
||||
:deep(.fal) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
:deep(.iconify) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-medium {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
min-width: 50px;
|
||||
|
||||
&.is-bordered {
|
||||
border-width: 1.6px;
|
||||
}
|
||||
|
||||
:deep(.fas),
|
||||
:deep(.fab),
|
||||
:deep(.far),
|
||||
:deep(.fal) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
|
||||
:deep(.iconify) {
|
||||
font-size: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-large {
|
||||
height: 68px;
|
||||
width: 68px;
|
||||
min-width: 68px;
|
||||
|
||||
&.is-bordered {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
:deep(.fas),
|
||||
:deep(.fab),
|
||||
:deep(.far),
|
||||
:deep(.fal) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
:deep(.iconify) {
|
||||
font-size: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-big {
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
|
||||
&.is-bordered {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
:deep(.fas),
|
||||
:deep(.fab),
|
||||
:deep(.far),
|
||||
:deep(.fal) {
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
font-size: 2.6rem;
|
||||
}
|
||||
|
||||
:deep(.iconify) {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
font-size: 40px;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-xl {
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
min-width: 100px;
|
||||
|
||||
&.is-bordered {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
:deep(.fas),
|
||||
:deep(.fab),
|
||||
:deep(.far),
|
||||
:deep(.fal) {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
font-size: 3.4rem;
|
||||
}
|
||||
|
||||
:deep(.iconify) {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
font-size: 50px;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-rounded {
|
||||
border-radius: var(--radius-rounded);
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
background: color-mix(in oklab, var(--primary), white 82%);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.lnil),
|
||||
:deep(.lnir),
|
||||
:deep(.fab) {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-secondary {
|
||||
background: color-mix(in oklab, var(--secondary), white 82%);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--secondary);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.lnil),
|
||||
:deep(.lnir),
|
||||
:deep(.fab) {
|
||||
color: var(--secondary);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-accent {
|
||||
background: color-mix(in oklab, var(--primary), white 76%);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.lnil),
|
||||
:deep(.lnir),
|
||||
:deep(.fab) {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
background: color-mix(in oklab, var(--success), white 80%);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.lnil),
|
||||
:deep(.lnir),
|
||||
:deep(.fab) {
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
background: color-mix(in oklab, var(--info), white 80%);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--info);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.lnil),
|
||||
:deep(.lnir),
|
||||
:deep(.fab) {
|
||||
color: var(--info);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
background: color-mix(in oklab, var(--warning), white 80%);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--warning);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.lnil),
|
||||
:deep(.lnir),
|
||||
:deep(.fab) {
|
||||
color: var(--warning);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
background: color-mix(in oklab, var(--danger), white 80%);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.lnil),
|
||||
:deep(.lnir),
|
||||
:deep(.fab) {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-purple {
|
||||
background: color-mix(in oklab, var(--purple), white 80%);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--purple);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.lnil),
|
||||
:deep(.lnir),
|
||||
:deep(.fab) {
|
||||
color: var(--purple);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-blue {
|
||||
background: color-mix(in oklab, var(--blue), white 80%);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--blue);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.lnil),
|
||||
:deep(.lnir),
|
||||
:deep(.fab) {
|
||||
color: var(--blue);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-yellow {
|
||||
background: color-mix(in oklab, var(--yellow), white 80%);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--yellow);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.lnil),
|
||||
:deep(.lnir),
|
||||
:deep(.fab) {
|
||||
color: var(--yellow);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-orange {
|
||||
background: color-mix(in oklab, var(--orange), white 80%);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--orange);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.lnil),
|
||||
:deep(.lnir),
|
||||
:deep(.fab) {
|
||||
color: var(--orange);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-green {
|
||||
background: color-mix(in oklab, var(--green), white 80%);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--green);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.lnil),
|
||||
:deep(.lnir),
|
||||
:deep(.fab) {
|
||||
color: var(--green);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-red {
|
||||
background: color-mix(in oklab, var(--red), white 80%);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--red);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.lnil),
|
||||
:deep(.lnir),
|
||||
:deep(.fab) {
|
||||
color: var(--red);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-bordered {
|
||||
border: 1px solid var(--muted-grey);
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.v-icon {
|
||||
background-color: color-mix(in oklab, var(--dark-sidebar), white 2%);
|
||||
|
||||
&.is-primary {
|
||||
background: var(--primary);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.fab) {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-accent {
|
||||
background: var(--primary);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.fab) {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
background: var(--success);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.fab) {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
background: var(--info);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--info);
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.fab) {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
background: var(--warning);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--warning);
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.fab) {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
background: var(--danger);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.fab) {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-purple {
|
||||
background: var(--purple);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--purple);
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.fab) {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-blue {
|
||||
background: var(--blue);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--blue);
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.fab) {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-yellow {
|
||||
background: var(--yellow);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--yellow);
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.fab) {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-orange {
|
||||
background: var(--orange);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--orange);
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.fab) {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-green {
|
||||
background: var(--green);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--green);
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.fab) {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-red {
|
||||
background: var(--red);
|
||||
|
||||
&.is-bordered {
|
||||
border-color: var(--red);
|
||||
}
|
||||
|
||||
:deep(.lnil),
|
||||
:deep(.lnir) {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
:deep(.iconify),
|
||||
:deep(.fas),
|
||||
:deep(.far),
|
||||
:deep(.fal),
|
||||
:deep(.fab) {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
171
src/components/base/VIconButton.vue
Normal file
171
src/components/base/VIconButton.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<script lang="ts">
|
||||
import { type PropType } from 'vue'
|
||||
|
||||
export type VIconButtonDark = '1' | '2' | '3' | '4' | '5' | '6'
|
||||
export type VIconButtonColor =
|
||||
| 'primary'
|
||||
| 'info'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'white'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
to: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
color: {
|
||||
type: String as PropType<VIconButtonColor>,
|
||||
default: undefined,
|
||||
validator: (value: VIconButtonColor) => {
|
||||
// The value must match one of these strings
|
||||
if (
|
||||
[undefined, 'primary', 'info', 'success', 'warning', 'danger', 'white'].indexOf(
|
||||
value,
|
||||
) === -1
|
||||
) {
|
||||
console.warn(
|
||||
`VIconButton: invalid "${value}" color. Should be primary, info, success, warning, danger, white or undefined`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
type: String as PropType<VIconButtonDark>,
|
||||
default: '1',
|
||||
validator: (value: VIconButtonDark) => {
|
||||
if (!value) return true
|
||||
// The value must match one of these strings
|
||||
if (['1', '2', '3', '4', '5', '6'].indexOf(value) === -1) {
|
||||
console.warn(
|
||||
`VIconButton: invalid "${value}" dark. Should be 1, 2, 3, 4, 5, 6 or undefined`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
circle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
bold: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
light: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
raised: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
outlined: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
darkOutlined: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { attrs }) {
|
||||
const classes = computed(() => {
|
||||
const defaultClasses = (attrs?.class || []) as string[] | string
|
||||
return [
|
||||
defaultClasses,
|
||||
props.disabled && 'is-disabled',
|
||||
props.circle && 'is-circle',
|
||||
props.bold && 'is-bold',
|
||||
props.outlined && 'is-outlined',
|
||||
props.raised && 'is-raised',
|
||||
props.dark && `is-dark-bg-${props.dark}`,
|
||||
props.darkOutlined && 'is-dark-outlined',
|
||||
props.loading && 'is-loading',
|
||||
props.color && `is-${props.color}`,
|
||||
props.light && 'is-light',
|
||||
]
|
||||
})
|
||||
const isIconify = computed(() => props.icon && props.icon.indexOf(':') !== -1)
|
||||
|
||||
return () => {
|
||||
let icon
|
||||
if (isIconify.value) {
|
||||
icon = h('iconify-icon', {
|
||||
class: 'iconify',
|
||||
icon: props.icon,
|
||||
})
|
||||
}
|
||||
else {
|
||||
icon = h('i', { 'aria-hidden': true, 'class': props.icon })
|
||||
}
|
||||
|
||||
const iconWrapper = h('span', { class: 'icon' }, icon)
|
||||
|
||||
if (props.to) {
|
||||
return h(
|
||||
resolveComponent('RouterLink'),
|
||||
{
|
||||
...attrs,
|
||||
to: props.to,
|
||||
class: ['button', ...classes.value],
|
||||
},
|
||||
iconWrapper,
|
||||
)
|
||||
}
|
||||
else if (props.href) {
|
||||
return h(
|
||||
'a',
|
||||
{
|
||||
...attrs,
|
||||
href: props.href,
|
||||
class: classes.value,
|
||||
},
|
||||
iconWrapper,
|
||||
)
|
||||
}
|
||||
|
||||
return h(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
...attrs,
|
||||
disabled: props.disabled,
|
||||
class: ['button', ...classes.value],
|
||||
},
|
||||
iconWrapper,
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.button {
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
}
|
||||
</style>
|
||||
140
src/components/base/VIconWrap.vue
Normal file
140
src/components/base/VIconWrap.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
export type VIconWrapDark = '1' | '2' | '3' | '4' | '5' | '6'
|
||||
export type VIconWrapSize = 'small' | 'medium' | 'large'
|
||||
export type VIconWrapColor =
|
||||
| 'white'
|
||||
| 'black'
|
||||
| 'light'
|
||||
| 'dark'
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'link'
|
||||
| 'info'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
export interface VIconWrapProps {
|
||||
icon?: string
|
||||
picture?: string
|
||||
color?: VIconWrapColor
|
||||
size?: VIconWrapSize
|
||||
dark?: VIconWrapDark
|
||||
hasLargeIcon?: boolean
|
||||
hasBackground?: boolean
|
||||
placeholder?: boolean
|
||||
darkPrimary?: boolean
|
||||
darkCardBordered?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VIconWrapProps>(), {
|
||||
icon: undefined,
|
||||
picture: undefined,
|
||||
color: undefined,
|
||||
size: undefined,
|
||||
dark: '3',
|
||||
})
|
||||
|
||||
const { onceError } = useImageError()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="icon-wrap"
|
||||
:class="[
|
||||
props.color && !props.hasBackground && `has-text-${props.color}`,
|
||||
props.color && props.hasBackground && `has-background-${props.color}`,
|
||||
props.color && props.color !== 'white' && props.hasBackground && `has-text-white`,
|
||||
props.color && props.color === 'white' && props.hasBackground && `has-text-black`,
|
||||
props.size && `is-${props.size}`,
|
||||
props.dark && !props.hasBackground && `is-dark-bg-${props.dark}`,
|
||||
props.darkPrimary && 'is-dark-primary',
|
||||
props.darkCardBordered && 'is-dark-card-bordered',
|
||||
props.hasLargeIcon && 'has-large-icon',
|
||||
props.picture && 'has-img',
|
||||
props.placeholder && 'is-placeholder',
|
||||
]"
|
||||
>
|
||||
<img
|
||||
v-if="props.picture"
|
||||
:src="props.picture"
|
||||
alt=""
|
||||
@error.once="onceError($event, 32)"
|
||||
>
|
||||
<VIcon :icon="props.icon" />
|
||||
<slot name="after" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.icon-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
border-radius: var(--radius-rounded);
|
||||
background: var(--white);
|
||||
border: 1px solid color-mix(in oklab, var(--fade-grey), black 3%);
|
||||
box-shadow: var(--light-box-shadow);
|
||||
color: var(--primary);
|
||||
font-size: 1rem;
|
||||
|
||||
&.has-large-icon {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
&.is-small {
|
||||
font-size: 0.9rem;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
|
||||
&.has-large-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-medium {
|
||||
font-size: 1.4rem;
|
||||
height: 42px;
|
||||
width: 42px;
|
||||
min-width: 42px;
|
||||
|
||||
&.has-large-icon {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-large {
|
||||
font-size: 2rem;
|
||||
height: 58px;
|
||||
width: 58px;
|
||||
min-width: 58px;
|
||||
|
||||
&.has-large-icon {
|
||||
font-size: 2.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: var(--radius-rounded);
|
||||
}
|
||||
|
||||
&.is-placeholder {
|
||||
background-color: color-mix(in oklab, var(--fade-grey), white 2%) !important;
|
||||
border-color: color-mix(in oklab, var(--fade-grey), black 3%) !important;
|
||||
color: var(--light-text);
|
||||
|
||||
.iconify {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.icon-wrap {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
57
src/components/base/VInput.vue
Normal file
57
src/components/base/VInput.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
export interface VInputProps {
|
||||
raw?: boolean
|
||||
trueValue?: boolean
|
||||
falseValue?: boolean
|
||||
}
|
||||
|
||||
const modelValue = defineModel<any>({
|
||||
default: '',
|
||||
})
|
||||
const props = withDefaults(defineProps<VInputProps>(), {
|
||||
modelValue: '',
|
||||
trueValue: true,
|
||||
falseValue: false,
|
||||
})
|
||||
|
||||
const { field, id } = useVFieldContext({
|
||||
create: false,
|
||||
help: 'VInput',
|
||||
})
|
||||
|
||||
const internal = computed({
|
||||
get() {
|
||||
if (field?.value) {
|
||||
return field.value.value
|
||||
}
|
||||
else {
|
||||
return modelValue.value
|
||||
}
|
||||
},
|
||||
set(value: any) {
|
||||
if (field?.value) {
|
||||
field.value.setValue(value)
|
||||
}
|
||||
modelValue.value = value
|
||||
},
|
||||
})
|
||||
|
||||
const classes = computed(() => {
|
||||
if (props.raw) return []
|
||||
|
||||
return ['input', 'v-input']
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
:id="id"
|
||||
v-model="internal"
|
||||
:class="classes"
|
||||
:name="id"
|
||||
:true-value="props.trueValue"
|
||||
:false-value="props.falseValue"
|
||||
@change="field?.handleChange"
|
||||
@blur="field?.handleBlur"
|
||||
>
|
||||
</template>
|
||||
30
src/components/base/VLabel.vue
Normal file
30
src/components/base/VLabel.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
export interface VLabelProps {
|
||||
id?: string
|
||||
raw?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VLabelProps>(), {
|
||||
id: undefined,
|
||||
})
|
||||
|
||||
const context = useVFieldContext({
|
||||
create: false,
|
||||
help: 'VLabel',
|
||||
})
|
||||
|
||||
const classes = computed(() => {
|
||||
if (props.raw) return []
|
||||
|
||||
return ['label']
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label
|
||||
:class="classes"
|
||||
:for="props.id || context.id.value"
|
||||
>
|
||||
<slot v-bind="context" />
|
||||
</label>
|
||||
</template>
|
||||
47
src/components/base/VLink.vue
Normal file
47
src/components/base/VLink.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
// @ts-ignore
|
||||
...RouterLink.props,
|
||||
})
|
||||
|
||||
const isExternalLink = computed(() => {
|
||||
return typeof props.to === 'string' && props.to.startsWith('http')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
v-if="isExternalLink"
|
||||
v-bind="$attrs"
|
||||
:href="to"
|
||||
target="_blank"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
<RouterLink
|
||||
v-else
|
||||
v-slot="{ href, navigate, isActive, isExactActive }"
|
||||
v-bind="({
|
||||
...$props,
|
||||
custom: true,
|
||||
} as any)"
|
||||
>
|
||||
<a
|
||||
v-bind="$attrs"
|
||||
:href="href"
|
||||
:class="[
|
||||
isActive && 'router-link-active',
|
||||
isExactActive && 'router-link-exact-active',
|
||||
]"
|
||||
@click="navigate"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
</RouterLink>
|
||||
</template>
|
||||
143
src/components/base/VLoader.vue
Normal file
143
src/components/base/VLoader.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
export type VLoaderSize = 'small' | 'large' | 'xl'
|
||||
export type VLoaderWrapperRadius = 'regular' | 'smooth' | 'rounded'
|
||||
export interface VLoaderProps {
|
||||
size?: VLoaderSize
|
||||
card?: VLoaderWrapperRadius
|
||||
active?: boolean
|
||||
grey?: boolean
|
||||
translucent?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VLoaderProps>(), {
|
||||
size: undefined,
|
||||
card: undefined,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="has-loader"
|
||||
:class="[props.active && 'has-loader-active']"
|
||||
>
|
||||
<div
|
||||
v-if="props.active"
|
||||
class="v-loader-wrapper is-active"
|
||||
:class="[
|
||||
grey && 'is-grey',
|
||||
translucent && 'is-translucent',
|
||||
card === 'regular' && 's-card',
|
||||
card === 'smooth' && 'r-card',
|
||||
card === 'rounded' && 'l-card',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="loader is-loading"
|
||||
:class="[props.size && `is-${props.size}`]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.has-loader {
|
||||
position: relative;
|
||||
|
||||
&.has-loader-active {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.v-loader-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: var(--white);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s;
|
||||
z-index: 5;
|
||||
|
||||
&.is-active {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
|
||||
&.is-translucent {
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-grey {
|
||||
background: var(--background-grey);
|
||||
}
|
||||
|
||||
.loader {
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
|
||||
&.is-small {
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
&.is-large {
|
||||
height: 5rem;
|
||||
width: 5rem;
|
||||
}
|
||||
|
||||
&.is-xl {
|
||||
height: 7rem;
|
||||
width: 7rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.has-loader {
|
||||
.v-loader-wrapper {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 6%);
|
||||
|
||||
&.is-grey {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$grey-lighter: hsl(0deg 0% 86%) !default;
|
||||
$radius-rounded: 290486px !default;
|
||||
|
||||
@keyframes spinAroundLoader {
|
||||
from {
|
||||
transform: rotate(calc(var(--transform-direction) * 0deg));
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(calc(var(--transform-direction) * 359deg));
|
||||
}
|
||||
}
|
||||
|
||||
@mixin loader {
|
||||
animation: spinAroundLoader 500ms infinite linear;
|
||||
border: 2px solid $grey-lighter;
|
||||
border-radius: var(--radius-rounded);
|
||||
border-inline-end-color: transparent;
|
||||
border-top-color: transparent;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 1em;
|
||||
position: relative;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
%loader {
|
||||
@include loader;
|
||||
}
|
||||
</style>
|
||||
224
src/components/base/VMessage.vue
Normal file
224
src/components/base/VMessage.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<script setup lang="ts">
|
||||
export type VMessageColor =
|
||||
| 'primary'
|
||||
| 'success'
|
||||
| 'info'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'white'
|
||||
export interface VMessageEmits {
|
||||
(e: 'close'): void
|
||||
}
|
||||
export interface VMessageProps {
|
||||
color?: VMessageColor
|
||||
closable?: boolean
|
||||
}
|
||||
|
||||
const emit = defineEmits<VMessageEmits>()
|
||||
const props = withDefaults(defineProps<VMessageProps>(), {
|
||||
color: undefined,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="message"
|
||||
:class="[props.color && `is-${props.color}`]"
|
||||
>
|
||||
<a
|
||||
v-if="props.closable"
|
||||
aria-label="Dismiss"
|
||||
class="delete"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="emit('close')"
|
||||
@click.prevent="emit('close')"
|
||||
/>
|
||||
<div class="message-body">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.message {
|
||||
position: relative;
|
||||
border: 1px solid color-mix(in oklab, var(--fade-grey), black 3%);
|
||||
box-shadow: var(--light-box-shadow);
|
||||
padding-inline-end: 20px;
|
||||
|
||||
&.is-primary {
|
||||
border-color: color-mix(in oklab, var(--primary), white 24%);
|
||||
|
||||
.delete {
|
||||
&::before,
|
||||
&::after {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
border-color: color-mix(in oklab, var(--info), white 24%);
|
||||
|
||||
.delete {
|
||||
&::before,
|
||||
&::after {
|
||||
background-color: var(--info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
border-color: color-mix(in oklab, var(--success), white 24%);
|
||||
|
||||
.delete {
|
||||
&::before,
|
||||
&::after {
|
||||
background-color: var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
border-color: color-mix(in oklab, var(--warning), white 24%);
|
||||
|
||||
.delete {
|
||||
&::before,
|
||||
&::after {
|
||||
background-color: var(--warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
border-color: color-mix(in oklab, var(--danger), white 24%);
|
||||
|
||||
.delete {
|
||||
&::before,
|
||||
&::after {
|
||||
background-color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete {
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
top: 6px;
|
||||
inset-inline-end: 6px;
|
||||
|
||||
&::before {
|
||||
height: 1px;
|
||||
background-color: var(--light-text);
|
||||
}
|
||||
|
||||
&::after {
|
||||
width: 1px;
|
||||
background-color: var(--light-text);
|
||||
}
|
||||
}
|
||||
|
||||
.message-body {
|
||||
border: none;
|
||||
font-family: var(--font);
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.message {
|
||||
&:not(.is-primary, .is-info, .is-success, .is-warning, .is-danger) {
|
||||
background-color: var(--dark-sidebar);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 3%);
|
||||
|
||||
.message-body {
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
|
||||
.message-body {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.delete {
|
||||
&::before,
|
||||
&::after {
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
background: var(--success);
|
||||
border-color: var(--success);
|
||||
|
||||
.message-body {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.delete {
|
||||
&::before,
|
||||
&::after {
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
background: var(--info);
|
||||
border-color: var(--info);
|
||||
|
||||
.message-body {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.delete {
|
||||
&::before,
|
||||
&::after {
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
background: var(--warning);
|
||||
border-color: var(--warning);
|
||||
|
||||
.message-body {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.delete {
|
||||
&::before,
|
||||
&::after {
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
background: var(--danger);
|
||||
border-color: var(--danger);
|
||||
|
||||
.message-body {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.delete {
|
||||
&::before,
|
||||
&::after {
|
||||
background-color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
425
src/components/base/VModal.vue
Normal file
425
src/components/base/VModal.vue
Normal file
@@ -0,0 +1,425 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue'
|
||||
import { FocusTrap } from 'focus-trap-vue'
|
||||
|
||||
export type VModalSize = 'small' | 'medium' | 'large' | 'big' | 'wide' | 'full'
|
||||
export type VModalAction = 'center' | 'right'
|
||||
|
||||
export interface VModalEmits {
|
||||
(e: 'close'): void
|
||||
}
|
||||
export interface VModalProps {
|
||||
title: string
|
||||
is?: string | Component
|
||||
size?: VModalSize
|
||||
actions?: VModalAction
|
||||
open?: boolean
|
||||
rounded?: boolean
|
||||
noscroll?: boolean
|
||||
noclose?: boolean
|
||||
tabs?: boolean
|
||||
cancelLabel?: string
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<VModalEmits>()
|
||||
const props = withDefaults(defineProps<VModalProps>(), {
|
||||
is: 'div',
|
||||
size: undefined,
|
||||
actions: undefined,
|
||||
cancelLabel: undefined,
|
||||
})
|
||||
|
||||
const wasOpen = ref(false)
|
||||
const cancelLabel = computed(() => props.cancelLabel || 'Cancel')
|
||||
|
||||
const checkScroll = () => {
|
||||
if (props.noscroll && props.open) {
|
||||
if (!import.meta.env.SSR) {
|
||||
document.documentElement.classList.add('no-scroll')
|
||||
}
|
||||
wasOpen.value = true
|
||||
}
|
||||
else if (wasOpen.value && props.noscroll && !props.open) {
|
||||
if (!import.meta.env.SSR) {
|
||||
document.documentElement.classList.remove('no-scroll')
|
||||
}
|
||||
wasOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(checkScroll)
|
||||
onUnmounted(() => {
|
||||
if (!import.meta.env.SSR) {
|
||||
document.documentElement.classList.remove('no-scroll')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport
|
||||
v-if="open"
|
||||
to="body"
|
||||
>
|
||||
<FocusTrap
|
||||
:initial-focus="() => ($refs.closeButton as any)?.el"
|
||||
>
|
||||
<component
|
||||
:is="is"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
:class="[open && 'is-active', size && `is-${size}`]"
|
||||
class="modal v-modal"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<div
|
||||
class="modal-background v-modal-close"
|
||||
tabindex="-1"
|
||||
role="button"
|
||||
@keydown.enter.prevent="() => noclose === false && emit('close')"
|
||||
@click="() => noclose === false && emit('close')"
|
||||
/>
|
||||
<div class="modal-content">
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<h3>{{ title }}</h3>
|
||||
<button
|
||||
ref="closeButton"
|
||||
class="v-modal-close ml-auto"
|
||||
aria-label="close"
|
||||
tabindex="0"
|
||||
@keydown.enter.prevent="emit('close')"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<VIcon icon="lucide:x" />
|
||||
</button>
|
||||
</header>
|
||||
<div
|
||||
class="modal-card-body"
|
||||
:class="[props.tabs && 'has-tabs has-slimscroll']"
|
||||
>
|
||||
<div class="inner-content">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="modal-card-foot"
|
||||
:class="[
|
||||
actions === 'center' && 'is-centered',
|
||||
actions === 'right' && 'is-end',
|
||||
]"
|
||||
>
|
||||
<slot
|
||||
name="cancel"
|
||||
:close="() => emit('close')"
|
||||
>
|
||||
<a
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="button v-button v-modal-close"
|
||||
:class="[rounded && 'is-rounded']"
|
||||
@keydown.enter.prevent="emit('close')"
|
||||
@click="emit('close')"
|
||||
>
|
||||
{{ cancelLabel }}
|
||||
</a>
|
||||
</slot>
|
||||
<slot
|
||||
name="action"
|
||||
:close="() => emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</FocusTrap>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.modal {
|
||||
transition: all 0.5s;
|
||||
|
||||
&.is-wide {
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
|
||||
.modal-card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-full {
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
max-width: 90%;
|
||||
|
||||
.modal-card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.form-layout {
|
||||
max-width: 98%;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-big {
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
max-width: 840px;
|
||||
|
||||
.modal-card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-large {
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
|
||||
.modal-card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-medium {
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
|
||||
.modal-card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-small {
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
|
||||
.modal-card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
transition: all 0.4s;
|
||||
}
|
||||
}
|
||||
|
||||
.v-modal {
|
||||
background: transparent;
|
||||
border: transparent;
|
||||
|
||||
&.is-active {
|
||||
z-index: 200 !important;
|
||||
|
||||
.v-modal-close {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.v-modal-card {
|
||||
width: 100%;
|
||||
background: var(--white);
|
||||
border: 1px solid var(--fade-grey);
|
||||
border-radius: 8px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
&::backdrop {
|
||||
background: var(--dark-sidebar);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
transform: scale(1) !important;
|
||||
opacity: 1 !important;
|
||||
max-width: 540px;
|
||||
overflow-x: hidden;
|
||||
animation: fadeInDown 0.5s;
|
||||
margin: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
.modal-card {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
&.is-rounded {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.modal-card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
background: var(--white);
|
||||
border-bottom-color: color-mix(in oklab, var(--fade-grey), black 3%);
|
||||
|
||||
&.no-border {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: var(--font-alt);
|
||||
color: var(--dark-text);
|
||||
font-weight: 600;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.v-modal-close {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: none;
|
||||
border-color: transparent;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline-offset: var(--accessibility-focus-outline-offset);
|
||||
outline-width: var(--accessibility-focus-outline-width);
|
||||
outline-style: var(--accessibility-focus-outline-style);
|
||||
outline-color: var(--accessibility-focus-outline-color);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
font-size: 20px;
|
||||
color: var(--muted-grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-card-body {
|
||||
max-height: 70vh;
|
||||
.modal-form {
|
||||
padding: 10px 0 20px;
|
||||
}
|
||||
|
||||
&.has-tabs {
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
|
||||
.tabs {
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 5px !important;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px !important;
|
||||
background: rgb(0 0 0 / 20%) !important;
|
||||
}
|
||||
|
||||
a {
|
||||
padding: 0.75em 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-card-foot {
|
||||
background-color: var(--white);
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid color-mix(in oklab, var(--fade-grey), black 3%);
|
||||
|
||||
&.no-border {
|
||||
border-top-color: transparent;
|
||||
}
|
||||
|
||||
&.is-start {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
&.is-centered {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
&.is-end {
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
|
||||
.v-button {
|
||||
min-width: 110px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.v-modal {
|
||||
.modal-background {
|
||||
background: rgb(101 101 104 / 80%) !important;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
.modal-card {
|
||||
.modal-card-head {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 6%) !important;
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
|
||||
h3 {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
|
||||
.v-modal-close {
|
||||
&:hover {
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-card-body {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 6%) !important;
|
||||
}
|
||||
|
||||
.modal-card-foot {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 6%) !important;
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (width >= 769px) {
|
||||
.modal.modal-lg {
|
||||
.modal-card,
|
||||
.modal-content {
|
||||
width: 800px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.modal.modal-sm {
|
||||
.modal-card,
|
||||
.modal-content {
|
||||
width: 400px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
src/components/base/VOptgroup.vue
Normal file
34
src/components/base/VOptgroup.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
const { field, id } = useVFieldContext({
|
||||
create: false,
|
||||
help: 'VOptgroup',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<optgroup>
|
||||
<slot v-bind="{ field, id }" />
|
||||
</optgroup>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
optgroup {
|
||||
padding: 0.5em 1em;
|
||||
|
||||
:deep(option) {
|
||||
&::before {
|
||||
content: '' !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
optgroup[disabled] {
|
||||
pointer-events: none;
|
||||
opacity: 0.4;
|
||||
cursor: default !important;
|
||||
|
||||
:deep(option[disabled]) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
20
src/components/base/VOption.vue
Normal file
20
src/components/base/VOption.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
const { field, id } = useVFieldContext({
|
||||
create: false,
|
||||
help: 'VOption',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<option>
|
||||
<slot v-bind="{ field, id }" />
|
||||
</option>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
option[disabled] {
|
||||
pointer-events: none;
|
||||
opacity: 0.4;
|
||||
cursor: default !important;
|
||||
}
|
||||
</style>
|
||||
105
src/components/base/VPlaceholderPage.vue
Normal file
105
src/components/base/VPlaceholderPage.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
export interface VPlaceholderPageProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
larger?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VPlaceholderPageProps>(), {
|
||||
subtitle: undefined,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-placeholder">
|
||||
<div class="placeholder-content">
|
||||
<slot name="image" />
|
||||
<h3 class="dark-inverted">
|
||||
{{ props.title }}
|
||||
</h3>
|
||||
<p
|
||||
v-if="props.subtitle"
|
||||
:class="[props.larger && 'is-larger']"
|
||||
>
|
||||
{{ props.subtitle }}
|
||||
</p>
|
||||
<slot name="action" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.page-placeholder {
|
||||
min-height: 400px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
|
||||
&.is-wider {
|
||||
.placeholder-content {
|
||||
> p {
|
||||
font-size: 1rem;
|
||||
max-width: 420px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 340px;
|
||||
margin: 0 auto 12px;
|
||||
|
||||
&.is-larger {
|
||||
max-width: 440px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-alt);
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
max-width: 440px;
|
||||
margin: 0 auto 12px;
|
||||
color: var(--light-text);
|
||||
|
||||
&.is-larger {
|
||||
max-width: 620px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.page-placeholder {
|
||||
.placeholder-content {
|
||||
h3 {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
.page-placeholder {
|
||||
.placeholder-content {
|
||||
img {
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
68
src/components/base/VPlaceholderSection.vue
Normal file
68
src/components/base/VPlaceholderSection.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
export interface VPlaceholderSectionProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VPlaceholderSectionProps>(), {
|
||||
subtitle: undefined,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-placeholder">
|
||||
<div class="placeholder-content">
|
||||
<slot name="image" />
|
||||
<h3 class="dark-inverted">
|
||||
{{ props.title }}
|
||||
</h3>
|
||||
<p v-if="props.subtitle">
|
||||
{{ props.subtitle }}
|
||||
</p>
|
||||
<slot name="action" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.section-placeholder {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
|
||||
.placeholder-content {
|
||||
text-align: center;
|
||||
|
||||
> img {
|
||||
display: block;
|
||||
max-width: 280px;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
|
||||
.v-avatar {
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: var(--font-alt);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: var(--font);
|
||||
font-size: 1rem;
|
||||
color: var(--light-text);
|
||||
max-width: 280px;
|
||||
margin: 0 auto 12px;
|
||||
}
|
||||
|
||||
.button {
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
96
src/components/base/VPlaceload.vue
Normal file
96
src/components/base/VPlaceload.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
export type VPlaceloadProps = {
|
||||
width?: string
|
||||
height?: string
|
||||
mobileWidth?: string
|
||||
mobileHeight?: string
|
||||
disabled?: boolean
|
||||
centered?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VPlaceloadProps>(), {
|
||||
width: '100%',
|
||||
height: '10px',
|
||||
mobileWidth: undefined,
|
||||
mobileHeight: undefined,
|
||||
})
|
||||
const mobileWidthValue = props.mobileWidth ?? props.width
|
||||
const mobileHeightValue = props.mobileHeight ?? props.height
|
||||
|
||||
if (props.width.match(CssUnitRe) === null) {
|
||||
console.warn(
|
||||
`VPlaceload: invalid "${props.width}" width. Should be a valid css unit value.`,
|
||||
)
|
||||
}
|
||||
if (props.height.match(CssUnitRe) === null) {
|
||||
console.warn(
|
||||
`VPlaceload: invalid "${props.height}" height. Should be a valid css unit value.`,
|
||||
)
|
||||
}
|
||||
if (mobileWidthValue.match(CssUnitRe) === null) {
|
||||
console.warn(
|
||||
`VPlaceload: invalid "${mobileWidthValue}" mobileWidth. Should be a valid css unit value.`,
|
||||
)
|
||||
}
|
||||
if (mobileHeightValue.match(CssUnitRe) === null) {
|
||||
console.warn(
|
||||
`VPlaceload: invalid "${mobileHeightValue}" mobileHeight. Should be a valid css unit value.`,
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="content-shape"
|
||||
:class="[props.centered && 'is-centered', !props.disabled && 'loads']"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content-shape {
|
||||
width: v-bind('props.width');
|
||||
height: v-bind('props.height');
|
||||
}
|
||||
|
||||
.content-shape {
|
||||
&.is-grow-1 {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.is-grow-2 {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
&.is-grow-3 {
|
||||
flex-grow: 3;
|
||||
}
|
||||
|
||||
&.is-grow-4 {
|
||||
flex-grow: 4;
|
||||
}
|
||||
|
||||
&.mw-30 {
|
||||
max-width: 30%;
|
||||
}
|
||||
|
||||
&.mw-60 {
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
&.mw-80 {
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
&.is-centered {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
.content-shape {
|
||||
width: v-bind(mobileWidthValue);
|
||||
height: v-bind(mobileHeightValue);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
95
src/components/base/VPlaceloadAvatar.vue
Normal file
95
src/components/base/VPlaceloadAvatar.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
export type VPlaceloadAvatarSize = 'small' | 'medium' | 'large' | 'big' | 'xl'
|
||||
export type VPlaceloadAvatarRounded = 'full' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
export interface VPlaceloadAvatarProps {
|
||||
size?: VPlaceloadAvatarSize
|
||||
rounded?: VPlaceloadAvatarRounded
|
||||
centered?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VPlaceloadAvatarProps>(), {
|
||||
size: undefined,
|
||||
rounded: 'full',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="placeload-avatar"
|
||||
:class="[
|
||||
!props.disabled && `loads`,
|
||||
props.size && `is-${props.size}`,
|
||||
props.centered && `is-centered`,
|
||||
props.rounded && `is-rounded-${props.rounded}`,
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.placeload-avatar {
|
||||
display: block;
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
|
||||
&.is-small {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
&.is-medium {
|
||||
width: 50px;
|
||||
min-width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
&.is-large {
|
||||
width: 68px;
|
||||
min-width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
|
||||
&.is-big {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
&.is-xl {
|
||||
width: 100px;
|
||||
min-width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
&.is-centered {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
|
||||
&.is-rounded-xs {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
&.is-rounded-sm {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&.is-rounded-md {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
&.is-rounded-lg {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
&.is-rounded-xl {
|
||||
border-radius: 1.25rem;
|
||||
}
|
||||
|
||||
&.is-rounded-full {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
86
src/components/base/VPlaceloadText.vue
Normal file
86
src/components/base/VPlaceloadText.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
export interface VPlaceloadTextProps {
|
||||
width?: string
|
||||
lastLineWidth?: string
|
||||
lines?: number
|
||||
disabled?: boolean
|
||||
centered?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VPlaceloadTextProps>(), {
|
||||
width: '100%',
|
||||
lastLineWidth: '100%',
|
||||
lines: 2,
|
||||
})
|
||||
if (props.width.match(CssUnitRe) === null) {
|
||||
console.warn(
|
||||
`VPlaceloadText: invalid "${props.width}" width. Should be a valid css unit value.`,
|
||||
)
|
||||
}
|
||||
if (props.lastLineWidth.match(CssUnitRe) === null) {
|
||||
console.warn(
|
||||
`VPlaceloadText: invalid "${props.lastLineWidth}" lastLineWidth. Should be a valid css unit value.`,
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="content-shape-group">
|
||||
<VPlaceload
|
||||
v-for="line of props.lines - 1"
|
||||
:key="line"
|
||||
:width="props.width"
|
||||
:centered="props.centered"
|
||||
/>
|
||||
<VPlaceload
|
||||
:width="props.lastLineWidth"
|
||||
:centered="props.centered"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.content-shape-group {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
.content-shape {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-grow-1 {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.is-grow-2 {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
&.is-grow-3 {
|
||||
flex-grow: 3;
|
||||
}
|
||||
|
||||
&.is-grow-4 {
|
||||
flex-grow: 4;
|
||||
}
|
||||
|
||||
&.mw-30 {
|
||||
max-width: 30%;
|
||||
}
|
||||
|
||||
&.mw-60 {
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
&.mw-80 {
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
&.is-centered {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
42
src/components/base/VPlaceloadWrap.vue
Normal file
42
src/components/base/VPlaceloadWrap.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="placeload-wrap is-flex">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.placeload-wrap {
|
||||
&.is-flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 767px) {
|
||||
.placeload-wrap {
|
||||
&.is-flex {
|
||||
flex-direction: column;
|
||||
padding: 1rem 0;
|
||||
|
||||
.content-shape-group {
|
||||
margin-top: 0.5rem;
|
||||
max-width: 70%;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
|
||||
.content-shape {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> .content-shape {
|
||||
margin-top: 0.5rem;
|
||||
max-width: 70%;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
165
src/components/base/VProgress.vue
Normal file
165
src/components/base/VProgress.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
export type VProgressSize = 'tiny' | 'smaller' | 'small'
|
||||
export type VProgressColor = 'primary' | 'success' | 'info' | 'warning' | 'danger'
|
||||
export interface VProgressProps {
|
||||
value?: number
|
||||
max?: number
|
||||
size?: VProgressSize
|
||||
color?: VProgressColor
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VProgressProps>(), {
|
||||
value: undefined,
|
||||
max: 100,
|
||||
size: undefined,
|
||||
color: 'primary',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<progress
|
||||
class="progress"
|
||||
:class="[props.size && `is-${props.size}`, props.color && `is-${props.color}`]"
|
||||
:value="props.value"
|
||||
:max="props.max"
|
||||
>
|
||||
{{ props.value ? `${(props.value / props.max) * 100}%` : '' }}
|
||||
</progress>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.progress {
|
||||
margin-bottom: 0;
|
||||
|
||||
&::-webkit-progress-value {
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
&::-ms-fill {
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
&.is-smaller {
|
||||
height: 0.5rem !important;
|
||||
}
|
||||
|
||||
&.is-tiny {
|
||||
height: 0.35rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.progress {
|
||||
background-color: var(--dark-sidebar);
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background-color: var(--dark-sidebar);
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
&::-webkit-progress-value {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
&::-ms-fill {
|
||||
background: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&:indeterminate {
|
||||
&.is-primary {
|
||||
background-color: var(--primary);
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--dark-sidebar) 30%,
|
||||
var(--primary) 30%
|
||||
);
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
background-color: var(--success);
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--dark-sidebar) 30%,
|
||||
var(--success) 30%
|
||||
);
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
background-color: var(--info);
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--dark-sidebar) 30%,
|
||||
var(--info) 30%
|
||||
);
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
background-color: var(--warning);
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--dark-sidebar) 30%,
|
||||
var(--warning) 30%
|
||||
);
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
background-color: var(--danger);
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--dark-sidebar) 30%,
|
||||
var(--danger) 30%
|
||||
);
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
339
src/components/base/VRadio.vue
Normal file
339
src/components/base/VRadio.vue
Normal file
@@ -0,0 +1,339 @@
|
||||
<script setup lang="ts">
|
||||
export type VRadioColor = 'primary' | 'info' | 'success' | 'warning' | 'danger'
|
||||
|
||||
export interface VRadioProps {
|
||||
id?: string
|
||||
value: any
|
||||
name?: string
|
||||
label?: string
|
||||
color?: VRadioColor
|
||||
square?: boolean
|
||||
solid?: boolean
|
||||
paddingless?: boolean
|
||||
}
|
||||
|
||||
const modelValue = defineModel<any>({
|
||||
default: undefined,
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<VRadioProps>(), {
|
||||
id: undefined,
|
||||
label: undefined,
|
||||
color: undefined,
|
||||
name: undefined,
|
||||
paddingless: false,
|
||||
})
|
||||
|
||||
const { field, id } = useVFieldContext({
|
||||
id: props.id,
|
||||
inherit: false,
|
||||
})
|
||||
|
||||
const internal = computed({
|
||||
get() {
|
||||
if (field?.value) {
|
||||
return field.value.value
|
||||
}
|
||||
else {
|
||||
return modelValue.value
|
||||
}
|
||||
},
|
||||
set(value: any) {
|
||||
if (field?.value) {
|
||||
field.value.setValue(value)
|
||||
}
|
||||
modelValue.value = value
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VLabel
|
||||
raw
|
||||
class="radio"
|
||||
:class="[
|
||||
props.solid ? 'is-solid' : 'is-outlined',
|
||||
props.square && 'is-square',
|
||||
props.color && `is-${props.color}`,
|
||||
props.paddingless && 'is-paddingless',
|
||||
]"
|
||||
>
|
||||
<input
|
||||
:id="id"
|
||||
v-model="internal"
|
||||
type="radio"
|
||||
:value="props.value"
|
||||
:name="props.name"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<span />
|
||||
<slot v-bind="{ field, id }">
|
||||
{{ props.label }}
|
||||
</slot>
|
||||
</VLabel>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
%controller {
|
||||
position: relative;
|
||||
font-family: var(--font);
|
||||
cursor: pointer;
|
||||
padding: 1em;
|
||||
|
||||
&::selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
input + span {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
background: var(--white);
|
||||
content: '';
|
||||
display: inline-block;
|
||||
margin-inline-end: 0.5rem;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
width: 1.4em;
|
||||
height: 1.4em;
|
||||
border: 1px solid color-mix(in oklab, var(--fade-grey), black 8%);
|
||||
transform: translate3d(0, 0, 0);
|
||||
backface-visibility: hidden;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
transform: scale(0);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (width >= 768px) {
|
||||
&:hover input + span {
|
||||
box-shadow: 0 2px 4px rgba(#000, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
input:active + span {
|
||||
box-shadow: 0 4px 8px rgba(#000, 0.15);
|
||||
}
|
||||
|
||||
input:checked + span::after {
|
||||
transform: translate(calc(var(--transform-direction) * -50%), -50%) scaleY(1)
|
||||
scaleX(calc(var(--transform-direction) * 1)) !important;
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.3s; // transition-all test
|
||||
}
|
||||
}
|
||||
|
||||
.radio {
|
||||
@extend %controller;
|
||||
|
||||
color: var(--light-text);
|
||||
|
||||
+ .radio {
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
&.is-paddingless {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
&.is-square {
|
||||
input + span {
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
input + span {
|
||||
background: color-mix(in oklab, var(--fade-grey), white 3%);
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
input + span {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
|
||||
&::after {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
input + span {
|
||||
border-color: var(--success);
|
||||
background: var(--success);
|
||||
|
||||
&::after {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
input + span {
|
||||
border-color: var(--info);
|
||||
background: var(--info);
|
||||
|
||||
&::after {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
input + span {
|
||||
border-color: var(--warning);
|
||||
background: var(--warning);
|
||||
|
||||
&::after {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
input + span {
|
||||
border-color: var(--danger);
|
||||
background: var(--danger);
|
||||
|
||||
&::after {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-outlined {
|
||||
&.is-primary {
|
||||
input:checked + span {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
input + span {
|
||||
&::after {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
input:checked + span {
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
input + span {
|
||||
&::after {
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
input:checked + span {
|
||||
border-color: var(--info);
|
||||
}
|
||||
|
||||
input + span {
|
||||
&::after {
|
||||
color: var(--info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
input:checked + span {
|
||||
border-color: var(--warning);
|
||||
}
|
||||
|
||||
input + span {
|
||||
&::after {
|
||||
color: var(--warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
input:checked + span {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
input + span {
|
||||
&::after {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input + span {
|
||||
border-radius: 100%;
|
||||
|
||||
&::after {
|
||||
background-size: contain;
|
||||
position: absolute;
|
||||
top: 49%;
|
||||
inset-inline-start: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
content: '\f111';
|
||||
font-family: 'Font Awesome\ 5 Free';
|
||||
font-weight: 900;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
input:focus + span,
|
||||
input:active + span {
|
||||
outline-offset: var(--accessibility-focus-outline-offset);
|
||||
outline-width: var(--accessibility-focus-outline-width);
|
||||
outline-color: var(--accessibility-focus-outline-color);
|
||||
outline-style: var(--accessibility-focus-outline-style);
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
%controller {
|
||||
input + span {
|
||||
background-color: color-mix(in oklab, var(--dark-sidebar), white 2%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 4%);
|
||||
|
||||
&::after {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
}
|
||||
|
||||
input + span {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 16%);
|
||||
}
|
||||
}
|
||||
|
||||
.radio {
|
||||
&.is-solid.is-primary {
|
||||
input + span {
|
||||
background-color: var(--primary) !important;
|
||||
border-color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-outlined.is-primary {
|
||||
input:checked + span {
|
||||
border-color: var(--primary) !important;
|
||||
|
||||
&::after {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
222
src/components/base/VRangeRating.vue
Normal file
222
src/components/base/VRangeRating.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<script setup lang="ts">
|
||||
const modelValue = defineModel<number | undefined>({
|
||||
default: undefined,
|
||||
})
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id?: string
|
||||
max?: number
|
||||
label?: string
|
||||
size?: 'small' | 'base' | 'medium' | 'large' | 'xlarge'
|
||||
readonly?: boolean
|
||||
disabled?: boolean
|
||||
}>(),
|
||||
{
|
||||
id: undefined,
|
||||
max: 5,
|
||||
size: 'base',
|
||||
label: undefined,
|
||||
readonly: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
const { field, id } = useVFieldContext({
|
||||
help: 'VRangeRating',
|
||||
id: props.id,
|
||||
})
|
||||
|
||||
const hasValue = computed(
|
||||
() => field?.value !== undefined || modelValue.value !== undefined,
|
||||
)
|
||||
const active = computed(() => !props.readonly && hasValue.value)
|
||||
|
||||
const internal = computed({
|
||||
get() {
|
||||
if (field?.value) {
|
||||
return field.value.value ?? 0
|
||||
}
|
||||
else {
|
||||
return modelValue.value ?? 0
|
||||
}
|
||||
},
|
||||
set(value: any) {
|
||||
if (field?.value) {
|
||||
field.value.setValue(value)
|
||||
}
|
||||
modelValue.value = value
|
||||
},
|
||||
})
|
||||
|
||||
const sizeStyle = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'small':
|
||||
return 'is-size-6'
|
||||
case 'base':
|
||||
return 'is-size-5'
|
||||
case 'medium':
|
||||
return 'is-size-4'
|
||||
case 'large':
|
||||
return 'is-size-3'
|
||||
case 'xlarge':
|
||||
return 'is-size-2'
|
||||
}
|
||||
})
|
||||
|
||||
const radiogroup = ref()
|
||||
function focus() {
|
||||
if (props.readonly) return
|
||||
if (props.disabled) return
|
||||
if (radiogroup.value) {
|
||||
radiogroup.value.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = ref()
|
||||
const { focused } = useFocusWithin(wrapper)
|
||||
onKeyStroke('ArrowLeft', (e) => {
|
||||
if (!focused.value) return
|
||||
if (props.disabled) return
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
if (internal.value > 0) {
|
||||
internal.value = internal.value - 1
|
||||
}
|
||||
})
|
||||
onKeyStroke('ArrowRight', (e) => {
|
||||
if (!focused.value) return
|
||||
if (props.disabled) return
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
if (internal.value < props.max) {
|
||||
internal.value = internal.value + 1
|
||||
}
|
||||
})
|
||||
|
||||
const highlighted = ref<number>()
|
||||
function highlightIndex(index: number) {
|
||||
if (props.readonly) return
|
||||
highlighted.value = index + 1
|
||||
}
|
||||
function unhighlight() {
|
||||
highlighted.value = undefined
|
||||
}
|
||||
|
||||
function selectIndex(index: number) {
|
||||
if (props.readonly) return
|
||||
if (props.disabled) return
|
||||
internal.value = index + 1
|
||||
}
|
||||
|
||||
function isStarSelected(index: number) {
|
||||
if (!hasValue.value) return 0
|
||||
|
||||
if (highlighted.value !== undefined) {
|
||||
return highlighted.value - index > 0
|
||||
}
|
||||
|
||||
return internal.value - index > 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="wrapper"
|
||||
class="rating-wrap"
|
||||
>
|
||||
<div
|
||||
v-if="props.label || 'label' in $slots"
|
||||
class="rating-label"
|
||||
>
|
||||
<slot name="label">
|
||||
<!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -->
|
||||
<label
|
||||
:id="`${id}-label`"
|
||||
:for="id"
|
||||
@click="focus"
|
||||
@keydown.enter="focus"
|
||||
>
|
||||
{{ props.label }}
|
||||
</label>
|
||||
</slot>
|
||||
</div>
|
||||
<div
|
||||
:id="id"
|
||||
ref="radiogroup"
|
||||
class="rating"
|
||||
:class="{
|
||||
'is-active': active,
|
||||
'is-highlighted': highlighted,
|
||||
[sizeStyle]: true,
|
||||
}"
|
||||
:aria-labelledby="
|
||||
props.label || ('label' in $slots && active) ? `${id}-label` : undefined
|
||||
"
|
||||
:tabindex="active ? 0 : undefined"
|
||||
:role="active ? 'radiogroup' : undefined"
|
||||
>
|
||||
<span
|
||||
v-for="(star, index) in props.max"
|
||||
:key="index"
|
||||
:role="active ? 'radio' : undefined"
|
||||
:aria-label="String(index + 1)"
|
||||
:aria-checked="active ? internal - index > 0 : undefined"
|
||||
aria-hidden="true"
|
||||
class="rating-star"
|
||||
:class="{
|
||||
'is-rating-star-selected': isStarSelected(index),
|
||||
}"
|
||||
@pointerenter.passive="() => highlightIndex(index)"
|
||||
@pointerleave.passive="() => unhighlight()"
|
||||
@click.passive="() => selectIndex(index)"
|
||||
>
|
||||
<slot
|
||||
v-bind="{
|
||||
value: index + 1,
|
||||
isSelected: isStarSelected(index),
|
||||
isHighlighted: index + 1 === highlighted,
|
||||
}"
|
||||
>
|
||||
<VIcon icon="ph:star-fill" />
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rating-wrap {
|
||||
text-align: inset-inline-end;
|
||||
|
||||
.rating-label {
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
.rating {
|
||||
display: inline-flex;
|
||||
|
||||
.rating-star {
|
||||
color: color-mix(in oklab, var(--widget-grey), black 8%);
|
||||
|
||||
&.is-rating-star-selected {
|
||||
color: var(--yellow) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
[role='radio'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark .rating-wrap {
|
||||
.rating {
|
||||
.rating-star {
|
||||
color: color-mix(in oklab, var(--dark-sidebar), white 22%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
60
src/components/base/VSelect.vue
Normal file
60
src/components/base/VSelect.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
export interface VSelectProps {
|
||||
raw?: boolean
|
||||
multiple?: boolean
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const modelValue = defineModel<any>({
|
||||
default: '',
|
||||
})
|
||||
const props = defineProps<VSelectProps>()
|
||||
const attrs = useAttrs()
|
||||
|
||||
const { field, id } = useVFieldContext({
|
||||
create: false,
|
||||
help: 'VSelect',
|
||||
})
|
||||
|
||||
const internal = computed({
|
||||
get() {
|
||||
if (field?.value) {
|
||||
return field.value.value
|
||||
}
|
||||
else {
|
||||
return modelValue.value
|
||||
}
|
||||
},
|
||||
set(value: any) {
|
||||
if (field?.value) {
|
||||
field.value.setValue(value)
|
||||
}
|
||||
modelValue.value = value
|
||||
},
|
||||
})
|
||||
|
||||
const classes = computed(() => {
|
||||
if (props.raw) return []
|
||||
|
||||
return ['select', props.multiple && 'is-multiple']
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<select
|
||||
:id="id"
|
||||
v-bind="attrs"
|
||||
v-model="internal"
|
||||
:name="id"
|
||||
:multiple="props.multiple"
|
||||
@change="field?.handleChange"
|
||||
@blur="field?.handleBlur"
|
||||
>
|
||||
<slot v-bind="{ selected: internal, id }" />
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
397
src/components/base/VSnack.vue
Normal file
397
src/components/base/VSnack.vue
Normal file
@@ -0,0 +1,397 @@
|
||||
<script setup lang="ts">
|
||||
export type VSnackColor = 'primary' | 'success' | 'info' | 'warning' | 'danger'
|
||||
export type VSnackSize = 'small'
|
||||
export interface VSnackProps {
|
||||
title: string
|
||||
icon?: string
|
||||
image?: string
|
||||
placeholder?: string
|
||||
color?: VSnackColor
|
||||
size?: VSnackSize
|
||||
solid?: boolean
|
||||
white?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VSnackProps>(), {
|
||||
icon: undefined,
|
||||
image: undefined,
|
||||
color: undefined,
|
||||
size: undefined,
|
||||
placeholder: 'https://via.placeholder.com/50x50',
|
||||
})
|
||||
|
||||
function placeholderHandler(event: Event) {
|
||||
const target = event.target as HTMLImageElement
|
||||
target.src = props.placeholder
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="snack"
|
||||
:class="[props.white && 'is-white', props.size && `is-${props.size}`]"
|
||||
>
|
||||
<div
|
||||
v-if="props.icon"
|
||||
class="snack-media is-icon"
|
||||
:class="[props.color && `is-${props.color}`, props.solid && `is-solid`]"
|
||||
>
|
||||
<VIcon :icon="props.icon" class="snack-icon" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="props.image"
|
||||
class="snack-media"
|
||||
>
|
||||
<img
|
||||
class="avatar"
|
||||
:src="props.image"
|
||||
alt=""
|
||||
@error.once="placeholderHandler"
|
||||
>
|
||||
</div>
|
||||
<span class="snack-text">
|
||||
<slot name="title">{{ props.title }}</slot>
|
||||
</span>
|
||||
<span class="snack-action">
|
||||
<slot />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.snacks {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.snack {
|
||||
margin: 0 8px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.snack {
|
||||
display: inline-block;
|
||||
background: color-mix(in oklab, var(--fade-grey), white 2%);
|
||||
height: 38px;
|
||||
width: auto;
|
||||
border-radius: 500px;
|
||||
border: 1px solid color-mix(in oklab, var(--fade-grey), black 3%);
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--light-box-shadow);
|
||||
}
|
||||
|
||||
&.is-white {
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
&.is-small {
|
||||
height: 30px;
|
||||
|
||||
.snack-media {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
margin-inline-end: 4px;
|
||||
|
||||
&.is-icon {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
|
||||
.iconify {
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.fas,
|
||||
.far,
|
||||
.fad,
|
||||
.fal,
|
||||
.fab {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.lnil,
|
||||
.lnir {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.snack-text {
|
||||
font-size: 0.9rem;
|
||||
top: -12px;
|
||||
}
|
||||
|
||||
.snack-action {
|
||||
top: -9px;
|
||||
margin: 0 10px 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.snack-media {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
display: inline-block;
|
||||
margin-inline-end: 6px;
|
||||
|
||||
&.is-icon {
|
||||
position: relative;
|
||||
inset-inline-start: -1px;
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
background: var(--white);
|
||||
border: 1px solid color-mix(in oklab, var(--fade-grey), black 3%);
|
||||
border-radius: var(--radius-rounded);
|
||||
|
||||
&.is-solid {
|
||||
.fas,
|
||||
.far,
|
||||
.fad,
|
||||
.fal,
|
||||
.fab,
|
||||
.lnil,
|
||||
.lnir {
|
||||
color: var(--white) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
border-color: var(--primary);
|
||||
|
||||
&.is-solid {
|
||||
background: var(--primary);
|
||||
|
||||
.iconify {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.fas,
|
||||
.far,
|
||||
.fad,
|
||||
.fal,
|
||||
.fab,
|
||||
.lnil,
|
||||
.lnir {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
border-color: var(--success);
|
||||
|
||||
&.is-solid {
|
||||
background: var(--success);
|
||||
|
||||
.iconify {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.fas,
|
||||
.far,
|
||||
.fad,
|
||||
.fal,
|
||||
.fab,
|
||||
.lnil,
|
||||
.lnir {
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
border-color: var(--info);
|
||||
|
||||
&.is-solid {
|
||||
background: var(--info);
|
||||
|
||||
.iconify {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--info);
|
||||
}
|
||||
|
||||
.fas,
|
||||
.far,
|
||||
.fad,
|
||||
.fal,
|
||||
.fab,
|
||||
.lnil,
|
||||
.lnir {
|
||||
color: var(--info);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
border-color: var(--warning);
|
||||
|
||||
&.is-solid {
|
||||
background: var(--warning);
|
||||
|
||||
.iconify {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.fas,
|
||||
.far,
|
||||
.fad,
|
||||
.fal,
|
||||
.fab,
|
||||
.lnil,
|
||||
.lnir {
|
||||
color: var(--warning);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
border-color: var(--danger);
|
||||
|
||||
&.is-solid {
|
||||
background: var(--danger);
|
||||
|
||||
.iconify {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.fas,
|
||||
.far,
|
||||
.fad,
|
||||
.fal,
|
||||
.fab,
|
||||
.lnil,
|
||||
.lnir {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
.snack-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
inset-inline-start: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
font-size: 18px;
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
.fas,
|
||||
.far,
|
||||
.fad,
|
||||
.fal,
|
||||
.fab {
|
||||
font-size: 15px;
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
.lnil,
|
||||
.lnir {
|
||||
font-size: 18px;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
display: inline-block;
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
border-radius: var(--radius-rounded);
|
||||
}
|
||||
}
|
||||
|
||||
.snack-text {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: -15px;
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
.snack-action {
|
||||
position: relative;
|
||||
top: -14px;
|
||||
display: inline-block;
|
||||
margin: 0 16px 0 10px;
|
||||
cursor: pointer;
|
||||
|
||||
.iconify {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.snack {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 2%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 4%);
|
||||
|
||||
.snack-media {
|
||||
&.is-icon {
|
||||
&:not(.is-solid) {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 4%);
|
||||
}
|
||||
|
||||
&.is-primary:not(.is-solid) {
|
||||
border-color: var(--primary);
|
||||
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.fas,
|
||||
.far,
|
||||
.fad,
|
||||
.fab,
|
||||
.fal,
|
||||
.lnil,
|
||||
.lnir {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-primary.is-solid {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.snack-text {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
541
src/components/base/VSwitchBlock.vue
Normal file
541
src/components/base/VSwitchBlock.vue
Normal file
@@ -0,0 +1,541 @@
|
||||
<script setup lang="ts">
|
||||
export type VSwitchBlockColor = 'primary' | 'info' | 'success' | 'warning' | 'danger'
|
||||
|
||||
export interface VSwitchBlockProps {
|
||||
label?: string
|
||||
color?: VSwitchBlockColor
|
||||
thin?: boolean
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const modelValue = defineModel<boolean>({
|
||||
default: false,
|
||||
})
|
||||
const props = withDefaults(defineProps<VSwitchBlockProps>(), {
|
||||
label: undefined,
|
||||
color: undefined,
|
||||
})
|
||||
|
||||
const { field, id } = useVFieldContext({
|
||||
create: false,
|
||||
help: 'VSwitchBlock',
|
||||
})
|
||||
|
||||
const internal = computed({
|
||||
get() {
|
||||
if (field?.value) {
|
||||
return field.value.value
|
||||
}
|
||||
else {
|
||||
return modelValue.value
|
||||
}
|
||||
},
|
||||
set(value: any) {
|
||||
if (field?.value) {
|
||||
field.value.setValue(value)
|
||||
}
|
||||
modelValue.value = value
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
(props.label || 'default' in $slots) && 'switch-block',
|
||||
props.thin && (props.label || 'default' in $slots) && 'thin-switch-block',
|
||||
]"
|
||||
>
|
||||
<template v-if="props.thin">
|
||||
<VLabel
|
||||
raw
|
||||
class="thin-switch"
|
||||
tabindex="0"
|
||||
:class="[props.color && `is-${props.color}`]"
|
||||
>
|
||||
<input
|
||||
:id="id"
|
||||
v-model="internal"
|
||||
:true-value="true"
|
||||
:false-value="false"
|
||||
class="input"
|
||||
type="checkbox"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<div class="slider" />
|
||||
</VLabel>
|
||||
</template>
|
||||
<template v-else>
|
||||
<VLabel
|
||||
raw
|
||||
class="form-switch"
|
||||
:class="[props.color && `is-${props.color}`]"
|
||||
>
|
||||
<input
|
||||
:id="id"
|
||||
v-model="internal"
|
||||
:true-value="true"
|
||||
:false-value="false"
|
||||
type="checkbox"
|
||||
class="is-switch"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<i aria-hidden="true" />
|
||||
</VLabel>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="props.label || 'default' in $slots"
|
||||
class="text"
|
||||
>
|
||||
<VLabel raw>
|
||||
<span>
|
||||
<slot>
|
||||
{{ props.label }}
|
||||
</slot>
|
||||
</span>
|
||||
</VLabel>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.form-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&:focus-within {
|
||||
border-radius: 50px;
|
||||
outline-offset: var(--accessibility-focus-outline-offset);
|
||||
outline-width: var(--accessibility-focus-outline-width);
|
||||
outline-style: var(--accessibility-focus-outline-style);
|
||||
outline-color: var(--accessibility-focus-outline-color);
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 46px;
|
||||
height: 26px;
|
||||
background-color: #e6e6e6;
|
||||
border-radius: 23px;
|
||||
vertical-align: text-bottom;
|
||||
transition: all 0.3s linear;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
width: 42px;
|
||||
height: 22px;
|
||||
background-color: var(--white);
|
||||
border-radius: 11px;
|
||||
transform: translate3d(calc(var(--transform-direction) * 2px), 2px, 0)
|
||||
scale3d(1, 1, 1);
|
||||
transition: all 0.25s linear;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background-color: var(--white);
|
||||
border-radius: 11px;
|
||||
box-shadow: 0 2px 2px rgb(0 0 0 / 24%);
|
||||
transform: translate3d(calc(var(--transform-direction) * 2px), 2px, 0);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
i::after {
|
||||
width: 28px;
|
||||
transform: translate3d(calc(var(--transform-direction) * 2px), 2px, 0);
|
||||
}
|
||||
|
||||
input {
|
||||
&:checked + i::after {
|
||||
transform: translate3d(calc(var(--transform-direction) * 16px), 2px, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:checked + i {
|
||||
background-color: var(--light-text);
|
||||
|
||||
&::before {
|
||||
transform: translate3d(calc(var(--transform-direction) * 18px), 2px, 0)
|
||||
scale3d(0, 0, 0);
|
||||
}
|
||||
|
||||
&::after {
|
||||
transform: translate3d(calc(var(--transform-direction) * 22px), 2px, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--muted-grey);
|
||||
position: relative;
|
||||
top: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
.switch-block {
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
margin-inline-start: 6px;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.form-switch {
|
||||
&.is-primary {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--primary) !important;
|
||||
|
||||
&::after {
|
||||
background: var(--white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--success) !important;
|
||||
|
||||
&::after {
|
||||
background: var(--white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--info) !important;
|
||||
|
||||
&::after {
|
||||
background: var(--white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--warning) !important;
|
||||
|
||||
&::after {
|
||||
background: var(--white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--danger) !important;
|
||||
|
||||
&::after {
|
||||
background: var(--white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
background: var(--dark-sidebar) !important;
|
||||
|
||||
&::before {
|
||||
background: var(--dark-sidebar) !important;
|
||||
}
|
||||
|
||||
&::after {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 22%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
&:checked + i {
|
||||
&::after {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 55%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thin-switch {
|
||||
display: block;
|
||||
margin-inline-start: 8px;
|
||||
|
||||
&:focus-visible .slider::after {
|
||||
border-radius: 50px;
|
||||
outline-offset: var(--accessibility-focus-outline-offset);
|
||||
outline-width: var(--accessibility-focus-outline-width);
|
||||
outline-style: var(--accessibility-focus-outline-style);
|
||||
outline-color: var(--accessibility-focus-outline-color);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
.input:checked ~ .slider {
|
||||
background: color-mix(in oklab, var(--primary), white 40%);
|
||||
|
||||
&::after {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
.input:checked ~ .slider {
|
||||
background: color-mix(in oklab, var(--success), white 40%);
|
||||
|
||||
&::after {
|
||||
background: var(--success);
|
||||
border-color: var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
.input:checked ~ .slider {
|
||||
background: color-mix(in oklab, var(--info), white 40%);
|
||||
|
||||
&::after {
|
||||
background: var(--info);
|
||||
border-color: var(--info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
.input:checked ~ .slider {
|
||||
background: color-mix(in oklab, var(--warning), white 40%);
|
||||
|
||||
&::after {
|
||||
background: var(--warning);
|
||||
border-color: var(--warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
.input:checked ~ .slider {
|
||||
background: color-mix(in oklab, var(--danger), white 40%);
|
||||
|
||||
&::after {
|
||||
background: var(--danger);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 8px;
|
||||
width: 32px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: #c5c5c5;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&::after {
|
||||
background: var(--light-grey);
|
||||
position: absolute;
|
||||
inset-inline-start: -8px;
|
||||
top: -8.5px;
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--radius-rounded);
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 2px 2px rgba(#000, 0.2);
|
||||
content: '';
|
||||
transition: all 0.3s; // transition-all test
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
margin-inline-end: 7px;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: none;
|
||||
|
||||
~ .label {
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
|
||||
&:checked ~ .slider {
|
||||
&::after {
|
||||
inset-inline-start: 32px - 24px + 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input:checked ~ .slider {
|
||||
&::after {
|
||||
background: var(--white);
|
||||
border: 1px solid var(--fade-grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thin-switch-block {
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
margin-inline-start: 16px;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
position: relative;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.thin-switch {
|
||||
&.is-primary {
|
||||
.input:checked ~ .slider {
|
||||
background: color-mix(in oklab, var(--primary), white 20%);
|
||||
|
||||
&::after {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
.input:checked ~ .slider {
|
||||
&::after {
|
||||
background: var(--success);
|
||||
border-color: var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
.input:checked ~ .slider {
|
||||
&::after {
|
||||
background: var(--info);
|
||||
border-color: var(--info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
.input:checked ~ .slider {
|
||||
&::after {
|
||||
background: var(--warning);
|
||||
border-color: var(--warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
.input:checked ~ .slider {
|
||||
&::after {
|
||||
background: var(--danger);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
background: var(--dark-sidebar);
|
||||
|
||||
&::after {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 22%);
|
||||
}
|
||||
}
|
||||
|
||||
.input:checked ~ .slider {
|
||||
&::after {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 55%);
|
||||
border: color-mix(in oklab, var(--dark-sidebar), white 55%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
552
src/components/base/VSwitchSegment.vue
Normal file
552
src/components/base/VSwitchSegment.vue
Normal file
@@ -0,0 +1,552 @@
|
||||
<script setup lang="ts">
|
||||
export type VSwitchSegmentColor = 'primary' | 'info' | 'success' | 'warning' | 'danger'
|
||||
|
||||
export interface VSwitchSegmentProps {
|
||||
labelTrue?: string
|
||||
labelFalse?: string
|
||||
color?: VSwitchSegmentColor
|
||||
}
|
||||
|
||||
const modelValue = defineModel<boolean>({
|
||||
default: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<VSwitchSegmentProps>(), {
|
||||
labelTrue: undefined,
|
||||
labelFalse: undefined,
|
||||
color: undefined,
|
||||
})
|
||||
|
||||
const { field, id } = useVFieldContext({
|
||||
create: false,
|
||||
help: 'VSwitchSegment',
|
||||
})
|
||||
|
||||
const internal = computed({
|
||||
get() {
|
||||
if (field?.value) {
|
||||
return field.value.value
|
||||
}
|
||||
else {
|
||||
return modelValue.value
|
||||
}
|
||||
},
|
||||
set(value: any) {
|
||||
if (field?.value) {
|
||||
field.value.setValue(value)
|
||||
}
|
||||
modelValue.value = value
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="switch-segment">
|
||||
<VLabel
|
||||
v-if="props.labelFalse || 'label-false' in $slots"
|
||||
raw
|
||||
class="is-label"
|
||||
>
|
||||
<slot name="label-false">
|
||||
{{ props.labelFalse }}
|
||||
</slot>
|
||||
</VLabel>
|
||||
<VLabel
|
||||
raw
|
||||
class="form-switch"
|
||||
:class="[props.color && `is-${props.color}`]"
|
||||
>
|
||||
<input
|
||||
:id="id"
|
||||
v-model="internal"
|
||||
:true-value="true"
|
||||
:false-value="false"
|
||||
v-bind="$attrs"
|
||||
type="checkbox"
|
||||
class="is-switch"
|
||||
>
|
||||
<i aria-hidden="true" />
|
||||
</VLabel>
|
||||
<VLabel
|
||||
v-if="props.labelTrue || 'label-true' in $slots"
|
||||
raw
|
||||
class="is-label"
|
||||
>
|
||||
<slot name="label-true">
|
||||
{{ props.labelTrue }}
|
||||
</slot>
|
||||
</VLabel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.form-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&:focus-within {
|
||||
border-radius: 50px;
|
||||
outline-offset: var(--accessibility-focus-outline-offset);
|
||||
outline-width: var(--accessibility-focus-outline-width);
|
||||
outline-style: var(--accessibility-focus-outline-style);
|
||||
outline-color: var(--accessibility-focus-outline-color);
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 46px;
|
||||
height: 26px;
|
||||
background-color: #e6e6e6;
|
||||
border-radius: 23px;
|
||||
vertical-align: text-bottom;
|
||||
transition: all 0.3s linear;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
width: 42px;
|
||||
height: 22px;
|
||||
background-color: var(--white);
|
||||
border-radius: 11px;
|
||||
transform: translate3d(calc(var(--transform-direction) * 2px), 2px, 0)
|
||||
scale3d(calc(var(--transform-direction) * 1), 1, 1);
|
||||
transition: all 0.25s linear;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background-color: var(--white);
|
||||
border-radius: 11px;
|
||||
box-shadow: 0 2px 2px rgb(0 0 0 / 24%);
|
||||
transform: translate3d(calc(var(--transform-direction) * 2px), 2px, 0);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
i::after {
|
||||
width: 28px;
|
||||
transform: translate3d(calc(var(--transform-direction) * 2px), 2px, 0);
|
||||
}
|
||||
|
||||
input {
|
||||
&:checked + i::after {
|
||||
transform: translate3d(calc(var(--transform-direction) * 16px), 2px, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:checked + i {
|
||||
background-color: var(--light-text);
|
||||
|
||||
&::before {
|
||||
transform: translate3d(calc(var(--transform-direction) * 18px), 2px, 0)
|
||||
scale3d(0, 0, 0);
|
||||
}
|
||||
|
||||
&::after {
|
||||
transform: translate3d(calc(var(--transform-direction) * 22px), 2px, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--muted-grey);
|
||||
position: relative;
|
||||
top: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
.switch-block {
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
margin-inline-start: 6px;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.form-switch {
|
||||
&.is-primary {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--primary) !important;
|
||||
|
||||
&::after {
|
||||
background: var(--white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--success) !important;
|
||||
|
||||
&::after {
|
||||
background: var(--white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--info) !important;
|
||||
|
||||
&::after {
|
||||
background: var(--white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--warning) !important;
|
||||
|
||||
&::after {
|
||||
background: var(--white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
input {
|
||||
&:checked + i {
|
||||
background-color: var(--danger) !important;
|
||||
|
||||
&::after {
|
||||
background: var(--white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
background: var(--dark-sidebar) !important;
|
||||
|
||||
&::before {
|
||||
background: var(--dark-sidebar) !important;
|
||||
}
|
||||
|
||||
&::after {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 22%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
&:checked + i {
|
||||
&::after {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 55%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thin-switch {
|
||||
display: block;
|
||||
margin-inline-start: 8px;
|
||||
|
||||
&:focus-visible .slider::after {
|
||||
border-radius: 50px;
|
||||
outline-offset: var(--accessibility-focus-outline-offset);
|
||||
outline-width: var(--accessibility-focus-outline-width);
|
||||
outline-style: var(--accessibility-focus-outline-style);
|
||||
outline-color: var(--accessibility-focus-outline-color);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
.input:checked ~ .slider {
|
||||
background: color-mix(in oklab, var(--primary), white 20%);
|
||||
|
||||
&::after {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
.input:checked ~ .slider {
|
||||
background: color-mix(in oklab, var(--success), white 20%);
|
||||
|
||||
&::after {
|
||||
background: var(--success);
|
||||
border-color: var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
.input:checked ~ .slider {
|
||||
background: color-mix(in oklab, var(--info), white 20%);
|
||||
|
||||
&::after {
|
||||
background: var(--info);
|
||||
border-color: var(--info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
.input:checked ~ .slider {
|
||||
background: color-mix(in oklab, var(--warning), white 20%);
|
||||
|
||||
&::after {
|
||||
background: var(--warning);
|
||||
border-color: var(--warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
.input:checked ~ .slider {
|
||||
background: color-mix(in oklab, var(--danger), white 20%);
|
||||
|
||||
&::after {
|
||||
background: var(--danger);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 8px;
|
||||
width: 32px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: #c5c5c5;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&::after {
|
||||
background: var(--light-grey);
|
||||
position: absolute;
|
||||
inset-inline-start: -8px;
|
||||
top: calc((7px - 24px) / 2);
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--radius-rounded);
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 2px 2px rgba(#000, 0.2);
|
||||
content: '';
|
||||
transition: all 0.3s; // transition-all test
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
margin-inline-end: 7px;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: none;
|
||||
|
||||
~ .label {
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
|
||||
&:checked ~ .slider {
|
||||
&::after {
|
||||
inset-inline-start: 32px - 24px + 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input:checked ~ .slider {
|
||||
&::after {
|
||||
background: var(--white);
|
||||
border: 1px solid var(--fade-grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thin-switch-block {
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
margin-inline-start: 16px;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
position: relative;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.thin-switch {
|
||||
&.is-primary {
|
||||
.input:checked ~ .slider {
|
||||
background: color-mix(in oklab, var(--primary), white 20%);
|
||||
|
||||
&::after {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
.input:checked ~ .slider {
|
||||
&::after {
|
||||
background: var(--success);
|
||||
border-color: var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
.input:checked ~ .slider {
|
||||
&::after {
|
||||
background: var(--info);
|
||||
border-color: var(--info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
.input:checked ~ .slider {
|
||||
&::after {
|
||||
background: var(--warning);
|
||||
border-color: var(--warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
.input:checked ~ .slider {
|
||||
&::after {
|
||||
background: var(--danger);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
background: var(--dark-sidebar);
|
||||
|
||||
&::after {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 22%);
|
||||
}
|
||||
}
|
||||
|
||||
.input:checked ~ .slider {
|
||||
&::after {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 55%);
|
||||
border: color-mix(in oklab, var(--dark-sidebar), white 55%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.switch-segment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.is-label {
|
||||
font-family: var(--font);
|
||||
font-size: 0.9rem;
|
||||
|
||||
&:first-child {
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
|
||||
.form-switch {
|
||||
transform: scale(0.7);
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.switch-segment {
|
||||
.is-label {
|
||||
&:first-child {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
702
src/components/base/VTabs.vue
Normal file
702
src/components/base/VTabs.vue
Normal file
@@ -0,0 +1,702 @@
|
||||
<script setup lang="ts">
|
||||
import type { RouteLocationAsString } from 'unplugin-vue-router'
|
||||
|
||||
export type VTabsType = 'boxed' | 'toggle' | 'rounded'
|
||||
export type VTabsAlign = 'centered' | 'right'
|
||||
export interface VTabsItem {
|
||||
label: string
|
||||
value: string
|
||||
icon?: string
|
||||
to?: RouteLocationAsString
|
||||
}
|
||||
export interface VTabsProps {
|
||||
tabs: VTabsItem[]
|
||||
selected?: string
|
||||
type?: VTabsType
|
||||
align?: VTabsAlign
|
||||
slider?: boolean
|
||||
slow?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:selected', value: any): void
|
||||
}>()
|
||||
|
||||
const props = withDefaults(defineProps<VTabsProps>(), {
|
||||
selected: undefined,
|
||||
type: undefined,
|
||||
align: undefined,
|
||||
})
|
||||
|
||||
const activeValue = ref(props.selected || props.tabs?.[0]?.value)
|
||||
const sliderClass = computed(() => {
|
||||
if (!props.slider) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (props.type === 'rounded') {
|
||||
if (props.tabs.length === 3) {
|
||||
return 'is-triple-slider'
|
||||
}
|
||||
if (props.tabs.length === 2) {
|
||||
return 'is-slider'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!props.type) {
|
||||
if (props.tabs.length === 3) {
|
||||
return 'is-squared is-triple-slider'
|
||||
}
|
||||
if (props.tabs.length === 2) {
|
||||
return 'is-squared is-slider'
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
|
||||
function toggle(value: string) {
|
||||
if (props.disabled) return
|
||||
activeValue.value = value
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.selected,
|
||||
(value) => {
|
||||
activeValue.value = value ?? ''
|
||||
},
|
||||
)
|
||||
|
||||
watch(activeValue, (value) => {
|
||||
emit('update:selected', value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="tabs-wrapper"
|
||||
:class="[sliderClass]"
|
||||
>
|
||||
<div class="tabs-inner">
|
||||
<div
|
||||
class="tabs"
|
||||
:class="[
|
||||
props.align === 'centered' && 'is-centered',
|
||||
props.align === 'right' && 'is-right',
|
||||
props.type === 'rounded' && !props.slider && 'is-toggle is-toggle-rounded',
|
||||
props.type === 'toggle' && 'is-toggle',
|
||||
props.type === 'boxed' && 'is-boxed',
|
||||
props.disabled && 'is-disabled',
|
||||
]"
|
||||
>
|
||||
<ul>
|
||||
<li
|
||||
v-for="(tab, key) in tabs"
|
||||
:key="key"
|
||||
:class="[activeValue === tab.value && 'is-active']"
|
||||
>
|
||||
<slot
|
||||
name="tab-link"
|
||||
v-bind="{
|
||||
activeValue,
|
||||
tab,
|
||||
key,
|
||||
toggle,
|
||||
}"
|
||||
>
|
||||
<a
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@keydown.prevent.enter="() => toggle(tab.value)"
|
||||
@click.prevent="() => toggle(tab.value)"
|
||||
>
|
||||
<VIcon
|
||||
v-if="tab.icon"
|
||||
:icon="tab.icon"
|
||||
/>
|
||||
<span>
|
||||
<slot
|
||||
name="tab-link-label"
|
||||
v-bind="{
|
||||
activeValue,
|
||||
tab,
|
||||
key,
|
||||
toggle,
|
||||
}"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</slot>
|
||||
</span>
|
||||
</a>
|
||||
</slot>
|
||||
</li>
|
||||
<li
|
||||
v-if="sliderClass"
|
||||
class="tab-naver"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="'tab' in $slots" class="tab-content is-active">
|
||||
<Transition
|
||||
:name="props.slow ? 'fade-slow' : 'fade-fast'"
|
||||
mode="out-in"
|
||||
>
|
||||
<slot
|
||||
name="tab"
|
||||
v-bind="{
|
||||
activeValue,
|
||||
}"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/*! _tabs.scss | Vuero | Css ninja 2020-2024 */
|
||||
|
||||
/*
|
||||
1. Tabs
|
||||
2. Tabs Dark mode
|
||||
3. Tab Content
|
||||
4. Sliding tabs 2X
|
||||
5. Sliding tabs 3X
|
||||
6. Sliding tabs Dark mode
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
1. Tabs
|
||||
========================================================================== */
|
||||
|
||||
.tabs {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&.is-toggle {
|
||||
li {
|
||||
&:first-child {
|
||||
a {
|
||||
border-inline-end: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
a {
|
||||
border-inline-start: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
a {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&:hover {
|
||||
border-color: #dbdbdb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
&.is-active {
|
||||
a {
|
||||
border-bottom-color: var(--primary);
|
||||
color: var(--primary);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-bottom-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
font-family: var(--font);
|
||||
border-bottom-width: 2px;
|
||||
color: var(--placeholder);
|
||||
border-bottom-color: transparent;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--light-text);
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-inline-end: 6px;
|
||||
}
|
||||
|
||||
.fas,
|
||||
.fal,
|
||||
.far,
|
||||
.fad,
|
||||
.fab {
|
||||
margin-inline-end: 6px;
|
||||
}
|
||||
|
||||
.lnil,
|
||||
.lnir {
|
||||
font-size: 20px;
|
||||
margin-inline-end: 6px;
|
||||
}
|
||||
|
||||
small {
|
||||
margin-inline-start: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
2. Tabs Dark mode
|
||||
========================================================================== */
|
||||
|
||||
.is-dark {
|
||||
.tabs {
|
||||
&.is-boxed {
|
||||
li {
|
||||
&.is-active {
|
||||
a,
|
||||
a:hover {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 1%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 4%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-toggle {
|
||||
li {
|
||||
&.is-active {
|
||||
a,
|
||||
a:hover {
|
||||
background: var(--primary) !important;
|
||||
border-color: var(--primary);
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 4%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
border-bottom-color: color-mix(in oklab, var(--dark-sidebar), white 16%);
|
||||
}
|
||||
|
||||
li {
|
||||
&.is-active {
|
||||
a {
|
||||
border-bottom-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
3. Tab Content
|
||||
========================================================================== */
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
animation-name: fadeInLeft;
|
||||
animation-duration: 0.5s;
|
||||
|
||||
&.is-active {
|
||||
display: block;
|
||||
|
||||
&.is-spaced {
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-spaced {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
&.is-spaced-lg {
|
||||
margin-top: 40px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
4. Sliding tabs 2X
|
||||
========================================================================== */
|
||||
|
||||
.tabs-wrapper,
|
||||
.tabs-wrapper-alt {
|
||||
&.is-slider {
|
||||
&.is-inverted {
|
||||
> .tabs-inner > .tabs {
|
||||
background: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-squared {
|
||||
> .tabs-inner > .tabs,
|
||||
.tab-naver {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .tabs-inner > .tabs {
|
||||
position: relative;
|
||||
background: color-mix(in oklab, var(--fade-grey), white 2%);
|
||||
border: 1px solid var(--fade-grey);
|
||||
max-width: 185px;
|
||||
height: 35px;
|
||||
border-bottom: none;
|
||||
border-radius: 500px;
|
||||
|
||||
ul {
|
||||
border-bottom: none;
|
||||
|
||||
&.is-profile {
|
||||
li {
|
||||
a {
|
||||
color: var(--smoke-white) !important;
|
||||
}
|
||||
|
||||
&.is-active a {
|
||||
color: var(--dark-text) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
width: 50%;
|
||||
|
||||
a {
|
||||
color: var(--light-text);
|
||||
font-family: var(--font);
|
||||
height: 40px;
|
||||
border-bottom: none;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
|
||||
span {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active a {
|
||||
color: var(--white);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&.is-active ~ .tab-naver {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
&.is-active ~ .tab-naver {
|
||||
margin-inline-start: 50% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-centered {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-naver {
|
||||
inset-inline-start: 0;
|
||||
background: var(--primary);
|
||||
position: absolute;
|
||||
top: 0.5px;
|
||||
display: block;
|
||||
height: 32px;
|
||||
transition: all 0.3s; // transition-all test
|
||||
z-index: 4;
|
||||
border-radius: 50px;
|
||||
|
||||
&.is-profile {
|
||||
background: var(--smoke-white) !important;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
margin-inline-start: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
5. Sliding tabs 3X
|
||||
========================================================================== */
|
||||
|
||||
.tabs-wrapper,
|
||||
.tabs-wrapper-alt {
|
||||
&.is-triple-slider {
|
||||
&.is-inverted {
|
||||
> .tabs-inner > .tabs {
|
||||
background: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-squared {
|
||||
> .tabs-inner > .tabs,
|
||||
.tab-naver {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .tabs-inner > .tabs {
|
||||
position: relative;
|
||||
background: color-mix(in oklab, var(--fade-grey), white 2%);
|
||||
border: 1px solid var(--fade-grey);
|
||||
max-width: 280px;
|
||||
height: 35px;
|
||||
border-bottom: none;
|
||||
border-radius: 500px;
|
||||
|
||||
ul {
|
||||
border-bottom: none;
|
||||
|
||||
&.is-profile {
|
||||
li {
|
||||
a {
|
||||
color: var(--smoke-white) !important;
|
||||
}
|
||||
|
||||
&.is-active a {
|
||||
color: var(--dark-text) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
width: 33.3%;
|
||||
|
||||
a {
|
||||
color: var(--light-text);
|
||||
font-family: var(--font);
|
||||
font-weight: 400;
|
||||
height: 40px;
|
||||
border-bottom: none;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
|
||||
span {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active a {
|
||||
color: var(--white);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&.is-active ~ .tab-naver {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
&.is-active ~ .tab-naver {
|
||||
margin-inline-start: 33% !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
&.is-active ~ .tab-naver {
|
||||
margin-inline-start: 66.6%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-naver {
|
||||
position: absolute;
|
||||
top: 0.5px;
|
||||
inset-inline-start: 0;
|
||||
display: block;
|
||||
width: 33.3% !important;
|
||||
background: var(--primary);
|
||||
height: 32px;
|
||||
transition: all 0.3s; // transition-all test
|
||||
z-index: 4;
|
||||
border-radius: 50px;
|
||||
|
||||
&.is-profile {
|
||||
background: var(--smoke-white) !important;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
margin-inline-start: 48%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
6. Sliding tabs Dark mode
|
||||
========================================================================== */
|
||||
|
||||
.is-dark {
|
||||
.tabs-wrapper {
|
||||
&.is-slider,
|
||||
&.is-triple-slider {
|
||||
&.is-inverted {
|
||||
> .tabs-inner > .tabs {
|
||||
border: 1px solid color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 2%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
> .tabs-inner > .tabs {
|
||||
border: 1px solid color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 2%) !important;
|
||||
|
||||
.tab-naver {
|
||||
background: var(--primary) !important;
|
||||
}
|
||||
|
||||
ul {
|
||||
border: none;
|
||||
}
|
||||
|
||||
li {
|
||||
&.is-active {
|
||||
a {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
4. Vertical tabs
|
||||
========================================================================== */
|
||||
|
||||
@media only screen and (width <= 767px) {
|
||||
.vertical-tabs-wrapper {
|
||||
.tabs {
|
||||
ul {
|
||||
li {
|
||||
&.is-active {
|
||||
a {
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
.tab-content {
|
||||
padding-top: 12px;
|
||||
display: none;
|
||||
animation: fadeInLeft 0.5s;
|
||||
|
||||
&.is-active {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) {
|
||||
.vertical-tabs-wrapper {
|
||||
display: flex;
|
||||
|
||||
.tabs {
|
||||
// min-width: 25%;
|
||||
// max-width: 25%;
|
||||
margin-inline-end: 30px;
|
||||
|
||||
ul {
|
||||
display: block;
|
||||
text-align: inset-inline-start;
|
||||
border-bottom-color: transparent !important;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
|
||||
&.is-active {
|
||||
a {
|
||||
color: var(--primary);
|
||||
border-inline-end-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
border-bottom-color: transparent !important;
|
||||
border-inline-end: 2px solid #dbdbdb;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
flex-grow: 2;
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
animation: fadeInLeft 0.5s;
|
||||
|
||||
&.is-active {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
265
src/components/base/VTag.vue
Normal file
265
src/components/base/VTag.vue
Normal file
@@ -0,0 +1,265 @@
|
||||
<script setup lang="ts">
|
||||
export type VTagColor =
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'info'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'orange'
|
||||
| 'blue'
|
||||
| 'green'
|
||||
| 'purple'
|
||||
| 'white'
|
||||
| 'light'
|
||||
| 'solid'
|
||||
|
||||
export type VTagSize = 'tiny'
|
||||
|
||||
export interface VTagProps {
|
||||
label?: string | number
|
||||
color?: VTagColor
|
||||
size?: VTagSize
|
||||
rounded?: boolean
|
||||
curved?: boolean
|
||||
outlined?: boolean
|
||||
elevated?: boolean
|
||||
remove?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VTagProps>(), {
|
||||
label: undefined,
|
||||
color: undefined,
|
||||
size: undefined,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<small
|
||||
class="tag"
|
||||
:class="[
|
||||
props.color && 'is-' + props.color,
|
||||
props.size && 'is-' + props.size,
|
||||
props.rounded && 'is-rounded',
|
||||
props.curved && 'is-curved',
|
||||
props.outlined && 'is-outlined',
|
||||
props.elevated && 'is-elevated',
|
||||
props.remove && 'is-delete',
|
||||
]"
|
||||
><slot>{{ props.label }}</slot></small>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.tag:not(body) {
|
||||
display: inline-block;
|
||||
line-height: 2.3;
|
||||
height: 2.4em;
|
||||
font-size: 0.75rem;
|
||||
|
||||
&.is-rounded {
|
||||
padding-inline-start: 1em;
|
||||
padding-inline-end: 1em;
|
||||
}
|
||||
|
||||
&.is-curved {
|
||||
padding-inline-start: 0.85em;
|
||||
padding-inline-end: 0.85em;
|
||||
line-height: 2.5;
|
||||
height: 2.6em;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&.is-tiny {
|
||||
line-height: 1.3;
|
||||
height: 1.6em;
|
||||
font-size: 0.7rem;
|
||||
|
||||
&.is-curved {
|
||||
padding-inline-start: 0.55em;
|
||||
padding-inline-end: 0.55em;
|
||||
line-height: 1.3;
|
||||
height: 1.6em;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-elevated {
|
||||
box-shadow: var(--light-box-shadow);
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
background: var(--white);
|
||||
border: 1px solid color-mix(in oklab, var(--fade-grey), black 3%);
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
&.is-elevated {
|
||||
box-shadow: var(--primary-box-shadow);
|
||||
}
|
||||
|
||||
&.is-outlined {
|
||||
background: none !important;
|
||||
color: var(--primary);
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
&.is-elevated {
|
||||
box-shadow: var(--success-box-shadow);
|
||||
}
|
||||
|
||||
&.is-outlined {
|
||||
background: none !important;
|
||||
color: var(--success);
|
||||
border: 1px solid var(--success);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
&.is-elevated {
|
||||
box-shadow: var(--info-box-shadow);
|
||||
}
|
||||
|
||||
&.is-outlined {
|
||||
background: none !important;
|
||||
color: var(--info);
|
||||
border: 1px solid var(--info);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
&.is-elevated {
|
||||
box-shadow: var(--warning-box-shadow);
|
||||
}
|
||||
|
||||
&.is-outlined {
|
||||
background: none !important;
|
||||
color: var(--warning);
|
||||
border: 1px solid var(--warning);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-danger {
|
||||
&.is-elevated {
|
||||
box-shadow: var(--danger-box-shadow);
|
||||
}
|
||||
|
||||
&.is-outlined {
|
||||
background: none !important;
|
||||
color: var(--danger);
|
||||
border: 1px solid var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-secondary {
|
||||
background: var(--secondary);
|
||||
color: var(--white);
|
||||
|
||||
&.is-elevated {
|
||||
box-shadow: var(--secondary-box-shadow);
|
||||
}
|
||||
|
||||
&.is-outlined {
|
||||
background: none !important;
|
||||
color: var(--secondary);
|
||||
border: 1px solid var(--secondary);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-green {
|
||||
background: var(--green);
|
||||
color: var(--white);
|
||||
|
||||
&.is-elevated {
|
||||
box-shadow: var(--green-box-shadow);
|
||||
}
|
||||
|
||||
&.is-outlined {
|
||||
background: none !important;
|
||||
color: var(--green);
|
||||
border: 1px solid var(--green);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-blue {
|
||||
background: var(--blue);
|
||||
color: var(--white);
|
||||
|
||||
&.is-elevated {
|
||||
box-shadow: var(--blue-box-shadow);
|
||||
}
|
||||
|
||||
&.is-outlined {
|
||||
background: none !important;
|
||||
color: var(--blue);
|
||||
border: 1px solid var(--blue);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-purple {
|
||||
background: var(--purple);
|
||||
color: var(--white);
|
||||
|
||||
&.is-elevated {
|
||||
box-shadow: var(--purple-box-shadow);
|
||||
}
|
||||
|
||||
&.is-outlined {
|
||||
background: none !important;
|
||||
color: var(--purple);
|
||||
border: 1px solid var(--purple);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-orange {
|
||||
background: var(--orange);
|
||||
color: var(--white);
|
||||
|
||||
&.is-elevated {
|
||||
box-shadow: var(--orange-box-shadow);
|
||||
}
|
||||
|
||||
&.is-outlined {
|
||||
background: none !important;
|
||||
color: var(--orange);
|
||||
border: 1px solid var(--orange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.tag {
|
||||
&:not(
|
||||
.is-primary,
|
||||
.is-secondary,
|
||||
.is-success,
|
||||
.is-info,
|
||||
.is-warning,
|
||||
.is-danger,
|
||||
.is-orange,
|
||||
.is-green,
|
||||
.is-blue,
|
||||
.is-purple
|
||||
) {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 10%);
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
background: var(--primary);
|
||||
|
||||
&.is-outlined {
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&.is-light {
|
||||
background: color-mix(in oklab, var(--primary), white 22%);
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
16
src/components/base/VTags.vue
Normal file
16
src/components/base/VTags.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
export interface VTagsProps {
|
||||
addons?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<VTagsProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="tags"
|
||||
:class="[props.addons && 'has-addons']"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
42
src/components/base/VTextEllipsis.vue
Normal file
42
src/components/base/VTextEllipsis.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
export interface VTextEllipsisProps {
|
||||
width?: string
|
||||
mobileWidth?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<VTextEllipsisProps>(), {
|
||||
width: '150px',
|
||||
mobileWidth: undefined,
|
||||
})
|
||||
const mobileWidthValue = props.mobileWidth ?? props.width
|
||||
|
||||
if (props.width.match(CssUnitRe) === null) {
|
||||
console.warn(
|
||||
`VTextEllipsis: invalid "${props.width}" width. Should be a valid css unit value.`,
|
||||
)
|
||||
}
|
||||
if (mobileWidthValue.match(CssUnitRe) === null) {
|
||||
console.warn(
|
||||
`VTextEllipsis: invalid "${mobileWidthValue}" mobileWidth. Should be a valid css unit value.`,
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="text-ellipsis"><slot /></span>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.text-ellipsis {
|
||||
max-width: v-bind('props.width');
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
.text-ellipsis {
|
||||
max-width: v-bind('mobileWidthValue');
|
||||
}
|
||||
}
|
||||
</style>
|
||||
68
src/components/base/VTextarea.vue
Normal file
68
src/components/base/VTextarea.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
export interface VTextareaProps {
|
||||
raw?: boolean
|
||||
autogrow?: boolean
|
||||
}
|
||||
|
||||
const modelValue = defineModel<string>({
|
||||
default: '',
|
||||
})
|
||||
const props = defineProps<{
|
||||
raw?: boolean
|
||||
autogrow?: boolean
|
||||
}>()
|
||||
|
||||
const { field, id } = useVFieldContext({
|
||||
create: false,
|
||||
help: 'VTextarea',
|
||||
})
|
||||
|
||||
const textareaRef = ref<HTMLTextAreaElement>()
|
||||
|
||||
const internal = computed({
|
||||
get() {
|
||||
if (field?.value) {
|
||||
return field.value.value
|
||||
}
|
||||
else {
|
||||
return modelValue.value
|
||||
}
|
||||
},
|
||||
set(value: any) {
|
||||
if (field?.value) {
|
||||
field.value.setValue(value)
|
||||
}
|
||||
modelValue.value = value
|
||||
},
|
||||
})
|
||||
|
||||
function fitSize() {
|
||||
if (!textareaRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (props.autogrow) {
|
||||
textareaRef.value.style.height = 'auto'
|
||||
textareaRef.value.style.height = textareaRef.value.scrollHeight + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
const classes = computed(() => {
|
||||
if (props.raw) return []
|
||||
|
||||
return ['textarea']
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<textarea
|
||||
:id="id"
|
||||
ref="textareaRef"
|
||||
v-model="internal"
|
||||
:class="classes"
|
||||
:name="id"
|
||||
@change="field?.handleChange"
|
||||
@blur="field?.handleBlur"
|
||||
@input="fitSize"
|
||||
/>
|
||||
</template>
|
||||
102
src/components/base/VUserList.vue
Normal file
102
src/components/base/VUserList.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { Person } from '/@src/utils/types'
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '직원검색',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
})
|
||||
|
||||
const emits = defineEmits(['appendUser'])
|
||||
|
||||
const vSelect = ref()
|
||||
const model = ref<Person>()
|
||||
const tagsOptions = ref([])
|
||||
|
||||
const onKeyup = (e: any) => {
|
||||
const regex_jaeum = /[ㄱ-ㅎ]/g
|
||||
if (e.key === 'Process') {
|
||||
if (e.target.value.match(regex_jaeum) === null) {
|
||||
api.findUser(e.target.value).then((res: Person[]) => {
|
||||
if (res.length > 0) {
|
||||
// res.map(u => {
|
||||
// if (model.value?.length > 0) {
|
||||
// const ignore = model.value?.reduce((a: Person, b: Person) => {
|
||||
// return a.sabun + '|' + b.sabun
|
||||
// })
|
||||
//
|
||||
// if (typeof ignore !== 'object') {
|
||||
// if (ignore.includes(u.value.sabun)) {
|
||||
// u['disabled'] = true
|
||||
// }
|
||||
// } else {
|
||||
// if (ignore.sabun.includes(u.value.sabun)) {
|
||||
// u['disabled'] = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
tagsOptions.value = res
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
if (e.target.value === '') {
|
||||
tagsOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
watch(model, (user) => {
|
||||
console.log(user)
|
||||
if (user === null || Object.keys(user).length === 0) return
|
||||
emits('appendUser', user)
|
||||
model.value = {}
|
||||
vSelect.value.clear()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VField v-slot="{ id }" class="pr-2 is-autocomplete-select">
|
||||
<VLabel class="has-fullwidth">
|
||||
{{props.label}}
|
||||
</VLabel>
|
||||
<VControl icon="lucide:search">
|
||||
<Multiselect
|
||||
v-model="model"
|
||||
ref="vSelect"
|
||||
:attrs="{ id }"
|
||||
:searchable="true"
|
||||
:disabled="props.disabled"
|
||||
:options="tagsOptions"
|
||||
noResultsText="조회된 결과가 없습니다."
|
||||
noOptionsText="검색된 결과가 없습니다."
|
||||
:placeholder="props.placeholder"
|
||||
@keyup="onKeyup"
|
||||
>
|
||||
<template #multiplelabel="{ values }">
|
||||
<div class="multiselect-multiple-label pl-6">
|
||||
{{props.placeholder}}
|
||||
</div>
|
||||
</template>
|
||||
</Multiselect>
|
||||
</VControl>
|
||||
</VField>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.field {
|
||||
padding-bottom: 10px !important;
|
||||
.label {
|
||||
font-size: 1.3em;
|
||||
color: var(--modal-text) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
366
src/components/layouts/auth/AuthLayout.vue
Normal file
366
src/components/layouts/auth/AuthLayout.vue
Normal file
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div class="auth-wrapper">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.auth-wrapper-inner {
|
||||
overflow: hidden !important;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
&.is-gapless:not(:last-child) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
&.is-single {
|
||||
background: var(--widget-grey);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.hero-banner {
|
||||
background: var(--widget-grey);
|
||||
|
||||
img {
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-heading {
|
||||
position: relative;
|
||||
max-width: 360px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 20px 0 0;
|
||||
|
||||
.auth-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.dark-mode {
|
||||
transform: scale(0.6);
|
||||
z-index: 2;
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
height: 42px;
|
||||
width: 42px;
|
||||
}
|
||||
|
||||
.top-logo {
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hero {
|
||||
&.is-white {
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.hero-body {
|
||||
.login {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.auth-content {
|
||||
max-width: 320px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
margin-top: -40px;
|
||||
margin-bottom: 40px;
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
font-family: var(--font);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 8px;
|
||||
color: var(--muted-grey);
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 0.9rem;
|
||||
font-family: var(--font-alt);
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-form-wrapper {
|
||||
max-width: 320px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
margin-top: 10px;
|
||||
|
||||
a {
|
||||
font-family: var(--font-alt);
|
||||
font-size: 0.9rem;
|
||||
color: var(--light-text);
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
|
||||
.setting-meta {
|
||||
font-family: var(--font);
|
||||
color: var(--light-text);
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.v-button {
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.auth-wrapper-inner {
|
||||
.hero-banner {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 4%);
|
||||
}
|
||||
|
||||
.hero {
|
||||
&.is-white {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 4%);
|
||||
}
|
||||
|
||||
.hero-body {
|
||||
.auth-content {
|
||||
h2 {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-nav {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
height: 80px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
z-index: 1;
|
||||
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.right {
|
||||
justify-content: flex-end;
|
||||
|
||||
.dark-mode {
|
||||
transform: scale(0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
flex-grow: 2;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-wrapper-inner {
|
||||
.single-form-wrap {
|
||||
min-height: 690px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.inner-wrap {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 40px auto 0;
|
||||
|
||||
.auth-head {
|
||||
max-width: 320px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
font-family: var(--font);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 8px;
|
||||
color: var(--muted-grey);
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 0.9rem;
|
||||
font-family: var(--font-alt);
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: var(--white);
|
||||
border: 1px solid color-mix(in oklab, var(--fade-grey), black 3%);
|
||||
border-radius: 10px;
|
||||
padding: 50px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.v-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.auth-wrapper-inner {
|
||||
&.is-single {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 4%);
|
||||
|
||||
.single-form-wrap {
|
||||
.inner-wrap {
|
||||
.auth-head {
|
||||
h2 {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 4%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 1%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <= 767px) {
|
||||
.avatar-carousel {
|
||||
&.resized-mobile {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.slick-custom {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.image-wrapper img {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-wrapper-inner {
|
||||
.hero {
|
||||
.hero-body {
|
||||
.auth-content {
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.single-form-wrap {
|
||||
.inner-wrap {
|
||||
.form-card {
|
||||
padding: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: portrait) {
|
||||
.modern-login {
|
||||
.top-logo {
|
||||
.iconify {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.dark-mode {
|
||||
top: -58px;
|
||||
inset-inline-end: 30%;
|
||||
}
|
||||
|
||||
.columns {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-wrapper-inner {
|
||||
.hero {
|
||||
.hero-body {
|
||||
.auth-content {
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.signup-columns {
|
||||
max-width: 460px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
241
src/components/layouts/landing/LandingFooter.vue
Normal file
241
src/components/layouts/landing/LandingFooter.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="vuero-footer is-relative">
|
||||
<div class="container">
|
||||
<div class="footer-copyright has-text-centered">
|
||||
<slot name="copyright" />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- /Simple light footer -->
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.vuero-footer {
|
||||
padding-bottom: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
background: var(--body-color);
|
||||
|
||||
.footer-head {
|
||||
padding-bottom: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid color-mix(in oklab, var(--fade-grey), black 4%);
|
||||
|
||||
.head-text {
|
||||
h3 {
|
||||
font-family: var(--font);
|
||||
font-size: 1.8rem;
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
|
||||
.head-action {
|
||||
.buttons {
|
||||
.button {
|
||||
&.action-button {
|
||||
height: 36px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
&.chat-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-body {
|
||||
padding-top: 3rem;
|
||||
|
||||
.footer-column {
|
||||
padding-top: 20px;
|
||||
|
||||
.column-header {
|
||||
font-family: var(--font-alt);
|
||||
text-transform: uppercase;
|
||||
color: var(--dark-text);
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.column-item {
|
||||
padding-bottom: 10px;
|
||||
|
||||
a {
|
||||
font-family: var(--font);
|
||||
color: var(--light-text);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
color: var(--light-text);
|
||||
font-size: 16px;
|
||||
margin-inline-end: 0.6rem;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-description {
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
.small-footer-logo {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-copyright {
|
||||
font-family: var(--font);
|
||||
color: var(--light-text);
|
||||
padding: 4rem 0 2rem;
|
||||
|
||||
a {
|
||||
color: var(--light-text);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.landing-page-wrapper {
|
||||
.vuero-footer {
|
||||
background: color-mix(in oklab, var(--landing-xxx), white 8%);
|
||||
|
||||
.footer-head {
|
||||
border-color: color-mix(in oklab, var(--landing-xxx), white 18%);
|
||||
|
||||
.head-text {
|
||||
h3 {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
|
||||
.head-action {
|
||||
.buttons {
|
||||
.button {
|
||||
&.action-button {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
&.chat-button {
|
||||
color: var(--primary);
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-body {
|
||||
.footer-column {
|
||||
.column-header {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
|
||||
.column-item {
|
||||
a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.social-links {
|
||||
a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-copyright {
|
||||
a {
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
.vuero-footer {
|
||||
.footer-head {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
|
||||
.head-text {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-body {
|
||||
padding-inline-start: 20px;
|
||||
padding-inline-end: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: portrait) {
|
||||
.vuero-footer {
|
||||
.footer-head,
|
||||
.footer-body {
|
||||
padding-inline-start: 20px;
|
||||
padding-inline-end: 20px;
|
||||
}
|
||||
|
||||
.footer-description {
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: landscape) {
|
||||
.vuero-footer {
|
||||
.footer-head,
|
||||
.footer-body {
|
||||
padding-inline-start: 20px;
|
||||
padding-inline-end: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
src/components/layouts/landing/LandingGrids.vue
Normal file
34
src/components/layouts/landing/LandingGrids.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
const { x, y } = useMouse()
|
||||
|
||||
const maskPosition = computed(() => `${Math.round(x.value - 400)}px ${Math.round(y.value - 400)}px`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grids gridlines" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.grids {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
will-change: mask-position;
|
||||
mask-image: radial-gradient(circle 400px at 400px 400px, black 60%, transparent 70%);
|
||||
mask-position: v-bind(maskPosition);
|
||||
mask-repeat: no-repeat;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.gridlines {
|
||||
background-image: url('/images/backgrounds/grid.svg');
|
||||
}
|
||||
|
||||
.is-dark .gridlines:not(.is-contrasted) {
|
||||
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PGcgZmlsbC1ydWxlPSJldmVub2RkIj48ZyBmaWxsPSIjMjcyZTNlIj48cGF0aCBvcGFjaXR5PSIuNSIgZD0iTTk2IDk1aDR2MWgtNHY0aC0xdi00aC05djRoLTF2LTRoLTl2NGgtMXYtNGgtOXY0aC0xdi00aC05djRoLTF2LTRoLTl2NGgtMXYtNGgtOXY0aC0xdi00aC05djRoLTF2LTRoLTl2NGgtMXYtNEgwdi0xaDE1di05SDB2LTFoMTV2LTlIMHYtMWgxNXYtOUgwdi0xaDE1di05SDB2LTFoMTV2LTlIMHYtMWgxNXYtOUgwdi0xaDE1di05SDB2LTFoMTV2LTlIMHYtMWgxNVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoOVYwaDF2MTVoNHYxaC00djloNHYxaC00djloNHYxaC00djloNHYxaC00djloNHYxaC00djloNHYxaC00djloNHYxaC00djloNHYxaC00djl6bS0xIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS05LTEwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTktMTB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS05LTEwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTktMTB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS05LTEwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTktMTB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS0xMCAwdi05aC05djloOXptLTEwIDB2LTloLTl2OWg5em0tMTAgMHYtOWgtOXY5aDl6bS05LTEwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6bTEwIDBoOXYtOWgtOXY5em0xMCAwaDl2LTloLTl2OXptMTAgMGg5di05aC05djl6IiBmaWxsPSIjMjcyZTNlIi8+PHBhdGggZD0iTTYgNVYwSDV2NUgwdjFoNXY5NGgxVjZoOTRWNUg2eiIgZmlsbD0iIzI3MmUzZSIvPjwvZz48L2c+PC9zdmc+');
|
||||
}
|
||||
|
||||
.is-dark .gridlines.is-contrasted {
|
||||
filter: brightness(0.30);
|
||||
}
|
||||
</style>
|
||||
27
src/components/layouts/landing/LandingLayout.vue
Normal file
27
src/components/layouts/landing/LandingLayout.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
|
||||
<template>
|
||||
<MinimalLayout theme="light">
|
||||
<main class="landing-page-wrapper">
|
||||
<LandingGrids />
|
||||
|
||||
<div class="hero">
|
||||
<LandingNavigation>
|
||||
<template #logo>
|
||||
<slot name="logo" />
|
||||
</template>
|
||||
<template #end>
|
||||
<slot name="nav-end" />
|
||||
</template>
|
||||
</LandingNavigation>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</main>
|
||||
</MinimalLayout>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '/@src/scss/abstracts/all';
|
||||
@import '/@src/scss/layout/landing';
|
||||
</style>
|
||||
365
src/components/layouts/landing/LandingNavigation.vue
Normal file
365
src/components/layouts/landing/LandingNavigation.vue
Normal file
@@ -0,0 +1,365 @@
|
||||
<script setup lang="ts">
|
||||
const isMobileNavOpen = ref(false)
|
||||
|
||||
const { y } = useWindowScroll()
|
||||
const { isLargeScreen } = useScreenSize()
|
||||
|
||||
const isScrolling = computed(() => {
|
||||
return y.value > 30
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (isLargeScreen.value) {
|
||||
isMobileNavOpen.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hero">
|
||||
<nav
|
||||
class="navbar is-fixed-top"
|
||||
:class="[!isScrolling && 'is-docked', isMobileNavOpen && 'is-solid']"
|
||||
aria-label="main navigation"
|
||||
>
|
||||
<div class="navbar-brand">
|
||||
<slot name="logo" />
|
||||
|
||||
<MobileBurger v-model="isMobileNavOpen" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="navbar-menu"
|
||||
:class="[isMobileNavOpen && 'is-active']"
|
||||
>
|
||||
<div class="navbar-start">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<slot name="end" />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.hero {
|
||||
.navbar {
|
||||
top: 15px;
|
||||
height: 65px;
|
||||
max-width: 1140px;
|
||||
margin: 0 auto;
|
||||
background-color: var(--white);
|
||||
box-shadow: var(--light-box-shadow);
|
||||
border: 1px solid var(--fade-grey);
|
||||
border-radius: 500px;
|
||||
font-family: var(--font);
|
||||
z-index: 99;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&.is-docked {
|
||||
&:not(.is-solid) {
|
||||
top: 0;
|
||||
border-color: transparent;
|
||||
height: 110px;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
|
||||
.navbar-brand {
|
||||
background: transparent;
|
||||
.brand-icon {
|
||||
height: 64px;
|
||||
//width: 200px;
|
||||
background: transparent;
|
||||
border-color: color-mix(in oklab, var(--fade-grey), black 3%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
height: 65px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
background: var(--white) !important;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
img,
|
||||
.iconify {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
//max-width: 34px;
|
||||
//max-height: 34px;
|
||||
margin-inline-start: 10px;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
height: 50px;
|
||||
//width: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
img,
|
||||
svg {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
.navbar-item {
|
||||
.dark-mode {
|
||||
transform: scale(0.6);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
position: relative;
|
||||
font-family: var(--font-alt);
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
color: var(--light-text);
|
||||
text-transform: capitalize;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
inset-inline-start: 2px;
|
||||
width: 50%;
|
||||
transform-origin: right center;
|
||||
height: 3px;
|
||||
border-radius: 50px;
|
||||
background: var(--primary);
|
||||
transform: scale(0, 1);
|
||||
transition: -webkit-transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition:
|
||||
transform 0.4s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
-webkit-transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
// Hover state
|
||||
&:hover,
|
||||
&.is-active {
|
||||
color: var(--dark-text);
|
||||
|
||||
&::before {
|
||||
transform-origin: left center;
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
&::before {
|
||||
background: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
font-weight: 400 !important;
|
||||
height: 44px;
|
||||
min-width: 110px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.navbar {
|
||||
&:not(.is-docked) {
|
||||
background: color-mix(in oklab, var(--landing-xxx), white 8%);
|
||||
border-color: color-mix(in oklab, var(--landing-xxx), white 14%);
|
||||
}
|
||||
|
||||
&.is-docked {
|
||||
.navbar-brand {
|
||||
a {
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
.brand-icon {
|
||||
background: transparent;
|
||||
//background: color-mix(in oklab, var(--landing-yyy), white 8%) !important;
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), white 18%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
background: color-mix(in oklab, var(--landing-xxx), white 8%) !important;
|
||||
border-color: color-mix(in oklab, var(--landing-xxx), white 14%) !important;
|
||||
|
||||
.navbar-brand {
|
||||
.brand-icon {
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
&.is-active {
|
||||
background: color-mix(in oklab, var(--landing-xxx), white 12%);
|
||||
border-color: color-mix(in oklab, var(--landing-xxx), white 14%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-item {
|
||||
.nav-link {
|
||||
&:hover,
|
||||
&.is-active {
|
||||
color: var(--white) !important;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
&.is-primary {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
|
||||
&.is-raised:hover {
|
||||
box-shadow: var(--primary-box-shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 767px) {
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: calc(100% - 32px);
|
||||
margin: 0 16px;
|
||||
|
||||
&.is-docked {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
top: 10px;
|
||||
box-shadow: var(--light-box-shadow) !important;
|
||||
|
||||
.navbar-brand {
|
||||
.brand-icon {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
box-shadow: var(--light-box-shadow) !important;
|
||||
top: 73px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
width: calc(100% - 32px);
|
||||
position: fixed;
|
||||
top: 78px;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
margin: 0 auto;
|
||||
border-radius: 0 0 10px 10px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--fade-grey);
|
||||
box-shadow: none;
|
||||
|
||||
.navbar-item {
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: portrait) {
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: calc(100% - 32px);
|
||||
margin: 0 16px;
|
||||
|
||||
&.is-docked {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
&:not(.is-docked) {
|
||||
&.is-solid {
|
||||
.navbar-menu {
|
||||
top: 73px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
top: 10px;
|
||||
box-shadow: var(--light-box-shadow) !important;
|
||||
|
||||
.navbar-brand {
|
||||
.brand-icon {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
box-shadow: var(--light-box-shadow) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
width: calc(100% - 32px);
|
||||
position: fixed;
|
||||
top: 78px;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
margin: 0 auto;
|
||||
border-radius: 0 0 10px 10px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--fade-grey);
|
||||
box-shadow: none;
|
||||
|
||||
.navbar-item {
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width >= 768px) and (width <= 1024px) and (orientation: landscape) {
|
||||
.navbar {
|
||||
width: calc(100% - 40px);
|
||||
margin: 0 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
18
src/components/layouts/landing/landing.types.ts
Normal file
18
src/components/layouts/landing/landing.types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface LandingNavItem {
|
||||
label: string
|
||||
to: string
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
export interface LandingFooterColumn {
|
||||
label: string
|
||||
children: {
|
||||
label: string
|
||||
to: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface LandingSocialItem {
|
||||
icon: string
|
||||
link: string
|
||||
}
|
||||
53
src/components/layouts/minimal/MinimalLayout.vue
Normal file
53
src/components/layouts/minimal/MinimalLayout.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
export type MinimalTheme = 'darker' | 'light'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
theme?: MinimalTheme
|
||||
}>(),
|
||||
{
|
||||
theme: 'darker',
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="minimal-wrapper"
|
||||
:class="[props.theme]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.minimal-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: var(--lighter-grey);
|
||||
|
||||
&.light {
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
&.lighter {
|
||||
background: var(--smoke-white);
|
||||
}
|
||||
|
||||
&.darker {
|
||||
background: var(--background-grey);
|
||||
}
|
||||
|
||||
.minimal-wrap {
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.minimal-wrapper {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 10%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 10%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
645
src/components/layouts/navbar/Navbar.vue
Normal file
645
src/components/layouts/navbar/Navbar.vue
Normal file
@@ -0,0 +1,645 @@
|
||||
<script setup lang="ts">
|
||||
import { useNavbarLayoutContext } from './navbar.context'
|
||||
|
||||
const { theme } = useNavbarLayoutContext()
|
||||
const { y } = useWindowScroll()
|
||||
|
||||
const isScrolling = computed(() => {
|
||||
return y.value > 30
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="navbar-navbar"
|
||||
:class="[
|
||||
isScrolling && 'is-scrolled',
|
||||
theme === 'fade' && 'is-transparent',
|
||||
theme === 'colored' && 'is-colored',
|
||||
]"
|
||||
>
|
||||
<div class="navbar-navbar-inner">
|
||||
<div class="left">
|
||||
<slot name="title" />
|
||||
</div>
|
||||
<div class="center">
|
||||
<slot name="links" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot name="toolbar" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.navbar-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
height: 65px;
|
||||
background: var(--white);
|
||||
transition: all 0.3s; // transition-all test
|
||||
border-bottom: 1px solid var(--fade-grey);
|
||||
z-index: 100;
|
||||
|
||||
&.is-transparent {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border-bottom-color: transparent;
|
||||
|
||||
&.is-solid,
|
||||
&.is-scrolled {
|
||||
background: var(--white);
|
||||
border-bottom-color: var(--fade-grey);
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.is-scrolled {
|
||||
box-shadow: 0 0 8px 0 rgb(0 0 0 / 12%);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-colored {
|
||||
background: var(--landing-yyy);
|
||||
border-bottom-color: var(--landing-yyy);
|
||||
|
||||
.navbar-navbar-inner {
|
||||
.left {
|
||||
.separator {
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), white 18%);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
.centered-links {
|
||||
.centered-link {
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 6%);
|
||||
|
||||
.iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
// background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
// border-color: color-mix(in oklab, var(--landing-yyy), white 6%);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--light-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centered-drops {
|
||||
.centered-drop {
|
||||
.dropdown {
|
||||
&:hover {
|
||||
.is-trigger {
|
||||
.button {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 6%);
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
.is-trigger {
|
||||
.button {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), white 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-trigger {
|
||||
.button {
|
||||
background: var(--landing-yyy);
|
||||
color: var(--light-text);
|
||||
|
||||
.caret {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centered-button {
|
||||
.button {
|
||||
background: var(--landing-yyy);
|
||||
color: var(--light-text);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 6%);
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centered-search {
|
||||
.field {
|
||||
.control {
|
||||
.input {
|
||||
background: color-mix(in oklab, var(--primary), black 10%);
|
||||
border-color: color-mix(in oklab, var(--primary), black 6%);
|
||||
color: var(--smoke-white);
|
||||
|
||||
&::placeholder {
|
||||
color: color-mix(in oklab, var(--primary), white 2%);
|
||||
}
|
||||
|
||||
&:focus ~ .form-icon.iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
.form-icon.iconify {
|
||||
color: color-mix(in oklab, var(--primary), white 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
.toolbar {
|
||||
.toolbar-link {
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
}
|
||||
|
||||
> .iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
&:hover {
|
||||
.is-trigger {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dots {
|
||||
&.is-active {
|
||||
.is-trigger {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-trigger .iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-link {
|
||||
background: var(--landing-yyy);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
}
|
||||
|
||||
> .iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-navbar-inner {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 17%;
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
min-width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: var(--font);
|
||||
font-size: 0.95rem;
|
||||
color: var(--muted-grey);
|
||||
letter-spacing: 1px;
|
||||
max-width: 50px;
|
||||
line-height: 1.2;
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 38px;
|
||||
width: 2px;
|
||||
border-inline-end: 1px solid color-mix(in oklab, var(--fade-grey), black 4%);
|
||||
margin: 0 20px 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 2;
|
||||
width: 50%;
|
||||
|
||||
.left-links {
|
||||
padding-left: 20px;
|
||||
justify-content: left !important;
|
||||
}
|
||||
|
||||
.centered-links, .left-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: 1rem;
|
||||
// max-width: 580px;
|
||||
|
||||
.centered-link {
|
||||
// flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
text-align: center;
|
||||
padding: 12px 24px;
|
||||
//border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
margin: 0 4px;
|
||||
transition: all 0.3s; // transition-all test
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
background: color-mix(in oklab, var(--fade-grey), white 4%);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
span {
|
||||
color: color-mix(in oklab, var(--primary), black 8%);
|
||||
}
|
||||
}
|
||||
|
||||
.iconify {
|
||||
font-size: 20px;
|
||||
color: color-mix(in oklab, var(--light-text), white 6%);
|
||||
stroke-width: 1.6px;
|
||||
transition: stroke 0.3s;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
font-family: var(--font);
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.6px;
|
||||
font-weight: 500;
|
||||
//color: var(--muted-grey);
|
||||
color: var(--modal-text);
|
||||
text-transform: uppercase;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 25%;
|
||||
|
||||
.icon-link {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 34px;
|
||||
width: 34px;
|
||||
font-size: 18px;
|
||||
border-radius: var(--radius-rounded);
|
||||
margin: 0 4px;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&:hover {
|
||||
background: var(--white);
|
||||
border-color: var(--fade-grey);
|
||||
box-shadow: var(--light-box-shadow);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
stroke-width: 1.6px;
|
||||
color: var(--light-text);
|
||||
transition: stroke 0.3s;
|
||||
vertical-align: 0;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
4. Webapp Navbar Dark mode
|
||||
========================================================================== */
|
||||
|
||||
.is-dark {
|
||||
.navbar-navbar:not(.is-colored) {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 1%);
|
||||
|
||||
&.is-transparent {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border-bottom-color: transparent;
|
||||
|
||||
&.is-solid,
|
||||
&.is-scrolled {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 1%);
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-navbar-inner {
|
||||
.left {
|
||||
a {
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
.separator {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
.centered-links {
|
||||
.centered-link {
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 2%);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
// background: color-mix(in oklab, var(--dark-sidebar), white 2%);
|
||||
// border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
|
||||
// &:hover,
|
||||
// &:focus {
|
||||
// background: color-mix(in oklab, var(--dark-sidebar), white 2%);
|
||||
// }
|
||||
|
||||
span {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
.icon-link {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 2%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-navbar {
|
||||
&.is-colored {
|
||||
.navbar-navbar-inner {
|
||||
.left {
|
||||
.title {
|
||||
color: var(--smoke-white) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
.centered-links {
|
||||
.centered-link {
|
||||
&:hover {
|
||||
.iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
.iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centered-drops {
|
||||
.centered-drop {
|
||||
.dropdown {
|
||||
&:hover {
|
||||
.is-trigger {
|
||||
.button {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
.is-trigger {
|
||||
.button {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-mega-dropdown {
|
||||
.dropdown-menu {
|
||||
.dropdown-content {
|
||||
.category-selector {
|
||||
.title-wrap {
|
||||
h4 {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
}
|
||||
|
||||
.category-selector-inner {
|
||||
.category-item {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 4%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
i,
|
||||
span {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--dark-dark-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mega-menus {
|
||||
.dropdown-item-group {
|
||||
.column-heading {
|
||||
color: var(--dark-dark-text);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
}
|
||||
|
||||
.column-content {
|
||||
.is-media {
|
||||
&:hover {
|
||||
.meta {
|
||||
span:first-child {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centered-button {
|
||||
.button {
|
||||
&:hover {
|
||||
color: var(--smoke-white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centered-search {
|
||||
.field {
|
||||
.control {
|
||||
.input {
|
||||
color: var(--smoke-white);
|
||||
|
||||
&:focus ~ .form-icon .iconify {
|
||||
color: var(--smoke-white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
.toolbar {
|
||||
.toolbar-link {
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%) !important;
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), black 12%) !important;
|
||||
}
|
||||
|
||||
> .iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
.is-trigger {
|
||||
&:hover {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%) !important;
|
||||
border-color: color-mix(in oklab, var(--landing-yyy), black 12%) !important;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
color: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
&:hover {
|
||||
background-color:white !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
111
src/components/layouts/navbar/NavbarDropdown.vue
Normal file
111
src/components/layouts/navbar/NavbarDropdown.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import type { NavbarItemDropdown } from './navbar.types'
|
||||
import { useNavbarLayoutContext } from './navbar.context'
|
||||
|
||||
const props = defineProps<{
|
||||
link: NavbarItemDropdown
|
||||
}>()
|
||||
|
||||
const tippyRef = ref<any>()
|
||||
|
||||
const {
|
||||
activeSubnavId,
|
||||
toggleSubnav,
|
||||
} = useNavbarLayoutContext()
|
||||
|
||||
watch(activeSubnavId, () => {
|
||||
if (activeSubnavId.value === props.link.id) {
|
||||
tippyRef.value?.show()
|
||||
}
|
||||
else {
|
||||
tippyRef.value?.hide()
|
||||
}
|
||||
})
|
||||
|
||||
function onHidden() {
|
||||
if (activeSubnavId.value === props.link.id) {
|
||||
activeSubnavId.value = undefined
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="is-flex">
|
||||
<Tippy
|
||||
ref="tippyRef"
|
||||
:key="`dropdown-${props.link.id}`"
|
||||
trigger="manual"
|
||||
class="is-flex mx-1"
|
||||
content-class="content-tet"
|
||||
interactive
|
||||
:offset="[0, 10]"
|
||||
:duration="[150, 100]"
|
||||
@hidden="onHidden"
|
||||
>
|
||||
<a
|
||||
:class="[
|
||||
activeSubnavId === props.link.id && 'is-active',
|
||||
]"
|
||||
class="centered-link centered-link-toggle"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="toggleSubnav(props.link.id)"
|
||||
@click="toggleSubnav(props.link.id)"
|
||||
>
|
||||
<VIcon
|
||||
v-if="props.link.icon"
|
||||
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
<span>{{ props.link.label }}</span>
|
||||
</a>
|
||||
|
||||
<template #content>
|
||||
<ul class="centered-link-dropdown has-slimscroll">
|
||||
<li v-for="item of props.link.children" :key="item.to">
|
||||
<VLink :to="item.to">
|
||||
<VIcon
|
||||
v-if="item.icon"
|
||||
|
||||
:icon="item.icon"
|
||||
/>
|
||||
<span>{{ item.label }}</span>
|
||||
</VLink>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</Tippy>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.centered-link-dropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-height: 300px;
|
||||
padding: 1rem 0;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
padding: 0 4rem 0 1rem;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: background-color 0.2s;
|
||||
font-family: var(--font);
|
||||
font-size: 0.9rem;
|
||||
color: color-mix(in oklab, var(--light-text), black 5%);
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
87
src/components/layouts/navbar/NavbarItem.vue
Normal file
87
src/components/layouts/navbar/NavbarItem.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import type { NavbarItem, NavbarItemMegamenu, NavbarItemAction } from './navbar.types'
|
||||
import { useNavbarLayoutContext } from './navbar.context'
|
||||
|
||||
const props = defineProps<{
|
||||
link: NavbarItem
|
||||
}>()
|
||||
|
||||
const { activeSubnavId, toggleSubnav } = useNavbarLayoutContext()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NavbarDropdown
|
||||
v-if="props.link.type === 'dropdown'"
|
||||
:key="`dropdown-${props.link.id}`"
|
||||
:link="props.link"
|
||||
/>
|
||||
<a
|
||||
v-if="props.link.type === 'megamenu'"
|
||||
:key="`megamenu-${props.link.id}`"
|
||||
:class="[
|
||||
activeSubnavId === props.link.id && 'is-active',
|
||||
]"
|
||||
class="centered-link centered-link-toggle"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="toggleSubnav((props.link as NavbarItemMegamenu).id)"
|
||||
@click="toggleSubnav((props.link as NavbarItemMegamenu).id)"
|
||||
>
|
||||
<VIcon
|
||||
v-if="props.link.icon"
|
||||
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
<span v-if="props.link.label">{{ props.link.label }}</span>
|
||||
</a>
|
||||
<a
|
||||
v-else-if="props.link.type === 'action'"
|
||||
:key="`action-${props.link.label}`"
|
||||
class="centered-link centered-link-search"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="(props.link as NavbarItemAction).onClick"
|
||||
@click="(props.link as NavbarItemAction).onClick"
|
||||
>
|
||||
<VIcon
|
||||
v-if="props.link.icon"
|
||||
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
<span v-if="props.link.label">{{ props.link.label }}</span>
|
||||
</a>
|
||||
<VLink
|
||||
v-else-if="props.link.type === 'link'"
|
||||
:key="`link-${props.link.label}`"
|
||||
class="centered-link centered-link-search"
|
||||
:to="props.link.to"
|
||||
>
|
||||
<!-- <VIcon-->
|
||||
<!-- v-if="props.link.icon"-->
|
||||
|
||||
<!-- :icon="props.link.icon"-->
|
||||
<!-- />-->
|
||||
<span v-if="props.link.label">{{ props.link.label }}</span>
|
||||
</VLink>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.centered-links, .left-links {
|
||||
.router-link-active {
|
||||
//border-color: var(--primary) !important;
|
||||
//background-color: var(--primary) !important;
|
||||
border-bottom: 3px solid var(--primary) !important;
|
||||
//.iconify {
|
||||
// color: white !important;
|
||||
//}
|
||||
&:hover {
|
||||
background-color:transparent !important;
|
||||
}
|
||||
span {
|
||||
color: var(--modal-text) !important;
|
||||
font-weight: bold !important;
|
||||
font-size: 1.2rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
58
src/components/layouts/navbar/NavbarItemMobile.vue
Normal file
58
src/components/layouts/navbar/NavbarItemMobile.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import type { NavbarItem, NavbarItemDropdown, NavbarItemMegamenu, NavbarItemAction } from './navbar.types'
|
||||
import { useNavbarLayoutContext } from './navbar.context'
|
||||
|
||||
const props = defineProps<{
|
||||
link: NavbarItem
|
||||
}>()
|
||||
|
||||
const { activeMobileSubsidebarId, toggleMobileSubnav } = useNavbarLayoutContext()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li>
|
||||
<a
|
||||
v-if="props.link.type === 'dropdown'"
|
||||
:class="[activeMobileSubsidebarId === props.link.id && 'is-active']"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="toggleMobileSubnav((props.link as NavbarItemDropdown).id)"
|
||||
@click="toggleMobileSubnav((props.link as NavbarItemDropdown).id)"
|
||||
>
|
||||
<VIcon
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
v-if="props.link.type === 'megamenu'"
|
||||
:class="[activeMobileSubsidebarId === props.link.id && 'is-active']"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="toggleMobileSubnav((props.link as NavbarItemMegamenu).id)"
|
||||
@click="toggleMobileSubnav((props.link as NavbarItemMegamenu).id)"
|
||||
>
|
||||
<VIcon
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
v-else-if="props.link.type === 'action'"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="(props.link as NavbarItemAction).onClick"
|
||||
@click="(props.link as NavbarItemAction).onClick"
|
||||
>
|
||||
<VIcon
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
</a>
|
||||
<VLink
|
||||
v-else-if="props.link.type === 'link'"
|
||||
:to="props.link.to"
|
||||
>
|
||||
<VIcon
|
||||
:icon="props.link.icon"
|
||||
/>
|
||||
</VLink>
|
||||
</li>
|
||||
</template>
|
||||
250
src/components/layouts/navbar/NavbarLayout.vue
Normal file
250
src/components/layouts/navbar/NavbarLayout.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
NavbarTheme,
|
||||
NavbarItem,
|
||||
NavbarItemMegamenu,
|
||||
NavbarItemDropdown,
|
||||
NavbarLayoutContext,
|
||||
} from './navbar.types'
|
||||
import { injectionKey } from './navbar.context'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
links?: NavbarItem[]
|
||||
theme?: NavbarTheme
|
||||
size?: 'default' | 'large' | 'wide' | 'full'
|
||||
}>(),
|
||||
{
|
||||
links: () => [],
|
||||
theme: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
)
|
||||
|
||||
const pageTitle = useVueroContext<string>('page-title')
|
||||
const route = useRoute()
|
||||
|
||||
const linksWithChildren = computed(() => {
|
||||
return props.links.filter(link => link.type === 'megamenu' || link.type === 'dropdown') as (NavbarItemMegamenu | NavbarItemDropdown)[]
|
||||
})
|
||||
|
||||
const isMobileSidebarOpen = ref(false)
|
||||
const activeMobileSubsidebarId = ref<string>(linksWithChildren.value?.[0]?.id)
|
||||
const activeSubnavId = ref<string | undefined>()
|
||||
|
||||
const activeSubnav = computed(() => {
|
||||
return linksWithChildren.value.find(link => link.id === activeSubnavId.value)
|
||||
})
|
||||
const activeMobileSubsidebar = computed(() => {
|
||||
return linksWithChildren.value.find(link => link.id === activeMobileSubsidebarId.value)
|
||||
})
|
||||
|
||||
function toggleSubnav(id: string) {
|
||||
if (activeSubnavId.value === id) {
|
||||
activeSubnavId.value = undefined
|
||||
}
|
||||
else {
|
||||
activeSubnavId.value = id
|
||||
}
|
||||
}
|
||||
function toggleMobileSubnav(id: string) {
|
||||
if (activeMobileSubsidebarId.value === id) {
|
||||
isMobileSidebarOpen.value = false
|
||||
}
|
||||
else {
|
||||
activeMobileSubsidebarId.value = id
|
||||
isMobileSidebarOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// provide context to children
|
||||
const context: NavbarLayoutContext = {
|
||||
links: computed(() => props.links),
|
||||
theme: computed(() => props.theme),
|
||||
|
||||
isMobileSidebarOpen,
|
||||
activeMobileSubsidebarId,
|
||||
activeSubnavId,
|
||||
|
||||
activeSubnav,
|
||||
activeMobileSubsidebar,
|
||||
|
||||
toggleSubnav,
|
||||
toggleMobileSubnav,
|
||||
}
|
||||
provide(injectionKey, context)
|
||||
|
||||
// using reactive context for slots, has better dev experience
|
||||
const contextRx = reactive(context)
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
activeSubnavId.value = undefined
|
||||
isMobileSidebarOpen.value = false
|
||||
},
|
||||
)
|
||||
|
||||
watch(() => Boolean(activeSubnav.value?.type === 'megamenu' || isMobileSidebarOpen.value), (value) => {
|
||||
if (value) {
|
||||
document.documentElement.classList.add('no-scroll')
|
||||
}
|
||||
else {
|
||||
document.documentElement.classList.remove('no-scroll')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="navbar-layout">
|
||||
<!-- Mobile navigation -->
|
||||
<MobileNavbar v-model="isMobileSidebarOpen">
|
||||
<template #logo>
|
||||
<slot name="logo" v-bind="contextRx" />
|
||||
|
||||
<div class="brand-end">
|
||||
<slot name="toolbar-mobile" v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
</MobileNavbar>
|
||||
|
||||
<MobileSidebar
|
||||
:class="[isMobileSidebarOpen && 'is-active']"
|
||||
>
|
||||
<template #links>
|
||||
<slot name="navbar-links-mobile" v-bind="contextRx">
|
||||
<NavbarItemMobile
|
||||
v-for="link in props.links"
|
||||
:key="link.label"
|
||||
:link
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
</MobileSidebar>
|
||||
<Transition name="fade">
|
||||
<MobileOverlay
|
||||
v-if="isMobileSidebarOpen"
|
||||
@click="isMobileSidebarOpen = false"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<Transition name="slide-x">
|
||||
<KeepAlive>
|
||||
<NavbarSubsidebarMobile
|
||||
v-if="isMobileSidebarOpen && activeMobileSubsidebar?.children"
|
||||
:key="activeMobileSubsidebarId"
|
||||
:label="activeMobileSubsidebar.label"
|
||||
:items="activeMobileSubsidebar.children"
|
||||
/>
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
<!-- /Mobile navigation -->
|
||||
|
||||
<!-- Desktop navigation -->
|
||||
<Navbar>
|
||||
<template #title>
|
||||
<slot name="logo" v-bind="contextRx" />
|
||||
|
||||
<!-- <div v-if="'logo' in $slots" class="separator" />-->
|
||||
<!-- <slot name="navbar-title" v-bind="contextRx">-->
|
||||
<!-- <h1 class="title is-5">-->
|
||||
<!-- {{ pageTitle }}-->
|
||||
<!-- </h1>-->
|
||||
<!-- </slot>-->
|
||||
</template>
|
||||
|
||||
<template #toolbar>
|
||||
<div class="toolbar desktop-toolbar">
|
||||
<slot name="toolbar" v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #links>
|
||||
<slot name="navbar-links" v-bind="contextRx">
|
||||
<div class="left-links">
|
||||
<NavbarItem
|
||||
v-for="link in props.links"
|
||||
:key="link.label"
|
||||
:link="link"
|
||||
/>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
</Navbar>
|
||||
|
||||
<div
|
||||
class="navbar-subnavbar is-hidden-mobile"
|
||||
:class="[activeSubnav?.type === 'megamenu' && 'is-active']"
|
||||
>
|
||||
<NavbarMegamenu
|
||||
v-if="activeSubnav?.type === 'megamenu'"
|
||||
:key="activeSubnavId"
|
||||
:children="activeSubnav.children"
|
||||
class="is-active"
|
||||
>
|
||||
<template v-if="'megamenu-start' in $slots" #start>
|
||||
<slot name="megamenu-start" v-bind="contextRx" />
|
||||
</template>
|
||||
<template v-if="'megamenu-end' in $slots" #end>
|
||||
<slot name="megamenu-end" v-bind="contextRx" />
|
||||
</template>
|
||||
<template v-if="'megamenu-top' in $slots" #top>
|
||||
<slot name="megamenu-top" v-bind="contextRx" />
|
||||
</template>
|
||||
<template v-if="'megamenu-bottom' in $slots" #bottom>
|
||||
<slot name="megamenu-bottom" v-bind="contextRx" />
|
||||
</template>
|
||||
</NavbarMegamenu>
|
||||
</div>
|
||||
<!-- /Desktop navigation -->
|
||||
|
||||
<ViewWrapper full top-nav>
|
||||
<template v-if="props.size === 'full'">
|
||||
<div class="is-navbar-md">
|
||||
<slot name="page-heading" v-bind="contextRx">
|
||||
<NavbarPageTitleMobile>
|
||||
<slot name="navbar-title-mobile" v-bind="contextRx">
|
||||
<h1 class="title is-4">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
</slot>
|
||||
|
||||
<template #toolbar>
|
||||
<slot name="toolbar" v-bind="contextRx" />
|
||||
</template>
|
||||
</NavbarPageTitleMobile>
|
||||
</slot>
|
||||
|
||||
<slot v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
<PageContentWrapper v-else :size="props.size">
|
||||
<PageContent class="is-relative">
|
||||
<div class="is-navbar-md">
|
||||
<slot name="page-heading" v-bind="contextRx">
|
||||
<NavbarPageTitleMobile>
|
||||
<slot name="navbar-title-mobile" v-bind="contextRx">
|
||||
<h1 class="title is-4">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
</slot>
|
||||
|
||||
<template #toolbar>
|
||||
<slot name="toolbar" v-bind="contextRx" />
|
||||
</template>
|
||||
</NavbarPageTitleMobile>
|
||||
</slot>
|
||||
<slot v-bind="contextRx" />
|
||||
</div>
|
||||
</PageContent>
|
||||
</PageContentWrapper>
|
||||
</ViewWrapper>
|
||||
|
||||
<slot name="extra" v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '/@src/scss/abstracts/all';
|
||||
@import '/@src/scss/layout/navbar';
|
||||
</style>
|
||||
245
src/components/layouts/navbar/NavbarMegamenu.vue
Normal file
245
src/components/layouts/navbar/NavbarMegamenu.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<script setup lang="ts">
|
||||
import type { NavbarMegamenu } from './navbar.types'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
children: NavbarMegamenu[]
|
||||
}>(), {
|
||||
children: () => [],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="navbar-subnavbar-inner">
|
||||
<div v-if="'start' in $slots" class="menu-grid-start">
|
||||
<slot name="start" />
|
||||
</div>
|
||||
|
||||
<div class="menu-grid-wrapper">
|
||||
<div v-if="'top' in $slots" class="menu-grid-top">
|
||||
<slot name="top" />
|
||||
</div>
|
||||
<div class="menu-grid-container has-slimscroll">
|
||||
<div class="menu-grid">
|
||||
<div
|
||||
v-for="group in props.children"
|
||||
:key="group.id"
|
||||
class="menu-block"
|
||||
>
|
||||
<h4 class="block-heading">
|
||||
<VIcon
|
||||
:icon="group.icon"
|
||||
/>
|
||||
<span>{{ group.label }}</span>
|
||||
</h4>
|
||||
<ul class="block-links">
|
||||
<li v-for="link in group.children" :key="link.to">
|
||||
<VLink :to="link.to">
|
||||
<span>{{ link.label }}</span>
|
||||
<VTag
|
||||
v-if="link.tag"
|
||||
color="primary"
|
||||
size="tiny"
|
||||
outlined
|
||||
>
|
||||
{{ link.tag }}
|
||||
</VTag>
|
||||
</VLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="'bottom' in $slots" class="menu-grid-bottom">
|
||||
<slot name="bottom" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="'end' in $slots" class="menu-grid-end">
|
||||
<slot name="end" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.navbar-subnavbar-inner {
|
||||
justify-content: space-between;
|
||||
|
||||
&.is-active {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-grid-start,
|
||||
.menu-grid-end {
|
||||
min-width: 260px;
|
||||
}
|
||||
// .menu-grid-wrapper-wrapper {
|
||||
// border-bottom: 4px solid green;
|
||||
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// flex-grow: 2;
|
||||
// // flex: 1 1 0;
|
||||
|
||||
// > div {
|
||||
// width: 100%;
|
||||
// }
|
||||
// }
|
||||
.menu-grid-top,
|
||||
.menu-grid-bottom {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu-grid-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// flex-wrap: wrap;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.menu-grid-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.menu-grid {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
// flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
gap: 0 4rem;
|
||||
padding: 1.5rem 0;
|
||||
|
||||
// display: grid;
|
||||
// grid-template-rows: 1fr 1fr 1fr;
|
||||
// grid-auto-flow: column dense;
|
||||
|
||||
&.is-horizontal {
|
||||
flex-direction: row;
|
||||
gap: 1rem 8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-block {
|
||||
min-width: 170px;
|
||||
padding-bottom: 2rem;
|
||||
|
||||
.block-heading {
|
||||
font-family: var(--font-alt);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--dark-text);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: top;
|
||||
gap: 0.5rem;
|
||||
|
||||
.iconify {
|
||||
font-size: 18px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.block-links {
|
||||
li {
|
||||
padding-inline-start: 26px;
|
||||
transition:
|
||||
color 0.3s,
|
||||
background-color 0.3s,
|
||||
border-color 0.3s,
|
||||
height 0.3s,
|
||||
width 0.3s;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
a {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-radius: 0;
|
||||
|
||||
a {
|
||||
color: color-mix(in oklab, var(--primary), black 14%);
|
||||
|
||||
.iconify {
|
||||
opacity: 1;
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.router-link-exact-active {
|
||||
color: color-mix(in oklab, var(--primary), black 14%);
|
||||
|
||||
.iconify {
|
||||
opacity: 1;
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: color-mix(in oklab, var(--light-text), white 5%);
|
||||
gap: 0.225rem;
|
||||
|
||||
span {
|
||||
font-family: var(--font);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.lnil,
|
||||
.lnir,
|
||||
.fas,
|
||||
.fal,
|
||||
.fab,
|
||||
.far {
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
line-height: 1.6;
|
||||
height: 1.7em;
|
||||
font-size: 0.65rem;
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
opacity: 0;
|
||||
position: relative;
|
||||
top: 0;
|
||||
margin-inline-start: 12px;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
stroke-width: 2px;
|
||||
fill: var(--primary);
|
||||
transition:
|
||||
color 0.3s,
|
||||
background-color 0.3s,
|
||||
border-color 0.3s,
|
||||
height 0.3s,
|
||||
width 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
a {
|
||||
opacity: 1;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark .navbar-subnavbar-inner {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%);
|
||||
}
|
||||
</style>
|
||||
11
src/components/layouts/navbar/NavbarPageTitleMobile.vue
Normal file
11
src/components/layouts/navbar/NavbarPageTitleMobile.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="page-title has-text-centered">
|
||||
<div class="title-wrap">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="toolbar mobile-toolbar">
|
||||
<slot name="toolbar" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
43
src/components/layouts/navbar/NavbarSubsidebarMobile.vue
Normal file
43
src/components/layouts/navbar/NavbarSubsidebarMobile.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import type { NavbarDropdown, NavbarMegamenu } from './navbar.types'
|
||||
|
||||
const props = defineProps<{
|
||||
label?: string
|
||||
items: (NavbarDropdown | NavbarMegamenu)[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-subsidebar">
|
||||
<div class="inner">
|
||||
<div class="sidebar-title">
|
||||
<slot>
|
||||
<h3>{{ props.label }}</h3>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
class="submenu has-slimscroll"
|
||||
>
|
||||
<template v-for="item of props.items">
|
||||
<VCollapseLinks
|
||||
v-if="'children' in item"
|
||||
:key="item.id"
|
||||
:links="item.children"
|
||||
>
|
||||
{{ item.label }}
|
||||
</VCollapseLinks>
|
||||
<li v-else-if="'to' in item" :key="item.label">
|
||||
<VLink :to="item.to">
|
||||
{{ item.label }}
|
||||
</VLink>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '/@src/scss/layout/mobile-subsidebar';
|
||||
</style>
|
||||
14
src/components/layouts/navbar/navbar.context.ts
Normal file
14
src/components/layouts/navbar/navbar.context.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
import type { NavbarLayoutContext } from './navbar.types'
|
||||
|
||||
export const injectionKey = Symbol('navbar-layout') as InjectionKey<NavbarLayoutContext>
|
||||
|
||||
export function useNavbarLayoutContext() {
|
||||
const context = inject(injectionKey)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useNavbarLayoutContext() is called outside of <NavbarLayout> tree.')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
74
src/components/layouts/navbar/navbar.types.ts
Normal file
74
src/components/layouts/navbar/navbar.types.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
export type NavbarTheme = 'default' | 'colored' | 'fade'
|
||||
|
||||
// -- Item
|
||||
export interface NavbarItemMegamenu {
|
||||
type: 'megamenu'
|
||||
label: string
|
||||
id: string
|
||||
icon: string
|
||||
|
||||
children: NavbarMegamenu[]
|
||||
}
|
||||
export interface NavbarItemDropdown {
|
||||
type: 'dropdown'
|
||||
label: string
|
||||
id: string
|
||||
icon: string
|
||||
|
||||
children: NavbarDropdown[]
|
||||
}
|
||||
export interface NavbarItemAction {
|
||||
type: 'action'
|
||||
label: string
|
||||
icon: string
|
||||
onClick: (event: Event) => void
|
||||
}
|
||||
export interface NavbarItemLink {
|
||||
id: string
|
||||
type: 'link'
|
||||
label: string
|
||||
icon: string
|
||||
to: string
|
||||
}
|
||||
|
||||
export type NavbarItem =
|
||||
| NavbarItemMegamenu
|
||||
| NavbarItemDropdown
|
||||
| NavbarItemAction
|
||||
| NavbarItemLink
|
||||
|
||||
// -- Item Megamenu
|
||||
export interface NavbarMegamenu {
|
||||
id: string
|
||||
label: string
|
||||
icon: string
|
||||
children: NavbarMegamenuLink[]
|
||||
}
|
||||
export interface NavbarMegamenuLink {
|
||||
label: string
|
||||
to: string
|
||||
tag?: string | number
|
||||
}
|
||||
|
||||
// -- Item Dropdown
|
||||
export interface NavbarDropdown {
|
||||
label: string
|
||||
to: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
// -- Context
|
||||
export interface NavbarLayoutContext {
|
||||
theme: ComputedRef<NavbarTheme>
|
||||
links: ComputedRef<NavbarItem[]>
|
||||
|
||||
isMobileSidebarOpen: Ref<boolean>
|
||||
activeMobileSubsidebarId: Ref<string>
|
||||
activeSubnavId: Ref<string | undefined>
|
||||
|
||||
activeSubnav: ComputedRef<NavbarItem | undefined>
|
||||
activeMobileSubsidebar: ComputedRef<NavbarItem | undefined>
|
||||
|
||||
toggleSubnav: (id: string) => void
|
||||
toggleMobileSubnav: (id: string) => void
|
||||
}
|
||||
338
src/components/layouts/navsearch/Navsearch.vue
Normal file
338
src/components/layouts/navsearch/Navsearch.vue
Normal file
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<div
|
||||
class="navbar-navbar-clean"
|
||||
>
|
||||
<div class="navbar-navbar-inner">
|
||||
<div class="left">
|
||||
<slot name="title" />
|
||||
</div>
|
||||
<div class="center">
|
||||
<slot name="search" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot name="toolbar" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="navbar-navbar-lower"
|
||||
:class="[
|
||||
'subtitle' in $slots && 'is-between',
|
||||
!('subtitle' in $slots) && 'is-centered',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="'subtitle' in $slots"
|
||||
class="left"
|
||||
>
|
||||
<slot name="subtitle" />
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
!('subtitle' in $slots) && 'left',
|
||||
'subtitle' in $slots && 'center',
|
||||
]"
|
||||
>
|
||||
<slot name="links" />
|
||||
</div>
|
||||
<div
|
||||
v-if="'toolbar-bottom' in $slots"
|
||||
class="right"
|
||||
>
|
||||
<slot name="toolbar-bottom" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.navbar-navbar-clean {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
background: var(--white);
|
||||
z-index: 15;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&.is-transparent {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border-bottom-color: transparent;
|
||||
|
||||
&.is-solid,
|
||||
&.is-scrolled {
|
||||
background: var(--white);
|
||||
border-bottom-color: var(--fade-grey);
|
||||
}
|
||||
|
||||
&.is-solid {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.is-scrolled {
|
||||
box-shadow: 0 0 8px 0 rgb(0 0 0 / 12%);
|
||||
}
|
||||
|
||||
&:not(.is-scrolled) {
|
||||
.navbar-navbar-lower {
|
||||
&.is-between,
|
||||
&.is-centered {
|
||||
.left,
|
||||
.center {
|
||||
.button:not(:hover) {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: var(--white);
|
||||
border-color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-navbar-inner {
|
||||
display: flex;
|
||||
height: 50px;
|
||||
padding: 0 20px;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 25%;
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
min-width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: var(--font);
|
||||
font-size: 0.95rem;
|
||||
color: var(--muted-grey);
|
||||
letter-spacing: 1px;
|
||||
max-width: 50px;
|
||||
line-height: 1.2;
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 38px;
|
||||
width: 2px;
|
||||
border-inline-end: 1px solid color-mix(in oklab, var(--fade-grey), black 4%);
|
||||
margin: 0 20px 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 25%;
|
||||
margin-inline-start: auto;
|
||||
|
||||
.icon-link {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 34px;
|
||||
width: 34px;
|
||||
border-radius: var(--radius-rounded);
|
||||
margin: 0 4px;
|
||||
transition: all 0.3s; // transition-all test
|
||||
|
||||
&:hover {
|
||||
background: var(--white);
|
||||
border-color: var(--fade-grey);
|
||||
box-shadow: var(--light-box-shadow);
|
||||
}
|
||||
|
||||
.iconify {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
font-size: 18px;
|
||||
stroke-width: 1.6px;
|
||||
color: var(--light-text);
|
||||
transition: stroke 0.3s;
|
||||
vertical-align: 0;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-navbar-lower {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
padding: 0 20px;
|
||||
|
||||
&.is-between,
|
||||
&.is-centered {
|
||||
justify-content: space-between;
|
||||
|
||||
.left,
|
||||
.right, {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
font-family: var(--font);
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
.left,
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.button {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
color: var(--light-text);
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.router-link-exact-active {
|
||||
background: color-mix(in oklab, var(--widget-grey), black 2%);
|
||||
color: var(--dark-text);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.avatar-stack {
|
||||
margin-inline-end: 1rem;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
.button {
|
||||
&.is-circle {
|
||||
min-width: 35px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-centered {
|
||||
.left,
|
||||
.right {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.center {
|
||||
justify-content: center;
|
||||
flex-grow: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
.navbar-navbar-clean {
|
||||
&:not(.is-colored) {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 1%);
|
||||
|
||||
&.is-transparent {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border-bottom-color: transparent;
|
||||
|
||||
&.is-solid,
|
||||
&.is-scrolled {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%);
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 1%);
|
||||
}
|
||||
|
||||
&:not(.is-scrolled) {
|
||||
.navbar-navbar-lower {
|
||||
&.is-between,
|
||||
&.is-centered {
|
||||
.left,
|
||||
.center {
|
||||
.button:not(:hover) {
|
||||
background: transparent !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%) !important;
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), black 2%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-navbar-inner {
|
||||
.left {
|
||||
.separator {
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
.icon-link {
|
||||
background: var(--landing-yyy);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--landing-yyy), black 12%);
|
||||
}
|
||||
|
||||
> .iconify {
|
||||
color: var(--smoke-white);
|
||||
stroke: var(--smoke-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-navbar-lower {
|
||||
&.is-between,
|
||||
&.is-centered {
|
||||
.left,
|
||||
.center {
|
||||
.button {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), black 2%) !important;
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), black 2%) !important;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: color-mix(in oklab, var(--dark-sidebar), white 4%) !important;
|
||||
border-color: color-mix(in oklab, var(--dark-sidebar), white 4%) !important;
|
||||
color: var(--white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
88
src/components/layouts/navsearch/NavsearchInput.vue
Normal file
88
src/components/layouts/navsearch/NavsearchInput.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
const props = withDefaults(defineProps<{
|
||||
suggestions?: T[]
|
||||
}>(), {
|
||||
suggestions: () => [],
|
||||
})
|
||||
const emits = defineEmits<{
|
||||
select: [item: T]
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<string>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="centered-search">
|
||||
<div class="field">
|
||||
<div class="control has-icon">
|
||||
<input
|
||||
v-model="modelValue"
|
||||
type="text"
|
||||
class="input search-input"
|
||||
placeholder="Search records..."
|
||||
>
|
||||
<div class="form-icon">
|
||||
<VIcon
|
||||
icon="lucide:search"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="modelValue"
|
||||
class="form-icon is-right"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="modelValue = ''"
|
||||
@click="modelValue = ''"
|
||||
>
|
||||
<VIcon
|
||||
icon="lucide:x"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.suggestions.length"
|
||||
class="search-results has-slimscroll is-active"
|
||||
>
|
||||
<a
|
||||
v-for="(item, key) in props.suggestions"
|
||||
:key="key"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="search-result"
|
||||
@click="() => emits('select', item)"
|
||||
@keydown.enter.prevent="() => emits('select', item)"
|
||||
>
|
||||
<slot v-bind="{ item }" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.centered-search {
|
||||
width: 100%;
|
||||
|
||||
.field {
|
||||
margin-bottom: 0;
|
||||
|
||||
.control {
|
||||
.input {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.form-icon {
|
||||
&.is-right {
|
||||
inset-inline-start: unset !important;
|
||||
inset-inline-end: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.search-results {
|
||||
top: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
247
src/components/layouts/navsearch/NavsearchLayout.vue
Normal file
247
src/components/layouts/navsearch/NavsearchLayout.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
NavsearchTheme,
|
||||
NavsearchItem,
|
||||
NavsearchScrollBehavior,
|
||||
NavsearchLayoutContext,
|
||||
} from './navsearch.types'
|
||||
import { injectionKey } from './navsearch.context'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
links?: NavsearchItem[]
|
||||
theme?: NavsearchTheme
|
||||
size?: 'default' | 'large' | 'wide' | 'full'
|
||||
scrollBehavior?: NavsearchScrollBehavior
|
||||
}>(),
|
||||
{
|
||||
links: () => [],
|
||||
scrollBehavior: 'fixed',
|
||||
theme: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
)
|
||||
|
||||
const pageTitle = useVueroContext<string>('page-title')
|
||||
const route = useRoute()
|
||||
const isMobileSidebarOpen = ref(false)
|
||||
|
||||
const { y } = useWindowScroll()
|
||||
|
||||
const threshold = 25 // half of the navbar height
|
||||
let lastScrollY = 0
|
||||
|
||||
const direction = ref<'up' | 'down'>('down')
|
||||
const isScrolled = computed(() => {
|
||||
return y.value > threshold
|
||||
})
|
||||
|
||||
// provide context to children
|
||||
const context: NavsearchLayoutContext = {
|
||||
links: computed(() => props.links),
|
||||
theme: computed(() => props.theme),
|
||||
scrollBehavior: computed(() => props.scrollBehavior),
|
||||
|
||||
isMobileSidebarOpen,
|
||||
}
|
||||
provide(injectionKey, context)
|
||||
|
||||
// using reactive context for slots, has better dev experience
|
||||
const contextRx = reactive(context)
|
||||
|
||||
watch(y, (value) => {
|
||||
if (lastScrollY < value) {
|
||||
direction.value = 'down'
|
||||
}
|
||||
else {
|
||||
direction.value = 'up'
|
||||
}
|
||||
|
||||
lastScrollY = value
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
isMobileSidebarOpen.value = false
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="navbar-layout navbar-layout-search"
|
||||
:class="[
|
||||
...(props.scrollBehavior === 'shrink' ? [
|
||||
'is-shrink',
|
||||
isScrolled ? 'is-scrolled' : '',
|
||||
] : []),
|
||||
|
||||
...(props.scrollBehavior === 'reveal' ? [
|
||||
'is-reveal',
|
||||
isScrolled && direction === 'down' ? 'is-scrolled' : '',
|
||||
] : []),
|
||||
]"
|
||||
>
|
||||
<!-- Mobile navigation -->
|
||||
<MobileNavbar v-model="isMobileSidebarOpen">
|
||||
<template #logo>
|
||||
<slot name="logo" v-bind="contextRx" />
|
||||
|
||||
<div class="brand-end">
|
||||
<slot name="toolbar-mobile" v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
</MobileNavbar>
|
||||
|
||||
<Transition name="slide-x">
|
||||
<NavsearchSubsidebarMobile
|
||||
v-if="isMobileSidebarOpen"
|
||||
:items="props.links"
|
||||
>
|
||||
<slot name="navbar-content" v-bind="contextRx" />
|
||||
|
||||
<template #links>
|
||||
<slot name="navbar-links-mobile" />
|
||||
</template>
|
||||
</NavsearchSubsidebarMobile>
|
||||
</Transition>
|
||||
<Transition name="fade">
|
||||
<MobileOverlay
|
||||
v-if="isMobileSidebarOpen"
|
||||
@click="isMobileSidebarOpen = false"
|
||||
/>
|
||||
</Transition>
|
||||
<!-- /Mobile navigation -->
|
||||
|
||||
<!-- Desktop navigation -->
|
||||
<Navsearch
|
||||
:class="[
|
||||
props.theme === 'fade' && 'is-transparent',
|
||||
props.theme === 'fade' && isScrolled && 'is-scrolled',
|
||||
]"
|
||||
>
|
||||
<template #title>
|
||||
<slot name="logo" v-bind="contextRx" />
|
||||
<div v-if="'logo' in $slots" class="separator" />
|
||||
<slot name="navbar-title" v-bind="contextRx">
|
||||
<h1 class="title is-6">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template #search>
|
||||
<slot name="navbar-content" v-bind="contextRx" />
|
||||
</template>
|
||||
|
||||
<template #toolbar>
|
||||
<div class="toolbar desktop-toolbar">
|
||||
<slot name="toolbar" v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="'subnav-start' in $slots" #subtitle>
|
||||
<slot name="subnav-start" v-bind="contextRx" />
|
||||
</template>
|
||||
|
||||
<template #links>
|
||||
<slot name="subnav-links" v-bind="contextRx">
|
||||
<div class="buttons">
|
||||
<VLink
|
||||
v-for="link in props.links"
|
||||
:key="link.to"
|
||||
:to="link.to"
|
||||
class="button"
|
||||
>
|
||||
{{ link.label }}
|
||||
</VLink>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template v-if="'subnav-end' in $slots" #toolbar-bottom>
|
||||
<slot name="subnav-end" v-bind="contextRx" />
|
||||
</template>
|
||||
</Navsearch>
|
||||
<!-- /Desktop navigation -->
|
||||
|
||||
<ViewWrapper top-nav>
|
||||
<template v-if="props.size === 'full'">
|
||||
<div class="is-navbar-lg">
|
||||
<slot name="page-heading">
|
||||
<NavsearchPageTitleMobile>
|
||||
<slot v-bind="contextRx" name="title-mobile">
|
||||
<h1 class="title is-4">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
</slot>
|
||||
|
||||
<template #toolbar>
|
||||
<slot v-bind="contextRx" name="toolbar" />
|
||||
</template>
|
||||
</NavsearchPageTitleMobile>
|
||||
</slot>
|
||||
|
||||
<slot v-bind="contextRx" />
|
||||
</div>
|
||||
</template>
|
||||
<PageContentWrapper v-else :size="props.size">
|
||||
<PageContent
|
||||
class="is-relative"
|
||||
>
|
||||
<div class="is-navbar-lg">
|
||||
<slot name="page-heading">
|
||||
<NavsearchPageTitleMobile>
|
||||
<slot v-bind="contextRx" name="title-mobile">
|
||||
<h1 class="title is-4">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
</slot>
|
||||
|
||||
<template #toolbar>
|
||||
<slot v-bind="contextRx" name="toolbar" />
|
||||
</template>
|
||||
</NavsearchPageTitleMobile>
|
||||
</slot>
|
||||
|
||||
<slot v-bind="contextRx" />
|
||||
</div>
|
||||
</PageContent>
|
||||
</PageContentWrapper>
|
||||
</ViewWrapper>
|
||||
|
||||
<slot v-bind="contextRx" name="extra" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.view-wrapper.has-top-nav) {
|
||||
.is-stuck {
|
||||
top: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.is-shrink,
|
||||
.is-reveal {
|
||||
.navbar-navbar-clean {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.is-shrink {
|
||||
&.is-scrolled {
|
||||
.navbar-navbar-clean:not(:has(.navbar-navbar-inner:focus-within)) {
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-reveal {
|
||||
&.is-scrolled {
|
||||
.navbar-navbar-clean:not(:focus-within) {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user