Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Workspace home

New

A project tool’s home screen: sidebar with a projects tree, four figures, tabbed tasks you can tick, project progress and deadlines.

workspace-home/workspace-home.tsx
Live · interactive
Maya

Good afternoon, Maya

Tuesday, October 13 · Here’s what’s happening across your workspace.

Active projects
5
1 at risk
Open tasks
39
7 assigned to you
Completed
8
this week
Overdue
6
need attention

My tasks

  • Weekly design sync notesWebsite RedesignToday
  • Launch readiness checklistProduct Launch!Tomorrow
  • Prepare design system tokensWebsite RedesignThu
  • Triage beta tester feedbackMobile AppFri
  • Go / no-go meetingProduct LaunchOct 20
  • Launch webinar deckMarketing CampaignOct 24

Project progress

ProjectStatusProgressDueTeam
Website RedesignIn Progress22%Nov 2+4
Mobile AppIn Progress20%Nov 30+2
Marketing CampaignPlanning14%Nov 18+2
Product LaunchAt Risk0%Oct 21+2
Design SystemIn Progress17%Dec 18

Upcoming deadlines

Today4

Tomorrow2

This week13

Recent activity

  1. You created Triage beta tester feedback

    2h ago
  2. Sam moved Hero illustration to Review

    3h ago
  3. Elif commented on Paid social creatives

    5h ago
  4. Jonah completed Offline cache spike

    1d ago

The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.

When to use

  • The first screen of a project or task tool: where someone lands to see what is theirs today.
  • A workspace with a handful of projects and people, where the reader’s own tasks matter most and the rest is context.
  • When ticking a task should update everything that counts it (tabs, figures, sidebar badge, activity) in one go.
  • A screen that has to work in a wide window, a narrow panel and a phone: it lays out by its own width, not the window’s.

When not to use

  • Hundreds of tasks or projects. The cards show a handful; use a real list or board with paging and filters.
  • Reporting on numbers over time. Use an analytics dashboard with charts, not four fixed figures.
  • A single project’s workspace. Open on its board or list view instead of a home that sums up many.
  • A product with no notion of workspaces, projects or assignees: the sidebar and cards would be empty shells.

You can add this component with the shadcn CLI or by hand:

Into components/workspace-home of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/workspace-home.json

Either way it needs React 19.2 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.

Usage

home.tsx
import { WorkspaceHome } from './workspace-home';export function Home() {  return (    <WorkspaceHome      workspace="Fernway Studio"      userName="Maya"      onTaskToggle={(id, done) => console.log(id, done)}      autoplay={false}    />  );}

Each state below is a live instance, locked so you can study it.

The pieces it is built from, each with its measurements read live.

The first screen of a studio’s workspace, in an app window. A sidebar with a workspace switcher, navigation with counts, a folding projects tree and teams; a greeting, four figures, My tasks in Upcoming, Overdue and Completed tabs, every project’s status and progress, the deadlines ahead and the latest activity. Tick a task and it folds into Completed while the counts, the figures and the activity feed follow. Light and dark.

  • One list of tasks drives everything that counts them: the three tab counts, the Open tasks and Completed figures, the My Tasks badge in the sidebar and the activity feed are all worked out from it on render, so they can never disagree.
  • Ticking a task checks and strikes it at once, keeps it in its tab for 650 ms so the tick reads, then folds it away in 300 ms by animating its grid row from 1fr to 0fr. Unticking it under Completed sends it back to Upcoming or Overdue the same way. A project’s views in the sidebar open and close by the same grid-row fold.
  • The search box, or the / key from anywhere in the screen, filters the tasks, the projects table and the deadlines at once; Escape clears it.
  • Layout by container queries on the root, so it answers to the room it is given rather than the window: the cards sit in two columns from 1340 px and in one under it, the deadlines and the activity side by side between 980 and 1340 px. Under 760 px the sidebar becomes a drawer behind a menu button, the figures go two by two and the projects table scrolls inside its card.
  • The sidebar’s button folds it to a rail of icons (58 px), the section titles turning into hairlines and the Inbox count into a dot; every item keeps its name for screen readers and a tooltip.
  • While on screen and untouched, a demo pointer hovers a task, ticks one, opens Overdue and Completed, unticks it, folds a project in the sidebar and walks the projects table; each glide is a Web Animations API flight. Any real input hands control to the reader, and what the demo changed is put back.
  • Every colour is a light-dark() token on the root, so the screen follows the page’s color-scheme or the colorScheme prop: near-black with a step-darker sidebar and a periwinkle accent at night, white and warm greys by day.
PropTypeDefaultDescription
workspacestring'Fernway Studio'The workspace’s name, in the switcher and the breadcrumb.
workspacesstring[]['Fernway Studio', 'Personal']The workspaces the switcher’s menu offers.
userNamestring'Maya'Who the greeting is for.
greetingstring'Good afternoon'The greeting’s first words.
dateLabelstring'Tuesday, October 13'Today, as the line under the greeting writes it.
defaultTab'upcoming' | 'overdue' | 'completed''upcoming'The tab My tasks opens on.
defaultCollapsedbooleanfalseThe sidebar starts folded to its rail of icons (from 760 px of width).
framebooleantrueDraws the app window around it: its bar with the three lights and the address.
addressstring'app.fernway.studio'The address in the window’s bar.
onTaskToggle(id: string, done: boolean) => void—Called when a task is ticked or unticked.
onWorkspaceChange(name: string) => void—Called when the reader picks a workspace in the switcher, with its name.
defaultStateWorkspaceHomeState—Opens in this state, held still: menu (the switcher’s menu open), query (the search filled in, filtering the tasks, the table and the deadlines), drawer (the sidebar open as a drawer, under 760 px), more (the deadlines’ “+N more” expanded), done (task ids already ticked, the newest first) and hover (a task’s or project row’s id, drawn as under the pointer). Turns the demo off; the menu doesn’t take the focus and stays open through presses outside until the screen is used.
autoplaybooleantrueWhile on screen and untouched, a demo pointer ticks a task, walks the tabs, folds a project and hovers the table.
colorScheme'light' | 'dark' | 'system'—Its colours: light, dark, or the reader’s system setting. Without it the component follows the page’s color-scheme, which is light unless the page sets one.
classNamestring—Extra class for the root.

The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.

