From f28a0ee3547237fd2f25b09e12124a85b9adf5ca Mon Sep 17 00:00:00 2001 From: Loki Date: Wed, 4 Nov 2026 19:01:28 +0800 Subject: [PATCH] feat(week-calendar): stack overlaps like Calendar, tip the full text A later overlapping event sits on top of the earlier one, indented 1rem, instead of splitting the column into slivers. Titles wrap, blocks of 90 minutes or more show the whole span, and every block and pill names its full title and time in a tooltip. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014sgfmQdt3cw61HNpzyCi4y --- README.md | 2 +- app/_docs/demos/week-calendar.tsx | 7 +- registry.json | 3 +- .../blocks/week-calendar/week-calendar.tsx | 138 ++++++++++-------- 4 files changed, 90 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 9c83fbf..1b946fc 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Moving an existing shadcn (Radix) app over: see [MIGRATING.md](MIGRATING.md) for | `focus` | `registry:block` | The one loud thing on a page, under its header: a label, a title with a highlight and a row of facts | | `avatar-stack` | `registry:block` | Several people in the space of one; the avatars spread apart when pointed at | | `countdown` | `registry:block` | Time left until a moment, to the minute, with rolling digits | -| `week-calendar` | `registry:block` | One week as a time grid like the week view of Calendar: hours down the side (08:00 to 20:00, stretching to fit), timed events as blocks from start to end side by side where they overlap, all-day events as pills above, today circled; one row per day below `md` | +| `week-calendar` | `registry:block` | One week as a time grid like the week view of Calendar: hours down the side (08:00 to 20:00, stretching to fit), timed events as blocks from start to end (a later overlapping one stacks on top, indented, as in Calendar), the full title and time in a tooltip, all-day events as pills above, today circled; one row per day below `md` | | `claude-code` | `registry:block` | Claude Code's screen in a `terminal` for demos and install guides: welcome, prompts, MCP calls, permission questions, the `/plugins` menus (marketplaces, the plugin list with its search box, plugin details) and `/mcp`; content through props; colors are `claude-*` tokens from Claude Code's dark theme | | `typing` | `registry:lib` | Timed keystrokes for typing animations: a steady pace (100ms a key, about 120 WPM) with jitter; Chinese typed as zhuyin that turns into the character on its tone key; `InputBox` shows the zhuyin with `composing` | | `expand-row` | `registry:block` | A list row that opens in place to show its details | diff --git a/app/_docs/demos/week-calendar.tsx b/app/_docs/demos/week-calendar.tsx index f863093..23a32d6 100644 --- a/app/_docs/demos/week-calendar.tsx +++ b/app/_docs/demos/week-calendar.tsx @@ -3,8 +3,13 @@ import { WeekCalendar } from "@/registry/winlab/blocks/week-calendar/week-calend const at = (day: string, time: string) => `2026-11-${day}T${time}:00+08:00` const events = [ - { title: "Lab Meeting", start: at("02", "15:30"), end: at("02", "17:30") }, + { title: "Lab Meeting", start: at("02", "15:30"), end: at("02", "18:30") }, { title: "實驗討論", start: at("02", "16:00"), end: at("02", "17:00") }, + { + title: "[nycu-robotics]-確認工作事項", + start: at("02", "17:30"), + end: at("02", "18:30"), + }, { title: "專題討論", start: at("02", "19:30"), end: at("02", "20:30") }, { title: "6G 雙週會議", start: at("03", "10:00"), end: at("03", "11:00") }, { diff --git a/registry.json b/registry.json index 77da67f..3f79360 100644 --- a/registry.json +++ b/registry.json @@ -1302,7 +1302,8 @@ "cn" ], "registryDependencies": [ - "https://ui.winlab.tw/r/utils.json" + "https://ui.winlab.tw/r/utils.json", + "https://ui.winlab.tw/r/tooltip.json" ], "files": [ { diff --git a/registry/winlab/blocks/week-calendar/week-calendar.tsx b/registry/winlab/blocks/week-calendar/week-calendar.tsx index aed3751..f3872cf 100644 --- a/registry/winlab/blocks/week-calendar/week-calendar.tsx +++ b/registry/winlab/blocks/week-calendar/week-calendar.tsx @@ -1,6 +1,11 @@ import * as React from "react" import { cn } from "@/registry/winlab/lib/utils" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/registry/winlab/ui/tooltip" export type WeekEvent = { /** Unique within the week; defaults to start, end and title. */ @@ -36,12 +41,13 @@ type Block = { event: Placed from: number to: number - lane: number - lanes: number + /** How many earlier events it sits on; each one indents it 1rem. */ + depth: number } -// The timed events of one day, cut to the day, side by side where they -// overlap: each cluster of overlapping events shares the column in lanes. +// The timed events of one day, cut to the day. Where events overlap, the +// later one sits on top of the earlier, indented by one step, the way +// Calendar stacks them, so each keeps most of the column's width. function blocks(day: string, events: Placed[]): Block[] { const items = events .filter((e) => !e.allDay && e.first <= day && e.last >= day) @@ -49,34 +55,21 @@ function blocks(day: string, events: Placed[]): Block[] { event, from: event.first === day ? event.startMinute : 0, to: event.last === day ? event.endMinute : FULL, - lane: 0, - lanes: 1, + depth: 0, })) .sort((a, b) => a.from - b.from || b.to - a.to) - let cluster: typeof items = [] - let clusterEnd = -1 - const close = () => { - const lanes = Math.max(...cluster.map((item) => item.lane)) + 1 - for (const item of cluster) item.lanes = lanes - } - for (const item of items) { - if (item.from >= clusterEnd && cluster.length) { - close() - cluster = [] - } - const taken = new Set( - cluster.filter((other) => other.to > item.from).map((o) => o.lane) - ) - while (taken.has(item.lane)) item.lane++ - cluster.push(item) - clusterEnd = Math.max(clusterEnd, item.to) - } - if (cluster.length) close() + items.forEach((item, index) => { + const under = items.slice(0, index).filter((other) => other.to > item.from) + item.depth = under.length + ? Math.max(...under.map((other) => other.depth)) + 1 + : 0 + }) return items } // One week the way the week view of Calendar draws it: days in columns, -// hours down the side, each timed event a block from its start to its end, +// hours down the side, each timed event a block from its start to its end +// (overlaps stack, indented), the whole title and time in its tip, // all-day events as pills above the hours. The hours run 08:00 to 20:00 and // stretch to fit any event outside them. Today's date sits in a primary // circle. Below md the columns fold into one row per day, listing events. @@ -154,7 +147,7 @@ function WeekCalendar({ .flatMap(({ timed }, d) => timed.map( (b, i) => - `[data-week-scope=${scope}] [data-block="${d}-${i}"] { --top: ${percent(b.from - startHour * 60)}; --height: ${percent(b.to - b.from)}; --left: ${((b.lane / b.lanes) * 100).toFixed(3)}%; --width: ${(100 / b.lanes).toFixed(3)}%; }` + `[data-week-scope=${scope}] [data-block="${d}-${i}"] { --top: ${percent(b.from - startHour * 60)}; --height: ${percent(b.to - b.from)}; --left: ${b.depth}rem; --width: calc(100% - ${b.depth}rem); }` ) ) .join("\n") @@ -177,15 +170,23 @@ function WeekCalendar({ ) const pill = (event: Placed) => ( -
  • - {event.title} +
  • + + + } + > + {event.title} + + {event.title} +
  • ) @@ -252,29 +253,52 @@ function WeekCalendar({
  • -
    - + 0 && "border-t border-t-background", + b.event.quiet + ? "border-l-muted-foreground bg-muted text-muted-foreground" + : "border-l-primary bg-primary/10" + )} + /> + } > - {b.event.title} - - - {clock(b.event.startMinute)} - -
    + + {b.event.title} + + {/* A short block has room for the start only; + the tip has the whole span. */} + + {b.to - b.from < 90 ? ( + clock(b.from) + ) : ( + <> + + {clock(b.from)} 到 + {" "} + + {clock(b.to % FULL)} + + + )} + + + + {b.event.title},{b.event.span} + +
  • ))}