chore: init read-star monorepo

This commit is contained in:
2026-01-16 13:06:44 +08:00
commit 1e510efe8e
508 changed files with 44803 additions and 0 deletions

View File

@ -0,0 +1,6 @@
import AdminLayout, { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './libs/admin-layout'
import PageTab from './libs/page-tab'
import SimpleScrollbar from './libs/simple-scrollbar'
export { AdminLayout, LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID, PageTab, SimpleScrollbar }
export * from './types'

View File

@ -0,0 +1,63 @@
/* @type */
.layout-header,
.layout-header-placement {
height: var(--soy-header-height);
}
.layout-header {
z-index: var(--soy-header-z-index);
}
.layout-tab {
top: var(--soy-header-height);
height: var(--soy-tab-height);
z-index: var(--soy-tab-z-index);
}
.layout-tab-placement {
height: var(--soy-tab-height);
}
.layout-sider {
width: var(--soy-sider-width);
z-index: var(--soy-sider-z-index);
}
.layout-mobile-sider {
z-index: var(--soy-sider-z-index);
}
.layout-mobile-sider-mask {
z-index: var(--soy-mobile-sider-z-index);
}
.layout-sider_collapsed {
width: var(--soy-sider-collapsed-width);
z-index: var(--soy-sider-z-index);
}
.layout-footer,
.layout-footer-placement {
height: var(--soy-footer-height);
}
.layout-footer {
z-index: var(--soy-footer-z-index);
}
.left-gap {
padding-left: var(--soy-sider-width);
}
.left-gap_collapsed {
padding-left: var(--soy-sider-collapsed-width);
}
.sider-padding-top {
padding-top: var(--soy-header-height);
}
.sider-padding-bottom {
padding-bottom: var(--soy-footer-height);
}

View File

@ -0,0 +1,18 @@
declare const styles: {
readonly 'layout-header': string
readonly 'layout-header-placement': string
readonly 'layout-tab': string
readonly 'layout-tab-placement': string
readonly 'layout-sider': string
readonly 'layout-mobile-sider': string
readonly 'layout-mobile-sider-mask': string
readonly 'layout-sider_collapsed': string
readonly 'layout-footer': string
readonly 'layout-footer-placement': string
readonly 'left-gap': string
readonly 'left-gap_collapsed': string
readonly 'sider-padding-top': string
readonly 'sider-padding-bottom': string
}
export default styles

View File

@ -0,0 +1,5 @@
import AdminLayout from './index.vue'
import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './shared'
export default AdminLayout
export { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID }

View File

@ -0,0 +1,236 @@
<script setup lang="ts">
import type { AdminLayoutProps } from '../../types'
import { computed } from 'vue'
import style from './index.module.css'
import { createLayoutCssVars, LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './shared'
defineOptions({
name: 'AdminLayout',
})
const props = withDefaults(defineProps<AdminLayoutProps>(), {
mode: 'vertical',
scrollMode: 'content',
scrollElId: LAYOUT_SCROLL_EL_ID,
commonClass: 'transition-all-300',
fixedTop: true,
maxZIndex: LAYOUT_MAX_Z_INDEX,
headerVisible: true,
headerHeight: 56,
tabVisible: true,
tabHeight: 48,
siderVisible: true,
siderCollapse: false,
siderWidth: 220,
siderCollapsedWidth: 64,
footerVisible: true,
footerHeight: 48,
rightFooter: false,
})
const emit = defineEmits<Emits>()
const slots = defineSlots<Slots>()
interface Emits {
/** Update siderCollapse */
(e: 'update:siderCollapse', collapse: boolean): void
}
type SlotFn = (props?: Record<string, unknown>) => any
interface Slots {
/** Main */
default?: SlotFn
/** Header */
header?: SlotFn
/** Tab */
tab?: SlotFn
/** Sider */
sider?: SlotFn
/** Footer */
footer?: SlotFn
}
const cssVars = computed(() => createLayoutCssVars(props))
// config visible
const showHeader = computed(() => Boolean(slots.header) && props.headerVisible)
const showTab = computed(() => Boolean(slots.tab) && props.tabVisible)
const showSider = computed(() => !props.isMobile && Boolean(slots.sider) && props.siderVisible)
const showMobileSider = computed(() => props.isMobile && Boolean(slots.sider) && props.siderVisible)
const showFooter = computed(() => Boolean(slots.footer) && props.footerVisible)
// scroll mode
const isWrapperScroll = computed(() => props.scrollMode === 'wrapper')
const isContentScroll = computed(() => props.scrollMode === 'content')
// layout direction
const isVertical = computed(() => props.mode === 'vertical')
const isHorizontal = computed(() => props.mode === 'horizontal')
const fixedHeaderAndTab = computed(() => props.fixedTop || (isHorizontal.value && isWrapperScroll.value))
// css
const leftGapClass = computed(() => {
if (!props.fullContent && showSider.value) {
return props.siderCollapse ? style['left-gap_collapsed'] : style['left-gap']
}
return ''
})
const headerLeftGapClass = computed(() => (isVertical.value ? leftGapClass.value : ''))
const footerLeftGapClass = computed(() => {
const condition1 = isVertical.value
const condition2 = isHorizontal.value && isWrapperScroll.value && !props.fixedFooter
const condition3 = Boolean(isHorizontal.value && props.rightFooter)
if (condition1 || condition2 || condition3) {
return leftGapClass.value
}
return ''
})
const siderPaddingClass = computed(() => {
let cls = ''
if (showHeader.value && !headerLeftGapClass.value) {
cls += style['sider-padding-top']
}
if (showFooter.value && !footerLeftGapClass.value) {
cls += ` ${style['sider-padding-bottom']}`
}
return cls
})
function handleClickMask() {
emit('update:siderCollapse', true)
}
</script>
<template>
<div class="relative h-full" :class="[commonClass]" :style="cssVars">
<div
:id="isWrapperScroll ? scrollElId : undefined"
class="h-full flex flex-col"
:class="[commonClass, scrollWrapperClass, { 'overflow-y-auto': isWrapperScroll }]"
>
<!-- Header -->
<template v-if="showHeader">
<header
v-show="!fullContent"
class="flex-shrink-0"
:class="[
style['layout-header'],
commonClass,
headerLeftGapClass,
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab },
]"
>
<slot name="header" />
</header>
<div
v-show="!fullContent && fixedHeaderAndTab"
class="flex-shrink-0 overflow-hidden"
:class="[style['layout-header-placement']]"
/>
</template>
<!-- Tab -->
<template v-if="showTab">
<div
class="flex-shrink-0"
:class="[
style['layout-tab'],
commonClass,
tabClass,
{ 'top-0!': fullContent || !showHeader },
leftGapClass,
{ 'absolute left-0 w-full': fixedHeaderAndTab },
]"
>
<slot name="tab" />
</div>
<div
v-show="fullContent || fixedHeaderAndTab"
class="flex-shrink-0 overflow-hidden"
:class="[style['layout-tab-placement']]"
/>
</template>
<!-- Sider -->
<template v-if="showSider">
<aside
v-show="!fullContent"
class="absolute left-0 top-0 h-full"
:class="[
commonClass,
siderClass,
siderPaddingClass,
siderCollapse ? style['layout-sider_collapsed'] : style['layout-sider'],
]"
>
<slot name="sider" />
</aside>
</template>
<!-- Mobile Sider -->
<template v-if="showMobileSider">
<aside
class="absolute left-0 top-0 h-full w-0 bg-white"
:class="[
commonClass,
mobileSiderClass,
style['layout-mobile-sider'],
siderCollapse ? 'overflow-hidden' : style['layout-sider'],
]"
>
<slot name="sider" />
</aside>
<div
v-show="!siderCollapse"
class="absolute left-0 top-0 h-full w-full bg-[rgba(0,0,0,0.2)]"
:class="[style['layout-mobile-sider-mask']]"
@click="handleClickMask"
/>
</template>
<!-- Main Content -->
<main
:id="isContentScroll ? scrollElId : undefined"
class="flex flex-col flex-grow"
:class="[commonClass, contentClass, leftGapClass, { 'overflow-y-auto': isContentScroll }]"
>
<slot />
</main>
<!-- Footer -->
<template v-if="showFooter">
<footer
v-show="!fullContent"
class="flex-shrink-0"
:class="[
style['layout-footer'],
commonClass,
footerClass,
footerLeftGapClass,
{ 'absolute left-0 bottom-0 w-full': fixedFooter },
]"
>
<slot name="footer" />
</footer>
<div
v-show="!fullContent && fixedFooter"
class="flex-shrink-0 overflow-hidden"
:class="[style['layout-footer-placement']]"
/>
</template>
</div>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,68 @@
import type { AdminLayoutProps, LayoutCssVars, LayoutCssVarsProps } from '../../types'
/** The id of the scroll element of the layout */
export const LAYOUT_SCROLL_EL_ID = '__SCROLL_EL_ID__'
/** The max z-index of the layout */
export const LAYOUT_MAX_Z_INDEX = 100
/**
* Create layout css vars by css vars props
*
* @param props Css vars props
*/
function createLayoutCssVarsByCssVarsProps(props: LayoutCssVarsProps) {
const cssVars: LayoutCssVars = {
'--soy-header-height': `${props.headerHeight}px`,
'--soy-header-z-index': props.headerZIndex,
'--soy-tab-height': `${props.tabHeight}px`,
'--soy-tab-z-index': props.tabZIndex,
'--soy-sider-width': `${props.siderWidth}px`,
'--soy-sider-collapsed-width': `${props.siderCollapsedWidth}px`,
'--soy-sider-z-index': props.siderZIndex,
'--soy-mobile-sider-z-index': props.mobileSiderZIndex,
'--soy-footer-height': `${props.footerHeight}px`,
'--soy-footer-z-index': props.footerZIndex,
}
return cssVars
}
/**
* Create layout css vars
*
* @param props
*/
export function createLayoutCssVars(props: AdminLayoutProps) {
const {
mode,
isMobile,
maxZIndex = LAYOUT_MAX_Z_INDEX,
headerHeight,
tabHeight,
siderWidth,
siderCollapsedWidth,
footerHeight,
} = props
const headerZIndex = maxZIndex - 3
const tabZIndex = maxZIndex - 5
const siderZIndex = mode === 'vertical' || isMobile ? maxZIndex - 1 : maxZIndex - 4
const mobileSiderZIndex = isMobile ? maxZIndex - 2 : 0
const footerZIndex = maxZIndex - 5
const cssProps: LayoutCssVarsProps = {
headerHeight,
headerZIndex,
tabHeight,
tabZIndex,
siderWidth,
siderZIndex,
mobileSiderZIndex,
siderCollapsedWidth,
footerHeight,
footerZIndex,
}
return createLayoutCssVarsByCssVarsProps(cssProps)
}

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import type { PageTabProps } from '../../types'
import style from './index.module.css'
defineOptions({
name: 'ButtonTab',
})
defineProps<PageTabProps>()
defineSlots<Slots>()
type SlotFn = (props?: Record<string, unknown>) => any
interface Slots {
/**
* Slot
*
* The center content of the tab
*/
default?: SlotFn
/**
* Slot
*
* The left content of the tab
*/
prefix?: SlotFn
/**
* Slot
*
* The right content of the tab
*/
suffix?: SlotFn
}
</script>
<template>
<div
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-12px whitespace-nowrap border-(1px solid) rounded-4px px-12px py-4px"
:class="[
style['button-tab'],
{ [style['button-tab_dark']]: darkMode },
{ [style['button-tab_active']]: active },
{ [style['button-tab_active_dark']]: active && darkMode },
]"
>
<slot name="prefix" />
<slot />
<slot name="suffix" />
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
defineOptions({
name: 'ChromeTabBg',
})
</script>
<template>
<svg class="size-full">
<defs>
<symbol id="geometry-left" viewBox="0 0 214 36">
<path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z" />
</symbol>
<symbol id="geometry-right" viewBox="0 0 214 36">
<use xlink:href="#geometry-left" />
</symbol>
<clipPath>
<rect width="100%" height="100%" x="0" />
</clipPath>
</defs>
<svg width="51%" height="100%">
<use xlink:href="#geometry-left" width="214" height="36" fill="currentColor" />
</svg>
<g transform="scale(-1, 1)">
<svg width="51%" height="100%" x="-100%" y="0">
<use xlink:href="#geometry-right" width="214" height="36" fill="currentColor" />
</svg>
</g>
</svg>
</template>
<style scoped></style>

