feat(ui): add base component tokens and primitives

This commit is contained in:
xiaohan.yuan committed 2026-10-18 19:11:38 +08:00
1 parent febb28aa6f
commit 0284603847
10 files changed
+1130

No files matched your search

+4
View File
@@ -4,6 +4,10 @@ import App from './App'
import { SettingsWindow } from './components/SettingsDialog' import { SettingsWindow } from './components/SettingsDialog'
import './workbuddy/typography/tokens.css' import './workbuddy/typography/tokens.css'
import './styles/tokens.css' import './styles/tokens.css'
import './ui/tokens.css'
import './ui/button.css'
import './ui/input.css'
import './ui/switch.css'
import './styles/app.css' import './styles/app.css'
import './styles/sidebar.css' import './styles/sidebar.css'
import './workbuddy/conversation/source-styles.css' import './workbuddy/conversation/source-styles.css'
+4
View File
@@ -3,6 +3,10 @@ import { createRoot } from 'react-dom/client'
import { SettingsWindow } from './components/SettingsDialog' import { SettingsWindow } from './components/SettingsDialog'
import './workbuddy/typography/tokens.css' import './workbuddy/typography/tokens.css'
import './styles/tokens.css' import './styles/tokens.css'
import './ui/tokens.css'
import './ui/button.css'
import './ui/input.css'
import './ui/switch.css'
import './styles/app.css' import './styles/app.css'
import './styles/settings-entry.css' import './styles/settings-entry.css'
+82
View File
@@ -0,0 +1,82 @@
import { Children, type ButtonHTMLAttributes, type ReactNode, type Ref } from 'react'
import './button.css'
export type ButtonVariant = 'primary' | 'secondary' | 'grey' | 'ghost' | 'link' | 'danger'
export type ButtonSize = 'sm' | 'md' | 'lg'
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant
size?: ButtonSize
iconOnly?: boolean
loading?: boolean
iconLeft?: ReactNode
iconRight?: ReactNode
ref?: Ref<HTMLButtonElement>
}
/**
* 原版按钮的加载图标(16 图标栅格、8 辐条、currentColor);
* 尺寸由 button.css 按 size 覆盖,旋转由 .yc-button__loading-icon 驱动。
*/
function ButtonLoadingIcon(): ReactNode {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="yc-button__loading-icon">
<path
fill="currentColor"
fillRule="evenodd"
transform="matrix(1 0 0 1 2.07103 1.5)"
d="M6.529 0L6.529 4L5.329 4L5.329 0L6.529 0ZM8.394 5.7697L11.8581 3.7697L11.258 2.7305L7.7939 4.7305L8.394 5.7697ZM3.4641 5.7698L0 3.7698L0.6 2.7305L4.0641 4.7305L3.4641 5.7698ZM7.7939 8.2697L11.258 10.2697L11.8581 9.2305L8.394 7.2305L7.7939 8.2697ZM4.0641 8.2697L0.6 10.2697L0 9.2305L3.4641 7.2305L4.0641 8.2697ZM6.529 9L6.529 13L5.329 13L5.329 9L6.529 9Z"
/>
</svg>
)
}
/**
* 统一按钮。DOM 结构与原版按钮一致:
* iconLeft -> .yc-button__icon--left,children -> .yc-button__content
* (iconOnly 时 children 进 .yc-button__icon),loading -> .yc-button__loading。
*/
export function Button({
variant = 'secondary',
size = 'md',
iconOnly = false,
loading = false,
iconLeft,
iconRight,
className,
children,
type,
disabled,
ref,
'aria-busy': ariaBusy,
...rest
}: ButtonProps): React.JSX.Element {
const classes = ['yc-button', `yc-button--${variant}`, `yc-button--${size}`]
if (iconOnly) classes.push('yc-button--icon-only')
if (loading) classes.push('yc-button--loading')
if (disabled || loading) classes.push('yc-button--disabled')
if (className) classes.push(className)
const hasContent = Children.count(children) > 0
return (
<button
{...rest}
ref={ref}
type={type ?? 'button'}
className={classes.join(' ')}
disabled={disabled || loading}
aria-busy={loading ? true : ariaBusy}
>
{iconLeft ? <span className="yc-button__icon yc-button__icon--left">{iconLeft}</span> : null}
{!iconOnly && hasContent ? <span className="yc-button__content">{children}</span> : null}
{iconOnly && hasContent ? <span className="yc-button__icon">{children}</span> : null}
{iconRight ? <span className="yc-button__icon yc-button__icon--right">{iconRight}</span> : null}
{loading ? (
<span className="yc-button__loading" aria-hidden="true">
<ButtonLoadingIcon />
</span>
) : null}
</button>
)
}
+40
View File
@@ -0,0 +1,40 @@
import type { ReactNode } from 'react'
import { Button, type ButtonProps } from './Button'
/** 图标按钮没有文本,必须有可读名称:aria-label 或 title 至少给一个。 */
type IconButtonLabel =
| { 'aria-label': string; title?: string }
| { title: string; 'aria-label'?: string }
export type IconButtonProps = Omit<ButtonProps, 'iconOnly' | 'children'> &
IconButtonLabel & {
children: ReactNode
}
/**
* Button 的 iconOnly 语法糖:默认 variant="ghost"、size="md"。
*
* 缺少 aria-label 与 title 时:开发环境抛出明确错误;生产构建不渲染 DOM,
* 避免留下对读屏不可用的空白按钮(类型层已用联合类型要求二者之一)。
*/
export function IconButton({
variant = 'ghost',
size = 'md',
children,
'aria-label': ariaLabel,
title,
...rest
}: IconButtonProps): React.JSX.Element | null {
if (!ariaLabel && !title) {
const message = 'IconButton 需要 aria-label 或 title:图标按钮缺少可读名称。'
if (import.meta.env.DEV) throw new Error(message)
console.error(message)
return null
}
return (
<Button {...rest} iconOnly variant={variant} size={size} aria-label={ariaLabel} title={title}>
{children}
</Button>
)
}
+54
View File
@@ -0,0 +1,54 @@
/**
* YCode UI Input(P2-B)。
*
* 结构与几何来源:`docs/界面基础组件规范-2026-10-18.md` 的 Input 章节,
* 等价于设置页恢复样式 source-general-settings-recovered.css 中 `.wb-input-wrapper`
* (height 28/32/48、padding-x 8/8/12、radius 6/8/12、filled/disabled/error 态)。
* 只消费 ui/tokens.css 的 `--yc-*` L2 token,不写裸值。
*/
import type { InputHTMLAttributes, ReactNode } from 'react'
import './input.css'
export type InputSize = 'sm' | 'md' | 'lg'
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix'> {
size?: InputSize
invalid?: boolean
filled?: boolean
prefix?: ReactNode
suffix?: ReactNode
wrapperClassName?: string
}
function classNames(...values: Array<string | false | null | undefined>): string {
return values.filter(Boolean).join(' ')
}
export function Input({
size = 'md',
invalid = false,
filled = false,
prefix,
suffix,
wrapperClassName,
className,
disabled,
...inputProps
}: InputProps): React.JSX.Element {
const wrapperClass = classNames(
'yc-input',
`yc-input--${size}`,
filled && 'yc-input--filled',
invalid && 'yc-input--invalid',
disabled && 'yc-input--disabled',
wrapperClassName
)
return (
<div className={wrapperClass} data-disabled={disabled ? 'true' : undefined}>
{prefix ? <span className="yc-input__prefix">{prefix}</span> : null}
<input className={classNames('yc-input__inner', className)} disabled={disabled} {...inputProps} />
{suffix ? <span className="yc-input__suffix">{suffix}</span> : null}
</div>
)
}
+65
View File
@@ -0,0 +1,65 @@
/**
* YCode UI Switch(P2-B)。
*
* 结构与几何来源:`docs/界面基础组件规范-2026-10-18.md` 的 Switch 章节,
* 对应 GeneralSettingsPanel 本地 Switch 与恢复样式 source-general-settings-recovered.css
* 的 `.wb-switch*` 闭包(track 24×14 / 26×16 / 34×20,thumb 10 / 12 / 16)。
* 只消费 ui/tokens.css 的 `--yc-*` L2 token,不写裸值。
*/
import type { ChangeEvent } from 'react'
import './switch.css'
export type SwitchSize = 'sm' | 'md' | 'lg'
export interface SwitchProps {
checked: boolean
onCheckedChange?(checked: boolean): void
disabled?: boolean
readOnly?: boolean
size?: SwitchSize
'aria-label': string
className?: string
}
function classNames(...values: Array<string | false | null | undefined>): string {
return values.filter(Boolean).join(' ')
}
export function Switch({
checked,
onCheckedChange,
disabled = false,
readOnly = false,
size = 'md',
className,
'aria-label': ariaLabel
}: SwitchProps): React.JSX.Element {
const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
if (readOnly) return
onCheckedChange?.(event.target.checked)
}
return (
<span
className={classNames('yc-switch', `yc-switch--${size}`, className)}
data-disabled={disabled ? 'true' : undefined}
data-readonly={readOnly ? 'true' : undefined}
>
<span className="yc-switch__control">
<input
className="yc-switch__input"
type="checkbox"
role="switch"
aria-label={ariaLabel}
aria-readonly={readOnly || undefined}
checked={checked}
disabled={disabled}
onChange={handleChange}
/>
<span className="yc-switch__track">
<span className="yc-switch__thumb" />
</span>
</span>
</span>
)
}
+318
View File
@@ -0,0 +1,318 @@
/**
* YCode Button / IconButton(L2:--yc-button-*)。
*
* 由原版 .wb-button 的已验证闭包翻译:类名前缀 wb-button -> yc-button,
* 子元素沿用原结构(__icon / __content / __loading / __loading-icon)。
* 颜色与几何只消费 ui/tokens.css 的 --yc-button-* 与 styles/tokens.css 的
* L0/L1 token;不写裸值,自身不带外间距(间距由调用方决定)。
*/
.yc-button {
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--yc-button-gap-md);
border: none;
border-radius: var(--yc-button-radius-md);
font-family: var(--yc-font-sans);
font-weight: var(--yc-font-weight-500);
white-space: nowrap;
user-select: none;
cursor: pointer;
text-decoration: none;
outline: none;
transition:
background-color var(--yc-button-transition-duration) var(--yc-button-transition-easing),
color var(--yc-button-transition-duration) var(--yc-button-transition-easing),
border-color var(--yc-button-transition-duration) var(--yc-button-transition-easing),
transform var(--yc-button-transition-duration) var(--yc-button-transition-easing);
}
/* ---------------------------------------------------------------- 尺寸 */
.yc-button--sm {
height: var(--yc-button-height-sm);
padding: 0 var(--yc-button-padding-sm);
gap: var(--yc-button-gap-sm);
border-radius: var(--yc-button-radius-sm);
font-size: var(--yc-button-font-size-sm);
line-height: var(--yc-button-line-height-sm);
}
.yc-button--md {
height: var(--yc-button-height-md);
padding: 0 var(--yc-button-padding-md);
gap: var(--yc-button-gap-md);
border-radius: var(--yc-button-radius-md);
font-size: var(--yc-button-font-size-md);
line-height: var(--yc-button-line-height-md);
}
.yc-button--lg {
height: var(--yc-button-height-lg);
padding: 0 var(--yc-button-padding-lg);
gap: var(--yc-button-gap-lg);
border-radius: var(--yc-button-radius-lg);
font-size: var(--yc-button-font-size-lg);
line-height: var(--yc-button-line-height-lg);
}
.yc-button--icon-only {
padding: 0;
}
.yc-button--sm:not(.yc-button--icon-only):not(.yc-button--link) {
min-width: var(--yc-button-min-width-sm);
}
.yc-button--md:not(.yc-button--icon-only):not(.yc-button--link) {
min-width: var(--yc-button-min-width-md);
}
.yc-button--icon-only.yc-button--sm {
width: var(--yc-button-height-sm);
}
.yc-button--icon-only.yc-button--md {
width: var(--yc-button-height-md);
}
.yc-button--icon-only.yc-button--lg {
width: var(--yc-button-height-lg);
}
/* ------------------------------------------------------ 图标与文本容器 */
.yc-button__icon,
.yc-button__loading-icon {
width: var(--yc-button-icon-md);
height: var(--yc-button-icon-md);
}
.yc-button--sm :is(.yc-button__icon, .yc-button__loading-icon) {
width: var(--yc-button-icon-sm);
height: var(--yc-button-icon-sm);
}
.yc-button--lg :is(.yc-button__icon, .yc-button__loading-icon) {
width: var(--yc-button-icon-lg);
height: var(--yc-button-icon-lg);
}
.yc-button__icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.yc-button__icon svg {
width: 100%;
height: 100%;
color: currentColor;
fill: currentColor;
}
.yc-button__content {
display: inline-flex;
align-items: center;
}
/* ---------------------------------------------------------------- 变体 */
.yc-button--primary {
background-color: var(--yc-button-primary-bg);
color: var(--yc-button-primary-fg);
}
.yc-button--primary:hover:not(:disabled) {
background-color: var(--yc-button-primary-bg-hover);
}
.yc-button--primary:active:not(:disabled) {
background-color: var(--yc-button-primary-bg-active);
}
.yc-button--primary:disabled,
.yc-button--primary.yc-button--disabled {
background-color: var(--yc-button-primary-bg-disabled);
color: var(--yc-button-primary-fg-disabled);
cursor: not-allowed;
}
.yc-button--secondary {
background-color: var(--yc-button-secondary-bg);
color: var(--yc-button-secondary-fg);
border: var(--yc-border-width) solid var(--yc-button-secondary-border);
}
.yc-button--secondary:hover:not(:disabled) {
background-color: var(--yc-button-secondary-bg-hover);
}
.yc-button--secondary:active:not(:disabled) {
background-color: var(--yc-button-secondary-bg-active);
}
.yc-button--secondary:disabled,
.yc-button--secondary.yc-button--disabled {
color: var(--yc-button-secondary-fg-disabled);
border-color: var(--yc-button-secondary-border-disabled);
cursor: not-allowed;
}
.yc-button--grey {
background-color: var(--yc-button-grey-bg);
color: var(--yc-button-grey-fg);
}
.yc-button--grey:hover:not(:disabled) {
background-color: var(--yc-button-grey-bg-hover);
}
.yc-button--grey:active:not(:disabled) {
background-color: var(--yc-button-grey-bg-active);
}
.yc-button--grey:disabled,
.yc-button--grey.yc-button--disabled {
background-color: var(--yc-button-grey-bg-disabled);
color: var(--yc-button-grey-fg-disabled);
cursor: not-allowed;
}
.yc-button--ghost {
background-color: var(--yc-button-ghost-bg);
color: var(--yc-button-ghost-fg);
font-weight: var(--yc-font-weight-400);
}
.yc-button--ghost:hover:not(:disabled) {
background-color: var(--yc-button-ghost-bg-hover);
color: var(--yc-button-ghost-fg-hover);
}
.yc-button--ghost:active:not(:disabled) {
background-color: var(--yc-button-ghost-bg-active);
color: var(--yc-button-ghost-fg-hover);
}
.yc-button--ghost:disabled,
.yc-button--ghost.yc-button--disabled {
background-color: var(--yc-button-ghost-bg);
color: var(--yc-button-ghost-fg-disabled);
cursor: not-allowed;
}
.yc-button--link {
height: auto;
padding: 0;
background-color: var(--yc-button-link-bg);
color: var(--yc-button-link-fg);
font-weight: var(--yc-font-weight-400);
}
.yc-button--link:hover:not(:disabled) {
color: var(--yc-button-link-fg-hover);
text-decoration: underline;
}
.yc-button--link:active:not(:disabled) {
color: var(--yc-button-link-fg-hover);
text-decoration: underline;
}
.yc-button--link:disabled,
.yc-button--link.yc-button--disabled {
color: var(--yc-button-link-fg-disabled);
cursor: not-allowed;
}
.yc-button--danger {
background-color: var(--yc-button-danger-bg);
color: var(--yc-button-danger-fg);
}
.yc-button--danger:hover:not(:disabled) {
background-color: var(--yc-button-danger-bg-hover);
}
.yc-button--danger:active:not(:disabled) {
background-color: var(--yc-button-danger-bg-active);
}
.yc-button--danger:disabled,
.yc-button--danger.yc-button--disabled {
background-color: var(--yc-button-danger-bg-disabled);
color: var(--yc-button-danger-fg-disabled);
cursor: not-allowed;
}
/* -------------------------------------------------------------- loading */
.yc-button--loading {
position: relative;
pointer-events: none;
}
/* 内容与图标保留占位(只隐藏),加载指示器绝对居中,几何不跳动 */
.yc-button--loading .yc-button__content,
.yc-button--loading .yc-button__icon {
opacity: 0;
}
.yc-button__loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.yc-button__loading-icon {
flex: none;
animation: yc-button-spin var(--yc-button-spinner-duration) linear infinite;
}
@keyframes yc-button-spin {
0% {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
/* 加载态按钮同时是 disabled,spinner 需取变体本身的可见色而非 disabled 前景色 */
.yc-button--primary .yc-button__loading-icon {
color: var(--yc-button-primary-fg);
}
.yc-button--secondary .yc-button__loading-icon {
color: var(--yc-button-secondary-fg);
}
.yc-button--grey .yc-button__loading-icon {
color: var(--yc-button-grey-fg);
}
.yc-button--ghost .yc-button__loading-icon {
color: var(--yc-button-ghost-fg);
}
.yc-button--link .yc-button__loading-icon {
color: var(--yc-button-link-fg);
}
.yc-button--danger .yc-button__loading-icon {
color: var(--yc-button-danger-bg-disabled);
}
/* ------------------------------------------------------------ 键盘焦点 */
.yc-button:focus-visible {
outline: var(--yc-focus-ring-width) solid var(--yc-focus-ring-color);
outline-offset: var(--yc-focus-ring-offset);
}
+124
View File
@@ -0,0 +1,124 @@
/**
* YCode UI Input 样式(P2-B)。
*
* 仅消费 `--yc-*` token(L2 组件 token + L0/L1 基础 token),不写裸值、强制声明或
* 外部外边距。几何与状态对应 source-general-settings-recovered.css
* 的 `.wb-input-wrapper*` 闭包。
*/
.yc-input {
box-sizing: border-box;
display: inline-flex;
align-items: center;
gap: var(--yc-space-2);
width: 100%;
color: var(--yc-input-fg);
font-family: inherit;
line-height: var(--yc-input-line-height);
background: var(--yc-input-bg);
border: var(--yc-border-width) solid var(--yc-input-border);
outline: none;
transition: border-color var(--yc-input-transition-duration) var(--yc-button-transition-easing);
}
.yc-input__inner {
box-sizing: border-box;
flex: 1 1 auto;
min-width: 0;
height: 100%;
margin: 0;
padding: 0;
color: inherit;
font-family: inherit;
font-size: inherit;
font-weight: inherit;
line-height: inherit;
background: none;
border: none;
outline: none;
}
.yc-input__inner::placeholder {
color: var(--yc-input-placeholder);
}
.yc-input__prefix,
.yc-input__suffix {
display: inline-flex;
flex: none;
align-items: center;
color: var(--yc-input-icon);
}
.yc-input__prefix svg,
.yc-input__suffix svg {
width: 1em;
height: 1em;
}
/* 尺寸 */
.yc-input--sm {
height: var(--yc-input-height-sm);
padding: 0 var(--yc-input-padding-x-sm);
border-radius: var(--yc-input-radius-sm);
font-size: var(--yc-input-font-size-sm);
}
.yc-input--md {
height: var(--yc-input-height-md);
padding: 0 var(--yc-input-padding-x-md);
border-radius: var(--yc-input-radius-md);
font-size: var(--yc-input-font-size-md);
}
.yc-input--lg {
height: var(--yc-input-height-lg);
padding: 0 var(--yc-input-padding-x-lg);
border-radius: var(--yc-input-radius-lg);
font-size: var(--yc-input-font-size-lg);
}
/* filled */
.yc-input--filled {
background: var(--yc-input-bg-filled);
border-color: transparent;
}
/* hover / focus(invalid 优先级更高,保持错误态边框) */
.yc-input:hover:not(.yc-input--disabled):not(:has(.yc-input__inner:read-only)) {
border-color: var(--yc-input-border-hover);
}
.yc-input:focus-within:not(.yc-input--disabled):not(:has(.yc-input__inner:read-only)) {
border-color: var(--yc-input-border-focus);
}
.yc-input--invalid {
border-color: var(--yc-input-border-invalid);
}
.yc-input--invalid:hover:not(.yc-input--disabled),
.yc-input--invalid:focus-within:not(.yc-input--disabled) {
border-color: var(--yc-input-border-invalid);
}
/* disabled */
.yc-input--disabled,
.yc-input:has(.yc-input__inner:disabled) {
color: var(--yc-input-fg-disabled);
background: var(--yc-input-bg-disabled);
cursor: not-allowed;
}
.yc-input--disabled .yc-input__inner,
.yc-input:has(.yc-input__inner:disabled) .yc-input__inner {
color: var(--yc-input-fg-disabled);
cursor: not-allowed;
}
.yc-input--disabled .yc-input__prefix,
.yc-input--disabled .yc-input__suffix,
.yc-input:has(.yc-input__inner:disabled) .yc-input__prefix,
.yc-input:has(.yc-input__inner:disabled) .yc-input__suffix {
color: var(--yc-input-icon-disabled);
}
+147
View File
@@ -0,0 +1,147 @@
/**
* YCode UI Switch 样式(P2-B)。
*
* 仅消费 `--yc-*` token,不写裸值、强制声明或外部外边距。
* 几何与状态对应 source-general-settings-recovered.css 的 `.wb-switch*` 闭包;
* 文档未提供 focus 环颜色 token,暂用同计算值的 `--yc-input-border-focus`。
*/
.yc-switch {
display: inline-flex;
align-items: center;
color: inherit;
cursor: pointer;
user-select: none;
}
.yc-switch[data-disabled='true'] {
cursor: not-allowed;
}
.yc-switch__control {
position: relative;
display: inline-flex;
flex: none;
}
.yc-switch__input {
position: absolute;
inset: 0;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
cursor: inherit;
opacity: 0;
}
.yc-switch__track {
box-sizing: border-box;
display: inline-flex;
align-items: center;
padding: 0 var(--yc-space-1);
border-radius: var(--yc-button-radius-pill);
background: var(--yc-switch-track-bg);
border: var(--yc-border-width) solid var(--yc-switch-track-border);
transition: background var(--yc-input-transition-duration) var(--yc-button-transition-easing),
border-color var(--yc-input-transition-duration) var(--yc-button-transition-easing);
}
.yc-switch__thumb {
border-radius: var(--yc-button-radius-pill);
background: var(--yc-switch-thumb-bg);
box-shadow: var(--yc-switch-thumb-shadow);
transition: transform var(--yc-input-transition-duration) var(--yc-button-transition-easing);
}
/* 尺寸:track 用 padding 把 thumb 定位到左侧,与恢复样式一致 */
.yc-switch--sm .yc-switch__track {
width: var(--yc-switch-track-width-sm);
height: var(--yc-switch-track-height-sm);
}
.yc-switch--sm .yc-switch__thumb {
width: var(--yc-switch-thumb-size-sm);
height: var(--yc-switch-thumb-size-sm);
}
.yc-switch--md .yc-switch__track {
width: var(--yc-switch-track-width-md);
height: var(--yc-switch-track-height-md);
}
.yc-switch--md .yc-switch__thumb {
width: var(--yc-switch-thumb-size-md);
height: var(--yc-switch-thumb-size-md);
}
.yc-switch--lg .yc-switch__track {
width: var(--yc-switch-track-width-lg);
height: var(--yc-switch-track-height-lg);
}
.yc-switch--lg .yc-switch__thumb {
width: var(--yc-switch-thumb-size-lg);
height: var(--yc-switch-thumb-size-lg);
}
/* checked */
.yc-switch__input:checked + .yc-switch__track {
background: var(--yc-switch-track-bg-checked);
border-color: var(--yc-switch-track-bg-checked);
}
.yc-switch__input:checked + .yc-switch__track:hover {
background: var(--yc-switch-track-bg-checked-hover);
border-color: var(--yc-switch-track-bg-checked-hover);
}
.yc-switch--sm .yc-switch__input:checked + .yc-switch__track .yc-switch__thumb {
transform: translateX(var(--yc-switch-thumb-translate-sm));
}
.yc-switch--md .yc-switch__input:checked + .yc-switch__track .yc-switch__thumb {
transform: translateX(var(--yc-switch-thumb-translate-md));
}
.yc-switch--lg .yc-switch__input:checked + .yc-switch__track .yc-switch__thumb {
transform: translateX(var(--yc-switch-thumb-translate-lg));
}
/* focus-visible */
.yc-switch__input:focus-visible + .yc-switch__track {
outline: var(--yc-focus-ring-width) solid var(--yc-focus-ring-color);
outline-offset: var(--yc-focus-ring-offset);
}
/* readOnly:保持几何,仅移除可点击语义 */
.yc-switch[data-readonly='true'] {
cursor: default;
}
.yc-switch[data-readonly='true'] .yc-switch__input {
cursor: default;
}
/* disabled:保持几何,用 token 降低视觉强度 */
.yc-switch[data-disabled='true'] .yc-switch__input:checked + .yc-switch__track {
background: var(--yc-switch-track-bg-checked-disabled);
border-color: var(--yc-switch-track-bg-checked-disabled);
}
.yc-switch[data-disabled='true'] .yc-switch__input:checked + .yc-switch__track:hover {
background: var(--yc-switch-track-bg-checked-disabled);
border-color: var(--yc-switch-track-bg-checked-disabled);
}
.yc-switch[data-disabled='true'] .yc-switch__input:not(:checked) + .yc-switch__track {
background: var(--yc-switch-track-bg-disabled);
}
.yc-switch[data-disabled='true'] .yc-switch__input:not(:checked) + .yc-switch__track:hover {
background: var(--yc-switch-track-bg-disabled);
}
.yc-switch[data-disabled='true'] .yc-switch__thumb {
opacity: 0.5;
}
+292
View File
@@ -0,0 +1,292 @@
/**
* YCode UI 组件 token(L2)。
*
* 只在组件层消费;页面不得重定义。数值来自当前 --wb-* 控件闭包,
* 迁移期间必须保持现有视觉,不允许在这里发明新设计。
* Import order: workbuddy/typography/tokens.css -> styles/tokens.css -> ui/tokens.css
*/
:root {
/* Button */
--yc-button-height-sm: 24px;
--yc-button-height-md: 32px;
--yc-button-height-lg: 48px;
--yc-button-padding-sm: 8px;
--yc-button-padding-md: 12px;
--yc-button-padding-lg: 32px;
--yc-button-gap-sm: 2px;
--yc-button-gap-md: 4px;
--yc-button-gap-lg: 8px;
--yc-button-radius-sm: 6px;
--yc-button-radius-md: 8px;
--yc-button-radius-lg: 12px;
--yc-button-radius-pill: 9999px;
--yc-button-font-size-sm: var(--yc-font-size-3);
--yc-button-font-size-md: var(--yc-font-size-5);
--yc-button-font-size-lg: var(--yc-font-size-5);
--yc-button-line-height-sm: var(--yc-line-height-5);
--yc-button-line-height-md: var(--yc-line-height-5-2);
--yc-button-line-height-lg: var(--yc-line-height-5-2);
--yc-button-icon-sm: var(--yc-size-3);
--yc-button-icon-md: var(--yc-size-4);
--yc-button-icon-lg: var(--yc-size-4);
--yc-button-transition-duration: 0.1s;
--yc-button-transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
--yc-button-spinner-duration: 0.8s;
--yc-button-min-width-sm: 40px;
--yc-button-min-width-md: 68px;
--yc-button-primary-bg: rgba(0, 0, 0, 0.9);
--yc-button-primary-bg-hover: rgba(0, 0, 0, 0.75);
--yc-button-primary-bg-active: rgba(0, 0, 0, 0.75);
--yc-button-primary-bg-disabled: rgba(0, 0, 0, 0.2);
--yc-button-primary-fg: #ffffff;
--yc-button-primary-fg-disabled: #ffffff;
--yc-button-secondary-bg: #ffffff;
--yc-button-secondary-bg-hover: #f7f7f7;
--yc-button-secondary-bg-active: #f7f7f7;
--yc-button-secondary-bg-disabled: #f7f7f7;
--yc-button-secondary-fg: rgba(0, 0, 0, 0.9);
--yc-button-secondary-fg-disabled: rgba(0, 0, 0, 0.3);
--yc-button-secondary-border: #dedede;
--yc-button-secondary-border-disabled: #ebebeb;
--yc-button-grey-bg: #ebebeb;
--yc-button-grey-bg-hover: #e6e6e6;
--yc-button-grey-bg-active: #e6e6e6;
--yc-button-grey-bg-disabled: #ebebeb;
--yc-button-grey-fg: rgba(0, 0, 0, 0.9);
--yc-button-grey-fg-disabled: rgba(0, 0, 0, 0.3);
--yc-button-ghost-bg: transparent;
--yc-button-ghost-bg-hover: rgba(0, 0, 0, 0.06);
--yc-button-ghost-bg-active: rgba(0, 0, 0, 0.06);
--yc-button-ghost-fg: rgba(0, 0, 0, 0.9);
--yc-button-ghost-fg-hover: rgba(0, 0, 0, 0.9);
--yc-button-ghost-fg-disabled: rgba(0, 0, 0, 0.3);
--yc-button-link-bg: transparent;
--yc-button-link-fg: #1470b4;
--yc-button-link-fg-hover: #1470b4;
--yc-button-link-fg-disabled: #94b7d1;
--yc-button-danger-bg: #e85c5c;
--yc-button-danger-bg-hover: #ec7474;
--yc-button-danger-bg-active: #ec7474;
--yc-button-danger-bg-disabled: #f5b9b9;
--yc-button-danger-fg: #ffffff;
--yc-button-danger-fg-disabled: #ffffff;
/* Focus ring(Button / Switch / 后续控件共用) */
--yc-focus-ring-color: rgba(0, 0, 0, 0.3);
--yc-focus-ring-width: 2px;
--yc-focus-ring-offset: 2px;
/* Input */
--yc-input-height-sm: 28px;
--yc-input-height-md: 32px;
--yc-input-height-lg: 48px;
--yc-input-padding-x-sm: 8px;
--yc-input-padding-x-md: 8px;
--yc-input-padding-x-lg: 12px;
--yc-input-radius-sm: 6px;
--yc-input-radius-md: 8px;
--yc-input-radius-lg: 12px;
--yc-input-font-size-sm: var(--yc-font-size-3);
--yc-input-font-size-md: var(--yc-font-size-5);
--yc-input-font-size-lg: var(--yc-font-size-5);
--yc-input-line-height: var(--yc-line-height-5-2);
--yc-input-bg: #ffffff;
--yc-input-bg-filled: #f2f2f2;
--yc-input-bg-disabled: #f7f7f7;
--yc-input-border: #e6e6e6;
--yc-input-border-hover: rgba(0, 0, 0, 0.3);
--yc-input-border-focus: rgba(0, 0, 0, 0.3);
--yc-input-border-invalid: #f64041;
--yc-input-fg: rgba(0, 0, 0, 0.9);
--yc-input-fg-disabled: rgba(0, 0, 0, 0.3);
--yc-input-placeholder: rgba(0, 0, 0, 0.3);
--yc-input-icon: rgba(0, 0, 0, 0.5);
--yc-input-icon-hover: rgba(0, 0, 0, 0.7);
--yc-input-icon-disabled: rgba(0, 0, 0, 0.3);
--yc-input-transition-duration: 0.1s;
/* Switch */
--yc-switch-track-width-sm: 24px;
--yc-switch-track-width-md: 26px;
--yc-switch-track-width-lg: 34px;
--yc-switch-track-height-sm: 14px;
--yc-switch-track-height-md: 16px;
--yc-switch-track-height-lg: 20px;
--yc-switch-thumb-size-sm: 10px;
--yc-switch-thumb-size-md: 12px;
--yc-switch-thumb-size-lg: 16px;
--yc-switch-thumb-translate-sm: 8px;
--yc-switch-thumb-translate-md: 8px;
--yc-switch-thumb-translate-lg: 12px;
--yc-switch-track-bg: #e6e6e6;
--yc-switch-track-bg-disabled: #e6e6e6;
--yc-switch-track-bg-checked: #00c29a;
--yc-switch-track-bg-checked-hover: #40d1b3;
--yc-switch-track-bg-checked-disabled: #9fe8d9;
--yc-switch-track-border: rgba(0, 0, 0, 0.08);
--yc-switch-thumb-bg: #ffffff;
--yc-switch-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
--yc-switch-focus-outline: 2px;
--yc-switch-focus-offset: 2px;
/* Select */
--yc-select-height-sm: 24px;
--yc-select-height-md: 32px;
--yc-select-height-lg: 48px;
--yc-select-padding-x-sm: 6px;
--yc-select-padding-x-md: 8px;
--yc-select-padding-x-lg: 12px;
--yc-select-radius-sm: 6px;
--yc-select-radius-md: 8px;
--yc-select-radius-lg: 12px;
--yc-select-font-size-sm: var(--yc-font-size-3);
--yc-select-font-size-md: var(--yc-font-size-5);
--yc-select-font-size-lg: var(--yc-font-size-5);
--yc-select-line-height: var(--yc-line-height-5-2);
--yc-select-icon: var(--yc-size-4);
--yc-select-bg: #ffffff;
--yc-select-fg: rgba(0, 0, 0, 0.9);
--yc-select-placeholder: rgba(0, 0, 0, 0.3);
--yc-select-border: #e6e6e6;
--yc-select-border-hover: rgba(0, 0, 0, 0.3);
--yc-select-border-open: rgba(0, 0, 0, 0.5);
--yc-select-bg-disabled: #f7f7f7;
--yc-select-fg-disabled: rgba(0, 0, 0, 0.3);
/* Dialog */
--yc-dialog-bg: #ffffff;
--yc-dialog-border: #dedede;
--yc-dialog-radius: 24px;
--yc-dialog-radius-confirm: 12px;
--yc-dialog-max-width-s: 480px;
--yc-dialog-max-width-m: 640px;
--yc-dialog-max-width-l: 960px;
--yc-dialog-max-height: calc(100vh - 160px);
--yc-dialog-overlay: rgba(0, 0, 0, 0.15);
--yc-dialog-shadow: var(--yc-shadow-dialog);
/* Popover */
--yc-popover-bg: #ffffff;
--yc-popover-border: #dedede;
--yc-popover-radius: 16px;
--yc-popover-padding: 8px;
--yc-popover-fg: rgba(0, 0, 0, 0.9);
--yc-popover-shadow: var(--yc-shadow-popover);
/* Segmented */
--yc-segmented-track-padding: 2px;
--yc-segmented-track-radius: 8px;
--yc-segmented-track-bg: #f2f2f2;
--yc-segmented-item-gap: 4px;
--yc-segmented-item-radius: 6px;
--yc-segmented-item-height: 32px;
--yc-segmented-item-padding-x: 12px;
--yc-segmented-item-font-size: var(--yc-font-size-5);
--yc-segmented-item-line-height: var(--yc-line-height-5-2);
--yc-segmented-item-fg: rgba(0, 0, 0, 0.7);
--yc-segmented-item-fg-hover: rgba(0, 0, 0, 0.9);
--yc-segmented-item-fg-active: rgba(0, 0, 0, 0.9);
--yc-segmented-item-fg-disabled: rgba(0, 0, 0, 0.3);
--yc-segmented-item-bg-hover: #e6e6e6;
--yc-segmented-thumb-bg: #ffffff;
--yc-segmented-thumb-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
--yc-segmented-item-weight: 400;
--yc-segmented-item-weight-active: 600;
}
html[data-theme='dark'] {
--yc-button-primary-bg: rgba(255, 255, 255, 0.9);
--yc-button-primary-bg-hover: rgba(255, 255, 255, 0.75);
--yc-button-primary-bg-active: rgba(255, 255, 255, 0.75);
--yc-button-primary-bg-disabled: rgba(255, 255, 255, 0.2);
--yc-button-primary-fg: rgba(0, 0, 0, 0.9);
--yc-button-primary-fg-disabled: rgba(0, 0, 0, 0.9);
--yc-button-secondary-bg: #262626;
--yc-button-secondary-bg-hover: #2e2e2e;
--yc-button-secondary-bg-active: #2e2e2e;
--yc-button-secondary-bg-disabled: #262626;
--yc-button-secondary-fg: rgba(255, 255, 255, 0.9);
--yc-button-secondary-fg-disabled: rgba(255, 255, 255, 0.3);
--yc-button-secondary-border: #3d3d3d;
--yc-button-secondary-border-disabled: #3a3a3a;
--yc-button-grey-bg: #383838;
--yc-button-grey-bg-hover: #404040;
--yc-button-grey-bg-active: #404040;
--yc-button-grey-bg-disabled: #404040;
--yc-button-grey-fg: rgba(255, 255, 255, 0.9);
--yc-button-grey-fg-disabled: rgba(255, 255, 255, 0.3);
--yc-button-ghost-bg-hover: rgba(255, 255, 255, 0.1);
--yc-button-ghost-bg-active: rgba(255, 255, 255, 0.1);
--yc-button-ghost-fg: rgba(255, 255, 255, 0.9);
--yc-button-ghost-fg-hover: rgba(255, 255, 255, 0.9);
--yc-button-ghost-fg-disabled: rgba(255, 255, 255, 0.3);
--yc-button-link-fg: #46a8f0;
--yc-button-link-fg-hover: #5ea9f2;
--yc-button-link-fg-disabled: #94b7d1;
--yc-button-danger-bg: #af191a;
--yc-button-danger-bg-hover: #a31717;
--yc-button-danger-bg-active: #a31717;
--yc-button-danger-bg-disabled: #803838;
--yc-button-danger-fg-disabled: rgba(255, 255, 255, 0.7);
--yc-focus-ring-color: rgba(255, 255, 255, 0.3);
--yc-input-bg: #242424;
--yc-input-bg-filled: #262626;
--yc-input-bg-disabled: #171717;
--yc-input-border: #454545;
--yc-input-border-hover: rgba(255, 255, 255, 0.3);
--yc-input-border-focus: rgba(255, 255, 255, 0.3);
--yc-input-fg: rgba(255, 255, 255, 0.9);
--yc-input-fg-disabled: rgba(255, 255, 255, 0.3);
--yc-input-placeholder: rgba(255, 255, 255, 0.3);
--yc-input-icon: rgba(255, 255, 255, 0.5);
--yc-input-icon-hover: rgba(255, 255, 255, 0.7);
--yc-input-icon-disabled: rgba(255, 255, 255, 0.3);
--yc-switch-track-bg: #3c3c3c;
--yc-switch-track-bg-disabled: #3c3c3c;
--yc-switch-track-bg-checked: #22cba8;
--yc-switch-track-bg-checked-hover: #40d1b3;
--yc-switch-track-bg-checked-disabled: rgba(61, 217, 182, 0.35);
--yc-switch-track-border: rgba(255, 255, 255, 0.14);
--yc-switch-thumb-bg: #ffffff;
--yc-switch-thumb-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
--yc-select-bg: #242424;
--yc-select-fg: rgba(255, 255, 255, 0.9);
--yc-select-placeholder: rgba(255, 255, 255, 0.3);
--yc-select-border: #454545;
--yc-select-border-hover: rgba(255, 255, 255, 0.3);
--yc-select-border-open: rgba(255, 255, 255, 0.5);
--yc-select-bg-disabled: #171717;
--yc-select-fg-disabled: rgba(255, 255, 255, 0.3);
--yc-dialog-bg: #1f1f1f;
--yc-dialog-border: #3d3d3d;
--yc-dialog-overlay: rgba(0, 0, 0, 0.4);
--yc-popover-bg: #242424;
--yc-popover-border: #3d3d3d;
--yc-popover-fg: rgba(255, 255, 255, 0.9);
--yc-segmented-track-bg: #242424;
--yc-segmented-item-fg: rgba(255, 255, 255, 0.7);
--yc-segmented-item-fg-hover: rgba(255, 255, 255, 0.9);
--yc-segmented-item-fg-active: rgba(255, 255, 255, 0.9);
--yc-segmented-item-fg-disabled: rgba(255, 255, 255, 0.3);
--yc-segmented-item-bg-hover: rgba(255, 255, 255, 0.08);
--yc-segmented-thumb-bg: #1f1f1f;
}