Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@project-graphite/ui",
"version": "1.1.1",
"version": "1.2.0",
"description": "Project Graphite design tokens, theme and React components.",
"license": "MIT",
"repository": {
Expand Down Expand Up @@ -32,8 +32,8 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/material": "^9.5.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react": "^19.3.0",
"react-dom": "^19.3.0"
},
"devDependencies": {
"@emotion/cache": "11.14.0",
Expand Down
8 changes: 5 additions & 3 deletions src/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export function AppShell({
}) {
const Link = useLink();
const [collapsed, setCollapsed] = useState(storedCollapsed);
const activeTab = tabs.findIndex((tab) => tab.active);
const toggle = () => {
setCollapsed(!collapsed);
storeCollapsed(!collapsed);
Expand All @@ -73,7 +74,7 @@ export function AppShell({
<a className="skip-link" href="#content">
Skip to content
</a>
<header className="sticky top-0 z-20 h-(--header-height) border-b border-line-soft bg-paper/90 pr-[env(safe-area-inset-right)] pl-[env(safe-area-inset-left)] backdrop-blur-md">
<header className="app-header sticky top-0 z-20 h-(--header-height) border-b bg-paper/90 pr-[env(safe-area-inset-right)] pl-[env(safe-area-inset-left)] backdrop-blur-md">
<div className={sidebar ? 'shell-bar flex h-full items-center gap-2' : 'shell flex h-full items-center gap-6'}>
{sidebar && (
<IconButton
Expand Down Expand Up @@ -134,9 +135,10 @@ export function AppShell({
aria-label="Main"
className="tab-bar md:hidden"
component="nav"
data-active={activeTab >= 0}
showLabels
style={{ '--tab-count': tabs.length } as CSSProperties}
value={tabs.find((tab) => tab.active)?.href ?? null}
style={{ '--tab-count': tabs.length, '--tab-index': activeTab } as CSSProperties}
value={tabs[activeTab]?.href ?? null}
>
{tabs.map((tab) =>
tab.loading ? (
Expand Down
9 changes: 7 additions & 2 deletions src/components/Choice.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import MuiCheckbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import MuiRadio from '@mui/material/Radio';
import type { InputHTMLAttributes, ReactNode } from 'react';
import { useState, type InputHTMLAttributes, type ReactNode } from 'react';

type ChoiceProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'type'> & { label?: ReactNode };

Expand All @@ -18,14 +18,19 @@ function Choice({
...input
}: ChoiceProps & { kind: 'checkbox' | 'radio' }) {
const Control = kind === 'checkbox' ? MuiCheckbox : MuiRadio;
const [pressed, setPressed] = useState(false);
const control = (
<Control
checked={checked}
className={label ? undefined : className}
data-pressed={pressed || undefined}
defaultChecked={defaultChecked}
disabled={disabled}
name={name}
onChange={onChange}
onChange={(event) => {
setPressed(true);
onChange?.(event);
}}
size="small"
slotProps={{ input }}
value={value}
Expand Down
9 changes: 9 additions & 0 deletions src/components/PageTransition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ViewTransition, type ReactNode } from 'react';

export function PageTransition({ children, page }: { children: ReactNode; page: string }) {
return (
<ViewTransition default="none" exit="page-leave" key={page}>
{children}
</ViewTransition>
);
}
14 changes: 12 additions & 2 deletions src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ListItemButton from '@mui/material/ListItemButton';
import type { ReactNode } from 'react';
import { useLayoutEffect, useRef, useState, type ReactNode } from 'react';
import { useLink } from '../UiProvider.js';
import { useShell } from './AppShell.js';
import { Icon, type IconName } from './Icon.js';
Expand Down Expand Up @@ -36,8 +36,18 @@ export function Sidebar({
}) {
const Link = useLink();
const { collapsed } = useShell();
const nav = useRef<HTMLElement>(null);
const [current, setCurrent] = useState<{ height: number; top: number }>();

useLayoutEffect(() => {
const item = nav.current?.querySelector<HTMLElement>('.sidebar-item[aria-current="page"]');
const next = item ? { height: item.offsetHeight, top: item.offsetTop } : undefined;
setCurrent((shown) => (shown && next && shown.top === next.top && shown.height === next.height ? shown : next));
}, [collapsed, loading, sections]);

return (
<nav aria-busy={loading || undefined} aria-label={label} className="sidebar" data-collapsed={collapsed}>
<nav aria-busy={loading || undefined} aria-label={label} className="sidebar" data-collapsed={collapsed} ref={nav}>
{current && <span aria-hidden="true" className="sidebar-indicator" style={{ height: current.height, translate: `0 ${current.top}px` }} />}
{loading
? Array.from({ length: 3 }, (_, section) => (
<div aria-hidden="true" className="grid gap-0.5" key={section}>
Expand Down
6 changes: 3 additions & 3 deletions src/components/Snackbar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import MuiButton from '@mui/material/Button';
import Grow from '@mui/material/Grow';
import Fade from '@mui/material/Fade';
import SnackbarContent from '@mui/material/SnackbarContent';
import {
createContext,
Expand Down Expand Up @@ -43,7 +43,7 @@ function SnackbarItem({ onDismiss, snack }: { onDismiss: (id: number) => void; s
}, [leave, leaving, paused, snack.tone]);

return (
<Grow appear in={!leaving} onExited={() => onDismiss(snack.id)}>
<Fade appear in={!leaving} onExited={() => onDismiss(snack.id)}>
<SnackbarContent
action={
<>
Expand Down Expand Up @@ -80,7 +80,7 @@ function SnackbarItem({ onDismiss, snack }: { onDismiss: (id: number) => void; s
onMouseLeave={() => setPaused(false)}
role={snack.tone === 'error' ? 'alert' : 'none'}
/>
</Grow>
</Fade>
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function Tabs({ items, label }: { items: TabItem[]; label: string }) {
return (
<MuiTabs
aria-label={label}
className="border-b border-line"
className="page-tabs border-b border-line"
role="navigation"
scrollButtons="auto"
value={items.find((item) => item.active)?.href ?? false}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export { Menu, type MenuItem } from './components/Menu.js';
export { Meter } from './components/Meter.js';
export { OutageGate } from './components/OutageGate.js';
export { PageHeader } from './components/PageHeader.js';
export { PageTransition } from './components/PageTransition.js';
export { Pagination } from './components/Pagination.js';
export { PickerField } from './components/PickerField.js';
export { Popover } from './components/Popover.js';
Expand Down
164 changes: 162 additions & 2 deletions src/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,41 @@ body {
backdrop-filter: blur(14px);
}

.tab-bar::before {
position: absolute;
top: 0.25rem;
left: calc(
env(safe-area-inset-left) + (100% - env(safe-area-inset-left) - env(safe-area-inset-right)) / var(--tab-count) *
(var(--tab-index) + 0.5) - 1.75rem
);
width: 3.5rem;
height: 1.75rem;
border-radius: 999px;
background: var(--color-line-soft);
content: '';
}

.tab-bar[data-active='false']::before {
visibility: hidden;
}

.app-header {
border-bottom-color: var(--color-line-soft);
}

@supports (animation-timeline: scroll()) {
.app-header {
border-bottom-color: transparent;
animation: header-lift linear both;
animation-timeline: scroll(root block);
animation-range: 0 3rem;
}
}

.page-tabs .MuiTabs-indicator {
view-transition-name: page-tabs-indicator;
}

.skeleton {
display: block;
border-radius: 0.375rem;
Expand Down Expand Up @@ -735,6 +770,7 @@ body {
}

.sidebar {
position: relative;
display: flex;
min-height: 100%;
flex-direction: column;
Expand Down Expand Up @@ -838,9 +874,24 @@ body {
}

.sidebar-item[aria-current='page'] {
background: var(--color-line-soft);
background: transparent;
color: var(--color-ink);
}

.sidebar-indicator {
position: absolute;
top: 0;
right: 0.75rem;
left: 0.75rem;
border-radius: var(--radius-field);
background: var(--color-line-soft);
pointer-events: none;
}
}

::view-transition-old(root),
::view-transition-new(root) {
animation: none;
}

@media (prefers-reduced-motion: no-preference) {
Expand All @@ -849,7 +900,7 @@ body {
}

.page-enter {
animation: page-enter 240ms ease-out backwards;
animation: page-enter 280ms var(--ease-settle) backwards;
}

.fade-in {
Expand All @@ -874,6 +925,77 @@ body {
.avatar-button:active {
transform: scale(0.95);
}

.panel-rows > * {
animation: fade-in 160ms ease-out backwards;
}

.stagger-in > * {
animation: rise-in 320ms var(--ease-settle) backwards;
animation-delay: calc(min(sibling-index(), 12) * 40ms - 40ms);
}

.sidebar-indicator {
transition:
translate 280ms var(--ease-settle),
height 280ms var(--ease-settle);
}

.tab-bar::before {
transition: left 280ms var(--ease-settle);
}

.MuiPopover-paper,
.MuiAutocomplete-paper {
animation: overlay-in 200ms var(--ease-settle);
}

.MuiDialog-paper {
animation: rise-in 260ms var(--ease-settle);
}

.snackbar-region .MuiSnackbarContent-root {
animation: rise-in 240ms var(--ease-settle);
}

.MuiCheckbox-root.Mui-checked[data-pressed] .MuiSvgIcon-root {
animation: check-pop 280ms var(--ease-settle);
}

@supports selector(::details-content) {
details {
interpolate-size: allow-keywords;
}

details::details-content {
block-size: 0;
overflow-y: clip;
transition:
block-size 240ms var(--ease-settle),
content-visibility 240ms allow-discrete;
}

details[open]::details-content {
block-size: auto;
}
}

::view-transition-old(.page-leave) {
animation: fade-out 160ms ease-in both;
}

::view-transition-group(page-tabs-indicator) {
animation-duration: 280ms;
animation-timing-function: var(--ease-settle);
}
}

@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none;
}
}

@property --scroll-x-start {
Expand Down Expand Up @@ -911,6 +1033,14 @@ body {
@keyframes page-enter {
from {
opacity: 0;
translate: 0 0.25rem;
}
}

@keyframes rise-in {
from {
opacity: 0;
translate: 0 0.5rem;
}
}

Expand All @@ -920,6 +1050,36 @@ body {
}
}

@keyframes fade-out {
to {
opacity: 0;
}
}

@keyframes overlay-in {
from {
opacity: 0;
scale: 0.96;
}
}

@keyframes check-pop {
40% {
scale: 1.15;
}
}

@keyframes header-lift {
from {
border-bottom-color: transparent;
}

to {
border-bottom-color: var(--color-line-soft);
box-shadow: 0 12px 32px -24px rgb(0 0 0 / 0.8);
}
}

@keyframes shimmer {
from {
background-position: 200% 0;
Expand Down
Loading
Loading