diff --git a/preview/main.tsx b/preview/main.tsx index d92b33c..e730551 100644 --- a/preview/main.tsx +++ b/preview/main.tsx @@ -8,6 +8,7 @@ import { ConfirmDialog, Dialog, EmptyState, + ErrorState, FormPanelSkeleton, GridListbox, Icon, @@ -15,6 +16,7 @@ import { ListSkeleton, Menu, OutageGate, + PageHeader, Pagination, Popover, Sidebar, @@ -242,14 +244,13 @@ function Preview() { {dialog === 'plain' && ( - setDialog(null)} title="A plain dialog"> + setDialog(null)} title="A plain dialog">

On a phone this opens as a bottom sheet.

)} {dialog === 'confirm' && ( setDialog(null)} onConfirm={() => Promise.reject(new Error('This preview cannot delete anything.'))} title="Delete this note?" @@ -293,7 +294,7 @@ function Preview() { { active: false, badge: 4, href: '#workspace', icon: 'bell', label: 'Inbox' }, ], }, - { label: 'spaces', items: [{ active: false, href: '#workspace', label: 'Friends trip' }] }, + { label: 'Spaces', items: [{ active: false, href: '#workspace', label: 'Friends trip' }] }, ]} /> @@ -329,7 +330,23 @@ function Preview() { -
+
+ + + New + + } + eyebrow="Shared space · owner" + title="Home" + /> +
+ +
+ {}} title="This didn’t load"> + The server answered with an error. +

Empty states sit in a dashed box.

diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index b806c0b..cc43926 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -1,4 +1,4 @@ -import type { CSSProperties, ReactNode } from 'react'; +import { createContext, useContext, useState, type CSSProperties, type ReactNode } from 'react'; import { useLink } from '../UiProvider.js'; import { Icon, type IconName } from './Icon.js'; import { Skeleton } from './Skeleton.js'; @@ -14,6 +14,30 @@ export interface ShellTab extends ShellNavItem { loading?: boolean; } +const collapseKey = 'graphite-sidebar-collapsed'; + +const ShellContext = createContext({ collapsed: false }); + +export function useShell() { + return useContext(ShellContext); +} + +function storedCollapsed() { + try { + return localStorage.getItem(collapseKey) === '1'; + } catch { + return false; + } +} + +function storeCollapsed(collapsed: boolean) { + try { + localStorage.setItem(collapseKey, collapsed ? '1' : '0'); + } catch { + return; + } +} + export function AppShell({ actions, alert, @@ -34,71 +58,94 @@ export function AppShell({ tabs: ShellTab[]; }) { const Link = useLink(); + const [collapsed, setCollapsed] = useState(storedCollapsed); + const toggle = () => { + setCollapsed(!collapsed); + storeCollapsed(!collapsed); + }; + return ( -
- - skip to content - -
-
- - {brand.mark} - {brand.name} - - -
{actions}
-
- {alert} -
- {sidebar ? ( -
- -
-
- {children} -
-
-
- ) : ( -
- {children} -
- )} - {footer} -
+ {alert} + + {sidebar ? ( +
+ +
+
+ {children} +
+
+
+ ) : ( +
+ {children} +
)} - - + {footer} + + + ); } diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx index 310b295..b7cf99c 100644 --- a/src/components/ConfirmDialog.tsx +++ b/src/components/ConfirmDialog.tsx @@ -18,7 +18,7 @@ export function ConfirmDialog({ children: ReactNode; confirmLabel: string; errorFallback?: string; - eyebrow: string; + eyebrow?: string; onClose: () => void; onConfirm: () => Promise; title: string; diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx index 7db2fc7..9c5688c 100644 --- a/src/components/Dialog.tsx +++ b/src/components/Dialog.tsx @@ -1,4 +1,5 @@ import { useEffect, useId, useRef, type ReactNode } from 'react'; +import { Icon } from './Icon.js'; export function Dialog({ children, @@ -7,7 +8,7 @@ export function Dialog({ title, }: { children: ReactNode; - eyebrow: string; + eyebrow?: string; onClose: () => void; title: string; }) { @@ -36,13 +37,13 @@ export function Dialog({
-

{eyebrow}

-

+ {eyebrow &&

{eyebrow}

} +

{title}

-
{children} diff --git a/src/components/ErrorState.tsx b/src/components/ErrorState.tsx new file mode 100644 index 0000000..923c2ef --- /dev/null +++ b/src/components/ErrorState.tsx @@ -0,0 +1,35 @@ +import type { ReactNode } from 'react'; +import { Icon } from './Icon.js'; + +export function ErrorState({ + action, + children, + onRetry, + title, +}: { + action?: ReactNode; + children?: ReactNode; + onRetry?: () => void; + title: string; +}) { + return ( +
+ + + +

{title}

+ {children &&
{children}
} + {(onRetry || action) && ( +
+ {onRetry && ( + + )} + {action} +
+ )} +
+ ); +} diff --git a/src/components/Field.tsx b/src/components/Field.tsx index fc9b9c5..6dfc012 100644 --- a/src/components/Field.tsx +++ b/src/components/Field.tsx @@ -13,7 +13,9 @@ function FieldMessage({ error, hint, id }: { error?: string; hint?: ReactNode; i {hint} - ) : null; + ) : ( +