Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions src/prototypes/example-codex-kitchen-sink/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<script setup lang="ts">
import { ref, type Component } from 'vue'
import { CdxTab, CdxTabs, CdxToastContainer } from '@wikimedia/codex'

import { parseLeafTab, type MainTabId } from './lib/playground-tabs'
import {
providePlaygroundLeafTab,
syncMainTabWithLeafTab,
usePlaygroundLeafTab,
} from './lib/use-playground-leaf-tab'

import ButtonsSection from './sections/ButtonsSection.vue'
import FeedbackSection from './sections/FeedbackSection.vue'
import IconsSection from './sections/IconsSection.vue'
import FormElementsSection from './sections/FormElementsSection.vue'
import MediaSection from './sections/MediaSection.vue'
import NavigationSection from './sections/NavigationSection.vue'
import SearchSection from './sections/SearchSection.vue'
import ContentDataSection from './sections/ContentDataSection.vue'
import ColorSection from './sections/ColorSection.vue'
import TokenSection from './sections/TokenSection.vue'
import TypographySection from './sections/TypographySection.vue'

definePage({
meta: {
title: 'Codex playground',
description: 'Full Codex component, token, typography, and icon catalogue.',
},
})

const navItems = [
{ id: 'typography', label: 'Typography' },
{ id: 'color', label: 'Color' },
{ id: 'tokens-layout', label: 'Layout' },
{ id: 'tokens-appearance', label: 'Appearance' },
{ id: 'tokens-animation', label: 'Animation' },
{ id: 'icons', label: 'Icons' },
{ id: 'components-buttons', label: 'Buttons' },
{ id: 'components-form-elements', label: 'Form elements' },
{ id: 'components-feedback', label: 'Feedback' },
{ id: 'components-content-data', label: 'Content & data' },
{ id: 'components-media', label: 'Media' },
{ id: 'components-navigation', label: 'Navigation' },
{ id: 'components-search', label: 'Search' },
] as const

const sectionByTab: Record<(typeof navItems)[number]['id'], Component> = {
typography: TypographySection,
color: ColorSection,
'tokens-layout': TokenSection,
'tokens-appearance': TokenSection,
'tokens-animation': TokenSection,
icons: IconsSection,
'components-buttons': ButtonsSection,
'components-form-elements': FormElementsSection,
'components-feedback': FeedbackSection,
'components-content-data': ContentDataSection,
'components-media': MediaSection,
'components-navigation': NavigationSection,
'components-search': SearchSection,
}

const tokenSectionByTab: Partial<Record<(typeof navItems)[number]['id'], 'layout' | 'appearance' | 'animation'>> = {
'tokens-layout': 'layout',
'tokens-appearance': 'appearance',
'tokens-animation': 'animation',
}

type TabId = MainTabId

const leafTab = usePlaygroundLeafTab()
const { subTabMemory } = providePlaygroundLeafTab(leafTab)

const activeTab = ref<TabId>(parseLeafTab(leafTab.value)?.main ?? 'typography')
syncMainTabWithLeafTab(activeTab, leafTab, subTabMemory)
</script>

<template>
<main>
<!-- <h1>Codex playground</h1> -->

<CdxTabs v-model:active="activeTab" class="playground-tabs" aria-label="Sections">
<CdxTab v-for="item in navItems" :key="item.id" :name="item.id" :label="item.label">
<article>
<TokenSection
v-if="tokenSectionByTab[item.id]"
:section="tokenSectionByTab[item.id]!"
/>
<component v-else :is="sectionByTab[item.id]" />
</article>
</CdxTab>
</CdxTabs>

<CdxToastContainer />
</main>
</template>

<style scoped>
main {
/* padding: 6px 0px; */
background-color: var(--background-color-base);
}

.playground-tabs {
/* padding: 0px var(--spacing-100); */
/* margin: 0px 0px; */
/* padding: 0px 4px; */
/* padding-top: 4px; */
/* background-color: var(--background-color-base); */
}

.playground-tabs :deep(> .cdx-tabs__header) {
position: sticky;
top: 0px;
z-index: 2;
background-color: var(--background-color-base);
padding-top: 4px;
margin: 0px 0px;
padding-left: 4px;
padding-right: 4px;
}

article {
/* padding: 0px 16px; */
}
</style>
99 changes: 99 additions & 0 deletions src/prototypes/example-codex-kitchen-sink/lib/color-contrast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
interface Rgba {
r: number
g: number
b: number
a: number
}

function parseColor(color: string): Rgba | null {
if (!color || color === 'transparent') {
return { r: 0, g: 0, b: 0, a: 0 }
}

const rgbaMatch = color.match(
/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:[,\s/]+([\d.]+))?\s*\)/,
)
if (rgbaMatch) {
return {
r: Number(rgbaMatch[1]),
g: Number(rgbaMatch[2]),
b: Number(rgbaMatch[3]),
a: rgbaMatch[4] !== undefined ? Number(rgbaMatch[4]) : 1,
}
}

