@hanzo/rooms 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-APACHE +205 -0
- package/LICENSE-MIT +21 -0
- package/README.md +23 -0
- package/dist/Account.d.ts +8 -0
- package/dist/Account.js +58 -0
- package/dist/Beluga.d.ts +12 -0
- package/dist/Beluga.js +152 -0
- package/dist/Billing.d.ts +1 -0
- package/dist/Billing.js +31 -0
- package/dist/Board.d.ts +9 -0
- package/dist/Board.js +447 -0
- package/dist/Boxes.d.ts +1 -0
- package/dist/Boxes.js +173 -0
- package/dist/CalScheduler.d.ts +15 -0
- package/dist/CalScheduler.js +196 -0
- package/dist/Chat.d.ts +35 -0
- package/dist/Chat.js +1049 -0
- package/dist/Column.d.ts +15 -0
- package/dist/Column.js +88 -0
- package/dist/Contacts.d.ts +1 -0
- package/dist/Contacts.js +301 -0
- package/dist/Directory.d.ts +24 -0
- package/dist/Directory.js +356 -0
- package/dist/Drive.d.ts +2 -0
- package/dist/Drive.js +466 -0
- package/dist/Find.d.ts +7 -0
- package/dist/Find.js +82 -0
- package/dist/Guide.d.ts +6 -0
- package/dist/Guide.js +272 -0
- package/dist/Home.d.ts +39 -0
- package/dist/Home.js +306 -0
- package/dist/Inbox.d.ts +9 -0
- package/dist/Inbox.js +193 -0
- package/dist/Market.d.ts +3 -0
- package/dist/Market.js +149 -0
- package/dist/Me.d.ts +27 -0
- package/dist/Me.js +251 -0
- package/dist/Meet.d.ts +9 -0
- package/dist/Meet.js +370 -0
- package/dist/ModeMark.d.ts +8 -0
- package/dist/ModeMark.js +18 -0
- package/dist/OrgSwitcher.d.ts +31 -0
- package/dist/OrgSwitcher.js +120 -0
- package/dist/Orgs.d.ts +10 -0
- package/dist/Orgs.js +582 -0
- package/dist/Profile.d.ts +4 -0
- package/dist/Profile.js +67 -0
- package/dist/Prose.d.ts +5 -0
- package/dist/Prose.js +111 -0
- package/dist/ProviderMark.d.ts +17 -0
- package/dist/ProviderMark.js +277 -0
- package/dist/Recents.d.ts +10 -0
- package/dist/Recents.js +91 -0
- package/dist/RightPane.d.ts +13 -0
- package/dist/RightPane.js +426 -0
- package/dist/Room.d.ts +8 -0
- package/dist/Room.js +50 -0
- package/dist/Scene.d.ts +12 -0
- package/dist/Scene.js +105 -0
- package/dist/Settings.d.ts +46 -0
- package/dist/Settings.js +321 -0
- package/dist/Setup.d.ts +1 -0
- package/dist/Setup.js +36 -0
- package/dist/Share.d.ts +3 -0
- package/dist/Share.js +130 -0
- package/dist/Shared.d.ts +1 -0
- package/dist/Shared.js +113 -0
- package/dist/Shell.d.ts +63 -0
- package/dist/Shell.js +1225 -0
- package/dist/Start.d.ts +1 -0
- package/dist/Start.js +96 -0
- package/dist/Support.d.ts +14 -0
- package/dist/Support.js +122 -0
- package/dist/Talk.d.ts +4 -0
- package/dist/Talk.js +107 -0
- package/dist/Upgrade.d.ts +11 -0
- package/dist/Upgrade.js +75 -0
- package/dist/Usage.d.ts +1 -0
- package/dist/Usage.js +21 -0
- package/dist/Vibe.d.ts +2 -0
- package/dist/Vibe.js +188 -0
- package/dist/Work.d.ts +12 -0
- package/dist/Work.js +256 -0
- package/dist/bots/Bots.d.ts +2 -0
- package/dist/bots/Bots.js +456 -0
- package/dist/bots/Lights.d.ts +8 -0
- package/dist/bots/Lights.js +13 -0
- package/dist/bots/api.d.ts +120 -0
- package/dist/bots/api.js +75 -0
- package/dist/bots/face.d.ts +5 -0
- package/dist/bots/face.js +13 -0
- package/dist/bots/ink.d.ts +7 -0
- package/dist/bots/ink.js +29 -0
- package/dist/bots/kit.d.ts +113 -0
- package/dist/bots/kit.js +43 -0
- package/dist/bots/runtime.d.ts +4 -0
- package/dist/bots/runtime.js +326 -0
- package/dist/card.d.ts +52 -0
- package/dist/card.js +52 -0
- package/dist/cast.d.ts +143 -0
- package/dist/cast.js +251 -0
- package/dist/connect.d.ts +29 -0
- package/dist/connect.js +64 -0
- package/dist/contacts-store.d.ts +111 -0
- package/dist/contacts-store.js +276 -0
- package/dist/conversations.d.ts +54 -0
- package/dist/conversations.js +98 -0
- package/dist/copy.d.ts +23 -0
- package/dist/copy.js +39 -0
- package/dist/crew.d.ts +36 -0
- package/dist/crew.js +304 -0
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +102 -0
- package/dist/enso.d.ts +70 -0
- package/dist/enso.js +112 -0
- package/dist/failure.d.ts +57 -0
- package/dist/failure.js +143 -0
- package/dist/ground.d.ts +32 -0
- package/dist/ground.js +32 -0
- package/dist/host.d.ts +76 -0
- package/dist/host.js +76 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/kept.d.ts +5 -0
- package/dist/kept.js +27 -0
- package/dist/lib/account.d.ts +49 -0
- package/dist/lib/account.js +229 -0
- package/dist/lib/ai.d.ts +46 -0
- package/dist/lib/ai.js +140 -0
- package/dist/lib/api.d.ts +43 -0
- package/dist/lib/api.js +62 -0
- package/dist/lib/bare.d.ts +9 -0
- package/dist/lib/bare.js +7 -0
- package/dist/lib/coding.d.ts +105 -0
- package/dist/lib/coding.js +87 -0
- package/dist/lib/destination.d.ts +102 -0
- package/dist/lib/destination.js +260 -0
- package/dist/lib/durable.d.ts +66 -0
- package/dist/lib/durable.js +107 -0
- package/dist/lib/first.d.ts +11 -0
- package/dist/lib/first.js +23 -0
- package/dist/lib/home.d.ts +61 -0
- package/dist/lib/home.js +54 -0
- package/dist/lib/host.d.ts +104 -0
- package/dist/lib/host.js +132 -0
- package/dist/lib/hostname.d.ts +1 -0
- package/dist/lib/hostname.js +14 -0
- package/dist/lib/hydrated.d.ts +1 -0
- package/dist/lib/hydrated.js +20 -0
- package/dist/lib/integrations.d.ts +21 -0
- package/dist/lib/integrations.js +1 -0
- package/dist/lib/intent.d.ts +6 -0
- package/dist/lib/intent.js +71 -0
- package/dist/lib/invite.d.ts +155 -0
- package/dist/lib/invite.js +245 -0
- package/dist/lib/limits.d.ts +55 -0
- package/dist/lib/limits.js +119 -0
- package/dist/lib/list.d.ts +9 -0
- package/dist/lib/list.js +11 -0
- package/dist/lib/mark.d.ts +13 -0
- package/dist/lib/mark.js +85 -0
- package/dist/lib/mix.d.ts +22 -0
- package/dist/lib/mix.js +22 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +106 -0
- package/dist/lib/org.d.ts +109 -0
- package/dist/lib/org.js +140 -0
- package/dist/lib/path.d.ts +9 -0
- package/dist/lib/path.js +29 -0
- package/dist/lib/plans.d.ts +193 -0
- package/dist/lib/plans.js +310 -0
- package/dist/lib/prose.d.ts +13 -0
- package/dist/lib/prose.js +62 -0
- package/dist/lib/reach.d.ts +17 -0
- package/dist/lib/reach.js +70 -0
- package/dist/lib/rooms.json +23 -0
- package/dist/lib/served.d.ts +20 -0
- package/dist/lib/served.js +92 -0
- package/dist/lib/session.d.ts +211 -0
- package/dist/lib/session.js +493 -0
- package/dist/lib/share.d.ts +131 -0
- package/dist/lib/share.js +216 -0
- package/dist/lib/signout.d.ts +14 -0
- package/dist/lib/signout.js +40 -0
- package/dist/lib/tags.d.ts +27 -0
- package/dist/lib/tags.js +46 -0
- package/dist/lib/tap.d.ts +17 -0
- package/dist/lib/tap.js +9 -0
- package/dist/lib/tier.d.ts +115 -0
- package/dist/lib/tier.js +207 -0
- package/dist/lib/tiers.d.ts +9 -0
- package/dist/lib/tiers.js +141 -0
- package/dist/lib/todo.d.ts +125 -0
- package/dist/lib/todo.js +379 -0
- package/dist/look.d.ts +1 -0
- package/dist/look.js +21 -0
- package/dist/meters.d.ts +8 -0
- package/dist/meters.js +31 -0
- package/dist/model.d.ts +4 -0
- package/dist/model.js +44 -0
- package/dist/network.d.ts +64 -0
- package/dist/network.js +216 -0
- package/dist/open.d.ts +81 -0
- package/dist/open.js +149 -0
- package/dist/pane.d.ts +92 -0
- package/dist/pane.js +231 -0
- package/dist/progress.d.ts +46 -0
- package/dist/progress.js +51 -0
- package/dist/recent.d.ts +67 -0
- package/dist/recent.js +161 -0
- package/dist/rooms.d.ts +131 -0
- package/dist/rooms.js +195 -0
- package/dist/roster.d.ts +28 -0
- package/dist/roster.js +70 -0
- package/dist/span.d.ts +14 -0
- package/dist/span.js +50 -0
- package/dist/speech.d.ts +44 -0
- package/dist/speech.js +229 -0
- package/dist/stars.d.ts +16 -0
- package/dist/stars.js +75 -0
- package/dist/tabs.d.ts +26 -0
- package/dist/tabs.js +25 -0
- package/dist/team.d.ts +11 -0
- package/dist/team.gen.d.ts +10 -0
- package/dist/team.gen.js +11 -0
- package/dist/team.js +30 -0
- package/dist/tools.d.ts +35 -0
- package/dist/tools.js +143 -0
- package/dist/tours.d.ts +24 -0
- package/dist/tours.js +311 -0
- package/dist/waiting.d.ts +15 -0
- package/dist/waiting.js +17 -0
- package/dist/where.d.ts +54 -0
- package/dist/where.js +14 -0
- package/package.json +108 -0
package/dist/Board.js
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// Board: the org's work, and the day beside it.
|
|
4
|
+
//
|
|
5
|
+
// NAMED BOARD, NOT TASKS, and the rename is the point rather than a tidy-up.
|
|
6
|
+
// `/v1/tasks` is Hanzo Tasks — the durable execution engine, ten thousand
|
|
7
|
+
// workflow runs deep — and this room is a board of forge issues. Two different
|
|
8
|
+
// things wearing one word is how somebody comes to believe the board IS the
|
|
9
|
+
// engine, and asks why their cards are not replayable. Tasks keeps the word it
|
|
10
|
+
// earned; this is the Board.
|
|
11
|
+
//
|
|
12
|
+
// A CARD IS A FORGE ISSUE. `/v1/todo` says so itself — "the column is a LABEL on
|
|
13
|
+
// the forge, so the board and the forge web UI are the same object seen twice:
|
|
14
|
+
// relabelling in either moves the card in both" — so this surface keeps no work
|
|
15
|
+
// of its own. Everything drawn here is a row somebody could also have moved from
|
|
16
|
+
// the forge, and everything written here is a row they will find there.
|
|
17
|
+
//
|
|
18
|
+
// TWO READINGS OF ONE SET OF CARDS, and one of them is up at a time: the board
|
|
19
|
+
// by column, the planner by date. The room opens on the board, which is what it
|
|
20
|
+
// is called. There is no inbox pane: the workspace already has one, and a second
|
|
21
|
+
// would be two places for the same arriving thing.
|
|
22
|
+
//
|
|
23
|
+
// WHAT A CARD CAN CARRY is what the store holds — a title, a description, a
|
|
24
|
+
// column, a priority, an assignee, labels, a start and a due date. Checklists
|
|
25
|
+
// ride in the description as markdown task items, which is not a workaround but
|
|
26
|
+
// the way a forge issue has always spelled one, so a step ticked here is ticked
|
|
27
|
+
// in the forge and a step written there arrives here. Covers and file
|
|
28
|
+
// attachments have nowhere to live yet, so they are not drawn.
|
|
29
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
30
|
+
import { useAgents, usePeople } from '@hanzo/ai/react';
|
|
31
|
+
import { Box, Text, View, XStack, YStack } from '@hanzo/ui';
|
|
32
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@hanzo/ui';
|
|
33
|
+
import { useIam, useOrganizations } from '@hanzo/iam/react';
|
|
34
|
+
import { Calendar, CalendarClock, Check, ChevronLeft, ChevronRight, Columns3, ListChecks, ListFilter, SquareTerminal, Plus, Tag, UserPlus, X, } from 'lucide-react';
|
|
35
|
+
import { useOpen } from './open.js';
|
|
36
|
+
import { useAi } from './lib/ai.js';
|
|
37
|
+
import { Face } from './cast.js';
|
|
38
|
+
import { say } from './failure.js';
|
|
39
|
+
import { AGENT_QUEUE, AGENT_WORKFLOW, reads, runOf, start as startRun, } from './lib/durable.js';
|
|
40
|
+
import { COLUMNS, PRIORITIES, addStep, board as readBoard, claim, edit, open as openCard, projects as readProjects, sift, steps, tick, } from './lib/todo.js';
|
|
41
|
+
import { BAD } from './lib/mix.js';
|
|
42
|
+
const DAY = 86400;
|
|
43
|
+
/** A pane with nothing in it yet, centred, saying which nothing it is. */
|
|
44
|
+
function Empty({ children }) {
|
|
45
|
+
return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$6", gap: "$2", children: [_jsx(Text, { render: "h1", fontSize: "$6", fontWeight: "600", color: "var(--foreground)", children: "Board" }), _jsx(Text, { fontSize: "$3", color: "$soft", text: "center", children: children })] }));
|
|
46
|
+
}
|
|
47
|
+
/** A quiet control that says its verb. */
|
|
48
|
+
function Act({ icon: Icon, label, on, onPress, }) {
|
|
49
|
+
return (_jsx(Box, { render: "button", onClick: onPress, "aria-pressed": on, px: "$2.5", py: "$1.5", rounded: "$2", bg: on ? '$raised' : 'transparent', borderWidth: 1, borderColor: on ? '$borderColor' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$1.5", children: [Icon ? _jsx(Icon, { size: 13, "aria-hidden": true }) : null, _jsx(Text, { fontSize: "$1", color: "$ink", children: label })] }) }));
|
|
50
|
+
}
|
|
51
|
+
/** A label: a neutral badge, so a board reads by its words, not a hue per name. */
|
|
52
|
+
function Label({ name }) {
|
|
53
|
+
return (_jsx(XStack, { px: "$2", py: "$0.5", rounded: "$2", bg: "$hover", borderWidth: 1, borderColor: "$borderColor", children: _jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: name }) }));
|
|
54
|
+
}
|
|
55
|
+
/** A date, as a person says it. */
|
|
56
|
+
function when(unix) {
|
|
57
|
+
if (!unix)
|
|
58
|
+
return '';
|
|
59
|
+
const at = new Date(unix * 1000);
|
|
60
|
+
const now = new Date();
|
|
61
|
+
const same = at.getFullYear() === now.getFullYear();
|
|
62
|
+
return at.toLocaleDateString(undefined, {
|
|
63
|
+
month: 'short',
|
|
64
|
+
day: 'numeric',
|
|
65
|
+
...(same ? {} : { year: 'numeric' }),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* WHAT A DAY IS CALLED, from where it sits relative to now.
|
|
70
|
+
*
|
|
71
|
+
* "Saturday" is the right name for a day three weeks out and the wrong one for
|
|
72
|
+
* the day you are standing in — a reader looking at the planner already knows
|
|
73
|
+
* what today is, and being told the weekday makes them work out whether that IS
|
|
74
|
+
* today. The three days a person has a word for get that word; the rest get the
|
|
75
|
+
* weekday, and one outside this week gets its date too, because "Tuesday" stops
|
|
76
|
+
* being an answer once there is more than one of them in view.
|
|
77
|
+
*/
|
|
78
|
+
function dayName(day, now = new Date()) {
|
|
79
|
+
const at = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
80
|
+
const days = Math.round((at(day) - at(now)) / 86400000);
|
|
81
|
+
if (days === 0)
|
|
82
|
+
return 'Today';
|
|
83
|
+
if (days === 1)
|
|
84
|
+
return 'Tomorrow';
|
|
85
|
+
if (days === -1)
|
|
86
|
+
return 'Yesterday';
|
|
87
|
+
const weekday = day.toLocaleDateString(undefined, { weekday: 'long' });
|
|
88
|
+
if (days > 1 && days < 7)
|
|
89
|
+
return weekday;
|
|
90
|
+
return `${weekday} ${day.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}`;
|
|
91
|
+
}
|
|
92
|
+
/** The value a date input wants, from unix seconds. */
|
|
93
|
+
const asInput = (unix) => unix ? new Date(unix * 1000).toISOString().slice(0, 10) : '';
|
|
94
|
+
// ── the card ────────────────────────────────────────────────────────────────
|
|
95
|
+
/**
|
|
96
|
+
* ONE CARD on the board.
|
|
97
|
+
*
|
|
98
|
+
* Everything on it is a field the row already carries, so a board of fifty
|
|
99
|
+
* costs the one read that drew it. The checklist count is the description's own
|
|
100
|
+
* task items — the same ones the forge renders.
|
|
101
|
+
*/
|
|
102
|
+
function Tile({ card, who, onOpen, }) {
|
|
103
|
+
const list = steps(card.description);
|
|
104
|
+
const done = list.filter((s) => s.done).length;
|
|
105
|
+
const late = card.dueAt ? card.dueAt * 1000 < Date.now() : false;
|
|
106
|
+
return (_jsx(View, { render: _jsx("div", { draggable: true }), onDragStart: (e) => e.dataTransfer.setData('text/plain', `${card.projectKey}#${card.number}`), display: "block", cursor: "grab", children: _jsx(Box, { render: "button", onClick: onOpen, width: "100%", p: "$3", gap: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", hoverStyle: { borderColor: '$ink' }, children: _jsxs(YStack, { gap: "$2", width: "100%", children: [card.labels.length > 0 ? (_jsx(XStack, { gap: "$1", flexWrap: "wrap", children: card.labels.slice(0, 4).map((l) => (_jsx(Label, { name: l }, l))) })) : null, _jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 3, text: "left", children: card.title }), _jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [card.dueAt ? (_jsxs(XStack, { items: "center", gap: "$1", children: [_jsx(CalendarClock, { size: 12, "aria-hidden": true }), _jsx(Text, { fontSize: "$1", color: late ? BAD : '$soft', children: when(card.dueAt) })] })) : null, list.length > 0 ? (_jsxs(XStack, { items: "center", gap: "$1", children: [_jsx(ListChecks, { size: 12, "aria-hidden": true }), _jsxs(Text, { fontSize: "$1", color: done === list.length ? '$ink' : '$soft', children: [done, "/", list.length] })] })) : null, card.priority && card.priority !== 'none' ? (_jsx(Text, { fontSize: "$1", color: "$soft", children: card.priority })) : null, _jsx(YStack, { flex: 1 }), card.assignee ? (_jsx(Face, { src: who?.avatar, emoji: who?.emoji, name: who?.name ?? card.assignee, size: 20 })) : null] })] }) }) }));
|
|
107
|
+
}
|
|
108
|
+
// ── the card, opened ────────────────────────────────────────────────────────
|
|
109
|
+
/**
|
|
110
|
+
* THE CARD IN FULL, over the board.
|
|
111
|
+
*
|
|
112
|
+
* Each panel writes one thing and re-reads, so nothing here holds a second copy
|
|
113
|
+
* of the card that could disagree with the store. The controls that have no
|
|
114
|
+
* field behind them — a cover, a file — are absent rather than drawn and inert.
|
|
115
|
+
*/
|
|
116
|
+
function Detail({ card, people, agents, onClose, onSaved, }) {
|
|
117
|
+
const { client } = useAi();
|
|
118
|
+
const [panel, setPanel] = useState(null);
|
|
119
|
+
const [title, setTitle] = useState(card.title);
|
|
120
|
+
const [body, setBody] = useState(card.description ?? '');
|
|
121
|
+
const [busy, setBusy] = useState(false);
|
|
122
|
+
const [wrong, setWrong] = useState(null);
|
|
123
|
+
const [step, setStep] = useState('');
|
|
124
|
+
/**
|
|
125
|
+
* THE DURABLE RUN this card's work is doing, if it was ever handed over.
|
|
126
|
+
*
|
|
127
|
+
* The board and the engine stay two stores; the workflow's NAME is the join,
|
|
128
|
+
* so this asks the engine directly rather than reading a column the card
|
|
129
|
+
* would have to keep in step. A card that was never started answers null,
|
|
130
|
+
* which the engine spells as an empty chain.
|
|
131
|
+
*/
|
|
132
|
+
const [run, setRun] = useState(null);
|
|
133
|
+
const [starting, setStarting] = useState(false);
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (!client)
|
|
136
|
+
return;
|
|
137
|
+
let live = true;
|
|
138
|
+
const stop = new AbortController();
|
|
139
|
+
void runOf(client, card.projectKey, card.number, stop.signal)
|
|
140
|
+
.then((r) => live && setRun(r))
|
|
141
|
+
// The engine being unreachable is not this card's failure to report; the
|
|
142
|
+
// run simply is not known, which is what null already means.
|
|
143
|
+
.catch(() => live && setRun(null));
|
|
144
|
+
return () => {
|
|
145
|
+
live = false;
|
|
146
|
+
stop.abort();
|
|
147
|
+
};
|
|
148
|
+
}, [client, card.projectKey, card.number]);
|
|
149
|
+
// WHAT THE STORE CALLS FINISHED. The column IS the status, so a card in the
|
|
150
|
+
// done column and a card marked done are one fact — reading it here rather
|
|
151
|
+
// than keeping a second flag is what keeps the circle and the column from
|
|
152
|
+
// ever disagreeing.
|
|
153
|
+
const finished = (card.status ?? '').toLowerCase() === 'done';
|
|
154
|
+
const list = steps(body);
|
|
155
|
+
const done = list.filter((s) => s.done).length;
|
|
156
|
+
const pct = list.length ? Math.round((done / list.length) * 100) : 0;
|
|
157
|
+
const who = people.find((p) => p.name === card.assignee);
|
|
158
|
+
const save = useCallback(async (patch) => {
|
|
159
|
+
if (!client)
|
|
160
|
+
return;
|
|
161
|
+
setBusy(true);
|
|
162
|
+
setWrong(null);
|
|
163
|
+
try {
|
|
164
|
+
await edit(client, card.projectKey, card.number, patch);
|
|
165
|
+
onSaved();
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
setWrong(e instanceof Error ? e.message : 'The change was refused');
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
setBusy(false);
|
|
172
|
+
}
|
|
173
|
+
}, [client, card.projectKey, card.number, onSaved]);
|
|
174
|
+
// TICKING A STEP REWRITES ITS LINE, and only its line — the description is
|
|
175
|
+
// the reader's prose as well as the checklist, so a save that reformatted it
|
|
176
|
+
// would eat what they wrote around the boxes.
|
|
177
|
+
const flip = (line, next) => {
|
|
178
|
+
const written = tick(body, line, next);
|
|
179
|
+
setBody(written);
|
|
180
|
+
void save({ description: written });
|
|
181
|
+
};
|
|
182
|
+
return (_jsxs(YStack, { position: "absolute", t: 0, r: 0, b: 0, width: 560, maxW: "100%", bg: "$panel", borderLeftWidth: 1, borderColor: "$borderColor", z: "var(--z-sheet)", children: [_jsxs(XStack, { items: "center", gap: "$2", px: "$4", py: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$1", color: "$soft", flex: 1, numberOfLines: 1, children: card.identifier }), _jsx(Box, { render: "button", onClick: onClose, p: "$1", rounded: "$2", "aria-label": "Close", children: _jsx(X, { size: 16, "aria-hidden": true }) })] }), _jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", p: "$4", gap: "$4", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Box, { render: "button", onClick: () => void save({ status: finished ? 'todo' : 'done' }), "aria-label": finished ? 'Reopen this card' : 'Mark this card done', "aria-pressed": finished, width: 22, height: 22, shrink: 0, rounded: 9999, borderWidth: finished ? 0 : 1.5, borderColor: "$soft", bg: finished ? '$ink' : 'transparent', items: "center", justify: "center", hoverStyle: { borderColor: '$ink' }, children: finished ? _jsx(Check, { size: 13, color: "var(--background)", "aria-hidden": true }) : null }), _jsx(Text, { render: _jsx("input", { value: title, onChange: (e) => setTitle(e.target.value), onBlur: () => title.trim() && title !== card.title && void save({ title: title.trim() }), "aria-label": "Card title" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$7", fontWeight: "600", textDecorationLine: finished ? 'line-through' : 'none' })] }), _jsxs(XStack, { gap: "$1.5", flexWrap: "wrap", children: [_jsx(Act, { icon: Tag, label: "Labels", on: panel === 'labels', onPress: () => setPanel(panel === 'labels' ? null : 'labels') }), _jsx(Act, { icon: Calendar, label: "Dates", on: panel === 'dates', onPress: () => setPanel(panel === 'dates' ? null : 'dates') }), _jsx(Act, { icon: UserPlus, label: "Members", on: panel === 'members', onPress: () => setPanel(panel === 'members' ? null : 'members') })] }), panel === 'labels' ? (_jsxs(YStack, { gap: "$2", p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Labels" }), card.labels.length > 0 ? (_jsx(XStack, { gap: "$1", flexWrap: "wrap", children: card.labels.map((l) => (_jsx(Label, { name: l }, l))) })) : (_jsx(Text, { fontSize: "$1", color: "$soft", children: "None yet." })), _jsx(Text, { fontSize: "$1", color: "$soft", children: "A label is a word on the forge issue, and its colour comes from the word \u2014 so it reads the same here and there. Labels are added on the forge; this board does not write them yet." })] })) : null, panel === 'dates' ? (_jsxs(YStack, { gap: "$2", p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Dates" }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Start" }), _jsx(Text, { render: _jsx("input", { type: "date", value: asInput(card.startAt), readOnly: true }), ...DATE })] }), _jsxs(YStack, { gap: "$1", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Due" }), _jsx(Text, { render: _jsx("input", { type: "date", value: asInput(card.dueAt), readOnly: true }), ...DATE })] })] }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "A card's dates come from its forge milestone, which is why they read here and cannot yet be set here." })] })) : null, panel === 'members' ? (_jsxs(YStack, { gap: "$2", p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Members" }), people.length === 0 ? (_jsx(Text, { fontSize: "$1", color: "$soft", children: "Nobody to assign yet." })) : (people.map((p) => (_jsx(Box, { render: "button", onClick: () => void save({ assignee: card.assignee === p.name ? '' : p.name }), width: "100%", p: "$2", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Face, { src: p.avatar, emoji: p.emoji, name: p.label, size: 22 }), _jsx(Text, { fontSize: "$2", color: "$ink", flex: 1, numberOfLines: 1, children: p.label }), p.agent ? (_jsx(Text, { fontSize: "$1", color: "$soft", children: "agent" })) : null, card.assignee === p.name ? _jsx(Check, { size: 14, "aria-hidden": true }) : null] }) }, p.id)))), _jsx(Box, { render: "button", onClick: () => {
|
|
183
|
+
if (!client || busy)
|
|
184
|
+
return;
|
|
185
|
+
setBusy(true);
|
|
186
|
+
void claim(client, card.projectKey, card.number)
|
|
187
|
+
.then(onSaved)
|
|
188
|
+
.catch((e) => setWrong(e.message))
|
|
189
|
+
.finally(() => setBusy(false));
|
|
190
|
+
}, p: "$2", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$1", color: "$ink", children: "Take it myself" }) })] })) : null, _jsxs(YStack, { gap: "$2", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Description" }), _jsx(Text, { render: _jsx("textarea", { value: body, onChange: (e) => setBody(e.target.value), onBlur: () => body !== (card.description ?? '') && void save({ description: body }), placeholder: "What is this card about?", rows: 5 }), width: "100%", bg: "transparent", borderWidth: 1, borderColor: "var(--border)", rounded: 8, p: 10, outlineStyle: "none", color: "inherit", fontSize: "$2" })] }), _jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", flex: 1, children: "Checklist" }), list.length > 0 ? (_jsxs(Text, { fontSize: "$1", color: "$soft", children: [pct, "%"] })) : null] }), list.length > 0 ? (_jsx(YStack, { height: 4, rounded: 9999, bg: "$hover", overflow: "hidden", children: _jsx(YStack, { width: `${pct}%`, height: 4, bg: "$ink" }) })) : null, list.map((s) => (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx("input", { type: "checkbox", checked: s.done, onChange: (e) => flip(s.line, e.target.checked), "aria-label": s.text }), _jsx(Text, { fontSize: "$2", color: s.done ? '$soft' : '$ink', textDecorationLine: s.done ? 'line-through' : 'none', flex: 1, children: s.text })] }, s.line))), _jsx(XStack, { gap: "$2", children: _jsx(Text, { render: _jsx("input", { value: step, onChange: (e) => setStep(e.target.value), onKeyDown: (e) => {
|
|
191
|
+
if (e.key !== 'Enter' || !step.trim())
|
|
192
|
+
return;
|
|
193
|
+
const written = addStep(body, step);
|
|
194
|
+
setBody(written);
|
|
195
|
+
setStep('');
|
|
196
|
+
void save({ description: written });
|
|
197
|
+
}, placeholder: "Add an item" }), flex: 1, bg: "transparent", borderWidth: 1, borderColor: "var(--border)", rounded: 6, px: 8, py: 6, outlineStyle: "none", color: "inherit", fontSize: "$2" }) })] }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Column" }), _jsxs(Select, { value: card.status, onValueChange: (v) => void save({ status: v }), children: [_jsx(SelectTrigger, { "aria-label": "Column", width: 160, children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: COLUMNS.map((c) => (_jsx(SelectItem, { value: c.id, children: c.label }, c.id))) })] })] }), _jsxs(YStack, { gap: "$1", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Priority" }), _jsxs(Select, { value: card.priority || 'none', onValueChange: (v) => void save({ priority: v }), children: [_jsx(SelectTrigger, { "aria-label": "Priority", width: 160, children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: PRIORITIES.map((p) => (_jsx(SelectItem, { value: p, children: p }, p))) })] })] })] }), _jsxs(YStack, { gap: "$2", pt: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Durable run" }), run ? (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(Text, { fontSize: "$2", color: "$ink", children: reads(run.status) }), _jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: run.workflowId }), run.historyLength ? (_jsxs(Text, { fontSize: "$1", color: "$soft", children: [run.historyLength, " events"] })) : null] })) : (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(Text, { fontSize: "$1", color: "$soft", flex: 1, children: card.assignee
|
|
198
|
+
? `${card.assignee} holds this. Hand the work to the engine and it survives a crash.`
|
|
199
|
+
: 'Nobody holds this yet — it can still be run, and the run is durable either way.' }), _jsx(Box, { render: "button", onClick: () => {
|
|
200
|
+
if (!client || starting)
|
|
201
|
+
return;
|
|
202
|
+
setStarting(true);
|
|
203
|
+
setWrong(null);
|
|
204
|
+
void startRun(client, card.projectKey, card.number, {
|
|
205
|
+
title: card.title,
|
|
206
|
+
queue: AGENT_QUEUE,
|
|
207
|
+
type: AGENT_WORKFLOW,
|
|
208
|
+
})
|
|
209
|
+
.then(setRun)
|
|
210
|
+
.catch((e) => setWrong(e.message))
|
|
211
|
+
.finally(() => setStarting(false));
|
|
212
|
+
}, px: "$2.5", py: "$1.5", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$1", color: "$ink", children: starting ? 'Handing over…' : 'Run it' }) })] }))] }), who ? (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Face, { src: who.avatar, emoji: who.emoji, name: who.label, size: 22 }), _jsxs(Text, { fontSize: "$2", color: "$soft", children: [who.label, " has this one."] })] })) : null, wrong ? (_jsx(Text, { fontSize: "$1", color: BAD, numberOfLines: 3, children: wrong })) : null] })] }));
|
|
213
|
+
}
|
|
214
|
+
/** A date the sheet shows and does not yet write. */
|
|
215
|
+
const DATE = {
|
|
216
|
+
bg: 'transparent',
|
|
217
|
+
color: 'inherit',
|
|
218
|
+
fontSize: '$2',
|
|
219
|
+
borderWidth: 1,
|
|
220
|
+
borderColor: 'var(--border)',
|
|
221
|
+
rounded: 6,
|
|
222
|
+
px: 8,
|
|
223
|
+
py: 6,
|
|
224
|
+
minW: 150,
|
|
225
|
+
};
|
|
226
|
+
// ── the planner ─────────────────────────────────────────────────────────────
|
|
227
|
+
/**
|
|
228
|
+
* THE DAY, beside the board.
|
|
229
|
+
*
|
|
230
|
+
* It draws the cards that are DUE on the day shown, against the hours — so the
|
|
231
|
+
* question it answers is "what is landing today", which is the one a board of
|
|
232
|
+
* columns cannot answer. A card with a due date but no hour sits at the top of
|
|
233
|
+
* the day rather than being placed at an hour nobody chose.
|
|
234
|
+
*/
|
|
235
|
+
function Planner({ cards, onOpen }) {
|
|
236
|
+
const [offset, setOffset] = useState(0);
|
|
237
|
+
const day = useMemo(() => {
|
|
238
|
+
const d = new Date();
|
|
239
|
+
d.setDate(d.getDate() + offset);
|
|
240
|
+
d.setHours(0, 0, 0, 0);
|
|
241
|
+
return d;
|
|
242
|
+
}, [offset]);
|
|
243
|
+
const start = Math.floor(day.getTime() / 1000);
|
|
244
|
+
const due = cards.filter((c) => c.dueAt && c.dueAt >= start && c.dueAt < start + DAY);
|
|
245
|
+
return (_jsxs(YStack, { flex: 1, minW: 0, minH: 0, children: [_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Calendar, { size: 14, "aria-hidden": true }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", flex: 1, children: day.toLocaleDateString(undefined, { month: 'long' }) }), _jsx(Box, { render: "button", onClick: () => setOffset((n) => n - 1), p: "$1", rounded: "$2", "aria-label": "Previous day", children: _jsx(ChevronLeft, { size: 14, "aria-hidden": true }) }), _jsx(Box, { render: "button", onClick: () => setOffset(0), px: "$2", py: "$1", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", children: _jsx(Text, { fontSize: "$1", color: "$ink", children: "Today" }) }), _jsx(Box, { render: "button", onClick: () => setOffset((n) => n + 1), p: "$1", rounded: "$2", "aria-label": "Next day", children: _jsx(ChevronRight, { size: 14, "aria-hidden": true }) })] }), _jsxs(YStack, { items: "center", py: "$2", gap: "$1", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: dayName(day) }), _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", children: day.getDate() })] }), _jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", children: [due.length > 0 ? (_jsxs(YStack, { p: "$3", gap: "$2", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Due today" }), due.map((c) => (_jsx(Box, { render: "button", onClick: () => onOpen(c), width: "100%", p: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", hoverStyle: { borderColor: '$ink' }, children: _jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 2, text: "left", children: c.title }) }, c.id)))] })) : null, Array.from({ length: 12 }, (_, i) => i + 8).map((hour) => (_jsx(XStack, { height: 56, borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(YStack, { width: 54, pt: "$1", pl: "$3", children: _jsxs(Text, { fontSize: "$1", color: "$soft", children: [hour % 12 === 0 ? 12 : hour % 12, " ", hour < 12 ? 'am' : 'pm'] }) }) }, hour)))] })] }));
|
|
246
|
+
}
|
|
247
|
+
// ── the board ───────────────────────────────────────────────────────────────
|
|
248
|
+
function Column({ column, cards, people, onDrop, onOpen, onAdd, loading, }) {
|
|
249
|
+
const [over, setOver] = useState(false);
|
|
250
|
+
const [adding, setAdding] = useState(false);
|
|
251
|
+
const [title, setTitle] = useState('');
|
|
252
|
+
return (_jsx(XStack, { onDragOver: (e) => {
|
|
253
|
+
e.preventDefault();
|
|
254
|
+
setOver(true);
|
|
255
|
+
}, onDragLeave: () => setOver(false), onDrop: (e) => {
|
|
256
|
+
e.preventDefault();
|
|
257
|
+
setOver(false);
|
|
258
|
+
const ref = e.dataTransfer.getData('text/plain');
|
|
259
|
+
if (ref)
|
|
260
|
+
onDrop(ref, column.id);
|
|
261
|
+
}, children: _jsxs(YStack, { width: 300, gap: "$2", p: "$2", rounded: "$3", borderWidth: 1, borderColor: over ? '$ink' : '$borderColor', bg: "$hover", maxH: "100%", children: [_jsxs(XStack, { items: "baseline", gap: "$2", px: "$2", pt: "$1", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", flex: 1, children: column.label }), _jsx(Text, { fontSize: "$1", color: "$soft", children: loading ? '' : cards.length })] }), _jsx(YStack, { gap: "$2", overflow: "scroll", shrink: 1, children: cards.map((c) => (_jsx(Tile, { card: c, who: people.find((p) => p.name === c.assignee), onOpen: () => onOpen(c) }, c.id))) }), adding ? (_jsx(YStack, { gap: "$2", children: _jsx(Text, { render: _jsx("textarea", { value: title, autoFocus: true, onChange: (e) => setTitle(e.target.value), onKeyDown: (e) => {
|
|
262
|
+
if (e.key === 'Escape') {
|
|
263
|
+
setAdding(false);
|
|
264
|
+
setTitle('');
|
|
265
|
+
}
|
|
266
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
267
|
+
e.preventDefault();
|
|
268
|
+
const said = title.trim();
|
|
269
|
+
if (!said)
|
|
270
|
+
return;
|
|
271
|
+
// THE TEXT SURVIVES A REFUSAL. Clearing before the answer is
|
|
272
|
+
// how a failed save costs somebody the sentence they wrote.
|
|
273
|
+
void onAdd(column.id, said).then((landed) => {
|
|
274
|
+
if (!landed)
|
|
275
|
+
return;
|
|
276
|
+
setTitle('');
|
|
277
|
+
setAdding(false);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}, placeholder: "What needs doing?", rows: 2 }), "$platform-web": { resize: 'none' }, width: "100%", bg: "var(--surface-card)", borderWidth: 1, borderColor: "var(--border)", rounded: 8, p: 8, outlineStyle: "none", color: "inherit", fontSize: "$2" }) })) : (_jsx(Box, { render: "button", onClick: () => setAdding(true), width: "100%", p: "$2", rounded: "$2",
|
|
281
|
+
// THE WHOLE ROW LIGHTS, not the words — it is a full-width target and
|
|
282
|
+
// a fill is what says so. The tint sits above the column's own
|
|
283
|
+
// ground so the row reads as raised rather than merely tinted.
|
|
284
|
+
hoverStyle: { bg: '$raised' }, children: _jsxs(XStack, { items: "center", gap: "$1.5", children: [_jsx(Plus, { size: 13, "aria-hidden": true }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Add a card" })] }) }))] }) }));
|
|
285
|
+
}
|
|
286
|
+
/** One narrowing, as a box a reader ticks. */
|
|
287
|
+
function Tick({ on, label, onPress, }) {
|
|
288
|
+
return (_jsx(Box, { render: "button", onClick: onPress, width: "100%", px: "$2", py: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(YStack, { width: 14, height: 14, rounded: 3, borderWidth: 1, borderColor: on ? '$ink' : '$borderColor', bg: on ? '$ink' : 'transparent', items: "center", justify: "center", shrink: 0, children: on ? _jsx(Check, { size: 10, color: "var(--background)", "aria-hidden": true }) : null }), typeof label === 'string' ? (_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: label })) : (label)] }) }));
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* THE FILTER, and it is what makes this board auditable.
|
|
292
|
+
*
|
|
293
|
+
* "Who did this" is the question a board of cards cannot answer by looking, and
|
|
294
|
+
* it is the one an org actually asks once agents work alongside people. The
|
|
295
|
+
* store answers it two ways at once — a row carries the `source` it was filed
|
|
296
|
+
* from and the `assignee` holding it — so "by an agent" means either, and this
|
|
297
|
+
* panel says so rather than picking one and being quietly wrong about the other.
|
|
298
|
+
*
|
|
299
|
+
* Every facet here is settled from a card the board already read. There is no
|
|
300
|
+
* narrowing that needs a second request, and none that needs a field the store
|
|
301
|
+
* does not keep.
|
|
302
|
+
*/
|
|
303
|
+
function Filter({ want, labels, onWant, onClose, }) {
|
|
304
|
+
const set = (patch) => onWant({ ...want, ...patch });
|
|
305
|
+
const flipLabel = (l) => {
|
|
306
|
+
const held = want.labels ?? [];
|
|
307
|
+
set({ labels: held.includes(l) ? held.filter((x) => x !== l) : [...held, l] });
|
|
308
|
+
};
|
|
309
|
+
const Head = ({ children }) => (_jsx(Text, { fontSize: "$1", color: "$soft", px: "$2", pt: "$2", children: children }));
|
|
310
|
+
return (_jsxs(YStack, { position: "absolute", t: 52, r: 12, width: 300, maxH: 560, overflow: "scroll", p: "$2", gap: "$0.5", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", z: "var(--z-dock)", children: [_jsxs(XStack, { items: "center", px: "$2", py: "$1", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", flex: 1, children: "Filter" }), _jsx(Box, { render: "button", onClick: onClose, p: "$1", rounded: "$2", "aria-label": "Close filter", children: _jsx(X, { size: 14, "aria-hidden": true }) })] }), _jsx(Tick, { on: want.holder === 'none', label: "Nobody has it", onPress: () => set({ holder: want.holder === 'none' ? undefined : 'none' }) }), _jsx(Tick, { on: want.holder === 'me', label: "Mine", onPress: () => set({ holder: want.holder === 'me' ? undefined : 'me' }) }), _jsx(Head, { children: "Who did the work" }), _jsx(Tick, { on: want.by === 'person', label: "A person", onPress: () => set({ by: want.by === 'person' ? undefined : 'person' }) }), _jsx(Tick, { on: want.by === 'agent', label: "An agent", onPress: () => set({ by: want.by === 'agent' ? undefined : 'agent' }) }), _jsx(Head, { children: "Card status" }), _jsx(Tick, { on: want.state === 'done', label: "Finished", onPress: () => set({ state: want.state === 'done' ? undefined : 'done' }) }), _jsx(Tick, { on: want.state === 'open', label: "Still open", onPress: () => set({ state: want.state === 'open' ? undefined : 'open' }) }), _jsx(Head, { children: "Due date" }), [['none', 'No dates'], ['late', 'Overdue'], ['day', 'Due in the next day'], ['week', 'Due in the next week'], ['month', 'Due in the next month']].map(([id, label]) => (_jsx(Tick, { on: want.due === id, label: label, onPress: () => set({ due: want.due === id ? undefined : id }) }, id))), labels.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Head, { children: "Labels" }), labels.map((l) => (_jsx(Tick, { on: (want.labels ?? []).includes(l), label: _jsx(Label, { name: l }), onPress: () => flipLabel(l) }, l)))] })) : null, _jsx(Head, { children: "Activity" }), [[7, 'Moved in the last week'], [14, 'Moved in the last two weeks'], [28, 'Moved in the last four weeks']].map(([days, label]) => (_jsx(Tick, { on: want.active === days, label: label, onPress: () => set({ active: want.active === days ? undefined : days }) }, days))), _jsx(Box, { render: "button", onClick: () => onWant({}), width: "100%", mt: "$2", p: "$2", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$1", color: "$ink", children: "Clear" }) })] }));
|
|
311
|
+
}
|
|
312
|
+
// ── the room ────────────────────────────────────────────────────────────────
|
|
313
|
+
/** Said when a write has no board to write to. It is the same event whichever
|
|
314
|
+
* write asked, so it is one sentence rather than two wordings of one. */
|
|
315
|
+
const NOWHERE = 'No project answered, so there is nowhere to file this card.';
|
|
316
|
+
function Cards() {
|
|
317
|
+
const { client } = useAi();
|
|
318
|
+
const { user } = useIam();
|
|
319
|
+
const { currentOrgId } = useOrganizations();
|
|
320
|
+
// WHO I AM on the forge, which is the name a card's assignee is spelled with.
|
|
321
|
+
const me = user?.name;
|
|
322
|
+
const { agents } = useAgents();
|
|
323
|
+
const { people: humans } = usePeople({ owner: currentOrgId ?? '', service: false });
|
|
324
|
+
const [cards, setCards] = useState(null);
|
|
325
|
+
const [key, setKey] = useState('');
|
|
326
|
+
const [keys, setKeys] = useState([]);
|
|
327
|
+
const [refused, setRefused] = useState(null);
|
|
328
|
+
// ONE STATE FOR TWO READINGS OF THE SAME CARDS. Two booleans — one per view
|
|
329
|
+
// — is how a room ends up showing neither or both, and the foot ended up
|
|
330
|
+
// lighting Board while the planner was the thing on screen.
|
|
331
|
+
const { aside, showAside } = useOpen();
|
|
332
|
+
const [view, setView] = useState('board');
|
|
333
|
+
const [opened, setOpened] = useState(null);
|
|
334
|
+
const [want, setWant] = useState({});
|
|
335
|
+
const [filtering, setFiltering] = useState(false);
|
|
336
|
+
const [nonce, bump] = useState(0);
|
|
337
|
+
// WHO CAN HAVE A CARD: the org's people and its agents, in one list, because
|
|
338
|
+
// the store keeps one assignee and does not ask which kind of worker it is.
|
|
339
|
+
const roster = useMemo(() => [
|
|
340
|
+
...humans.map((p) => ({
|
|
341
|
+
id: p.id,
|
|
342
|
+
name: p.name,
|
|
343
|
+
label: p.displayName || p.name,
|
|
344
|
+
agent: false,
|
|
345
|
+
})),
|
|
346
|
+
...agents.map((a) => ({
|
|
347
|
+
id: a.id,
|
|
348
|
+
name: a.name,
|
|
349
|
+
label: a.name,
|
|
350
|
+
avatar: a.avatar,
|
|
351
|
+
emoji: a.emoji,
|
|
352
|
+
agent: true,
|
|
353
|
+
})),
|
|
354
|
+
], [humans, agents]);
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
let live = true;
|
|
357
|
+
const stop = new AbortController();
|
|
358
|
+
void readProjects(client, stop.signal)
|
|
359
|
+
.then((ps) => {
|
|
360
|
+
if (!live)
|
|
361
|
+
return;
|
|
362
|
+
const ks = ps.map((p) => p.key).filter(Boolean);
|
|
363
|
+
setKeys(ks);
|
|
364
|
+
setKey((k) => k || ks[0] || '');
|
|
365
|
+
})
|
|
366
|
+
.catch(() => { });
|
|
367
|
+
void readBoard(client, key ? { key } : {}, stop.signal)
|
|
368
|
+
.then((rows) => live && (setCards(rows), setRefused(null)))
|
|
369
|
+
.catch((e) => live && (setCards([]), setRefused(say(e, 'this board'))));
|
|
370
|
+
return () => {
|
|
371
|
+
live = false;
|
|
372
|
+
stop.abort();
|
|
373
|
+
};
|
|
374
|
+
}, [client, key, nonce]);
|
|
375
|
+
// THE AGENT NAMES, so "an agent did this" can be answered about a card whose
|
|
376
|
+
// source says `team` but whose holder is a bot — which is what a person filing
|
|
377
|
+
// work and handing it over actually looks like.
|
|
378
|
+
const bots = useMemo(() => new Set(agents.map((a) => a.name)), [agents]);
|
|
379
|
+
const labels = useMemo(() => [...new Set((cards ?? []).flatMap((c) => c.labels))].sort(), [cards]);
|
|
380
|
+
const shown = useMemo(() => sift(cards ?? [], want, me, bots), [cards, want, me, bots]);
|
|
381
|
+
const narrowed = Object.values(want).some((v) => (Array.isArray(v) ? v.length : v !== undefined));
|
|
382
|
+
const filed = useMemo(() => {
|
|
383
|
+
const out = new Map();
|
|
384
|
+
for (const c of shown) {
|
|
385
|
+
const held = out.get(c.status);
|
|
386
|
+
if (held)
|
|
387
|
+
held.push(c);
|
|
388
|
+
else
|
|
389
|
+
out.set(c.status, [c]);
|
|
390
|
+
}
|
|
391
|
+
return out;
|
|
392
|
+
}, [shown]);
|
|
393
|
+
const move = useCallback((ref, status) => {
|
|
394
|
+
const [k, n] = ref.split('#');
|
|
395
|
+
// THE CARD'S OWN REF FIRST, then the board on screen, then the first one
|
|
396
|
+
// the store named — and nothing after that. The chain used to end
|
|
397
|
+
// `|| currentOrgId || 'hanzo'`, which files a write against a board whose
|
|
398
|
+
// owner nobody established: an org id is not a project key, and `hanzo` is
|
|
399
|
+
// somebody's real board. A write with no name to write under is refused.
|
|
400
|
+
const targetKey = k || key || keys[0];
|
|
401
|
+
if (!n)
|
|
402
|
+
return;
|
|
403
|
+
if (!targetKey)
|
|
404
|
+
return setRefused(NOWHERE);
|
|
405
|
+
void edit(client, targetKey, Number(n), { status })
|
|
406
|
+
.then(() => bump((x) => x + 1))
|
|
407
|
+
.catch((e) => setRefused(say(e, 'this card', 'save')));
|
|
408
|
+
}, [client, key, keys]);
|
|
409
|
+
/**
|
|
410
|
+
* Files a card, and ANSWERS WHETHER IT LANDED.
|
|
411
|
+
*/
|
|
412
|
+
const add = useCallback(async (status, title) => {
|
|
413
|
+
const targetKey = key || keys[0];
|
|
414
|
+
if (!targetKey) {
|
|
415
|
+
setRefused(NOWHERE);
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
await openCard(client, targetKey, { title, status });
|
|
420
|
+
setRefused(null);
|
|
421
|
+
if (!key) {
|
|
422
|
+
setKey(targetKey);
|
|
423
|
+
setKeys((prev) => (prev.includes(targetKey) ? prev : [...prev, targetKey]));
|
|
424
|
+
}
|
|
425
|
+
bump((x) => x + 1);
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
catch (e) {
|
|
429
|
+
setRefused(say(e, 'this card', 'save'));
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
}, [client, key, keys]);
|
|
433
|
+
return (_jsxs(YStack, { flex: 1, minH: 0, position: "relative", children: [_jsxs(XStack, { height: 52, shrink: 0, items: "center", gap: "$3", px: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { render: "h1", fontSize: "$4", fontWeight: "600", color: "$ink", numberOfLines: 1, children: "Board" }), _jsxs(Select, { value: key || 'all', onValueChange: setKey, children: [_jsx(SelectTrigger, { "aria-label": "Board", width: 140, $sm: { width: 100 }, children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All Boards" }, "all"), keys.map((k) => (_jsx(SelectItem, { value: k, children: k.charAt(0).toUpperCase() + k.slice(1) }, k)))] })] }), _jsx(YStack, { flex: 1 }), cards ? (_jsx(Text, { fontSize: "$1", color: "$soft", display: "none", $md: { display: 'flex' }, children: narrowed ? `${shown.length} of ${cards.length}` : `${cards.length} cards` })) : null, _jsx(Act, { icon: ListFilter, label: "Filter", on: filtering || narrowed, onPress: () => setFiltering((v) => !v) }), _jsx(XStack, { display: "none", $md: { display: 'flex' }, children: _jsx(Act, { icon: SquareTerminal, label: "Tabs & Shell", on: Boolean(aside), onPress: () => showAside(aside ? null : "tasks") }) })] }), _jsxs(XStack, { flex: 1, minH: 0, children: [view === 'planner' ? _jsx(Planner, { cards: cards ?? [], onOpen: setOpened }) : null, _jsxs(YStack, { flex: 1, minW: 0, minH: 0,
|
|
434
|
+
// One view at a time: the planner is the other reading of these cards.
|
|
435
|
+
display: view === 'planner' ? "none" : "flex", children: [refused ? (_jsxs(XStack, { items: "center", gap: "$2", mx: "$4", mt: "$3", px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$hover", children: [_jsxs(Text, { fontSize: "$1", color: "$soft", flex: 1, minW: 0, children: [refused, " Cards will appear here when it answers."] }), _jsx(Box, { render: "button", onClick: () => bump((x) => x + 1), shrink: 0, px: "$2", py: "$1", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$1", color: "$ink", children: "Try again" }) })] })) : null, _jsx(XStack, { flex: 1, minH: 0, overflow: "scroll", p: "$4", gap: "$3", items: "flex-start", children: COLUMNS.map((c) => (_jsx(Column, { column: c, cards: filed.get(c.id) ?? [], people: roster, onDrop: move, onOpen: setOpened, onAdd: add, loading: cards === null && !refused }, c.id))) })] })] }), _jsxs(XStack, { shrink: 0, items: "center", gap: "$2", px: "$3", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Act, { icon: Columns3, label: "Board", on: view === 'board', onPress: () => setView('board') }), _jsx(Act, { icon: Calendar, label: "Planner", on: view === 'planner', onPress: () => setView('planner') }), _jsx(YStack, { flex: 1 }), _jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, display: "none", $md: { display: 'flex' }, children: "A card is a forge issue \u2014 moving it here moves it there." })] }), filtering ? (_jsx(Filter, { want: want, labels: labels, onWant: setWant, onClose: () => setFiltering(false) })) : null, opened ? (_jsx(Detail, { card: cards?.find((c) => c.id === opened.id) ?? opened, people: roster, agents: agents, onClose: () => setOpened(null), onSaved: () => bump((x) => x + 1) })) : null] }));
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* The pane, and the session its reads need.
|
|
439
|
+
*
|
|
440
|
+
* SPLIT so the hooks below never run for a reader with none: every route this
|
|
441
|
+
* pane reads is org-scoped and answers 403 without one, and a hook cannot be
|
|
442
|
+
* called conditionally — so the condition is a component boundary rather than
|
|
443
|
+
* an `if` the rules forbid.
|
|
444
|
+
*/
|
|
445
|
+
export function Board() {
|
|
446
|
+
return _jsx(Cards, {});
|
|
447
|
+
}
|
package/dist/Boxes.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Boxes(): import("react").JSX.Element;
|