refactor: move studio to frontends workspace
- Move studio from root to frontends/studio/ - Add owner-tools frontend for live blog admin UI - Add shared ui component library - Set up npm workspaces for frontends - Add enhanced code block extension for editor Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c662e41b97
commit
bef5dd4437
108 changed files with 8650 additions and 441 deletions
74
frontends/studio/src/components/ui/Tabs.tsx
Normal file
74
frontends/studio/src/components/ui/Tabs.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { useRef, useState, useLayoutEffect } from 'react'
|
||||
import type { IconComponent } from '../shared/Icons'
|
||||
|
||||
export interface Tab<T extends string = string> {
|
||||
value: T
|
||||
label: string
|
||||
Icon?: IconComponent
|
||||
}
|
||||
|
||||
interface TabsProps<T extends string = string> {
|
||||
value: T
|
||||
onChange: (value: T) => void
|
||||
tabs: Tab<T>[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Tabs<T extends string = string>({
|
||||
value,
|
||||
onChange,
|
||||
tabs,
|
||||
className = '',
|
||||
}: TabsProps<T>) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 })
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
|
||||
const activeIndex = tabs.findIndex(tab => tab.value === value)
|
||||
const buttons = container.querySelectorAll('button')
|
||||
const activeButton = buttons[activeIndex]
|
||||
|
||||
if (activeButton) {
|
||||
setIndicatorStyle({
|
||||
left: activeButton.offsetLeft,
|
||||
width: activeButton.offsetWidth,
|
||||
})
|
||||
}
|
||||
}, [value, tabs])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`relative flex bg-border/50 border border-border ${className}`}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-y-0 bg-surface border-x border-border/50 transition-all duration-200 ease-out"
|
||||
style={{
|
||||
left: indicatorStyle.left,
|
||||
width: indicatorStyle.width,
|
||||
}}
|
||||
/>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.value === value
|
||||
return (
|
||||
<button
|
||||
key={tab.value}
|
||||
type="button"
|
||||
onClick={() => onChange(tab.value)}
|
||||
className={`relative z-10 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium tracking-wide transition-colors duration-150 ${
|
||||
isActive
|
||||
? 'text-text'
|
||||
: 'text-muted hover:text-text/70'
|
||||
}`}
|
||||
>
|
||||
{tab.Icon && <tab.Icon className="w-3.5 h-3.5" />}
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue