1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
|
<script lang="ts">
import type { WorkspaceEntry } from "@dispatch/wire";
import type { WorkspaceStore } from "../store.svelte";
import { relativeTime } from "../logic/view-model";
import type { Placement } from "../logic/tile-layout";
let {
ws,
store,
placement,
selected = false,
renaming = false,
active = false,
onRenameEnd,
}: {
ws: WorkspaceEntry;
store: WorkspaceStore;
/** The tile's resolved footprint (already preview-adjusted by the grid). */
placement: Placement;
selected?: boolean;
/** The parent-driven rename mode (the action bar's Rename action). */
renaming?: boolean;
/** At least one conversation in this workspace is generating/queued. */
active?: boolean;
/** The tile finished its rename session (saved or cancelled). */
onRenameEnd: () => void;
} = $props();
// ── Title: double-click to rename inline (semantics preserved from the
// WorkspaceCard), or enter via the action bar's Rename action (`renaming`).
let editing = $state(false);
let titleDraft = $state("");
let titleInput = $state<HTMLInputElement | undefined>();
let titleError = $state<string | null>(null);
function startEdit(): void {
titleDraft = ws.title;
titleError = null;
editing = true;
queueMicrotask(() => titleInput?.focus());
}
// Seed (only) when the parent-driven rename mode turns on.
$effect(() => {
if (renaming) startEdit();
});
async function saveTitle(): Promise<void> {
if (!editing) return;
const title = titleDraft.trim();
editing = false;
onRenameEnd();
if (title === "" || title === ws.title) return;
const result = await store.rename(ws.id, title);
if (!result.ok) titleError = result.error;
}
function cancelTitle(): void {
editing = false;
titleError = null;
onRenameEnd();
}
// The face's semantic key (tests read it).
const faceKey = $derived(`tile-${placement.id}`);
// Span-aware face: bigger footprints get more air + bigger type (all styles
// static so Tailwind sees them).
const wide = $derived(placement.colSpan > 1);
const tall = $derived(placement.rowSpan > 1);
const facePadding = $derived(wide || tall ? "p-5" : "p-4");
const titleClass = $derived(
wide && tall
? "text-3xl leading-tight line-clamp-3"
: wide
? "text-2xl leading-snug line-clamp-2"
: tall
? "text-xl leading-snug line-clamp-4"
: "text-lg leading-snug line-clamp-3",
);
/** 1×1 tiles are tight: "20 conv." — wider tiles spell "conversations" out. */
const countLabel = $derived(
ws.conversationCount === 1 ? (wide ? "conversation" : "conv.") : wide ? "conversations" : "conv.",
);
</script>
<div data-tile-face={faceKey} class={`relative flex h-full w-full flex-col ${facePadding}`}>
<!-- The slug label at the very top — the Win8 tile's small "app" caption. -->
<div class="flex items-center gap-1.5 text-[11px] font-semibold tracking-wider uppercase">
<span class="truncate opacity-70">/{ws.id}</span>
{#if active}
<span
class="loading loading-dots loading-xs shrink-0"
aria-label="Workspace has active conversations"
title="A conversation in this workspace is generating"></span>
{/if}
</div>
{#if ws.starred}
<!-- Starred is a badge, not a tint — the tone stays the tile's identity. -->
<span
class="absolute top-2.5 right-3 text-lg leading-none opacity-90"
aria-label="Starred workspace"
title="Starred — priority in the agent queue">★</span
>
{/if}
{#if editing}
<input
bind:this={titleInput}
bind:value={titleDraft}
class="input input-sm mt-2 w-full border-current/40 bg-black/25 text-inherit placeholder:text-current/50"
aria-label="Workspace title"
onkeydown={(e) => {
if (e.key === "Enter") void saveTitle();
else if (e.key === "Escape") cancelTitle();
}}
onblur={() => void saveTitle()}
/>
{:else}
<!-- svelte-ignore a11y_no_static_element_interactions, a11y_click_events_have_key_events -->
<span
class={`mt-1 cursor-default font-bold ${titleClass}`}
title="Double-click to rename"
ondblclick={startEdit}>{ws.title}</span
>
{/if}
{#if titleError}
<p class="mt-1 text-xs font-semibold">{titleError}</p>
{/if}
<!-- The stat line pinned to the tile's bottom edge, like a live tile's
status row. Single-line, truncation-safe (narrow 1×1 tiles stay clean). -->
<div
class="mt-auto flex items-center gap-2 overflow-hidden border-t border-current/20 pt-2 text-xs font-medium whitespace-nowrap"
>
<span class="min-w-0 truncate">
{ws.conversationCount}
{countLabel}
</span>
<span class="ml-auto shrink-0 font-mono opacity-70">
{relativeTime(ws.lastActivityAt, Date.now())}
</span>
</div>
</div>
|