View File

@ -0,0 +1,58 @@
<script setup lang="ts">
import type { PageTabProps } from '../../types'
import ChromeTabBg from './chrome-tab-bg.vue'
import style from './index.module.css'
defineOptions({
name: 'ChromeTab',
})
defineProps<PageTabProps>()
defineSlots<Slots>()
type SlotFn = (props?: Record<string, unknown>) => any
interface Slots {
/**
* Slot
*
* The center content of the tab
*/
default?: SlotFn
/**
* Slot
*
* The left content of the tab
*/
prefix?: SlotFn
/**
* Slot
*
* The right content of the tab
*/
suffix?: SlotFn
}
</script>
<template>
<div
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-16px whitespace-nowrap px-24px py-6px -mr-18px"
:class="[
style['chrome-tab'],
{ [style['chrome-tab_dark']]: darkMode },
{ [style['chrome-tab_active']]: active },
{ [style['chrome-tab_active_dark']]: active && darkMode },
]"
>
<div class=":soy: pointer-events-none absolute left-0 top-0 h-full w-full -z-1" :class="[style['chrome-tab__bg']]">
<ChromeTabBg />
</div>
<slot name="prefix" />
<slot />
<slot name="suffix" />
<div class=":soy: absolute right-7px h-16px w-1px bg-#1f2225" :class="[style['chrome-tab-divider']]" />
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,121 @@
/* @type */
.button-tab {
border-color: #e5e7eb;
}
.button-tab_dark {
border-color: #ffffff3d;
}
.button-tab:hover {
color: var(--soy-primary-color);
border-color: var(--soy-primary-color-opacity3);
}
.button-tab_active {
color: var(--soy-primary-color);
border-color: var(--soy-primary-color-opacity3);
background-color: var(--soy-primary-color-opacity1);
}
.button-tab_active_dark {
background-color: var(--soy-primary-color-opacity2);
}
.button-tab .svg-close:hover {
font-size: 12px;
color: #ffffff;
background-color: var(--soy-primary-color);
}
.button-tab_dark .svg-close:hover {
color: #000000;
}
.chrome-tab:hover {
z-index: 9;
}
.chrome-tab_active {
z-index: 10;
color: var(--soy-primary-color);
}
.chrome-tab__bg {
color: transparent;
}
.chrome-tab_active .chrome-tab__bg {
color: var(--soy-primary-color1);
}
.chrome-tab_active_dark .chrome-tab__bg {
color: var(--soy-primary-color2);
}
.chrome-tab:hover .chrome-tab__bg {
color: #dee1e6;
}
.chrome-tab_active:hover .chrome-tab__bg {
color: var(--soy-primary-color1);
}
.chrome-tab_dark:hover .chrome-tab__bg {
color: #333333;
}
.chrome-tab_active_dark:hover .chrome-tab__bg {
color: var(--soy-primary-color2);
}
.chrome-tab .svg-close:hover {
font-size: 12px;
color: #ffffff;
background-color: #9ca3af;
}
.chrome-tab_active .svg-close:hover {
background-color: var(--soy-primary-color);
}
.chrome-tab_dark .svg-close:hover {
color: #000000;
}
.chrome-tab_active .chrome-tab-divider {
opacity: 0;
}
.chrome-tab:hover .chrome-tab-divider {
opacity: 0;
}
.chrome-tab_dark .chrome-tab-divider {
background-color: rgba(255, 255, 255, 0.9);
}
.slider-tab {
background-color: transparent;
height: 100%;
border-bottom: 2px solid transparent;
}
.slider-tab_dark {
background-color: transparent;
}
.slider-tab:hover {
color: var(--soy-primary-color);
}
.slider-tab_active {
color: var(--soy-primary-color);
background-color: var(--soy-primary-color-opacity1);
border-bottom-color: var(--soy-primary-color);
}
.slider-tab_active_dark {
background-color: var(--soy-primary-color-opacity2);
}

