feat: add props system and refactor asset library architecture
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user