workspace-home.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import {  useEffect,  useEffectEvent,  useId,  useRef,  useState,  type CSSProperties,  type KeyboardEvent as ReactKeyboardEvent,  type ReactNode,} from 'react';import {  ACTIVITY,  DEADLINES,  PEOPLE,  PRIORITY_LABEL,  PROJECTS,  TASKS,  TEAMS,  projectById,  type Activity,
workspace-home.module.css
/* Workspace home (workspace-home.tsx): a project tool's first screen in an app window. Near-black   at night, the sidebar a step darker than the page, cards drawn by hairlines rather than fills,   one periwinkle accent; by day the same in white and warm greys.   Colours are the tokens below, light-dark(light, dark): the component takes the color-scheme it   inherits from the page, or the one its colorScheme prop sets (data-color-scheme).   Layout by container queries on the root, so it answers to the room it is given: the sidebar   is a column from 760 px (or a rail of icons, folded), a drawer under it; the cards sit in two   columns from 1340 px and in one under it.   Memory. Folds (a project's views, a ticked task leaving) animate grid rows on one small box;   everything else moves by transforms and opacity, and the demo pointer on its own layer. */.root {  --app: #ffffff;  --side: #f7f7f5;  --line: #e6e6e2;  --divider: #efefec;  --hover: #f4f4f2;  --active: #ebebe8;  --ink: #1b1b19;  --ink-2: #5c5c58;  --muted: #8a8a85;
data.ts
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The workspace the home screen shows: an invented design studio, its projects, its people, the * reader's tasks and what is due soon. All of it is made up for the demo; the dates are written as * the app would show them on Tuesday, October 13. */import type { PATHS } from './icons';export type ProjectId = 'web' | 'mobile' | 'marketing' | 'launch' | 'system' | 'portal';export type IconName = keyof typeof PATHS;export type Project = {  id: ProjectId;  name: string;  icon: IconName;  /** Its colour: the dot beside its tasks and the tint of its tile. */  hue: string;  status: 'In Progress' | 'Planning' | 'At Risk' | 'On Hold';  progress: number;  due: string;  team: PersonId[];  /** Members beyond the faces shown. */
icons.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The line icons, drawn on a 24-unit grid with a 1.7-unit round stroke in the text's colour. Each * is a list of path data; a circle is written as two arcs. The stylesheet sizes them (the lab page * would otherwise force every unsized SVG to 16 px). */const circle = (cx: number, cy: number, r: number) =>  `M${cx - r} ${cy}a${r} ${r} 0 1 0 ${r * 2} 0a${r} ${r} 0 1 0 ${-r * 2} 0`;export const PATHS = {  home: ['M4 10.5 12 4l8 6.5V19a1 1 0 0 1-1 1h-4.5v-6h-5v6H5a1 1 0 0 1-1-1z'],  inbox: [    'M4 13.5 6.4 6.2A1.8 1.8 0 0 1 8.1 5h7.8a1.8 1.8 0 0 1 1.7 1.2L20 13.5V18a1.8 1.8 0 0 1-1.8 1.8H5.8A1.8 1.8 0 0 1 4 18z',    'M4 13.5h4.5l1.2 2.3h4.6l1.2-2.3H20',  ],  checkCircle: [circle(12, 12, 8.2), 'm8.6 12.2 2.3 2.3 4.6-4.8'],  star: ['m12 3.8 2.5 5.1 5.6.8-4 3.9 1 5.6L12 16.6l-5 2.6 1-5.6-4.1-3.9 5.6-.8z'],  search: [circle(11, 11, 6.3), 'm15.7 15.7 4.2 4.2'],  help: [circle(12, 12, 8.2), 'M9.7 9.6a2.4 2.4 0 1 1 3.3 2.2c-.6.3-1 .8-1 1.4v.4', 'M12 16.3v.1'],  bell: ['M6.3 16.5V11a5.7 5.7 0 0 1 11.4 0v5.5l1.5 1.5H4.8z', 'M10 20.5a2.2 2.2 0 0 0 4 0'],  overview: ['M4.5 4.5h6v6h-6z', 'M13.5 4.5h6v4h-6z', 'M13.5 11.5h6v8h-6z', 'M4.5 13.5h6v6h-6z'],  folder: [
demo-pointer.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import type { CSSProperties, Ref } from 'react';/* * The demo's pointer, and the steps that move it. A demo is a script of awaited steps: wait, or * glide the pointer somewhere. Stopping the demo (the reader takes over, or it scrolls away) ends * the step in progress at once: its promise rejects, which ends the script right there, and the * pointer fades out. * * Each glide is a curved flight played by the Web Animations API, so nothing runs frame by frame. * The points of the flight are handed to whoever wants to move along with it. The arrow and the * hand are HTML boxes that swap by their visibility: a change of style inside an SVG would lay the * SVG out again. */export type Point = { x: number; y: number };/** A glide's path and timing, for an animation that plays along with it. */export type Flight = { points: Point[]; timing: KeyframeAnimationOptions };export type Demo = {
use-autoplay.ts
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) {  const ref = useRef<T>(null);  const [visible, setVisible] = useState(false);  const [reduced, setReduced] = useState(true);  const [taken, setTaken] = useState(false);  const timer = useRef(0);  useEffect(() => {    const element = ref.current;    if (!element) return;    let onScreen = false;    const update = () => setVisible(onScreen && !document.hidden);    const observer = new IntersectionObserver(
  • The sidebar is a labelled navigation landmark; the current item carries aria-current="page", each project is a button with aria-expanded over its views, and the folded rail keeps every name for screen readers. The workspace switcher is a menu button with a radio menu: arrow keys move, Escape closes it and returns focus.
  • My tasks is a tablist with arrow, Home and End keys and one tab stop. Each task’s circle is a checkbox (role="checkbox", aria-checked) named after the task; its status, priority and project dot have text equivalents.
  • The projects table is a real table with column and row headers; each progress bar is a progressbar with its value, each team stack an image named with its people.
  • Ticking, unticking and switching workspace are announced in a polite live region. / focuses the search from anywhere in the screen; Escape clears it, and closes the drawer on a phone.
  • Reduced motion removes the folds and transitions and stops the demo; the demo pointer is hidden from assistive technology.