View File

@ -0,0 +1,19 @@
declare const styles: {
readonly 'button-tab': string
readonly 'button-tab_dark': string
readonly 'button-tab_active': string
readonly 'button-tab_active_dark': string
readonly 'chrome-tab': string
readonly 'chrome-tab_active': string
readonly 'chrome-tab__bg': string
readonly 'chrome-tab_active_dark': string
readonly 'chrome-tab_dark': string
readonly 'chrome-tab-divider': string
readonly 'svg-close': string
readonly 'slider-tab': string
readonly 'slider-tab_active': string
readonly 'slider-tab_active_dark': string
readonly 'slider-tab_dark': string
}
export default styles

View File

@ -0,0 +1,3 @@
import PageTab from './index.vue'
export default PageTab

View File

@ -0,0 +1,77 @@
<script setup lang="ts">
import type { Component } from 'vue'
import type { PageTabMode, PageTabProps } from '../../types'
import { computed } from 'vue'
import ButtonTab from './button-tab.vue'
import ChromeTab from './chrome-tab.vue'
import style from './index.module.css'
import { ACTIVE_COLOR, createTabCssVars } from './shared'
import SliderTab from './slider-tab.vue'
import SvgClose from './svg-close.vue'
defineOptions({
name: 'PageTab',
})
const props = withDefaults(defineProps<PageTabProps>(), {
mode: 'chrome',
commonClass: 'transition-all-300',
activeColor: ACTIVE_COLOR,
closable: true,
})
const emit = defineEmits<Emits>()
interface Emits {
(e: 'close'): void
}
const activeTabComponent = computed(() => {
const { mode, chromeClass, buttonClass, sliderClass } = props
const tabComponentMap = {
chrome: {
component: ChromeTab,
class: chromeClass,
},
button: {
component: ButtonTab,
class: buttonClass,
},
slider: {
component: SliderTab,
class: sliderClass,
},
} satisfies Record<PageTabMode, { component: Component, class?: string }>
return tabComponentMap[mode]
})
const cssVars = computed(() => createTabCssVars(props.activeColor))
const bindProps = computed(() => {
const { chromeClass: _chromeCls, buttonClass: _btnCls, sliderClass: _sliderCls, ...rest } = props
return rest
})
function handleClose() {
emit('close')
}
</script>
<template>
<component :is="activeTabComponent.component" :class="activeTabComponent.class" :style="cssVars" v-bind="bindProps">
<template #prefix>
<slot name="prefix" />
</template>
<slot />
<template #suffix>
<slot name="suffix">
<SvgClose v-if="closable" :class="[style['svg-close']]" @pointerdown.stop="handleClose" />
</slot>
</template>
</component>
</template>
<style scoped></style>

