first commit

This commit is contained in:
2026-07-21 14:54:36 +10:00
commit e79c793b9c
134 changed files with 14427 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
import { defineComponent, h, type PropType } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
import { useNativeRouter } from '@native-vue-router/core'
import './style.css'
export type NativePlatform = 'ios' | 'android' | 'desktop'
export function detectNativePlatform(): NativePlatform {
if (typeof navigator === 'undefined') return 'desktop'
if (/iPad|iPhone|iPod|Macintosh/.test(navigator.userAgent) && ('ontouchend' in document)) return 'ios'
if (/Android/.test(navigator.userAgent)) return 'android'
return 'desktop'
}
export const NativeBackButton = defineComponent({
name: 'NativeBackButton',
props: { label: { type: String, default: 'Back' } },
setup(props) {
const native = useNativeRouter()
return () => h('button', {
type: 'button',
class: 'nvr-native-back',
'aria-label': props.label,
onClick: () => void native.pop(),
}, [h('span', { 'aria-hidden': 'true' }, ''), h('span', props.label)])
},
})
export interface NativeTabItem {
label: string
to: RouteLocationRaw
icon?: string
}
export const NativeTabBar = defineComponent({
name: 'NativeTabBar',
props: {
items: { type: Array as PropType<NativeTabItem[]>, required: true },
},
setup(props) {
const native = useNativeRouter()
return () => h('nav', { class: 'nvr-native-tabs', 'aria-label': 'Primary navigation' },
props.items.map((item) => {
const active = native.router.currentRoute.value.path === native.router.resolve(item.to).path
const href = native.router.resolve(item.to).href
return h('a', {
href,
class: ['nvr-native-tab', active && 'nvr-native-tab--active'],
'aria-current': active ? 'page' : undefined,
onClick: (event: MouseEvent) => {
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
event.preventDefault()
if (active) return
void native.sibling(item.to, { replace: true })
},
}, [
h('span', { class: 'nvr-native-tab__icon', 'aria-hidden': 'true' }, item.icon ?? '•'),
h('span', item.label),
])
}))
},
})
export const nativeMotionTokens = {
ios: { edgeWidth: 28, commitThreshold: 0.36 },
android: { edgeWidth: 24, commitThreshold: 0.32 },
desktop: { edgeWidth: 18, commitThreshold: 0.4 },
} as const

View File

@@ -0,0 +1,56 @@
:root {
--nvr-accent: #7c5cff;
--nvr-safe-top: env(safe-area-inset-top, 0px);
--nvr-safe-right: env(safe-area-inset-right, 0px);
--nvr-safe-bottom: env(safe-area-inset-bottom, 0px);
--nvr-safe-left: env(safe-area-inset-left, 0px);
}
.nvr-native-back {
display: inline-flex;
align-items: center;
gap: 2px;
min-width: 44px;
min-height: 44px;
padding: 0 10px 0 4px;
border: 0;
color: var(--nvr-accent);
background: transparent;
font: inherit;
cursor: pointer;
}
.nvr-native-back > span:first-child {
font-size: 34px;
font-weight: 300;
line-height: 1;
}
.nvr-native-tabs {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
padding: 8px max(12px, var(--nvr-safe-right)) calc(8px + var(--nvr-safe-bottom)) max(12px, var(--nvr-safe-left));
border-top: 1px solid color-mix(in srgb, currentColor 12%, transparent);
background: color-mix(in srgb, var(--nvr-view-background, #fff) 88%, transparent);
backdrop-filter: blur(24px) saturate(1.6);
}
.nvr-native-tab {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
min-height: 44px;
color: color-mix(in srgb, currentColor 54%, transparent);
text-decoration: none;
font-size: 11px;
font-weight: 600;
}
.nvr-native-tab--active { color: var(--nvr-accent); }
.nvr-native-tab__icon { font-size: 19px; line-height: 1.2; }
@media (pointer: fine) and (min-width: 900px) {
.nvr-native-tabs { border-radius: 18px 18px 0 0; }
}