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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
7 changes: 6 additions & 1 deletion app/_docs/demos/week-calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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") },
{
Expand Down
3 changes: 2 additions & 1 deletion registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
138 changes: 81 additions & 57 deletions registry/winlab/blocks/week-calendar/week-calendar.tsx
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down Expand Up @@ -36,47 +41,35 @@ 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)
.map((event) => ({
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.
Expand Down Expand Up @@ -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")
Expand All @@ -177,15 +170,23 @@ function WeekCalendar({
</span>
)
const pill = (event: Placed) => (
<li
key={event.key}
title={event.title}
className={cn(
"truncate rounded-full bg-muted px-2",
event.quiet && "text-muted-foreground"
)}
>
{event.title}
<li key={event.key} className="min-w-0">
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
className={cn(
"w-full truncate rounded-full bg-muted px-2 text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50",
event.quiet && "text-muted-foreground"
)}
/>
}
>
{event.title}
</TooltipTrigger>
<TooltipContent>{event.title}</TooltipContent>
</Tooltip>
</li>
)

Expand Down Expand Up @@ -252,29 +253,52 @@ function WeekCalendar({
<li
key={b.event.key}
data-block={`${d}-${i}`}
title={`${b.event.title},${b.event.span}`}
className="absolute top-(--top) left-(--left) h-(--height) w-(--width) p-px"
className="absolute top-(--top) left-(--left) h-(--height) w-(--width) bg-background pb-px"
>
<div
className={cn(
"flex size-full min-h-6 flex-col overflow-hidden border-l-2 px-2",
b.event.quiet
? "border-muted-foreground bg-muted text-muted-foreground"
: "border-primary bg-primary/10"
)}
>
<span
className={cn(
"truncate",
!b.event.quiet && "font-medium"
)}
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
className={cn(
"flex size-full min-h-6 flex-col overflow-hidden border-l-2 px-2 text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50",
b.depth > 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}
</span>
<span className="truncate text-muted-foreground tabular-nums">
{clock(b.event.startMinute)}
</span>
</div>
<span
className={cn(
"break-words",
!b.event.quiet && "font-medium"
)}
>
{b.event.title}
</span>
{/* A short block has room for the start only;
the tip has the whole span. */}
<span className="text-muted-foreground tabular-nums">
{b.to - b.from < 90 ? (
clock(b.from)
) : (
<>
<span className="whitespace-nowrap">
{clock(b.from)} 到
</span>{" "}
<span className="whitespace-nowrap">
{clock(b.to % FULL)}
</span>
</>
)}
</span>
</TooltipTrigger>
<TooltipContent>
{b.event.title},{b.event.span}
</TooltipContent>
</Tooltip>
</li>
))}
</ul>
Expand Down
Loading