View File

@ -0,0 +1,31 @@
import type { PageTabCssVars, PageTabCssVarsProps } from '../../types'
import { addColorAlpha, transformColorWithOpacity } from '@sa/color'
/** The active color of the tab */
export const ACTIVE_COLOR = '#1890ff'
function createCssVars(props: PageTabCssVarsProps) {
const cssVars: PageTabCssVars = {
'--soy-primary-color': props.primaryColor,
'--soy-primary-color1': props.primaryColor1,
'--soy-primary-color2': props.primaryColor2,
'--soy-primary-color-opacity1': props.primaryColorOpacity1,
'--soy-primary-color-opacity2': props.primaryColorOpacity2,
'--soy-primary-color-opacity3': props.primaryColorOpacity3,
}
return cssVars
}
export function createTabCssVars(primaryColor: string) {
const cssProps: PageTabCssVarsProps = {
primaryColor,
primaryColor1: transformColorWithOpacity(primaryColor, 0.1, '#ffffff'),
primaryColor2: transformColorWithOpacity(primaryColor, 0.3, '#000000'),
primaryColorOpacity1: addColorAlpha(primaryColor, 0.1),
primaryColorOpacity2: addColorAlpha(primaryColor, 0.15),
primaryColorOpacity3: addColorAlpha(primaryColor, 0.3),
}
return createCssVars(cssProps)
}

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import type { PageTabProps } from '../../types'
import style from './index.module.css'
defineOptions({
name: 'SliderTab',
})
defineProps<PageTabProps>()
defineSlots<Slots>()
type SlotFn = (props?: Record<string, unknown>) => any
interface Slots {
/**
* Slot
*
* The center content of the tab
*/
default?: SlotFn
/**
* Slot
*
* The left content of the tab
*/
prefix?: SlotFn
/**
* Slot
*
* The right content of the tab
*/
suffix?: SlotFn
}
</script>
<template>
<div
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-6px whitespace-nowrap px-12px py-4px"
:class="[
style['slider-tab'],
{ [style['slider-tab_dark']]: darkMode },
{ [style['slider-tab_active']]: active },
{ [style['slider-tab_active_dark']]: active && darkMode },
]"
>
<slot name="prefix" />
<slot />
<slot name="suffix" />
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
defineOptions({
name: 'SvgClose',
})
</script>
<template>
<div class=":soy: relative h-16px w-16px inline-flex items-center justify-center rd-50% text-14px">
<svg width="1em" height="1em" viewBox="0 0 1024 1024">
<path
fill="currentColor"
d="m563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8L295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512L196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1l216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,3 @@
import SimpleScrollbar from './index.vue'
export default SimpleScrollbar

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import Simplebar from 'simplebar-vue'
import 'simplebar-vue/dist/simplebar.min.css'
defineOptions({
name: 'SimpleScrollbar',
})
</script>
<template>
<div class="h-full flex-1-hidden">
<Simplebar class="h-full">
<slot />
</Simplebar>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,291 @@
/** Header config */
interface AdminLayoutHeaderConfig {
/**
* Whether header is visible
*
* @default true
*/
headerVisible?: boolean
/**
* Header height
*
* @default 56px
*/
headerHeight?: number
}
/** Tab config */
interface AdminLayoutTabConfig {
/**
* Whether tab is visible
*
* @default true
*/
tabVisible?: boolean
/**
* Tab class
*
* @default ''
*/
tabClass?: string
/**
* Tab height
*
* @default 48px
*/
tabHeight?: number
}
/** Sider config */
interface AdminLayoutSiderConfig {
/**
* Whether sider is visible
*
* @default true
*/
siderVisible?: boolean
/**
* Sider class
*
* @default ''
*/
siderClass?: string
/**
* Mobile sider class
*
* @default ''
*/
mobileSiderClass?: string
/**
* Sider collapse status
*
* @default false
*/
siderCollapse?: boolean
/**
* Sider width when collapse is false
*
* @default '220px'
*/
siderWidth?: number
/**
* Sider width when collapse is true
*
* @default '64px'
*/
siderCollapsedWidth?: number
}
/** Content config */
export interface AdminLayoutContentConfig {
/**
* Content class
*
* @default ''
*/
contentClass?: string
/**
* Whether content is full the page
*
* If true, other elements will be hidden by `display: none`
*/
fullContent?: boolean
}
/** Footer config */
export interface AdminLayoutFooterConfig {
/**
* Whether footer is visible
*
* @default true
*/
footerVisible?: boolean
/**
* Whether footer is fixed
*
* @default true
*/
fixedFooter?: boolean
/**
* Footer class
*
* @default ''
*/
footerClass?: string
/**
* Footer height
*
* @default 48px
*/
footerHeight?: number
/**
* Whether footer is on the right side
*
* When the layout is vertical, the footer is on the right side
*/
rightFooter?: boolean
}
/**
* Layout mode
*
* - Horizontal
* - Vertical
*/
export type LayoutMode = 'horizontal' | 'vertical'
/**
* The scroll mode when content overflow
*
* - Wrapper: the layout component's wrapper element has a scrollbar
* - Content: the layout component's content element has a scrollbar
*
* @default 'wrapper'
*/
export type LayoutScrollMode = 'wrapper' | 'content'
/** Admin layout props */
export interface AdminLayoutProps
extends
AdminLayoutHeaderConfig,
AdminLayoutTabConfig,
AdminLayoutSiderConfig,
AdminLayoutContentConfig,
AdminLayoutFooterConfig {
/**
* Layout mode
*
* - {@link LayoutMode}
*/
mode?: LayoutMode
/** Is mobile layout */
isMobile?: boolean
/**
* Scroll mode
*
* - {@link ScrollMode}
*/
scrollMode?: LayoutScrollMode
/**
* The id of the scroll element of the layout
*
* It can be used to get the corresponding Dom and scroll it
*
* @example
* use the default id by import
* ```ts
* import { adminLayoutScrollElId } from '@sa/vue-materials';
* ```
*
* @default
* ```ts
* const adminLayoutScrollElId = '__ADMIN_LAYOUT_SCROLL_EL_ID__'
* ```
*/
scrollElId?: string
/** The class of the scroll element */
scrollElClass?: string
/** The class of the scroll wrapper element */
scrollWrapperClass?: string
/**
* The common class of the layout
*
* Is can be used to configure the transition animation
*
* @default 'transition-all-300'
*/
commonClass?: string
/**
* Whether fix the header and tab
*
* @default true
*/
fixedTop?: boolean
/**
* The max z-index of the layout
*
* The z-index of Header,Tab,Sider and Footer will not exceed this value
*/
maxZIndex?: number
}
type Kebab<S extends string> = S extends Uncapitalize<S> ? S : `-${Uncapitalize<S>}`
type KebabCase<S extends string> = S extends `${infer Start}${infer End}`
? `${Uncapitalize<Start>}${KebabCase<Kebab<End>>}`
: S
type Prefix = '--soy-'
export type LayoutCssVarsProps = Pick<
AdminLayoutProps,
'headerHeight' | 'tabHeight' | 'siderWidth' | 'siderCollapsedWidth' | 'footerHeight'
> & {
headerZIndex?: number
tabZIndex?: number
siderZIndex?: number
mobileSiderZIndex?: number
footerZIndex?: number
}
export type LayoutCssVars = {
[K in keyof LayoutCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
}
/**
* The mode of the tab
*
* - Button: button style
* - Chrome: chrome style
*
* @default chrome
*/
export type PageTabMode = 'button' | 'chrome' | 'slider'
export interface PageTabProps {
/** Whether is dark mode */
darkMode?: boolean
/**
* The mode of the tab
*
* - {@link TabMode}
*/
mode?: PageTabMode
/**
* The common class of the layout
*
* Is can be used to configure the transition animation
*
* @default 'transition-all-300'
*/
commonClass?: string
/** The class of the button tab */
buttonClass?: string
/** The class of the chrome tab */
chromeClass?: string
/** The class of the title tab */
sliderClass?: string
/** Whether the tab is active */
active?: boolean
/** The color of the active tab */
activeColor?: string
/**
* Whether the tab is closable
*
* Show the close icon when true
*/
closable?: boolean
}
export interface PageTabCssVarsProps {
primaryColor: string
primaryColor1: string
primaryColor2: string
primaryColorOpacity1: string
primaryColorOpacity2: string
primaryColorOpacity3: string
}
export type PageTabCssVars = {
[K in keyof PageTabCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
}