diff --git a/src/components/Card/Card.css b/src/components/Card/Card.css index 034ba6d..dda61b6 100644 --- a/src/components/Card/Card.css +++ b/src/components/Card/Card.css @@ -12,7 +12,7 @@ color: var(--text-primary); font-family: var(--font-ui); - font-size: var(--font-size-sm); + font-size: var(--font-sm); display: flex; flex-direction: column; diff --git a/src/components/Sidebar/Sidebar.css b/src/components/Sidebar/Sidebar.css index 975734c..5f61ea5 100644 --- a/src/components/Sidebar/Sidebar.css +++ b/src/components/Sidebar/Sidebar.css @@ -1,27 +1,219 @@ -/* ---------- Label ---------- */ +/* ---------- Sidebar ---------- */ + +.sidebar-rail { + position: fixed; + top: var(--header-height); + left: 0; + bottom: 0; + + width: var(--sidebar-width-collapsed); + + display: flex; + flex-direction: column; + + background: var(--bg-void); + border-right: var(--border-width) solid var(--border-hairline); + + z-index: 30; + overflow: visible; + + transition: width var(--dur-base) var(--ease-standard); +} + +.sidebar-rail--expanded { + width: var(--sidebar-width-expanded); +} + +/* ---------- Header (app name + toggle) ---------- */ + +.sidebar-header { + display: flex; + align-items: center; + justify-content: center; + + flex: 0 0 auto; + padding: var(--space-1); + height: var(--header-height); +} + +.sidebar-rail--expanded .sidebar-header { + justify-content: space-between; + padding-inline: var(--space-3); +} + +.sidebar-app-name { + display: flex; + align-items: center; + aspect-ratio: 1; + height: calc(var(--header-height) * 0.7); + + font-size: var(--font-md); + font-weight: var(--font-weight-semibold); + color: var(--text-primary); + + white-space: nowrap; + overflow: hidden; + + flex: 0 1 auto; + min-width: 0; -.sidebar-label { - font-size: var(--sidebar-label-size); opacity: 0; max-width: 0; - overflow: hidden; - white-space: nowrap; + pointer-events: none; + transition: opacity var(--dur-fast) var(--ease-standard), max-width var(--dur-base) var(--ease-standard); } -.sidebar-rail--expanded .sidebar-label { +.sidebar-rail--expanded .sidebar-app-name { opacity: 1; max-width: 200px; + pointer-events: auto; + transition-delay: var(--dur-fast); +} + +.sidebar-toggle { + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + + aspect-ratio: 1; + height: calc(var(--header-height) * 0.7); + + border: var(--border-width-active) solid transparent; + border-radius: var(--radius-pill); + + background: var(--bg-void); + color: var(--text-muted); + + cursor: pointer; + padding: 0; + + transition: + background var(--dur-base) var(--ease-standard), + color var(--dur-base) var(--ease-standard), + border-color var(--dur-base) var(--ease-standard); +} + +.sidebar-toggle:hover { + background: var(--bg-raised); + color: var(--text-primary); + border-color: var(--accent-brand); +} + +.sidebar-toggle-icon { + aspect-ratio: 1; + width: 70%; + transition: transform var(--dur-base) var(--ease-standard); +} + +.sidebar-rail--expanded .sidebar-toggle-icon { + transform: rotate(180deg); +} + +/* ---------- Scroll ---------- */ + +.sidebar-scroll { + flex: 1 1 auto; + min-height: 0; + + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-1); + + padding-block: var(--space-1) var(--space-3); + overflow-y: auto; + + -webkit-mask-image: linear-gradient(to bottom, transparent, black 0); + mask-image: linear-gradient(to bottom, transparent, black 0); +} + +.sidebar-rail--expanded .sidebar-scroll { + align-items: stretch; + padding-inline: var(--space-2); +} + +.sidebar-scroll[data-fade-top='true'] { + -webkit-mask-image: linear-gradient( + to bottom, + transparent, + black 24px, + black calc(100% - 24px), + black + ); + mask-image: linear-gradient(to bottom, transparent, black 24px, black calc(100% - 24px), black); +} +.sidebar-scroll[data-fade-bottom='true'] { + -webkit-mask-image: linear-gradient(to bottom, black, black calc(100% - 24px), transparent); + mask-image: linear-gradient(to bottom, black, black calc(100% - 24px), transparent); +} +.sidebar-scroll[data-fade-top='true'][data-fade-bottom='true'] { + -webkit-mask-image: linear-gradient( + to bottom, + transparent, + black 24px, + black calc(100% - 24px), + transparent + ); + mask-image: linear-gradient( + to bottom, + transparent, + black 24px, + black calc(100% - 24px), + transparent + ); +} + +/* ---------- Item ---------- */ + +.sidebar-item { + display: flex; + align-items: center; + justify-content: center; + gap: var(--sidebar-item-gap); + + aspect-ratio: 1; + height: calc(var(--header-height) * 0.7); + + border: var(--border-width-active) solid transparent; + border-radius: var(--radius-lg); + + background: transparent; + color: var(--text-muted); + + cursor: pointer; + + transition: + background var(--dur-base) var(--ease-standard), + color var(--dur-base) var(--ease-standard), + border-color var(--dur-base) var(--ease-standard); +} + +.sidebar-item:hover { + background: var(--bg-raised); + color: var(--text-primary); + border: var(--border-width-active) solid var(--accent-brand); +} + +.sidebar-item--active { + border-color: var(--accent-brand); + color: var(--text-primary); +} + +.sidebar-rail--expanded .sidebar-item { + width: 100%; + justify-content: flex-start; + padding-inline: var(--space-2); } /* ---------- Icon ---------- */ .sidebar-icon { - width: var(--sidebar-icon-size, 20px); - height: var(--sidebar-icon-size, 20px); - + aspect-ratio: 1; + height: 80%; flex-shrink: 0; display: flex; @@ -34,208 +226,32 @@ .sidebar-icon svg { width: 100%; height: 100%; - display: block; - stroke: currentColor; fill: none; } -/* ---------- Item ---------- */ - -.sidebar-item { - width: var(--sidebar-item-size); - height: var(--sidebar-item-size); - - margin: 0 auto; - - display: flex; - align-items: center; - justify-content: center; - - border: var(--border-width) solid transparent; - border-radius: var(--radius-lg); - - background: transparent; - color: var(--text-muted); - - cursor: pointer; - - transition: - width var(--dur-base) var(--ease-standard), - background var(--dur-base) var(--ease-standard), - color var(--dur-base) var(--ease-standard), - border-color var(--dur-base) var(--ease-standard); -} - -.sidebar-rail--expanded .sidebar-item { - width: calc(100% - var(--space-4)); - gap: var(--sidebar-item-gap); - - justify-content: flex-start; - - padding-left: var(--sidebar-item-padding); -} - -/* ---------- Item States ---------- */ - -.sidebar-item:hover { - background: var(--bg-raised); - color: var(--text-primary); -} - -.sidebar-item--active { - border-color: var(--accent-brand); - color: var(--text-primary); -} - -/* ---------- Sidebar ---------- */ - -.sidebar-rail { - position: fixed; - - top: calc(var(--header-height)); - left: 0; - bottom: 0; - - width: var(--sidebar-width-collapsed); - - display: flex; - flex-direction: column; - - background: var(--bg-void); - - border-right: var(--border-width) solid var(--border-hairline); - - z-index: 30; - - overflow: visible; - - transition: width var(--dur-base) var(--ease-standard); -} - -.sidebar-rail--expanded { - width: var(--sidebar-width-expanded); -} - -/* ---------- App name ---------- */ - -.sidebar-app-name { - position: absolute; - - top: var(--space-3); - left: var(--space-4); - - height: var(--sidebar-toggle-size); - - display: flex; - align-items: center; - - font-size: var(--font-size-sm, 13px); - font-weight: var(--font-weight-semibold); - color: var(--text-primary); +/* ---------- Label ---------- */ +.sidebar-label { + font-size: var(--font-sm); white-space: nowrap; overflow: hidden; + flex: 1 1 auto; + min-width: 0; + opacity: 0; - pointer-events: none; - - transition: opacity var(--dur-fast) var(--ease-standard); - - z-index: 31; -} - -.sidebar-rail--expanded .sidebar-app-name { - opacity: 1; - pointer-events: auto; - transition-delay: var(--dur-fast); -} - -/* ---------- Scroll ---------- */ - -.sidebar-scroll { - flex: 1; - - display: flex; - flex-direction: column; - align-items: center; - - gap: var(--space-1); - - padding: calc(var(--sidebar-toggle-size) + var(--space-3) * 2) 0 var(--space-3); - - overflow-y: auto; -} - -.sidebar-rail--expanded .sidebar-scroll { - align-items: stretch; -} - -/* ---------- Toggle ---------- */ - -.sidebar-toggle { - position: absolute; - - top: var(--space-3); - left: calc((var(--sidebar-width-collapsed) - var(--sidebar-toggle-size)) / 2); - - width: var(--sidebar-toggle-size); - height: var(--sidebar-toggle-size); - - display: flex; - align-items: center; - justify-content: center; - - border: none; - border-radius: var(--radius-pill); - - background: var(--bg-void); - color: var(--text-muted); - - cursor: pointer; - padding: 0; - - box-shadow: none; + max-width: 0; transition: - left var(--dur-base) var(--ease-standard), - background var(--dur-base) var(--ease-standard), - color var(--dur-base) var(--ease-standard), - transform var(--dur-fast) var(--ease-standard); - - z-index: 32; + opacity var(--dur-fast) var(--ease-standard), + max-width var(--dur-base) var(--ease-standard); } -.sidebar-rail--expanded .sidebar-toggle { - left: calc(var(--sidebar-width-expanded) - var(--sidebar-toggle-size) - var(--space-3)); -} - -.sidebar-toggle:hover { - background: var(--bg-raised); - - color: var(--text-primary); - - border-color: var(--accent-brand); -} - -.sidebar-toggle:active { - transform: scale(0.92); -} - -.sidebar-rail--expanded .sidebar-toggle:active { - transform: scale(0.92); -} - -.sidebar-toggle-icon { - width: 14px; - height: 14px; - - transition: transform var(--dur-base) var(--ease-standard); -} - -.sidebar-rail--expanded .sidebar-toggle-icon { - transform: rotate(180deg); +.sidebar-rail--expanded .sidebar-label { + opacity: 1; + max-width: 200px; } /* ---------- Content ---------- */ diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index 39e8050..fa7026c 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -1,3 +1,4 @@ +import { useEffect, useRef } from 'react'; import './Sidebar.css'; import { ChevronLeftIcon } from '../../assets/icons/ChevronLeftIcon'; @@ -7,6 +8,29 @@ export type SidebarItem = { icon: React.ReactNode; }; +function SidebarButton({ + item, + active, + onSelect, +}: { + item: SidebarItem; + active: boolean; + onSelect: () => void; +}) { + return ( + + ); +} + export default function Sidebar({ items, activeId, @@ -22,32 +46,50 @@ export default function Sidebar({ expanded: boolean; onToggle: () => void; }) { + const scrollRef = useRef(null); + + const updateFade = () => { + const el = scrollRef.current; + if (!el) return; + const atTop = el.scrollTop <= 1; + const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1; + el.dataset.fadeTop = String(!atTop); + el.dataset.fadeBottom = String(!atBottom && el.scrollHeight > el.clientHeight); + }; + + useEffect(() => { + updateFade(); + const el = scrollRef.current; + el?.addEventListener('scroll', updateFade); + window.addEventListener('resize', updateFade); + return () => { + el?.removeEventListener('scroll', updateFade); + window.removeEventListener('resize', updateFade); + }; + }, [items.length]); + return ( -