const hexMatch = color.match(/^#([0-9a-f]{3,8})$/i)
if (!hexMatch) return null

let hex = hexMatch[1]
if (hex.length === 3) {
hex = hex
.split('')
.map((char) => char + char)
.join('')
}

if (hex.length < 6) return null

return {
r: parseInt(hex.slice(0, 2), 16),
g: parseInt(hex.slice(2, 4), 16),
b: parseInt(hex.slice(4, 6), 16),
a: 1,
}
}

function relativeLuminance(r: number, g: number, b: number): number {
const channel = (value: number) => {
const normalized = value / 255
return normalized <= 0.03928
? normalized / 12.92
: ((normalized + 0.055) / 1.055) ** 2.4
}

return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)
}

function compositeOver(fg: Rgba, bg: Rgba): [number, number, number] {
if (fg.a <= 0) return [bg.r, bg.g, bg.b]
if (fg.a >= 1) return [fg.r, fg.g, fg.b]

const backdropAlpha = bg.a * (1 - fg.a)
const outAlpha = fg.a + backdropAlpha
if (outAlpha <= 0) return [bg.r, bg.g, bg.b]

return [
Math.round((fg.r * fg.a + bg.r * backdropAlpha) / outAlpha),
Math.round((fg.g * fg.a + bg.g * backdropAlpha) / outAlpha),
Math.round((fg.b * fg.a + bg.b * backdropAlpha) / outAlpha),
]
}

function getCanvasColor(): Rgba | null {
return parseColor(getComputedStyle(document.documentElement).backgroundColor)
}

function resolvedBackgroundLuminance(element: HTMLElement): number | null {
const canvasColor = getCanvasColor()
const backgroundColor = parseColor(getComputedStyle(element).backgroundColor)
if (!backgroundColor || !canvasColor) return null

const [r, g, b] = compositeOver(backgroundColor, canvasColor)
return relativeLuminance(r, g, b)
}

export type SwatchTextTone = 'light' | 'dark'

export function getSwatchTextToneForBackground(element: HTMLElement): SwatchTextTone {
const luminance = resolvedBackgroundLuminance(element)
if (luminance === null) return 'dark'

return luminance > 0.5 ? 'dark' : 'light'
}

export function needsInvertedTextBackground(element: HTMLElement): boolean {
const color = parseColor(getComputedStyle(element).color)
if (!color) return false

return relativeLuminance(color.r, color.g, color.b) > 0.65
}
70 changes: 70 additions & 0 deletions src/prototypes/example-codex-kitchen-sink/lib/component-tabs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
export const buttonsSubTabs = [
{ id: 'button', label: 'Button' },
{ id: 'button-group', label: 'ButtonGroup' },
{ id: 'menu-button', label: 'MenuButton' },
{ id: 'toggle-button', label: 'ToggleButton' },
{ id: 'toggle-button-group', label: 'ToggleButtonGroup' },
] as const

export const formElementsSubTabs = [
{ id: 'checkbox', label: 'Checkbox' },
{ id: 'chip-input', label: 'ChipInput' },
{ id: 'combobox', label: 'Combobox' },
{ id: 'field', label: 'Field' },
{ id: 'label', label: 'Label' },
{ id: 'lookup', label: 'Lookup' },
{ id: 'multiselect-lookup', label: 'MultiselectLookup' },
{ id: 'radio', label: 'Radio' },
{ id: 'select', label: 'Select' },
{ id: 'text-area', label: 'TextArea' },
{ id: 'text-input', label: 'TextInput' },
{ id: 'toggle-switch', label: 'ToggleSwitch' },
] as const

export const contentDataSubTabs = [
{ id: 'accordion', label: 'Accordion' },
{ id: 'card', label: 'Card' },
{ id: 'dialog', label: 'Dialog' },
{ id: 'menu', label: 'Menu' },
{ id: 'menu-item', label: 'MenuItem' },
{ id: 'popover', label: 'Popover' },
{ id: 'table', label: 'Table' },
{ id: 'tooltip', label: 'Tooltip' },
] as const

export const feedbackSubTabs = [
{ id: 'info-chip', label: 'InfoChip' },
{ id: 'message', label: 'Message' },
{ id: 'progress-bar', label: 'ProgressBar' },
{ id: 'progress-indicator', label: 'ProgressIndicator' },
{ id: 'toast', label: 'Toast' },
] as const

export const mediaSubTabs = [
{ id: 'image', label: 'Image' },
{ id: 'thumbnail', label: 'Thumbnail' },
] as const

export const navigationSubTabs = [
{ id: 'link', label: 'Link' },
{ id: 'tabs', label: 'Tabs' },
] as const

export const searchSubTabs = [
{ id: 'search-input', label: 'SearchInput' },
{ id: 'typeahead-search', label: 'TypeaheadSearch' },
{ id: 'search-result-title', label: 'SearchResultTitle' },
] as const

export const iconsSubTabs = [
{ id: 'size', label: 'Size' },
{ id: 'type', label: 'Type' },
] as const

/** Codex 2.x IconSizes — xx-small was removed and no longer applies sizing. */
export const iconSizeEntries = [
{ id: 'x-small', deprecated: false },
{ id: 'small', deprecated: false },
{ id: 'medium', deprecated: false },
{ id: 'xx-small', deprecated: true },
] as const
Loading
Loading