feat: add props system and refactor asset library architecture

This commit is contained in:
saturn
2026-03-19 15:37:47 +08:00
parent 9aff44e37a
commit f364bbc9e4
139 changed files with 9112 additions and 2827 deletions

View File

@@ -1,6 +1,6 @@
'use client'
import type { ReactNode } from 'react'
import { useRef, useState, useEffect, type ReactNode } from 'react'
// ─── Types ────────────────────────────────────────────
@@ -20,11 +20,12 @@ interface SegmentedControlProps<T extends string = string> {
// ─── Component ────────────────────────────────────────
/**
* Unified iOS-style segmented control.
* Unified iOS-style segmented control with sliding pill indicator.
*
* Single source of truth for all tab/segment UIs across the app.
* Uses per-button selected styling (not a sliding indicator) for
* pixel-perfect equal padding on all four sides.
* Indicator lives inside the grid container to share the same
* positioning context as buttons — guaranteeing equal padding
* on all four sides (Apple-style).
*/
export function SegmentedControl<T extends string = string>({
options,
@@ -32,20 +33,39 @@ export function SegmentedControl<T extends string = string>({
onChange,
className = '',
}: SegmentedControlProps<T>) {
const gridRef = useRef<HTMLDivElement>(null)
const [indicator, setIndicator] = useState<{ left: number; width: number }>({ left: 0, width: 0 })
useEffect(() => {
if (!gridRef.current) return
const activeIndex = options.findIndex((opt) => opt.value === value)
const buttons = gridRef.current.querySelectorAll<HTMLButtonElement>('button')
const activeButton = buttons[activeIndex]
if (activeButton) {
setIndicator({ left: activeButton.offsetLeft, width: activeButton.offsetWidth })
}
}, [value, options])
return (
<div className={`rounded-lg p-[3px] bg-[#f2f2f7] dark:bg-[#1c1c1e] shadow-inner ${className}`}>
<div className={`rounded-xl p-[3px] bg-[#e8e8ed] dark:bg-[#1c1c1e] ${className}`}>
<div
className="grid"
ref={gridRef}
className="relative grid"
style={{ gridTemplateColumns: `repeat(${Math.max(1, options.length)}, minmax(0, 1fr))` }}
>
{/* Sliding pill indicator */}
<div
className="absolute top-0 bottom-0 rounded-[10px] bg-white dark:bg-[#3a3a3c] shadow-[0_1px_3px_rgba(0,0,0,0.08),0_4px_12px_rgba(0,0,0,0.05)] transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]"
style={{ left: indicator.left, width: indicator.width }}
/>
{options.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => onChange(opt.value)}
className={`flex items-center justify-center gap-1.5 rounded-md px-3 py-1.5 text-[13px] font-medium transition-all cursor-pointer ${value === opt.value
? 'bg-white text-[var(--glass-text-primary)] dark:bg-[#2c2c2e] dark:text-white shadow-[0_3px_8px_rgba(0,0,0,0.12),0_3px_1px_rgba(0,0,0,0.04)] font-bold'
: 'text-[var(--glass-text-tertiary)] hover:text-[var(--glass-text-secondary)]'
className={`relative z-10 flex items-center justify-center gap-1.5 rounded-[10px] px-3 py-1.5 text-[13px] font-semibold transition-colors duration-200 cursor-pointer ${value === opt.value
? 'text-[#1d1d1f] dark:text-white'
: 'text-[#86868b] hover:text-[#6e6e73]'
}`}
>
{opt.label}