@hiroleague/taskmanager 0.0.2 → 0.0.4
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/README.md +1 -1
- package/dist/assets/architecture-YZFGNWBL-DoE0KxgG.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-DeuBhy7X.js +36 -0
- package/dist/assets/{blockDiagram-DXYQGD6D-DQzEOPT2.js → blockDiagram-DXYQGD6D-BDBy9ns9.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-B2Yfcwbo.js → c4Diagram-AHTNJAMY-CpqJj_8a.js} +1 -1
- package/dist/assets/channel-PHRyjspt.js +1 -0
- package/dist/assets/{chunk-2KRD3SAO-C2e-_49I.js → chunk-2KRD3SAO-DEpUsxdZ.js} +1 -1
- package/dist/assets/chunk-336JU56O-BGQvSwLk.js +2 -0
- package/dist/assets/chunk-426QAEUC-Cl9nQN9c.js +1 -0
- package/dist/assets/{chunk-4TB4RGXK-AZq3s1Dh.js → chunk-4TB4RGXK-Dq7aiIrZ.js} +2 -2
- package/dist/assets/{chunk-5FUZZQ4R-XEga0hMC.js → chunk-5FUZZQ4R-B_HuuUjf.js} +1 -1
- package/dist/assets/{chunk-5PVQY5BW-BrmXs2Gs.js → chunk-5PVQY5BW-cGfZCZGU.js} +2 -2
- package/dist/assets/{chunk-67CJDMHE-B1-M78qu.js → chunk-67CJDMHE-BMYAVZfw.js} +1 -1
- package/dist/assets/{chunk-7N4EOEYR-D7mYFpz-.js → chunk-7N4EOEYR-Ct-EY7Nc.js} +1 -1
- package/dist/assets/{chunk-AA7GKIK3-VWI9k39i.js → chunk-AA7GKIK3-Bd4HFpeo.js} +1 -1
- package/dist/assets/{chunk-CIAEETIT-hnu4zamm.js → chunk-CIAEETIT-CrFUkPMT.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-DxUqDyxy.js → chunk-EDXVE4YY-DMDyt0NF.js} +1 -1
- package/dist/assets/{chunk-ENJZ2VHE-BgZKYo1l.js → chunk-ENJZ2VHE-DrWzOrpd.js} +1 -1
- package/dist/assets/{chunk-FOC6F5B3-BJsh9nO9.js → chunk-FOC6F5B3-Bemzq96j.js} +1 -1
- package/dist/assets/{chunk-ICPOFSXX-BNR1V8rT.js → chunk-ICPOFSXX-DkUVjrLw.js} +5 -5
- package/dist/assets/{chunk-K5T4RW27-BLIPdXaZ.js → chunk-K5T4RW27-ALKIf000.js} +5 -5
- package/dist/assets/{chunk-KGLVRYIC-DvaW2TkT.js → chunk-KGLVRYIC-Bg6HNTZ-.js} +1 -1
- package/dist/assets/{chunk-LIHQZDEY-CUsM0M11.js → chunk-LIHQZDEY-DeyGongE.js} +1 -1
- package/dist/assets/{chunk-ORNJ4GCN-CfluNV0_.js → chunk-ORNJ4GCN-Bx83s1bJ.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-CkWzw4JX.js → chunk-OYMX7WX6-BqRUtRpL.js} +1 -1
- package/dist/assets/{chunk-U2HBQHQK-DTJPeU7W.js → chunk-U2HBQHQK-DogcerR6.js} +1 -1
- package/dist/assets/{chunk-X2U36JSP-CrTnmMqG.js → chunk-X2U36JSP-CwVWdmZV.js} +1 -1
- package/dist/assets/chunk-XPW4576I-DQpNCogT.js +32 -0
- package/dist/assets/{chunk-YZCP3GAM-9wq0QKUn.js → chunk-YZCP3GAM-crQSbji9.js} +1 -1
- package/dist/assets/{chunk-ZZ45TVLE-D3I1kLlo.js → chunk-ZZ45TVLE-Bk1S1YtS.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-B_TabGaU.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-CGnZkUWw.js +1 -0
- package/dist/assets/clone-D4ka472w.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-BygGvZGW.js → cose-bilkent-S5V4N54A-RBTHUit8.js} +1 -1
- package/dist/assets/cytoscape.esm-BGJwlmkf.js +321 -0
- package/dist/assets/dagre-B32eYLtm.js +1 -0
- package/dist/assets/{dagre-KV5264BT-lveZDhBf.js → dagre-KV5264BT-nX7tuXXn.js} +1 -1
- package/dist/assets/diagram-5BDNPKRD-DRxMXlQr.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-CoojevGm.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-CWtJyfVW.js +43 -0
- package/dist/assets/diagram-TYMM5635-CsDJC4Hq.js +24 -0
- package/dist/assets/{erDiagram-SMLLAGMA-dx09stuy.js → erDiagram-SMLLAGMA-Cf7Xtd9A.js} +2 -2
- package/dist/assets/{flatten-B2BZ0pzY.js → flatten-CYX_pHZ7.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CJi2WISS.js → flowDiagram-DWJPFMVM-DQaeR16a.js} +3 -3
- package/dist/assets/{ganttDiagram-T4ZO3ILL-OCTvbRxF.js → ganttDiagram-T4ZO3ILL-8EIcztcH.js} +1 -1
- package/dist/assets/gitGraph-7Q5UKJZL-BH9A1SAZ.js +1 -0
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-Bjj94M12.js → gitGraphDiagram-UUTBAWPF-DO9ODqYw.js} +1 -1
- package/dist/assets/graphlib-bPBqlJKT.js +1 -0
- package/dist/assets/identity-Me9aart9.js +1 -0
- package/dist/assets/index-BpzHnKdP.css +1 -0
- package/dist/assets/index-DmNErTAP.js +273 -0
- package/dist/assets/info-OMHHGYJF-BvKR-zWh.js +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-pRTXCm5C.js +2 -0
- package/dist/assets/isEmpty-Cu0k-j1j.js +1 -0
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-Cnc1bwBo.js → ishikawaDiagram-UXIWVN3A-BP2YE5QI.js} +2 -2
- package/dist/assets/{journeyDiagram-VCZTEJTY-BkMxoaPq.js → journeyDiagram-VCZTEJTY-B3l2juoL.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-CwHbIze0.js → kanban-definition-6JOO6SKY-BpIpEOZZ.js} +4 -4
- package/dist/assets/{line-DNzQATGr.js → line-otOkzGl8.js} +1 -1
- package/dist/assets/mermaid-parser.core-xWsW24Gq.js +4 -0
- package/dist/assets/{mindmap-definition-QFDTVHPH-DswAJiEd.js → mindmap-definition-QFDTVHPH-B9khyC7X.js} +3 -3
- package/dist/assets/packet-4T2RLAQJ-D8Dw3nmf.js +1 -0
- package/dist/assets/pie-ZZUOXDRM-ZghowlAE.js +1 -0
- package/dist/assets/{pieDiagram-DEJITSTG-DgQTCddl.js → pieDiagram-DEJITSTG-v32hL3i7.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-c0iZxo2I.js → quadrantDiagram-34T5L4WZ-DIL3GDFt.js} +1 -1
- package/dist/assets/radar-PYXPWWZC-D-PK3JOd.js +1 -0
- package/dist/assets/reduce-CImcgAcU.js +1 -0
- package/dist/assets/{requirementDiagram-MS252O5E-D1moa23Z.js → requirementDiagram-MS252O5E-D8os2-4y.js} +2 -2
- package/dist/assets/{sankeyDiagram-XADWPNL6-woJZoQ58.js → sankeyDiagram-XADWPNL6-BV70D4l5.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-Dvhj7HGn.js → sequenceDiagram-FGHM5R23-Cwu8hQW1.js} +1 -1
- package/dist/assets/stateDiagram-FHFEXIEX-oYUWv7Fb.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-CFUTpFu-.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-z-IncVmK.js → timeline-definition-GMOUNBTQ-CxSdKxpL.js} +1 -1
- package/dist/assets/treeView-SZITEDCU-uVgaJQzG.js +1 -0
- package/dist/assets/treemap-W4RFUUIX-Dcad_9AN.js +1 -0
- package/dist/assets/vennDiagram-DHZGUBPP-D4wgD7QI.js +34 -0
- package/dist/assets/wardley-RL74JXVD-CFXrK8mx.js +1 -0
- package/dist/assets/{wardleyDiagram-NUSXRM2D-D-kouujI.js → wardleyDiagram-NUSXRM2D-5Q201ea3.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-D1lnM0pL.js → xychartDiagram-5P7HB3ND-BPZv_axd.js} +3 -3
- package/dist/index.html +17 -13
- package/package.json +2 -4
- package/skills/hiro-task-manager-cli/SKILL.md +6 -4
- package/skills/hiro-task-manager-cli/reference/cli-access-policy.md +1 -0
- package/skills/hiro-task-manager-cli/reference/releases.md +14 -0
- package/src/cli/bootstrap/launcher.ts +19 -5
- package/src/cli/bootstrap/program.test.ts +46 -0
- package/src/cli/bootstrap/program.ts +50 -1
- package/src/cli/commands/query.ts +56 -56
- package/src/cli/commands/releases.ts +22 -0
- package/src/cli/handlers/boards.test.ts +669 -669
- package/src/cli/handlers/cli-wiring.test.ts +38 -1
- package/src/cli/handlers/releases.ts +15 -0
- package/src/cli/handlers/search.test.ts +374 -374
- package/src/cli/handlers/search.ts +17 -17
- package/src/cli/lib/cli-http-errors.test.ts +85 -85
- package/src/cli/lib/launcherUi.test.ts +74 -0
- package/src/cli/lib/launcherUi.ts +47 -0
- package/src/cli/lib/write/releases.ts +64 -1
- package/src/cli/lib/write-result.test.ts +3 -0
- package/src/cli/lib/write-result.ts +3 -0
- package/src/cli/lib/writeCommands.breadth.test.ts +143 -0
- package/src/cli/lib/writeCommands.ts +1 -0
- package/src/cli/subprocess.real-stack.test.ts +625 -611
- package/src/cli/subprocess.smoke.test.ts +954 -954
- package/src/client/api/useBoardChangeStream.ts +421 -168
- package/src/client/api/useBoardIndexStream.ts +35 -0
- package/src/client/components/board/BoardStatsChips.tsx +233 -233
- package/src/client/components/board/BoardStatsContext.tsx +41 -41
- package/src/client/components/board/boardHeaderButtonStyles.ts +38 -38
- package/src/client/components/board/shortcuts/useBoardShortcutKeydown.ts +49 -49
- package/src/client/components/board/useBoardCanvasPanScroll.ts +108 -108
- package/src/client/components/board/useBoardTaskContainerDroppableReact.ts +33 -33
- package/src/client/components/board/useBoardTaskSortableReact.ts +26 -26
- package/src/client/components/layout/AppShell.tsx +5 -2
- package/src/client/components/layout/NotificationToasts.tsx +38 -1
- package/src/client/components/multi-select.tsx +1206 -1206
- package/src/client/components/routing/BoardPage.tsx +20 -20
- package/src/client/components/routing/NavigationRegistrar.tsx +13 -13
- package/src/client/components/task/TaskCard.tsx +643 -643
- package/src/client/components/ui/badge.tsx +49 -49
- package/src/client/components/ui/button.tsx +65 -65
- package/src/client/components/ui/command.tsx +193 -193
- package/src/client/components/ui/dialog.tsx +163 -163
- package/src/client/components/ui/input-group.tsx +155 -155
- package/src/client/components/ui/input.tsx +19 -19
- package/src/client/components/ui/popover.tsx +87 -87
- package/src/client/components/ui/separator.tsx +28 -28
- package/src/client/components/ui/textarea.tsx +18 -18
- package/src/client/index.css +248 -248
- package/src/client/lib/appNavigate.ts +16 -16
- package/src/client/lib/taskCardDate.ts +111 -111
- package/src/client/lib/utils.ts +6 -6
- package/src/client/store/notificationUi.ts +14 -0
- package/src/server/auth.ts +351 -351
- package/src/server/events.ts +31 -4
- package/src/server/migrations/registry.ts +43 -43
- package/src/server/notificationEvents.ts +8 -1
- package/src/server/routes/boards.ts +15 -1
- package/src/server/routes/trash.ts +6 -1
- package/src/shared/boardEvents.ts +6 -0
- package/src/shared/runtimeConfig.ts +256 -256
- package/src/shared/skillsInstall.ts +2 -3
- package/dist/assets/architecture-YZFGNWBL-C1MoQeSs.js +0 -1
- package/dist/assets/architectureDiagram-Q4EWVU46-DUEfvDBu.js +0 -36
- package/dist/assets/channel-yBmN_ln0.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-Dx_f-9b7.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-CSfvZ-nt.js +0 -1
- package/dist/assets/clone-CXokakwV.js +0 -1
- package/dist/assets/cytoscape.esm-BIYWHPG0.js +0 -321
- package/dist/assets/dagre-Do0eD9eI.js +0 -1
- package/dist/assets/diagram-5BDNPKRD-Dq5yM_uY.js +0 -10
- package/dist/assets/diagram-G4DWMVQ6-D-SYOmKm.js +0 -24
- package/dist/assets/diagram-MMDJMWI5-lU5t9BZA.js +0 -43
- package/dist/assets/diagram-TYMM5635-6tfUbY3R.js +0 -24
- package/dist/assets/gitGraph-7Q5UKJZL-BXTuQaDM.js +0 -1
- package/dist/assets/graphlib-BIlXYGdM.js +0 -1
- package/dist/assets/identity-D4WOnl_h.js +0 -1
- package/dist/assets/index-CZZuue3D.js +0 -304
- package/dist/assets/index-hMFTu7sr.css +0 -1
- package/dist/assets/info-OMHHGYJF-BeeKt8-X.js +0 -1
- package/dist/assets/infoDiagram-42DDH7IO-wq_opQKO.js +0 -2
- package/dist/assets/mermaid-parser.core-DrLhKJ48.js +0 -4
- package/dist/assets/packet-4T2RLAQJ-DQ-H9_jd.js +0 -1
- package/dist/assets/pie-ZZUOXDRM-BSj0Jsyj.js +0 -1
- package/dist/assets/radar-PYXPWWZC-B7-oRPFL.js +0 -1
- package/dist/assets/reduce-Uumu9GdR.js +0 -1
- package/dist/assets/stateDiagram-FHFEXIEX-Dx5CjenB.js +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-C_PkrTdc.js +0 -1
- package/dist/assets/treeView-SZITEDCU-CFXle9Az.js +0 -1
- package/dist/assets/treemap-W4RFUUIX-CAW3vWh8.js +0 -1
- package/dist/assets/vennDiagram-DHZGUBPP-CT1ehozU.js +0 -34
- package/dist/assets/wardley-RL74JXVD-7q3ju4kc.js +0 -1
- package/scripts/postinstall-message.mjs +0 -160
- /package/dist/assets/{chunk-4BX2VUAB-ean5NKtU.js → chunk-4BX2VUAB-C70mcfQR.js} +0 -0
- /package/dist/assets/{chunk-55IACEB6-CvSRyJqy.js → chunk-55IACEB6-CWfnqcLM.js} +0 -0
- /package/dist/assets/{chunk-BSJP7CBP-D8kBlJsf.js → chunk-BSJP7CBP-B0LrXV9y.js} +0 -0
- /package/dist/assets/{chunk-FMBD7UC4-DrNhFt1N.js → chunk-FMBD7UC4-_mV71Mwu.js} +0 -0
- /package/dist/assets/{chunk-QZHKN3VN-Csp3OYJY.js → chunk-QZHKN3VN-t2nrsegL.js} +0 -0
- /package/dist/assets/{katex-8mXVa4k3.js → katex-B2dtGfSp.js} +0 -0
- /package/dist/assets/{rough.esm-DtEqI08j.js → rough.esm-DEh6Frf9.js} +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { devDirectApiOrigin } from "./devDirectApiOrigin";
|
|
4
|
+
import { boardKeys } from "./queries";
|
|
5
|
+
|
|
6
|
+
/** Shell-wide SSE: `GET /api/events` without boardId — keep sidebar board list in sync with CLI/agent writes. */
|
|
7
|
+
function boardIndexEventsUrl(): string {
|
|
8
|
+
const path = "/api/events";
|
|
9
|
+
if (import.meta.env.PROD) return path;
|
|
10
|
+
const raw = import.meta.env.VITE_API_ORIGIN as string | undefined;
|
|
11
|
+
const fallbackOrigin = devDirectApiOrigin();
|
|
12
|
+
const origin =
|
|
13
|
+
raw && raw.length > 0 ? raw.replace(/\/$/, "") : fallbackOrigin;
|
|
14
|
+
return `${origin}${path}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useBoardIndexStream(): void {
|
|
18
|
+
const qc = useQueryClient();
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const es = new EventSource(boardIndexEventsUrl(), {
|
|
22
|
+
withCredentials: true,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const onIndexChanged = () => {
|
|
26
|
+
void qc.invalidateQueries({ queryKey: boardKeys.all, exact: true });
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
es.addEventListener("board-index-changed", onIndexChanged);
|
|
30
|
+
return () => {
|
|
31
|
+
es.removeEventListener("board-index-changed", onIndexChanged);
|
|
32
|
+
es.close();
|
|
33
|
+
};
|
|
34
|
+
}, [qc]);
|
|
35
|
+
}
|
|
@@ -1,233 +1,233 @@
|
|
|
1
|
-
import NumberFlow, {
|
|
2
|
-
continuous,
|
|
3
|
-
useCanAnimate,
|
|
4
|
-
} from "@number-flow/react";
|
|
5
|
-
import { useLayoutEffect, useRef, useState } from "react";
|
|
6
|
-
import type { TaskCountStat } from "../../../shared/boardStats";
|
|
7
|
-
import { cn } from "@/lib/utils";
|
|
8
|
-
|
|
9
|
-
/** Low-saturation chip fills so T/O/C read as soft status tints, not full banners. */
|
|
10
|
-
const chipBoardT =
|
|
11
|
-
"border-border/60 bg-muted/55 text-foreground dark:border-border/50 dark:bg-muted/45";
|
|
12
|
-
/** O = non-closed tasks — orange tint with readable opacity (avoid washed-out / red-looking tints). */
|
|
13
|
-
const chipBoardO =
|
|
14
|
-
"border-orange-500/45 bg-orange-500/68 text-orange-950 dark:border-orange-400/50 dark:text-orange-50";
|
|
15
|
-
const chipBoardC =
|
|
16
|
-
"border-emerald-600/35 bg-emerald-600/68 text-emerald-950 dark:border-emerald-500/40 dark:text-emerald-100";
|
|
17
|
-
|
|
18
|
-
const chipBoardL =
|
|
19
|
-
"border-border/60 bg-muted/55 text-foreground dark:border-border/50 dark:bg-muted/45";
|
|
20
|
-
|
|
21
|
-
const chipListT =
|
|
22
|
-
"border-border/50 bg-muted/35 text-foreground dark:bg-muted/30";
|
|
23
|
-
const chipListO =
|
|
24
|
-
"border-orange-500/38 bg-orange-500/60 text-orange-950 dark:border-orange-400/42 dark:text-orange-50";
|
|
25
|
-
const chipListC =
|
|
26
|
-
"border-emerald-600/18 bg-emerald-600/50 text-emerald-950 dark:border-emerald-500/18 dark:text-emerald-100";
|
|
27
|
-
|
|
28
|
-
const STATS_FLOW_TIMING = {
|
|
29
|
-
spinTiming: {
|
|
30
|
-
duration: 450,
|
|
31
|
-
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
|
|
32
|
-
} as const,
|
|
33
|
-
transformTiming: {
|
|
34
|
-
duration: 400,
|
|
35
|
-
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
|
|
36
|
-
} as const,
|
|
37
|
-
opacityTiming: {
|
|
38
|
-
duration: 220,
|
|
39
|
-
easing: "ease-out",
|
|
40
|
-
} as const,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
function StatChip({
|
|
44
|
-
label,
|
|
45
|
-
value,
|
|
46
|
-
className,
|
|
47
|
-
showSpinner,
|
|
48
|
-
entryToken,
|
|
49
|
-
valueTitle,
|
|
50
|
-
}: {
|
|
51
|
-
label: "T" | "O" | "C" | "L";
|
|
52
|
-
value: number;
|
|
53
|
-
className?: string;
|
|
54
|
-
showSpinner: boolean;
|
|
55
|
-
entryToken: number;
|
|
56
|
-
/** Exposed to assistive tech — full word, not shown as chip text. */
|
|
57
|
-
valueTitle: string;
|
|
58
|
-
}) {
|
|
59
|
-
const canAnimate = useCanAnimate();
|
|
60
|
-
const [flowValue, setFlowValue] = useState(() => (showSpinner ? 0 : value));
|
|
61
|
-
const prevShowSpinner = useRef(showSpinner);
|
|
62
|
-
const prevEntryToken = useRef(entryToken);
|
|
63
|
-
|
|
64
|
-
// After the loading spinner hides, run 0 → value once so NumberFlow performs an entry count.
|
|
65
|
-
// useLayoutEffect avoids one painted frame at the old count before resetting to 0.
|
|
66
|
-
// When the stats are merely revealed from hidden state, `entryToken` provides the same one-shot
|
|
67
|
-
// 0 → value path even if TanStack Query already has cached numbers and skips the spinner.
|
|
68
|
-
// When the spinner is off and `value` changes (filters, refetch without spinner), sync directly.
|
|
69
|
-
useLayoutEffect(() => {
|
|
70
|
-
if (showSpinner) {
|
|
71
|
-
prevShowSpinner.current = true;
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (prevShowSpinner.current) {
|
|
75
|
-
prevShowSpinner.current = false;
|
|
76
|
-
setFlowValue(0);
|
|
77
|
-
let raf1 = 0;
|
|
78
|
-
let raf2 = 0;
|
|
79
|
-
raf1 = requestAnimationFrame(() => {
|
|
80
|
-
raf2 = requestAnimationFrame(() => setFlowValue(value));
|
|
81
|
-
});
|
|
82
|
-
return () => {
|
|
83
|
-
cancelAnimationFrame(raf1);
|
|
84
|
-
cancelAnimationFrame(raf2);
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
if (entryToken !== prevEntryToken.current) {
|
|
88
|
-
prevEntryToken.current = entryToken;
|
|
89
|
-
setFlowValue(0);
|
|
90
|
-
let raf1 = 0;
|
|
91
|
-
let raf2 = 0;
|
|
92
|
-
raf1 = requestAnimationFrame(() => {
|
|
93
|
-
raf2 = requestAnimationFrame(() => setFlowValue(value));
|
|
94
|
-
});
|
|
95
|
-
return () => {
|
|
96
|
-
cancelAnimationFrame(raf1);
|
|
97
|
-
cancelAnimationFrame(raf2);
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
setFlowValue(value);
|
|
101
|
-
}, [entryToken, showSpinner, value]);
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<span
|
|
105
|
-
className={cn(
|
|
106
|
-
"inline-flex min-w-[2.25rem] items-center justify-center gap-1 rounded-md border px-2 py-0.5 text-xs font-semibold tabular-nums shadow-sm",
|
|
107
|
-
className,
|
|
108
|
-
)}
|
|
109
|
-
title={valueTitle}
|
|
110
|
-
>
|
|
111
|
-
<span aria-hidden className="opacity-90">
|
|
112
|
-
{label}
|
|
113
|
-
</span>
|
|
114
|
-
{showSpinner ? (
|
|
115
|
-
// css-loaders.com/dots — styles: `index.css` → `.board-stats-dots-loader`
|
|
116
|
-
<div
|
|
117
|
-
className="board-stats-dots-loader shrink-0"
|
|
118
|
-
aria-hidden
|
|
119
|
-
/>
|
|
120
|
-
) : (
|
|
121
|
-
// @number-flow/react: flowValue drives both entry (0→n after spinner) and later updates.
|
|
122
|
-
<span
|
|
123
|
-
className="inline-flex min-w-[1.25rem] justify-end [font-variant-numeric:tabular-nums]"
|
|
124
|
-
aria-label={`${valueTitle}: ${value}`}
|
|
125
|
-
>
|
|
126
|
-
<NumberFlow
|
|
127
|
-
value={flowValue}
|
|
128
|
-
plugins={[continuous]}
|
|
129
|
-
animated={canAnimate}
|
|
130
|
-
className="leading-none"
|
|
131
|
-
{...STATS_FLOW_TIMING}
|
|
132
|
-
willChange
|
|
133
|
-
/>
|
|
134
|
-
</span>
|
|
135
|
-
)}
|
|
136
|
-
</span>
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function BoardStatsChipsRow({
|
|
141
|
-
stats,
|
|
142
|
-
listCount,
|
|
143
|
-
showSpinner,
|
|
144
|
-
entryToken,
|
|
145
|
-
}: {
|
|
146
|
-
stats: TaskCountStat;
|
|
147
|
-
/** Structural count of lists on the board (not affected by task filters). */
|
|
148
|
-
listCount: number;
|
|
149
|
-
showSpinner: boolean;
|
|
150
|
-
entryToken: number;
|
|
151
|
-
}) {
|
|
152
|
-
return (
|
|
153
|
-
<div
|
|
154
|
-
className="inline-flex flex-wrap items-center gap-1.5"
|
|
155
|
-
aria-label="Task counts for current filters"
|
|
156
|
-
>
|
|
157
|
-
<StatChip
|
|
158
|
-
label="L"
|
|
159
|
-
value={listCount}
|
|
160
|
-
showSpinner={false}
|
|
161
|
-
entryToken={entryToken}
|
|
162
|
-
valueTitle="Lists on this board"
|
|
163
|
-
className={chipBoardL}
|
|
164
|
-
/>
|
|
165
|
-
<StatChip
|
|
166
|
-
label="T"
|
|
167
|
-
value={stats.total}
|
|
168
|
-
showSpinner={showSpinner}
|
|
169
|
-
entryToken={entryToken}
|
|
170
|
-
valueTitle="Total tasks"
|
|
171
|
-
className={chipBoardT}
|
|
172
|
-
/>
|
|
173
|
-
<StatChip
|
|
174
|
-
label="O"
|
|
175
|
-
value={stats.open}
|
|
176
|
-
showSpinner={showSpinner}
|
|
177
|
-
entryToken={entryToken}
|
|
178
|
-
valueTitle="Open / in-progress tasks"
|
|
179
|
-
className={chipBoardO}
|
|
180
|
-
/>
|
|
181
|
-
<StatChip
|
|
182
|
-
label="C"
|
|
183
|
-
value={stats.closed}
|
|
184
|
-
showSpinner={showSpinner}
|
|
185
|
-
entryToken={entryToken}
|
|
186
|
-
valueTitle="Closed tasks"
|
|
187
|
-
className={chipBoardC}
|
|
188
|
-
/>
|
|
189
|
-
</div>
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export function ListStatsChipsRow({
|
|
194
|
-
stats,
|
|
195
|
-
showSpinner,
|
|
196
|
-
entryToken,
|
|
197
|
-
}: {
|
|
198
|
-
stats: TaskCountStat;
|
|
199
|
-
showSpinner: boolean;
|
|
200
|
-
entryToken: number;
|
|
201
|
-
}) {
|
|
202
|
-
return (
|
|
203
|
-
<div
|
|
204
|
-
className="flex items-center justify-center gap-1 border-b border-border/60 bg-muted/40 px-2 py-1"
|
|
205
|
-
aria-label="List task counts"
|
|
206
|
-
>
|
|
207
|
-
<StatChip
|
|
208
|
-
label="T"
|
|
209
|
-
value={stats.total}
|
|
210
|
-
showSpinner={showSpinner}
|
|
211
|
-
entryToken={entryToken}
|
|
212
|
-
valueTitle="Total tasks in this list"
|
|
213
|
-
className={chipListT}
|
|
214
|
-
/>
|
|
215
|
-
<StatChip
|
|
216
|
-
label="O"
|
|
217
|
-
value={stats.open}
|
|
218
|
-
showSpinner={showSpinner}
|
|
219
|
-
entryToken={entryToken}
|
|
220
|
-
valueTitle="Open / in-progress tasks in this list"
|
|
221
|
-
className={chipListO}
|
|
222
|
-
/>
|
|
223
|
-
<StatChip
|
|
224
|
-
label="C"
|
|
225
|
-
value={stats.closed}
|
|
226
|
-
showSpinner={showSpinner}
|
|
227
|
-
entryToken={entryToken}
|
|
228
|
-
valueTitle="Closed tasks in this list"
|
|
229
|
-
className={chipListC}
|
|
230
|
-
/>
|
|
231
|
-
</div>
|
|
232
|
-
);
|
|
233
|
-
}
|
|
1
|
+
import NumberFlow, {
|
|
2
|
+
continuous,
|
|
3
|
+
useCanAnimate,
|
|
4
|
+
} from "@number-flow/react";
|
|
5
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
6
|
+
import type { TaskCountStat } from "../../../shared/boardStats";
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
/** Low-saturation chip fills so T/O/C read as soft status tints, not full banners. */
|
|
10
|
+
const chipBoardT =
|
|
11
|
+
"border-border/60 bg-muted/55 text-foreground dark:border-border/50 dark:bg-muted/45";
|
|
12
|
+
/** O = non-closed tasks — orange tint with readable opacity (avoid washed-out / red-looking tints). */
|
|
13
|
+
const chipBoardO =
|
|
14
|
+
"border-orange-500/45 bg-orange-500/68 text-orange-950 dark:border-orange-400/50 dark:text-orange-50";
|
|
15
|
+
const chipBoardC =
|
|
16
|
+
"border-emerald-600/35 bg-emerald-600/68 text-emerald-950 dark:border-emerald-500/40 dark:text-emerald-100";
|
|
17
|
+
|
|
18
|
+
const chipBoardL =
|
|
19
|
+
"border-border/60 bg-muted/55 text-foreground dark:border-border/50 dark:bg-muted/45";
|
|
20
|
+
|
|
21
|
+
const chipListT =
|
|
22
|
+
"border-border/50 bg-muted/35 text-foreground dark:bg-muted/30";
|
|
23
|
+
const chipListO =
|
|
24
|
+
"border-orange-500/38 bg-orange-500/60 text-orange-950 dark:border-orange-400/42 dark:text-orange-50";
|
|
25
|
+
const chipListC =
|
|
26
|
+
"border-emerald-600/18 bg-emerald-600/50 text-emerald-950 dark:border-emerald-500/18 dark:text-emerald-100";
|
|
27
|
+
|
|
28
|
+
const STATS_FLOW_TIMING = {
|
|
29
|
+
spinTiming: {
|
|
30
|
+
duration: 450,
|
|
31
|
+
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
|
|
32
|
+
} as const,
|
|
33
|
+
transformTiming: {
|
|
34
|
+
duration: 400,
|
|
35
|
+
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
|
|
36
|
+
} as const,
|
|
37
|
+
opacityTiming: {
|
|
38
|
+
duration: 220,
|
|
39
|
+
easing: "ease-out",
|
|
40
|
+
} as const,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function StatChip({
|
|
44
|
+
label,
|
|
45
|
+
value,
|
|
46
|
+
className,
|
|
47
|
+
showSpinner,
|
|
48
|
+
entryToken,
|
|
49
|
+
valueTitle,
|
|
50
|
+
}: {
|
|
51
|
+
label: "T" | "O" | "C" | "L";
|
|
52
|
+
value: number;
|
|
53
|
+
className?: string;
|
|
54
|
+
showSpinner: boolean;
|
|
55
|
+
entryToken: number;
|
|
56
|
+
/** Exposed to assistive tech — full word, not shown as chip text. */
|
|
57
|
+
valueTitle: string;
|
|
58
|
+
}) {
|
|
59
|
+
const canAnimate = useCanAnimate();
|
|
60
|
+
const [flowValue, setFlowValue] = useState(() => (showSpinner ? 0 : value));
|
|
61
|
+
const prevShowSpinner = useRef(showSpinner);
|
|
62
|
+
const prevEntryToken = useRef(entryToken);
|
|
63
|
+
|
|
64
|
+
// After the loading spinner hides, run 0 → value once so NumberFlow performs an entry count.
|
|
65
|
+
// useLayoutEffect avoids one painted frame at the old count before resetting to 0.
|
|
66
|
+
// When the stats are merely revealed from hidden state, `entryToken` provides the same one-shot
|
|
67
|
+
// 0 → value path even if TanStack Query already has cached numbers and skips the spinner.
|
|
68
|
+
// When the spinner is off and `value` changes (filters, refetch without spinner), sync directly.
|
|
69
|
+
useLayoutEffect(() => {
|
|
70
|
+
if (showSpinner) {
|
|
71
|
+
prevShowSpinner.current = true;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (prevShowSpinner.current) {
|
|
75
|
+
prevShowSpinner.current = false;
|
|
76
|
+
setFlowValue(0);
|
|
77
|
+
let raf1 = 0;
|
|
78
|
+
let raf2 = 0;
|
|
79
|
+
raf1 = requestAnimationFrame(() => {
|
|
80
|
+
raf2 = requestAnimationFrame(() => setFlowValue(value));
|
|
81
|
+
});
|
|
82
|
+
return () => {
|
|
83
|
+
cancelAnimationFrame(raf1);
|
|
84
|
+
cancelAnimationFrame(raf2);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (entryToken !== prevEntryToken.current) {
|
|
88
|
+
prevEntryToken.current = entryToken;
|
|
89
|
+
setFlowValue(0);
|
|
90
|
+
let raf1 = 0;
|
|
91
|
+
let raf2 = 0;
|
|
92
|
+
raf1 = requestAnimationFrame(() => {
|
|
93
|
+
raf2 = requestAnimationFrame(() => setFlowValue(value));
|
|
94
|
+
});
|
|
95
|
+
return () => {
|
|
96
|
+
cancelAnimationFrame(raf1);
|
|
97
|
+
cancelAnimationFrame(raf2);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
setFlowValue(value);
|
|
101
|
+
}, [entryToken, showSpinner, value]);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<span
|
|
105
|
+
className={cn(
|
|
106
|
+
"inline-flex min-w-[2.25rem] items-center justify-center gap-1 rounded-md border px-2 py-0.5 text-xs font-semibold tabular-nums shadow-sm",
|
|
107
|
+
className,
|
|
108
|
+
)}
|
|
109
|
+
title={valueTitle}
|
|
110
|
+
>
|
|
111
|
+
<span aria-hidden className="opacity-90">
|
|
112
|
+
{label}
|
|
113
|
+
</span>
|
|
114
|
+
{showSpinner ? (
|
|
115
|
+
// css-loaders.com/dots — styles: `index.css` → `.board-stats-dots-loader`
|
|
116
|
+
<div
|
|
117
|
+
className="board-stats-dots-loader shrink-0"
|
|
118
|
+
aria-hidden
|
|
119
|
+
/>
|
|
120
|
+
) : (
|
|
121
|
+
// @number-flow/react: flowValue drives both entry (0→n after spinner) and later updates.
|
|
122
|
+
<span
|
|
123
|
+
className="inline-flex min-w-[1.25rem] justify-end [font-variant-numeric:tabular-nums]"
|
|
124
|
+
aria-label={`${valueTitle}: ${value}`}
|
|
125
|
+
>
|
|
126
|
+
<NumberFlow
|
|
127
|
+
value={flowValue}
|
|
128
|
+
plugins={[continuous]}
|
|
129
|
+
animated={canAnimate}
|
|
130
|
+
className="leading-none"
|
|
131
|
+
{...STATS_FLOW_TIMING}
|
|
132
|
+
willChange
|
|
133
|
+
/>
|
|
134
|
+
</span>
|
|
135
|
+
)}
|
|
136
|
+
</span>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function BoardStatsChipsRow({
|
|
141
|
+
stats,
|
|
142
|
+
listCount,
|
|
143
|
+
showSpinner,
|
|
144
|
+
entryToken,
|
|
145
|
+
}: {
|
|
146
|
+
stats: TaskCountStat;
|
|
147
|
+
/** Structural count of lists on the board (not affected by task filters). */
|
|
148
|
+
listCount: number;
|
|
149
|
+
showSpinner: boolean;
|
|
150
|
+
entryToken: number;
|
|
151
|
+
}) {
|
|
152
|
+
return (
|
|
153
|
+
<div
|
|
154
|
+
className="inline-flex flex-wrap items-center gap-1.5"
|
|
155
|
+
aria-label="Task counts for current filters"
|
|
156
|
+
>
|
|
157
|
+
<StatChip
|
|
158
|
+
label="L"
|
|
159
|
+
value={listCount}
|
|
160
|
+
showSpinner={false}
|
|
161
|
+
entryToken={entryToken}
|
|
162
|
+
valueTitle="Lists on this board"
|
|
163
|
+
className={chipBoardL}
|
|
164
|
+
/>
|
|
165
|
+
<StatChip
|
|
166
|
+
label="T"
|
|
167
|
+
value={stats.total}
|
|
168
|
+
showSpinner={showSpinner}
|
|
169
|
+
entryToken={entryToken}
|
|
170
|
+
valueTitle="Total tasks"
|
|
171
|
+
className={chipBoardT}
|
|
172
|
+
/>
|
|
173
|
+
<StatChip
|
|
174
|
+
label="O"
|
|
175
|
+
value={stats.open}
|
|
176
|
+
showSpinner={showSpinner}
|
|
177
|
+
entryToken={entryToken}
|
|
178
|
+
valueTitle="Open / in-progress tasks"
|
|
179
|
+
className={chipBoardO}
|
|
180
|
+
/>
|
|
181
|
+
<StatChip
|
|
182
|
+
label="C"
|
|
183
|
+
value={stats.closed}
|
|
184
|
+
showSpinner={showSpinner}
|
|
185
|
+
entryToken={entryToken}
|
|
186
|
+
valueTitle="Closed tasks"
|
|
187
|
+
className={chipBoardC}
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function ListStatsChipsRow({
|
|
194
|
+
stats,
|
|
195
|
+
showSpinner,
|
|
196
|
+
entryToken,
|
|
197
|
+
}: {
|
|
198
|
+
stats: TaskCountStat;
|
|
199
|
+
showSpinner: boolean;
|
|
200
|
+
entryToken: number;
|
|
201
|
+
}) {
|
|
202
|
+
return (
|
|
203
|
+
<div
|
|
204
|
+
className="flex items-center justify-center gap-1 border-b border-border/60 bg-muted/40 px-2 py-1"
|
|
205
|
+
aria-label="List task counts"
|
|
206
|
+
>
|
|
207
|
+
<StatChip
|
|
208
|
+
label="T"
|
|
209
|
+
value={stats.total}
|
|
210
|
+
showSpinner={showSpinner}
|
|
211
|
+
entryToken={entryToken}
|
|
212
|
+
valueTitle="Total tasks in this list"
|
|
213
|
+
className={chipListT}
|
|
214
|
+
/>
|
|
215
|
+
<StatChip
|
|
216
|
+
label="O"
|
|
217
|
+
value={stats.open}
|
|
218
|
+
showSpinner={showSpinner}
|
|
219
|
+
entryToken={entryToken}
|
|
220
|
+
valueTitle="Open / in-progress tasks in this list"
|
|
221
|
+
className={chipListO}
|
|
222
|
+
/>
|
|
223
|
+
<StatChip
|
|
224
|
+
label="C"
|
|
225
|
+
value={stats.closed}
|
|
226
|
+
showSpinner={showSpinner}
|
|
227
|
+
entryToken={entryToken}
|
|
228
|
+
valueTitle="Closed tasks in this list"
|
|
229
|
+
className={chipListC}
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
-
import type { TaskCountStat } from "../../../shared/boardStats";
|
|
3
|
-
|
|
4
|
-
export interface BoardStatsDisplayValue {
|
|
5
|
-
/** Board-level T/O/C when stats are enabled and loaded (or placeholder). */
|
|
6
|
-
board: TaskCountStat | null;
|
|
7
|
-
listStat(listId: number): TaskCountStat;
|
|
8
|
-
/** Increments when stats visibility turns on so chips can run one-shot entry motion. */
|
|
9
|
-
entryToken: number;
|
|
10
|
-
/** True while fetching (including background refresh after filter change). */
|
|
11
|
-
fetching: boolean;
|
|
12
|
-
/** True on first load with no cached placeholder. */
|
|
13
|
-
pending: boolean;
|
|
14
|
-
/** Show spinner inside chips (initial load or stale placeholder during refetch). */
|
|
15
|
-
showChipSpinner: boolean;
|
|
16
|
-
/** True when the stats request failed; avoid showing misleading zero chips. */
|
|
17
|
-
statsError: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const BoardStatsDisplayContext = createContext<BoardStatsDisplayValue | null>(
|
|
21
|
-
null,
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
export function BoardStatsDisplayProvider({
|
|
25
|
-
value,
|
|
26
|
-
children,
|
|
27
|
-
}: {
|
|
28
|
-
value: BoardStatsDisplayValue;
|
|
29
|
-
children: ReactNode;
|
|
30
|
-
}) {
|
|
31
|
-
return (
|
|
32
|
-
<BoardStatsDisplayContext.Provider value={value}>
|
|
33
|
-
{children}
|
|
34
|
-
</BoardStatsDisplayContext.Provider>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** List columns read per-list stats; returns null when stats are hidden or unavailable. */
|
|
39
|
-
export function useBoardStatsDisplayOptional(): BoardStatsDisplayValue | null {
|
|
40
|
-
return useContext(BoardStatsDisplayContext);
|
|
41
|
-
}
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
import type { TaskCountStat } from "../../../shared/boardStats";
|
|
3
|
+
|
|
4
|
+
export interface BoardStatsDisplayValue {
|
|
5
|
+
/** Board-level T/O/C when stats are enabled and loaded (or placeholder). */
|
|
6
|
+
board: TaskCountStat | null;
|
|
7
|
+
listStat(listId: number): TaskCountStat;
|
|
8
|
+
/** Increments when stats visibility turns on so chips can run one-shot entry motion. */
|
|
9
|
+
entryToken: number;
|
|
10
|
+
/** True while fetching (including background refresh after filter change). */
|
|
11
|
+
fetching: boolean;
|
|
12
|
+
/** True on first load with no cached placeholder. */
|
|
13
|
+
pending: boolean;
|
|
14
|
+
/** Show spinner inside chips (initial load or stale placeholder during refetch). */
|
|
15
|
+
showChipSpinner: boolean;
|
|
16
|
+
/** True when the stats request failed; avoid showing misleading zero chips. */
|
|
17
|
+
statsError: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const BoardStatsDisplayContext = createContext<BoardStatsDisplayValue | null>(
|
|
21
|
+
null,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export function BoardStatsDisplayProvider({
|
|
25
|
+
value,
|
|
26
|
+
children,
|
|
27
|
+
}: {
|
|
28
|
+
value: BoardStatsDisplayValue;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
}) {
|
|
31
|
+
return (
|
|
32
|
+
<BoardStatsDisplayContext.Provider value={value}>
|
|
33
|
+
{children}
|
|
34
|
+
</BoardStatsDisplayContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** List columns read per-list stats; returns null when stats are hidden or unavailable. */
|
|
39
|
+
export function useBoardStatsDisplayOptional(): BoardStatsDisplayValue | null {
|
|
40
|
+
return useContext(BoardStatsDisplayContext);
|
|
41
|
+
}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import { cn } from "@/lib/utils";
|
|
2
|
-
|
|
3
|
-
/** Labels above filter button rows — foreground-tinted for readable contrast on header surfaces in light and dark themes. */
|
|
4
|
-
export const BOARD_HEADER_FILTER_SECTION_LABEL_CLASS =
|
|
5
|
-
"text-xs font-semibold uppercase tracking-wide text-foreground/90";
|
|
6
|
-
|
|
7
|
-
const BOARD_HEADER_TEXT_BUTTON_BASE_CLASS =
|
|
8
|
-
"inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium transition-colors";
|
|
9
|
-
|
|
10
|
-
// Keep header text buttons on a single shared visual system so filters and
|
|
11
|
-
// board actions stay consistent as more controls are added to the strip.
|
|
12
|
-
export function boardHeaderToggleButtonClass(active: boolean) {
|
|
13
|
-
return cn(
|
|
14
|
-
BOARD_HEADER_TEXT_BUTTON_BASE_CLASS,
|
|
15
|
-
active
|
|
16
|
-
// Use the app surface token instead of brand color so active buttons stay neutral across board themes.
|
|
17
|
-
? "border-border/80 bg-background/75 text-foreground shadow-sm backdrop-blur-sm"
|
|
18
|
-
: "border-border bg-muted/40 text-foreground/60 hover:bg-muted hover:text-foreground",
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function boardHeaderActionButtonClass() {
|
|
23
|
-
return cn(
|
|
24
|
-
BOARD_HEADER_TEXT_BUTTON_BASE_CLASS,
|
|
25
|
-
"border-border bg-muted/40 text-foreground hover:bg-muted",
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Reserves space for the section edit icon so filter labels and buttons do not shift on header hover. */
|
|
30
|
-
export const BOARD_HEADER_SECTION_EDIT_ICON_SLOT_CLASS =
|
|
31
|
-
"inline-flex h-5 w-5 shrink-0 items-center justify-center";
|
|
32
|
-
|
|
33
|
-
export function boardHeaderSectionEditIconButtonClass(headerHovered: boolean) {
|
|
34
|
-
return cn(
|
|
35
|
-
"inline-flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground transition-opacity duration-150 hover:bg-black/[0.06] hover:text-foreground dark:hover:bg-white/[0.06]",
|
|
36
|
-
headerHovered ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
37
|
-
);
|
|
38
|
-
}
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
|
|
3
|
+
/** Labels above filter button rows — foreground-tinted for readable contrast on header surfaces in light and dark themes. */
|
|
4
|
+
export const BOARD_HEADER_FILTER_SECTION_LABEL_CLASS =
|
|
5
|
+
"text-xs font-semibold uppercase tracking-wide text-foreground/90";
|
|
6
|
+
|
|
7
|
+
const BOARD_HEADER_TEXT_BUTTON_BASE_CLASS =
|
|
8
|
+
"inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium transition-colors";
|
|
9
|
+
|
|
10
|
+
// Keep header text buttons on a single shared visual system so filters and
|
|
11
|
+
// board actions stay consistent as more controls are added to the strip.
|
|
12
|
+
export function boardHeaderToggleButtonClass(active: boolean) {
|
|
13
|
+
return cn(
|
|
14
|
+
BOARD_HEADER_TEXT_BUTTON_BASE_CLASS,
|
|
15
|
+
active
|
|
16
|
+
// Use the app surface token instead of brand color so active buttons stay neutral across board themes.
|
|
17
|
+
? "border-border/80 bg-background/75 text-foreground shadow-sm backdrop-blur-sm"
|
|
18
|
+
: "border-border bg-muted/40 text-foreground/60 hover:bg-muted hover:text-foreground",
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function boardHeaderActionButtonClass() {
|
|
23
|
+
return cn(
|
|
24
|
+
BOARD_HEADER_TEXT_BUTTON_BASE_CLASS,
|
|
25
|
+
"border-border bg-muted/40 text-foreground hover:bg-muted",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Reserves space for the section edit icon so filter labels and buttons do not shift on header hover. */
|
|
30
|
+
export const BOARD_HEADER_SECTION_EDIT_ICON_SLOT_CLASS =
|
|
31
|
+
"inline-flex h-5 w-5 shrink-0 items-center justify-center";
|
|
32
|
+
|
|
33
|
+
export function boardHeaderSectionEditIconButtonClass(headerHovered: boolean) {
|
|
34
|
+
return cn(
|
|
35
|
+
"inline-flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground transition-opacity duration-150 hover:bg-black/[0.06] hover:text-foreground dark:hover:bg-white/[0.06]",
|
|
36
|
+
headerHovered ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
37
|
+
);
|
|
38
|
+
}
|