ENH - align Sky UI with Konsta kitchen sink
@@ -1387,7 +1387,9 @@ onBeforeUnmount(() => {
|
||||
<component
|
||||
:is="Component"
|
||||
v-if="isAppRoute || isDevelopmentRoute"
|
||||
:key="route.path"
|
||||
:key="
|
||||
isDevelopmentRoute ? String(route.name) : route.path
|
||||
"
|
||||
/>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
|
||||
@@ -416,6 +416,14 @@ button {
|
||||
background: var(--phone-performance-glass);
|
||||
box-shadow: none;
|
||||
}
|
||||
.phone-app--performance .sky-segmented--navbar {
|
||||
background: var(--phone-performance-glass);
|
||||
box-shadow: none;
|
||||
}
|
||||
.phone-app--performance .sky-tabbar__pane {
|
||||
background: var(--phone-performance-glass);
|
||||
box-shadow: none;
|
||||
}
|
||||
.phone-app--performance .control-center__backdrop {
|
||||
background: rgb(20 20 24 / 92%);
|
||||
}
|
||||
@@ -662,6 +670,12 @@ button {
|
||||
animation: springboard-unlock 0.72s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.lock-screen {
|
||||
--lock-notification-background: rgb(38 36 40 / 76%);
|
||||
--lock-notification-clear-background: rgb(28 28 32 / 72%);
|
||||
--lock-notification-color: #fff;
|
||||
--lock-notification-muted: rgb(255 255 255 / 65%);
|
||||
--lock-notification-focus: #fff;
|
||||
--lock-notification-text-shadow: 0 1px 3px #0009;
|
||||
position: absolute;
|
||||
z-index: 80;
|
||||
inset: 0;
|
||||
@@ -675,6 +689,17 @@ button {
|
||||
transition: transform 0.42s cubic-bezier(0.2, 0.9, 0.24, 1);
|
||||
will-change: transform, opacity, filter;
|
||||
}
|
||||
.phone-app--light .lock-screen {
|
||||
--lock-notification-background: rgb(247 247 248 / 82%);
|
||||
--lock-notification-clear-background: rgb(247 247 248 / 88%);
|
||||
--lock-notification-color: #111;
|
||||
--lock-notification-muted: rgb(0 0 0 / 55%);
|
||||
--lock-notification-focus: #007aff;
|
||||
--lock-notification-text-shadow: none;
|
||||
}
|
||||
.phone-app--performance .lock-screen {
|
||||
--lock-notification-clear-background: var(--phone-performance-glass);
|
||||
}
|
||||
.lock-screen--dragging {
|
||||
transition: none;
|
||||
}
|
||||
@@ -774,8 +799,8 @@ button {
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 5px 10px;
|
||||
color: #fff;
|
||||
background: #1c1c20b8;
|
||||
color: var(--lock-notification-color);
|
||||
background: var(--lock-notification-clear-background);
|
||||
box-shadow: 0 2px 10px #0004;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
@@ -830,9 +855,9 @@ button {
|
||||
min-height: 76px;
|
||||
border-radius: 18px;
|
||||
padding: 11px;
|
||||
color: #fff;
|
||||
background-color: rgb(38 36 40 / 76%);
|
||||
text-shadow: 0 1px 3px #0009;
|
||||
color: var(--lock-notification-color);
|
||||
background-color: var(--lock-notification-background);
|
||||
text-shadow: var(--lock-notification-text-shadow);
|
||||
touch-action: pan-y;
|
||||
transform: translate3d(var(--notification-swipe), 0, 0);
|
||||
transition: transform 0.28s cubic-bezier(0.22, 0.78, 0.24, 1);
|
||||
@@ -850,7 +875,7 @@ button {
|
||||
cursor: pointer;
|
||||
}
|
||||
.lock-screen__notification.is-actionable:focus-visible {
|
||||
outline: 2px solid #fff;
|
||||
outline: 2px solid var(--lock-notification-focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.lock-screen__notification-icon {
|
||||
@@ -878,7 +903,7 @@ button {
|
||||
}
|
||||
.lock-screen__notification-heading span,
|
||||
.lock-screen__notification-copy small {
|
||||
color: #ffffffa6;
|
||||
color: var(--lock-notification-muted);
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const styles = readFileSync(
|
||||
new URL('../assets/main.css', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
describe('PhoneLockScreen notification theme contract', () => {
|
||||
it('uses explicit light and dark notification material tokens', () => {
|
||||
expect(styles).toMatch(
|
||||
/\.lock-screen\s*\{[^}]*--lock-notification-background:\s*rgb\(38 36 40 \/ 76%\)[^}]*--lock-notification-color:\s*#fff/s,
|
||||
)
|
||||
expect(styles).toMatch(
|
||||
/\.phone-app--light \.lock-screen\s*\{[^}]*--lock-notification-background:\s*rgb\(247 247 248 \/ 82%\)[^}]*--lock-notification-color:\s*#111[^}]*--lock-notification-muted:\s*rgb\(0 0 0 \/ 55%\)/s,
|
||||
)
|
||||
})
|
||||
|
||||
it('applies the adaptive tokens to cards, metadata, focus and clear all', () => {
|
||||
expect(styles).toMatch(
|
||||
/\.lock-screen__notification\s*\{[^}]*color:\s*var\(--lock-notification-color\)[^}]*background-color:\s*var\(--lock-notification-background\)[^}]*text-shadow:\s*var\(--lock-notification-text-shadow\)/s,
|
||||
)
|
||||
expect(styles).toMatch(
|
||||
/\.lock-screen__notifications-clear\s*\{[^}]*color:\s*var\(--lock-notification-color\)[^}]*background:\s*var\(--lock-notification-clear-background\)/s,
|
||||
)
|
||||
expect(styles).toContain('color: var(--lock-notification-muted);')
|
||||
expect(styles).toContain(
|
||||
'outline: 2px solid var(--lock-notification-focus);',
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the solid mode material for the performance clear action', () => {
|
||||
expect(styles).toMatch(
|
||||
/\.phone-app--performance \.lock-screen\s*\{[^}]*--lock-notification-clear-background:\s*var\(--phone-performance-glass\)/s,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -13,7 +13,7 @@ const developmentRoutes: RouteRecordRaw[] = import.meta.env.DEV
|
||||
{
|
||||
component: () => import('@/views/development/SkyUiKitchenSinkView.vue'),
|
||||
name: 'development-sky-ui',
|
||||
path: '/development/sky-ui',
|
||||
path: '/development/sky-ui/:demo?',
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
@@ -188,19 +188,13 @@ describe('SkyNavbar', () => {
|
||||
})
|
||||
|
||||
it('separates and fades the Konsta navbar blur before the scroll content', () => {
|
||||
const blurEnhancement = foundationStyles.slice(
|
||||
foundationStyles.indexOf(
|
||||
'@supports (\n (-webkit-backdrop-filter: blur(2px))',
|
||||
),
|
||||
)
|
||||
|
||||
expect(foundationStyles).toMatch(
|
||||
/\.sky-navbar__background\s*\{[\s\S]*?linear-gradient\([\s\S]*?--sky-navbar-glass[\s\S]*?transparent 100%/,
|
||||
)
|
||||
expect(blurEnhancement).toMatch(
|
||||
expect(foundationStyles).toMatch(
|
||||
/@supports[\s\S]*?-webkit-mask-image: linear-gradient\(#000, transparent\)[\s\S]*?\.sky-navbar__blur\s*\{[\s\S]*?backdrop-filter: blur\(2px\)[\s\S]*?-webkit-mask-image: linear-gradient\([\s\S]*?#000 50%[\s\S]*?transparent 100%/,
|
||||
)
|
||||
expect(blurEnhancement).not.toMatch(
|
||||
expect(foundationStyles).not.toMatch(
|
||||
/\.sky-navbar__background\s*\{[^}]*backdrop-filter/,
|
||||
)
|
||||
})
|
||||
@@ -265,7 +259,7 @@ describe('SkyNavbar', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('provides the Konsta navigation context to segmented subnavbar content', async () => {
|
||||
it('provides the Konsta navbar Glass context to segmented content', async () => {
|
||||
const html = await renderToString(
|
||||
createSSRApp({
|
||||
render: () =>
|
||||
@@ -292,8 +286,10 @@ describe('SkyNavbar', () => {
|
||||
expect(html).toContain('sky-navbar__subnavbar')
|
||||
expect(html).toContain('sky-glass')
|
||||
expect(html).toContain('sky-glass--highlight')
|
||||
expect(html).toContain('sky-segmented--navigation')
|
||||
expect(html).toContain('sky-segmented--navbar')
|
||||
expect(html).not.toContain('sky-segmented--navigation')
|
||||
expect(html).toContain('sky-segmented__highlight')
|
||||
expect(foundationStyles).toContain('height: 56px')
|
||||
})
|
||||
|
||||
it('lets subnavbar content explicitly opt out of navigation Glass', async () => {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { createSSRApp } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
@@ -5,20 +8,75 @@ import { describe, expect, it } from 'vitest'
|
||||
import SkyTabBar from '@/ui/SkyTabBar.vue'
|
||||
|
||||
describe('SkyTabBar', () => {
|
||||
it('keeps the docked tab bar as the default', async () => {
|
||||
const source = readFileSync(
|
||||
fileURLToPath(new URL('./SkyTabBar.vue', import.meta.url)),
|
||||
'utf8',
|
||||
)
|
||||
const foundation = readFileSync(
|
||||
fileURLToPath(new URL('./foundation.css', import.meta.url)),
|
||||
'utf8',
|
||||
)
|
||||
const controls = readFileSync(
|
||||
fileURLToPath(new URL('./controls.css', import.meta.url)),
|
||||
'utf8',
|
||||
)
|
||||
const mainCss = readFileSync(
|
||||
fileURLToPath(new URL('../assets/main.css', import.meta.url)),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
it('uses the Konsta-style floating glass capsule by default', async () => {
|
||||
const html = await renderToString(
|
||||
createSSRApp(SkyTabBar, { label: 'Navigation' }),
|
||||
)
|
||||
|
||||
expect(html).toContain('class="sky-tabbar"')
|
||||
expect(html).not.toContain('sky-tabbar--floating')
|
||||
expect(html).toContain('sky-tabbar sky-tabbar--floating')
|
||||
expect(html).toContain('sky-tabbar--icons')
|
||||
expect(html).toContain('sky-tabbar--labels')
|
||||
expect(html).toContain('sky-glass')
|
||||
expect(html).toContain('sky-tabbar__blur')
|
||||
expect(html).toContain('sky-tabbar__background')
|
||||
})
|
||||
|
||||
it('exposes the shared floating capsule variant', async () => {
|
||||
it('keeps icon and label density explicit', async () => {
|
||||
const html = await renderToString(
|
||||
createSSRApp(SkyTabBar, { floating: true, label: 'Navigation' }),
|
||||
createSSRApp(SkyTabBar, {
|
||||
icons: false,
|
||||
label: 'Navigation',
|
||||
labels: true,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain('sky-tabbar sky-tabbar--floating')
|
||||
expect(html).not.toContain('sky-tabbar--icons')
|
||||
expect(html).toContain('sky-tabbar--labels')
|
||||
})
|
||||
|
||||
it('matches the Konsta iOS glass, density, and moving highlight contract', () => {
|
||||
expect(foundation).toMatch(
|
||||
/\.sky-tabbar\s*\{[^}]*--sky-tabbar-pane-height:\s*48px[^}]*padding-right:\s*calc\(var\(--sky-safe-area-right\) \+ 16px\)[^}]*padding-bottom:\s*calc\(var\(--sky-safe-area-bottom\) \+ 16px\)/s,
|
||||
)
|
||||
expect(foundation).toMatch(
|
||||
/\.sky-tabbar--icons\.sky-tabbar--labels\s*\{[^}]*--sky-tabbar-pane-height:\s*64px/s,
|
||||
)
|
||||
expect(foundation).toMatch(
|
||||
/\.sky-tabbar__highlight-inner,[\s\S]*?inset:\s*4px/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-tab-button__icon\s*\{[^}]*width:\s*28px[^}]*height:\s*28px/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-tab-button__label\s*\{[^}]*font-size:\s*12px[^}]*font-weight:\s*500/s,
|
||||
)
|
||||
expect(source).toContain("attributeFilter: ['class']")
|
||||
expect(source).toContain(
|
||||
"button.classList.contains('sky-tab-button--active')",
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps Performance solid while Ultimate retains shared Glass', () => {
|
||||
expect(mainCss).toMatch(
|
||||
/\.phone-app--performance \.sky-tabbar__pane\s*\{[^}]*background:\s*var\(--phone-performance-glass\)[^}]*box-shadow:\s*none/s,
|
||||
)
|
||||
expect(source).toContain('<SkyGlass class="sky-tabbar__pane">')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,28 +1,224 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
onUpdated,
|
||||
ref,
|
||||
type CSSProperties,
|
||||
} from 'vue'
|
||||
|
||||
import SkyGlass from './controls/SkyGlass.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
withDefaults(
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
floating?: boolean
|
||||
icons?: boolean
|
||||
label: string
|
||||
labels?: boolean
|
||||
}>(),
|
||||
{
|
||||
floating: false,
|
||||
floating: true,
|
||||
icons: true,
|
||||
labels: true,
|
||||
},
|
||||
)
|
||||
|
||||
const links = ref<HTMLElement | null>(null)
|
||||
const hasLinks = ref(false)
|
||||
const pressed = ref(false)
|
||||
const highlightWidth = ref('0%')
|
||||
const highlightTransform = ref('translateX(0%)')
|
||||
const transitionTimingFunction = ref('')
|
||||
let activeIndex = 0
|
||||
let newActiveIndex = 0
|
||||
let touchRect: DOMRect | null = null
|
||||
let touchActive = false
|
||||
let frame = 0
|
||||
let suppressNativeClick = false
|
||||
let observer: MutationObserver | null = null
|
||||
|
||||
const highlightStyle = computed<CSSProperties>(() => ({
|
||||
transform: highlightTransform.value,
|
||||
transitionTimingFunction: transitionTimingFunction.value,
|
||||
width: highlightWidth.value,
|
||||
}))
|
||||
|
||||
function getButtons(): HTMLElement[] {
|
||||
return links.value
|
||||
? Array.from(links.value.querySelectorAll<HTMLElement>('.sky-tab-button'))
|
||||
: []
|
||||
}
|
||||
|
||||
function isRtl(): boolean {
|
||||
return links.value ? getComputedStyle(links.value).direction === 'rtl' : false
|
||||
}
|
||||
|
||||
function indexTransform(index: number): string {
|
||||
return `translateX(${(isRtl() ? -1 : 1) * index * 100}%)`
|
||||
}
|
||||
|
||||
function updateHighlight(): void {
|
||||
const buttons = getButtons()
|
||||
if (!buttons.length) {
|
||||
hasLinks.value = false
|
||||
return
|
||||
}
|
||||
|
||||
hasLinks.value = true
|
||||
const requestedIndex = buttons.findIndex((button) =>
|
||||
button.classList.contains('sky-tab-button--active'),
|
||||
)
|
||||
activeIndex = requestedIndex >= 0 ? requestedIndex : 0
|
||||
newActiveIndex = activeIndex
|
||||
highlightWidth.value = `${100 / buttons.length}%`
|
||||
highlightTransform.value = indexTransform(activeIndex)
|
||||
}
|
||||
|
||||
function startAnimation(transform: string): void {
|
||||
cancelAnimationFrame(frame)
|
||||
frame = requestAnimationFrame(() => {
|
||||
highlightTransform.value = transform
|
||||
transitionTimingFunction.value = 'ease-out'
|
||||
})
|
||||
}
|
||||
|
||||
function updateTouchHighlight(event: PointerEvent): void {
|
||||
const buttons = getButtons()
|
||||
if (!touchRect || !buttons.length) return
|
||||
|
||||
const width = touchRect.width / buttons.length
|
||||
const pointerOffset = isRtl()
|
||||
? touchRect.right - event.clientX - width / 2
|
||||
: event.clientX - touchRect.left - width / 2
|
||||
const offset = Math.max(0, Math.min(pointerOffset, touchRect.width - width))
|
||||
newActiveIndex = Math.max(
|
||||
0,
|
||||
Math.min(buttons.length - 1, Math.round(offset / width)),
|
||||
)
|
||||
pressed.value = true
|
||||
startAnimation(`translateX(${(isRtl() ? -1 : 1) * offset}px)`)
|
||||
}
|
||||
|
||||
function handlePointerDown(event: PointerEvent): void {
|
||||
if (event.pointerType !== 'touch' || !links.value || !hasLinks.value) return
|
||||
touchRect = links.value.getBoundingClientRect()
|
||||
touchActive = true
|
||||
updateTouchHighlight(event)
|
||||
}
|
||||
|
||||
function finishTouch(commit: boolean): void {
|
||||
if (!touchActive) return
|
||||
|
||||
cancelAnimationFrame(frame)
|
||||
touchActive = false
|
||||
pressed.value = false
|
||||
touchRect = null
|
||||
|
||||
const buttons = getButtons()
|
||||
if (commit && activeIndex !== newActiveIndex) {
|
||||
buttons[newActiveIndex]?.click()
|
||||
suppressNativeClick = true
|
||||
}
|
||||
|
||||
const settledIndex = commit ? newActiveIndex : activeIndex
|
||||
highlightTransform.value = indexTransform(settledIndex)
|
||||
transitionTimingFunction.value = ''
|
||||
}
|
||||
|
||||
function handlePointerMove(event: PointerEvent): void {
|
||||
if (touchActive && event.pointerType === 'touch') updateTouchHighlight(event)
|
||||
}
|
||||
|
||||
function handlePointerUp(event: PointerEvent): void {
|
||||
if (event.pointerType === 'touch') finishTouch(true)
|
||||
}
|
||||
|
||||
function handlePointerCancel(event: PointerEvent): void {
|
||||
if (event.pointerType === 'touch') finishTouch(false)
|
||||
}
|
||||
|
||||
function handleClickCapture(event: MouseEvent): void {
|
||||
if (!suppressNativeClick || !event.isTrusted) return
|
||||
suppressNativeClick = false
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateHighlight()
|
||||
observer = new MutationObserver((mutations) => {
|
||||
const needsUpdate = mutations.some(
|
||||
(mutation) =>
|
||||
mutation.type === 'childList' ||
|
||||
(mutation.type === 'attributes' &&
|
||||
mutation.target instanceof Element &&
|
||||
mutation.target.classList.contains('sky-tab-button')),
|
||||
)
|
||||
if (needsUpdate) updateHighlight()
|
||||
})
|
||||
if (links.value) {
|
||||
observer.observe(links.value, {
|
||||
attributeFilter: ['class'],
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
})
|
||||
}
|
||||
links.value?.addEventListener('pointerdown', handlePointerDown)
|
||||
links.value?.addEventListener('click', handleClickCapture, true)
|
||||
document.addEventListener('pointermove', handlePointerMove)
|
||||
document.addEventListener('pointerup', handlePointerUp)
|
||||
document.addEventListener('pointercancel', handlePointerCancel)
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
void nextTick(updateHighlight)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cancelAnimationFrame(frame)
|
||||
observer?.disconnect()
|
||||
observer = null
|
||||
links.value?.removeEventListener('pointerdown', handlePointerDown)
|
||||
links.value?.removeEventListener('click', handleClickCapture, true)
|
||||
document.removeEventListener('pointermove', handlePointerMove)
|
||||
document.removeEventListener('pointerup', handlePointerUp)
|
||||
document.removeEventListener('pointercancel', handlePointerCancel)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav
|
||||
v-bind="$attrs"
|
||||
class="sky-tabbar"
|
||||
:class="{ 'sky-tabbar--floating': floating }"
|
||||
:aria-label="label"
|
||||
:class="{
|
||||
'sky-tabbar--floating': props.floating,
|
||||
'sky-tabbar--icons': props.icons,
|
||||
'sky-tabbar--labels': props.labels,
|
||||
}"
|
||||
:aria-label="props.label"
|
||||
>
|
||||
<span class="sky-tabbar__blur" aria-hidden="true" />
|
||||
<span class="sky-tabbar__background" aria-hidden="true" />
|
||||
<div class="sky-tabbar__inner">
|
||||
<div class="sky-tabbar__pane">
|
||||
<slot />
|
||||
</div>
|
||||
<SkyGlass class="sky-tabbar__pane">
|
||||
<div ref="links" class="sky-tabbar__links">
|
||||
<slot />
|
||||
</div>
|
||||
<span
|
||||
v-if="hasLinks"
|
||||
class="sky-tabbar__highlight"
|
||||
:class="{ 'sky-tabbar__highlight--pressed': pressed }"
|
||||
:style="highlightStyle"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span class="sky-tabbar__highlight-inner" />
|
||||
<span class="sky-tabbar__highlight-thumb" />
|
||||
</span>
|
||||
</SkyGlass>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
@@ -72,6 +72,9 @@ describe('Sky UI graphics modes', () => {
|
||||
expect(performanceCss).toMatch(
|
||||
/\.phone-app--performance \.sky-segmented--navigation\s*\{[^}]*background:\s*var\(--phone-performance-glass\)[^}]*box-shadow:\s*none/s,
|
||||
)
|
||||
expect(performanceCss).toMatch(
|
||||
/\.phone-app--performance \.sky-segmented--navbar\s*\{[^}]*background:\s*var\(--phone-performance-glass\)[^}]*box-shadow:\s*none/s,
|
||||
)
|
||||
expect(controlsCss).toMatch(
|
||||
/\.sky-glass\s*\{[^}]*background:\s*var\(--sky-glass-solid[^}]*box-shadow:\s*var\(--sky-shadow-glass\)/s,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { createSSRApp, h } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import SkyBreadcrumbs from './SkyBreadcrumbs.vue'
|
||||
import SkyBreadcrumbsCollapsed from './SkyBreadcrumbsCollapsed.vue'
|
||||
import SkyBreadcrumbsItem from './SkyBreadcrumbsItem.vue'
|
||||
import SkyBreadcrumbsSeparator from './SkyBreadcrumbsSeparator.vue'
|
||||
|
||||
const controls = readFileSync(
|
||||
new URL('../controls.css', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
describe('SkyBreadcrumbs', () => {
|
||||
it('renders semantic current-page and collapsed navigation controls', async () => {
|
||||
const html = await renderToString(
|
||||
createSSRApp({
|
||||
render: () =>
|
||||
h(
|
||||
SkyBreadcrumbs,
|
||||
{ ariaLabel: 'Catalog breadcrumb' },
|
||||
{
|
||||
default: () => [
|
||||
h(SkyBreadcrumbsItem, { component: 'button' }, () => 'Home'),
|
||||
h(SkyBreadcrumbsSeparator),
|
||||
h(SkyBreadcrumbsCollapsed, {
|
||||
ariaControls: 'hidden-levels',
|
||||
ariaLabel: 'Show hidden levels',
|
||||
expanded: false,
|
||||
}),
|
||||
h(SkyBreadcrumbsSeparator),
|
||||
h(SkyBreadcrumbsItem, { active: true }, () => 'iPhone 12'),
|
||||
],
|
||||
},
|
||||
),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain('<nav')
|
||||
expect(html).toContain('aria-label="Catalog breadcrumb"')
|
||||
expect(html).toContain('aria-current="page"')
|
||||
expect(html).toContain('aria-controls="hidden-levels"')
|
||||
expect(html).toContain('aria-expanded="false"')
|
||||
expect(html.match(/sky-breadcrumbs-collapsed__dot/g)).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('matches Konsta iOS geometry while retaining 44px hit boxes', () => {
|
||||
expect(controls).toMatch(
|
||||
/\.sky-breadcrumbs\s*\{[^}]*height:\s*var\(--sky-touch-target, 44px\)[^}]*gap:\s*12px[^}]*overflow-x:\s*auto[^}]*overflow-y:\s*hidden[^}]*margin-block:\s*-6px[^}]*padding:\s*10px 0[^}]*font-size:\s*17px[^}]*line-height:\s*24px/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-breadcrumbs-separator\s*\{[^}]*width:\s*12px[^}]*min-width:\s*12px[^}]*opacity:\s*0\.35/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-breadcrumbs-collapsed\s*\{[^}]*width:\s*var\(--sky-touch-target, 44px\)[^}]*height:\s*var\(--sky-touch-target, 44px\)[^}]*margin-inline:\s*-7px[^}]*padding:\s*0/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-breadcrumbs-collapsed::before\s*\{[^}]*width:\s*30px[^}]*height:\s*17px[^}]*background:\s*rgba\(0, 0, 0, 0\.15\)/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-app-page--dark \.sky-breadcrumbs-collapsed::before\s*\{[^}]*background:\s*rgba\(255, 255, 255, 0\.15\)/s,
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps rounded outline segments rounded at both ends', () => {
|
||||
expect(controls).toMatch(
|
||||
/\.sky-segmented--outline\.sky-segmented--rounded[\s\S]*?\.sky-segmented-button:first-child\s*\{[^}]*border-start-start-radius:\s*var\(--sky-radius-pill, 999px\)[^}]*border-end-start-radius:\s*var\(--sky-radius-pill, 999px\)/,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-segmented--outline\.sky-segmented--rounded[\s\S]*?\.sky-segmented-button:last-child\s*\{[^}]*border-start-end-radius:\s*var\(--sky-radius-pill, 999px\)[^}]*border-end-end-radius:\s*var\(--sky-radius-pill, 999px\)/,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { createSSRApp, h } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import SkyButton from './SkyButton.vue'
|
||||
|
||||
describe('SkyButton', () => {
|
||||
it('renders a native button with the requested variants', async () => {
|
||||
const html = await renderToString(
|
||||
createSSRApp({
|
||||
render: () =>
|
||||
h(
|
||||
SkyButton,
|
||||
{ outline: true, rounded: true, tonal: true, type: 'submit' },
|
||||
() => 'Continue',
|
||||
),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain('<button')
|
||||
expect(html).toContain('type="submit"')
|
||||
expect(html).toContain('sky-button--outline')
|
||||
expect(html).toContain('sky-button--rounded')
|
||||
expect(html).toContain('sky-button--tonal')
|
||||
expect(html).toContain('Continue')
|
||||
})
|
||||
|
||||
it('keeps focus and pressed feedback on the contextual accent', () => {
|
||||
const uiDirectory = fileURLToPath(new URL('..', import.meta.url))
|
||||
const controls = readFileSync(`${uiDirectory}/controls.css`, 'utf8')
|
||||
const buttonStyles = controls.slice(
|
||||
controls.indexOf('.sky-button:focus-visible'),
|
||||
controls.indexOf('.sky-badge'),
|
||||
)
|
||||
|
||||
expect(buttonStyles).toMatch(
|
||||
/\.sky-button:focus-visible[\s\S]*?outline: 2px solid var\(--sky-app-accent, #007aff\)/,
|
||||
)
|
||||
expect(buttonStyles).toMatch(
|
||||
/\.sky-button--primary:active:not\(:disabled\)\s*\{\s*background: var\(--sky-app-accent, #007aff\);\s*filter: brightness\(0\.86\)/,
|
||||
)
|
||||
expect(buttonStyles).toMatch(
|
||||
/\.sky-button--tonal:active:not\(:disabled\)\s*\{\s*background: var\(--sky-app-accent-soft, rgba\(0, 122, 255, 0\.15\)\);\s*filter: brightness\(0\.92\)/,
|
||||
)
|
||||
expect(buttonStyles).not.toContain('--sky-app-accent-shade')
|
||||
})
|
||||
|
||||
it('keeps outline hover border, surface, and text in one color state', () => {
|
||||
const uiDirectory = fileURLToPath(new URL('..', import.meta.url))
|
||||
const controls = readFileSync(`${uiDirectory}/controls.css`, 'utf8')
|
||||
const buttonStyles = controls.slice(
|
||||
controls.indexOf('.sky-button:focus-visible'),
|
||||
controls.indexOf('.sky-badge'),
|
||||
)
|
||||
|
||||
expect(buttonStyles).toMatch(
|
||||
/@media \(hover: hover\) and \(pointer: fine\)[\s\S]*?\.sky-button--outline:hover:not\(:disabled\)\s*\{[\s\S]*?border-color:\s*var\(--sky-app-accent, #007aff\);[\s\S]*?background:\s*var\(--sky-app-accent, #007aff\);[\s\S]*?color:\s*#ffffff;/,
|
||||
)
|
||||
expect(buttonStyles).toMatch(
|
||||
/\.sky-button--danger\.sky-button--outline:hover:not\(:disabled\)\s*\{[\s\S]*?border-color:\s*var\(--sky-danger, #dc2626\);[\s\S]*?background:\s*var\(--sky-danger, #dc2626\);[\s\S]*?color:\s*#ffffff;/,
|
||||
)
|
||||
expect(buttonStyles).toContain(
|
||||
'color var(--sky-transition-fast, 100ms) ease,',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { createSSRApp, h } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import SkyChip from './SkyChip.vue'
|
||||
|
||||
const controls = readFileSync(
|
||||
new URL('../controls.css', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
describe('SkyChip', () => {
|
||||
it('matches Konsta iOS chip geometry and colors', () => {
|
||||
expect(controls).toMatch(
|
||||
/\.sky-chip\s*\{[^}]*height:\s*28px[^}]*min-height:\s*28px[^}]*padding:\s*0 12px[^}]*border:\s*0[^}]*background:\s*rgba\(0, 0, 0, 0\.1\)[^}]*font-size:\s*14px[^}]*font-weight:\s*400/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-app-page--dark \.sky-chip:not\(\.sky-chip--outline\):not\(\.sky-chip--selected\)\s*\{[^}]*background:\s*rgba\(255, 255, 255, 0\.1\)/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-chip--outline\s*\{[^}]*border:\s*1px solid\s*var\(--sky-chip-outline-border, rgba\(0, 0, 0, 0\.2\)\)[^}]*background:\s*transparent[^}]*color:\s*var\(--sky-chip-outline-text, currentColor\)/s,
|
||||
)
|
||||
})
|
||||
|
||||
it('matches Konsta media and delete geometry while retaining 44px hit areas', () => {
|
||||
expect(controls).toMatch(
|
||||
/\.sky-chip__media\s*\{[^}]*margin-block:\s*-4px[^}]*margin-inline-end:\s*4px[^}]*margin-inline-start:\s*-12px/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-chip__delete\s*\{[^}]*width:\s*24px[^}]*height:\s*28px[^}]*margin-inline-end:\s*-8px[^}]*margin-inline-start:\s*4px/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-chip__delete::before\s*\{[^}]*inset:\s*-8px -10px/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/button\.sky-chip::before,[\s\S]*?a\.sky-chip::before\s*\{[^}]*inset:\s*-8px -4px/s,
|
||||
)
|
||||
})
|
||||
|
||||
it('renders one accessible delete action with the Konsta delete icon', async () => {
|
||||
const html = await renderToString(
|
||||
createSSRApp({
|
||||
render: () =>
|
||||
h(
|
||||
SkyChip,
|
||||
{
|
||||
deleteButton: true,
|
||||
deleteLabel: 'Delete Adam Smith',
|
||||
},
|
||||
() => 'Adam Smith',
|
||||
),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain('sky-chip--with-delete')
|
||||
expect(html).toContain('aria-label="Delete Adam Smith"')
|
||||
expect(html).toContain('sky-chip__delete-icon')
|
||||
expect(html.match(/role="button"/g)).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import SkyChipDeleteIcon from './SkyChipDeleteIcon.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -104,7 +106,7 @@ function handleDelete(event: KeyboardEvent | MouseEvent): void {
|
||||
@keydown.space.prevent.stop="handleDelete"
|
||||
>
|
||||
<slot name="delete">
|
||||
<span class="sky-chip__delete-icon" aria-hidden="true" />
|
||||
<SkyChipDeleteIcon />
|
||||
</slot>
|
||||
</span>
|
||||
</component>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<svg
|
||||
class="sky-chip__delete-icon"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 28 28"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M14 0C21.732 0 28 6.268 28 14S21.732 28 14 28 0 21.732 0 14 6.268 0 14 0Zm4.939 6.939L14 11.879 9.061 6.939 6.939 9.061 11.879 14l-4.94 4.939 2.122 2.122L14 16.121l4.939 4.94 2.122-2.122L16.121 14l4.94-4.939-2.122-2.122Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,58 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { createSSRApp, h } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import SkyFab from './SkyFab.vue'
|
||||
|
||||
describe('SkyFab', () => {
|
||||
it('composes the iOS glass layers with native button semantics', async () => {
|
||||
const html = await renderToString(
|
||||
createSSRApp({
|
||||
render: () =>
|
||||
h(
|
||||
SkyFab,
|
||||
{ ariaLabel: 'Create', text: 'Create' },
|
||||
{ icon: () => '+' },
|
||||
),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain('<button')
|
||||
expect(html).toContain('type="button"')
|
||||
expect(html).toContain('aria-label="Create"')
|
||||
expect(html).toContain('sky-glass')
|
||||
expect(html).toContain('sky-fab__accent-layer')
|
||||
expect(html).toContain('sky-fab__dark-accent-layer')
|
||||
expect(html).toContain('sky-fab__surface-layer')
|
||||
expect(html).toContain('Create')
|
||||
})
|
||||
|
||||
it('keeps every FAB shadow layer on the contextual accent', () => {
|
||||
const uiDirectory = fileURLToPath(new URL('..', import.meta.url))
|
||||
const controls = readFileSync(`${uiDirectory}/controls.css`, 'utf8')
|
||||
const tokens = readFileSync(`${uiDirectory}/tokens.css`, 'utf8')
|
||||
const fabStyles = controls.slice(
|
||||
controls.indexOf('.sky-glass.sky-fab {'),
|
||||
controls.indexOf('.sky-glass {'),
|
||||
)
|
||||
const fabTokens = tokens.slice(
|
||||
tokens.indexOf('--sky-shadow-glass-fab:'),
|
||||
tokens.indexOf('--sky-shadow-glass-thumb:'),
|
||||
)
|
||||
|
||||
expect(fabStyles).toContain('background: var(--sky-app-accent, #007aff)')
|
||||
expect(fabStyles).toMatch(
|
||||
/\.sky-fab__surface-layer\s*\{[\s\S]*?var\(--sky-fab-accent-inset-start\) var\(--sky-app-accent, #007aff\)/,
|
||||
)
|
||||
expect(fabStyles).toMatch(
|
||||
/\.sky-fab__dark-accent-layer\s*\{[\s\S]*?inset 0 -5px 5px var\(--sky-app-accent, #007aff\)/,
|
||||
)
|
||||
expect(fabStyles).not.toContain('--sky-app-accent-shade')
|
||||
expect(fabStyles).not.toContain('scale(0.96)')
|
||||
expect(fabTokens).toContain('--sky-fab-accent-inset-start')
|
||||
expect(fabTokens).not.toContain('rgba(10, 132, 255, 0.25)')
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, useSlots } from 'vue'
|
||||
|
||||
import SkyGlass from './SkyGlass.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -59,17 +61,24 @@ function handleClick(event: MouseEvent): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="component"
|
||||
<SkyGlass
|
||||
v-bind="{ ...$attrs, ...elementProps }"
|
||||
:component="component"
|
||||
:disabled="disabled"
|
||||
:href="href"
|
||||
:type="type"
|
||||
class="sky-fab"
|
||||
:class="{
|
||||
'sky-fab--disabled': disabled,
|
||||
'sky-fab--icon-only': !hasText,
|
||||
'sky-fab--with-text': hasText,
|
||||
}"
|
||||
role="button"
|
||||
@click="handleClick"
|
||||
>
|
||||
<span class="sky-fab__accent-layer" aria-hidden="true"></span>
|
||||
<span class="sky-fab__dark-accent-layer" aria-hidden="true"></span>
|
||||
<span class="sky-fab__surface-layer" aria-hidden="true"></span>
|
||||
<span v-if="hasText && textPosition === 'before'" class="sky-fab__text">
|
||||
{{ text }}<slot name="text" />
|
||||
</span>
|
||||
@@ -80,5 +89,5 @@ function handleClick(event: MouseEvent): void {
|
||||
{{ text }}<slot name="text" />
|
||||
</span>
|
||||
<slot />
|
||||
</component>
|
||||
</SkyGlass>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { createSSRApp, h } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import SkyField from '@/ui/controls/SkyField.vue'
|
||||
|
||||
describe('SkyField numeric constraints', () => {
|
||||
describe('SkyField', () => {
|
||||
it('forwards min, max, and step to the native input', async () => {
|
||||
const app = createSSRApp(SkyField, {
|
||||
ariaLabel: 'Primary frequency',
|
||||
@@ -73,6 +75,97 @@ describe('SkyField numeric constraints', () => {
|
||||
expect(input).toContain('spellcheck="false"')
|
||||
})
|
||||
|
||||
it('renders a native date input without changing its accessible label', async () => {
|
||||
const app = createSSRApp(SkyField, {
|
||||
id: 'birthday',
|
||||
label: 'Birthday',
|
||||
modelValue: '2014-04-30',
|
||||
type: 'date',
|
||||
})
|
||||
|
||||
const html = await renderToString(app)
|
||||
|
||||
expect(html).toContain('<label class="sky-field__label" for="birthday"')
|
||||
expect(html).toMatch(
|
||||
/<input[^>]+id="birthday"[^>]+type="date"[^>]+value="2014-04-30"/,
|
||||
)
|
||||
})
|
||||
|
||||
it('renders typed select options and native options from the default slot', async () => {
|
||||
const options = [
|
||||
{ label: 'Male', value: 'male' },
|
||||
{ disabled: true, label: 'Unavailable', value: 0 },
|
||||
] as const
|
||||
const app = createSSRApp({
|
||||
render: () =>
|
||||
h(
|
||||
SkyField,
|
||||
{
|
||||
ariaLabel: 'Gender',
|
||||
modelValue: 'male',
|
||||
options,
|
||||
placeholder: 'Please choose...',
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
default: () => h('option', { value: 'custom' }, 'Custom'),
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
const html = await renderToString(app)
|
||||
const select = html.match(/<select[^>]+>/)?.[0] ?? ''
|
||||
|
||||
expect(select).toContain('class="sky-field__input sky-field__select"')
|
||||
expect(select).toContain('aria-label="Gender"')
|
||||
expect(select).toContain('value="male"')
|
||||
expect(html).toContain(
|
||||
'<option disabled value="">Please choose...</option>',
|
||||
)
|
||||
expect(html).toContain('<option value="male">Male</option>')
|
||||
expect(html).toContain('<option disabled value="0">Unavailable</option>')
|
||||
expect(html).toContain('<option value="custom">Custom</option>')
|
||||
})
|
||||
|
||||
it('raises floating labels only after the field has a value', async () => {
|
||||
const emptyApp = createSSRApp(SkyField, {
|
||||
floatingLabel: true,
|
||||
label: 'Name',
|
||||
placeholder: 'Your name',
|
||||
})
|
||||
const valuedApp = createSSRApp(SkyField, {
|
||||
floatingLabel: true,
|
||||
label: 'Name',
|
||||
modelValue: 'Sky',
|
||||
placeholder: 'Your name',
|
||||
})
|
||||
|
||||
const emptyHtml = await renderToString(emptyApp)
|
||||
const valuedHtml = await renderToString(valuedApp)
|
||||
|
||||
expect(emptyHtml).toContain('sky-field--floating-label')
|
||||
expect(emptyHtml).not.toContain('sky-field--floating-raised')
|
||||
expect(valuedHtml).toContain('sky-field--floating-label')
|
||||
expect(valuedHtml).toContain('sky-field--floating-raised')
|
||||
})
|
||||
|
||||
it('keeps floating-label controls touch-sized and reduced-motion aware', () => {
|
||||
const controls = readFileSync(
|
||||
new URL('../controls.css', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
expect(controls).toMatch(
|
||||
/\.sky-field__input\s*\{[^}]*min-height:\s*var\(--sky-touch-target, 44px\)/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-field--floating-label:not\(\.sky-field--inline\) \.sky-field__control\s*\{[^}]*min-height:\s*58px;[^}]*padding-top:\s*14px/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/@media \(prefers-reduced-motion: reduce\)\s*\{[^}]*\.sky-field__label/s,
|
||||
)
|
||||
})
|
||||
|
||||
it('renders an explicitly labelled clear control only for a non-empty value', async () => {
|
||||
const app = createSSRApp(SkyField, {
|
||||
clearButton: true,
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, useId, useSlots, type CSSProperties } from 'vue'
|
||||
import { computed, ref, useId, useSlots, type CSSProperties } from 'vue'
|
||||
|
||||
import { useComposedFieldValue } from '@/ui/controls/useComposedFieldValue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
type FieldValue = number | string
|
||||
type FieldOption = {
|
||||
disabled?: boolean
|
||||
label: string
|
||||
value: FieldValue
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -23,6 +28,7 @@ const props = withDefaults(
|
||||
clearLabel?: string
|
||||
disabled?: boolean
|
||||
error?: string
|
||||
floatingLabel?: boolean
|
||||
help?: string
|
||||
id?: string
|
||||
inputMode?:
|
||||
@@ -43,6 +49,7 @@ const props = withDefaults(
|
||||
modelValue?: FieldValue
|
||||
name?: string
|
||||
outline?: boolean
|
||||
options?: readonly FieldOption[]
|
||||
pattern?: string
|
||||
placeholder?: string
|
||||
readonly?: boolean
|
||||
@@ -51,11 +58,13 @@ const props = withDefaults(
|
||||
spellcheck?: boolean
|
||||
step?: number | string
|
||||
type?:
|
||||
| 'date'
|
||||
| 'datetime-local'
|
||||
| 'email'
|
||||
| 'number'
|
||||
| 'password'
|
||||
| 'search'
|
||||
| 'select'
|
||||
| 'tel'
|
||||
| 'text'
|
||||
| 'textarea'
|
||||
@@ -72,6 +81,7 @@ const props = withDefaults(
|
||||
clearLabel: '',
|
||||
disabled: false,
|
||||
error: '',
|
||||
floatingLabel: false,
|
||||
help: '',
|
||||
id: undefined,
|
||||
inputMode: undefined,
|
||||
@@ -84,6 +94,7 @@ const props = withDefaults(
|
||||
modelValue: undefined,
|
||||
name: undefined,
|
||||
outline: false,
|
||||
options: () => [],
|
||||
pattern: undefined,
|
||||
placeholder: '',
|
||||
readonly: false,
|
||||
@@ -107,6 +118,7 @@ const emit = defineEmits<{
|
||||
|
||||
const generatedId = useId()
|
||||
const slots = useSlots()
|
||||
const isFocused = ref(false)
|
||||
const inputId = computed(() => props.id || generatedId)
|
||||
const helpId = computed(() => `${inputId.value}-help`)
|
||||
const errorId = computed(() => `${inputId.value}-error`)
|
||||
@@ -126,6 +138,12 @@ const hasClearButton = computed(
|
||||
Boolean(props.clearLabel) &&
|
||||
localValue.value.length > 0,
|
||||
)
|
||||
const isFloatingRaised = computed(
|
||||
() =>
|
||||
Boolean(props.label) &&
|
||||
props.floatingLabel &&
|
||||
(isFocused.value || localValue.value.length > 0),
|
||||
)
|
||||
|
||||
const describedBy = computed(() => {
|
||||
const ids: string[] = []
|
||||
@@ -138,6 +156,7 @@ function fieldValue(event: Event): string {
|
||||
const target = event.target
|
||||
if (
|
||||
!(target instanceof HTMLInputElement) &&
|
||||
!(target instanceof HTMLSelectElement) &&
|
||||
!(target instanceof HTMLTextAreaElement)
|
||||
) {
|
||||
return ''
|
||||
@@ -155,6 +174,16 @@ function handleCompositionEnd(event: CompositionEvent): void {
|
||||
endComposition(fieldValue(event))
|
||||
}
|
||||
|
||||
function handleFocus(event: FocusEvent): void {
|
||||
isFocused.value = true
|
||||
emit('focus', event)
|
||||
}
|
||||
|
||||
function handleBlur(event: FocusEvent): void {
|
||||
isFocused.value = false
|
||||
emit('blur', event)
|
||||
}
|
||||
|
||||
function clear(): void {
|
||||
if (props.disabled || props.readonly) return
|
||||
clearValue()
|
||||
@@ -169,6 +198,8 @@ function clear(): void {
|
||||
:class="{
|
||||
'sky-field--disabled': disabled,
|
||||
'sky-field--error': Boolean(error),
|
||||
'sky-field--floating-label': Boolean(label) && floatingLabel,
|
||||
'sky-field--floating-raised': isFloatingRaised,
|
||||
'sky-field--inline': layout === 'inline',
|
||||
'sky-field--outline': outline,
|
||||
'sky-field--has-leading': Boolean(slots.leading),
|
||||
@@ -200,13 +231,43 @@ function clear(): void {
|
||||
:spellcheck="spellcheck"
|
||||
:style="inputStyle"
|
||||
:value="localValue"
|
||||
@blur="emit('blur', $event)"
|
||||
@blur="handleBlur"
|
||||
@change="emit('change', $event)"
|
||||
@compositionend="handleCompositionEnd"
|
||||
@compositionstart="startComposition"
|
||||
@focus="emit('focus', $event)"
|
||||
@focus="handleFocus"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<select
|
||||
v-else-if="type === 'select'"
|
||||
:id="inputId"
|
||||
class="sky-field__input sky-field__select"
|
||||
:aria-describedby="describedBy"
|
||||
:aria-invalid="error ? true : undefined"
|
||||
:aria-label="ariaLabel || undefined"
|
||||
:disabled="disabled"
|
||||
:name="name"
|
||||
:required="required"
|
||||
:style="inputStyle"
|
||||
:value="localValue"
|
||||
@blur="handleBlur"
|
||||
@change="emit('change', $event)"
|
||||
@focus="handleFocus"
|
||||
@input="handleInput"
|
||||
>
|
||||
<option v-if="placeholder" disabled value="">
|
||||
{{ placeholder }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(option, index) in options"
|
||||
:key="`${typeof option.value}:${String(option.value)}:${index}`"
|
||||
:disabled="option.disabled"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
<slot />
|
||||
</select>
|
||||
<input
|
||||
v-else
|
||||
:id="inputId"
|
||||
@@ -232,11 +293,11 @@ function clear(): void {
|
||||
:style="inputStyle"
|
||||
:type="type"
|
||||
:value="localValue"
|
||||
@blur="emit('blur', $event)"
|
||||
@blur="handleBlur"
|
||||
@change="emit('change', $event)"
|
||||
@compositionend="handleCompositionEnd"
|
||||
@compositionstart="startComposition"
|
||||
@focus="emit('focus', $event)"
|
||||
@focus="handleFocus"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<span v-if="$slots.trailing" class="sky-field__trailing">
|
||||
|
||||
@@ -12,6 +12,7 @@ const props = withDefaults(
|
||||
| 'button'
|
||||
| 'div'
|
||||
| 'label'
|
||||
| 'nav'
|
||||
| 'section'
|
||||
| 'span'
|
||||
disabled?: boolean
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { createSSRApp, h } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import SkyRadio from './SkyRadio.vue'
|
||||
|
||||
describe('SkyRadio', () => {
|
||||
it('keeps native radio semantics and accessible label wiring', async () => {
|
||||
const html = await renderToString(
|
||||
createSSRApp({
|
||||
render: () =>
|
||||
h(
|
||||
SkyRadio,
|
||||
{
|
||||
modelValue: 'apps',
|
||||
name: 'view',
|
||||
value: 'apps',
|
||||
},
|
||||
{ default: () => 'Apps' },
|
||||
),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain('type="radio"')
|
||||
expect(html).toContain('checked')
|
||||
expect(html).toContain('name="view"')
|
||||
expect(html).toContain('value="apps"')
|
||||
expect(html).toContain('aria-labelledby=')
|
||||
expect(html).toContain('sky-radio--checked')
|
||||
expect(html).toContain('Apps')
|
||||
})
|
||||
|
||||
it('uses the 22px mark footprint without shrinking the 44px input target', () => {
|
||||
const uiDirectory = fileURLToPath(new URL('..', import.meta.url))
|
||||
const controls = readFileSync(`${uiDirectory}/controls.css`, 'utf8')
|
||||
const radioStyles = controls.slice(
|
||||
controls.indexOf('.sky-radio {'),
|
||||
controls.indexOf('.sky-range {'),
|
||||
)
|
||||
|
||||
expect(radioStyles).toMatch(
|
||||
/\.sky-radio\s*\{\s*min-width: 22px;\s*min-height: 22px/,
|
||||
)
|
||||
expect(radioStyles).toMatch(
|
||||
/\.sky-radio__input\s*\{[\s\S]*?width: var\(--sky-touch-target, 44px\)[\s\S]*?height: var\(--sky-touch-target, 44px\)[\s\S]*?inset-inline-start: -11px/,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -45,6 +45,27 @@ async function renderNavigation(activeIndex = 1): Promise<string> {
|
||||
}
|
||||
|
||||
describe('SkySegmented navigation', () => {
|
||||
it('keeps ordinary iOS segments at Konsta height and Strong colors', () => {
|
||||
expect(controls).toMatch(
|
||||
/\.sky-segmented\s*\{[^}]*height:\s*34px[^}]*min-height:\s*34px/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-segmented-button\s*\{[^}]*height:\s*34px[^}]*min-height:\s*34px[^}]*padding:\s*0 8px[^}]*font-size:\s*15px[^}]*font-weight:\s*500/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-segmented--strong:not\(\.sky-segmented--navigation\):not\([\s\S]*?height:\s*38px[^}]*padding:\s*2px[^}]*background:\s*rgba\(0, 0, 0, 0\.05\)/,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-segmented--strong:not\(\.sky-segmented--navigation\):not\([\s\S]*?\.sky-segmented-button\s*\{[^}]*color:\s*var\(--sky-text, #000000\)/,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-segmented--strong:not\(\.sky-segmented--navigation\):not\([\s\S]*?\.sky-segmented-button--active\s*\{[^}]*color:\s*#000000[^}]*0 1px 3px rgba\(0, 0, 0, 0\.1\)[^}]*0 1px 2px -1px rgba\(0, 0, 0, 0\.1\)/,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-segmented--rounded:not\(\.sky-segmented--outline\):not\([\s\S]*?\.sky-segmented-button--active\s*\{[^}]*border-radius:\s*var\(--sky-radius-pill, 999px\)/,
|
||||
)
|
||||
})
|
||||
|
||||
it('renders the Konsta iOS glass stack and one moving highlight', async () => {
|
||||
const html = await renderNavigation()
|
||||
|
||||
@@ -144,6 +165,21 @@ describe('SkySegmented navigation', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps Navbar segmented visuals at Konsta size with 44px targets', () => {
|
||||
expect(controls).toMatch(
|
||||
/\.sky-glass\.sky-segmented--navbar\s*\{[^}]*height:\s*var\(--sky-touch-target, 44px\)[^}]*gap:\s*4px[^}]*padding:\s*0 4px/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-segmented--navbar \.sky-segmented-button\s*\{[^}]*min-height:\s*var\(--sky-touch-target, 44px\)[^}]*padding:\s*0 8px[^}]*font-size:\s*15px[^}]*font-weight:\s*500/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-segmented--navbar \.sky-segmented__highlight\s*\{[^}]*top:\s*5px[^}]*bottom:\s*5px[^}]*background:\s*#ffffff/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-app-page--dark \.sky-segmented--navbar \.sky-segmented__highlight\s*\{[^}]*background:\s*rgba\(255, 255, 255, 0\.75\)/s,
|
||||
)
|
||||
})
|
||||
|
||||
it('calculates the same sliding pill for full-width five-item navigation', async () => {
|
||||
const html = await renderToString(
|
||||
createSSRApp({
|
||||
|
||||
@@ -34,13 +34,17 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const insideNavbar = useSkyNavbar()
|
||||
const isNavigation = computed(() => props.navigation ?? insideNavbar)
|
||||
const rootComponent = computed(() => (isNavigation.value ? SkyGlass : 'div'))
|
||||
const isNavbarSegmented = computed(
|
||||
() => insideNavbar && typeof props.navigation === 'undefined',
|
||||
)
|
||||
const isNavigation = computed(() => props.navigation === true)
|
||||
const usesGlass = computed(() => isNavigation.value || isNavbarSegmented.value)
|
||||
const rootComponent = computed(() => (usesGlass.value ? SkyGlass : 'div'))
|
||||
const indicatorStyle = computed<CSSProperties | undefined>(() => {
|
||||
const itemCount = Number.isFinite(props.itemCount)
|
||||
? Math.max(0, Math.floor(props.itemCount))
|
||||
: 0
|
||||
if (!isNavigation.value || !props.strong || itemCount === 0) return undefined
|
||||
if (!usesGlass.value || !props.strong || itemCount === 0) return undefined
|
||||
|
||||
const requestedIndex = Number.isFinite(props.activeIndex)
|
||||
? Math.floor(props.activeIndex)
|
||||
@@ -63,10 +67,11 @@ const indicatorStyle = computed<CSSProperties | undefined>(() => {
|
||||
<component
|
||||
:is="rootComponent"
|
||||
v-bind="$attrs"
|
||||
:highlight="isNavigation ? glassHighlight : undefined"
|
||||
:highlight="usesGlass ? glassHighlight : undefined"
|
||||
class="sky-segmented"
|
||||
:class="{
|
||||
'sky-segmented--navigation': isNavigation,
|
||||
'sky-segmented--navbar': isNavbarSegmented,
|
||||
'sky-segmented--compact': compact,
|
||||
'sky-segmented--outline': outline,
|
||||
'sky-segmented--raised': raised,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { createSSRApp, h } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import SkyStepper from './SkyStepper.vue'
|
||||
|
||||
describe('SkyStepper', () => {
|
||||
it('matches Konsta iOS small, medium and large geometry', () => {
|
||||
const uiDirectory = fileURLToPath(new URL('..', import.meta.url))
|
||||
const controls = readFileSync(`${uiDirectory}/controls.css`, 'utf8')
|
||||
|
||||
expect(controls).toMatch(
|
||||
/\.sky-stepper\s*\{[^}]*height:\s*34px[^}]*min-height:\s*34px/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-stepper__button\s*\{[^}]*width:\s*40px[^}]*min-width:\s*40px[^}]*height:\s*34px[^}]*min-height:\s*34px/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-stepper__value\s*\{[^}]*width:\s*44px[^}]*min-width:\s*44px[^}]*height:\s*34px[^}]*min-height:\s*34px/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-stepper--small\s*\{[^}]*height:\s*28px[^}]*min-height:\s*28px/s,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-stepper--large\s*\{[^}]*height:\s*48px[^}]*min-height:\s*48px/s,
|
||||
)
|
||||
})
|
||||
|
||||
it('renders a native, labelled text input between the step buttons', async () => {
|
||||
const html = await renderToString(
|
||||
createSSRApp({
|
||||
render: () =>
|
||||
h(SkyStepper, {
|
||||
decrementLabel: 'Decrease value',
|
||||
incrementLabel: 'Increase value',
|
||||
input: true,
|
||||
inputLabel: 'Stepper value',
|
||||
modelValue: 1,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain('sky-stepper--input')
|
||||
expect(html).toContain('aria-label="Decrease value"')
|
||||
expect(html).toContain('aria-label="Stepper value"')
|
||||
expect(html).toContain('aria-label="Increase value"')
|
||||
expect(html).toContain('value="1"')
|
||||
})
|
||||
|
||||
it('keeps the value and input on the contextual accent', () => {
|
||||
const uiDirectory = fileURLToPath(new URL('..', import.meta.url))
|
||||
const controls = readFileSync(`${uiDirectory}/controls.css`, 'utf8')
|
||||
|
||||
expect(controls).toMatch(
|
||||
/\.sky-stepper__button\s*\{[\s\S]*?background: var\(--sky-app-accent, #007aff\);[\s\S]*?color: #ffffff;/,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-stepper__value\s*\{[\s\S]*?border-top: 2px solid var\(--sky-app-accent, #007aff\);[\s\S]*?border-bottom: 2px solid var\(--sky-app-accent, #007aff\);[\s\S]*?color: var\(--sky-app-accent, #007aff\);[\s\S]*?caret-color: var\(--sky-app-accent, #007aff\);/,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-stepper--outline \.sky-stepper__button\s*\{[\s\S]*?border: 2px solid var\(--sky-app-accent, #007aff\);[\s\S]*?background: transparent;[\s\S]*?color: var\(--sky-app-accent, #007aff\);/,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-stepper__decrement\s*\{[\s\S]*?border-start-start-radius: inherit;[\s\S]*?border-end-start-radius: inherit;/,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-stepper__increment\s*\{[\s\S]*?border-start-end-radius: inherit;[\s\S]*?border-end-end-radius: inherit;/,
|
||||
)
|
||||
expect(controls).not.toMatch(
|
||||
/\.sky-stepper__value\s*\{[\s\S]*?color: inherit;/,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -93,4 +93,19 @@ describe('SkyToggle', () => {
|
||||
expect(reducedMotion).toContain('.sky-toggle__thumb-wrap')
|
||||
expect(reducedMotion).toContain('transform: translateX(-22px)')
|
||||
})
|
||||
|
||||
it('keeps trailing list toggles compact without shrinking their touch target', () => {
|
||||
const uiDirectory = fileURLToPath(new URL('..', import.meta.url))
|
||||
const controls = readFileSync(`${uiDirectory}/controls.css`, 'utf8')
|
||||
|
||||
expect(controls).toMatch(
|
||||
/\.sky-list-item__after > \.sky-toggle\s*\{\s*margin-block: -8px;/,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-toggle,\s*\.sky-radio\s*\{[\s\S]*?min-height: var\(--sky-touch-target, 44px\)/,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-list-item__row\s*\{[\s\S]*?padding: 12px 16px;/,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { createSSRApp, h } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import SkyToolbar from './SkyToolbar.vue'
|
||||
import SkyToolbarPane from './SkyToolbarPane.vue'
|
||||
|
||||
describe('SkyToolbar', () => {
|
||||
it('renders separate fade layers and glass panes', async () => {
|
||||
const html = await renderToString(
|
||||
createSSRApp({
|
||||
render: () =>
|
||||
h(SkyToolbar, { ariaLabel: 'Actions', component: 'nav' }, () => [
|
||||
h(SkyToolbarPane, {}, () => 'Link 1'),
|
||||
h(SkyToolbarPane, {}, () => 'Link 2'),
|
||||
]),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain('role="toolbar"')
|
||||
expect(html).toContain('aria-label="Actions"')
|
||||
expect(html).toContain('sky-toolbar__blur')
|
||||
expect(html).toContain('sky-toolbar__background')
|
||||
expect(html.match(/sky-toolbar-pane/g)).toHaveLength(2)
|
||||
expect(html.match(/sky-glass/g)?.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
it('matches the Konsta iOS pane and background geometry', () => {
|
||||
const uiDirectory = fileURLToPath(new URL('..', import.meta.url))
|
||||
const controls = readFileSync(`${uiDirectory}/controls.css`, 'utf8')
|
||||
const toolbarStyles = controls.slice(
|
||||
controls.indexOf('.sky-toolbar {'),
|
||||
controls.indexOf('.sky-visually-hidden'),
|
||||
)
|
||||
|
||||
expect(toolbarStyles).toMatch(
|
||||
/\.sky-toolbar__inner\s*\{[\s\S]*?justify-content: space-between;[\s\S]*?gap: 16px;/,
|
||||
)
|
||||
expect(toolbarStyles).toMatch(
|
||||
/\.sky-toolbar\s*\{[\s\S]*?padding-right: calc\(var\(--sky-safe-area-right, 0px\) \+ 16px\);[\s\S]*?padding-left: calc\(var\(--sky-safe-area-left, 0px\) \+ 16px\);/,
|
||||
)
|
||||
expect(toolbarStyles).toMatch(
|
||||
/\.sky-toolbar-pane\s*\{[\s\S]*?height: 48px;[\s\S]*?border-radius: var\(--sky-radius-pill, 999px\);/,
|
||||
)
|
||||
expect(toolbarStyles).toMatch(
|
||||
/\.sky-toolbar--top \.sky-toolbar__blur,[\s\S]*?\.sky-toolbar--top \.sky-toolbar__background\s*\{\s*display: none;/,
|
||||
)
|
||||
expect(controls).toMatch(
|
||||
/\.sky-toolbar__blur\s*\{[\s\S]*?backdrop-filter: blur\(2px\);[\s\S]*?mask-image: linear-gradient/,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -43,6 +43,7 @@ withDefaults(
|
||||
:aria-orientation="tabbar ? undefined : 'horizontal'"
|
||||
:role="tabbar ? 'tablist' : 'toolbar'"
|
||||
>
|
||||
<span class="sky-toolbar__blur" aria-hidden="true" />
|
||||
<span class="sky-toolbar__background" :class="bgClass" aria-hidden="true" />
|
||||
<div class="sky-toolbar__inner" :class="innerClass">
|
||||
<slot />
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import SkyGlass from './SkyGlass.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
withDefaults(
|
||||
@@ -12,7 +14,7 @@ withDefaults(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="component" v-bind="$attrs" class="sky-toolbar-pane">
|
||||
<SkyGlass :component="component" v-bind="$attrs" class="sky-toolbar-pane">
|
||||
<slot />
|
||||
</component>
|
||||
</SkyGlass>
|
||||
</template>
|
||||
|
||||
@@ -11,6 +11,7 @@ export { default as SkyButton } from './SkyButton.vue'
|
||||
export { default as SkyCard } from './SkyCard.vue'
|
||||
export { default as SkyCheckbox } from './SkyCheckbox.vue'
|
||||
export { default as SkyChip } from './SkyChip.vue'
|
||||
export { default as SkyChipDeleteIcon } from './SkyChipDeleteIcon.vue'
|
||||
export { default as SkyFab } from './SkyFab.vue'
|
||||
export { default as SkyField } from './SkyField.vue'
|
||||
export { default as SkyGlass } from './SkyGlass.vue'
|
||||
|
||||
@@ -683,72 +683,129 @@
|
||||
}
|
||||
|
||||
.sky-tabbar {
|
||||
--sky-tabbar-pane-height: 48px;
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
min-height: calc(var(--sky-tabbar-height) + var(--sky-safe-area-bottom));
|
||||
padding-bottom: var(--sky-safe-area-bottom);
|
||||
border-top: 1px solid var(--sky-hairline);
|
||||
background: var(--sky-glass-solid);
|
||||
min-height: calc(
|
||||
var(--sky-safe-area-bottom) + var(--sky-tabbar-pane-height) + 16px
|
||||
);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-right: calc(var(--sky-safe-area-right) + 16px);
|
||||
padding-bottom: calc(var(--sky-safe-area-bottom) + 16px);
|
||||
padding-left: calc(var(--sky-safe-area-left) + 16px);
|
||||
color: var(--sky-text);
|
||||
}
|
||||
|
||||
.sky-tabbar--floating {
|
||||
right: var(--sky-space-4);
|
||||
bottom: calc(var(--sky-safe-area-bottom) + 10px);
|
||||
left: var(--sky-space-4);
|
||||
min-height: 62px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--sky-hairline);
|
||||
border-radius: 999px;
|
||||
background: var(--sky-surface);
|
||||
box-shadow:
|
||||
0 12px 28px rgba(0, 0, 0, 0.34),
|
||||
inset 0 1px rgba(255, 255, 255, 0.04);
|
||||
.sky-tabbar--icons.sky-tabbar--labels {
|
||||
--sky-tabbar-pane-height: 64px;
|
||||
}
|
||||
|
||||
.sky-tabbar--floating .sky-tabbar__inner {
|
||||
min-height: 60px;
|
||||
padding: 3px;
|
||||
.sky-tabbar__blur,
|
||||
.sky-tabbar__background {
|
||||
width: 100%;
|
||||
height: calc(
|
||||
var(--sky-safe-area-bottom) + var(--sky-tabbar-pane-height) + 32px
|
||||
);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sky-tabbar--floating .sky-tabbar__pane {
|
||||
min-height: 60px;
|
||||
.sky-tabbar__blur {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sky-tabbar__background {
|
||||
background: linear-gradient(to top, var(--sky-bg), transparent);
|
||||
}
|
||||
|
||||
.sky-tabbar__inner {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: var(--sky-tabbar-pane-height);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sky-tabbar__inner,
|
||||
.sky-tabbar__pane {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: var(--sky-tabbar-height);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: var(--sky-radius-pill);
|
||||
}
|
||||
|
||||
.sky-tabbar__pane {
|
||||
.sky-tabbar__links {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.sky-tabbar::before {
|
||||
.sky-tabbar__highlight {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 100%;
|
||||
z-index: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 16px;
|
||||
content: '';
|
||||
background: linear-gradient(to top, var(--sky-glass-solid), transparent);
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
transition: transform 300ms ease;
|
||||
}
|
||||
|
||||
.sky-tabbar__pane > button,
|
||||
.sky-tabbar__pane > a {
|
||||
[dir='rtl'] .sky-tabbar__highlight,
|
||||
.sky-tabbar[dir='rtl'] .sky-tabbar__highlight {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.sky-tabbar__highlight-inner,
|
||||
.sky-tabbar__highlight-thumb {
|
||||
position: absolute;
|
||||
inset: 4px;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
transition:
|
||||
opacity 300ms ease,
|
||||
transform 300ms ease;
|
||||
}
|
||||
|
||||
.sky-tabbar__highlight-inner {
|
||||
background: var(--sky-tabbar-highlight-background, rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.sky-tabbar__highlight-thumb {
|
||||
background: var(--sky-tabbar-thumb-background, rgba(0, 0, 0, 0.05));
|
||||
box-shadow: var(--sky-shadow-glass-thumb);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sky-tabbar__highlight--pressed .sky-tabbar__highlight-inner {
|
||||
opacity: 0;
|
||||
transform: scale(1.4);
|
||||
}
|
||||
|
||||
.sky-tabbar__highlight--pressed .sky-tabbar__highlight-thumb {
|
||||
opacity: 1;
|
||||
transform: scale(1.4);
|
||||
}
|
||||
|
||||
.sky-tabbar__links > button,
|
||||
.sky-tabbar__links > a {
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
min-height: var(--sky-tabbar-height);
|
||||
min-height: var(--sky-tabbar-pane-height);
|
||||
margin: 0;
|
||||
padding: var(--sky-space-1) var(--sky-space-2);
|
||||
padding: 4px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -770,13 +827,6 @@
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.sky-tabbar__pane > button:active,
|
||||
.sky-tabbar__pane > a:active {
|
||||
background: var(--sky-app-accent-soft);
|
||||
transform: scale(0.96);
|
||||
transition-duration: 0ms;
|
||||
}
|
||||
|
||||
@supports (
|
||||
(-webkit-backdrop-filter: blur(2px)) or (backdrop-filter: blur(2px))
|
||||
) {
|
||||
@@ -786,14 +836,12 @@
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.sky-tabbar {
|
||||
background: var(--sky-glass);
|
||||
.sky-tabbar__blur {
|
||||
display: block;
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.sky-tabbar::before {
|
||||
background: linear-gradient(to top, var(--sky-glass), transparent);
|
||||
-webkit-mask-image: linear-gradient(transparent 0%, #000 50%, #000 100%);
|
||||
mask-image: linear-gradient(transparent 0%, #000 50%, #000 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,19 +860,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sky-tabbar__pane > button > span,
|
||||
.sky-tabbar__pane > a > span {
|
||||
.sky-tabbar__links > button > span,
|
||||
.sky-tabbar__links > a > span {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.sky-tabbar__pane > button > span > span:last-child,
|
||||
.sky-tabbar__pane > a > span > span:last-child {
|
||||
.sky-tabbar__links > button > span > span:last-child,
|
||||
.sky-tabbar__links > a > span > span:last-child {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 13px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 14px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,10 +40,12 @@
|
||||
inset 0 0 24px rgba(0, 0, 0, 0.1), 0 0 25px rgba(0, 0, 0, 0.2);
|
||||
--sky-shadow-glass-fab:
|
||||
inset -3px -3px 0 -3.5px #ffffff, inset 3px 3px 0 -3.5px #ffffff,
|
||||
inset 0 0 0 0.5px rgba(255, 255, 255, 0.5),
|
||||
inset 3px 3px 10px -2px var(--sky-app-accent, #007aff),
|
||||
inset -3px -3px 10px -2px var(--sky-app-accent, #007aff),
|
||||
inset 0 0 5px 1px #ffffff, 0 0 15px 4px rgba(0, 0, 0, 0.2);
|
||||
inset 0 0 0 0.5px rgba(255, 255, 255, 0.5), inset 0 0 5px 1px #ffffff;
|
||||
--sky-shadow-glass-fab-outer: 0 0 15px 4px rgba(0, 0, 0, 0.2);
|
||||
--sky-fab-accent-inset-start: 3px 3px 10px -2px;
|
||||
--sky-fab-accent-inset-end: -3px -3px 10px -2px;
|
||||
--sky-fab-accent-layer-opacity: 1;
|
||||
--sky-fab-dark-accent-layer-opacity: 0;
|
||||
--sky-shadow-glass-thumb:
|
||||
inset -3px -3px 0 -3.5px #ffffff, inset 3px 3px 0 -3.5px #ffffff,
|
||||
inset 0 0 0 0.5px rgba(255, 255, 255, 0.5), inset 3px 3px 10px -2px #eeeeee,
|
||||
@@ -116,6 +118,13 @@
|
||||
--sky-app-accent-tint: #298fff;
|
||||
--sky-app-accent-shade: #0066d6;
|
||||
--sky-app-accent-soft: rgba(0, 122, 255, 0.15);
|
||||
--sky-shadow-glass-fab:
|
||||
inset -3px -3px 0 -3.5px #ffffff, inset 3px 3px 0 -3.5px #ffffff,
|
||||
inset 0 0 0 0.5px rgba(255, 255, 255, 0.5), inset 0 0 5px 1px #ffffff;
|
||||
--sky-fab-accent-inset-start: 3px 3px 10px -2px;
|
||||
--sky-fab-accent-inset-end: -3px -3px 10px -2px;
|
||||
--sky-fab-accent-layer-opacity: 1;
|
||||
--sky-fab-dark-accent-layer-opacity: 0;
|
||||
--sky-bg: #efeff4;
|
||||
--sky-surface: #ffffff;
|
||||
--sky-surface-shade: #f1f1f5;
|
||||
@@ -133,6 +142,8 @@
|
||||
--sky-subtle: rgba(0, 0, 0, 0.3);
|
||||
--sky-hairline: rgba(0, 0, 0, 0.2);
|
||||
--sky-pressed: rgba(0, 0, 0, 0.1);
|
||||
--sky-tabbar-highlight-background: rgba(0, 0, 0, 0.1);
|
||||
--sky-tabbar-thumb-background: rgba(0, 0, 0, 0.05);
|
||||
--sky-overlay-backdrop: rgba(0, 0, 0, 0.4);
|
||||
--sky-danger: #ff3b30;
|
||||
--sky-danger-soft: rgba(255, 59, 48, 0.15);
|
||||
@@ -168,6 +179,8 @@
|
||||
--sky-subtle: rgba(255, 255, 255, 0.3);
|
||||
--sky-hairline: rgba(255, 255, 255, 0.15);
|
||||
--sky-pressed: rgba(255, 255, 255, 0.1);
|
||||
--sky-tabbar-highlight-background: rgba(255, 255, 255, 0.15);
|
||||
--sky-tabbar-thumb-background: rgba(255, 255, 255, 0.1);
|
||||
--sky-overlay-backdrop: rgba(0, 0, 0, 0.52);
|
||||
--sky-danger: #ff453a;
|
||||
--sky-danger-soft: rgba(255, 69, 58, 0.16);
|
||||
@@ -183,12 +196,13 @@
|
||||
inset 0 0 10px rgba(255, 255, 255, 0.075),
|
||||
inset 0 0 24px rgba(255, 255, 255, 0.05), 0 0 25px rgba(0, 0, 0, 0.15);
|
||||
--sky-shadow-glass-fab:
|
||||
inset 3px 3px 0 -3.5px var(--sky-app-accent, #0a84ff),
|
||||
inset -3px -3px 0 -3.5px var(--sky-app-accent, #0a84ff),
|
||||
inset -0.5px -0.5px 0 rgba(255, 255, 255, 0.5),
|
||||
inset 0.5px 0.5px 0 rgba(255, 255, 255, 0.1),
|
||||
inset -3px 3px 0 -3.5px rgba(255, 255, 255, 0.25),
|
||||
inset 0 -5px 5px rgba(10, 132, 255, 0.25), 0 0 15px 4px rgba(0, 0, 0, 0.2);
|
||||
inset -3px 3px 0 -3.5px rgba(255, 255, 255, 0.25);
|
||||
--sky-fab-accent-inset-start: 3px 3px 0 -3.5px;
|
||||
--sky-fab-accent-inset-end: -3px -3px 0 -3.5px;
|
||||
--sky-fab-accent-layer-opacity: 0.5;
|
||||
--sky-fab-dark-accent-layer-opacity: 0.5;
|
||||
--sky-shadow-glass-thumb:
|
||||
inset 3px 3px 0 -3.5px #ffffff, inset -3px -3px 0 -3.5px #ffffff,
|
||||
inset -0.5px -0.5px 0 rgba(255, 255, 255, 0.5),
|
||||
|
||||
@@ -1,31 +1,62 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { SKY_UI_DEMO_CATALOG } from './sky-ui-demo/catalog'
|
||||
|
||||
const developmentDirectory = fileURLToPath(new URL('.', import.meta.url))
|
||||
const demoDirectory = join(developmentDirectory, 'sky-ui-demo')
|
||||
const pagesDirectory = join(demoDirectory, 'pages')
|
||||
const viewSource = readFileSync(
|
||||
new URL('./SkyUiKitchenSinkView.vue', import.meta.url),
|
||||
join(developmentDirectory, 'SkyUiKitchenSinkView.vue'),
|
||||
'utf8',
|
||||
)
|
||||
const homeSource = readFileSync(
|
||||
join(demoDirectory, 'SkyUiDemoHome.vue'),
|
||||
'utf8',
|
||||
)
|
||||
const extensionsSource = readFileSync(
|
||||
join(pagesDirectory, 'SkyExtensionsDemo.vue'),
|
||||
'utf8',
|
||||
)
|
||||
const toastSource = readFileSync(join(pagesDirectory, 'ToastDemo.vue'), 'utf8')
|
||||
const stepperSource = readFileSync(
|
||||
join(pagesDirectory, 'StepperDemo.vue'),
|
||||
'utf8',
|
||||
)
|
||||
const appSource = readFileSync(
|
||||
new URL('../../App.vue', import.meta.url),
|
||||
join(developmentDirectory, '../../App.vue'),
|
||||
'utf8',
|
||||
)
|
||||
const routerSource = readFileSync(
|
||||
new URL('../../router/index.ts', import.meta.url),
|
||||
join(developmentDirectory, '../../router/index.ts'),
|
||||
'utf8',
|
||||
)
|
||||
const settingsSource = readFileSync(
|
||||
new URL('../apps/SettingsApp.vue', import.meta.url),
|
||||
join(developmentDirectory, '../apps/SettingsApp.vue'),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
function sourceFiles(directory: string): string[] {
|
||||
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const path = join(directory, entry.name)
|
||||
if (entry.isDirectory()) return sourceFiles(path)
|
||||
return /\.(?:css|ts|vue)$/.test(entry.name) ? [path] : []
|
||||
})
|
||||
}
|
||||
|
||||
const demoSources = sourceFiles(demoDirectory).map((path) =>
|
||||
readFileSync(path, 'utf8'),
|
||||
)
|
||||
const combinedDemoSource = [viewSource, ...demoSources].join('\n')
|
||||
const publicIndexSources = [
|
||||
'../../ui/index.ts',
|
||||
'../../ui/controls/index.ts',
|
||||
'../../ui/overlays/index.ts',
|
||||
'../../ui/settings/index.ts',
|
||||
].map((path) => readFileSync(new URL(path, import.meta.url), 'utf8'))
|
||||
|
||||
const publicComponents = Array.from(
|
||||
new Set(
|
||||
publicIndexSources.flatMap((source) =>
|
||||
@@ -37,17 +68,84 @@ const publicComponents = Array.from(
|
||||
),
|
||||
).sort()
|
||||
|
||||
const referenceCatalog = [
|
||||
['action-sheet', 'Action Sheet'],
|
||||
['badge', 'Badge'],
|
||||
['breadcrumbs', 'Breadcrumbs'],
|
||||
['buttons', 'Buttons'],
|
||||
['cards', 'Cards'],
|
||||
['checkbox', 'Checkbox'],
|
||||
['chips', 'Chips'],
|
||||
['contacts-list', 'Contacts List'],
|
||||
['content-block', 'Content Block'],
|
||||
['data-table', 'Data Table'],
|
||||
['dialog', 'Dialog'],
|
||||
['fab', 'FAB (Floating Action Button)'],
|
||||
['form-inputs', 'Form Inputs'],
|
||||
['list', 'List'],
|
||||
['list-button', 'List Button'],
|
||||
['menu-list', 'Menu List'],
|
||||
['messages', 'Messages'],
|
||||
['navbar', 'Navbar'],
|
||||
['notification', 'Notification'],
|
||||
['side-panels', 'Panel / Side Panels'],
|
||||
['popover', 'Popover'],
|
||||
['popup', 'Popup'],
|
||||
['preloader', 'Preloader'],
|
||||
['progressbar', 'Progressbar'],
|
||||
['radio', 'Radio'],
|
||||
['range-slider', 'Range Slider'],
|
||||
['searchbar', 'Searchbar'],
|
||||
['segmented-control', 'Segmented Control'],
|
||||
['sheet-modal', 'Sheet Modal'],
|
||||
['stepper', 'Stepper'],
|
||||
['subnavbar', 'Subnavbar'],
|
||||
['tabbar', 'Tabbar'],
|
||||
['toast', 'Toast'],
|
||||
['toggle', 'Toggle'],
|
||||
['toolbar', 'Toolbar'],
|
||||
] as const
|
||||
|
||||
function demoFileName(id: string): string {
|
||||
return `${id
|
||||
.split('-')
|
||||
.map((part) => `${part[0]?.toUpperCase()}${part.slice(1)}`)
|
||||
.join('')}Demo.vue`
|
||||
}
|
||||
|
||||
describe('development Sky UI Kitchen Sink contract', () => {
|
||||
it('demonstrates every public Sky component without Konsta imports', () => {
|
||||
it('mirrors the exact Konsta 5.3 Vue component submenu catalog', () => {
|
||||
expect(SKY_UI_DEMO_CATALOG.map(({ id, title }) => [id, title])).toEqual(
|
||||
referenceCatalog,
|
||||
)
|
||||
expect(new Set(SKY_UI_DEMO_CATALOG.map(({ id }) => id)).size).toBe(35)
|
||||
|
||||
const missingPages = SKY_UI_DEMO_CATALOG.filter(
|
||||
({ id }) => !existsSync(join(pagesDirectory, demoFileName(id))),
|
||||
).map(({ id }) => id)
|
||||
expect(missingPages).toEqual([])
|
||||
})
|
||||
|
||||
it('uses a real catalog and parameterized submenu navigation', () => {
|
||||
expect(homeSource).toContain('v-for="entry in SKY_UI_DEMO_CATALOG"')
|
||||
expect(homeSource).toContain('@click="demo.navigate(entry.id)"')
|
||||
expect(viewSource).toContain('params: { demo: id }')
|
||||
expect(combinedDemoSource).toContain('demo.returnToCatalog')
|
||||
expect(viewSource).toContain('defineAsyncComponent')
|
||||
expect(routerSource).toContain("path: '/development/sky-ui/:demo?'")
|
||||
expect(combinedDemoSource).toContain('./assets/demo-icon.png')
|
||||
})
|
||||
|
||||
it('demonstrates every public Sky component without Konsta runtime code', () => {
|
||||
const missingComponents = publicComponents.filter(
|
||||
(component) => !new RegExp(`<${component}(?:\\s|/?>)`).test(viewSource),
|
||||
(component) =>
|
||||
!new RegExp(`<${component}(?:\\s|/?>)`).test(combinedDemoSource),
|
||||
)
|
||||
|
||||
expect(publicComponents.length).toBeGreaterThanOrEqual(63)
|
||||
expect(missingComponents).toEqual([])
|
||||
expect(viewSource).toContain("from '@/ui'")
|
||||
expect(viewSource).not.toContain('konsta/vue')
|
||||
expect(viewSource).not.toMatch(/<\/?k-[a-z]/)
|
||||
expect(combinedDemoSource).not.toContain('konsta/vue')
|
||||
expect(combinedDemoSource).not.toMatch(/<\/?k-[a-z]/)
|
||||
})
|
||||
|
||||
it('keeps the lazy route and launcher development-only', () => {
|
||||
@@ -58,6 +156,9 @@ describe('development Sky UI Kitchen Sink contract', () => {
|
||||
expect(appSource).toContain(
|
||||
"isDevelopment && route.name === 'development-sky-ui'",
|
||||
)
|
||||
expect(appSource).toContain(
|
||||
'isDevelopmentRoute ? String(route.name) : route.path',
|
||||
)
|
||||
expect(settingsSource).toContain(
|
||||
'const isDevelopment = import.meta.env.DEV',
|
||||
)
|
||||
@@ -66,5 +167,35 @@ describe('development Sky UI Kitchen Sink contract', () => {
|
||||
"router.push({ name: 'development-sky-ui' })",
|
||||
)
|
||||
expect(settingsSource).not.toContain('SkyUiKitchenSinkView')
|
||||
expect(viewSource).not.toContain('<SkyProvider')
|
||||
expect(viewSource).not.toContain('safe-areas')
|
||||
})
|
||||
|
||||
it('keeps the demo source within the conservative CEF contract', () => {
|
||||
expect(combinedDemoSource).not.toMatch(
|
||||
/:has\(|@container|\b(?:dvh|svh|lvh)\b|color-mix\(|oklch\(|view-transition|draggable\s*=\s*["']true/,
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps exactly one split-navigation highlight active', () => {
|
||||
expect(extensionsSource).toContain(':strong="splitTab < 2"')
|
||||
expect(extensionsSource).toContain(':strong="splitTab === 2"')
|
||||
})
|
||||
|
||||
it('matches the compact Konsta toast buttons without shrinking their hit area', () => {
|
||||
expect(toastSource).toContain('class="toast-demo__button"')
|
||||
expect(toastSource).toMatch(
|
||||
/\.toast-demo__button\s*\{[\s\S]*?height: 34px;[\s\S]*?min-height: 34px;/,
|
||||
)
|
||||
expect(toastSource).toMatch(
|
||||
/\.toast-demo__button::before\s*\{[\s\S]*?inset: -5px 0;/,
|
||||
)
|
||||
})
|
||||
|
||||
it('centers the Konsta text-input stepper examples', () => {
|
||||
expect(stepperSource).toContain('sky-ui-demo-stepper-inputs')
|
||||
expect(stepperSource).toMatch(
|
||||
/\.sky-ui-demo-stepper-inputs\s*\{[\s\S]*?justify-items: center;/,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,522 +1,222 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed, defineAsyncComponent, ref, watch, type Component } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import { isSkyUiDemoId, type SkyUiDemoId } from './sky-ui-demo/catalog'
|
||||
import {
|
||||
SkyActionButton,
|
||||
SkyActionGroup,
|
||||
SkyActionSheet,
|
||||
SkyActionsLabel,
|
||||
SkyApp,
|
||||
SkyAppPage,
|
||||
SkyBadge,
|
||||
SkyBlock,
|
||||
SkyBlockFooter,
|
||||
SkyBlockHeader,
|
||||
SkyBlockTitle,
|
||||
SkyBreadcrumbs,
|
||||
SkyBreadcrumbsCollapsed,
|
||||
SkyBreadcrumbsItem,
|
||||
SkyBreadcrumbsSeparator,
|
||||
SkyButton,
|
||||
SkyCard,
|
||||
SkyCheckbox,
|
||||
SkyChip,
|
||||
SkyDialog,
|
||||
SkyDialogButton,
|
||||
SkyEmptyState,
|
||||
SkyFab,
|
||||
SkyField,
|
||||
SkyGlass,
|
||||
SkyIcon,
|
||||
SkyInfiniteLoader,
|
||||
SkyLink,
|
||||
SkyList,
|
||||
SkyListButton,
|
||||
SkyListCard,
|
||||
SkyListGroup,
|
||||
SkyListItem,
|
||||
SkyMenuList,
|
||||
SkyMenuListItem,
|
||||
SkyMessage,
|
||||
SkyMessagebar,
|
||||
SkyMessages,
|
||||
SkyMessagesTitle,
|
||||
SkyNavbar,
|
||||
SkyNavbarBackLink,
|
||||
SkyNotification,
|
||||
SkyPanel,
|
||||
SkyPillNavigation,
|
||||
SkyPopover,
|
||||
SkyPopup,
|
||||
SkyProgress,
|
||||
SkyProvider,
|
||||
SkyRadio,
|
||||
SkyRange,
|
||||
SkyScrollArea,
|
||||
SkyScrollRail,
|
||||
SkySearchbar,
|
||||
SkySection,
|
||||
SkySegmented,
|
||||
SkySegmentedButton,
|
||||
SkySettingsGroup,
|
||||
SkySettingsIcon,
|
||||
SkySettingsRangeRow,
|
||||
SkySettingsRow,
|
||||
SkySheet,
|
||||
SkySpinner,
|
||||
SkyStatusCard,
|
||||
SkyStepper,
|
||||
SkySurface,
|
||||
SkyTabBar,
|
||||
SkyTabButton,
|
||||
SkyTable,
|
||||
SkyTableBody,
|
||||
SkyTableCell,
|
||||
SkyTableHead,
|
||||
SkyTableRow,
|
||||
SkyToast,
|
||||
SkyToggle,
|
||||
SkyToolbar,
|
||||
SkyToolbarPane,
|
||||
SkyWidgetFrame,
|
||||
} from '@/ui'
|
||||
createSkyUiDemoTheme,
|
||||
provideSkyUiDemoContext,
|
||||
type SkyUiDemoAccent,
|
||||
} from './sky-ui-demo/context'
|
||||
import SkyUiDemoHome from './sky-ui-demo/SkyUiDemoHome.vue'
|
||||
import './sky-ui-demo/demo.css'
|
||||
|
||||
const phone = usePhoneStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const checked = ref(true)
|
||||
const fieldValue = ref('Sky UI')
|
||||
const messageValue = ref('')
|
||||
const radioValue = ref('first')
|
||||
const rangeValue = ref(58)
|
||||
const searchValue = ref('')
|
||||
const segmentIndex = ref(0)
|
||||
const stepperValue = ref(2)
|
||||
const toggleValue = ref(true)
|
||||
const initialAccent: SkyUiDemoAccent = {
|
||||
color: '#007aff',
|
||||
name: 'Blue',
|
||||
soft: 'rgba(0, 122, 255, 0.16)',
|
||||
}
|
||||
const dark = ref(phone.isDarkMode)
|
||||
const accentChoice = ref(initialAccent)
|
||||
|
||||
const actionSheetOpened = ref(false)
|
||||
const dialogOpened = ref(false)
|
||||
const notificationOpened = ref(false)
|
||||
const panelOpened = ref(false)
|
||||
const popoverOpened = ref(false)
|
||||
const popupOpened = ref(false)
|
||||
const sheetOpened = ref(false)
|
||||
const toastOpened = ref(false)
|
||||
const demoPages: Record<SkyUiDemoId, Component> = {
|
||||
'action-sheet': defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/ActionSheetDemo.vue'),
|
||||
),
|
||||
badge: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/BadgeDemo.vue'),
|
||||
),
|
||||
breadcrumbs: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/BreadcrumbsDemo.vue'),
|
||||
),
|
||||
buttons: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/ButtonsDemo.vue'),
|
||||
),
|
||||
cards: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/CardsDemo.vue'),
|
||||
),
|
||||
checkbox: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/CheckboxDemo.vue'),
|
||||
),
|
||||
chips: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/ChipsDemo.vue'),
|
||||
),
|
||||
'contacts-list': defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/ContactsListDemo.vue'),
|
||||
),
|
||||
'content-block': defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/ContentBlockDemo.vue'),
|
||||
),
|
||||
'data-table': defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/DataTableDemo.vue'),
|
||||
),
|
||||
dialog: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/DialogDemo.vue'),
|
||||
),
|
||||
fab: defineAsyncComponent(() => import('./sky-ui-demo/pages/FabDemo.vue')),
|
||||
'form-inputs': defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/FormInputsDemo.vue'),
|
||||
),
|
||||
list: defineAsyncComponent(() => import('./sky-ui-demo/pages/ListDemo.vue')),
|
||||
'list-button': defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/ListButtonDemo.vue'),
|
||||
),
|
||||
'menu-list': defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/MenuListDemo.vue'),
|
||||
),
|
||||
messages: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/MessagesDemo.vue'),
|
||||
),
|
||||
navbar: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/NavbarDemo.vue'),
|
||||
),
|
||||
notification: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/NotificationDemo.vue'),
|
||||
),
|
||||
'side-panels': defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/SidePanelsDemo.vue'),
|
||||
),
|
||||
popover: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/PopoverDemo.vue'),
|
||||
),
|
||||
popup: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/PopupDemo.vue'),
|
||||
),
|
||||
preloader: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/PreloaderDemo.vue'),
|
||||
),
|
||||
progressbar: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/ProgressbarDemo.vue'),
|
||||
),
|
||||
radio: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/RadioDemo.vue'),
|
||||
),
|
||||
'range-slider': defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/RangeSliderDemo.vue'),
|
||||
),
|
||||
searchbar: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/SearchbarDemo.vue'),
|
||||
),
|
||||
'segmented-control': defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/SegmentedControlDemo.vue'),
|
||||
),
|
||||
'sheet-modal': defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/SheetModalDemo.vue'),
|
||||
),
|
||||
stepper: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/StepperDemo.vue'),
|
||||
),
|
||||
subnavbar: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/SubnavbarDemo.vue'),
|
||||
),
|
||||
tabbar: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/TabbarDemo.vue'),
|
||||
),
|
||||
toast: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/ToastDemo.vue'),
|
||||
),
|
||||
toggle: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/ToggleDemo.vue'),
|
||||
),
|
||||
toolbar: defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/ToolbarDemo.vue'),
|
||||
),
|
||||
'sky-extensions': defineAsyncComponent(
|
||||
() => import('./sky-ui-demo/pages/SkyExtensionsDemo.vue'),
|
||||
),
|
||||
}
|
||||
|
||||
const requestedDemo = computed(() => route.params.demo)
|
||||
const activeDemo = computed<SkyUiDemoId | null>(() =>
|
||||
isSkyUiDemoId(requestedDemo.value) ? requestedDemo.value : null,
|
||||
)
|
||||
const activePage = computed(() =>
|
||||
activeDemo.value ? demoPages[activeDemo.value] : SkyUiDemoHome,
|
||||
)
|
||||
const theme = createSkyUiDemoTheme(dark, accentChoice)
|
||||
|
||||
function navigate(id: SkyUiDemoId): void {
|
||||
void router.push({ name: 'development-sky-ui', params: { demo: id } })
|
||||
}
|
||||
|
||||
function returnToCatalog(): void {
|
||||
const previousPath = window.history.state?.back
|
||||
const catalogPath = router.resolve({ name: 'development-sky-ui' }).fullPath
|
||||
|
||||
if (previousPath === catalogPath) {
|
||||
router.back()
|
||||
return
|
||||
}
|
||||
|
||||
void router.replace({ name: 'development-sky-ui' })
|
||||
}
|
||||
|
||||
function exit(): void {
|
||||
router.back()
|
||||
}
|
||||
|
||||
provideSkyUiDemoContext({
|
||||
...theme,
|
||||
exit,
|
||||
navigate,
|
||||
returnToCatalog,
|
||||
})
|
||||
|
||||
watch(
|
||||
requestedDemo,
|
||||
(value) => {
|
||||
if (value === undefined || isSkyUiDemoId(value)) return
|
||||
void router.replace({ name: 'development-sky-ui' })
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyAppPage
|
||||
class="sky-ui-kitchen-sink"
|
||||
:label="phone.t('Apps.settings.skyUiKitchenSink')"
|
||||
>
|
||||
<SkyNavbar
|
||||
:back-label="phone.t('Apps.settings.back')"
|
||||
show-back
|
||||
:title="phone.t('Apps.settings.skyUiKitchenSink')"
|
||||
@back="router.back()"
|
||||
/>
|
||||
|
||||
<SkyScrollArea class="sky-ui-kitchen-sink__scroll">
|
||||
<SkyBlockHeader component="header" inset>
|
||||
{{ phone.t('Apps.settings.skyUiKitchenSinkDescription') }}
|
||||
</SkyBlockHeader>
|
||||
|
||||
<SkySection title="SkyApp / SkyProvider / SkySurface / SkyGlass">
|
||||
<div class="sky-ui-kitchen-sink__grid">
|
||||
<SkyApp class="sky-ui-kitchen-sink__mini-app" :safe-areas="false">
|
||||
<SkyProvider :safe-areas="false">
|
||||
<SkySurface highlight>
|
||||
<SkyGlass component="button" type="button"> SkyGlass </SkyGlass>
|
||||
</SkySurface>
|
||||
</SkyProvider>
|
||||
</SkyApp>
|
||||
<SkyWidgetFrame label="SkyWidgetFrame" size="small">
|
||||
<SkyIcon label="SkyIcon" :size="28">S</SkyIcon>
|
||||
</SkyWidgetFrame>
|
||||
</div>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="SkyBlock / SkyCard / SkyStatusCard">
|
||||
<SkyBlockTitle medium>SkyBlockTitle</SkyBlockTitle>
|
||||
<SkyBlock inset outline strong>
|
||||
<SkyBlockHeader>SkyBlockHeader</SkyBlockHeader>
|
||||
SkyBlock
|
||||
<SkyBlockFooter>SkyBlockFooter</SkyBlockFooter>
|
||||
</SkyBlock>
|
||||
<div class="sky-ui-kitchen-sink__grid">
|
||||
<SkyCard header="SkyCard" footer="Footer" header-divider outline>
|
||||
Content
|
||||
</SkyCard>
|
||||
<SkyStatusCard
|
||||
indicator
|
||||
subtitle="SkyStatusCard"
|
||||
title="Ready"
|
||||
tone="success"
|
||||
/>
|
||||
</div>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="SkyButton / SkyLink / SkyChip / SkyBadge / SkyFab">
|
||||
<div class="sky-ui-kitchen-sink__row">
|
||||
<SkyButton>SkyButton</SkyButton>
|
||||
<SkyButton outline variant="secondary">Outline</SkyButton>
|
||||
<SkyLink>SkyLink</SkyLink>
|
||||
<SkyChip delete-button delete-label="Remove SkyChip">
|
||||
SkyChip
|
||||
</SkyChip>
|
||||
<SkyBadge tone="danger">3</SkyBadge>
|
||||
<SkyFab text="SkyFab">+</SkyFab>
|
||||
</div>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="SkyField / SkySearchbar / selection controls">
|
||||
<SkyList inset strong>
|
||||
<SkyField
|
||||
v-model="fieldValue"
|
||||
clear-button
|
||||
clear-label="Clear field"
|
||||
label="SkyField"
|
||||
/>
|
||||
</SkyList>
|
||||
<SkySearchbar
|
||||
v-model="searchValue"
|
||||
cancel-button
|
||||
cancel-label="Cancel"
|
||||
clear-label="Clear search"
|
||||
label="SkySearchbar"
|
||||
placeholder="SkySearchbar"
|
||||
/>
|
||||
<div class="sky-ui-kitchen-sink__row">
|
||||
<SkyCheckbox v-model="checked">SkyCheckbox</SkyCheckbox>
|
||||
<SkyRadio v-model="radioValue" value="first">SkyRadio</SkyRadio>
|
||||
<SkyToggle v-model="toggleValue">SkyToggle</SkyToggle>
|
||||
</div>
|
||||
<SkyRange
|
||||
v-model="rangeValue"
|
||||
aria-label="SkyRange"
|
||||
caption="SkyRange"
|
||||
/>
|
||||
<SkyStepper
|
||||
v-model="stepperValue"
|
||||
decrement-label="Decrease"
|
||||
increment-label="Increase"
|
||||
input
|
||||
input-label="SkyStepper value"
|
||||
/>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="SkyList / SkyMenuList">
|
||||
<SkyListCard>
|
||||
<SkyListItem title="SkyListItem" subtitle="Subtitle" after="After" />
|
||||
<SkyListGroup inset strong>
|
||||
<SkyListItem title="SkyListGroup" />
|
||||
</SkyListGroup>
|
||||
<SkyListButton value="SkyListButton" />
|
||||
</SkyListCard>
|
||||
<SkyMenuList aria-label="SkyMenuList" inset strong>
|
||||
<SkyMenuListItem active title="SkyMenuListItem" footer="Active" />
|
||||
<SkyMenuListItem title="Second item" />
|
||||
</SkyMenuList>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="SkyBreadcrumbs / SkyNavbarBackLink / SkyToolbar">
|
||||
<SkyBreadcrumbs aria-label="SkyBreadcrumbs">
|
||||
<SkyBreadcrumbsItem component="button">First</SkyBreadcrumbsItem>
|
||||
<SkyBreadcrumbsSeparator />
|
||||
<SkyBreadcrumbsCollapsed ariaLabel="Collapsed breadcrumbs" />
|
||||
<SkyBreadcrumbsSeparator />
|
||||
<SkyBreadcrumbsItem active>Current</SkyBreadcrumbsItem>
|
||||
</SkyBreadcrumbs>
|
||||
<SkyToolbar aria-label="SkyToolbar" outline>
|
||||
<SkyToolbarPane>
|
||||
<SkyNavbarBackLink
|
||||
ariaLabel="Back"
|
||||
show-text
|
||||
text="SkyNavbarBackLink"
|
||||
/>
|
||||
<SkyButton small>Action</SkyButton>
|
||||
</SkyToolbarPane>
|
||||
</SkyToolbar>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="SkySegmented / SkyPillNavigation / SkyTabBar">
|
||||
<SkySegmented
|
||||
:active-index="segmentIndex"
|
||||
aria-label="SkySegmented"
|
||||
:item-count="2"
|
||||
navigation
|
||||
>
|
||||
<SkySegmentedButton
|
||||
:active="segmentIndex === 0"
|
||||
tab
|
||||
@click="segmentIndex = 0"
|
||||
>
|
||||
First
|
||||
</SkySegmentedButton>
|
||||
<SkySegmentedButton
|
||||
:active="segmentIndex === 1"
|
||||
tab
|
||||
@click="segmentIndex = 1"
|
||||
>
|
||||
Second
|
||||
</SkySegmentedButton>
|
||||
</SkySegmented>
|
||||
<div class="sky-ui-kitchen-sink__inline-navigation">
|
||||
<SkyPillNavigation label="SkyPillNavigation">
|
||||
<SkyButton small>Apps</SkyButton>
|
||||
<SkyButton small variant="secondary">Games</SkyButton>
|
||||
</SkyPillNavigation>
|
||||
<SkyTabBar label="SkyTabBar">
|
||||
<SkyTabButton active label="First" />
|
||||
<SkyTabButton label="Second" />
|
||||
</SkyTabBar>
|
||||
</div>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="SkyScrollRail / loading and feedback">
|
||||
<SkyScrollRail label="SkyScrollRail">
|
||||
<SkyCard
|
||||
v-for="index in 4"
|
||||
:key="index"
|
||||
class="sky-ui-kitchen-sink__rail-card"
|
||||
>
|
||||
SkyCard {{ index }}
|
||||
</SkyCard>
|
||||
</SkyScrollRail>
|
||||
<div class="sky-ui-kitchen-sink__stack">
|
||||
<SkyProgress label="SkyProgress" :progress="0.68" />
|
||||
<SkySpinner label="SkySpinner" />
|
||||
<SkyInfiniteLoader
|
||||
:has-more="false"
|
||||
loading
|
||||
loading-label="SkyInfiniteLoader"
|
||||
retry-label="Retry"
|
||||
/>
|
||||
<SkyEmptyState body="SkyEmptyState body" title="SkyEmptyState">
|
||||
<template #actions><SkyButton small>Action</SkyButton></template>
|
||||
</SkyEmptyState>
|
||||
</div>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="SkyTable">
|
||||
<SkyTable aria-label="SkyTable" compact striped>
|
||||
<SkyTableHead>
|
||||
<SkyTableRow header>
|
||||
<SkyTableCell header scope="col">Component</SkyTableCell>
|
||||
<SkyTableCell header numeric scope="col">Count</SkyTableCell>
|
||||
</SkyTableRow>
|
||||
</SkyTableHead>
|
||||
<SkyTableBody>
|
||||
<SkyTableRow selected>
|
||||
<SkyTableCell>SkyTableRow</SkyTableCell>
|
||||
<SkyTableCell numeric>1</SkyTableCell>
|
||||
</SkyTableRow>
|
||||
</SkyTableBody>
|
||||
</SkyTable>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="SkyMessages / SkyMessagebar">
|
||||
<SkyMessages>
|
||||
<SkyMessagesTitle>SkyMessagesTitle</SkyMessagesTitle>
|
||||
<SkyMessage text="SkyMessage received" type="received" />
|
||||
<SkyMessage text="SkyMessage sent" type="sent" />
|
||||
</SkyMessages>
|
||||
<SkyMessagebar
|
||||
v-model="messageValue"
|
||||
aria-label="SkyMessagebar"
|
||||
placeholder="SkyMessagebar"
|
||||
/>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="SkySettingsGroup / SkySettingsRow">
|
||||
<SkySettingsGroup title="SkySettingsGroup">
|
||||
<SkySettingsRow kind="navigation" title="SkySettingsRow">
|
||||
<template #leading>
|
||||
<SkySettingsIcon color="#5856d6">S</SkySettingsIcon>
|
||||
</template>
|
||||
</SkySettingsRow>
|
||||
<SkySettingsRangeRow
|
||||
v-model="rangeValue"
|
||||
title="SkySettingsRangeRow"
|
||||
/>
|
||||
</SkySettingsGroup>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="Overlays">
|
||||
<div class="sky-ui-kitchen-sink__row">
|
||||
<SkyButton @click="actionSheetOpened = true"
|
||||
>SkyActionSheet</SkyButton
|
||||
>
|
||||
<SkyButton @click="dialogOpened = true">SkyDialog</SkyButton>
|
||||
<SkyButton @click="notificationOpened = true"
|
||||
>SkyNotification</SkyButton
|
||||
>
|
||||
<SkyButton @click="panelOpened = true">SkyPanel</SkyButton>
|
||||
<SkyButton id="sky-ui-popover-anchor" @click="popoverOpened = true">
|
||||
SkyPopover
|
||||
</SkyButton>
|
||||
<SkyButton @click="popupOpened = true">SkyPopup</SkyButton>
|
||||
<SkyButton @click="sheetOpened = true">SkySheet</SkyButton>
|
||||
<SkyButton @click="toastOpened = true">SkyToast</SkyButton>
|
||||
</div>
|
||||
</SkySection>
|
||||
</SkyScrollArea>
|
||||
|
||||
<SkyActionSheet
|
||||
aria-label="SkyActionSheet"
|
||||
:opened="actionSheetOpened"
|
||||
@backdropclick="actionSheetOpened = false"
|
||||
@escape="actionSheetOpened = false"
|
||||
>
|
||||
<SkyActionGroup>
|
||||
<SkyActionsLabel>SkyActionsLabel</SkyActionsLabel>
|
||||
<SkyActionButton>SkyActionButton</SkyActionButton>
|
||||
</SkyActionGroup>
|
||||
<SkyActionGroup>
|
||||
<SkyActionButton bold @click="actionSheetOpened = false">
|
||||
Close
|
||||
</SkyActionButton>
|
||||
</SkyActionGroup>
|
||||
</SkyActionSheet>
|
||||
|
||||
<SkyDialog
|
||||
:opened="dialogOpened"
|
||||
title="SkyDialog"
|
||||
@backdropclick="dialogOpened = false"
|
||||
@escape="dialogOpened = false"
|
||||
>
|
||||
<p>SkyDialog content</p>
|
||||
<template #buttons>
|
||||
<SkyDialogButton strong @click="dialogOpened = false">
|
||||
Close
|
||||
</SkyDialogButton>
|
||||
</template>
|
||||
</SkyDialog>
|
||||
|
||||
<SkyNotification
|
||||
button="Close"
|
||||
close-label="Close notification"
|
||||
:opened="notificationOpened"
|
||||
text="SkyNotification text"
|
||||
title="SkyNotification"
|
||||
@close="notificationOpened = false"
|
||||
/>
|
||||
|
||||
<SkyPanel
|
||||
aria-label="SkyPanel"
|
||||
:opened="panelOpened"
|
||||
@backdropclick="panelOpened = false"
|
||||
@escape="panelOpened = false"
|
||||
>
|
||||
<SkyBlock
|
||||
><SkyButton @click="panelOpened = false">Close</SkyButton></SkyBlock
|
||||
>
|
||||
</SkyPanel>
|
||||
|
||||
<SkyPopover
|
||||
aria-label="SkyPopover"
|
||||
:opened="popoverOpened"
|
||||
target="#sky-ui-popover-anchor"
|
||||
@backdropclick="popoverOpened = false"
|
||||
@escape="popoverOpened = false"
|
||||
>
|
||||
<SkyBlock
|
||||
><SkyButton @click="popoverOpened = false">Close</SkyButton></SkyBlock
|
||||
>
|
||||
</SkyPopover>
|
||||
|
||||
<SkyPopup
|
||||
aria-label="SkyPopup"
|
||||
:opened="popupOpened"
|
||||
@backdropclick="popupOpened = false"
|
||||
@escape="popupOpened = false"
|
||||
>
|
||||
<SkyBlock
|
||||
><SkyButton @click="popupOpened = false">Close</SkyButton></SkyBlock
|
||||
>
|
||||
</SkyPopup>
|
||||
|
||||
<SkySheet
|
||||
aria-label="SkySheet"
|
||||
:opened="sheetOpened"
|
||||
@backdropclick="sheetOpened = false"
|
||||
@escape="sheetOpened = false"
|
||||
>
|
||||
<SkyBlock
|
||||
><SkyButton @click="sheetOpened = false">Close</SkyButton></SkyBlock
|
||||
>
|
||||
</SkySheet>
|
||||
|
||||
<SkyToast :opened="toastOpened">
|
||||
SkyToast
|
||||
<template #button>
|
||||
<SkyButton small @click="toastOpened = false">Close</SkyButton>
|
||||
</template>
|
||||
</SkyToast>
|
||||
</SkyAppPage>
|
||||
<div class="sky-ui-kitchen-sink">
|
||||
<Transition name="sky-ui-demo-page" mode="out-in">
|
||||
<component :is="activePage" :key="activeDemo ?? 'catalog'" />
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sky-ui-kitchen-sink__scroll {
|
||||
display: grid;
|
||||
gap: var(--sky-space-5);
|
||||
padding: var(--sky-space-4) var(--sky-page-gutter)
|
||||
calc(var(--sky-space-6) + var(--sky-safe-area-bottom));
|
||||
}
|
||||
|
||||
.sky-ui-kitchen-sink__grid {
|
||||
display: grid;
|
||||
gap: var(--sky-space-3);
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sky-ui-kitchen-sink__row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sky-space-2);
|
||||
}
|
||||
|
||||
.sky-ui-kitchen-sink__stack {
|
||||
display: grid;
|
||||
gap: var(--sky-space-3);
|
||||
}
|
||||
|
||||
.sky-ui-kitchen-sink__mini-app {
|
||||
border-radius: var(--sky-radius-card);
|
||||
min-height: 96px;
|
||||
overflow: hidden;
|
||||
padding: var(--sky-space-3);
|
||||
}
|
||||
|
||||
.sky-ui-kitchen-sink__mini-app :deep(.sky-surface) {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
.sky-ui-kitchen-sink {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
.sky-ui-kitchen-sink__rail-card {
|
||||
flex: 0 0 140px;
|
||||
.sky-ui-demo-page-enter-active,
|
||||
.sky-ui-demo-page-leave-active {
|
||||
transition:
|
||||
opacity 180ms ease,
|
||||
transform 180ms ease;
|
||||
}
|
||||
|
||||
.sky-ui-kitchen-sink__inline-navigation {
|
||||
display: grid;
|
||||
gap: var(--sky-space-3);
|
||||
margin-top: var(--sky-space-3);
|
||||
.sky-ui-demo-page-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.sky-ui-kitchen-sink__inline-navigation :deep(.sky-pill-navigation),
|
||||
.sky-ui-kitchen-sink__inline-navigation :deep(.sky-tabbar) {
|
||||
position: relative;
|
||||
.sky-ui-demo-page-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.sky-ui-kitchen-sink__grid {
|
||||
grid-template-columns: 1fr;
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sky-ui-demo-page-enter-active,
|
||||
.sky-ui-demo-page-leave-active {
|
||||
transition-duration: 0.01ms;
|
||||
}
|
||||
|
||||
.sky-ui-demo-page-enter-from,
|
||||
.sky-ui-demo-page-leave-to {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
<script setup lang="ts">
|
||||
import { Palette } from 'lucide-vue-next'
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyAppPage,
|
||||
SkyBlockTitle,
|
||||
SkyList,
|
||||
SkyListItem,
|
||||
SkyNavbar,
|
||||
SkyPopover,
|
||||
SkyRadio,
|
||||
SkyScrollArea,
|
||||
SkyToggle,
|
||||
} from '@/ui'
|
||||
|
||||
import demoIcon from './assets/demo-icon.png'
|
||||
import { SKY_UI_DEMO_CATALOG, SKY_UI_EXTENSION_CATALOG } from './catalog'
|
||||
import { useSkyUiDemoContext, type SkyUiDemoAccent } from './context'
|
||||
|
||||
const demo = useSkyUiDemoContext()
|
||||
const colorPickerOpened = ref(false)
|
||||
const colorPickerTarget = ref<HTMLElement | null>(null)
|
||||
|
||||
const accents: SkyUiDemoAccent[] = [
|
||||
{ color: '#007aff', name: 'Blue', soft: 'rgba(0, 122, 255, 0.16)' },
|
||||
{ color: '#ff3b30', name: 'Red', soft: 'rgba(255, 59, 48, 0.16)' },
|
||||
{ color: '#34c759', name: 'Green', soft: 'rgba(52, 199, 89, 0.16)' },
|
||||
{ color: '#ffcc00', name: 'Yellow', soft: 'rgba(255, 204, 0, 0.18)' },
|
||||
{ color: '#af52de', name: 'Purple', soft: 'rgba(175, 82, 222, 0.16)' },
|
||||
]
|
||||
|
||||
function chooseAccent(accent: SkyUiDemoAccent): void {
|
||||
demo.accentChoice.value = accent
|
||||
colorPickerOpened.value = false
|
||||
}
|
||||
|
||||
function syncColorPickerTarget(): void {
|
||||
colorPickerTarget.value = document.querySelector<HTMLElement>(
|
||||
'#sky-ui-demo-color-anchor',
|
||||
)
|
||||
}
|
||||
|
||||
function handleDarkModeRowClick(event: MouseEvent): void {
|
||||
const target = event.target
|
||||
if (target instanceof Element && target.closest('.sky-toggle')) return
|
||||
demo.dark.value = !demo.dark.value
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
syncColorPickerTarget()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
colorPickerTarget.value = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyAppPage
|
||||
class="sky-ui-demo-home"
|
||||
:accent="demo.accent.value"
|
||||
:accent-soft="demo.accentSoft.value"
|
||||
:dark="demo.dark.value"
|
||||
label="Sky UI Kitchen Sink"
|
||||
>
|
||||
<SkyNavbar
|
||||
back-label="Back to Settings"
|
||||
show-back
|
||||
title="Sky UI"
|
||||
transparent
|
||||
variant="large"
|
||||
@back="demo.exit"
|
||||
/>
|
||||
|
||||
<SkyScrollArea class="sky-ui-demo-home__scroll">
|
||||
<SkyBlockTitle>Theme</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyListItem label title="iOS Theme">
|
||||
<template #media>
|
||||
<SkyRadio aria-label="iOS Theme" checked value="ios" />
|
||||
</template>
|
||||
</SkyListItem>
|
||||
<SkyListItem
|
||||
disabled
|
||||
label
|
||||
subtitle="Sky UI currently follows the Konsta iOS reference."
|
||||
title="Material Theme"
|
||||
>
|
||||
<template #media>
|
||||
<SkyRadio
|
||||
aria-label="Material Theme (not available)"
|
||||
disabled
|
||||
value="material"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyList inset strong>
|
||||
<SkyListItem
|
||||
class="sky-ui-demo-home__control-row"
|
||||
title="Dark Mode"
|
||||
@click="handleDarkModeRowClick"
|
||||
>
|
||||
<template #after>
|
||||
<SkyToggle
|
||||
v-model="demo.dark.value"
|
||||
aria-label="Dark Mode"
|
||||
component="div"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
<SkyListItem
|
||||
id="sky-ui-demo-color-anchor"
|
||||
link
|
||||
title="Color Theme"
|
||||
@click="colorPickerOpened = true"
|
||||
>
|
||||
<template #after>
|
||||
<span
|
||||
class="sky-ui-demo-home__current-color"
|
||||
:style="{ backgroundColor: demo.accent.value }"
|
||||
:title="demo.accentChoice.value.name"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Components</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyListItem
|
||||
v-for="entry in SKY_UI_DEMO_CATALOG"
|
||||
:key="entry.id"
|
||||
link
|
||||
:title="entry.title"
|
||||
@click="demo.navigate(entry.id)"
|
||||
>
|
||||
<template #media>
|
||||
<img
|
||||
class="sky-ui-demo-home__component-icon"
|
||||
:src="demoIcon"
|
||||
alt=""
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Sky Extensions</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyListItem
|
||||
v-for="entry in SKY_UI_EXTENSION_CATALOG"
|
||||
:key="entry.id"
|
||||
link
|
||||
:title="entry.title"
|
||||
@click="demo.navigate(entry.id)"
|
||||
>
|
||||
<template #media>
|
||||
<Palette :size="28" :stroke-width="1.8" aria-hidden="true" />
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
</SkyScrollArea>
|
||||
|
||||
<SkyPopover
|
||||
angle
|
||||
aria-label="Choose color theme"
|
||||
:opened="colorPickerOpened"
|
||||
:target="colorPickerTarget"
|
||||
@backdropclick="colorPickerOpened = false"
|
||||
@escape="colorPickerOpened = false"
|
||||
>
|
||||
<div class="sky-ui-demo-home__palette">
|
||||
<button
|
||||
v-for="accent in accents"
|
||||
:key="accent.name"
|
||||
class="sky-ui-demo-home__color-button"
|
||||
type="button"
|
||||
:aria-label="`${accent.name} color theme`"
|
||||
:aria-pressed="accent.color === demo.accent.value"
|
||||
@click="chooseAccent(accent)"
|
||||
>
|
||||
<span
|
||||
class="sky-ui-demo-home__color-swatch"
|
||||
:style="{ backgroundColor: accent.color }"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</SkyPopover>
|
||||
</SkyAppPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sky-ui-demo-home__scroll {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.sky-ui-demo-home__current-color {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
border: 1px solid rgb(255 255 255 / 20%);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.sky-ui-demo-home__component-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sky-ui-demo-home__control-row :deep(.sky-list-item__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sky-ui-demo-home__palette {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 48px);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.sky-ui-demo-home__color-button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sky-ui-demo-home__color-button:focus-visible {
|
||||
outline: 2px solid var(--sky-app-accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.sky-ui-demo-home__color-swatch {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.sky-ui-demo-home__color-button[aria-pressed='true'] {
|
||||
background: var(--sky-pressed);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { SkyAppPage, SkyNavbar, SkyScrollArea } from '@/ui'
|
||||
|
||||
import { useSkyUiDemoContext } from './context'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
scrollClass?: string
|
||||
subtitle?: string
|
||||
title: string
|
||||
transparent?: boolean
|
||||
variant?: 'compact' | 'large' | 'medium'
|
||||
withTabbar?: boolean
|
||||
}>(),
|
||||
{
|
||||
scrollClass: '',
|
||||
subtitle: '',
|
||||
transparent: false,
|
||||
variant: 'compact',
|
||||
withTabbar: false,
|
||||
},
|
||||
)
|
||||
|
||||
const demo = useSkyUiDemoContext()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyAppPage
|
||||
class="sky-ui-demo-page"
|
||||
:accent="demo.accent.value"
|
||||
:accent-soft="demo.accentSoft.value"
|
||||
:dark="demo.dark.value"
|
||||
:label="title"
|
||||
>
|
||||
<SkyNavbar
|
||||
back-appearance="surface"
|
||||
back-label="Back"
|
||||
show-back
|
||||
:subtitle="subtitle"
|
||||
:title="title"
|
||||
:transparent="transparent"
|
||||
:variant="variant"
|
||||
@back="demo.returnToCatalog"
|
||||
>
|
||||
<template v-if="$slots.navbarRight" #right>
|
||||
<slot name="navbarRight" />
|
||||
</template>
|
||||
<template v-if="$slots.subnavbar" #subnavbar>
|
||||
<slot name="subnavbar" />
|
||||
</template>
|
||||
</SkyNavbar>
|
||||
|
||||
<SkyScrollArea
|
||||
class="sky-ui-demo-page__scroll"
|
||||
:class="scrollClass"
|
||||
:with-tabbar="withTabbar"
|
||||
>
|
||||
<slot />
|
||||
</SkyScrollArea>
|
||||
|
||||
<slot name="fixed" />
|
||||
</SkyAppPage>
|
||||
</template>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,53 @@
|
||||
export const SKY_UI_DEMO_CATALOG = [
|
||||
{ id: 'action-sheet', title: 'Action Sheet' },
|
||||
{ id: 'badge', title: 'Badge' },
|
||||
{ id: 'breadcrumbs', title: 'Breadcrumbs' },
|
||||
{ id: 'buttons', title: 'Buttons' },
|
||||
{ id: 'cards', title: 'Cards' },
|
||||
{ id: 'checkbox', title: 'Checkbox' },
|
||||
{ id: 'chips', title: 'Chips' },
|
||||
{ id: 'contacts-list', title: 'Contacts List' },
|
||||
{ id: 'content-block', title: 'Content Block' },
|
||||
{ id: 'data-table', title: 'Data Table' },
|
||||
{ id: 'dialog', title: 'Dialog' },
|
||||
{ id: 'fab', title: 'FAB (Floating Action Button)' },
|
||||
{ id: 'form-inputs', title: 'Form Inputs' },
|
||||
{ id: 'list', title: 'List' },
|
||||
{ id: 'list-button', title: 'List Button' },
|
||||
{ id: 'menu-list', title: 'Menu List' },
|
||||
{ id: 'messages', title: 'Messages' },
|
||||
{ id: 'navbar', title: 'Navbar' },
|
||||
{ id: 'notification', title: 'Notification' },
|
||||
{ id: 'side-panels', title: 'Panel / Side Panels' },
|
||||
{ id: 'popover', title: 'Popover' },
|
||||
{ id: 'popup', title: 'Popup' },
|
||||
{ id: 'preloader', title: 'Preloader' },
|
||||
{ id: 'progressbar', title: 'Progressbar' },
|
||||
{ id: 'radio', title: 'Radio' },
|
||||
{ id: 'range-slider', title: 'Range Slider' },
|
||||
{ id: 'searchbar', title: 'Searchbar' },
|
||||
{ id: 'segmented-control', title: 'Segmented Control' },
|
||||
{ id: 'sheet-modal', title: 'Sheet Modal' },
|
||||
{ id: 'stepper', title: 'Stepper' },
|
||||
{ id: 'subnavbar', title: 'Subnavbar' },
|
||||
{ id: 'tabbar', title: 'Tabbar' },
|
||||
{ id: 'toast', title: 'Toast' },
|
||||
{ id: 'toggle', title: 'Toggle' },
|
||||
{ id: 'toolbar', title: 'Toolbar' },
|
||||
] as const
|
||||
|
||||
export const SKY_UI_EXTENSION_CATALOG = [
|
||||
{ id: 'sky-extensions', title: 'Sky Extensions' },
|
||||
] as const
|
||||
|
||||
export type SkyUiDemoId =
|
||||
| (typeof SKY_UI_DEMO_CATALOG)[number]['id']
|
||||
| (typeof SKY_UI_EXTENSION_CATALOG)[number]['id']
|
||||
|
||||
const demoIds = new Set<string>(
|
||||
[...SKY_UI_DEMO_CATALOG, ...SKY_UI_EXTENSION_CATALOG].map(({ id }) => id),
|
||||
)
|
||||
|
||||
export function isSkyUiDemoId(value: unknown): value is SkyUiDemoId {
|
||||
return typeof value === 'string' && demoIds.has(value)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
computed,
|
||||
inject,
|
||||
provide,
|
||||
type ComputedRef,
|
||||
type InjectionKey,
|
||||
type Ref,
|
||||
} from 'vue'
|
||||
|
||||
import type { SkyUiDemoId } from './catalog'
|
||||
|
||||
export interface SkyUiDemoAccent {
|
||||
color: string
|
||||
name: string
|
||||
soft: string
|
||||
}
|
||||
|
||||
export interface SkyUiDemoContext {
|
||||
accent: ComputedRef<string>
|
||||
accentChoice: Ref<SkyUiDemoAccent>
|
||||
accentSoft: ComputedRef<string>
|
||||
dark: Ref<boolean>
|
||||
exit: () => void
|
||||
navigate: (id: SkyUiDemoId) => void
|
||||
returnToCatalog: () => void
|
||||
}
|
||||
|
||||
const skyUiDemoContextKey: InjectionKey<SkyUiDemoContext> = Symbol(
|
||||
'sky-ui-demo-context',
|
||||
)
|
||||
|
||||
export function provideSkyUiDemoContext(context: SkyUiDemoContext): void {
|
||||
provide(skyUiDemoContextKey, context)
|
||||
}
|
||||
|
||||
export function createSkyUiDemoTheme(
|
||||
dark: Ref<boolean>,
|
||||
accentChoice: Ref<SkyUiDemoAccent>,
|
||||
): Pick<SkyUiDemoContext, 'accent' | 'accentChoice' | 'accentSoft' | 'dark'> {
|
||||
return {
|
||||
accent: computed(() => accentChoice.value.color),
|
||||
accentChoice,
|
||||
accentSoft: computed(() => accentChoice.value.soft),
|
||||
dark,
|
||||
}
|
||||
}
|
||||
|
||||
export function useSkyUiDemoContext(): SkyUiDemoContext {
|
||||
const context = inject(skyUiDemoContextKey)
|
||||
if (!context) throw new Error('Sky UI demo context is unavailable.')
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
.sky-ui-demo-page__scroll {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.sky-ui-demo-row {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--sky-space-2);
|
||||
}
|
||||
|
||||
.sky-ui-demo-row--center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sky-ui-demo-stack {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: var(--sky-space-3);
|
||||
}
|
||||
|
||||
.sky-ui-demo-grid {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--sky-space-3);
|
||||
}
|
||||
|
||||
.sky-ui-demo-copy {
|
||||
margin: 0;
|
||||
color: var(--sky-muted);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.sky-ui-demo-media {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sky-ui-demo-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
background: var(--sky-app-accent-soft);
|
||||
color: var(--sky-app-accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sky-ui-demo-fill {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sky-ui-demo-spacer {
|
||||
height: 360px;
|
||||
}
|
||||
|
||||
.sky-ui-demo-color-red {
|
||||
--sky-app-accent: #ff3b30;
|
||||
--sky-app-accent-soft: rgba(255, 59, 48, 0.16);
|
||||
--sky-chip-outline-border: #ff3b30;
|
||||
--sky-chip-outline-text: #ff3b30;
|
||||
}
|
||||
|
||||
.sky-ui-demo-color-green {
|
||||
--sky-app-accent: #4cd964;
|
||||
--sky-app-accent-soft: rgba(76, 217, 100, 0.16);
|
||||
--sky-chip-outline-border: #4cd964;
|
||||
--sky-chip-outline-text: #4cd964;
|
||||
}
|
||||
|
||||
.sky-ui-demo-color-yellow {
|
||||
--sky-app-accent: #ffcc00;
|
||||
--sky-app-accent-soft: rgba(255, 204, 0, 0.18);
|
||||
--sky-chip-outline-border: #ffcc00;
|
||||
--sky-chip-outline-text: #ffcc00;
|
||||
}
|
||||
|
||||
.sky-ui-demo-color-purple {
|
||||
--sky-app-accent: #9c27b0;
|
||||
--sky-app-accent-soft: rgba(156, 39, 176, 0.16);
|
||||
}
|
||||
|
||||
.sky-ui-demo-overlay-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.sky-ui-demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyActionButton,
|
||||
SkyActionGroup,
|
||||
SkyActionSheet,
|
||||
SkyActionsLabel,
|
||||
SkyBlock,
|
||||
SkyBlockTitle,
|
||||
SkyButton,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const actionsOneOpened = ref(false)
|
||||
const actionsTwoOpened = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage class="action-sheet-demo" title="Action Sheet">
|
||||
<SkyBlock inset strong>
|
||||
Action Sheet is a slide-up pane for presenting the user with a set of
|
||||
alternatives for how to proceed with a given task.
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Open Action Sheet</SkyBlockTitle>
|
||||
<SkyBlock class="sky-ui-demo-row" inset strong>
|
||||
<SkyButton
|
||||
class="action-sheet-demo__trigger"
|
||||
rounded
|
||||
@click="actionsOneOpened = true"
|
||||
>
|
||||
One group
|
||||
</SkyButton>
|
||||
<SkyButton
|
||||
class="action-sheet-demo__trigger"
|
||||
rounded
|
||||
@click="actionsTwoOpened = true"
|
||||
>
|
||||
Two groups
|
||||
</SkyButton>
|
||||
</SkyBlock>
|
||||
|
||||
<template #fixed>
|
||||
<SkyActionSheet
|
||||
aria-label="Do something"
|
||||
:opened="actionsOneOpened"
|
||||
@backdropclick="actionsOneOpened = false"
|
||||
@escape="actionsOneOpened = false"
|
||||
>
|
||||
<SkyActionGroup>
|
||||
<SkyActionsLabel>Do something</SkyActionsLabel>
|
||||
<SkyActionButton bold @click="actionsOneOpened = false">
|
||||
Button 1
|
||||
</SkyActionButton>
|
||||
<SkyActionButton @click="actionsOneOpened = false">
|
||||
Button 2
|
||||
</SkyActionButton>
|
||||
<SkyActionButton @click="actionsOneOpened = false">
|
||||
Cancel
|
||||
</SkyActionButton>
|
||||
</SkyActionGroup>
|
||||
</SkyActionSheet>
|
||||
|
||||
<SkyActionSheet
|
||||
aria-label="Do something"
|
||||
:opened="actionsTwoOpened"
|
||||
@backdropclick="actionsTwoOpened = false"
|
||||
@escape="actionsTwoOpened = false"
|
||||
>
|
||||
<SkyActionGroup>
|
||||
<SkyActionsLabel>Do something</SkyActionsLabel>
|
||||
<SkyActionButton bold @click="actionsTwoOpened = false">
|
||||
Button 1
|
||||
</SkyActionButton>
|
||||
<SkyActionButton @click="actionsTwoOpened = false">
|
||||
Button 2
|
||||
</SkyActionButton>
|
||||
</SkyActionGroup>
|
||||
<SkyActionGroup>
|
||||
<SkyActionButton @click="actionsTwoOpened = false">
|
||||
Cancel
|
||||
</SkyActionButton>
|
||||
</SkyActionGroup>
|
||||
</SkyActionSheet>
|
||||
</template>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.action-sheet-demo__trigger {
|
||||
min-width: 0;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(new URL('./BadgeDemo.vue', import.meta.url), 'utf8')
|
||||
|
||||
describe('BadgeDemo', () => {
|
||||
it('uses the shared interactive Glass system for the profile action', () => {
|
||||
expect(source).toContain('SkyGlass,')
|
||||
expect(source).toMatch(
|
||||
/<SkyGlass[\s\S]*?aria-label="Profile, 5 notifications"[\s\S]*?class="badge-demo__profile-action"[\s\S]*?component="button"[\s\S]*?>/,
|
||||
)
|
||||
expect(source).not.toContain('<SkyLink aria-label="Profile')
|
||||
})
|
||||
|
||||
it('keeps the profile action circular, accessible, and token sized', () => {
|
||||
expect(source).toMatch(
|
||||
/\.badge-demo__profile-action\s*\{[^}]*width:\s*var\(--sky-touch-target, 44px\)[^}]*height:\s*var\(--sky-touch-target, 44px\)[^}]*border:\s*0[^}]*border-radius:\s*50%[^}]*color:\s*inherit/s,
|
||||
)
|
||||
expect(source).toMatch(
|
||||
/\.badge-demo__profile-action:focus-visible\s*\{[^}]*outline:\s*2px solid var\(--sky-app-accent, #007aff\)/s,
|
||||
)
|
||||
expect(source).toMatch(
|
||||
/\.badge-demo__icon-badge\s*\{[^}]*inset-block-start:\s*-2px[^}]*inset-inline-end:\s*-6px/s,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CalendarDays,
|
||||
CircleUserRound,
|
||||
CloudUpload,
|
||||
Mail,
|
||||
} from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyBadge,
|
||||
SkyGlass,
|
||||
SkyIcon,
|
||||
SkyList,
|
||||
SkyListItem,
|
||||
SkyTabBar,
|
||||
SkyTabButton,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
import demoIcon from '../assets/demo-icon.png'
|
||||
|
||||
const activeTab = ref('inbox')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Badge" with-tabbar>
|
||||
<template #navbarRight>
|
||||
<SkyGlass
|
||||
aria-label="Profile, 5 notifications"
|
||||
class="badge-demo__profile-action"
|
||||
component="button"
|
||||
type="button"
|
||||
>
|
||||
<span class="badge-demo__icon-wrap">
|
||||
<SkyIcon :size="28"><CircleUserRound /></SkyIcon>
|
||||
<SkyBadge class="badge-demo__icon-badge" small tone="danger">
|
||||
5
|
||||
</SkyBadge>
|
||||
</span>
|
||||
</SkyGlass>
|
||||
</template>
|
||||
|
||||
<SkyList inset strong>
|
||||
<SkyListItem title="Foo Bar">
|
||||
<template #media>
|
||||
<img class="badge-demo__list-icon" :src="demoIcon" alt="" />
|
||||
</template>
|
||||
<template #after><SkyBadge>0</SkyBadge></template>
|
||||
</SkyListItem>
|
||||
<SkyListItem title="Ivan Petrov">
|
||||
<template #media>
|
||||
<img class="badge-demo__list-icon" :src="demoIcon" alt="" />
|
||||
</template>
|
||||
<template #after><SkyBadge tone="info">CEO</SkyBadge></template>
|
||||
</SkyListItem>
|
||||
<SkyListItem title="John Doe">
|
||||
<template #media>
|
||||
<img class="badge-demo__list-icon" :src="demoIcon" alt="" />
|
||||
</template>
|
||||
<template #after><SkyBadge tone="success">5</SkyBadge></template>
|
||||
</SkyListItem>
|
||||
<SkyListItem title="Jane Doe">
|
||||
<template #media>
|
||||
<img class="badge-demo__list-icon" :src="demoIcon" alt="" />
|
||||
</template>
|
||||
<template #after><SkyBadge tone="warning">NEW</SkyBadge></template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<template #fixed>
|
||||
<SkyTabBar label="Badge demo tabs">
|
||||
<SkyTabButton
|
||||
:active="activeTab === 'inbox'"
|
||||
label="Inbox"
|
||||
@click="activeTab = 'inbox'"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="badge-demo__icon-wrap">
|
||||
<Mail />
|
||||
<SkyBadge class="badge-demo__icon-badge" small tone="success">
|
||||
5
|
||||
</SkyBadge>
|
||||
</span>
|
||||
</template>
|
||||
</SkyTabButton>
|
||||
<SkyTabButton
|
||||
:active="activeTab === 'calendar'"
|
||||
label="Calendar"
|
||||
@click="activeTab = 'calendar'"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="badge-demo__icon-wrap">
|
||||
<CalendarDays />
|
||||
<SkyBadge class="badge-demo__icon-badge" small tone="danger">
|
||||
7
|
||||
</SkyBadge>
|
||||
</span>
|
||||
</template>
|
||||
</SkyTabButton>
|
||||
<SkyTabButton
|
||||
:active="activeTab === 'upload'"
|
||||
label="Upload"
|
||||
@click="activeTab = 'upload'"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="badge-demo__icon-wrap">
|
||||
<CloudUpload />
|
||||
<SkyBadge class="badge-demo__icon-badge" small tone="danger">
|
||||
1
|
||||
</SkyBadge>
|
||||
</span>
|
||||
</template>
|
||||
</SkyTabButton>
|
||||
</SkyTabBar>
|
||||
</template>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.badge-demo__profile-action {
|
||||
box-sizing: border-box;
|
||||
width: var(--sky-touch-target, 44px);
|
||||
height: var(--sky-touch-target, 44px);
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.badge-demo__profile-action:focus-visible {
|
||||
outline: 2px solid var(--sky-app-accent, #007aff);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.badge-demo__icon-wrap {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.badge-demo__icon-wrap > svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.badge-demo__icon-badge {
|
||||
position: absolute;
|
||||
inset-block-start: -2px;
|
||||
inset-inline-end: -6px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.badge-demo__list-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(
|
||||
new URL('./BreadcrumbsDemo.vue', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
describe('BreadcrumbsDemo', () => {
|
||||
it('matches the Konsta collapsed breadcrumb popover composition', () => {
|
||||
const popover = source.slice(
|
||||
source.indexOf('<SkyPopover'),
|
||||
source.indexOf('</SkyPopover>'),
|
||||
)
|
||||
|
||||
expect(popover).toContain(':offset="0"')
|
||||
expect(popover).not.toMatch(/\sangle(?:\s|>)/)
|
||||
expect(popover).not.toContain('placement="top"')
|
||||
expect(popover.match(/<SkyListItem/g)).toHaveLength(3)
|
||||
expect(popover.match(/\blink\b/g)).toHaveLength(3)
|
||||
expect(popover).not.toMatch(/\smenu(?:\s|=|>)/)
|
||||
expect(source).toMatch(/\.breadcrumbs-demo__menu\s*\{[^}]*width:\s*100%;/s)
|
||||
expect(source).not.toMatch(/width:\s*120px;/)
|
||||
})
|
||||
|
||||
it('keeps the collapsed popover usable through every close path', () => {
|
||||
expect(source).toContain('@click="popoverOpened = true"')
|
||||
expect(source).toContain('@backdropclick="popoverOpened = false"')
|
||||
expect(source).toContain('@escape="popoverOpened = false"')
|
||||
expect(source.match(/@click="popoverOpened = false"/g)).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyBlock,
|
||||
SkyBlockHeader,
|
||||
SkyBlockTitle,
|
||||
SkyBreadcrumbs,
|
||||
SkyBreadcrumbsCollapsed,
|
||||
SkyBreadcrumbsItem,
|
||||
SkyBreadcrumbsSeparator,
|
||||
SkyList,
|
||||
SkyListItem,
|
||||
SkyPopover,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const popoverOpened = ref(false)
|
||||
const popoverTarget = ref<HTMLElement | null>(null)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Breadcrumbs">
|
||||
<SkyBlock inset strong>
|
||||
Breadcrumbs allow users to keep track and maintain awareness of their
|
||||
locations within the app or website. They should be used for large sites
|
||||
and apps with hierarchically arranged pages.
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Basic</SkyBlockTitle>
|
||||
<SkyBlock outline strong>
|
||||
<SkyBreadcrumbs aria-label="Basic breadcrumb">
|
||||
<SkyBreadcrumbsItem component="button">Home</SkyBreadcrumbsItem>
|
||||
<SkyBreadcrumbsSeparator />
|
||||
<SkyBreadcrumbsItem component="button">Catalog</SkyBreadcrumbsItem>
|
||||
<SkyBreadcrumbsSeparator />
|
||||
<SkyBreadcrumbsItem active>Phones</SkyBreadcrumbsItem>
|
||||
</SkyBreadcrumbs>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Scrollable</SkyBlockTitle>
|
||||
<SkyBlockHeader>
|
||||
Breadcrumbs will be scrollable if they don't fit the screen
|
||||
</SkyBlockHeader>
|
||||
<SkyBlock outline strong>
|
||||
<SkyBreadcrumbs aria-label="Scrollable breadcrumb">
|
||||
<SkyBreadcrumbsItem component="button">Home</SkyBreadcrumbsItem>
|
||||
<SkyBreadcrumbsSeparator />
|
||||
<SkyBreadcrumbsItem component="button">Catalog</SkyBreadcrumbsItem>
|
||||
<SkyBreadcrumbsSeparator />
|
||||
<SkyBreadcrumbsItem component="button">Phones</SkyBreadcrumbsItem>
|
||||
<SkyBreadcrumbsSeparator />
|
||||
<SkyBreadcrumbsItem component="button">Apple</SkyBreadcrumbsItem>
|
||||
<SkyBreadcrumbsSeparator />
|
||||
<SkyBreadcrumbsItem active>iPhone 12</SkyBreadcrumbsItem>
|
||||
</SkyBreadcrumbs>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Collapsed</SkyBlockTitle>
|
||||
<SkyBlock outline strong>
|
||||
<SkyBreadcrumbs aria-label="Collapsed breadcrumb">
|
||||
<SkyBreadcrumbsItem component="button">Home</SkyBreadcrumbsItem>
|
||||
<SkyBreadcrumbsSeparator />
|
||||
<span ref="popoverTarget" class="breadcrumbs-demo__target">
|
||||
<SkyBreadcrumbsCollapsed
|
||||
ariaControls="breadcrumbs-menu"
|
||||
ariaLabel="Show hidden breadcrumbs"
|
||||
:expanded="popoverOpened"
|
||||
@click="popoverOpened = true"
|
||||
/>
|
||||
</span>
|
||||
<SkyBreadcrumbsSeparator />
|
||||
<SkyBreadcrumbsItem active>iPhone 12</SkyBreadcrumbsItem>
|
||||
</SkyBreadcrumbs>
|
||||
</SkyBlock>
|
||||
|
||||
<template #fixed>
|
||||
<SkyPopover
|
||||
id="breadcrumbs-menu"
|
||||
aria-label="Hidden breadcrumbs"
|
||||
:opened="popoverOpened"
|
||||
:offset="0"
|
||||
role="region"
|
||||
:target="popoverTarget"
|
||||
@backdropclick="popoverOpened = false"
|
||||
@escape="popoverOpened = false"
|
||||
>
|
||||
<SkyList class="breadcrumbs-demo__menu" nested>
|
||||
<SkyListItem link title="Catalog" @click="popoverOpened = false" />
|
||||
<SkyListItem link title="Phones" @click="popoverOpened = false" />
|
||||
<SkyListItem link title="Apple" @click="popoverOpened = false" />
|
||||
</SkyList>
|
||||
</SkyPopover>
|
||||
</template>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.breadcrumbs-demo__menu {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.breadcrumbs-demo__target {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import { SkyBlock, SkyBlockTitle, SkyButton } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Buttons">
|
||||
<SkyBlockTitle>Default Buttons</SkyBlockTitle>
|
||||
<SkyBlock class="button-examples" inset strong>
|
||||
<div class="button-row">
|
||||
<SkyButton>Button</SkyButton>
|
||||
<SkyButton class="sky-ui-demo-color-red">Button</SkyButton>
|
||||
<SkyButton>Button</SkyButton>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<SkyButton outline>Outline</SkyButton>
|
||||
<SkyButton class="sky-ui-demo-color-red" outline>Outline</SkyButton>
|
||||
<SkyButton outline>Outline</SkyButton>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<SkyButton clear>Clear</SkyButton>
|
||||
<SkyButton class="sky-ui-demo-color-red" clear>Clear</SkyButton>
|
||||
<SkyButton clear>Clear</SkyButton>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<SkyButton tonal>Tonal</SkyButton>
|
||||
<SkyButton class="sky-ui-demo-color-red" tonal>Tonal</SkyButton>
|
||||
<SkyButton tonal>Tonal</SkyButton>
|
||||
</div>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Rounded Buttons</SkyBlockTitle>
|
||||
<SkyBlock class="button-examples" inset strong>
|
||||
<div class="button-row">
|
||||
<SkyButton rounded>Button</SkyButton>
|
||||
<SkyButton class="sky-ui-demo-color-green" rounded>Button</SkyButton>
|
||||
<SkyButton rounded>Button</SkyButton>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<SkyButton outline rounded>Outline</SkyButton>
|
||||
<SkyButton class="sky-ui-demo-color-green" outline rounded>
|
||||
Outline
|
||||
</SkyButton>
|
||||
<SkyButton outline rounded>Outline</SkyButton>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<SkyButton clear rounded>Clear</SkyButton>
|
||||
<SkyButton class="sky-ui-demo-color-green" clear rounded>
|
||||
Clear
|
||||
</SkyButton>
|
||||
<SkyButton clear rounded>Clear</SkyButton>
|
||||
</div>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Large Buttons</SkyBlockTitle>
|
||||
<SkyBlock class="button-examples" inset strong>
|
||||
<div class="button-row">
|
||||
<SkyButton large>Button</SkyButton>
|
||||
<SkyButton class="sky-ui-demo-color-yellow" large>Button</SkyButton>
|
||||
<SkyButton large rounded>Button</SkyButton>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<SkyButton large outline>Outline</SkyButton>
|
||||
<SkyButton class="sky-ui-demo-color-yellow" large outline>
|
||||
Outline
|
||||
</SkyButton>
|
||||
<SkyButton large outline rounded>Outline</SkyButton>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<SkyButton clear large>Clear</SkyButton>
|
||||
<SkyButton class="sky-ui-demo-color-yellow" clear large>
|
||||
Clear
|
||||
</SkyButton>
|
||||
<SkyButton clear large rounded>Clear</SkyButton>
|
||||
</div>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Small Buttons</SkyBlockTitle>
|
||||
<SkyBlock class="button-examples" inset strong>
|
||||
<div class="button-row">
|
||||
<SkyButton small>Button</SkyButton>
|
||||
<SkyButton small>Button</SkyButton>
|
||||
<SkyButton rounded small>Button</SkyButton>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<SkyButton outline small>Outline</SkyButton>
|
||||
<SkyButton outline small>Outline</SkyButton>
|
||||
<SkyButton outline rounded small>Outline</SkyButton>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<SkyButton clear small>Clear</SkyButton>
|
||||
<SkyButton clear small>Clear</SkyButton>
|
||||
<SkyButton clear rounded small>Clear</SkyButton>
|
||||
</div>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Raised Buttons</SkyBlockTitle>
|
||||
<SkyBlock class="button-examples" inset strong>
|
||||
<div class="button-row">
|
||||
<SkyButton raised>Button</SkyButton>
|
||||
<SkyButton raised>Button</SkyButton>
|
||||
<SkyButton raised rounded>Button</SkyButton>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<SkyButton outline raised>Outline</SkyButton>
|
||||
<SkyButton outline raised>Outline</SkyButton>
|
||||
<SkyButton outline raised rounded>Outline</SkyButton>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<SkyButton clear raised>Clear</SkyButton>
|
||||
<SkyButton clear raised>Clear</SkyButton>
|
||||
<SkyButton clear raised rounded>Clear</SkyButton>
|
||||
</div>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Disabled Buttons</SkyBlockTitle>
|
||||
<SkyBlock inset strong>
|
||||
<div class="button-row">
|
||||
<SkyButton disabled>Button</SkyButton>
|
||||
<SkyButton disabled outline>Outline</SkyButton>
|
||||
<SkyButton clear disabled>Clear</SkyButton>
|
||||
</div>
|
||||
</SkyBlock>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.button-examples {
|
||||
display: grid;
|
||||
gap: var(--sky-space-2, 8px);
|
||||
}
|
||||
|
||||
.button-row {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
column-gap: var(--sky-space-4, 16px);
|
||||
}
|
||||
|
||||
.button-row :deep(.sky-button) {
|
||||
width: 100%;
|
||||
min-width: 44px;
|
||||
padding-inline: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import { SkyBlockTitle, SkyCard, SkyLink, SkyList, SkyListItem } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Cards">
|
||||
<SkyBlockTitle>Simple Cards</SkyBlockTitle>
|
||||
<div class="cards-demo__stack">
|
||||
<SkyCard>
|
||||
This is a simple card with plain text, but cards can also contain their
|
||||
own header, footer, list view, image, or any other element.
|
||||
</SkyCard>
|
||||
<SkyCard footer="Card footer" header="Card header">
|
||||
Card with header and footer. Card headers are used to display card
|
||||
titles and footers for additional information or just for custom
|
||||
actions.
|
||||
</SkyCard>
|
||||
<SkyCard>
|
||||
Another card. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Suspendisse feugiat sem est, non tincidunt ligula volutpat sit amet.
|
||||
Mauris aliquet magna justo.
|
||||
</SkyCard>
|
||||
</div>
|
||||
|
||||
<SkyBlockTitle>Outline Cards</SkyBlockTitle>
|
||||
<div class="cards-demo__stack">
|
||||
<SkyCard outline>
|
||||
This is a simple card with plain text, but cards can also contain their
|
||||
own header, footer, list view, image, or any other element.
|
||||
</SkyCard>
|
||||
<SkyCard footer="Card footer" header="Card header" outline>
|
||||
Card with header and footer. Card headers are used to display card
|
||||
titles and footers for additional information or just for custom
|
||||
actions.
|
||||
</SkyCard>
|
||||
<SkyCard outline>
|
||||
Another card. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Suspendisse feugiat sem est, non tincidunt ligula volutpat sit amet.
|
||||
Mauris aliquet magna justo.
|
||||
</SkyCard>
|
||||
</div>
|
||||
|
||||
<SkyBlockTitle>Outline With Dividers</SkyBlockTitle>
|
||||
<SkyCard
|
||||
footer="Card footer"
|
||||
footer-divider
|
||||
header="Card header"
|
||||
header-divider
|
||||
outline
|
||||
>
|
||||
Card with header and footer. Card headers are used to display card titles
|
||||
and footers for additional information or just for custom actions.
|
||||
</SkyCard>
|
||||
|
||||
<SkyBlockTitle>Raised Cards</SkyBlockTitle>
|
||||
<div class="cards-demo__stack">
|
||||
<SkyCard raised>
|
||||
This is a simple card with plain text, but cards can also contain their
|
||||
own header, footer, list view, image, or any other element.
|
||||
</SkyCard>
|
||||
<SkyCard footer="Card footer" header="Card header" raised>
|
||||
Card with header and footer. Card headers are used to display card
|
||||
titles and footers for additional information or just for custom
|
||||
actions.
|
||||
</SkyCard>
|
||||
<SkyCard raised>
|
||||
Another card. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Suspendisse feugiat sem est, non tincidunt ligula volutpat sit amet.
|
||||
Mauris aliquet magna justo.
|
||||
</SkyCard>
|
||||
</div>
|
||||
|
||||
<SkyBlockTitle>Card With List View</SkyBlockTitle>
|
||||
<SkyCard :content-wrap="false">
|
||||
<SkyList flush nested>
|
||||
<SkyListItem
|
||||
v-for="index in 5"
|
||||
:key="index"
|
||||
link
|
||||
:title="`Link ${index}`"
|
||||
/>
|
||||
</SkyList>
|
||||
</SkyCard>
|
||||
|
||||
<SkyBlockTitle>Styled Cards</SkyBlockTitle>
|
||||
<div class="cards-demo__grid">
|
||||
<SkyCard outline>
|
||||
<div class="cards-demo__image cards-demo__image--mountains">
|
||||
Journey To Mountains
|
||||
</div>
|
||||
<div class="cards-demo__date">Posted on January 21, 2021</div>
|
||||
<p class="cards-demo__copy">
|
||||
Quisque eget vestibulum nulla. Quisque quis dui quis ex ultricies
|
||||
efficitur vitae non felis. Phasellus quis nibh hendrerit...
|
||||
</p>
|
||||
<template #footer>
|
||||
<div class="cards-demo__actions">
|
||||
<SkyLink>Like</SkyLink>
|
||||
<SkyLink>Read more</SkyLink>
|
||||
</div>
|
||||
</template>
|
||||
</SkyCard>
|
||||
|
||||
<SkyCard>
|
||||
<div class="cards-demo__image cards-demo__image--people">
|
||||
Lorem Ipsum
|
||||
</div>
|
||||
<div class="cards-demo__date">Posted on January 21, 2021</div>
|
||||
<p class="cards-demo__copy">
|
||||
Quisque eget vestibulum nulla. Quisque quis dui quis ex ultricies
|
||||
efficitur vitae non felis. Phasellus quis nibh hendrerit...
|
||||
</p>
|
||||
<template #footer>
|
||||
<div class="cards-demo__actions">
|
||||
<SkyLink>Like</SkyLink>
|
||||
<SkyLink>Read more</SkyLink>
|
||||
</div>
|
||||
</template>
|
||||
</SkyCard>
|
||||
</div>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cards-demo__stack {
|
||||
display: grid;
|
||||
gap: var(--sky-space-4);
|
||||
}
|
||||
|
||||
.cards-demo__grid {
|
||||
display: grid;
|
||||
gap: var(--sky-space-4);
|
||||
}
|
||||
|
||||
.cards-demo__image {
|
||||
height: 192px;
|
||||
margin: calc(var(--sky-space-4) * -1) calc(var(--sky-space-4) * -1)
|
||||
var(--sky-space-4);
|
||||
padding: var(--sky-space-4);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
color: #ffffff;
|
||||
background-color: #314864;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cards-demo__image--mountains {
|
||||
background-image:
|
||||
linear-gradient(to top, rgba(7, 20, 34, 0.82), rgba(7, 20, 34, 0.06)),
|
||||
url('../assets/nature-1000x600-3.jpg');
|
||||
}
|
||||
|
||||
.cards-demo__image--people {
|
||||
background-image:
|
||||
linear-gradient(to top, rgba(24, 18, 32, 0.82), rgba(24, 18, 32, 0.05)),
|
||||
url('../assets/people-1000x600-6.jpg');
|
||||
}
|
||||
|
||||
.cards-demo__date {
|
||||
margin-bottom: var(--sky-space-3);
|
||||
color: var(--sky-muted);
|
||||
}
|
||||
|
||||
.cards-demo__copy {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cards-demo__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sky-space-2);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.cards-demo__grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyBlock,
|
||||
SkyBlockTitle,
|
||||
SkyCheckbox,
|
||||
SkyList,
|
||||
SkyListItem,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const checked1 = ref(false)
|
||||
const checked2 = ref(true)
|
||||
const group = ref(['Books'])
|
||||
const movies = ref(['Movie 1'])
|
||||
const media = ref(['Item 1'])
|
||||
|
||||
function toggleValue(collection: string[], value: string): string[] {
|
||||
return collection.includes(value)
|
||||
? collection.filter((item) => item !== value)
|
||||
: [...collection, value]
|
||||
}
|
||||
|
||||
function toggleGroupValue(value: string): void {
|
||||
group.value = toggleValue(group.value, value)
|
||||
}
|
||||
|
||||
function toggleMovie(value: string): void {
|
||||
movies.value = toggleValue(movies.value, value)
|
||||
}
|
||||
|
||||
function toggleMovies(): void {
|
||||
movies.value = movies.value.length === 2 ? [] : ['Movie 1', 'Movie 2']
|
||||
}
|
||||
|
||||
function toggleMediaValue(value: string): void {
|
||||
media.value = toggleValue(media.value, value)
|
||||
}
|
||||
|
||||
function handleCheckboxRowClick(event: MouseEvent, toggle: () => void): void {
|
||||
const target = event.target
|
||||
if (target instanceof Element && target.closest('.sky-checkbox')) return
|
||||
toggle()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage class="checkbox-demo" title="Checkbox">
|
||||
<SkyBlockTitle>Inline</SkyBlockTitle>
|
||||
<SkyBlock inset strong>
|
||||
<p class="checkbox-demo__paragraph">
|
||||
Lorem
|
||||
<SkyCheckbox
|
||||
class="checkbox-demo__inline"
|
||||
aria-label="First inline checkbox"
|
||||
:checked="checked1"
|
||||
name="checkbox-1"
|
||||
@change="checked1 = !checked1"
|
||||
/>
|
||||
ipsum dolor sit amet, consectetur adipisicing elit. Alias beatae illo
|
||||
nihil aut eius commodi sint eveniet aliquid eligendi
|
||||
<SkyCheckbox
|
||||
class="checkbox-demo__inline"
|
||||
aria-label="Second inline checkbox"
|
||||
:checked="checked2"
|
||||
name="checkbox-2"
|
||||
@change="checked2 = !checked2"
|
||||
/>
|
||||
ad delectus impedit tempore nemo, enim vel praesentium consequatur nulla
|
||||
mollitia!
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Checkbox Group</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyListItem
|
||||
v-for="item in ['Books', 'Movies', 'Food', 'Drinks']"
|
||||
:key="item"
|
||||
:title="item"
|
||||
@click="handleCheckboxRowClick($event, () => toggleGroupValue(item))"
|
||||
>
|
||||
<template #media>
|
||||
<SkyCheckbox
|
||||
:aria-label="item"
|
||||
component="div"
|
||||
:checked="group.includes(item)"
|
||||
name="demo-checkbox"
|
||||
@change="toggleGroupValue(item)"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Indeterminate State</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyListItem
|
||||
title="Movies"
|
||||
@click="handleCheckboxRowClick($event, toggleMovies)"
|
||||
>
|
||||
<template #media>
|
||||
<SkyCheckbox
|
||||
aria-label="Movies"
|
||||
component="div"
|
||||
:checked="movies.length === 2"
|
||||
:indeterminate="movies.length === 1"
|
||||
name="demo-movies-checkbox"
|
||||
@change="toggleMovies"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
<SkyListItem
|
||||
v-for="movie in ['Movie 1', 'Movie 2']"
|
||||
:key="movie"
|
||||
class="checkbox-demo__child"
|
||||
:title="movie"
|
||||
@click="handleCheckboxRowClick($event, () => toggleMovie(movie))"
|
||||
>
|
||||
<template #media>
|
||||
<SkyCheckbox
|
||||
:aria-label="movie"
|
||||
component="div"
|
||||
:checked="movies.includes(movie)"
|
||||
name="demo-movie-checkbox"
|
||||
:value="movie"
|
||||
@change="toggleMovie(movie)"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>With Media Lists</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyListItem
|
||||
after="17:14"
|
||||
subtitle="New messages from John Doe"
|
||||
text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sagittis tellus ut turpis condimentum, ut dignissim lacus tincidunt. Cras dolor metus, ultrices condimentum sodales sit amet, pharetra sodales eros. Phasellus vel felis tellus. Mauris rutrum ligula nec dapibus feugiat. In vel dui laoreet, commodo augue id, pulvinar lacus."
|
||||
title="Facebook"
|
||||
@click="
|
||||
handleCheckboxRowClick($event, () => toggleMediaValue('Item 1'))
|
||||
"
|
||||
>
|
||||
<template #media>
|
||||
<SkyCheckbox
|
||||
aria-label="Facebook"
|
||||
component="div"
|
||||
:checked="media.includes('Item 1')"
|
||||
name="demo-media-checkbox"
|
||||
@change="toggleMediaValue('Item 1')"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
<SkyListItem
|
||||
after="17:11"
|
||||
subtitle="John Doe (@_johndoe) mentioned you on Twitter!"
|
||||
text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sagittis tellus ut turpis condimentum, ut dignissim lacus tincidunt. Cras dolor metus, ultrices condimentum sodales sit amet, pharetra sodales eros. Phasellus vel felis tellus. Mauris rutrum ligula nec dapibus feugiat. In vel dui laoreet, commodo augue id, pulvinar lacus."
|
||||
title="John Doe (via Twitter)"
|
||||
@click="
|
||||
handleCheckboxRowClick($event, () => toggleMediaValue('Item 2'))
|
||||
"
|
||||
>
|
||||
<template #media>
|
||||
<SkyCheckbox
|
||||
aria-label="John Doe via Twitter"
|
||||
component="div"
|
||||
:checked="media.includes('Item 2')"
|
||||
name="demo-media-checkbox"
|
||||
@change="toggleMediaValue('Item 2')"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.checkbox-demo__paragraph {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.checkbox-demo__inline {
|
||||
min-width: 22px;
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
.checkbox-demo__inline :deep(.sky-checkbox__input) {
|
||||
width: var(--sky-touch-target);
|
||||
height: var(--sky-touch-target);
|
||||
inset: auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.checkbox-demo :deep(.sky-list-item__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-demo__child :deep(.sky-list-item__row) {
|
||||
padding-left: calc(var(--sky-safe-area-left) + 56px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(new URL('./ChipsDemo.vue', import.meta.url), 'utf8')
|
||||
|
||||
describe('ChipsDemo', () => {
|
||||
it('uses Konsta half-space chip rhythm instead of the generic demo gap', () => {
|
||||
expect(source).toContain('class="chips-demo__group"')
|
||||
expect(source).not.toContain('class="sky-ui-demo-row"')
|
||||
expect(source).toMatch(
|
||||
/\.chips-demo__group :deep\(\.sky-chip\)\s*\{[^}]*margin:\s*2px/s,
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps contact, delete, fill, and outline examples', () => {
|
||||
expect(source).toContain('<SkyBlockTitle>Contact Chips</SkyBlockTitle>')
|
||||
expect(source).toContain(
|
||||
'<SkyBlockTitle>Deletable Chips / Tags</SkyBlockTitle>',
|
||||
)
|
||||
expect(source).toContain('<SkyBlockTitle>Color Chips</SkyBlockTitle>')
|
||||
expect(source).toContain('delete-button')
|
||||
expect(source).toContain('outline')
|
||||
expect(source).toContain('selected')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,138 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { SkyBlock, SkyBlockTitle, SkyChip, SkyChipDeleteIcon } from '@/ui'
|
||||
|
||||
import adamAvatar from '../assets/people-100x100-7.jpg'
|
||||
import janeAvatar from '../assets/people-100x100-9.jpg'
|
||||
import johnAvatar from '../assets/people-100x100-3.jpg'
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const textChips = [
|
||||
'Example Chip',
|
||||
'Another Chip',
|
||||
'One More Chip',
|
||||
'Fourth Chip',
|
||||
'Last One',
|
||||
]
|
||||
const contacts = [
|
||||
{ avatar: janeAvatar, name: 'Jane Doe' },
|
||||
{ avatar: johnAvatar, name: 'John Doe' },
|
||||
{ avatar: adamAvatar, name: 'Adam Smith' },
|
||||
]
|
||||
const deletableChips = ref([
|
||||
{ avatar: '', id: 'example', name: 'Example Chip' },
|
||||
{ avatar: adamAvatar, id: 'adam', name: 'Adam Smith' },
|
||||
])
|
||||
const colorChips = [
|
||||
{ className: 'sky-ui-demo-color-red', name: 'Red Chip' },
|
||||
{ className: 'sky-ui-demo-color-green', name: 'Green Chip' },
|
||||
{ className: 'chips-demo__blue', name: 'Blue Chip' },
|
||||
{ className: 'sky-ui-demo-color-yellow', name: 'Yellow Chip' },
|
||||
{ className: 'chips-demo__pink', name: 'Pink Chip' },
|
||||
]
|
||||
|
||||
function deleteChip(id: string): void {
|
||||
deletableChips.value = deletableChips.value.filter((chip) => chip.id !== id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage class="chips-demo" title="Chips">
|
||||
<SkyBlockTitle>Chips With Text</SkyBlockTitle>
|
||||
<SkyBlock class="chips-demo__group" inset strong>
|
||||
<SkyChip v-for="chip in textChips" :key="chip" component="span">
|
||||
{{ chip }}
|
||||
</SkyChip>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Outline Chips</SkyBlockTitle>
|
||||
<SkyBlock class="chips-demo__group" inset strong>
|
||||
<SkyChip v-for="chip in textChips" :key="chip" component="span" outline>
|
||||
{{ chip }}
|
||||
</SkyChip>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Contact Chips</SkyBlockTitle>
|
||||
<SkyBlock class="chips-demo__group" inset strong>
|
||||
<SkyChip v-for="contact in contacts" :key="contact.name" component="span">
|
||||
<template #media>
|
||||
<img class="chips-demo__avatar" :src="contact.avatar" alt="" />
|
||||
</template>
|
||||
{{ contact.name }}
|
||||
</SkyChip>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Deletable Chips / Tags</SkyBlockTitle>
|
||||
<SkyBlock class="chips-demo__group" inset strong>
|
||||
<SkyChip
|
||||
v-for="chip in deletableChips"
|
||||
:key="chip.id"
|
||||
delete-button
|
||||
:delete-label="`Delete ${chip.name}`"
|
||||
@delete="deleteChip(chip.id)"
|
||||
>
|
||||
<template v-if="chip.avatar" #media>
|
||||
<img class="chips-demo__avatar" :src="chip.avatar" alt="" />
|
||||
</template>
|
||||
{{ chip.name }}
|
||||
<template #delete>
|
||||
<SkyChipDeleteIcon />
|
||||
</template>
|
||||
</SkyChip>
|
||||
<span v-if="deletableChips.length === 0" class="sky-ui-demo-copy">
|
||||
All chips deleted.
|
||||
</span>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Color Chips</SkyBlockTitle>
|
||||
<SkyBlock class="chips-demo__group" inset strong>
|
||||
<SkyChip
|
||||
v-for="chip in colorChips"
|
||||
:key="`fill-${chip.name}`"
|
||||
:class="chip.className"
|
||||
component="span"
|
||||
selected
|
||||
>
|
||||
{{ chip.name }}
|
||||
</SkyChip>
|
||||
<SkyChip
|
||||
v-for="chip in colorChips"
|
||||
:key="`outline-${chip.name}`"
|
||||
:class="chip.className"
|
||||
component="span"
|
||||
outline
|
||||
>
|
||||
{{ chip.name }}
|
||||
</SkyChip>
|
||||
</SkyBlock>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chips-demo__group :deep(.sky-chip) {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.chips-demo__avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.chips-demo__pink {
|
||||
--sky-app-accent: #ff2d55;
|
||||
--sky-app-accent-soft: rgba(255, 45, 85, 0.16);
|
||||
--sky-chip-outline-border: #ff2d55;
|
||||
--sky-chip-outline-text: #ff2d55;
|
||||
}
|
||||
|
||||
.chips-demo__blue {
|
||||
--sky-app-accent: #007aff;
|
||||
--sky-app-accent-soft: rgba(0, 122, 255, 0.16);
|
||||
--sky-chip-outline-border: #007aff;
|
||||
--sky-chip-outline-text: #007aff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { SkyList, SkyListGroup, SkyListItem } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const contacts = [
|
||||
{
|
||||
letter: 'A',
|
||||
names: [
|
||||
'Aaron',
|
||||
'Abbie',
|
||||
'Adam',
|
||||
'Adele',
|
||||
'Agatha',
|
||||
'Agnes',
|
||||
'Albert',
|
||||
'Alexander',
|
||||
],
|
||||
},
|
||||
{
|
||||
letter: 'B',
|
||||
names: ['Bailey', 'Barclay', 'Bartolo', 'Bellamy', 'Belle', 'Benjamin'],
|
||||
},
|
||||
{
|
||||
letter: 'C',
|
||||
names: [
|
||||
'Caiden',
|
||||
'Calvin',
|
||||
'Candy',
|
||||
'Carl',
|
||||
'Cherilyn',
|
||||
'Chester',
|
||||
'Chloe',
|
||||
],
|
||||
},
|
||||
{ letter: 'V', names: ['Vladimir'] },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Contacts List">
|
||||
<SkyList class="contacts-list-demo__list" flush strong>
|
||||
<SkyListGroup
|
||||
v-for="group in contacts"
|
||||
:key="group.letter"
|
||||
:dividers="false"
|
||||
>
|
||||
<SkyListItem group-title :title="group.letter" />
|
||||
<SkyListItem v-for="name in group.names" :key="name" :title="name" />
|
||||
</SkyListGroup>
|
||||
</SkyList>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.contacts-list-demo__list :deep(.sky-list-item--group-title) {
|
||||
min-height: 32px;
|
||||
padding: 0 var(--sky-space-4);
|
||||
align-items: center;
|
||||
background: var(--sky-surface-muted);
|
||||
color: var(--sky-text);
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { SkyBlock, SkyBlockFooter, SkyBlockHeader, SkyBlockTitle } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Content Block">
|
||||
<SkyBlockTitle>Block Title</SkyBlockTitle>
|
||||
<SkyBlock>
|
||||
<p class="content-copy">
|
||||
Donec et nulla auctor massa pharetra adipiscing ut sit amet sem.
|
||||
Suspendisse molestie velit vitae mattis tincidunt. Ut sit amet quam
|
||||
mollis, vulputate turpis vel, sagittis felis.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Strong Block</SkyBlockTitle>
|
||||
<SkyBlock strong>
|
||||
<p class="content-copy">
|
||||
Donec et nulla auctor massa pharetra adipiscing ut sit amet sem.
|
||||
Suspendisse molestie velit vitae mattis tincidunt. Ut sit amet quam
|
||||
mollis, vulputate turpis vel, sagittis felis.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Strong Outline Block</SkyBlockTitle>
|
||||
<SkyBlock outline strong>
|
||||
<p class="content-copy">
|
||||
Donec et nulla auctor massa pharetra adipiscing ut sit amet sem.
|
||||
Suspendisse molestie velit vitae mattis tincidunt. Ut sit amet quam
|
||||
mollis, vulputate turpis vel, sagittis felis.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Strong Inset Block</SkyBlockTitle>
|
||||
<SkyBlock inset strong>
|
||||
<p class="content-copy">
|
||||
Donec et nulla auctor massa pharetra adipiscing ut sit amet sem.
|
||||
Suspendisse molestie velit vitae mattis tincidunt. Ut sit amet quam
|
||||
mollis, vulputate turpis vel, sagittis felis.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Strong Inset Outline Block</SkyBlockTitle>
|
||||
<SkyBlock inset outline strong>
|
||||
<p class="content-copy">
|
||||
Donec et nulla auctor massa pharetra adipiscing ut sit amet sem.
|
||||
Suspendisse molestie velit vitae mattis tincidunt. Ut sit amet quam
|
||||
mollis, vulputate turpis vel, sagittis felis.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>With Header & Footer</SkyBlockTitle>
|
||||
<SkyBlockHeader>Header</SkyBlockHeader>
|
||||
<SkyBlock outline strong>
|
||||
<p class="content-copy">
|
||||
Donec et nulla auctor massa pharetra adipiscing ut sit amet sem.
|
||||
Suspendisse molestie velit vitae mattis tincidunt. Ut sit amet quam
|
||||
mollis, vulputate turpis vel, sagittis felis.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
<SkyBlockFooter>Footer</SkyBlockFooter>
|
||||
|
||||
<SkyBlockTitle medium>Medium Title</SkyBlockTitle>
|
||||
<SkyBlock outline strong>
|
||||
<p class="content-copy">
|
||||
Donec et nulla auctor massa pharetra adipiscing ut sit amet sem.
|
||||
Suspendisse molestie velit vitae mattis tincidunt. Ut sit amet quam
|
||||
mollis, vulputate turpis vel, sagittis felis.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle large>Large Title</SkyBlockTitle>
|
||||
<SkyBlock outline strong>
|
||||
<p class="content-copy">
|
||||
Donec et nulla auctor massa pharetra adipiscing ut sit amet sem.
|
||||
Suspendisse molestie velit vitae mattis tincidunt. Ut sit amet quam
|
||||
mollis, vulputate turpis vel, sagittis felis.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.content-copy {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
SkyBlock,
|
||||
SkyBlockTitle,
|
||||
SkyCard,
|
||||
SkyTable,
|
||||
SkyTableBody,
|
||||
SkyTableCell,
|
||||
SkyTableHead,
|
||||
SkyTableRow,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const desserts = [
|
||||
{
|
||||
calories: 159,
|
||||
carbs: 24,
|
||||
fat: '6.0',
|
||||
name: 'Frozen yogurt',
|
||||
protein: '4.0',
|
||||
},
|
||||
{
|
||||
calories: 237,
|
||||
carbs: 37,
|
||||
fat: '9.0',
|
||||
name: 'Ice cream sandwich',
|
||||
protein: '4.4',
|
||||
},
|
||||
{ calories: 262, carbs: 24, fat: '16.0', name: 'Eclair', protein: '6.0' },
|
||||
{ calories: 305, carbs: 67, fat: '3.7', name: 'Cupcake', protein: '4.3' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Data Table">
|
||||
<SkyBlockTitle>Plain table</SkyBlockTitle>
|
||||
<SkyBlock class="data-table-demo__scroll">
|
||||
<SkyTable aria-label="Dessert nutrition plain table">
|
||||
<SkyTableHead>
|
||||
<SkyTableRow header>
|
||||
<SkyTableCell header nowrap scope="col">
|
||||
Dessert (100g serving)
|
||||
</SkyTableCell>
|
||||
<SkyTableCell align="end" header nowrap scope="col">
|
||||
Calories
|
||||
</SkyTableCell>
|
||||
<SkyTableCell align="end" header nowrap scope="col">
|
||||
Fat (g)
|
||||
</SkyTableCell>
|
||||
<SkyTableCell align="end" header nowrap scope="col">
|
||||
Carbs
|
||||
</SkyTableCell>
|
||||
<SkyTableCell align="end" header nowrap scope="col">
|
||||
Protein (g)
|
||||
</SkyTableCell>
|
||||
</SkyTableRow>
|
||||
</SkyTableHead>
|
||||
<SkyTableBody>
|
||||
<SkyTableRow v-for="dessert in desserts" :key="dessert.name">
|
||||
<SkyTableCell nowrap>{{ dessert.name }}</SkyTableCell>
|
||||
<SkyTableCell numeric>{{ dessert.calories }}</SkyTableCell>
|
||||
<SkyTableCell numeric>{{ dessert.fat }}</SkyTableCell>
|
||||
<SkyTableCell numeric>{{ dessert.carbs }}</SkyTableCell>
|
||||
<SkyTableCell numeric>{{ dessert.protein }}</SkyTableCell>
|
||||
</SkyTableRow>
|
||||
</SkyTableBody>
|
||||
</SkyTable>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Within card</SkyBlockTitle>
|
||||
<SkyCard class="data-table-demo__scroll" :content-wrap="false">
|
||||
<SkyTable aria-label="Dessert nutrition card table">
|
||||
<SkyTableHead>
|
||||
<SkyTableRow header>
|
||||
<SkyTableCell header nowrap scope="col">
|
||||
Dessert (100g serving)
|
||||
</SkyTableCell>
|
||||
<SkyTableCell align="end" header nowrap scope="col">
|
||||
Calories
|
||||
</SkyTableCell>
|
||||
<SkyTableCell align="end" header nowrap scope="col">
|
||||
Fat (g)
|
||||
</SkyTableCell>
|
||||
<SkyTableCell align="end" header nowrap scope="col">
|
||||
Carbs
|
||||
</SkyTableCell>
|
||||
<SkyTableCell align="end" header nowrap scope="col">
|
||||
Protein (g)
|
||||
</SkyTableCell>
|
||||
</SkyTableRow>
|
||||
</SkyTableHead>
|
||||
<SkyTableBody>
|
||||
<SkyTableRow v-for="dessert in desserts" :key="dessert.name">
|
||||
<SkyTableCell nowrap>{{ dessert.name }}</SkyTableCell>
|
||||
<SkyTableCell numeric>{{ dessert.calories }}</SkyTableCell>
|
||||
<SkyTableCell numeric>{{ dessert.fat }}</SkyTableCell>
|
||||
<SkyTableCell numeric>{{ dessert.carbs }}</SkyTableCell>
|
||||
<SkyTableCell numeric>{{ dessert.protein }}</SkyTableCell>
|
||||
</SkyTableRow>
|
||||
</SkyTableBody>
|
||||
</SkyTable>
|
||||
</SkyCard>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.data-table-demo__scroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.data-table-demo__scroll :deep(.sky-table) {
|
||||
min-width: 520px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,145 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyBlock,
|
||||
SkyButton,
|
||||
SkyDialog,
|
||||
SkyDialogButton,
|
||||
SkyList,
|
||||
SkyListItem,
|
||||
SkyRadio,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const basicOpened = ref(false)
|
||||
const alertOpened = ref(false)
|
||||
const confirmOpened = ref(false)
|
||||
const listOpened = ref(false)
|
||||
const radioValue = ref('batman')
|
||||
|
||||
function handleHeroRowClick(event: MouseEvent, value: string): void {
|
||||
const target = event.target
|
||||
if (target instanceof Element && target.closest('.sky-radio')) return
|
||||
radioValue.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Dialog">
|
||||
<SkyBlock inset strong>
|
||||
Dialog is a type of modal window that appears in front of app content to
|
||||
provide critical information, or prompt for a decision to be made.
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlock class="dialog-demo__buttons" inset strong>
|
||||
<SkyButton rounded @click="basicOpened = true">Basic</SkyButton>
|
||||
<SkyButton rounded @click="alertOpened = true">Alert</SkyButton>
|
||||
<SkyButton rounded @click="confirmOpened = true">Confirm</SkyButton>
|
||||
<SkyButton rounded @click="listOpened = true">List</SkyButton>
|
||||
</SkyBlock>
|
||||
|
||||
<template #fixed>
|
||||
<SkyDialog
|
||||
:opened="basicOpened"
|
||||
@backdropclick="basicOpened = false"
|
||||
@escape="basicOpened = false"
|
||||
>
|
||||
<template #title>Dialog Title</template>
|
||||
Dialog is a type of modal window that appears in front of app content to
|
||||
provide critical information, or prompt for a decision to be made.
|
||||
<template #buttons>
|
||||
<SkyDialogButton @click="basicOpened = false">
|
||||
Action 2
|
||||
</SkyDialogButton>
|
||||
<SkyDialogButton strong @click="basicOpened = false">
|
||||
Action 1
|
||||
</SkyDialogButton>
|
||||
</template>
|
||||
</SkyDialog>
|
||||
|
||||
<SkyDialog
|
||||
:opened="alertOpened"
|
||||
role="alertdialog"
|
||||
@backdropclick="alertOpened = false"
|
||||
@escape="alertOpened = false"
|
||||
>
|
||||
<template #title>Konsta UI</template>
|
||||
Hello world!
|
||||
<template #buttons>
|
||||
<SkyDialogButton strong @click="alertOpened = false">
|
||||
Ok
|
||||
</SkyDialogButton>
|
||||
</template>
|
||||
</SkyDialog>
|
||||
|
||||
<SkyDialog
|
||||
:opened="confirmOpened"
|
||||
role="alertdialog"
|
||||
@backdropclick="confirmOpened = false"
|
||||
@escape="confirmOpened = false"
|
||||
>
|
||||
<template #title>Konsta UI</template>
|
||||
All good today?
|
||||
<template #buttons>
|
||||
<SkyDialogButton @click="confirmOpened = false">No</SkyDialogButton>
|
||||
<SkyDialogButton strong @click="confirmOpened = false">
|
||||
Yes
|
||||
</SkyDialogButton>
|
||||
</template>
|
||||
</SkyDialog>
|
||||
|
||||
<SkyDialog
|
||||
:opened="listOpened"
|
||||
@backdropclick="listOpened = false"
|
||||
@escape="listOpened = false"
|
||||
>
|
||||
<template #title>Your super hero</template>
|
||||
<SkyList class="dialog-demo__list" nested>
|
||||
<SkyListItem
|
||||
v-for="hero in [
|
||||
{ label: 'Batman', value: 'batman' },
|
||||
{ label: 'Spider-man', value: 'spiderman' },
|
||||
{ label: 'Hulk', value: 'hulk' },
|
||||
]"
|
||||
:key="hero.value"
|
||||
:title="hero.label"
|
||||
@click="handleHeroRowClick($event, hero.value)"
|
||||
>
|
||||
<template #after>
|
||||
<SkyRadio
|
||||
v-model="radioValue"
|
||||
:aria-label="hero.label"
|
||||
name="super-hero"
|
||||
:value="hero.value"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
<template #buttons>
|
||||
<SkyDialogButton strong @click="listOpened = false">
|
||||
Confirm
|
||||
</SkyDialogButton>
|
||||
</template>
|
||||
</SkyDialog>
|
||||
</template>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dialog-demo__buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--sky-space-3);
|
||||
}
|
||||
|
||||
.dialog-demo__list {
|
||||
margin: var(--sky-space-3) calc(var(--sky-space-4) * -1)
|
||||
calc(var(--sky-space-4) * -1);
|
||||
}
|
||||
|
||||
.dialog-demo__list :deep(.sky-list-item__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,183 @@
|
||||
<script setup lang="ts">
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
|
||||
import { SkyBlock, SkyFab } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="FAB">
|
||||
<SkyBlock class="fab-demo__copy" inset strong>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quia, quo rem
|
||||
beatae, delectus eligendi est saepe molestias perferendis suscipit,
|
||||
commodi labore ipsa non quasi eum magnam neque ducimus! Quasi, numquam.
|
||||
</p>
|
||||
<p>
|
||||
Maiores culpa, itaque! Eaque natus ab cum ipsam numquam blanditiis a,
|
||||
quia, molestiae aut laudantium recusandae ipsa. Ad iste ex asperiores
|
||||
ipsa, mollitia perferendis consectetur quam eaque, voluptate laboriosam
|
||||
unde.
|
||||
</p>
|
||||
<p>
|
||||
Sed odit quis aperiam temporibus vitae necessitatibus, laboriosam,
|
||||
exercitationem dolores odio sapiente provident. Accusantium id, itaque
|
||||
aliquam libero ipsum eos fugiat distinctio laboriosam exercitationem
|
||||
sequi facere quas quidem magnam reprehenderit.
|
||||
</p>
|
||||
<p>
|
||||
Pariatur corporis illo, amet doloremque. Ab veritatis sunt nisi
|
||||
consectetur error modi, nam illo et nostrum quia aliquam ipsam vitae
|
||||
facere voluptates atque similique odit mollitia, rerum placeat nobis
|
||||
est.
|
||||
</p>
|
||||
<p>
|
||||
Et impedit soluta minus a autem adipisci cupiditate eius dignissimos
|
||||
nihil officia dolore voluptatibus aperiam reprehenderit esse facilis
|
||||
labore qui, officiis consectetur. Ipsa obcaecati aspernatur odio
|
||||
assumenda veniam, ipsum alias.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlock class="fab-demo__copy" inset strong>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Culpa ipsa
|
||||
debitis sed nihil eaque dolore cum iste quibusdam, accusamus doloribus,
|
||||
tempora quia quos voluptatibus corporis officia at quas dolorem earum!
|
||||
</p>
|
||||
<p>
|
||||
Quod soluta eos inventore magnam suscipit enim at hic in maiores
|
||||
temporibus pariatur tempora minima blanditiis vero autem est
|
||||
perspiciatis totam dolorum, itaque repellat? Nobis necessitatibus aut
|
||||
odit aliquam adipisci.
|
||||
</p>
|
||||
<p>
|
||||
Tenetur delectus perspiciatis ex numquam, unde corrupti velit! Quam
|
||||
aperiam, animi fuga veritatis consectetur, voluptatibus atque
|
||||
consequuntur dignissimos itaque, sint impedit cum cumque at. Adipisci
|
||||
sint, iusto blanditiis ullam? Vel?
|
||||
</p>
|
||||
<p>
|
||||
Dignissimos velit officia quibusdam! Eveniet beatae, aut, omnis
|
||||
temporibus consequatur expedita eaque aliquid quos accusamus fugiat id
|
||||
iusto autem obcaecati repellat fugit cupiditate suscipit natus quas
|
||||
doloribus? Temporibus necessitatibus, libero.
|
||||
</p>
|
||||
<p>
|
||||
Architecto quisquam ipsa fugit facere, repudiandae asperiores vitae
|
||||
obcaecati possimus, labore excepturi reprehenderit consectetur
|
||||
perferendis, ullam quidem hic, repellat fugiat eaque fuga. Consectetur
|
||||
in eveniet, deleniti recusandae omnis eum quas?
|
||||
</p>
|
||||
<p>
|
||||
Quos nulla consequatur quo, officia quaerat. Nulla voluptatum, assumenda
|
||||
quibusdam, placeat cum aut illo deleniti dolores commodi odio ipsam,
|
||||
recusandae est pariatur veniam repudiandae blanditiis. Voluptas unde
|
||||
deleniti quisquam, nobis?
|
||||
</p>
|
||||
<p>
|
||||
Atque qui quaerat quasi officia molestiae, molestias totam incidunt
|
||||
reprehenderit laboriosam facilis veritatis, non iusto! Dolore ipsam
|
||||
obcaecati voluptates minima maxime minus qui mollitia facere. Nostrum
|
||||
esse recusandae voluptatibus eligendi.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<template #fixed>
|
||||
<SkyFab
|
||||
aria-label="Add from the right bottom"
|
||||
class="fab-demo__button fab-demo__button--right-bottom"
|
||||
>
|
||||
<template #icon><Plus /></template>
|
||||
</SkyFab>
|
||||
<SkyFab
|
||||
aria-label="Add from the left bottom"
|
||||
class="fab-demo__button fab-demo__button--left-bottom sky-ui-demo-color-green"
|
||||
>
|
||||
<template #icon><Plus /></template>
|
||||
</SkyFab>
|
||||
<SkyFab
|
||||
aria-label="Add from the top left"
|
||||
class="fab-demo__button fab-demo__button--left-top sky-ui-demo-color-yellow"
|
||||
>
|
||||
<template #icon><Plus /></template>
|
||||
</SkyFab>
|
||||
<SkyFab
|
||||
aria-label="Add from the center"
|
||||
class="fab-demo__button fab-demo__button--center"
|
||||
>
|
||||
<template #icon><Plus /></template>
|
||||
</SkyFab>
|
||||
<SkyFab
|
||||
aria-label="Create"
|
||||
class="fab-demo__button fab-demo__button--center-bottom"
|
||||
text="Create"
|
||||
text-position="after"
|
||||
>
|
||||
<template #icon><Plus /></template>
|
||||
</SkyFab>
|
||||
</template>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fab-demo__copy {
|
||||
display: grid;
|
||||
gap: var(--sky-space-4);
|
||||
}
|
||||
|
||||
.fab-demo__copy p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.fab-demo__button {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.fab-demo__button--right-bottom {
|
||||
right: calc(var(--sky-safe-area-right) + var(--sky-space-4));
|
||||
bottom: calc(var(--sky-safe-area-bottom) + var(--sky-space-4));
|
||||
}
|
||||
|
||||
.fab-demo__button--left-bottom {
|
||||
bottom: calc(var(--sky-safe-area-bottom) + var(--sky-space-4));
|
||||
left: calc(var(--sky-safe-area-left) + var(--sky-space-4));
|
||||
}
|
||||
|
||||
.fab-demo__button--left-top {
|
||||
top: calc(var(--sky-safe-area-top) + var(--sky-navbar-height) + 16px);
|
||||
left: calc(var(--sky-safe-area-left) + var(--sky-space-4));
|
||||
}
|
||||
|
||||
.fab-demo__button--center {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.fab-demo__button--center.sky-glass--touch-highlight {
|
||||
transform: translate(-50%, -50%) scale(var(--sky-glass-touch-scale, 1.25));
|
||||
}
|
||||
|
||||
.fab-demo__button--center-bottom {
|
||||
bottom: calc(var(--sky-safe-area-bottom) + var(--sky-space-4));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.fab-demo__button--center-bottom.sky-glass--touch-highlight {
|
||||
transform: translateX(-50%) scale(var(--sky-glass-touch-scale, 1.25));
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.fab-demo__button--center.sky-glass--touch-highlight {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.fab-demo__button--center-bottom.sky-glass--touch-highlight {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,299 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
import { SkyBlockTitle, SkyField, SkyList } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
import demoIcon from '../assets/demo-icon.png'
|
||||
|
||||
type DemoFieldType =
|
||||
| 'date'
|
||||
| 'datetime-local'
|
||||
| 'email'
|
||||
| 'password'
|
||||
| 'select'
|
||||
| 'tel'
|
||||
| 'text'
|
||||
| 'textarea'
|
||||
| 'url'
|
||||
|
||||
interface DemoField {
|
||||
key: string
|
||||
label: string
|
||||
options?: readonly { label: string; value: string }[]
|
||||
placeholder: string
|
||||
type: DemoFieldType
|
||||
}
|
||||
|
||||
const genderOptions = [
|
||||
{ label: 'Male', value: 'Male' },
|
||||
{ label: 'Female', value: 'Female' },
|
||||
] as const
|
||||
|
||||
const defaultFields: readonly DemoField[] = [
|
||||
{ key: 'name', label: 'Name', placeholder: 'Your name', type: 'text' },
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Password',
|
||||
placeholder: 'Your password',
|
||||
type: 'password',
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
label: 'E-mail',
|
||||
placeholder: 'Your e-mail',
|
||||
type: 'email',
|
||||
},
|
||||
{ key: 'url', label: 'URL', placeholder: 'URL', type: 'url' },
|
||||
{
|
||||
key: 'phone',
|
||||
label: 'Phone',
|
||||
placeholder: 'Your phone number',
|
||||
type: 'tel',
|
||||
},
|
||||
{
|
||||
key: 'gender',
|
||||
label: 'Gender',
|
||||
options: genderOptions,
|
||||
placeholder: 'Please choose...',
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
key: 'birthday',
|
||||
label: 'Birthday',
|
||||
placeholder: 'Please choose...',
|
||||
type: 'date',
|
||||
},
|
||||
{
|
||||
key: 'datetime',
|
||||
label: 'Date time',
|
||||
placeholder: 'Please choose...',
|
||||
type: 'datetime-local',
|
||||
},
|
||||
{
|
||||
key: 'textarea',
|
||||
label: 'Textarea',
|
||||
placeholder: 'Bio',
|
||||
type: 'textarea',
|
||||
},
|
||||
]
|
||||
|
||||
const floatingFields = defaultFields.slice(0, 5)
|
||||
const shortFields = defaultFields.slice(0, 4)
|
||||
|
||||
function initialValues(): Record<string, string> {
|
||||
return {
|
||||
birthday: '2014-04-30',
|
||||
datetime: '',
|
||||
email: '',
|
||||
gender: 'Male',
|
||||
name: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
textarea: '',
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
|
||||
const defaultValues = reactive(initialValues())
|
||||
const outlineValues = reactive(initialValues())
|
||||
const floatingValues = reactive(initialValues())
|
||||
const floatingOutlineValues = reactive(initialValues())
|
||||
const iconValues = reactive(initialValues())
|
||||
const labelValues = reactive(initialValues())
|
||||
const inputValues = reactive(initialValues())
|
||||
const infoValues = reactive(initialValues())
|
||||
const name = ref('')
|
||||
const nameChanged = ref(false)
|
||||
const demoValue = ref('')
|
||||
const nameError = computed(() =>
|
||||
nameChanged.value && !name.value.trim() ? 'Please specify your name' : '',
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Form Inputs">
|
||||
<SkyBlockTitle>Default</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyField
|
||||
v-for="field in defaultFields"
|
||||
:key="field.key"
|
||||
v-model="defaultValues[field.key]"
|
||||
:input-style="
|
||||
field.type === 'textarea'
|
||||
? { height: '80px', resize: 'none' }
|
||||
: undefined
|
||||
"
|
||||
:label="field.label"
|
||||
:options="field.options"
|
||||
:placeholder="field.placeholder"
|
||||
:type="field.type"
|
||||
>
|
||||
<template #leading>
|
||||
<img class="form-inputs-demo__icon" :src="demoIcon" alt="" />
|
||||
</template>
|
||||
</SkyField>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Outline</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyField
|
||||
v-for="field in defaultFields"
|
||||
:key="field.key"
|
||||
v-model="outlineValues[field.key]"
|
||||
:input-style="
|
||||
field.type === 'textarea'
|
||||
? { height: '80px', resize: 'none' }
|
||||
: undefined
|
||||
"
|
||||
:label="field.label"
|
||||
:options="field.options"
|
||||
outline
|
||||
:placeholder="field.placeholder"
|
||||
:type="field.type"
|
||||
>
|
||||
<template #leading>
|
||||
<img class="form-inputs-demo__icon" :src="demoIcon" alt="" />
|
||||
</template>
|
||||
</SkyField>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Floating Labels</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyField
|
||||
v-for="field in floatingFields"
|
||||
:key="field.key"
|
||||
v-model="floatingValues[field.key]"
|
||||
floating-label
|
||||
:label="field.label"
|
||||
:placeholder="field.placeholder"
|
||||
:type="field.type"
|
||||
>
|
||||
<template #leading>
|
||||
<img class="form-inputs-demo__icon" :src="demoIcon" alt="" />
|
||||
</template>
|
||||
</SkyField>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Outline + Floating Labels</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyField
|
||||
v-for="field in floatingFields"
|
||||
:key="field.key"
|
||||
v-model="floatingOutlineValues[field.key]"
|
||||
floating-label
|
||||
:label="field.label"
|
||||
outline
|
||||
:placeholder="field.placeholder"
|
||||
:type="field.type"
|
||||
>
|
||||
<template #leading>
|
||||
<img class="form-inputs-demo__icon" :src="demoIcon" alt="" />
|
||||
</template>
|
||||
</SkyField>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Validation + Additional Info</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyField
|
||||
v-model="name"
|
||||
:error="nameError"
|
||||
help="Basic string checking"
|
||||
label="Name"
|
||||
placeholder="Your name"
|
||||
type="text"
|
||||
@input="nameChanged = true"
|
||||
>
|
||||
<template #leading>
|
||||
<img class="form-inputs-demo__icon" :src="demoIcon" alt="" />
|
||||
</template>
|
||||
</SkyField>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Clear Button</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyField
|
||||
v-model="demoValue"
|
||||
clear-button
|
||||
clear-label="Clear TV show"
|
||||
help="Type something to see clear button"
|
||||
label="TV Show"
|
||||
placeholder="Your favorite TV show"
|
||||
type="text"
|
||||
>
|
||||
<template #leading>
|
||||
<img class="form-inputs-demo__icon" :src="demoIcon" alt="" />
|
||||
</template>
|
||||
</SkyField>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Icon + Input</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyField
|
||||
v-for="field in shortFields"
|
||||
:key="field.key"
|
||||
v-model="iconValues[field.key]"
|
||||
:aria-label="field.label"
|
||||
:placeholder="field.placeholder"
|
||||
:type="field.type"
|
||||
>
|
||||
<template #leading>
|
||||
<img class="form-inputs-demo__icon" :src="demoIcon" alt="" />
|
||||
</template>
|
||||
</SkyField>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Label + Input</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyField
|
||||
v-for="field in shortFields"
|
||||
:key="field.key"
|
||||
v-model="labelValues[field.key]"
|
||||
:label="field.label"
|
||||
:placeholder="field.placeholder"
|
||||
:type="field.type"
|
||||
/>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Only Inputs</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyField
|
||||
v-for="field in shortFields"
|
||||
:key="field.key"
|
||||
v-model="inputValues[field.key]"
|
||||
:aria-label="field.label"
|
||||
:placeholder="field.placeholder"
|
||||
:type="field.type"
|
||||
/>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Inputs + Additional Info</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyField
|
||||
v-for="(field, index) in shortFields"
|
||||
:key="field.key"
|
||||
v-model="infoValues[field.key]"
|
||||
:aria-label="field.label"
|
||||
:help="
|
||||
[
|
||||
'Full name please',
|
||||
'8 characters minimum',
|
||||
'Your work e-mail address',
|
||||
'Your website URL',
|
||||
][index]
|
||||
"
|
||||
:placeholder="field.placeholder"
|
||||
:type="field.type"
|
||||
/>
|
||||
</SkyList>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.form-inputs-demo__icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { SkyList, SkyListButton } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="List Button">
|
||||
<SkyList dividers strong>
|
||||
<SkyListButton>Button 1</SkyListButton>
|
||||
<SkyListButton>Button 2</SkyListButton>
|
||||
<SkyListButton>Button 3</SkyListButton>
|
||||
</SkyList>
|
||||
<SkyList dividers inset strong>
|
||||
<SkyListButton>Button 1</SkyListButton>
|
||||
<SkyListButton>Button 2</SkyListButton>
|
||||
<SkyListButton>Button 3</SkyListButton>
|
||||
</SkyList>
|
||||
<SkyList inset strong>
|
||||
<SkyListButton variant="danger">Red Button</SkyListButton>
|
||||
</SkyList>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
@@ -0,0 +1,285 @@
|
||||
<script setup lang="ts">
|
||||
import { UserRound } from 'lucide-vue-next'
|
||||
|
||||
import {
|
||||
SkyBadge,
|
||||
SkyBlock,
|
||||
SkyBlockTitle,
|
||||
SkyIcon,
|
||||
SkyList,
|
||||
SkyListGroup,
|
||||
SkyListItem,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
import albumOne from '../assets/people-160x160-1.jpg'
|
||||
import albumTwo from '../assets/people-160x160-2.jpg'
|
||||
import albumThree from '../assets/people-160x160-3.jpg'
|
||||
|
||||
const people = ['Ivan Petrov', 'John Doe', 'Jenna Smith']
|
||||
const alphabetGroups = [
|
||||
{ letter: 'A', names: ['Aaron', 'Abbie', 'Adam'] },
|
||||
{ letter: 'B', names: ['Bailey', 'Barclay', 'Bartolo'] },
|
||||
{ letter: 'C', names: ['Caiden', 'Calvin', 'Candy'] },
|
||||
]
|
||||
const songs = [
|
||||
{
|
||||
artist: 'Beatles',
|
||||
image: albumOne,
|
||||
price: '$15',
|
||||
title: 'Yellow Submarine',
|
||||
},
|
||||
{
|
||||
artist: 'Queen',
|
||||
image: albumTwo,
|
||||
price: '$22',
|
||||
title: "Don't Stop Me Now",
|
||||
},
|
||||
{
|
||||
artist: 'Michael Jackson',
|
||||
image: albumThree,
|
||||
price: '$16',
|
||||
title: 'Billie Jean',
|
||||
},
|
||||
]
|
||||
const mediaText =
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sagittis tellus ut turpis condimentum, ut dignissim lacus tincidunt. Cras dolor metus, ultrices condimentum sodales sit amet, pharetra sodales eros. Phasellus vel felis tellus. Mauris rutrum ligula nec dapibus feugiat. In vel dui laoreet, commodo augue id, pulvinar lacus.'
|
||||
const mailItems = [
|
||||
{
|
||||
after: '17:14',
|
||||
subtitle: 'New messages from John Doe',
|
||||
title: 'Facebook',
|
||||
},
|
||||
{
|
||||
after: '17:11',
|
||||
subtitle: 'John Doe (@_johndoe) mentioned you on Twitter!',
|
||||
title: 'John Doe (via Twitter)',
|
||||
},
|
||||
{
|
||||
after: '16:48',
|
||||
subtitle: 'New messages from John Doe',
|
||||
title: 'Facebook',
|
||||
},
|
||||
{
|
||||
after: '15:32',
|
||||
subtitle: 'John Doe (@_johndoe) mentioned you on Twitter!',
|
||||
title: 'John Doe (via Twitter)',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="List">
|
||||
<SkyBlockTitle>Simple List</SkyBlockTitle>
|
||||
<SkyList dividers>
|
||||
<SkyListItem v-for="index in 3" :key="index" :title="`Item ${index}`" />
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Strong List</SkyBlockTitle>
|
||||
<SkyList dividers strong>
|
||||
<SkyListItem v-for="index in 3" :key="index" :title="`Item ${index}`" />
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Strong Outline List</SkyBlockTitle>
|
||||
<SkyList dividers outline strong>
|
||||
<SkyListItem v-for="index in 3" :key="index" :title="`Item ${index}`" />
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Strong Inset List</SkyBlockTitle>
|
||||
<SkyList dividers inset strong>
|
||||
<SkyListItem v-for="index in 3" :key="index" :title="`Item ${index}`" />
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Strong Outline Inset List</SkyBlockTitle>
|
||||
<SkyList dividers inset outline strong>
|
||||
<SkyListItem v-for="index in 3" :key="index" :title="`Item ${index}`" />
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Strong List with Dividers</SkyBlockTitle>
|
||||
<SkyList dividers strong>
|
||||
<SkyListItem v-for="index in 3" :key="index" :title="`Item ${index}`" />
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Simple Links List</SkyBlockTitle>
|
||||
<SkyList outline strong>
|
||||
<SkyListItem
|
||||
v-for="index in 3"
|
||||
:key="index"
|
||||
link
|
||||
:title="`Link ${index}`"
|
||||
/>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Data list, with icons</SkyBlockTitle>
|
||||
<SkyList outline strong>
|
||||
<SkyListItem
|
||||
v-for="(person, index) in people"
|
||||
:key="person"
|
||||
:after="index === 0 ? 'CEO' : undefined"
|
||||
:title="person"
|
||||
>
|
||||
<template #media>
|
||||
<span class="sky-ui-demo-list__person-icon">
|
||||
<SkyIcon :size="22"><UserRound /></SkyIcon>
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="index === 1" #after
|
||||
><SkyBadge tone="info">5</SkyBadge></template
|
||||
>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Links</SkyBlockTitle>
|
||||
<SkyList outline strong>
|
||||
<SkyListItem
|
||||
v-for="(person, index) in people"
|
||||
:key="person"
|
||||
:after="index === 0 ? 'CEO' : index === 1 ? 'Cleaner' : undefined"
|
||||
link
|
||||
:title="person"
|
||||
>
|
||||
<template #media>
|
||||
<span class="sky-ui-demo-list__person-icon">
|
||||
<SkyIcon :size="22"><UserRound /></SkyIcon>
|
||||
</span>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Links, Header, Footer</SkyBlockTitle>
|
||||
<SkyList outline strong>
|
||||
<SkyListItem after="Edit" header="Name" link title="John Doe">
|
||||
<template #media>
|
||||
<span class="sky-ui-demo-list__person-icon">
|
||||
<SkyIcon :size="22"><UserRound /></SkyIcon>
|
||||
</span>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
<SkyListItem after="Edit" header="Phone" link title="+7 90 111-22-3344">
|
||||
<template #media>
|
||||
<span class="sky-ui-demo-list__person-icon">
|
||||
<SkyIcon :size="22"><UserRound /></SkyIcon>
|
||||
</span>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
<SkyListItem
|
||||
after="Edit"
|
||||
footer="Home"
|
||||
header="Email"
|
||||
link
|
||||
title="john@doe"
|
||||
>
|
||||
<template #media>
|
||||
<span class="sky-ui-demo-list__person-icon">
|
||||
<SkyIcon :size="22"><UserRound /></SkyIcon>
|
||||
</span>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
<SkyListItem
|
||||
after="Edit"
|
||||
footer="Work"
|
||||
header="Email"
|
||||
link
|
||||
title="john@konsta"
|
||||
>
|
||||
<template #media>
|
||||
<span class="sky-ui-demo-list__person-icon">
|
||||
<SkyIcon :size="22"><UserRound /></SkyIcon>
|
||||
</span>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Links, no icons</SkyBlockTitle>
|
||||
<SkyList outline strong>
|
||||
<SkyListItem link title="Ivan Petrov" />
|
||||
<SkyListItem link title="John Doe" />
|
||||
<SkyListItem group-title title="Group title Here" />
|
||||
<SkyListItem link title="Ivan Petrov" />
|
||||
<SkyListItem link title="Jenna Smith" />
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Grouped with sticky titles</SkyBlockTitle>
|
||||
<SkyList outline strong>
|
||||
<SkyListGroup v-for="group in alphabetGroups" :key="group.letter">
|
||||
<SkyListItem
|
||||
class="sky-ui-demo-list__sticky-title"
|
||||
group-title
|
||||
:title="group.letter"
|
||||
/>
|
||||
<SkyListItem v-for="name in group.names" :key="name" :title="name" />
|
||||
</SkyListGroup>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle large>Media Lists</SkyBlockTitle>
|
||||
<SkyBlock>
|
||||
<p class="sky-ui-demo-copy">
|
||||
Media Lists are almost the same as Data Lists, but with a more flexible
|
||||
layout for visualization of more complex data, like products, services,
|
||||
users, and more.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Songs</SkyBlockTitle>
|
||||
<SkyList outline strong>
|
||||
<SkyListItem
|
||||
v-for="song in songs"
|
||||
:key="song.title"
|
||||
:after="song.price"
|
||||
:chevron="false"
|
||||
link
|
||||
:subtitle="song.artist"
|
||||
:text="mediaText"
|
||||
:title="song.title"
|
||||
>
|
||||
<template #media>
|
||||
<img class="sky-ui-demo-list__album" :src="song.image" alt="" />
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Mail App</SkyBlockTitle>
|
||||
<SkyList outline strong>
|
||||
<SkyListItem
|
||||
v-for="mail in mailItems"
|
||||
:key="`${mail.title}-${mail.after}`"
|
||||
:after="mail.after"
|
||||
:chevron="false"
|
||||
link
|
||||
:subtitle="mail.subtitle"
|
||||
:text="mediaText"
|
||||
:title="mail.title"
|
||||
/>
|
||||
</SkyList>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sky-ui-demo-list__person-icon,
|
||||
.sky-ui-demo-list__album {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
background: var(--sky-app-accent-soft);
|
||||
color: var(--sky-app-accent);
|
||||
}
|
||||
|
||||
.sky-ui-demo-list__person-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.sky-ui-demo-list__album {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.sky-ui-demo-list__sticky-title {
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { Grid2X2 } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { SkyBlock, SkyIcon, SkyMenuList, SkyMenuListItem } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const firstActive = ref('home')
|
||||
const secondActive = ref('home')
|
||||
const items = [
|
||||
{ id: 'home', subtitle: 'Home subtitle', title: 'Home' },
|
||||
{ id: 'profile', subtitle: 'Profile subtitle', title: 'Profile' },
|
||||
{ id: 'settings', subtitle: 'Settings subtitle', title: 'Settings' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Menu List">
|
||||
<SkyBlock inset strong>
|
||||
<p class="menu-list-copy">
|
||||
Menu list unlike usual links list is designed to indicate currently
|
||||
active screen (or section) of your app. Think about it like a Tabbar but
|
||||
in a form of a list.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyMenuList aria-label="Primary menu" outline strong>
|
||||
<SkyMenuListItem
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:active="firstActive === item.id"
|
||||
:title="item.title"
|
||||
@click="firstActive = item.id"
|
||||
>
|
||||
<template #media>
|
||||
<SkyIcon :size="24"><Grid2X2 /></SkyIcon>
|
||||
</template>
|
||||
</SkyMenuListItem>
|
||||
</SkyMenuList>
|
||||
|
||||
<SkyMenuList aria-label="Detailed menu" outline strong>
|
||||
<SkyMenuListItem
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:active="secondActive === item.id"
|
||||
:subtitle="item.subtitle"
|
||||
:title="item.title"
|
||||
@click="secondActive = item.id"
|
||||
>
|
||||
<template #media>
|
||||
<SkyIcon :size="24"><Grid2X2 /></SkyIcon>
|
||||
</template>
|
||||
</SkyMenuListItem>
|
||||
</SkyMenuList>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.menu-list-copy {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,208 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowUpCircle, Camera } from 'lucide-vue-next'
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyIcon,
|
||||
SkyLink,
|
||||
SkyMessage,
|
||||
SkyMessagebar,
|
||||
SkyMessages,
|
||||
SkyMessagesTitle,
|
||||
SkyToolbarPane,
|
||||
} from '@/ui'
|
||||
|
||||
import blueNinjaAvatar from '../assets/people-100x100-7.jpg'
|
||||
import kateAvatar from '../assets/people-100x100-9.jpg'
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
interface DemoMessage {
|
||||
avatar?: string
|
||||
name?: string
|
||||
text: string
|
||||
type: 'received' | 'sent'
|
||||
}
|
||||
|
||||
const messageText = ref('')
|
||||
const messagesEnd = ref<HTMLElement | null>(null)
|
||||
const messagesData = ref<DemoMessage[]>([
|
||||
{ text: 'Hi, Kate', type: 'sent' },
|
||||
{ text: 'How are you?', type: 'sent' },
|
||||
{
|
||||
avatar: kateAvatar,
|
||||
name: 'Kate',
|
||||
text: 'Hi, I am good!',
|
||||
type: 'received',
|
||||
},
|
||||
{
|
||||
avatar: blueNinjaAvatar,
|
||||
name: 'Blue Ninja',
|
||||
text: 'Hi there, I am also fine, thanks! And how are you?',
|
||||
type: 'received',
|
||||
},
|
||||
{ text: 'Hey, Blue Ninja! Glad to see you ;)', type: 'sent' },
|
||||
{
|
||||
text: 'How do you feel about going to the movies today?',
|
||||
type: 'sent',
|
||||
},
|
||||
{
|
||||
avatar: kateAvatar,
|
||||
name: 'Kate',
|
||||
text: 'Oh, great idea!',
|
||||
type: 'received',
|
||||
},
|
||||
{
|
||||
avatar: kateAvatar,
|
||||
name: 'Kate',
|
||||
text: 'What cinema are we going to?',
|
||||
type: 'received',
|
||||
},
|
||||
{
|
||||
avatar: blueNinjaAvatar,
|
||||
name: 'Blue Ninja',
|
||||
text: 'Great. And what movie?',
|
||||
type: 'received',
|
||||
},
|
||||
{
|
||||
avatar: blueNinjaAvatar,
|
||||
name: 'Blue Ninja',
|
||||
text: 'What time?',
|
||||
type: 'received',
|
||||
},
|
||||
])
|
||||
|
||||
const canSend = computed(() => messageText.value.trim().length > 0)
|
||||
const currentDate = new Date()
|
||||
const currentDay = new Intl.DateTimeFormat('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
weekday: 'long',
|
||||
}).format(currentDate)
|
||||
const currentTime = new Intl.DateTimeFormat('en-US', {
|
||||
hour: '2-digit',
|
||||
hour12: false,
|
||||
minute: '2-digit',
|
||||
}).format(currentDate)
|
||||
|
||||
function scrollToBottom(animate = true): void {
|
||||
messagesEnd.value?.scrollIntoView({
|
||||
behavior: animate ? 'smooth' : 'auto',
|
||||
block: 'end',
|
||||
})
|
||||
}
|
||||
|
||||
async function sendMessage(): Promise<void> {
|
||||
const text = messageText.value.trim()
|
||||
if (!text) return
|
||||
|
||||
messagesData.value.push({ text, type: 'sent' })
|
||||
messageText.value = ''
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
function handleMessageKeydown(event: KeyboardEvent): void {
|
||||
if (
|
||||
event.key !== 'Enter' ||
|
||||
event.shiftKey ||
|
||||
event.isComposing ||
|
||||
!canSend.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
void sendMessage()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
scrollToBottom(false)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Messages" with-tabbar>
|
||||
<SkyMessages>
|
||||
<SkyMessagesTitle
|
||||
><b>{{ currentDay }}</b
|
||||
>, {{ currentTime }}</SkyMessagesTitle
|
||||
>
|
||||
<SkyMessage
|
||||
v-for="(message, index) in messagesData"
|
||||
:key="`${index}-${message.text}`"
|
||||
:name="message.name"
|
||||
:text="message.text"
|
||||
:type="message.type"
|
||||
>
|
||||
<template v-if="message.type === 'received'" #avatar>
|
||||
<img
|
||||
class="sky-ui-demo-messages__avatar"
|
||||
:src="message.avatar"
|
||||
alt=""
|
||||
/>
|
||||
</template>
|
||||
</SkyMessage>
|
||||
<span
|
||||
ref="messagesEnd"
|
||||
class="sky-ui-demo-messages__end"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</SkyMessages>
|
||||
|
||||
<template #fixed>
|
||||
<SkyMessagebar
|
||||
v-model="messageText"
|
||||
aria-label="Message"
|
||||
class="sky-ui-demo-messages__bar"
|
||||
placeholder="Message"
|
||||
@keydown="handleMessageKeydown"
|
||||
>
|
||||
<template #left>
|
||||
<SkyToolbarPane>
|
||||
<SkyLink aria-label="Open camera" icon-only>
|
||||
<SkyIcon :size="22"><Camera /></SkyIcon>
|
||||
</SkyLink>
|
||||
</SkyToolbarPane>
|
||||
</template>
|
||||
<template #right>
|
||||
<SkyToolbarPane>
|
||||
<SkyLink
|
||||
aria-label="Send message"
|
||||
:disabled="!canSend"
|
||||
icon-only
|
||||
:style="{ opacity: canSend ? 1 : 0.3 }"
|
||||
@click="sendMessage"
|
||||
>
|
||||
<SkyIcon :size="28"><ArrowUpCircle /></SkyIcon>
|
||||
</SkyLink>
|
||||
</SkyToolbarPane>
|
||||
</template>
|
||||
</SkyMessagebar>
|
||||
</template>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sky-ui-demo-messages__avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.sky-ui-demo-messages__end {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sky-ui-demo-messages__bar {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyBlock,
|
||||
SkyBlockHeader,
|
||||
SkyBlockTitle,
|
||||
SkyLink,
|
||||
SkyList,
|
||||
SkyListItem,
|
||||
SkyRadio,
|
||||
SkyToggle,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
type NavbarSize = 'Default' | 'Large' | 'Medium'
|
||||
|
||||
const size = ref<NavbarSize>('Default')
|
||||
const isTransparent = ref(false)
|
||||
const navbarVariant = computed<'compact' | 'large' | 'medium'>(() => {
|
||||
if (size.value === 'Large') return 'large'
|
||||
if (size.value === 'Medium') return 'medium'
|
||||
return 'compact'
|
||||
})
|
||||
const sizeOptions: NavbarSize[] = ['Default', 'Medium', 'Large']
|
||||
const paragraphs = [
|
||||
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Iusto tempore ratione unde accusantium distinctio nulla quia numquam earum odio, optio, nisi rem deserunt. Molestiae delectus, ut assumenda numquam magni enim.',
|
||||
'Architecto molestias cum dolor dolorem provident consequuntur incidunt sunt fugiat tenetur odio, recusandae placeat rem veniam. Voluptates, repellendus odit, magni nesciunt, optio laborum asperiores repudiandae consectetur suscipit ab cupiditate eum.',
|
||||
'Aliquam, iste accusamus deleniti temporibus exercitationem neque perferendis optio, blanditiis quisquam molestias perspiciatis cumque harum tenetur veniam. Dolorum fugit doloribus est, deserunt, eligendi, quaerat quidem itaque tempore laborum non illum?',
|
||||
'Rerum magni sunt quis veniam, dolor ratione saepe ducimus tempore voluptatum porro quod commodi? Aperiam laudantium deleniti totam dolorum qui accusantium iste saepe facere optio, soluta maxime mollitia deserunt cumque.',
|
||||
'Iusto tempore quis provident, saepe illum ex ipsum cupiditate explicabo ratione unde facere nemo delectus harum, blanditiis eius sit asperiores nam. Aut cupiditate est tempore officia, perspiciatis esse asperiores repudiandae?',
|
||||
'Consequuntur itaque harum eos vero, reiciendis dolorum iure non earum molestias tenetur sint enim, maxime recusandae ad perferendis repudiandae! Sit, quos exercitationem beatae numquam laborum nobis natus. Obcaecati, ea inventore.',
|
||||
'Fugit culpa labore sapiente excepturi reiciendis, nulla, nihil neque ut veritatis quis quibusdam dolorum? Voluptatibus animi officia perspiciatis doloremque cum voluptatem, quia ratione modi vero, consequatur ipsum, praesentium quibusdam amet?',
|
||||
'Laudantium nihil sint nam placeat, nemo rerum ipsam explicabo iusto dolores molestiae expedita eos consequuntur ut architecto consequatur soluta ad maiores voluptatem tenetur in velit. Minima quia molestiae nobis voluptatibus.',
|
||||
'Expedita soluta quia inventore et placeat id exercitationem quisquam eligendi est eius sapiente quo, cum nesciunt mollitia, sit veniam ducimus tempora culpa adipisci commodi in autem nihil voluptatem corporis? Perspiciatis.',
|
||||
'Molestias, est? Eligendi vero distinctio voluptatem cumque id voluptatibus, officia minima repellendus sit illo tempora labore provident? Eum tenetur consectetur quae, in facilis autem ipsam doloribus voluptate vitae suscipit nobis.',
|
||||
'Obcaecati optio iste hic, soluta minus ullam, perferendis pariatur non possimus autem nostrum libero sapiente. Corporis quo cum iusto exercitationem velit. Non beatae eveniet asperiores ipsa consequuntur temporibus sapiente earum!',
|
||||
'Temporibus, omnis. Excepturi dolorum expedita laudantium quasi quod id adipisci, esse, nam atque in, incidunt ex ab distinctio repellendus beatae voluptatem alias odit illum quis. Illo numquam voluptatibus error voluptatum!',
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage
|
||||
:subtitle="'Subtitle'"
|
||||
title="Navbar"
|
||||
:transparent="isTransparent"
|
||||
:variant="navbarVariant"
|
||||
>
|
||||
<template #navbarRight><SkyLink>Right</SkyLink></template>
|
||||
|
||||
<SkyBlock inset strong>
|
||||
<p class="sky-ui-demo-copy">
|
||||
Navbar is a fixed area at the top of a screen that contains the page
|
||||
title and navigation elements.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Size</SkyBlockTitle>
|
||||
<SkyBlockHeader>
|
||||
Medium and Large will collapse to usual size on page scroll
|
||||
</SkyBlockHeader>
|
||||
<SkyList inset strong>
|
||||
<SkyListItem
|
||||
v-for="option in sizeOptions"
|
||||
:key="option"
|
||||
label
|
||||
:title="option"
|
||||
>
|
||||
<template #after>
|
||||
<SkyRadio v-model="size" name="navbar-size" :value="option" />
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Transparent</SkyBlockTitle>
|
||||
<SkyBlockHeader>
|
||||
When navbar is transparent, its title and background will become visible
|
||||
on page scroll
|
||||
</SkyBlockHeader>
|
||||
<SkyList inset strong>
|
||||
<SkyListItem title="Transparent">
|
||||
<template #after>
|
||||
<SkyToggle v-model="isTransparent" aria-label="Transparent navbar" />
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlock class="sky-ui-demo-stack" inset strong>
|
||||
<p
|
||||
v-for="paragraph in paragraphs"
|
||||
:key="paragraph"
|
||||
class="sky-ui-demo-copy"
|
||||
>
|
||||
{{ paragraph }}
|
||||
</p>
|
||||
</SkyBlock>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
@@ -0,0 +1,177 @@
|
||||
<script setup lang="ts">
|
||||
import { BellRing } from 'lucide-vue-next'
|
||||
import { onBeforeUnmount, reactive, ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyBlock,
|
||||
SkyButton,
|
||||
SkyDialog,
|
||||
SkyDialogButton,
|
||||
SkyIcon,
|
||||
SkyNotification,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
type NotificationId =
|
||||
| 'notificationCallbackOnClose'
|
||||
| 'notificationCloseOnClick'
|
||||
| 'notificationFull'
|
||||
| 'notificationWithButton'
|
||||
|
||||
const opened = reactive<Record<NotificationId, boolean>>({
|
||||
notificationCallbackOnClose: false,
|
||||
notificationCloseOnClick: false,
|
||||
notificationFull: false,
|
||||
notificationWithButton: false,
|
||||
})
|
||||
const alertOpened = ref(false)
|
||||
let autoCloseTimer: number | null = null
|
||||
|
||||
function clearAutoCloseTimer(): void {
|
||||
if (autoCloseTimer === null) return
|
||||
window.clearTimeout(autoCloseTimer)
|
||||
autoCloseTimer = null
|
||||
}
|
||||
|
||||
function closeAllNotifications(): void {
|
||||
opened.notificationCallbackOnClose = false
|
||||
opened.notificationCloseOnClick = false
|
||||
opened.notificationFull = false
|
||||
opened.notificationWithButton = false
|
||||
}
|
||||
|
||||
function openNotification(id: NotificationId): void {
|
||||
clearAutoCloseTimer()
|
||||
closeAllNotifications()
|
||||
opened[id] = true
|
||||
|
||||
if (id === 'notificationFull') {
|
||||
autoCloseTimer = window.setTimeout(() => {
|
||||
opened.notificationFull = false
|
||||
autoCloseTimer = null
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
function closeWithCallback(): void {
|
||||
opened.notificationCallbackOnClose = false
|
||||
alertOpened.value = true
|
||||
}
|
||||
|
||||
onBeforeUnmount(clearAutoCloseTimer)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Notification">
|
||||
<SkyBlock class="sky-ui-demo-stack" inset strong>
|
||||
<p class="sky-ui-demo-copy">
|
||||
Sky UI comes with a Notifications component that allows you to show
|
||||
useful messages to users and request basic actions.
|
||||
</p>
|
||||
<SkyButton rounded @click="openNotification('notificationFull')">
|
||||
Full layout notification
|
||||
</SkyButton>
|
||||
<SkyButton rounded @click="openNotification('notificationWithButton')">
|
||||
With Close Button
|
||||
</SkyButton>
|
||||
<SkyButton rounded @click="openNotification('notificationCloseOnClick')">
|
||||
Click to Close
|
||||
</SkyButton>
|
||||
<SkyButton
|
||||
rounded
|
||||
@click="openNotification('notificationCallbackOnClose')"
|
||||
>
|
||||
Callback on Close
|
||||
</SkyButton>
|
||||
</SkyBlock>
|
||||
|
||||
<template #fixed>
|
||||
<SkyNotification
|
||||
:opened="opened.notificationFull"
|
||||
role="alert"
|
||||
subtitle="This is a subtitle"
|
||||
text="This is a simple notification message"
|
||||
title="Sky UI"
|
||||
title-right-text="now"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="sky-ui-demo-notification__icon">
|
||||
<SkyIcon :size="24"><BellRing /></SkyIcon>
|
||||
</span>
|
||||
</template>
|
||||
</SkyNotification>
|
||||
|
||||
<SkyNotification
|
||||
close-label="Close notification"
|
||||
:opened="opened.notificationWithButton"
|
||||
subtitle="Notification with close button"
|
||||
text="Click (x) button to close me"
|
||||
title="Sky UI"
|
||||
@close="opened.notificationWithButton = false"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="sky-ui-demo-notification__icon">
|
||||
<SkyIcon :size="24"><BellRing /></SkyIcon>
|
||||
</span>
|
||||
</template>
|
||||
</SkyNotification>
|
||||
|
||||
<SkyNotification
|
||||
:opened="opened.notificationCloseOnClick"
|
||||
subtitle="Notification with close on click"
|
||||
text="Click me to close"
|
||||
title="Sky UI"
|
||||
title-right-text="now"
|
||||
@click="opened.notificationCloseOnClick = false"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="sky-ui-demo-notification__icon">
|
||||
<SkyIcon :size="24"><BellRing /></SkyIcon>
|
||||
</span>
|
||||
</template>
|
||||
</SkyNotification>
|
||||
|
||||
<SkyNotification
|
||||
:opened="opened.notificationCallbackOnClose"
|
||||
subtitle="Notification with close on click"
|
||||
text="Click me to close"
|
||||
title="Sky UI"
|
||||
title-right-text="now"
|
||||
@click="closeWithCallback"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="sky-ui-demo-notification__icon">
|
||||
<SkyIcon :size="24"><BellRing /></SkyIcon>
|
||||
</span>
|
||||
</template>
|
||||
</SkyNotification>
|
||||
|
||||
<SkyDialog
|
||||
content="Notification closed"
|
||||
:opened="alertOpened"
|
||||
title="Sky UI"
|
||||
@backdropclick="alertOpened = false"
|
||||
@escape="alertOpened = false"
|
||||
>
|
||||
<template #buttons>
|
||||
<SkyDialogButton strong @click="alertOpened = false"
|
||||
>Ok</SkyDialogButton
|
||||
>
|
||||
</template>
|
||||
</SkyDialog>
|
||||
</template>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sky-ui-demo-notification__icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 12px;
|
||||
background: var(--sky-app-accent-soft);
|
||||
color: var(--sky-app-accent);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyBlock,
|
||||
SkyButton,
|
||||
SkyLink,
|
||||
SkyList,
|
||||
SkyListItem,
|
||||
SkyPopover,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const popoverOpened = ref(false)
|
||||
const popoverTarget = ref<string | null>(null)
|
||||
|
||||
function openPopover(target: string): void {
|
||||
popoverTarget.value = target
|
||||
popoverOpened.value = true
|
||||
}
|
||||
|
||||
function closePopover(): void {
|
||||
popoverOpened.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Popover">
|
||||
<template #navbarRight>
|
||||
<SkyLink
|
||||
id="sky-ui-demo-popover-navbar"
|
||||
@click="openPopover('#sky-ui-demo-popover-navbar')"
|
||||
>
|
||||
Popover
|
||||
</SkyLink>
|
||||
</template>
|
||||
|
||||
<SkyBlock class="sky-ui-demo-stack" inset strong>
|
||||
<SkyButton
|
||||
id="sky-ui-demo-popover-button"
|
||||
rounded
|
||||
@click="openPopover('#sky-ui-demo-popover-button')"
|
||||
>
|
||||
Open popover on me
|
||||
</SkyButton>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlock class="sky-ui-demo-stack" inset strong>
|
||||
<p class="sky-ui-demo-copy">
|
||||
Mauris fermentum neque et luctus venenatis. Vivamus a sem rhoncus,
|
||||
ornare tellus eu, euismod mauris. In porta turpis at semper convallis.
|
||||
Duis adipiscing leo eu nulla lacinia, quis rhoncus metus condimentum.
|
||||
Etiam nec malesuada nibh. Maecenas quis lacinia nisl, vel posuere dolor.
|
||||
Vestibulum condimentum, nisl ac vulputate egestas, neque enim dignissim
|
||||
elit, rhoncus volutpat magna enim a est. Aenean sit amet ligula neque.
|
||||
Cras suscipit rutrum enim. Nam a odio facilisis, elementum tellus non,
|
||||
<SkyLink
|
||||
id="sky-ui-demo-popover-link-1"
|
||||
@click="openPopover('#sky-ui-demo-popover-link-1')"
|
||||
>popover</SkyLink
|
||||
>
|
||||
tortor. Pellentesque felis eros, dictum vitae lacinia quis, lobortis
|
||||
vitae ipsum. Cras vehicula bibendum lorem quis imperdiet.
|
||||
</p>
|
||||
<p class="sky-ui-demo-copy">
|
||||
In hac habitasse platea dictumst. Etiam varius, ante vel ornare
|
||||
facilisis, velit massa rutrum dolor, ac porta magna magna lacinia nunc.
|
||||
Curabitur
|
||||
<SkyLink
|
||||
id="sky-ui-demo-popover-link-2"
|
||||
@click="openPopover('#sky-ui-demo-popover-link-2')"
|
||||
>popover!</SkyLink
|
||||
>
|
||||
cursus laoreet. Aenean vel tempus augue. Pellentesque in imperdiet nibh.
|
||||
Mauris rhoncus nulla id sem suscipit volutpat. Pellentesque ac arcu in
|
||||
nisi viverra pulvinar. Nullam nulla orci, bibendum sed ligula non,
|
||||
ullamcorper iaculis mi. In hac habitasse platea dictumst. Praesent
|
||||
varius at nisl eu luctus. Cras aliquet porta est. Quisque elementum quis
|
||||
dui et consectetur. Cum sociis natoque penatibus et magnis dis
|
||||
parturient montes, nascetur ridiculus mus. Sed sed laoreet purus.
|
||||
Pellentesque eget ante ante.
|
||||
</p>
|
||||
<p class="sky-ui-demo-copy">
|
||||
Duis et ultricies nibh. Sed facilisis turpis urna, ac imperdiet erat
|
||||
venenatis eu. Proin sit amet faucibus tortor, et varius sem. Etiam vitae
|
||||
lacinia neque. Aliquam nisi purus, interdum in arcu sed, ultrices rutrum
|
||||
arcu. Nulla mi turpis, consectetur vel enim quis, facilisis viverra dui.
|
||||
Aliquam quis convallis tortor, quis semper ligula. Morbi ullamcorper
|
||||
<SkyLink
|
||||
id="sky-ui-demo-popover-link-3"
|
||||
@click="openPopover('#sky-ui-demo-popover-link-3')"
|
||||
>one more popover</SkyLink
|
||||
>
|
||||
massa at accumsan. Etiam purus odio, posuere in ligula vitae, viverra
|
||||
ultricies justo. Vestibulum nec interdum nisi. Aenean ac consectetur
|
||||
velit, non malesuada magna. Sed pharetra vehicula augue, vel venenatis
|
||||
lectus gravida eget. Curabitur lacus tellus, venenatis eu arcu in,
|
||||
interdum auctor nunc. Nunc non metus neque. Suspendisse viverra lectus
|
||||
sed risus aliquet, vel accumsan dolor feugiat.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<template #fixed>
|
||||
<SkyPopover
|
||||
angle
|
||||
aria-label="Popover menu"
|
||||
:opened="popoverOpened"
|
||||
role="dialog"
|
||||
:target="popoverTarget"
|
||||
@backdropclick="closePopover"
|
||||
@escape="closePopover"
|
||||
>
|
||||
<SkyList component="div" nested>
|
||||
<SkyListItem
|
||||
v-for="item in [
|
||||
'Item 1',
|
||||
'List Item 2',
|
||||
'Item 3',
|
||||
'List Item 4',
|
||||
'Item 5',
|
||||
]"
|
||||
:key="item"
|
||||
component="div"
|
||||
link
|
||||
:title="item"
|
||||
@click="closePopover"
|
||||
/>
|
||||
</SkyList>
|
||||
</SkyPopover>
|
||||
</template>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyBlock,
|
||||
SkyButton,
|
||||
SkyIcon,
|
||||
SkyLink,
|
||||
SkyNavbar,
|
||||
SkyPopup,
|
||||
SkyScrollArea,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const popupOpened = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Popup">
|
||||
<SkyBlock class="sky-ui-demo-stack" inset strong>
|
||||
<p class="sky-ui-demo-copy">
|
||||
Popup is a modal window with any HTML content that pops up over the
|
||||
app's main content. Like the other overlays, it is part of the temporary
|
||||
views.
|
||||
</p>
|
||||
<SkyButton rounded @click="popupOpened = true">Open Popup</SkyButton>
|
||||
</SkyBlock>
|
||||
|
||||
<template #fixed>
|
||||
<SkyPopup
|
||||
aria-label="Popup"
|
||||
class="sky-ui-demo-popup"
|
||||
:opened="popupOpened"
|
||||
@backdropclick="popupOpened = false"
|
||||
@escape="popupOpened = false"
|
||||
>
|
||||
<div class="sky-app-page sky-ui-demo-overlay-page">
|
||||
<SkyNavbar title="Popup">
|
||||
<template #right>
|
||||
<SkyLink
|
||||
aria-label="Close popup"
|
||||
icon-only
|
||||
@click="popupOpened = false"
|
||||
>
|
||||
<SkyIcon :size="24"><X /></SkyIcon>
|
||||
</SkyLink>
|
||||
</template>
|
||||
</SkyNavbar>
|
||||
<SkyScrollArea>
|
||||
<SkyBlock class="sky-ui-demo-stack" inset strong>
|
||||
<p class="sky-ui-demo-copy">
|
||||
Here comes popup. You can put anything here, even an independent
|
||||
view with its own navigation. By default a popup also adapts to
|
||||
its available device size; on a phone it is fullscreen.
|
||||
</p>
|
||||
<p class="sky-ui-demo-copy">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Suspendisse faucibus mauris leo, eu bibendum neque congue non.
|
||||
Ut leo mauris, eleifend eu commodo a, egestas ac urna. Maecenas
|
||||
in lacus faucibus, viverra ipsum pulvinar, molestie arcu. Etiam
|
||||
lacinia venenatis dignissim. Suspendisse non nisl semper tellus
|
||||
malesuada suscipit eu et eros. Nulla eu enim quis quam elementum
|
||||
vulputate. Mauris ornare consequat nunc viverra pellentesque.
|
||||
Aenean semper eu massa sit amet aliquam. Integer et neque sed
|
||||
libero mollis elementum at vitae ligula. Vestibulum pharetra sed
|
||||
libero sed porttitor. Suspendisse a faucibus lectus.
|
||||
</p>
|
||||
<p class="sky-ui-demo-copy">
|
||||
Duis ut mauris sollicitudin, venenatis nisi sed, luctus ligula.
|
||||
Phasellus blandit nisl ut lorem semper pharetra. Nullam tortor
|
||||
nibh, suscipit in consequat vel, feugiat sed quam. Nam risus
|
||||
libero, auctor vel tristique ac, malesuada ut ante. Sed
|
||||
molestie, est in eleifend sagittis, leo tortor ullamcorper erat,
|
||||
at vulputate eros sapien nec libero. Mauris dapibus laoreet nibh
|
||||
quis bibendum. Fusce dolor sem, suscipit in iaculis id, pharetra
|
||||
at urna. Pellentesque tempor congue massa quis faucibus.
|
||||
Vestibulum nunc eros, convallis blandit dui sit amet, gravida
|
||||
adipiscing libero.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
</SkyScrollArea>
|
||||
</div>
|
||||
</SkyPopup>
|
||||
</template>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sky-ui-demo-popup :deep(.sky-popup__panel) {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { SkyBlock, SkyBlockTitle, SkySpinner } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Preloader">
|
||||
<SkyBlockTitle>Default</SkyBlockTitle>
|
||||
<SkyBlock class="preloader-single" inset strong>
|
||||
<SkySpinner label="Loading" />
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Colors</SkyBlockTitle>
|
||||
<SkyBlock class="preloader-grid" inset strong>
|
||||
<SkySpinner class="sky-ui-demo-color-red" label="Red loader" />
|
||||
<SkySpinner class="sky-ui-demo-color-green" label="Green loader" />
|
||||
<SkySpinner class="sky-ui-demo-color-purple" label="Purple loader" />
|
||||
<SkySpinner class="sky-ui-demo-color-yellow" label="Yellow loader" />
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Sizes</SkyBlockTitle>
|
||||
<SkyBlock class="preloader-grid preloader-grid--sizes" outline strong>
|
||||
<SkySpinner label="Small loader" :size="16" />
|
||||
<SkySpinner label="Medium loader" :size="32" />
|
||||
<SkySpinner label="Large loader" :size="48" />
|
||||
<SkySpinner label="Extra large loader" :size="64" />
|
||||
</SkyBlock>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.preloader-single {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preloader-grid {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.preloader-grid--sizes {
|
||||
min-height: 96px;
|
||||
}
|
||||
|
||||
.preloader-grid :deep(.sky-ui-demo-color-red) {
|
||||
color: #ff3b30;
|
||||
}
|
||||
|
||||
.preloader-grid :deep(.sky-ui-demo-color-green) {
|
||||
color: #34c759;
|
||||
}
|
||||
|
||||
.preloader-grid :deep(.sky-ui-demo-color-purple) {
|
||||
color: #af52de;
|
||||
}
|
||||
|
||||
.preloader-grid :deep(.sky-ui-demo-color-yellow) {
|
||||
color: #ffcc00;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyBlock,
|
||||
SkyBlockTitle,
|
||||
SkyProgress,
|
||||
SkySegmented,
|
||||
SkySegmentedButton,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const progress = ref(0.1)
|
||||
const values = [0.1, 0.3, 0.5, 1]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Progressbar">
|
||||
<SkyBlockTitle>Determinate Progress Bar</SkyBlockTitle>
|
||||
<SkyBlock class="progress-examples" inset strong>
|
||||
<SkyProgress
|
||||
class="progress-examples__selected"
|
||||
label="Selected progress"
|
||||
:progress="progress"
|
||||
/>
|
||||
<SkySegmented
|
||||
:active-index="values.indexOf(progress)"
|
||||
:item-count="4"
|
||||
rounded
|
||||
strong
|
||||
>
|
||||
<SkySegmentedButton
|
||||
v-for="value in values"
|
||||
:key="value"
|
||||
:active="progress === value"
|
||||
@click="progress = value"
|
||||
>
|
||||
{{ value * 100 }}%
|
||||
</SkySegmentedButton>
|
||||
</SkySegmented>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Colors</SkyBlockTitle>
|
||||
<SkyBlock class="progress-colors" inset strong>
|
||||
<SkyProgress
|
||||
class="sky-ui-demo-color-red"
|
||||
label="Red progress"
|
||||
:progress="0.25"
|
||||
/>
|
||||
<SkyProgress
|
||||
class="sky-ui-demo-color-green"
|
||||
label="Green progress"
|
||||
:progress="0.5"
|
||||
/>
|
||||
<SkyProgress
|
||||
class="sky-ui-demo-color-yellow"
|
||||
label="Yellow progress"
|
||||
:progress="0.75"
|
||||
/>
|
||||
<SkyProgress
|
||||
class="sky-ui-demo-color-purple"
|
||||
label="Purple progress"
|
||||
:progress="1"
|
||||
/>
|
||||
</SkyBlock>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.progress-examples__selected {
|
||||
margin-block: var(--sky-space-4, 16px);
|
||||
}
|
||||
|
||||
.progress-colors {
|
||||
display: grid;
|
||||
gap: var(--sky-space-4, 16px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { SkyBlock, SkyBlockTitle, SkyList, SkyListItem, SkyRadio } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const inlineValue = ref('inline-1')
|
||||
const groupValue = ref('Books')
|
||||
const mediaValue = ref('Item 1')
|
||||
|
||||
const groupOptions = ['Books', 'Movies', 'Food', 'Drinks'] as const
|
||||
|
||||
function handleRadioRowClick(
|
||||
event: MouseEvent,
|
||||
value: string,
|
||||
group: 'group' | 'media',
|
||||
): void {
|
||||
const target = event.target
|
||||
if (target instanceof Element && target.closest('.sky-radio')) return
|
||||
|
||||
if (group === 'group') groupValue.value = value
|
||||
else mediaValue.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Radio">
|
||||
<SkyBlockTitle>Inline</SkyBlockTitle>
|
||||
<SkyBlock inset strong>
|
||||
<p class="radio-demo__copy">
|
||||
Lorem
|
||||
<SkyRadio
|
||||
v-model="inlineValue"
|
||||
aria-label="First inline option"
|
||||
name="demo-radio-inline"
|
||||
value="inline-1"
|
||||
/>
|
||||
ipsum dolor sit amet, consectetur adipisicing elit. Alias beatae illo
|
||||
nihil aut eius commodi sint eveniet aliquid eligendi
|
||||
<SkyRadio
|
||||
v-model="inlineValue"
|
||||
aria-label="Second inline option"
|
||||
name="demo-radio-inline"
|
||||
value="inline-2"
|
||||
/>
|
||||
ad delectus impedit tempore nemo, enim vel praesentium consequatur nulla
|
||||
mollitia!
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Radio Group</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyListItem
|
||||
v-for="item in groupOptions"
|
||||
:key="`leading-${item}`"
|
||||
class="radio-demo__row"
|
||||
:title="item"
|
||||
@click="handleRadioRowClick($event, item, 'group')"
|
||||
>
|
||||
<template #media>
|
||||
<SkyRadio
|
||||
v-model="groupValue"
|
||||
:aria-label="item"
|
||||
name="demo-radio-group"
|
||||
:value="item"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyList inset strong>
|
||||
<SkyListItem
|
||||
v-for="item in groupOptions"
|
||||
:key="`trailing-${item}`"
|
||||
class="radio-demo__row"
|
||||
:title="item"
|
||||
@click="handleRadioRowClick($event, item, 'group')"
|
||||
>
|
||||
<template #after>
|
||||
<SkyRadio
|
||||
v-model="groupValue"
|
||||
:aria-label="item"
|
||||
name="demo-radio-group"
|
||||
:value="item"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>With Media Lists</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyListItem
|
||||
after="17:14"
|
||||
class="radio-demo__row"
|
||||
subtitle="New messages from John Doe"
|
||||
text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sagittis tellus ut turpis condimentum, ut dignissim lacus tincidunt. Cras dolor metus, ultrices condimentum sodales sit amet, pharetra sodales eros. Phasellus vel felis tellus. Mauris rutrum ligula nec dapibus feugiat. In vel dui laoreet, commodo augue id, pulvinar lacus."
|
||||
title="Facebook"
|
||||
@click="handleRadioRowClick($event, 'Item 1', 'media')"
|
||||
>
|
||||
<template #media>
|
||||
<SkyRadio
|
||||
v-model="mediaValue"
|
||||
aria-label="Facebook"
|
||||
name="demo-radio-media"
|
||||
value="Item 1"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
<SkyListItem
|
||||
after="17:11"
|
||||
class="radio-demo__row"
|
||||
subtitle="John Doe (@_johndoe) mentioned you on Twitter!"
|
||||
text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sagittis tellus ut turpis condimentum, ut dignissim lacus tincidunt. Cras dolor metus, ultrices condimentum sodales sit amet, pharetra sodales eros. Phasellus vel felis tellus. Mauris rutrum ligula nec dapibus feugiat. In vel dui laoreet, commodo augue id, pulvinar lacus."
|
||||
title="John Doe (via Twitter)"
|
||||
@click="handleRadioRowClick($event, 'Item 2', 'media')"
|
||||
>
|
||||
<template #media>
|
||||
<SkyRadio
|
||||
v-model="mediaValue"
|
||||
aria-label="John Doe via Twitter"
|
||||
name="demo-radio-media"
|
||||
value="Item 2"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.radio-demo__copy {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.radio-demo__row :deep(.sky-list-item__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyBlockHeader,
|
||||
SkyBlockTitle,
|
||||
SkyList,
|
||||
SkyListItem,
|
||||
SkyRange,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const volume = ref(50)
|
||||
const price = ref(150)
|
||||
const red = ref(100)
|
||||
const green = ref(50)
|
||||
const blue = ref(75)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Range Slider">
|
||||
<SkyBlockTitle>Volume: {{ volume }}</SkyBlockTitle>
|
||||
<SkyBlockHeader>From 0 to 100 with step 10</SkyBlockHeader>
|
||||
<SkyList inset strong>
|
||||
<SkyListItem>
|
||||
<template #inner>
|
||||
<span class="range-slider-demo__row">
|
||||
<span>0</span>
|
||||
<SkyRange
|
||||
v-model="volume"
|
||||
aria-label="Volume"
|
||||
:max="100"
|
||||
:min="0"
|
||||
:step="10"
|
||||
/>
|
||||
<span>100</span>
|
||||
</span>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Price: ${{ price }}</SkyBlockTitle>
|
||||
<SkyBlockHeader>From 0 to 1000 with step 1</SkyBlockHeader>
|
||||
<SkyList inset strong>
|
||||
<SkyListItem>
|
||||
<template #inner>
|
||||
<span class="range-slider-demo__row">
|
||||
<span>$0</span>
|
||||
<SkyRange
|
||||
v-model="price"
|
||||
aria-label="Price"
|
||||
:max="1000"
|
||||
:min="0"
|
||||
:step="1"
|
||||
/>
|
||||
<span>$1000</span>
|
||||
</span>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>
|
||||
Color: rgb({{ red }}, {{ green }}, {{ blue }})
|
||||
</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyListItem>
|
||||
<template #inner>
|
||||
<SkyRange
|
||||
v-model="red"
|
||||
aria-label="Red"
|
||||
class="sky-ui-demo-color-red"
|
||||
:max="255"
|
||||
:min="0"
|
||||
:step="1"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
<SkyListItem>
|
||||
<template #inner>
|
||||
<SkyRange
|
||||
v-model="green"
|
||||
aria-label="Green"
|
||||
class="sky-ui-demo-color-green"
|
||||
:max="255"
|
||||
:min="0"
|
||||
:step="1"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
<SkyListItem>
|
||||
<template #inner>
|
||||
<SkyRange
|
||||
v-model="blue"
|
||||
aria-label="Blue"
|
||||
class="range-slider-demo__blue"
|
||||
:max="255"
|
||||
:min="0"
|
||||
:step="1"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.range-slider-demo__row {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sky-space-4);
|
||||
}
|
||||
|
||||
.range-slider-demo__row .sky-range {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.range-slider-demo__blue {
|
||||
--sky-app-accent: #007aff;
|
||||
--sky-app-accent-soft: rgba(0, 122, 255, 0.16);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { SkyList, SkyListItem, SkySearchbar } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const items = [
|
||||
'FC Ajax',
|
||||
'FC Arsenal',
|
||||
'FC Athletic',
|
||||
'FC Barcelona',
|
||||
'FC Bayern München',
|
||||
'FC Bordeaux',
|
||||
'FC Borussia Dortmund',
|
||||
'FC Chelsea',
|
||||
'FC Galatasaray',
|
||||
'FC Juventus',
|
||||
'FC Liverpool',
|
||||
'FC Manchester City',
|
||||
'FC Manchester United',
|
||||
'FC Paris Saint-Germain',
|
||||
'FC Real Madrid',
|
||||
'FC Tottenham Hotspur',
|
||||
'FC Valencia',
|
||||
'FC West Ham United',
|
||||
]
|
||||
|
||||
const searchQuery = ref('')
|
||||
const filteredItems = computed(() => {
|
||||
const query = searchQuery.value.trim().toLocaleLowerCase('en-US')
|
||||
return query
|
||||
? items.filter((item) => item.toLocaleLowerCase('en-US').includes(query))
|
||||
: items
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Searchbar">
|
||||
<template #subnavbar>
|
||||
<SkySearchbar
|
||||
v-model="searchQuery"
|
||||
class="sky-ui-demo-fill"
|
||||
clear-label="Clear search"
|
||||
disable-button
|
||||
disable-label="Cancel"
|
||||
label="Search football clubs"
|
||||
placeholder="Search"
|
||||
@disable="searchQuery = ''"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<SkyList inset strong>
|
||||
<SkyListItem v-if="filteredItems.length === 0" title="Nothing found" />
|
||||
<SkyListItem v-for="item in filteredItems" :key="item" :title="item" />
|
||||
</SkyList>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { SkyBlock, SkyBlockTitle, SkySegmented, SkySegmentedButton } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
interface SegmentedExample {
|
||||
outline: boolean
|
||||
raised: boolean
|
||||
strong: boolean
|
||||
title: string
|
||||
}
|
||||
|
||||
const activeSegmented = ref(1)
|
||||
const sections: readonly SegmentedExample[] = [
|
||||
{
|
||||
outline: false,
|
||||
raised: false,
|
||||
strong: false,
|
||||
title: 'Default Segmented',
|
||||
},
|
||||
{
|
||||
outline: false,
|
||||
raised: true,
|
||||
strong: false,
|
||||
title: 'Raised Segmented',
|
||||
},
|
||||
{ outline: true, raised: false, strong: false, title: 'Outline' },
|
||||
{
|
||||
outline: false,
|
||||
raised: false,
|
||||
strong: true,
|
||||
title: 'Strong Segmented',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Segmented Control">
|
||||
<template v-for="section in sections" :key="section.title">
|
||||
<SkyBlockTitle>{{ section.title }}</SkyBlockTitle>
|
||||
<SkyBlock class="segmented-control-demo__surface" inset strong>
|
||||
<SkySegmented
|
||||
:active-index="activeSegmented - 1"
|
||||
:aria-label="section.title"
|
||||
:item-count="3"
|
||||
:navigation="false"
|
||||
:outline="section.outline"
|
||||
:raised="section.raised"
|
||||
:rounded="false"
|
||||
:strong="section.strong"
|
||||
>
|
||||
<SkySegmentedButton
|
||||
v-for="index in 3"
|
||||
:key="index"
|
||||
:active="activeSegmented === index"
|
||||
@click="activeSegmented = index"
|
||||
>
|
||||
Button
|
||||
</SkySegmentedButton>
|
||||
</SkySegmented>
|
||||
<SkySegmented
|
||||
:active-index="activeSegmented - 1"
|
||||
:aria-label="`${section.title}, rounded`"
|
||||
:item-count="3"
|
||||
:navigation="false"
|
||||
:outline="section.outline"
|
||||
:raised="section.raised"
|
||||
:rounded="true"
|
||||
:strong="section.strong"
|
||||
>
|
||||
<SkySegmentedButton
|
||||
v-for="index in 3"
|
||||
:key="index"
|
||||
:active="activeSegmented === index"
|
||||
@click="activeSegmented = index"
|
||||
>
|
||||
Button
|
||||
</SkySegmentedButton>
|
||||
</SkySegmented>
|
||||
</SkyBlock>
|
||||
</template>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.segmented-control-demo__surface {
|
||||
display: grid;
|
||||
gap: var(--sky-space-4);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(
|
||||
new URL('./SheetModalDemo.vue', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
describe('SheetModalDemo', () => {
|
||||
it('matches the Konsta sheet content and close-control geometry', () => {
|
||||
expect(source).toMatch(
|
||||
/<SkyToolbar[\s\S]*?class="sky-ui-demo-sheet__toolbar"[\s\S]*?<SkyToolbarPane>/,
|
||||
)
|
||||
expect(source).toMatch(
|
||||
/\.sky-ui-demo-sheet__toolbar\s*\{[\s\S]*?justify-content: flex-end;/,
|
||||
)
|
||||
expect(source).toMatch(
|
||||
/\.sky-ui-demo-sheet__content\s*\{[\s\S]*?gap: var\(--sky-space-4\);[\s\S]*?padding-right: calc\([\s\S]*?var\(--sky-page-gutter\)[\s\S]*?var\(--sky-safe-area-right, 0px\)[\s\S]*?padding-left: calc\([\s\S]*?var\(--sky-page-gutter\)[\s\S]*?var\(--sky-safe-area-left, 0px\)/,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyBlock,
|
||||
SkyButton,
|
||||
SkyIcon,
|
||||
SkyLink,
|
||||
SkySheet,
|
||||
SkyToolbar,
|
||||
SkyToolbarPane,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const sheetOpened = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Sheet Modal">
|
||||
<SkyBlock class="sky-ui-demo-stack" inset strong>
|
||||
<p class="sky-ui-demo-copy">
|
||||
Sheet Modals slide up from the bottom of the screen to reveal more
|
||||
content. Such modals allow custom overlays with custom content.
|
||||
</p>
|
||||
<SkyButton rounded @click="sheetOpened = true">Open Sheet</SkyButton>
|
||||
</SkyBlock>
|
||||
|
||||
<template #fixed>
|
||||
<SkySheet
|
||||
aria-label="Sheet modal"
|
||||
class="sky-ui-demo-sheet"
|
||||
:opened="sheetOpened"
|
||||
@backdropclick="sheetOpened = false"
|
||||
@escape="sheetOpened = false"
|
||||
>
|
||||
<SkyToolbar
|
||||
aria-label="Sheet actions"
|
||||
class="sky-ui-demo-sheet__toolbar"
|
||||
top
|
||||
>
|
||||
<SkyToolbarPane>
|
||||
<SkyLink
|
||||
aria-label="Close sheet"
|
||||
icon-only
|
||||
@click="sheetOpened = false"
|
||||
>
|
||||
<SkyIcon :size="24"><X /></SkyIcon>
|
||||
</SkyLink>
|
||||
</SkyToolbarPane>
|
||||
</SkyToolbar>
|
||||
<SkyBlock class="sky-ui-demo-stack sky-ui-demo-sheet__content">
|
||||
<p class="sky-ui-demo-copy">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Harum ad
|
||||
excepturi nesciunt nobis aliquam. Quibusdam ducimus neque
|
||||
necessitatibus, molestias cupiditate velit nihil alias incidunt,
|
||||
excepturi voluptatem dolore itaque sapiente dolores!
|
||||
</p>
|
||||
<SkyButton block large rounded @click="sheetOpened = false"
|
||||
>Action</SkyButton
|
||||
>
|
||||
</SkyBlock>
|
||||
</SkySheet>
|
||||
</template>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sky-ui-demo-sheet__toolbar {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.sky-ui-demo-sheet__content {
|
||||
gap: var(--sky-space-4);
|
||||
padding-right: calc(var(--sky-page-gutter) + var(--sky-safe-area-right, 0px));
|
||||
padding-left: calc(var(--sky-page-gutter) + var(--sky-safe-area-left, 0px));
|
||||
}
|
||||
|
||||
.sky-ui-demo-sheet :deep(.sky-sheet__panel) {
|
||||
padding-bottom: var(--sky-safe-area-bottom);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
import {
|
||||
SkyBlock,
|
||||
SkyBlockTitle,
|
||||
SkyButton,
|
||||
SkyIcon,
|
||||
SkyLink,
|
||||
SkyNavbar,
|
||||
SkyPanel,
|
||||
SkyScrollArea,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
type PanelId = 'left' | 'leftFloating' | 'right' | 'rightFloating'
|
||||
|
||||
const panels = [
|
||||
{ floating: false, id: 'left', side: 'left', title: 'Left Panel' },
|
||||
{ floating: false, id: 'right', side: 'right', title: 'Right Panel' },
|
||||
{ floating: true, id: 'leftFloating', side: 'left', title: 'Left Panel' },
|
||||
{ floating: true, id: 'rightFloating', side: 'right', title: 'Right Panel' },
|
||||
] as const
|
||||
const opened = reactive<Record<PanelId, boolean>>({
|
||||
left: false,
|
||||
leftFloating: false,
|
||||
right: false,
|
||||
rightFloating: false,
|
||||
})
|
||||
|
||||
function openPanel(id: PanelId): void {
|
||||
opened[id] = true
|
||||
}
|
||||
|
||||
function closePanel(id: PanelId): void {
|
||||
opened[id] = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Panel / Side Panels">
|
||||
<SkyBlock class="sky-ui-demo-stack" inset strong>
|
||||
<p class="sky-ui-demo-copy">
|
||||
Sky UI comes with two panels, one on the left and one on the right. Both
|
||||
are optional and can contain lists, forms, custom content, or an
|
||||
independent view.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlock class="sky-ui-demo-panel-actions" inset strong>
|
||||
<SkyButton block rounded @click="openPanel('left')">Left Panel</SkyButton>
|
||||
<SkyButton block rounded @click="openPanel('right')"
|
||||
>Right Panel</SkyButton
|
||||
>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Floating Panels</SkyBlockTitle>
|
||||
<SkyBlock class="sky-ui-demo-panel-actions" inset strong>
|
||||
<SkyButton block rounded @click="openPanel('leftFloating')">
|
||||
Left Panel
|
||||
</SkyButton>
|
||||
<SkyButton block rounded @click="openPanel('rightFloating')">
|
||||
Right Panel
|
||||
</SkyButton>
|
||||
</SkyBlock>
|
||||
|
||||
<template #fixed>
|
||||
<SkyPanel
|
||||
v-for="panel in panels"
|
||||
:key="panel.id"
|
||||
:aria-label="panel.title"
|
||||
class="sky-ui-demo-panel"
|
||||
:floating="panel.floating"
|
||||
:opened="opened[panel.id]"
|
||||
:side="panel.side"
|
||||
@backdropclick="closePanel(panel.id)"
|
||||
@escape="closePanel(panel.id)"
|
||||
>
|
||||
<div class="sky-app-page sky-ui-demo-panel-page">
|
||||
<SkyNavbar :title="panel.title">
|
||||
<template #right>
|
||||
<SkyLink
|
||||
:aria-label="`Close ${panel.title.toLowerCase()}`"
|
||||
icon-only
|
||||
@click="closePanel(panel.id)"
|
||||
>
|
||||
<SkyIcon :size="24"><X /></SkyIcon>
|
||||
</SkyLink>
|
||||
</template>
|
||||
</SkyNavbar>
|
||||
<SkyScrollArea>
|
||||
<SkyBlock class="sky-ui-demo-stack">
|
||||
<p class="sky-ui-demo-copy">Here comes {{ panel.side }} panel.</p>
|
||||
<p v-if="panel.side === 'left'" class="sky-ui-demo-copy">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Suspendisse faucibus mauris leo, eu bibendum neque congue non.
|
||||
Ut leo mauris, eleifend eu commodo a, egestas ac urna. Maecenas
|
||||
in lacus faucibus, viverra ipsum pulvinar, molestie arcu. Etiam
|
||||
lacinia venenatis dignissim. Suspendisse non nisl semper tellus
|
||||
malesuada suscipit eu et eros. Nulla eu enim quis quam elementum
|
||||
vulputate. Mauris ornare consequat nunc viverra pellentesque.
|
||||
Aenean semper eu massa sit amet aliquam. Integer et neque sed
|
||||
libero mollis elementum at vitae ligula. Vestibulum pharetra sed
|
||||
libero sed porttitor. Suspendisse a faucibus lectus.
|
||||
</p>
|
||||
<p v-else class="sky-ui-demo-copy">
|
||||
Duis ut mauris sollicitudin, venenatis nisi sed, luctus ligula.
|
||||
Phasellus blandit nisl ut lorem semper pharetra. Nullam tortor
|
||||
nibh, suscipit in consequat vel, feugiat sed quam. Nam risus
|
||||
libero, auctor vel tristique ac, malesuada ut ante. Sed
|
||||
molestie, est in eleifend sagittis, leo tortor ullamcorper erat,
|
||||
at vulputate eros sapien nec libero. Mauris dapibus laoreet nibh
|
||||
quis bibendum. Fusce dolor sem, suscipit in iaculis id, pharetra
|
||||
at urna. Pellentesque tempor congue massa quis faucibus.
|
||||
Vestibulum nunc eros, convallis blandit dui sit amet, gravida
|
||||
adipiscing libero.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
</SkyScrollArea>
|
||||
</div>
|
||||
</SkyPanel>
|
||||
</template>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sky-ui-demo-panel-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--sky-space-3);
|
||||
}
|
||||
|
||||
.sky-ui-demo-panel-page {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.sky-ui-demo-panel :deep(.sky-panel__panel) {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,357 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyApp,
|
||||
SkyButton,
|
||||
SkyCard,
|
||||
SkyEmptyState,
|
||||
SkyGlass,
|
||||
SkyInfiniteLoader,
|
||||
SkyListCard,
|
||||
SkyListItem,
|
||||
SkyNavbarBackLink,
|
||||
SkyPillNavigation,
|
||||
SkyProvider,
|
||||
SkyScrollRail,
|
||||
SkySection,
|
||||
SkySegmented,
|
||||
SkySegmentedButton,
|
||||
SkySettingsGroup,
|
||||
SkySettingsIcon,
|
||||
SkySettingsRangeRow,
|
||||
SkySettingsRow,
|
||||
SkyStatusCard,
|
||||
SkySurface,
|
||||
SkyWidgetFrame,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
import { useSkyUiDemoContext } from '../context'
|
||||
|
||||
const range = ref(45)
|
||||
const notifications = ref(true)
|
||||
const selectedChoice = ref<'automatic' | 'manual'>('automatic')
|
||||
const fullTab = ref(0)
|
||||
const compactTab = ref(0)
|
||||
const splitTab = ref(0)
|
||||
const loaderState = ref<'error' | 'loading' | 'ready'>('loading')
|
||||
const demo = useSkyUiDemoContext()
|
||||
|
||||
const loaderError = computed(() =>
|
||||
loaderState.value === 'error' ? 'Could not load the next page.' : false,
|
||||
)
|
||||
|
||||
function retryLoading(): void {
|
||||
loaderState.value = 'loading'
|
||||
}
|
||||
|
||||
function requestLoading(): void {
|
||||
loaderState.value = 'loading'
|
||||
}
|
||||
|
||||
function resetExamples(): void {
|
||||
range.value = 45
|
||||
notifications.value = true
|
||||
selectedChoice.value = 'automatic'
|
||||
fullTab.value = 0
|
||||
compactTab.value = 0
|
||||
splitTab.value = 0
|
||||
loaderState.value = 'loading'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Sky Extensions">
|
||||
<SkySection title="Status and empty states">
|
||||
<div class="sky-ui-demo-stack">
|
||||
<SkyStatusCard
|
||||
indicator
|
||||
subtitle="Sky-only status primitive"
|
||||
title="Ready"
|
||||
tone="success"
|
||||
/>
|
||||
<SkyEmptyState
|
||||
body="No additional results are available."
|
||||
title="Nothing here"
|
||||
>
|
||||
<template #actions><SkyButton small>Action</SkyButton></template>
|
||||
</SkyEmptyState>
|
||||
<SkyStatusCard
|
||||
v-if="loaderState === 'error'"
|
||||
aria-live="polite"
|
||||
subtitle="Use Retry to return to the loading state."
|
||||
title="Additional results unavailable"
|
||||
tone="danger"
|
||||
/>
|
||||
<SkyInfiniteLoader
|
||||
:error="loaderError"
|
||||
:has-more="loaderState === 'ready'"
|
||||
:load-key="loaderState"
|
||||
:loading="loaderState === 'loading'"
|
||||
loading-label="Loading more results"
|
||||
retry-label="Retry loading results"
|
||||
@load="requestLoading"
|
||||
@retry="retryLoading"
|
||||
/>
|
||||
<div class="sky-ui-demo-row sky-ui-demo-row--center">
|
||||
<SkyButton small @click="loaderState = 'loading'">Loading</SkyButton>
|
||||
<SkyButton small variant="secondary" @click="loaderState = 'error'">
|
||||
Error
|
||||
</SkyButton>
|
||||
<SkyButton small variant="secondary" @click="loaderState = 'ready'">
|
||||
Ready
|
||||
</SkyButton>
|
||||
</div>
|
||||
</div>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="Cards and horizontal rail">
|
||||
<SkyListCard inset strong>
|
||||
<SkyListItem title="SkyListCard" subtitle="A list surface extension" />
|
||||
</SkyListCard>
|
||||
<SkyScrollRail label="Card examples">
|
||||
<SkyCard
|
||||
v-for="index in 4"
|
||||
:key="index"
|
||||
class="sky-ui-demo-extension-card"
|
||||
>
|
||||
Card {{ index }}
|
||||
</SkyCard>
|
||||
</SkyScrollRail>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="Surface and nested theme provider">
|
||||
<div class="sky-ui-demo-stack">
|
||||
<SkySurface class="sky-ui-demo-extension-surface" highlight>
|
||||
SkySurface
|
||||
</SkySurface>
|
||||
<SkyProvider
|
||||
:accent="demo.accent.value"
|
||||
:accent-soft="demo.accentSoft.value"
|
||||
:dark="demo.dark.value"
|
||||
:safe-areas="false"
|
||||
>
|
||||
<SkyApp
|
||||
class="sky-ui-demo-extension-app"
|
||||
:accent="demo.accent.value"
|
||||
:accent-soft="demo.accentSoft.value"
|
||||
:dark="demo.dark.value"
|
||||
:safe-areas="false"
|
||||
>
|
||||
Nested SkyApp follows the demo theme
|
||||
</SkyApp>
|
||||
</SkyProvider>
|
||||
</div>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="Widget sizes">
|
||||
<div class="sky-ui-demo-extension-widgets">
|
||||
<SkyWidgetFrame
|
||||
v-for="size in ['small', 'medium', 'large'] as const"
|
||||
:key="size"
|
||||
:label="`${size} widget`"
|
||||
:size="size"
|
||||
>
|
||||
<SkySurface class="sky-ui-demo-extension-widget">
|
||||
{{ size }}
|
||||
</SkySurface>
|
||||
</SkyWidgetFrame>
|
||||
</div>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="Glass and standalone back link">
|
||||
<SkyGlass class="sky-ui-demo-extension-glass" component="button">
|
||||
Interactive SkyGlass
|
||||
</SkyGlass>
|
||||
<SkyNavbarBackLink
|
||||
ariaLabel="Standalone back link example"
|
||||
show-text
|
||||
text="Back"
|
||||
@click="demo.returnToCatalog"
|
||||
/>
|
||||
</SkySection>
|
||||
|
||||
<SkySettingsGroup title="Settings extensions">
|
||||
<SkySettingsRow kind="navigation" title="Navigation row">
|
||||
<template #leading><SkySettingsIcon>S</SkySettingsIcon></template>
|
||||
</SkySettingsRow>
|
||||
<SkySettingsRow
|
||||
v-model="notifications"
|
||||
description="Uses the toggle row variant"
|
||||
kind="toggle"
|
||||
title="Notifications"
|
||||
/>
|
||||
<SkySettingsRow
|
||||
kind="choice"
|
||||
:selected="selectedChoice === 'automatic'"
|
||||
title="Automatic"
|
||||
@activate="selectedChoice = 'automatic'"
|
||||
/>
|
||||
<SkySettingsRow
|
||||
kind="choice"
|
||||
:selected="selectedChoice === 'manual'"
|
||||
title="Manual"
|
||||
@activate="selectedChoice = 'manual'"
|
||||
/>
|
||||
<SkySettingsRow kind="value" title="Current mode" value="High" />
|
||||
<SkySettingsRow
|
||||
kind="custom"
|
||||
title="Custom row"
|
||||
value="Slot-compatible"
|
||||
/>
|
||||
<SkySettingsRow
|
||||
kind="action"
|
||||
title="Reset examples"
|
||||
tone="danger"
|
||||
@activate="resetExamples"
|
||||
/>
|
||||
<SkySettingsRangeRow v-model="range" title="Range row" />
|
||||
</SkySettingsGroup>
|
||||
|
||||
<SkySection title="Full pill navigation">
|
||||
<div class="sky-ui-demo-extension-navigation-stage">
|
||||
<SkyPillNavigation label="Full pill navigation" layout="full">
|
||||
<SkySegmented
|
||||
:active-index="fullTab"
|
||||
:item-count="3"
|
||||
aria-label="Full navigation tabs"
|
||||
navigation
|
||||
rounded
|
||||
strong
|
||||
>
|
||||
<SkySegmentedButton
|
||||
v-for="(label, index) in ['Apps', 'Games', 'Search']"
|
||||
:key="label"
|
||||
:active="fullTab === index"
|
||||
@click="fullTab = index"
|
||||
>
|
||||
{{ label }}
|
||||
</SkySegmentedButton>
|
||||
</SkySegmented>
|
||||
</SkyPillNavigation>
|
||||
</div>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="Compact pill navigation">
|
||||
<div class="sky-ui-demo-extension-navigation-stage">
|
||||
<SkyPillNavigation
|
||||
align="start"
|
||||
label="Compact pill navigation"
|
||||
layout="compact"
|
||||
>
|
||||
<SkySegmented
|
||||
:active-index="compactTab"
|
||||
:item-count="2"
|
||||
aria-label="Compact navigation tabs"
|
||||
navigation
|
||||
rounded
|
||||
strong
|
||||
>
|
||||
<SkySegmentedButton
|
||||
v-for="(label, index) in ['Apps', 'Games']"
|
||||
:key="label"
|
||||
:active="compactTab === index"
|
||||
@click="compactTab = index"
|
||||
>
|
||||
{{ label }}
|
||||
</SkySegmentedButton>
|
||||
</SkySegmented>
|
||||
</SkyPillNavigation>
|
||||
</div>
|
||||
</SkySection>
|
||||
|
||||
<SkySection title="Split pill navigation">
|
||||
<div class="sky-ui-demo-extension-navigation-stage">
|
||||
<SkyPillNavigation label="Split pill navigation" layout="split">
|
||||
<SkySegmented
|
||||
:active-index="Math.min(splitTab, 1)"
|
||||
:item-count="2"
|
||||
aria-label="Primary split navigation tabs"
|
||||
navigation
|
||||
rounded
|
||||
:strong="splitTab < 2"
|
||||
>
|
||||
<SkySegmentedButton
|
||||
v-for="(label, index) in ['Apps', 'Games']"
|
||||
:key="label"
|
||||
:active="splitTab === index"
|
||||
@click="splitTab = index"
|
||||
>
|
||||
{{ label }}
|
||||
</SkySegmentedButton>
|
||||
</SkySegmented>
|
||||
<template #end>
|
||||
<SkySegmented
|
||||
:active-index="0"
|
||||
:item-count="1"
|
||||
aria-label="Secondary split navigation tabs"
|
||||
navigation
|
||||
rounded
|
||||
:strong="splitTab === 2"
|
||||
>
|
||||
<SkySegmentedButton
|
||||
:active="splitTab === 2"
|
||||
@click="splitTab = 2"
|
||||
>
|
||||
Search
|
||||
</SkySegmentedButton>
|
||||
</SkySegmented>
|
||||
</template>
|
||||
</SkyPillNavigation>
|
||||
</div>
|
||||
</SkySection>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sky-ui-demo-extension-card {
|
||||
flex: 0 0 140px;
|
||||
}
|
||||
|
||||
.sky-ui-demo-extension-surface,
|
||||
.sky-ui-demo-extension-app,
|
||||
.sky-ui-demo-extension-widget,
|
||||
.sky-ui-demo-extension-glass {
|
||||
min-height: 92px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: var(--sky-space-3);
|
||||
}
|
||||
|
||||
.sky-ui-demo-extension-widgets {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--sky-space-3);
|
||||
}
|
||||
|
||||
.sky-ui-demo-extension-widgets > :nth-child(1) {
|
||||
height: 104px;
|
||||
}
|
||||
|
||||
.sky-ui-demo-extension-widgets > :nth-child(2) {
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
.sky-ui-demo-extension-widgets > :nth-child(3) {
|
||||
height: 152px;
|
||||
}
|
||||
|
||||
.sky-ui-demo-extension-navigation-stage {
|
||||
min-height: 76px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sky-ui-demo-extension-navigation-stage :deep(.sky-pill-navigation) {
|
||||
position: relative;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.sky-ui-demo-extension-widgets {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,226 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { SkyBlock, SkyBlockTitle, SkyList, SkyListItem, SkyStepper } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const value = ref(1)
|
||||
const inputValue = ref(1)
|
||||
interface StepperExample {
|
||||
label: string
|
||||
large: boolean
|
||||
outline: boolean
|
||||
rounded: boolean
|
||||
small: boolean
|
||||
}
|
||||
|
||||
interface StepperColorExample {
|
||||
className: string
|
||||
label: string
|
||||
rounded: boolean
|
||||
}
|
||||
|
||||
const shapeExamples: readonly StepperExample[] = [
|
||||
{
|
||||
label: 'Default',
|
||||
large: false,
|
||||
outline: false,
|
||||
rounded: false,
|
||||
small: false,
|
||||
},
|
||||
{ label: 'Round', large: false, outline: false, rounded: true, small: false },
|
||||
{
|
||||
label: 'Outline',
|
||||
large: false,
|
||||
outline: true,
|
||||
rounded: false,
|
||||
small: false,
|
||||
},
|
||||
{
|
||||
label: 'Rounded Outline',
|
||||
large: false,
|
||||
outline: true,
|
||||
rounded: true,
|
||||
small: false,
|
||||
},
|
||||
{ label: 'Small', large: false, outline: false, rounded: false, small: true },
|
||||
{
|
||||
label: 'Small Round',
|
||||
large: false,
|
||||
outline: false,
|
||||
rounded: true,
|
||||
small: true,
|
||||
},
|
||||
{
|
||||
label: 'Small Outline',
|
||||
large: false,
|
||||
outline: true,
|
||||
rounded: false,
|
||||
small: true,
|
||||
},
|
||||
{
|
||||
label: 'Small Rounded Outline',
|
||||
large: false,
|
||||
outline: true,
|
||||
rounded: true,
|
||||
small: true,
|
||||
},
|
||||
{ label: 'Large', large: true, outline: false, rounded: false, small: false },
|
||||
{
|
||||
label: 'Large Round',
|
||||
large: true,
|
||||
outline: false,
|
||||
rounded: true,
|
||||
small: false,
|
||||
},
|
||||
{
|
||||
label: 'Large Outline',
|
||||
large: true,
|
||||
outline: true,
|
||||
rounded: false,
|
||||
small: false,
|
||||
},
|
||||
{
|
||||
label: 'Large Rounded Outline',
|
||||
large: true,
|
||||
outline: true,
|
||||
rounded: true,
|
||||
small: false,
|
||||
},
|
||||
]
|
||||
|
||||
const raisedExamples: readonly StepperExample[] = shapeExamples.map(
|
||||
(example) => ({
|
||||
...example,
|
||||
label:
|
||||
example.label === 'Rounded Outline' ? 'Round Outline' : example.label,
|
||||
}),
|
||||
)
|
||||
|
||||
const colorExamples: readonly StepperColorExample[] = [
|
||||
{ className: 'sky-ui-demo-color-red', label: 'Red', rounded: false },
|
||||
{ className: 'sky-ui-demo-color-green', label: 'Green', rounded: true },
|
||||
{ className: 'sky-ui-demo-color-yellow', label: 'Yellow', rounded: false },
|
||||
{ className: 'sky-ui-demo-color-purple', label: 'Purple', rounded: true },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Stepper">
|
||||
<SkyBlockTitle>Shape and size</SkyBlockTitle>
|
||||
<SkyBlock class="sky-ui-demo-stepper-grid" inset strong>
|
||||
<div v-for="example in shapeExamples" :key="example.label">
|
||||
<small>{{ example.label }}</small>
|
||||
<SkyStepper
|
||||
v-model="value"
|
||||
decrement-label="Decrease value"
|
||||
increment-label="Increase value"
|
||||
:large="example.large"
|
||||
:outline="example.outline"
|
||||
:rounded="example.rounded"
|
||||
:small="example.small"
|
||||
:min="0"
|
||||
/>
|
||||
</div>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Raised</SkyBlockTitle>
|
||||
<SkyBlock class="sky-ui-demo-stepper-grid" inset strong>
|
||||
<div v-for="example in raisedExamples" :key="example.label">
|
||||
<small>{{ example.label }}</small>
|
||||
<SkyStepper
|
||||
v-model="value"
|
||||
decrement-label="Decrease value"
|
||||
increment-label="Increase value"
|
||||
:large="example.large"
|
||||
:outline="example.outline"
|
||||
raised
|
||||
:rounded="example.rounded"
|
||||
:small="example.small"
|
||||
:min="0"
|
||||
/>
|
||||
</div>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>With Text Input</SkyBlockTitle>
|
||||
<SkyBlock class="sky-ui-demo-stack sky-ui-demo-stepper-inputs" inset strong>
|
||||
<SkyStepper
|
||||
v-model="inputValue"
|
||||
decrement-label="Decrease input value"
|
||||
increment-label="Increase input value"
|
||||
input
|
||||
input-label="Stepper value"
|
||||
:min="0"
|
||||
/>
|
||||
<SkyStepper
|
||||
v-model="inputValue"
|
||||
decrement-label="Decrease outlined input value"
|
||||
increment-label="Increase outlined input value"
|
||||
input
|
||||
input-label="Outlined stepper value"
|
||||
:min="0"
|
||||
outline
|
||||
/>
|
||||
</SkyBlock>
|
||||
|
||||
<SkyBlockTitle>Only Buttons</SkyBlockTitle>
|
||||
<SkyList inset strong>
|
||||
<SkyListItem
|
||||
v-for="example in [
|
||||
{ label: 'Default' },
|
||||
{ label: 'Outline', outline: true },
|
||||
{ label: 'Raised Outline', outline: true, raised: true },
|
||||
]"
|
||||
:key="example.label"
|
||||
:title="`Value is ${value}`"
|
||||
>
|
||||
<template #after>
|
||||
<SkyStepper
|
||||
v-model="value"
|
||||
buttons-only
|
||||
:decrement-label="`Decrease ${example.label} value`"
|
||||
:increment-label="`Increase ${example.label} value`"
|
||||
:min="0"
|
||||
:outline="example.outline"
|
||||
:raised="example.raised"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlockTitle>Colors</SkyBlockTitle>
|
||||
<SkyBlock class="sky-ui-demo-stepper-grid" inset strong>
|
||||
<SkyStepper
|
||||
v-for="example in colorExamples"
|
||||
:key="example.label"
|
||||
v-model="value"
|
||||
:class="example.className"
|
||||
:decrement-label="`Decrease ${example.label.toLowerCase()} value`"
|
||||
:increment-label="`Increase ${example.label.toLowerCase()} value`"
|
||||
:min="0"
|
||||
:rounded="example.rounded"
|
||||
/>
|
||||
</SkyBlock>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sky-ui-demo-stepper-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--sky-space-4);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sky-ui-demo-stepper-grid > div {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: var(--sky-space-1);
|
||||
}
|
||||
|
||||
.sky-ui-demo-stepper-inputs {
|
||||
justify-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { SkyBlock, SkySegmented, SkySegmentedButton } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Subnavbar">
|
||||
<template #subnavbar>
|
||||
<SkySegmented
|
||||
:active-index="0"
|
||||
aria-label="Subnavbar buttons"
|
||||
class="subnavbar-demo__segmented"
|
||||
:item-count="3"
|
||||
rounded
|
||||
strong
|
||||
>
|
||||
<SkySegmentedButton active>Button</SkySegmentedButton>
|
||||
<SkySegmentedButton>Button</SkySegmentedButton>
|
||||
<SkySegmentedButton>Button</SkySegmentedButton>
|
||||
</SkySegmented>
|
||||
</template>
|
||||
|
||||
<div class="subnavbar-demo__content">
|
||||
<SkyBlock inset strong>
|
||||
<p>
|
||||
Subnavbar is useful when you need to put any additional elements into
|
||||
Navbar, like Tab Links or Search Bar. It also remains visible when
|
||||
Navbar hidden.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
<SkyBlock class="subnavbar-demo__copy" inset strong>
|
||||
<p>
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Ratione
|
||||
consequuntur numquam quia labore eligendi asperiores obcaecati neque
|
||||
sapiente fugit vel praesentium accusantium, earum corporis officiis
|
||||
tempora est delectus fugiat laudantium?
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Iure
|
||||
possimus, magni excepturi molestias consequatur commodi nihil velit
|
||||
quo odit adipisci voluptatibus quam fugit nesciunt repellat
|
||||
accusantium libero repudiandae consectetur alias!
|
||||
</p>
|
||||
<p>
|
||||
Nihil unde iusto ab animi eum? Quia natus provident fugit, molestias
|
||||
ea numquam qui distinctio ex perferendis fuga! Fugit voluptates
|
||||
obcaecati ipsa recusandae eos saepe ducimus repudiandae. Explicabo,
|
||||
fuga obcaecati!
|
||||
</p>
|
||||
<p>
|
||||
Neque voluptas eius, quod consequatur officia dolor sunt cupiditate,
|
||||
animi commodi cumque nam ex totam autem ipsum doloribus! Possimus
|
||||
eligendi unde provident, esse molestias amet minus! Laboriosam veniam
|
||||
alias eum.
|
||||
</p>
|
||||
<p>
|
||||
Dicta obcaecati, architecto at ducimus suscipit dolor ea praesentium
|
||||
sint? Deleniti ipsam magni dolorem amet quibusdam temporibus pariatur
|
||||
suscipit voluptas, quas harum nesciunt culpa facere consequuntur id et
|
||||
accusamus exercitationem!
|
||||
</p>
|
||||
<p>
|
||||
Officiis, quibusdam reprehenderit corporis non nisi debitis magnam.
|
||||
Deleniti, recusandae iusto repellendus dicta magni exercitationem
|
||||
labore natus reiciendis sapiente error dolorum necessitatibus maxime
|
||||
laborum esse suscipit praesentium ratione. Ex, in.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
</div>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.subnavbar-demo__segmented {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.subnavbar-demo__content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.subnavbar-demo__copy {
|
||||
display: grid;
|
||||
gap: var(--sky-space-4);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { CalendarDays, CloudUpload, Mail } from 'lucide-vue-next'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import {
|
||||
SkyBlock,
|
||||
SkyIcon,
|
||||
SkyList,
|
||||
SkyListItem,
|
||||
SkyTabBar,
|
||||
SkyTabButton,
|
||||
SkyToggle,
|
||||
} from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
type TabId = 'tab-1' | 'tab-2' | 'tab-3'
|
||||
|
||||
const activeTab = ref<TabId>('tab-1')
|
||||
const isTabbarLabels = ref(true)
|
||||
const isTabbarIcons = ref(true)
|
||||
const tabs = [
|
||||
{ icon: Mail, id: 'tab-1', label: 'Tab 1' },
|
||||
{ icon: CalendarDays, id: 'tab-2', label: 'Tab 2' },
|
||||
{ icon: CloudUpload, id: 'tab-3', label: 'Tab 3' },
|
||||
] as const
|
||||
const tabCopy: Record<TabId, string[]> = {
|
||||
'tab-1': [
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias accusantium necessitatibus, nihil quas praesentium at quibusdam cupiditate possimus et repudiandae dolorum delectus quo, similique voluptatem magni explicabo adipisci magnam ratione!',
|
||||
'Quod praesentium consequatur autem veritatis, magni alias consectetur ut quo, voluptatum earum in repellat! Id, autem! Minus suscipit, ad possimus non voluptatem aliquam praesentium earum corrupti optio velit tenetur numquam?',
|
||||
'Illo id ipsa natus quidem dignissimos odio dolore veniam, accusamus vel assumenda nulla aliquam amet distinctio! Debitis deserunt, et, cum voluptate similique culpa assumenda inventore, facilis eveniet iure optio velit.',
|
||||
'Maiores minus laborum placeat harum impedit, saepe veniam iusto voluptates delectus omnis consectetur tenetur ex ipsa error debitis aspernatur amet et alias! Sit odit cum voluptas quae? Est, omnis eos?',
|
||||
],
|
||||
'tab-2': [
|
||||
'Dicta beatae repudiandae ab pariatur mollitia praesentium fuga ipsum adipisci, quia nam expedita, est dolore eveniet, dolorum obcaecati? Veniam repellendus mollitia sapiente minus saepe voluptatibus necessitatibus laboriosam incidunt nihil autem.',
|
||||
'Officia pariatur qui, deleniti eum, et minus nisi aliquam nobis hic provident quisquam quidem voluptatem eveniet ducimus. Commodi ea dolorem, impedit eaque dolor, sint blanditiis magni accusantium natus reprehenderit minima?',
|
||||
'Dicta reiciendis ut vitae laborum aut repellendus quasi beatae nulla sapiente et. Quod distinctio velit, modi ipsam exercitationem iste quia eaque blanditiis neque rerum optio, nulla tenetur pariatur ex officiis.',
|
||||
'Consectetur accusamus delectus sit voluptatem at esse! Aperiam unde maxime est nemo dicta minus autem esse nobis quibusdam iusto, reprehenderit harum, perferendis quae. Ipsum sit fugit similique recusandae quas inventore?',
|
||||
],
|
||||
'tab-3': [
|
||||
'Vero esse ab natus neque commodi aut quidem nobis. Unde, quam asperiores. A labore quod commodi autem explicabo distinctio saepe ex amet iste recusandae porro consectetur, sed dolorum sapiente voluptatibus?',
|
||||
'Commodi ipsum, voluptatem obcaecati voluptatibus illum hic aliquam veritatis modi natus unde, assumenda expedita, esse eum fugit? Saepe aliquam ipsam illum nihil facilis, laborum quia, eius ea dolores molestias dicta.',
|
||||
'Consequatur quam laudantium, magnam facere ducimus tempora temporibus omnis cupiditate obcaecati tempore? Odit qui a, voluptas eveniet similique, doloribus eum dolorum ad, enim ea itaque voluptates porro minima. Omnis, magnam.',
|
||||
'Debitis, delectus! Eligendi excepturi rem veritatis, ad exercitationem tempore eveniet voluptates aut labore harum dolorem nemo repellendus accusantium quibusdam neque? Itaque iusto quisquam reprehenderit aperiam maiores dicta iure necessitatibus est.',
|
||||
],
|
||||
}
|
||||
const activeCopy = computed(() => tabCopy[activeTab.value])
|
||||
const activeLabel = computed(
|
||||
() => tabs.find((tab) => tab.id === activeTab.value)?.label ?? '',
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Tabbar" with-tabbar>
|
||||
<SkyList inset strong>
|
||||
<SkyListItem title="Tabbar Labels">
|
||||
<template #after>
|
||||
<SkyToggle v-model="isTabbarLabels" aria-label="Show tabbar labels" />
|
||||
</template>
|
||||
</SkyListItem>
|
||||
<SkyListItem title="Tabbar Icons">
|
||||
<template #after>
|
||||
<SkyToggle v-model="isTabbarIcons" aria-label="Show tabbar icons" />
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<SkyBlock aria-live="polite" class="sky-ui-demo-stack" inset strong>
|
||||
<p class="sky-ui-demo-tabbar__title">
|
||||
<b>{{ activeLabel }}</b>
|
||||
</p>
|
||||
<p class="sky-ui-demo-tabbar__copy">
|
||||
<span v-for="paragraph in activeCopy" :key="paragraph">{{
|
||||
paragraph
|
||||
}}</span>
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<template #fixed>
|
||||
<SkyTabBar
|
||||
:icons="isTabbarIcons"
|
||||
label="Demo tabs"
|
||||
:labels="isTabbarLabels"
|
||||
>
|
||||
<SkyTabButton
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
:active="activeTab === tab.id"
|
||||
:aria-label="tab.label"
|
||||
:label="isTabbarLabels ? tab.label : ''"
|
||||
@click="activeTab = tab.id"
|
||||
>
|
||||
<template v-if="isTabbarIcons" #icon>
|
||||
<SkyIcon :size="26"><component :is="tab.icon" /></SkyIcon>
|
||||
</template>
|
||||
</SkyTabButton>
|
||||
</SkyTabBar>
|
||||
</template>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sky-ui-demo-tabbar__title,
|
||||
.sky-ui-demo-tabbar__copy {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sky-ui-demo-tabbar__copy {
|
||||
display: grid;
|
||||
gap: var(--sky-space-3);
|
||||
color: var(--sky-muted);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
|
||||
import { SkyBlock, SkyButton, SkyToast } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
type ToastPosition = 'center' | 'left' | 'right'
|
||||
|
||||
const opened = reactive<Record<ToastPosition, boolean>>({
|
||||
center: false,
|
||||
left: false,
|
||||
right: false,
|
||||
})
|
||||
|
||||
function openToast(position: ToastPosition): void {
|
||||
opened.left = false
|
||||
opened.center = false
|
||||
opened.right = false
|
||||
opened[position] = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Toast">
|
||||
<SkyBlock class="sky-ui-demo-stack toast-demo__controls" inset strong>
|
||||
<p class="sky-ui-demo-copy">
|
||||
Toasts provide brief feedback about an operation through a message on
|
||||
the screen.
|
||||
</p>
|
||||
<SkyButton class="toast-demo__button" rounded @click="openToast('left')"
|
||||
>Toast on Left</SkyButton
|
||||
>
|
||||
<SkyButton class="toast-demo__button" rounded @click="openToast('center')"
|
||||
>Toast on Center</SkyButton
|
||||
>
|
||||
<SkyButton class="toast-demo__button" rounded @click="openToast('right')"
|
||||
>Toast on Right</SkyButton
|
||||
>
|
||||
</SkyBlock>
|
||||
|
||||
<template #fixed>
|
||||
<SkyToast
|
||||
v-for="position in ['left', 'center', 'right'] as const"
|
||||
:key="position"
|
||||
:opened="opened[position]"
|
||||
:position="position"
|
||||
>
|
||||
Hello this is {{ position }} toast!
|
||||
<template #button>
|
||||
<SkyButton clear inline rounded @click="opened[position] = false">
|
||||
Close
|
||||
</SkyButton>
|
||||
</template>
|
||||
</SkyToast>
|
||||
</template>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toast-demo__controls {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.toast-demo__button {
|
||||
height: 34px;
|
||||
min-height: 34px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toast-demo__button::before {
|
||||
position: absolute;
|
||||
inset: -5px 0;
|
||||
content: '';
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { SkyList, SkyListItem, SkyToggle } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const values = ref([true, true, true, true])
|
||||
const colorClasses = [
|
||||
'',
|
||||
'sky-ui-demo-color-red',
|
||||
'sky-ui-demo-color-green',
|
||||
'sky-ui-demo-color-yellow',
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Toggle">
|
||||
<SkyList inset strong>
|
||||
<SkyListItem
|
||||
v-for="(_, index) in values"
|
||||
:key="index"
|
||||
:title="`Item ${index + 1}`"
|
||||
>
|
||||
<template #after>
|
||||
<SkyToggle
|
||||
v-model="values[index]"
|
||||
:aria-label="`Item ${index + 1}`"
|
||||
:class="colorClasses[index]"
|
||||
:name="`demo-toggle-${index + 1}`"
|
||||
/>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { SkyBlock, SkyButton, SkyLink, SkyToolbar, SkyToolbarPane } from '@/ui'
|
||||
|
||||
import SkyUiDemoPage from '../SkyUiDemoPage.vue'
|
||||
|
||||
const isTop = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SkyUiDemoPage title="Toolbar" :with-tabbar="!isTop">
|
||||
<SkyToolbar
|
||||
v-if="isTop"
|
||||
aria-label="Top toolbar example"
|
||||
class="sky-ui-demo-toolbar sky-ui-demo-toolbar--top"
|
||||
component="nav"
|
||||
top
|
||||
>
|
||||
<SkyToolbarPane>
|
||||
<SkyLink>Link 1</SkyLink>
|
||||
<SkyLink>Link 2</SkyLink>
|
||||
</SkyToolbarPane>
|
||||
<SkyToolbarPane><SkyLink>Link 3</SkyLink></SkyToolbarPane>
|
||||
</SkyToolbar>
|
||||
|
||||
<SkyBlock class="sky-ui-demo-stack" inset strong>
|
||||
<p class="sky-ui-demo-copy">
|
||||
Toolbar supports both top and bottom positions. Click the following
|
||||
button to change its position.
|
||||
</p>
|
||||
<SkyButton rounded @click="isTop = !isTop">
|
||||
Toggle Toolbar Position
|
||||
</SkyButton>
|
||||
</SkyBlock>
|
||||
<SkyBlock class="sky-ui-demo-stack" inset strong>
|
||||
<p class="sky-ui-demo-copy">
|
||||
Mauris fermentum neque et luctus venenatis. Vivamus a sem rhoncus,
|
||||
ornare tellus eu, euismod mauris. In porta turpis at semper convallis.
|
||||
Duis adipiscing leo eu nulla lacinia, quis rhoncus metus condimentum.
|
||||
Etiam nec malesuada nibh. Maecenas quis lacinia nisl, vel posuere dolor.
|
||||
Vestibulum condimentum, nisl ac vulputate egestas, neque enim dignissim
|
||||
elit, rhoncus volutpat magna enim a est.
|
||||
</p>
|
||||
<p class="sky-ui-demo-copy">
|
||||
In hac habitasse platea dictumst. Etiam varius, ante vel ornare
|
||||
facilisis, velit massa rutrum dolor, ac porta magna magna lacinia nunc.
|
||||
Curabitur cursus laoreet. Aenean vel tempus augue. Pellentesque in
|
||||
imperdiet nibh. Mauris rhoncus nulla id sem suscipit volutpat.
|
||||
</p>
|
||||
<p class="sky-ui-demo-copy">
|
||||
Duis et ultricies nibh. Sed facilisis turpis urna, ac imperdiet erat
|
||||
venenatis eu. Proin sit amet faucibus tortor, et varius sem. Etiam vitae
|
||||
lacinia neque. Aliquam nisi purus, interdum in arcu sed, ultrices rutrum
|
||||
arcu.
|
||||
</p>
|
||||
</SkyBlock>
|
||||
|
||||
<template #fixed>
|
||||
<SkyToolbar
|
||||
v-if="!isTop"
|
||||
aria-label="Bottom toolbar example"
|
||||
class="sky-ui-demo-toolbar sky-ui-demo-toolbar--bottom"
|
||||
component="nav"
|
||||
>
|
||||
<SkyToolbarPane>
|
||||
<SkyLink>Link 1</SkyLink>
|
||||
<SkyLink>Link 2</SkyLink>
|
||||
</SkyToolbarPane>
|
||||
<SkyToolbarPane><SkyLink>Link 3</SkyLink></SkyToolbarPane>
|
||||
</SkyToolbar>
|
||||
</template>
|
||||
</SkyUiDemoPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sky-ui-demo-toolbar--top {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.sky-ui-demo-toolbar--bottom {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||