UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

202 lines 10.6 kB
import { LocationService, Shade, createComponent, transitionedValue } from '@furystack/shades'; import { buildTransition, cssVariableTheme } from '../services/css-variable-theme.js'; import { close } from './icons/icon-definitions.js'; import { Icon } from './icons/icon.js'; const TabHeader = Shade({ customElementName: 'shade-tab-header', css: { padding: `${cssVariableTheme.spacing.md} 40px`, cursor: 'pointer', transition: buildTransition(['box-shadow', cssVariableTheme.transitions.duration.slow, 'ease'], ['background', cssVariableTheme.transitions.duration.slow, 'ease'], ['color', cssVariableTheme.transitions.duration.slow, 'ease'], ['font-weight', cssVariableTheme.transitions.duration.slow, 'ease']), fontWeight: 'inherit', background: cssVariableTheme.background.default, color: cssVariableTheme.text.secondary, boxShadow: 'none', display: 'inline-flex', alignItems: 'center', gap: cssVariableTheme.spacing.sm, textDecoration: 'none', '&[data-active]': { fontWeight: 'bolder', background: cssVariableTheme.background.paper, color: cssVariableTheme.text.primary, boxShadow: `inset 0 -2px 0 ${cssVariableTheme.palette.primary.main}`, }, }, elementBase: HTMLAnchorElement, elementBaseName: 'a', render: ({ children, injector, props, useObservable, useHostProps }) => { const locationService = injector.get(LocationService); const [hash] = useObservable('updateLocation', locationService.onLocationHashChanged); const isActive = hash === props.hash; useHostProps({ href: `#${props.hash}`, ...(isActive ? { 'data-active': '' } : {}), }); return createComponent(createComponent, null, children); }, }); export const Tabs = Shade({ customElementName: 'shade-tabs', css: { fontFamily: cssVariableTheme.typography.fontFamily, width: '100%', height: '100%', display: 'flex', flexDirection: 'column', '& .shade-tabs-header-container': { display: 'inline-flex', borderRadius: `${cssVariableTheme.shape.borderRadius.md} ${cssVariableTheme.shape.borderRadius.md} 0 0`, overflow: 'hidden', flexShrink: '0', }, // Controlled mode tab button '& .shade-tab-btn': { padding: `${cssVariableTheme.spacing.md} 40px`, cursor: 'pointer', transition: buildTransition(['box-shadow', cssVariableTheme.transitions.duration.slow, 'ease'], ['background', cssVariableTheme.transitions.duration.slow, 'ease'], ['color', cssVariableTheme.transitions.duration.slow, 'ease'], ['font-weight', cssVariableTheme.transitions.duration.slow, 'ease']), fontWeight: 'inherit', background: cssVariableTheme.background.default, color: cssVariableTheme.text.secondary, boxShadow: 'none', display: 'inline-flex', alignItems: 'center', gap: cssVariableTheme.spacing.sm, border: 'none', font: 'inherit', }, '& .shade-tab-btn.active': { fontWeight: 'bolder', background: cssVariableTheme.background.paper, color: cssVariableTheme.text.primary, boxShadow: `inset 0 -2px 0 ${cssVariableTheme.palette.primary.main}`, }, '& .shade-tab-btn:focus-visible': { outline: cssVariableTheme.action.focusOutline, outlineOffset: '-2px', }, // Close button (span with role="button" via event delegation) '& .shade-tab-close': { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '16px', height: '16px', borderRadius: cssVariableTheme.shape.borderRadius.xs, opacity: '0.5', fontSize: '12px', lineHeight: '1', transition: buildTransition(['opacity', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.default], ['background', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.default]), }, '& .shade-tab-close:hover': { opacity: '1', background: cssVariableTheme.action.hoverBackground, }, // Add tab button '& .shade-tab-add': { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: `${cssVariableTheme.spacing.sm} ${cssVariableTheme.spacing.md}`, border: 'none', background: 'transparent', color: cssVariableTheme.text.secondary, cursor: 'pointer', fontSize: '18px', lineHeight: '1', transition: buildTransition(['color', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.default], ['background', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.default]), }, '& .shade-tab-add:hover': { color: cssVariableTheme.text.primary, background: cssVariableTheme.action.hoverBackground, }, // --- Vertical orientation --- '&[data-orientation="vertical"]': { flexDirection: 'row', }, '&[data-orientation="vertical"] .shade-tabs-header-container': { flexDirection: 'column', borderRadius: `${cssVariableTheme.shape.borderRadius.md} 0 0 ${cssVariableTheme.shape.borderRadius.md}`, }, '&[data-orientation="vertical"] a[is="shade-tab-header"][data-active], &[data-orientation="vertical"] .shade-tab-btn.active': { boxShadow: `inset -2px 0 0 ${cssVariableTheme.palette.primary.main}`, }, // --- Card type --- '&[data-type="card"] .shade-tabs-header-container': { gap: '2px', borderRadius: '0', overflow: 'visible', borderBottom: `1px solid ${cssVariableTheme.action.subtleBorder}`, }, '&[data-type="card"] a[is="shade-tab-header"], &[data-type="card"] .shade-tab-btn': { borderRadius: `${cssVariableTheme.shape.borderRadius.md} ${cssVariableTheme.shape.borderRadius.md} 0 0`, border: '1px solid transparent', borderBottom: 'none', marginBottom: '-1px', background: 'transparent', }, '&[data-type="card"] a[is="shade-tab-header"][data-active], &[data-type="card"] .shade-tab-btn.active': { boxShadow: 'none', background: cssVariableTheme.background.paper, border: `1px solid ${cssVariableTheme.action.subtleBorder}`, borderBottom: `1px solid ${cssVariableTheme.background.paper}`, }, // --- Card type + vertical --- '&[data-type="card"][data-orientation="vertical"] .shade-tabs-header-container': { borderBottom: 'none', borderRight: `1px solid ${cssVariableTheme.action.subtleBorder}`, }, '&[data-type="card"][data-orientation="vertical"] a[is="shade-tab-header"], &[data-type="card"][data-orientation="vertical"] .shade-tab-btn': { borderRadius: `${cssVariableTheme.shape.borderRadius.md} 0 0 ${cssVariableTheme.shape.borderRadius.md}`, border: '1px solid transparent', borderRight: 'none', marginRight: '-1px', marginBottom: '0', }, '&[data-type="card"][data-orientation="vertical"] a[is="shade-tab-header"][data-active], &[data-type="card"][data-orientation="vertical"] .shade-tab-btn.active': { boxShadow: 'none', background: cssVariableTheme.background.paper, border: `1px solid ${cssVariableTheme.action.subtleBorder}`, borderRight: `1px solid ${cssVariableTheme.background.paper}`, }, }, render: ({ props, useObservable, injector, useHostProps, useState }) => { useHostProps({ ...(props.containerStyle ? { style: props.containerStyle } : {}), ...(props.orientation === 'vertical' ? { 'data-orientation': 'vertical' } : {}), ...(props.type === 'card' ? { 'data-type': 'card' } : {}), }); const isControlled = props.activeKey !== undefined; const [hash] = useObservable('updateLocation', injector.get(LocationService).onLocationHashChanged); const activeKey = isControlled ? props.activeKey : hash.replace('#', ''); const displayedKey = transitionedValue(useState, 'displayedKey', activeKey, props.viewTransition); const displayedTab = props.tabs.find((t) => t.hash === displayedKey); const handleTabClick = (e, tab, index) => { const target = e.target; if (target.closest('.shade-tab-close')) { e.stopPropagation(); e.preventDefault(); props.onClose?.(tab.hash); return; } props.onTabChange?.(tab.hash); props.onChange?.(index); }; return (createComponent(createComponent, null, createComponent("div", { className: "shade-tabs-header-container", role: "tablist" }, props.tabs.map((tab, index) => { const isActive = tab.hash === activeKey; const hasCloseButton = tab.closable && props.onClose; return isControlled ? (createComponent("button", { className: `shade-tab-btn${isActive ? ' active' : ''}`, role: "tab", "aria-selected": String(isActive), tabIndex: isActive ? 0 : -1, onclick: (e) => handleTabClick(e, tab, index) }, tab.header, hasCloseButton ? (createComponent("span", { className: "shade-tab-close" }, createComponent(Icon, { icon: close, size: 12 }))) : null)) : (createComponent(TabHeader, { hash: tab.hash, onclick: (e) => handleTabClick(e, tab, index) }, tab.header, hasCloseButton ? (createComponent("span", { className: "shade-tab-close" }, createComponent(Icon, { icon: close, size: 12 }))) : null)); }), props.onAdd ? (createComponent("button", { className: "shade-tab-add", "aria-label": "Add tab", onclick: () => props.onAdd() }, "+")) : null), displayedTab?.component)); }, }); //# sourceMappingURL=tabs.js.map