feat: add emoji search aliases and icon catalog components
- Created `emoji-search-aliases.ts` to define aliases for emojis. - Added `icon-catalog.ts` to maintain a catalog of icons with their names and categories. - Introduced `glyph-demo.scss` for styling the glyph demo page. - Implemented routing in `index.ts` for component navigation. - Added base styles in `demo.css` for consistent layout and button styles. - Updated TypeScript configuration to include component types. - Modified Vitest configuration for improved testing setup.main
parent
8c54d1f8db
commit
3495e9d544
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,71 @@
|
|||||||
|
param(
|
||||||
|
[string]$UnicodeVersion = '17.0'
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$sourceUrl = "https://www.unicode.org/Public/$UnicodeVersion.0/emoji/emoji-test.txt"
|
||||||
|
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$outputPath = Join-Path $projectRoot 'src\pages\components\emoji-catalog.ts'
|
||||||
|
$response = Invoke-WebRequest -Uri $sourceUrl -UseBasicParsing
|
||||||
|
|
||||||
|
$group = ''
|
||||||
|
$subgroup = ''
|
||||||
|
$entries = [System.Collections.Generic.List[object]]::new()
|
||||||
|
|
||||||
|
foreach ($rawLine in ($response.Content -split "`n")) {
|
||||||
|
$line = $rawLine.Trim()
|
||||||
|
|
||||||
|
if ($line -match '^# group: (.+)$') {
|
||||||
|
$group = $Matches[1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($line -match '^# subgroup: (.+)$') {
|
||||||
|
$subgroup = $Matches[1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($line -match '^([0-9A-F ]+)\s*;\s*(?:fully-qualified|component)\s*#\s*(\S+)\s+E([0-9.]+)\s+(.+)$') {
|
||||||
|
$entries.Add([PSCustomObject]@{
|
||||||
|
emoji = $Matches[2]
|
||||||
|
name = $Matches[4]
|
||||||
|
group = $group
|
||||||
|
subgroup = $subgroup
|
||||||
|
version = $Matches[3]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = [System.Collections.Generic.List[string]]::new()
|
||||||
|
$lines.Add('// Generated from the official Unicode emoji-test.txt data file.')
|
||||||
|
$lines.Add("// Unicode Emoji version: $UnicodeVersion")
|
||||||
|
$lines.Add("// Source: $sourceUrl")
|
||||||
|
$lines.Add('// License: https://www.unicode.org/license.txt')
|
||||||
|
$lines.Add('')
|
||||||
|
$lines.Add('export interface EmojiCatalogEntry {')
|
||||||
|
$lines.Add(' emoji: string')
|
||||||
|
$lines.Add(' name: string')
|
||||||
|
$lines.Add(' group: string')
|
||||||
|
$lines.Add(' subgroup: string')
|
||||||
|
$lines.Add(' version: string')
|
||||||
|
$lines.Add('}')
|
||||||
|
$lines.Add('')
|
||||||
|
$lines.Add("export const emojiCatalogVersion = '$UnicodeVersion'")
|
||||||
|
$lines.Add('')
|
||||||
|
$lines.Add('export const emojiCatalog: EmojiCatalogEntry[] = [')
|
||||||
|
|
||||||
|
foreach ($entry in $entries) {
|
||||||
|
$emoji = ConvertTo-Json $entry.emoji -Compress
|
||||||
|
$name = ConvertTo-Json $entry.name -Compress
|
||||||
|
$entryGroup = ConvertTo-Json $entry.group -Compress
|
||||||
|
$entrySubgroup = ConvertTo-Json $entry.subgroup -Compress
|
||||||
|
$version = ConvertTo-Json $entry.version -Compress
|
||||||
|
$lines.Add(" { emoji: $emoji, name: $name, group: $entryGroup, subgroup: $entrySubgroup, version: $version },")
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines.Add(']')
|
||||||
|
$lines.Add('')
|
||||||
|
|
||||||
|
Set-Content -LiteralPath $outputPath -Value $lines -Encoding utf8
|
||||||
|
Write-Output "Generated $($entries.Count) emoji entries at $outputPath"
|
||||||
@ -1,11 +1,25 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
|
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||||
import App from '../App.vue'
|
import App from '../App.vue'
|
||||||
|
|
||||||
describe('App', () => {
|
describe('App', () => {
|
||||||
it('mounts renders properly', () => {
|
it('renders the active route', async () => {
|
||||||
const wrapper = mount(App)
|
const router = createRouter({
|
||||||
expect(wrapper.text()).toContain('You did it!')
|
history: createMemoryHistory(),
|
||||||
|
routes: [{ path: '/', component: { template: '<h1>Concise UI</h1>' } }],
|
||||||
|
})
|
||||||
|
|
||||||
|
await router.push('/')
|
||||||
|
await router.isReady()
|
||||||
|
|
||||||
|
const wrapper = mount(App, {
|
||||||
|
global: {
|
||||||
|
plugins: [router],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('Concise UI')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
tag?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
tag: 'div',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="tag" class="c-app-bar">
|
||||||
|
<div v-if="$slots.start" class="section start">
|
||||||
|
<slot name="start" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.center" class="section center">
|
||||||
|
<slot name="center" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.end" class="section end">
|
||||||
|
<slot name="end" />
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.c-app-bar {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: var(--c-app-bar-height, 38px);
|
||||||
|
padding: var(--c-app-bar-padding, 3px 8px);
|
||||||
|
color: var(--c-text-color, #20242a);
|
||||||
|
background: var(--c-surface-color, #fff);
|
||||||
|
border-bottom: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
gap: var(--c-app-bar-gap, 6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.start {
|
||||||
|
grid-column: 1;
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
grid-column: 2;
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end {
|
||||||
|
grid-column: 3;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,195 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import CIcon from '../icon/CIcon.vue'
|
||||||
|
import type { CButtonSize, CButtonType, CButtonVariant } from './types'
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
type?: CButtonType
|
||||||
|
variant?: CButtonVariant
|
||||||
|
size?: CButtonSize
|
||||||
|
icon?: string
|
||||||
|
disabled?: boolean
|
||||||
|
loading?: boolean
|
||||||
|
ariaLabel?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
variant: 'default',
|
||||||
|
size: 'medium',
|
||||||
|
icon: undefined,
|
||||||
|
disabled: false,
|
||||||
|
loading: false,
|
||||||
|
ariaLabel: undefined,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="c-button"
|
||||||
|
:class="[
|
||||||
|
`is-${variant}`,
|
||||||
|
`is-${size}`,
|
||||||
|
{ 'is-loading': loading, 'is-icon-only': !$slots.default },
|
||||||
|
]"
|
||||||
|
:type="type"
|
||||||
|
:disabled="disabled || loading"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
:aria-busy="loading ? 'true' : undefined"
|
||||||
|
>
|
||||||
|
<span v-if="loading" class="icon" aria-hidden="true">
|
||||||
|
<CIcon :rotate="1">↻</CIcon>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="icon || $slots.icon" class="icon" aria-hidden="true">
|
||||||
|
<slot name="icon">
|
||||||
|
<CIcon>{{ icon }}</CIcon>
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="$slots.default" class="label"><slot /></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.c-button {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: var(--c-control-height, 30px);
|
||||||
|
padding: 4px 10px;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--c-text-color, #20242a);
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--c-surface-color, #fff);
|
||||||
|
border: 1px solid var(--c-control-border-color, #bfc5ce);
|
||||||
|
border-radius: var(--c-border-radius, 3px);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: var(--c-hover-color, #eef1f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: var(--c-active-color, #e1e5ea);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
outline: 2px solid var(--c-focus-color, #3578c6);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: var(--c-disabled-text-color, #8a9099);
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: var(--c-disabled-background-color, #f1f3f5);
|
||||||
|
border-color: var(--c-disabled-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-primary {
|
||||||
|
color: var(--c-primary-text-color, #fff);
|
||||||
|
background: var(--c-primary-color, #286aa6);
|
||||||
|
border-color: var(--c-primary-border-color, #245f95);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: var(--c-primary-hover-color, #245f95);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: var(--c-primary-active-color, #1f5687);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-success {
|
||||||
|
color: var(--c-success-text-color, #fff);
|
||||||
|
background: var(--c-success-color, #2f7d32);
|
||||||
|
border-color: var(--c-success-border-color, #27692a);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: var(--c-success-hover-color, #27692a);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: var(--c-success-active-color, #205923);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-warning {
|
||||||
|
color: var(--c-warning-text-color, #3d2a00);
|
||||||
|
background: var(--c-warning-color, #f0ad32);
|
||||||
|
border-color: var(--c-warning-border-color, #d7951d);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: var(--c-warning-hover-color, #e3a025);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: var(--c-warning-active-color, #cf8d18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-danger {
|
||||||
|
color: var(--c-danger-text-color, #fff);
|
||||||
|
background: var(--c-danger-color, #b42318);
|
||||||
|
border-color: var(--c-danger-border-color, #991b1b);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: var(--c-danger-hover-color, #991b1b);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: var(--c-danger-active-color, #7f1d1d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-primary:disabled,
|
||||||
|
&.is-success:disabled,
|
||||||
|
&.is-warning:disabled,
|
||||||
|
&.is-danger:disabled {
|
||||||
|
color: var(--c-disabled-text-color, #8a9099);
|
||||||
|
background: var(--c-disabled-background-color, #f1f3f5);
|
||||||
|
border-color: var(--c-disabled-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-icon-only {
|
||||||
|
width: var(--c-control-height, 30px);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-small {
|
||||||
|
min-height: 26px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&.is-icon-only {
|
||||||
|
width: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-large {
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&.is-icon-only {
|
||||||
|
width: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export type CButtonVariant = 'default' | 'primary' | 'success' | 'warning' | 'danger'
|
||||||
|
export type CButtonType = 'button' | 'submit' | 'reset'
|
||||||
|
export type CButtonSize = 'small' | 'medium' | 'large'
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import { isGlyphSize } from './types'
|
||||||
|
import type { CGlyphDisplay, CGlyphSize } from './types'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
display?: CGlyphDisplay
|
||||||
|
size?: CGlyphSize
|
||||||
|
rotate?: number
|
||||||
|
label?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
display: 'inline',
|
||||||
|
size: undefined,
|
||||||
|
rotate: 0,
|
||||||
|
label: undefined,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const isRotating = computed(() => Number.isFinite(props.rotate) && props.rotate !== 0)
|
||||||
|
|
||||||
|
const glyphStyle = computed<Record<string, string>>(() => {
|
||||||
|
const style: Record<string, string> = {}
|
||||||
|
|
||||||
|
if (isGlyphSize(props.size)) style['--c-glyph-size'] = props.size
|
||||||
|
|
||||||
|
if (isRotating.value) {
|
||||||
|
style['--c-glyph-rotation-duration'] = `${1 / Math.abs(props.rotate)}s`
|
||||||
|
style['--c-glyph-rotation-direction'] = props.rotate < 0 ? 'reverse' : 'normal'
|
||||||
|
}
|
||||||
|
|
||||||
|
return style
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="c-glyph"
|
||||||
|
:class="[`is-${display}`, { 'is-rotating': isRotating }]"
|
||||||
|
:style="glyphStyle"
|
||||||
|
:role="label ? 'img' : undefined"
|
||||||
|
:aria-label="label"
|
||||||
|
:aria-hidden="label ? undefined : 'true'"
|
||||||
|
>
|
||||||
|
<span class="content"><slot /></span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.c-glyph {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-flex;
|
||||||
|
flex: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: var(--c-glyph-size, 1em);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1;
|
||||||
|
vertical-align: -0.125em;
|
||||||
|
|
||||||
|
&.is-inline {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-circle {
|
||||||
|
width: 1.75em;
|
||||||
|
height: 1.75em;
|
||||||
|
background: var(--c-glyph-circle-background, #eef1f5);
|
||||||
|
border: 1px solid var(--c-glyph-circle-border-color, #d5d9df);
|
||||||
|
border-radius: 50%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: inline-block;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-rotating .content {
|
||||||
|
animation: c-glyph-rotate var(--c-glyph-rotation-duration) linear infinite;
|
||||||
|
animation-direction: var(--c-glyph-rotation-direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes c-glyph-rotate {
|
||||||
|
to {
|
||||||
|
transform: rotate(1turn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.c-glyph.is-rotating .content {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
export type CGlyphDisplay = 'inline' | 'circle'
|
||||||
|
export type CGlyphSizeUnit = 'px' | 'em' | 'rem'
|
||||||
|
export type CGlyphSize = `${number}${CGlyphSizeUnit}`
|
||||||
|
|
||||||
|
export interface CGlyphPresentationProps {
|
||||||
|
display?: CGlyphDisplay
|
||||||
|
size?: CGlyphSize
|
||||||
|
rotate?: number
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGlyphSize(value: unknown): value is CGlyphSize {
|
||||||
|
return typeof value === 'string' && /^(?:0|[1-9]\d*)(?:\.\d+)?(?:px|em|rem)$/.test(value)
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import CGlyph from '../glyph/CGlyph.vue'
|
||||||
|
import type { CGlyphDisplay, CGlyphSize } from '../glyph/types'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
display?: CGlyphDisplay
|
||||||
|
size?: CGlyphSize
|
||||||
|
rotate?: number
|
||||||
|
color?: string
|
||||||
|
label?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
display: 'inline',
|
||||||
|
size: undefined,
|
||||||
|
rotate: 0,
|
||||||
|
color: undefined,
|
||||||
|
label: undefined,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const iconStyle = computed(() => (props.color ? { color: props.color } : undefined))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CGlyph
|
||||||
|
class="c-icon"
|
||||||
|
:display="display"
|
||||||
|
:size="size"
|
||||||
|
:rotate="rotate"
|
||||||
|
:label="label"
|
||||||
|
:style="iconStyle"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CGlyph>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.c-icon {
|
||||||
|
color: inherit;
|
||||||
|
font-family: "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", "Segoe UI Symbol",
|
||||||
|
"Noto Sans Symbols 2", system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import CMenuList from './CMenuList.vue'
|
||||||
|
import type {
|
||||||
|
CMenuEntry,
|
||||||
|
CMenuOrientation,
|
||||||
|
CMenuSelectEvent,
|
||||||
|
CMenuSubmenuMode,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
items: CMenuEntry[]
|
||||||
|
orientation?: CMenuOrientation
|
||||||
|
submenuMode?: CMenuSubmenuMode
|
||||||
|
ariaLabel?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
orientation: 'vertical',
|
||||||
|
submenuMode: 'auto',
|
||||||
|
ariaLabel: undefined,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
select: [event: CMenuSelectEvent]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const expandedKeys = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
|
const resolvedSubmenuMode = computed<'inline' | 'flyout'>(() => {
|
||||||
|
if (props.submenuMode !== 'auto') return props.submenuMode
|
||||||
|
return props.orientation === 'horizontal' ? 'flyout' : 'inline'
|
||||||
|
})
|
||||||
|
|
||||||
|
function parentKey(key: string) {
|
||||||
|
const separatorIndex = key.lastIndexOf('/')
|
||||||
|
return separatorIndex === -1 ? '' : key.slice(0, separatorIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleItem(key: string, force?: boolean) {
|
||||||
|
const next = new Set(expandedKeys.value)
|
||||||
|
const shouldExpand = force ?? !next.has(key)
|
||||||
|
|
||||||
|
if (shouldExpand) {
|
||||||
|
if (resolvedSubmenuMode.value === 'flyout') {
|
||||||
|
const siblingParent = parentKey(key)
|
||||||
|
|
||||||
|
for (const expandedKey of next) {
|
||||||
|
if (parentKey(expandedKey) === siblingParent && expandedKey !== key) {
|
||||||
|
next.delete(expandedKey)
|
||||||
|
|
||||||
|
for (const descendantKey of next) {
|
||||||
|
if (descendantKey.startsWith(`${expandedKey}/`)) next.delete(descendantKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next.add(key)
|
||||||
|
} else {
|
||||||
|
next.delete(key)
|
||||||
|
|
||||||
|
for (const expandedKey of next) {
|
||||||
|
if (expandedKey.startsWith(`${key}/`)) next.delete(expandedKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expandedKeys.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectItem(event: CMenuSelectEvent) {
|
||||||
|
expandedKeys.value = new Set()
|
||||||
|
emit('select', event)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CMenuList
|
||||||
|
:items="items"
|
||||||
|
:orientation="orientation"
|
||||||
|
:submenu-mode="resolvedSubmenuMode"
|
||||||
|
:expanded-keys="expandedKeys"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
@toggle="toggleItem"
|
||||||
|
@select="selectItem"
|
||||||
|
>
|
||||||
|
<template v-if="$slots.item" #item="slotProps">
|
||||||
|
<slot name="item" v-bind="slotProps" />
|
||||||
|
</template>
|
||||||
|
</CMenuList>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
export type CMenuOrientation = 'horizontal' | 'vertical'
|
||||||
|
export type CMenuSubmenuMode = 'auto' | 'inline' | 'flyout'
|
||||||
|
|
||||||
|
export interface CMenuCommandContext {
|
||||||
|
item: CMenuActionItem
|
||||||
|
originalEvent: MouseEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CMenuActionItem {
|
||||||
|
type?: 'item'
|
||||||
|
id?: string
|
||||||
|
label: string
|
||||||
|
icon?: string
|
||||||
|
url?: string
|
||||||
|
target?: string
|
||||||
|
children?: CMenuEntry[]
|
||||||
|
disabled?: boolean
|
||||||
|
hidden?: boolean
|
||||||
|
active?: boolean
|
||||||
|
command?: (context: CMenuCommandContext) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CMenuSeparatorItem {
|
||||||
|
type: 'separator'
|
||||||
|
id?: string
|
||||||
|
hidden?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CMenuEntry = CMenuActionItem | CMenuSeparatorItem
|
||||||
|
|
||||||
|
export interface CMenuSelectEvent extends CMenuCommandContext {}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import type { CSeparatorOrientation } from './types'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
orientation?: CSeparatorOrientation
|
||||||
|
decorative?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
orientation: 'horizontal',
|
||||||
|
decorative: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const separatorRole = computed(() => (props.decorative ? undefined : 'separator'))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="c-separator"
|
||||||
|
:class="`is-${orientation}`"
|
||||||
|
:role="separatorRole"
|
||||||
|
:aria-hidden="decorative ? 'true' : undefined"
|
||||||
|
:aria-orientation="decorative ? undefined : orientation"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.c-separator {
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: none;
|
||||||
|
border: 0 solid var(--c-border-color, #d5d9df);
|
||||||
|
|
||||||
|
&.is-horizontal {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-vertical {
|
||||||
|
align-self: stretch;
|
||||||
|
width: 1px;
|
||||||
|
min-height: 1rem;
|
||||||
|
border-left-width: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export type CSeparatorOrientation = 'horizontal' | 'vertical'
|
||||||
@ -0,0 +1,168 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import CButton from '../button/CButton.vue'
|
||||||
|
import type { CSideBarPlacement } from './types'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
tag?: string
|
||||||
|
placement?: CSideBarPlacement
|
||||||
|
width?: string
|
||||||
|
collapsible?: boolean
|
||||||
|
collapsed?: boolean
|
||||||
|
collapsedWidth?: string
|
||||||
|
ariaLabel?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
tag: 'aside',
|
||||||
|
placement: 'start',
|
||||||
|
width: 'var(--c-side-bar-width, 240px)',
|
||||||
|
collapsible: false,
|
||||||
|
collapsed: undefined,
|
||||||
|
collapsedWidth: '38px',
|
||||||
|
ariaLabel: undefined,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:collapsed': [collapsed: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const internalCollapsed = ref(false)
|
||||||
|
const isCollapsed = computed(() =>
|
||||||
|
props.collapsible ? (props.collapsed ?? internalCollapsed.value) : false,
|
||||||
|
)
|
||||||
|
|
||||||
|
const sideBarStyle = computed(() => ({
|
||||||
|
'--c-side-bar-current-width': isCollapsed.value ? props.collapsedWidth : props.width,
|
||||||
|
}))
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.collapsed,
|
||||||
|
(collapsed) => {
|
||||||
|
if (collapsed !== undefined) internalCollapsed.value = collapsed
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function toggleCollapsed() {
|
||||||
|
const collapsed = !isCollapsed.value
|
||||||
|
internalCollapsed.value = collapsed
|
||||||
|
emit('update:collapsed', collapsed)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="tag"
|
||||||
|
class="c-side-bar"
|
||||||
|
:class="[`is-${placement}`, { 'is-collapsed': isCollapsed }]"
|
||||||
|
:style="sideBarStyle"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
>
|
||||||
|
<div v-if="$slots.header || collapsible" class="header">
|
||||||
|
<CButton
|
||||||
|
v-if="collapsible"
|
||||||
|
class="toggle"
|
||||||
|
icon="☰"
|
||||||
|
:aria-expanded="!isCollapsed"
|
||||||
|
:aria-label="isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'"
|
||||||
|
@click="toggleCollapsed"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-show="!isCollapsed && $slots.header" class="header-content">
|
||||||
|
<slot name="header" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="!isCollapsed" class="content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.footer" v-show="!isCollapsed" class="footer">
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.c-side-bar {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 var(--c-side-bar-current-width);
|
||||||
|
flex-direction: column;
|
||||||
|
width: var(--c-side-bar-current-width);
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--c-text-color, #20242a);
|
||||||
|
font-family: var(--c-font-family, system-ui, -apple-system, "Segoe UI", sans-serif);
|
||||||
|
font-size: var(--c-font-size, 13px);
|
||||||
|
background: var(--c-surface-color, #fff);
|
||||||
|
transition:
|
||||||
|
width var(--c-side-bar-transition-duration, 160ms) ease,
|
||||||
|
flex-basis var(--c-side-bar-transition-duration, 160ms) ease;
|
||||||
|
|
||||||
|
&.is-start {
|
||||||
|
border-inline-end: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-end {
|
||||||
|
border-inline-start: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header,
|
||||||
|
.footer {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex: none;
|
||||||
|
align-items: center;
|
||||||
|
min-height: var(--c-side-bar-section-height, 38px);
|
||||||
|
padding: var(--c-side-bar-section-padding, 5px 8px);
|
||||||
|
gap: var(--c-side-bar-gap, 6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
border-bottom: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
flex: none;
|
||||||
|
--c-surface-color: transparent;
|
||||||
|
--c-control-border-color: color-mix(in srgb, currentColor 35%, transparent);
|
||||||
|
--c-hover-color: color-mix(in srgb, currentColor 10%, transparent);
|
||||||
|
--c-active-color: color-mix(in srgb, currentColor 18%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-collapsed .header {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
border-top: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
padding: var(--c-side-bar-content-padding, 4px);
|
||||||
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.c-side-bar {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export type CSideBarPlacement = 'start' | 'end'
|
||||||
@ -0,0 +1,206 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||||
|
import Prism from 'prismjs'
|
||||||
|
import 'prismjs/components/prism-typescript'
|
||||||
|
|
||||||
|
type CCodeLanguage = 'markup' | 'css' | 'javascript' | 'typescript'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
code: string
|
||||||
|
language?: CCodeLanguage
|
||||||
|
wrap?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
language: 'markup',
|
||||||
|
wrap: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const copyStatus = ref('')
|
||||||
|
let copyStatusTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
|
const highlightedCode = computed(() => {
|
||||||
|
const grammar = Prism.languages[props.language] ?? Prism.languages.markup!
|
||||||
|
return Prism.highlight(props.code, grammar, props.language)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function copyCode() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(props.code)
|
||||||
|
copyStatus.value = 'Copied'
|
||||||
|
} catch {
|
||||||
|
copyStatus.value = 'Copy failed'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copyStatusTimer) clearTimeout(copyStatusTimer)
|
||||||
|
copyStatusTimer = setTimeout(() => {
|
||||||
|
copyStatus.value = ''
|
||||||
|
}, 1800)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (copyStatusTimer) clearTimeout(copyStatusTimer)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="code-block" :class="{ 'is-wrapped': wrap }">
|
||||||
|
<div class="header">
|
||||||
|
<span>{{ language }}</span>
|
||||||
|
<button type="button" @click="copyCode">{{ copyStatus || 'Copy' }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre><code :class="`language-${language}`" v-html="highlightedCode" /></pre>
|
||||||
|
<span class="status" aria-live="polite">{{ copyStatus }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.code-block {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #24292f;
|
||||||
|
background: #f7f8fa;
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
border-radius: var(--c-border-radius, 3px);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 3px 5px 3px 9px;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: #eef1f5;
|
||||||
|
border-bottom: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-width: 52px;
|
||||||
|
height: 22px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-transform: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c7ccd3;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #20242a;
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--c-focus-color, #3578c6);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 11px 13px;
|
||||||
|
overflow: auto;
|
||||||
|
tab-size: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: inherit;
|
||||||
|
font-family: Consolas, "Cascadia Code", "SFMono-Regular", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-shadow: none;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-wrapped code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
clip-path: inset(50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.token) {
|
||||||
|
&.comment,
|
||||||
|
&.prolog,
|
||||||
|
&.doctype,
|
||||||
|
&.cdata {
|
||||||
|
color: #6a737d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.punctuation {
|
||||||
|
color: #57606a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag,
|
||||||
|
&.constant,
|
||||||
|
&.symbol,
|
||||||
|
&.deleted {
|
||||||
|
color: #cf222e;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.boolean,
|
||||||
|
&.number {
|
||||||
|
color: #0550ae;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selector,
|
||||||
|
&.attr-name,
|
||||||
|
&.string,
|
||||||
|
&.char,
|
||||||
|
&.builtin,
|
||||||
|
&.inserted {
|
||||||
|
color: #116329;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.operator,
|
||||||
|
&.entity,
|
||||||
|
&.url {
|
||||||
|
color: #0a3069;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.atrule,
|
||||||
|
&.attr-value,
|
||||||
|
&.keyword {
|
||||||
|
color: #8250df;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.function,
|
||||||
|
&.class-name {
|
||||||
|
color: #953800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.regex,
|
||||||
|
&.important,
|
||||||
|
&.variable {
|
||||||
|
color: #bc4c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bold,
|
||||||
|
&.important {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.language-css .token.string) {
|
||||||
|
color: #0a3069;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,2 +1,25 @@
|
|||||||
// Public exports for Concise UI belong here.
|
export { default as CAppBar } from './components/app-bar/CAppBar.vue'
|
||||||
export {}
|
export { default as CButton } from './components/button/CButton.vue'
|
||||||
|
export { default as CIcon } from './components/icon/CIcon.vue'
|
||||||
|
export { default as CMenu } from './components/menu/CMenu.vue'
|
||||||
|
export { default as CSeparator } from './components/separator/CSeparator.vue'
|
||||||
|
export { default as CSideBar } from './components/side-bar/CSideBar.vue'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
CMenuActionItem,
|
||||||
|
CMenuCommandContext,
|
||||||
|
CMenuEntry,
|
||||||
|
CMenuOrientation,
|
||||||
|
CMenuSelectEvent,
|
||||||
|
CMenuSeparatorItem,
|
||||||
|
CMenuSubmenuMode,
|
||||||
|
} from './components/menu/types'
|
||||||
|
export type { CButtonSize, CButtonType, CButtonVariant } from './components/button/types'
|
||||||
|
export type { CSeparatorOrientation } from './components/separator/types'
|
||||||
|
export type { CSideBarPlacement } from './components/side-bar/types'
|
||||||
|
export type {
|
||||||
|
CGlyphDisplay,
|
||||||
|
CGlyphPresentationProps,
|
||||||
|
CGlyphSize,
|
||||||
|
CGlyphSizeUnit,
|
||||||
|
} from './components/glyph/types'
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { CAppBar, CMenu, CSideBar } from '@/index'
|
||||||
|
|
||||||
|
import { useComponentNavigation, useTopNavigation } from './useLayoutNavigation'
|
||||||
|
|
||||||
|
const navigation = useTopNavigation()
|
||||||
|
const componentNavigation = useComponentNavigation()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="demo-layout">
|
||||||
|
<CAppBar tag="header" aria-label="Application header">
|
||||||
|
<template #start>
|
||||||
|
<RouterLink class="demo-layout__brand" to="/">Concise UI</RouterLink>
|
||||||
|
<CMenu :items="navigation" orientation="horizontal" aria-label="Primary navigation" />
|
||||||
|
</template>
|
||||||
|
</CAppBar>
|
||||||
|
|
||||||
|
<div class="demo-layout__workspace">
|
||||||
|
<CSideBar width="220px" aria-label="Component navigation">
|
||||||
|
<template #header>
|
||||||
|
<strong>Components</strong>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<CMenu
|
||||||
|
:items="componentNavigation"
|
||||||
|
orientation="vertical"
|
||||||
|
submenu-mode="inline"
|
||||||
|
aria-label="Component pages"
|
||||||
|
/>
|
||||||
|
</CSideBar>
|
||||||
|
|
||||||
|
<main class="demo-layout__main demo-layout__main--with-sidebar">
|
||||||
|
<RouterView />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { CAppBar, CMenu } from '@/index'
|
||||||
|
|
||||||
|
import { useTopNavigation } from './useLayoutNavigation'
|
||||||
|
|
||||||
|
const navigation = useTopNavigation()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="demo-layout">
|
||||||
|
<CAppBar tag="header" aria-label="Application header">
|
||||||
|
<template #start>
|
||||||
|
<RouterLink class="demo-layout__brand" to="/">Concise UI</RouterLink>
|
||||||
|
<CMenu :items="navigation" orientation="horizontal" aria-label="Primary navigation" />
|
||||||
|
</template>
|
||||||
|
</CAppBar>
|
||||||
|
|
||||||
|
<main class="demo-layout__main">
|
||||||
|
<RouterView />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import type { CMenuEntry } from '@/index'
|
||||||
|
|
||||||
|
export function useTopNavigation() {
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
return computed<CMenuEntry[]>(() => [
|
||||||
|
{
|
||||||
|
id: 'home',
|
||||||
|
label: 'Home',
|
||||||
|
icon: '⌂',
|
||||||
|
active: route.name === 'home',
|
||||||
|
command: () => void router.push({ name: 'home' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'components',
|
||||||
|
label: 'Components',
|
||||||
|
icon: '▦',
|
||||||
|
active: route.path.startsWith('/components'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'app-bar-menu',
|
||||||
|
label: 'AppBar & Menu',
|
||||||
|
icon: '☰',
|
||||||
|
active: route.name === 'app-bar-menu',
|
||||||
|
command: () => void router.push({ name: 'app-bar-menu' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sidebar',
|
||||||
|
label: 'Sidebar',
|
||||||
|
icon: '◧',
|
||||||
|
active: route.name === 'sidebar',
|
||||||
|
command: () => void router.push({ name: 'sidebar' }),
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
id: 'button',
|
||||||
|
label: 'Button',
|
||||||
|
icon: '▭',
|
||||||
|
active: route.name === 'button',
|
||||||
|
command: () => void router.push({ name: 'button' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'icon',
|
||||||
|
label: 'Icon',
|
||||||
|
icon: '☆',
|
||||||
|
active: route.name === 'icon',
|
||||||
|
command: () => void router.push({ name: 'icon' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'emoji',
|
||||||
|
label: 'Emoji',
|
||||||
|
icon: '☺︎',
|
||||||
|
active: route.name === 'emoji',
|
||||||
|
command: () => void router.push({ name: 'emoji' }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useComponentNavigation() {
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
return computed<CMenuEntry[]>(() => [
|
||||||
|
{
|
||||||
|
id: 'app-bar-menu',
|
||||||
|
label: 'AppBar & Menu',
|
||||||
|
icon: '☰',
|
||||||
|
active: route.name === 'app-bar-menu',
|
||||||
|
command: () => void router.push({ name: 'app-bar-menu' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sidebar',
|
||||||
|
label: 'Sidebar',
|
||||||
|
icon: '◧',
|
||||||
|
active: route.name === 'sidebar',
|
||||||
|
command: () => void router.push({ name: 'sidebar' }),
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
id: 'button',
|
||||||
|
label: 'Button',
|
||||||
|
icon: '▭',
|
||||||
|
active: route.name === 'button',
|
||||||
|
command: () => void router.push({ name: 'button' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'icon',
|
||||||
|
label: 'Icon',
|
||||||
|
icon: '☆',
|
||||||
|
active: route.name === 'icon',
|
||||||
|
command: () => void router.push({ name: 'icon' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'emoji',
|
||||||
|
label: 'Emoji',
|
||||||
|
icon: '☺︎',
|
||||||
|
active: route.name === 'emoji',
|
||||||
|
command: () => void router.push({ name: 'emoji' }),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
@ -1,4 +1,6 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import './styles/demo.css'
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
createApp(App).use(router).mount('#app')
|
||||||
|
|||||||
@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<article class="home-page">
|
||||||
|
<p class="eyebrow">Vue 3 component framework</p>
|
||||||
|
<h1>Concise UI</h1>
|
||||||
|
<p class="intro">
|
||||||
|
Compact, predictable components for desktop enterprise and productivity applications.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<RouterLink class="demo-button demo-button--primary" to="/components/app-bar-menu">
|
||||||
|
Browse components
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="principles" aria-label="Design principles">
|
||||||
|
<div>
|
||||||
|
<strong>Dense</strong>
|
||||||
|
<span>Controls and layouts use space deliberately.</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Professional</strong>
|
||||||
|
<span>Neutral surfaces let application data stay prominent.</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Keyboard-friendly</strong>
|
||||||
|
<span>Desktop productivity is treated as a first-class concern.</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.home-page {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 56px 24px;
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: var(--c-primary-color, #2f6fad);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
max-width: 580px;
|
||||||
|
margin: 12px 0 0;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.principles {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
margin-top: 48px;
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 14px;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
+ div {
|
||||||
|
border-inline-start: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,412 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { CAppBar, CButton, CMenu, CSeparator } from '@/index'
|
||||||
|
import type { CMenuEntry, CMenuSelectEvent } from '@/index'
|
||||||
|
import CCodeBlock from '@/documentation/CCodeBlock.vue'
|
||||||
|
|
||||||
|
const lastAction = ref('No command selected')
|
||||||
|
|
||||||
|
const appBarUsage = `<CAppBar>
|
||||||
|
<template #start>
|
||||||
|
<CMenu :items="mainMenu" orientation="horizontal" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #center>Order Management</template>
|
||||||
|
|
||||||
|
<template #end>
|
||||||
|
<input type="search" placeholder="Search" />
|
||||||
|
<CButton variant="primary">Create order</CButton>
|
||||||
|
</template>
|
||||||
|
</CAppBar>`
|
||||||
|
|
||||||
|
const appBarJavaScript = `const mainMenu = [
|
||||||
|
{
|
||||||
|
label: 'File',
|
||||||
|
icon: '▤',
|
||||||
|
children: [
|
||||||
|
{ label: 'New', icon: '+', command: () => createDocument() },
|
||||||
|
{ label: 'Open…', icon: '📂', command: () => openDocument() },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Save', icon: '💾', command: () => saveDocument() },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: '✎',
|
||||||
|
children: [
|
||||||
|
{ label: 'Undo', icon: '↶', command: () => undo() },
|
||||||
|
{ label: 'Redo', icon: '↷', command: () => redo(), disabled: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Help', icon: '?', url: '#help' },
|
||||||
|
]`
|
||||||
|
|
||||||
|
const itemStructureJavaScript = `const fileMenu = [
|
||||||
|
{
|
||||||
|
label: 'File',
|
||||||
|
icon: '▤',
|
||||||
|
children: [
|
||||||
|
{ label: 'Open…', icon: '📂', command: () => openDocument() },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Save', icon: '💾', command: () => saveDocument() },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]`
|
||||||
|
|
||||||
|
const disabledAndHiddenJavaScript = `import { computed } from 'vue'
|
||||||
|
|
||||||
|
const editMenu = computed(() => [
|
||||||
|
{
|
||||||
|
label: 'Redo',
|
||||||
|
icon: '↷',
|
||||||
|
disabled: !canRedo.value,
|
||||||
|
command: () => redo(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Administration',
|
||||||
|
icon: '⚙︎',
|
||||||
|
hidden: !currentUser.value.isAdministrator,
|
||||||
|
url: '/administration',
|
||||||
|
},
|
||||||
|
])`
|
||||||
|
|
||||||
|
const linkStateJavaScript = `import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const navigation = computed(() => [
|
||||||
|
{
|
||||||
|
label: 'Dashboard',
|
||||||
|
icon: '⌂',
|
||||||
|
url: '/dashboard',
|
||||||
|
active: route.path === '/dashboard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Documentation',
|
||||||
|
icon: '?',
|
||||||
|
url: 'https://example.com/docs',
|
||||||
|
target: '_blank',
|
||||||
|
},
|
||||||
|
])`
|
||||||
|
|
||||||
|
const topMenu: CMenuEntry[] = [
|
||||||
|
{
|
||||||
|
label: 'File',
|
||||||
|
icon: '▤',
|
||||||
|
children: [
|
||||||
|
{ label: 'New', icon: '+' },
|
||||||
|
{ label: 'Open…', icon: '📂' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Save', icon: '💾' },
|
||||||
|
{ label: 'Save As…', icon: '💾' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: '✎',
|
||||||
|
children: [
|
||||||
|
{ label: 'Undo', icon: '↶' },
|
||||||
|
{ label: 'Redo', icon: '↷', disabled: true },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Preferences', icon: '⚙︎' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Help', icon: '?', url: '#help' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function recordSelection(event: CMenuSelectEvent) {
|
||||||
|
lastAction.value = event.item.label
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article id="help" class="component-page">
|
||||||
|
<header class="page-header">
|
||||||
|
<div>
|
||||||
|
<p class="category">Navigation</p>
|
||||||
|
<h1>AppBar & Menu</h1>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Compose application-level regions with <code>CAppBar</code>, then place data-driven
|
||||||
|
horizontal or vertical menus where the workflow needs them.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<CSeparator />
|
||||||
|
|
||||||
|
<section class="example">
|
||||||
|
<div class="description">
|
||||||
|
<h2>Application bar</h2>
|
||||||
|
<p>A horizontal menu can share the bar with centered context and controls at the end.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview">
|
||||||
|
<CAppBar aria-label="Example application bar">
|
||||||
|
<template #start>
|
||||||
|
<strong class="brand">Operations</strong>
|
||||||
|
<CMenu
|
||||||
|
:items="topMenu"
|
||||||
|
orientation="horizontal"
|
||||||
|
aria-label="Example application menu"
|
||||||
|
@select="recordSelection"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #center>
|
||||||
|
<span class="context">Order Management</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #end>
|
||||||
|
<input class="input" type="search" aria-label="Search orders" placeholder="Search" />
|
||||||
|
<CButton variant="primary">Create order</CButton>
|
||||||
|
</template>
|
||||||
|
</CAppBar>
|
||||||
|
<div class="status" aria-live="polite">Last action: {{ lastAction }}</div>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="appBarUsage" />
|
||||||
|
<CCodeBlock class="code-sample" :code="appBarJavaScript" language="javascript" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="example">
|
||||||
|
<div class="description">
|
||||||
|
<h2>Item content and hierarchy</h2>
|
||||||
|
<p>
|
||||||
|
Each regular menu entry has a label and can include a Unicode icon or a nested menu.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="property-list">
|
||||||
|
<div>
|
||||||
|
<dt><code>label</code></dt>
|
||||||
|
<dd>The required text displayed for the menu item.</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><code>icon</code></dt>
|
||||||
|
<dd>
|
||||||
|
An optional Unicode symbol or emoji rendered with <code>CIcon</code> before the label.
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><code>children</code></dt>
|
||||||
|
<dd>
|
||||||
|
An optional array of menu entries. Providing children turns the item into a submenu
|
||||||
|
trigger. Children can contain regular items, deeper submenus, and separators.
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<CCodeBlock class="code-sample" :code="itemStructureJavaScript" language="javascript" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="example">
|
||||||
|
<div class="description">
|
||||||
|
<h2>Disabled and hidden items</h2>
|
||||||
|
<p>
|
||||||
|
Set <code>disabled</code> when an action is currently unavailable. The item remains
|
||||||
|
visible but cannot be activated, is skipped by menu keyboard navigation, and its command
|
||||||
|
will not run. Use <code>hidden</code> when an item should not be shown at all.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="property-list">
|
||||||
|
<div>
|
||||||
|
<dt><code>disabled</code></dt>
|
||||||
|
<dd>
|
||||||
|
Applies disabled styling and <code>aria-disabled="true"</code>. It is commonly derived
|
||||||
|
from application state, permissions, or whether an operation can currently run.
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><code>hidden</code></dt>
|
||||||
|
<dd>
|
||||||
|
Removes the item from rendering and keyboard navigation. It is commonly derived from
|
||||||
|
permissions, feature availability, or application context, and also works on
|
||||||
|
separators.
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<CCodeBlock
|
||||||
|
class="code-sample"
|
||||||
|
:code="disabledAndHiddenJavaScript"
|
||||||
|
language="javascript"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="example">
|
||||||
|
<div class="description">
|
||||||
|
<h2>Link state and targets</h2>
|
||||||
|
<p>
|
||||||
|
Menu entries with a <code>url</code> render as links. Use <code>target</code> to choose
|
||||||
|
where a link opens and <code>active</code> to identify the current page.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="property-list">
|
||||||
|
<div>
|
||||||
|
<dt><code>url</code></dt>
|
||||||
|
<dd>
|
||||||
|
Becomes the link's <code>href</code>. Without it, the item renders as a button for a
|
||||||
|
command or submenu.
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><code>target</code></dt>
|
||||||
|
<dd>
|
||||||
|
Becomes the link's HTML <code>target</code>. Omit it for the current tab
|
||||||
|
(<code>_self</code>), or use <code>_blank</code> for a new tab. It has no effect without
|
||||||
|
<code>url</code>.
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><code>active</code></dt>
|
||||||
|
<dd>
|
||||||
|
Applies current-page styling and <code>aria-current="page"</code>. Set it from the
|
||||||
|
router or application state; <code>CMenu</code> does not match routes automatically.
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<CCodeBlock class="code-sample" :code="linkStateJavaScript" language="javascript" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.component-page {
|
||||||
|
width: min(100%, 1100px);
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 0.7fr) minmax(320px, 1.3fr);
|
||||||
|
align-items: end;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
gap: 32px;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
color: var(--c-primary-color, #2f6fad);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example {
|
||||||
|
padding: 22px 0;
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
overflow: visible;
|
||||||
|
background: var(--c-surface-color, #fff);
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
padding: 6px 9px;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 12px;
|
||||||
|
background: var(--c-subtle-surface-color, #f7f8fa);
|
||||||
|
border-top: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-sample {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-list {
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100px minmax(0, 1fr);
|
||||||
|
|
||||||
|
+ div {
|
||||||
|
border-top: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dt,
|
||||||
|
dd {
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--c-subtle-surface-color, #f7f8fa);
|
||||||
|
border-inline-end: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
padding-inline-end: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context {
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 150px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 4px 7px;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--c-border-color, #bfc5ce);
|
||||||
|
border-radius: var(--c-border-radius, 3px);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--c-focus-color, #3578c6);
|
||||||
|
outline: 1px solid var(--c-focus-color, #3578c6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,271 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { CButton, CIcon, CSeparator } from '@/index'
|
||||||
|
import CCodeBlock from '@/documentation/CCodeBlock.vue'
|
||||||
|
|
||||||
|
const clickCount = ref(0)
|
||||||
|
|
||||||
|
const basicUsage = `<CButton @click="save">Save</CButton>
|
||||||
|
<CButton variant="primary" @click="submit">Submit</CButton>
|
||||||
|
<CButton variant="success" @click="approve">Approve</CButton>
|
||||||
|
<CButton variant="warning" @click="suspend">Suspend</CButton>
|
||||||
|
<CButton variant="danger" @click="remove">Delete</CButton>`
|
||||||
|
|
||||||
|
const iconUsage = `<CButton icon="💾">Save</CButton>
|
||||||
|
|
||||||
|
<CButton icon="🗑️" variant="danger" aria-label="Delete record" />
|
||||||
|
|
||||||
|
<CButton>
|
||||||
|
<template #icon><CIcon>📂</CIcon></template>
|
||||||
|
Open
|
||||||
|
</CButton>`
|
||||||
|
|
||||||
|
const stateUsage = `<CButton :disabled="!canSave">Save</CButton>
|
||||||
|
<CButton :loading="isSaving" variant="primary">Save changes</CButton>`
|
||||||
|
|
||||||
|
const sizeUsage = `<CButton size="small">Small</CButton>
|
||||||
|
<CButton size="medium">Medium</CButton>
|
||||||
|
<CButton size="large">Large</CButton>`
|
||||||
|
|
||||||
|
const formUsage = `<form @submit.prevent="submit">
|
||||||
|
<CButton type="submit" variant="primary">Submit</CButton>
|
||||||
|
<CButton type="reset">Reset</CButton>
|
||||||
|
</form>`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="button-page">
|
||||||
|
<header class="page-header">
|
||||||
|
<div>
|
||||||
|
<p class="category">Controls</p>
|
||||||
|
<h1>Button</h1>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<code>CButton</code> provides compact actions with native button behavior, restrained
|
||||||
|
semantic variants, icons, and asynchronous states.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<CSeparator />
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Variants</h2>
|
||||||
|
<p>
|
||||||
|
Use color only when it communicates meaning: primary for the main action, success for a
|
||||||
|
positive workflow outcome, warning for a consequential action, and danger for a
|
||||||
|
destructive operation.
|
||||||
|
</p>
|
||||||
|
<div class="preview-row">
|
||||||
|
<CButton @click="clickCount++">Default</CButton>
|
||||||
|
<CButton variant="primary" @click="clickCount++">Primary</CButton>
|
||||||
|
<CButton variant="success" @click="clickCount++">Success</CButton>
|
||||||
|
<CButton variant="warning" @click="clickCount++">Warning</CButton>
|
||||||
|
<CButton variant="danger" @click="clickCount++">Danger</CButton>
|
||||||
|
<span class="status" aria-live="polite">Clicks: {{ clickCount }}</span>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="basicUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Icons</h2>
|
||||||
|
<p>
|
||||||
|
Pass a Unicode symbol or emoji through <code>icon</code>, or use the named
|
||||||
|
<code>icon</code> slot for custom presentation. Icon-only buttons require an
|
||||||
|
<code>aria-label</code>.
|
||||||
|
</p>
|
||||||
|
<div class="preview-row">
|
||||||
|
<CButton icon="💾">Save</CButton>
|
||||||
|
<CButton icon="📂">Open</CButton>
|
||||||
|
<CButton icon="🗑️" variant="danger" aria-label="Delete record" />
|
||||||
|
<CButton aria-label="Confirm">
|
||||||
|
<template #icon><CIcon color="green">✓</CIcon></template>
|
||||||
|
</CButton>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="iconUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Disabled and loading</h2>
|
||||||
|
<p>
|
||||||
|
Disabled buttons cannot be activated. Loading also disables the button, adds
|
||||||
|
<code>aria-busy="true"</code>, and replaces its icon with a rotating progress symbol.
|
||||||
|
</p>
|
||||||
|
<div class="preview-row">
|
||||||
|
<CButton disabled>Unavailable</CButton>
|
||||||
|
<CButton loading>Loading</CButton>
|
||||||
|
<CButton loading variant="primary">Saving changes</CButton>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="stateUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Sizes</h2>
|
||||||
|
<p>
|
||||||
|
Use semantic sizes to keep controls consistent. The compact <code>medium</code> size is the
|
||||||
|
default; small and large are available for denser tool areas or more prominent actions.
|
||||||
|
</p>
|
||||||
|
<div class="preview-row">
|
||||||
|
<CButton size="small" icon="✓">Small</CButton>
|
||||||
|
<CButton size="medium" icon="✓">Medium</CButton>
|
||||||
|
<CButton size="large" icon="✓">Large</CButton>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="sizeUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Native form behavior</h2>
|
||||||
|
<p>
|
||||||
|
The default <code>type</code> is <code>button</code>, preventing accidental form submission.
|
||||||
|
Set it explicitly to <code>submit</code> or <code>reset</code> when needed. Native attributes
|
||||||
|
and event listeners are forwarded to the underlying button.
|
||||||
|
</p>
|
||||||
|
<CCodeBlock class="code-sample" :code="formUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Properties</h2>
|
||||||
|
<dl class="property-list">
|
||||||
|
<div>
|
||||||
|
<dt><code>variant</code></dt>
|
||||||
|
<dd>
|
||||||
|
<code>default</code>, <code>primary</code>, <code>success</code>,
|
||||||
|
<code>warning</code>, or <code>danger</code>.
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><code>type</code></dt>
|
||||||
|
<dd><code>button</code>, <code>submit</code>, or <code>reset</code>.</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><code>size</code></dt>
|
||||||
|
<dd>
|
||||||
|
<code>small</code> (26px), <code>medium</code> (30px, default), or
|
||||||
|
<code>large</code> (34px).
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><code>icon</code></dt>
|
||||||
|
<dd>An optional Unicode symbol or emoji displayed before the label.</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><code>disabled</code></dt>
|
||||||
|
<dd>Disables native interaction and applies disabled styling.</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><code>loading</code></dt>
|
||||||
|
<dd>Disables interaction and displays an accessible progress state.</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><code>aria-label</code></dt>
|
||||||
|
<dd>Provides an accessible name, particularly for icon-only buttons.</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.button-page {
|
||||||
|
width: min(100%, 1100px);
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 0.7fr) minmax(320px, 1.3fr);
|
||||||
|
align-items: end;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
gap: 32px;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
color: var(--c-primary-color, #2f6fad);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 22px 0;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 8px;
|
||||||
|
gap: 8px;
|
||||||
|
background: var(--c-surface-color, #fff);
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-sample {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-list {
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 110px minmax(0, 1fr);
|
||||||
|
|
||||||
|
+ div {
|
||||||
|
border-top: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dt,
|
||||||
|
dd {
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--c-subtle-surface-color, #f7f8fa);
|
||||||
|
border-inline-end: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,236 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { CIcon, CSeparator } from '@/index'
|
||||||
|
import CCodeBlock from '@/documentation/CCodeBlock.vue'
|
||||||
|
|
||||||
|
import { emojiCatalog, emojiCatalogVersion } from './emoji-catalog'
|
||||||
|
import type { EmojiCatalogEntry } from './emoji-catalog'
|
||||||
|
import { emojiSearchAliases } from './emoji-search-aliases'
|
||||||
|
|
||||||
|
const pageSize = 120
|
||||||
|
const search = ref('')
|
||||||
|
const selectedGroup = ref('All')
|
||||||
|
const selectedSubgroup = ref('All')
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const copyStatus = ref('')
|
||||||
|
|
||||||
|
const emojiUsage = `<CIcon>🚚</CIcon>
|
||||||
|
<CIcon display="circle" size="20px">📦</CIcon>
|
||||||
|
<CIcon :rotate="-0.25" label="Launching">🚀</CIcon>`
|
||||||
|
|
||||||
|
const unicodeGroups = Array.from(new Set(emojiCatalog.map((entry) => entry.group)))
|
||||||
|
const groups = ['All', ...unicodeGroups]
|
||||||
|
|
||||||
|
const groupCounts = new Map<string, number>()
|
||||||
|
groupCounts.set('All', emojiCatalog.length)
|
||||||
|
|
||||||
|
for (const entry of emojiCatalog) {
|
||||||
|
groupCounts.set(entry.group, (groupCounts.get(entry.group) ?? 0) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const subgroups = computed(() => {
|
||||||
|
if (selectedGroup.value === 'All') return ['All']
|
||||||
|
|
||||||
|
return [
|
||||||
|
'All',
|
||||||
|
...new Set(
|
||||||
|
emojiCatalog
|
||||||
|
.filter((entry) => entry.group === selectedGroup.value)
|
||||||
|
.map((entry) => entry.subgroup),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredEntries = computed(() => {
|
||||||
|
const query = search.value.trim().toLocaleLowerCase()
|
||||||
|
|
||||||
|
return emojiCatalog.filter((entry) => {
|
||||||
|
if (selectedGroup.value !== 'All' && entry.group !== selectedGroup.value) return false
|
||||||
|
if (selectedSubgroup.value !== 'All' && entry.subgroup !== selectedSubgroup.value) return false
|
||||||
|
if (!query) return true
|
||||||
|
|
||||||
|
const aliases = emojiSearchAliases[entry.name] ?? []
|
||||||
|
|
||||||
|
return (
|
||||||
|
entry.name.toLocaleLowerCase().includes(query) ||
|
||||||
|
entry.group.toLocaleLowerCase().includes(query) ||
|
||||||
|
entry.subgroup.toLocaleLowerCase().includes(query) ||
|
||||||
|
aliases.some((alias) => alias.includes(query))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const pageCount = computed(() => Math.max(1, Math.ceil(filteredEntries.value.length / pageSize)))
|
||||||
|
|
||||||
|
const pageEntries = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize
|
||||||
|
return filteredEntries.value.slice(start, start + pageSize)
|
||||||
|
})
|
||||||
|
|
||||||
|
const groupedPageEntries = computed(() => {
|
||||||
|
const sections: Array<{
|
||||||
|
key: string
|
||||||
|
group: string
|
||||||
|
subgroup: string
|
||||||
|
entries: EmojiCatalogEntry[]
|
||||||
|
}> = []
|
||||||
|
|
||||||
|
for (const entry of pageEntries.value) {
|
||||||
|
const key = `${entry.group}/${entry.subgroup}`
|
||||||
|
let section = sections.at(-1)
|
||||||
|
|
||||||
|
if (section?.key !== key) {
|
||||||
|
section = { key, group: entry.group, subgroup: entry.subgroup, entries: [] }
|
||||||
|
sections.push(section)
|
||||||
|
}
|
||||||
|
|
||||||
|
section.entries.push(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections
|
||||||
|
})
|
||||||
|
|
||||||
|
function selectGroup(group: string) {
|
||||||
|
selectedGroup.value = group
|
||||||
|
selectedSubgroup.value = 'All'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyEmoji(entry: EmojiCatalogEntry) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(entry.emoji)
|
||||||
|
copyStatus.value = `Copied ${entry.emoji} — ${entry.name}`
|
||||||
|
} catch {
|
||||||
|
copyStatus.value = 'Copy failed. Select the emoji text and copy it manually.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([selectedGroup, selectedSubgroup, search], () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(search, (value) => {
|
||||||
|
if (!value.trim()) return
|
||||||
|
selectedGroup.value = 'All'
|
||||||
|
selectedSubgroup.value = 'All'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="glyph-page">
|
||||||
|
<header class="page-header">
|
||||||
|
<div>
|
||||||
|
<p class="category">Visual</p>
|
||||||
|
<h1>Emoji</h1>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Unicode Emoji {{ emojiCatalogVersion }} contains {{ emojiCatalog.length.toLocaleString() }}
|
||||||
|
emoji characters and sequences. Render them with <code>CIcon</code>; artwork and color
|
||||||
|
depend on the operating system.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<CSeparator />
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Unicode catalog</h2>
|
||||||
|
<p>
|
||||||
|
Browse the official Unicode groups and subgroups, then copy an emoji directly into a
|
||||||
|
<code>CIcon</code> default slot.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="category-grid" aria-label="Emoji categories">
|
||||||
|
<button
|
||||||
|
v-for="group in groups"
|
||||||
|
:key="group"
|
||||||
|
type="button"
|
||||||
|
class="category-button"
|
||||||
|
:class="{ 'is-active': selectedGroup === group }"
|
||||||
|
:aria-pressed="selectedGroup === group"
|
||||||
|
@click="selectGroup(group)"
|
||||||
|
>
|
||||||
|
<span>{{ group }}</span>
|
||||||
|
<span class="count">{{ groupCounts.get(group) }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filters">
|
||||||
|
<label>
|
||||||
|
<span>Search</span>
|
||||||
|
<input v-model="search" type="search" placeholder="Name, group, or subgroup" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span>Subgroup</span>
|
||||||
|
<select v-model="selectedSubgroup" :disabled="selectedGroup === 'All'">
|
||||||
|
<option v-for="subgroup in subgroups" :key="subgroup" :value="subgroup">
|
||||||
|
{{ subgroup }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="result-count">
|
||||||
|
{{ filteredEntries.length.toLocaleString() }} result<span v-if="filteredEntries.length !== 1">s</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="groupedPageEntries.length" class="catalog">
|
||||||
|
<section v-for="section in groupedPageEntries" :key="section.key" class="subgroup">
|
||||||
|
<header class="subgroup-header">
|
||||||
|
<strong>{{ section.subgroup }}</strong>
|
||||||
|
<span>{{ section.group }}</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="grid is-wide">
|
||||||
|
<button
|
||||||
|
v-for="entry in section.entries"
|
||||||
|
:key="`${entry.emoji}-${entry.name}`"
|
||||||
|
type="button"
|
||||||
|
class="card interactive"
|
||||||
|
:title="`Copy ${entry.name} · Emoji ${entry.version}`"
|
||||||
|
@click="copyEmoji(entry)"
|
||||||
|
>
|
||||||
|
<CIcon size="22px">{{ entry.emoji }}</CIcon>
|
||||||
|
<span class="details">
|
||||||
|
<span>{{ entry.name }}</span>
|
||||||
|
<code>E{{ entry.version }}</code>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-else class="empty">No emoji match the current filters.</p>
|
||||||
|
|
||||||
|
<p class="copy-status" aria-live="polite">{{ copyStatus }}</p>
|
||||||
|
|
||||||
|
<nav v-if="pageCount > 1" class="pagination" aria-label="Emoji catalog pages">
|
||||||
|
<button type="button" :disabled="currentPage === 1" @click="currentPage--">Previous</button>
|
||||||
|
<span>Page {{ currentPage }} of {{ pageCount }}</span>
|
||||||
|
<button type="button" :disabled="currentPage === pageCount" @click="currentPage++">Next</button>
|
||||||
|
</nav>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Presentation</h2>
|
||||||
|
<p>Paste the selected character directly into the component's default slot.</p>
|
||||||
|
<CCodeBlock :code="emojiUsage" />
|
||||||
|
<div class="example-row">
|
||||||
|
<div class="example">
|
||||||
|
<CIcon size="16px">🍇</CIcon>
|
||||||
|
<code><CIcon>🍇</CIcon></code>
|
||||||
|
</div>
|
||||||
|
<div class="example">
|
||||||
|
<CIcon display="circle" size="18px">📦</CIcon>
|
||||||
|
<code>18px circle</code>
|
||||||
|
</div>
|
||||||
|
<div class="example">
|
||||||
|
<CIcon size="24px" :rotate="-0.25" label="Launching">🚀</CIcon>
|
||||||
|
<code>:rotate="-0.25"</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss" src="./glyph-demo.scss"></style>
|
||||||
@ -0,0 +1,492 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import CCodeBlock from '@/documentation/CCodeBlock.vue'
|
||||||
|
import { CMenu, CSeparator, CSideBar } from '@/index'
|
||||||
|
import type { CMenuEntry, CMenuSelectEvent } from '@/index'
|
||||||
|
|
||||||
|
const lastAction = ref('No item selected')
|
||||||
|
const sidebarCollapsed = ref(false)
|
||||||
|
|
||||||
|
const navigation: CMenuEntry[] = [
|
||||||
|
{ label: 'Dashboard', icon: '⌂', active: true },
|
||||||
|
{
|
||||||
|
label: 'Sales',
|
||||||
|
icon: '∑',
|
||||||
|
children: [
|
||||||
|
{ label: 'Orders', icon: '▤' },
|
||||||
|
{ label: 'Customers', icon: '👥' },
|
||||||
|
{
|
||||||
|
label: 'Reports',
|
||||||
|
icon: '▦',
|
||||||
|
children: [
|
||||||
|
{ label: 'Daily Report', icon: '▤' },
|
||||||
|
{ label: 'Monthly Report', icon: '▤' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Administration',
|
||||||
|
icon: '⚙︎',
|
||||||
|
children: [
|
||||||
|
{ label: 'Users', icon: '👥' },
|
||||||
|
{ label: 'System Settings', icon: '⚙︎' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const sidebarUsage = `<CSideBar
|
||||||
|
v-model:collapsed="sidebarCollapsed"
|
||||||
|
collapsible
|
||||||
|
width="250px"
|
||||||
|
aria-label="Application navigation"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<strong>Company workspace</strong>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<CMenu
|
||||||
|
:items="navigation"
|
||||||
|
orientation="vertical"
|
||||||
|
submenu-mode="inline"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template #footer>Administrator</template>
|
||||||
|
</CSideBar>`
|
||||||
|
|
||||||
|
const collapsibleUsage = `<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const sidebarCollapsed = ref(false)
|
||||||
|
<\/script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CSideBar
|
||||||
|
collapsible
|
||||||
|
v-model:collapsed="sidebarCollapsed"
|
||||||
|
collapsed-width="38px"
|
||||||
|
aria-label="Application navigation"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<strong>Company workspace</strong>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<CMenu :items="navigation" submenu-mode="inline" />
|
||||||
|
</CSideBar>
|
||||||
|
</template>`
|
||||||
|
|
||||||
|
const placementUsage = `<div class="workspace">
|
||||||
|
<CSideBar placement="start" width="180px" aria-label="Navigation">
|
||||||
|
<template #header><strong>Navigation</strong></template>
|
||||||
|
Main menu
|
||||||
|
</CSideBar>
|
||||||
|
|
||||||
|
<main>Application content</main>
|
||||||
|
|
||||||
|
<CSideBar placement="end" width="180px" aria-label="Properties">
|
||||||
|
<template #header><strong>Properties</strong></template>
|
||||||
|
Selection details
|
||||||
|
</CSideBar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.workspace {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace > main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
</style>`
|
||||||
|
|
||||||
|
const backgroundUsage = `<CSideBar style="background: #dbe8f5">
|
||||||
|
Solid color
|
||||||
|
</CSideBar>
|
||||||
|
|
||||||
|
<CSideBar style="background: linear-gradient(180deg, #ffffff, #e7eef6)">
|
||||||
|
Gradient
|
||||||
|
</CSideBar>
|
||||||
|
|
||||||
|
<CSideBar class="image-sidebar">
|
||||||
|
Background image
|
||||||
|
</CSideBar>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.image-sidebar {
|
||||||
|
--c-text-color: #fff;
|
||||||
|
background: center / cover no-repeat url('/images/sidebar.jpg');
|
||||||
|
}
|
||||||
|
</style>`
|
||||||
|
|
||||||
|
const sidebarJavaScript = `import { ref } from 'vue'
|
||||||
|
|
||||||
|
const sidebarCollapsed = ref(false)
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
{
|
||||||
|
label: 'Dashboard',
|
||||||
|
icon: '⌂',
|
||||||
|
url: '/dashboard',
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Sales',
|
||||||
|
icon: '∑',
|
||||||
|
children: [
|
||||||
|
{ label: 'Orders', icon: '▤', url: '/sales/orders' },
|
||||||
|
{ label: 'Customers', icon: '👥', url: '/sales/customers' },
|
||||||
|
{
|
||||||
|
label: 'Reports',
|
||||||
|
icon: '▦',
|
||||||
|
children: [
|
||||||
|
{ label: 'Daily Report', icon: '▤', url: '/sales/reports/daily' },
|
||||||
|
{ label: 'Monthly Report', icon: '▤', url: '/sales/reports/monthly' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Administration',
|
||||||
|
icon: '⚙︎',
|
||||||
|
children: [
|
||||||
|
{ label: 'Users', icon: '👥', url: '/admin/users' },
|
||||||
|
{ label: 'System Settings', icon: '⚙︎', url: '/admin/settings' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]`
|
||||||
|
|
||||||
|
function recordSelection(event: CMenuSelectEvent) {
|
||||||
|
lastAction.value = event.item.label
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="sidebar-page">
|
||||||
|
<header class="page-header">
|
||||||
|
<div>
|
||||||
|
<p class="category">Layout</p>
|
||||||
|
<h1>Sidebar</h1>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<code>CSideBar</code> provides header, scrollable content, and footer regions for compact
|
||||||
|
desktop application navigation.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<CSeparator />
|
||||||
|
|
||||||
|
<section class="example">
|
||||||
|
<div class="description">
|
||||||
|
<h2>Navigation sidebar</h2>
|
||||||
|
<p>
|
||||||
|
A vertical menu expands inline while the surrounding application content remains fixed.
|
||||||
|
Multiple branches can remain expanded.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview">
|
||||||
|
<CSideBar
|
||||||
|
v-model:collapsed="sidebarCollapsed"
|
||||||
|
collapsible
|
||||||
|
width="250px"
|
||||||
|
aria-label="Example sidebar"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<strong>Company workspace</strong>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<CMenu
|
||||||
|
:items="navigation"
|
||||||
|
orientation="vertical"
|
||||||
|
submenu-mode="inline"
|
||||||
|
aria-label="Example sidebar navigation"
|
||||||
|
@select="recordSelection"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="user">Administrator</span>
|
||||||
|
</template>
|
||||||
|
</CSideBar>
|
||||||
|
|
||||||
|
<div class="placeholder">
|
||||||
|
<strong>Application content</strong>
|
||||||
|
<span>Select or expand items in the sidebar.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status" aria-live="polite">Last selection: {{ lastAction }}</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="sidebarUsage" />
|
||||||
|
<CCodeBlock class="code-sample" :code="sidebarJavaScript" language="javascript" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="example">
|
||||||
|
<div class="description">
|
||||||
|
<h2>Collapsible sidebar</h2>
|
||||||
|
<p>
|
||||||
|
Add <code>collapsible</code> to show a boxed menu button before the header content. The
|
||||||
|
sidebar slides between its expanded and collapsed widths and keeps its menu state,
|
||||||
|
including expanded menu branches.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="property-list">
|
||||||
|
<div>
|
||||||
|
<dt><code>collapsible</code></dt>
|
||||||
|
<dd>Enables the header toggle. Without it, the sidebar always remains expanded.</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><code>collapsed</code></dt>
|
||||||
|
<dd>
|
||||||
|
Controls the collapsed state through <code>v-model:collapsed</code>. The model is
|
||||||
|
optional; without it, <code>CSideBar</code> manages its own state.
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><code>collapsed-width</code></dt>
|
||||||
|
<dd>Sets the width of the collapsed toggle strip. The default is <code>38px</code>.</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<CCodeBlock class="code-sample" :code="collapsibleUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="example">
|
||||||
|
<div class="description">
|
||||||
|
<h2>Placement</h2>
|
||||||
|
<p>
|
||||||
|
Use the default <code>placement="start"</code> for navigation before the main content and
|
||||||
|
<code>placement="end"</code> for tools or details after it. These are logical directions:
|
||||||
|
start is left and end is right in left-to-right layouts, with the meaning reversed in
|
||||||
|
right-to-left layouts.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Placement controls which inner edge receives the divider. The parent flex or grid layout
|
||||||
|
and the sidebar's document order determine its actual position. For the usual
|
||||||
|
side-by-side application shell, give the parent <code>display: flex</code> and allow the
|
||||||
|
main content to grow with <code>flex: 1</code>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="placement-preview">
|
||||||
|
<CSideBar placement="start" width="180px" aria-label="Example navigation sidebar">
|
||||||
|
<template #header><strong>Navigation</strong></template>
|
||||||
|
Main menu
|
||||||
|
</CSideBar>
|
||||||
|
|
||||||
|
<main class="placement-content">Application content</main>
|
||||||
|
|
||||||
|
<CSideBar placement="end" width="180px" aria-label="Example properties sidebar">
|
||||||
|
<template #header><strong>Properties</strong></template>
|
||||||
|
Selection details
|
||||||
|
</CSideBar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CCodeBlock class="code-sample" :code="placementUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="example">
|
||||||
|
<div class="description">
|
||||||
|
<h2>Custom backgrounds</h2>
|
||||||
|
<p>
|
||||||
|
Apply standard <code>class</code> or <code>style</code> attributes to customize the
|
||||||
|
sidebar with any valid CSS background, including a color, gradient, or image.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For dark backgrounds, set <code>--c-text-color</code> so sidebar content and nested
|
||||||
|
Concise UI components use a suitable foreground color.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="background-preview">
|
||||||
|
<CSideBar
|
||||||
|
class="solid-background"
|
||||||
|
width="200px"
|
||||||
|
aria-label="Solid background example"
|
||||||
|
>
|
||||||
|
<template #header><strong>Solid color</strong></template>
|
||||||
|
#dbe8f5
|
||||||
|
</CSideBar>
|
||||||
|
<CSideBar
|
||||||
|
class="gradient-background"
|
||||||
|
width="200px"
|
||||||
|
aria-label="Gradient background example"
|
||||||
|
>
|
||||||
|
<template #header><strong>Gradient</strong></template>
|
||||||
|
linear-gradient(...)
|
||||||
|
</CSideBar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CCodeBlock class="code-sample" :code="backgroundUsage" />
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.sidebar-page {
|
||||||
|
width: min(100%, 1100px);
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 0.7fr) minmax(320px, 1.3fr);
|
||||||
|
align-items: end;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
gap: 32px;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
color: var(--c-primary-color, #2f6fad);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example {
|
||||||
|
padding: 22px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
display: flex;
|
||||||
|
height: 360px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--c-surface-color, #fff);
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
background: var(--c-subtle-surface-color, #f7f8fa);
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
padding: 6px 9px;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 12px;
|
||||||
|
background: var(--c-subtle-surface-color, #f7f8fa);
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-sample {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-list {
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 130px minmax(0, 1fr);
|
||||||
|
|
||||||
|
+ div {
|
||||||
|
border-top: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dt,
|
||||||
|
dd {
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--c-subtle-surface-color, #f7f8fa);
|
||||||
|
border-inline-end: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.placement-preview {
|
||||||
|
display: flex;
|
||||||
|
height: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--c-surface-color, #fff);
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
.placement-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
background: var(--c-subtle-surface-color, #f7f8fa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-preview {
|
||||||
|
display: flex;
|
||||||
|
height: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.c-side-bar {
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
.solid-background {
|
||||||
|
background: #dbe8f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-background {
|
||||||
|
--c-text-color: #fff;
|
||||||
|
background: linear-gradient(180deg, #34495e, #1f2d3a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,8 @@
|
|||||||
|
export const emojiSearchAliases: Record<string, readonly string[]> = {
|
||||||
|
'floppy disk': ['disk', 'save', 'storage'],
|
||||||
|
wastebasket: ['trash', 'trash can', 'delete', 'bin', 'recycle bin'],
|
||||||
|
'file folder': ['folder', 'directory', 'closed folder'],
|
||||||
|
'open file folder': ['folder', 'directory', 'open folder'],
|
||||||
|
'bust in silhouette': ['user', 'person', 'account', 'profile'],
|
||||||
|
'busts in silhouette': ['users', 'people', 'group', 'team', 'accounts'],
|
||||||
|
}
|
||||||
@ -0,0 +1,359 @@
|
|||||||
|
.glyph-page {
|
||||||
|
width: min(100%, 980px);
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(180px, 0.7fr) minmax(320px, 1.3fr);
|
||||||
|
align-items: end;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
gap: 32px;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
color: var(--c-primary-color, #2f6fad);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 22px 0;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .code-block {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
||||||
|
margin-top: 12px;
|
||||||
|
border-top: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
border-inline-start: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
|
||||||
|
&.is-wide {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(185px, 1fr));
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 7px 9px;
|
||||||
|
gap: 9px;
|
||||||
|
background: var(--c-surface-color, #fff);
|
||||||
|
border-right: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
border-bottom: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
|
||||||
|
> code {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 12px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.interactive {
|
||||||
|
min-height: 48px;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: start;
|
||||||
|
cursor: pointer;
|
||||||
|
border-top: 0;
|
||||||
|
border-left: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--c-hover-color, #eef1f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
outline: 2px solid var(--c-focus-color, #3578c6);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.25;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-top: 12px;
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 180px;
|
||||||
|
min-height: 54px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
+ .example {
|
||||||
|
border-inline-start: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dark {
|
||||||
|
background: #343a43;
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: #e8ebef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--c-subtle-surface-color, #f7f8fa);
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 30px;
|
||||||
|
padding: 4px 7px;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
background: var(--c-surface-color, #fff);
|
||||||
|
border: 1px solid var(--c-border-color, #bfc5ce);
|
||||||
|
border-radius: var(--c-border-radius, 3px);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
border-color: var(--c-focus-color, #3578c6);
|
||||||
|
outline: 2px solid var(--c-focus-color, #3578c6);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
width: 190px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-count {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
padding: 0 3px 6px;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 12px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-section {
|
||||||
|
+ .catalog-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
margin-top: 12px;
|
||||||
|
border-top: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
border-inline-start: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: start;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--c-surface-color, #fff);
|
||||||
|
border: 0;
|
||||||
|
border-right: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
border-bottom: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--c-hover-color, #eef1f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
outline: 2px solid var(--c-focus-color, #3578c6);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: var(--c-selection-text-color, #173f70);
|
||||||
|
background: var(--c-selection-color, #dceafa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 11px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup {
|
||||||
|
+ .subgroup {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
padding: 28px;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-status {
|
||||||
|
min-height: 18px;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
|
margin-top: 10px;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font-size: 12px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 30px;
|
||||||
|
padding: 4px 7px;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--c-surface-color, #fff);
|
||||||
|
border: 1px solid var(--c-border-color, #bfc5ce);
|
||||||
|
border-radius: var(--c-border-radius, 3px);
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--c-focus-color, #3578c6);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: var(--c-disabled-text-color, #8a9099);
|
||||||
|
cursor: default;
|
||||||
|
background: var(--c-subtle-surface-color, #f7f8fa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
import ComponentsLayout from '@/layouts/ComponentsLayout.vue'
|
||||||
|
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||||||
|
import HomePage from '@/pages/_Home.vue'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: DefaultLayout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'home',
|
||||||
|
component: HomePage,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/components',
|
||||||
|
component: ComponentsLayout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirect: { name: 'app-bar-menu' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'app-bar-menu',
|
||||||
|
name: 'app-bar-menu',
|
||||||
|
component: () => import('@/pages/components/AppBarMenu.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sidebar',
|
||||||
|
name: 'sidebar',
|
||||||
|
component: () => import('@/pages/components/Sidebar.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'button',
|
||||||
|
name: 'button',
|
||||||
|
component: () => import('@/pages/components/Button.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'icon',
|
||||||
|
name: 'icon',
|
||||||
|
component: () => import('@/pages/components/Icon.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'emoji',
|
||||||
|
name: 'emoji',
|
||||||
|
component: () => import('@/pages/components/Emoji.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
:root {
|
||||||
|
color: #20242a;
|
||||||
|
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #f4f6f8;
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-layout__brand {
|
||||||
|
padding: 0 8px 0 2px;
|
||||||
|
color: #20242a;
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-layout__workspace {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-layout__main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 24px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-layout__main--with-sidebar {
|
||||||
|
padding: 22px 24px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-button {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
color: #20242a;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #bfc5ce;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-button:hover {
|
||||||
|
background: #eef1f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-button--primary {
|
||||||
|
color: #fff;
|
||||||
|
background: #286aa6;
|
||||||
|
border-color: #245f95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-button--primary:hover {
|
||||||
|
background: #245f95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-button:focus-visible,
|
||||||
|
.demo-layout__brand:focus-visible {
|
||||||
|
outline: 2px solid #3578c6;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue