Feat: Prepare frontend future model

This commit is contained in:
MangoPig
2026-06-18 16:58:31 +01:00
parent fcf96590bb
commit 25c6934801
13 changed files with 342 additions and 146 deletions

View File

@@ -84,20 +84,48 @@ export type SidebarItem = {
export type WorkspaceStaticKind = "workspace" | "home" | "settings";
// Keep this open-ended so future server-driven or plugin-provided item types do
// not require a frontend source edit before they can be represented safely.
export type WorkspaceItemTypeId = string;
export type WorkspaceStaticItem = SidebarItem & {
contextKind: WorkspaceStaticKind;
};
export type WorkspaceTreeNode = {
export type WorkspaceFolderNode = {
id: string;
label: string;
kind: "folder" | "board" | "doc";
kind: "folder";
icon: ShellIcon;
active?: boolean;
meta?: string;
children?: readonly WorkspaceTreeNode[];
};
export type WorkspaceItemNode = {
id: string;
label: string;
kind: "item";
itemType: WorkspaceItemTypeId;
active?: boolean;
meta?: string;
children?: undefined;
};
export type WorkspaceTreeNode = WorkspaceFolderNode | WorkspaceItemNode;
export type WorkspaceItemTypeDefinition = {
id: WorkspaceItemTypeId;
label: string;
shortLabel: string;
icon: ShellIcon;
noun: string;
actionPrefix: string;
defaultCreateLabel: string;
includeInWorkspaceCreate?: boolean;
description?: string;
};
export type SidebarHeaderAction = {
id: string;
label: string;
@@ -117,11 +145,23 @@ export type MobileBottomNavItem = {
active?: boolean;
};
export type WorkspaceContextMenuTarget = {
id: string;
label: string;
kind: WorkspaceStaticKind | WorkspaceTreeNode["kind"];
};
export type WorkspaceContextMenuTarget =
| {
id: string;
label: string;
kind: WorkspaceStaticKind;
}
| {
id: string;
label: string;
kind: "folder";
}
| {
id: string;
label: string;
kind: "item";
itemType: WorkspaceItemTypeId;
};
export type WorkspaceContextMenuAction = {
id: string;
@@ -146,6 +186,81 @@ export type WorkspaceContextMenuSection = {
items: readonly WorkspaceContextMenuAction[];
};
export const firstPartyWorkspaceItemTypes: readonly WorkspaceItemTypeDefinition[] = [
{
id: "core.doc",
label: "Doc",
shortLabel: "Doc",
icon: FileText,
noun: "doc",
actionPrefix: "doc",
defaultCreateLabel: "New doc",
includeInWorkspaceCreate: true,
description: "Rich text documents and notes.",
},
{
id: "core.board.kanban",
label: "Kanban board",
shortLabel: "Board",
icon: LayoutGrid,
noun: "board",
actionPrefix: "board",
defaultCreateLabel: "New board",
includeInWorkspaceCreate: true,
description: "Default board-style workspace item.",
},
{
id: "core.board.list",
label: "List board",
shortLabel: "Board",
icon: LayoutGrid,
noun: "board",
actionPrefix: "list-board",
defaultCreateLabel: "New list board",
description: "Alternate first-party board view prepared for the future registry.",
},
] as const;
const workspaceItemTypeMap = new Map<WorkspaceItemTypeId, WorkspaceItemTypeDefinition>(
firstPartyWorkspaceItemTypes.map((definition) => [definition.id, definition]),
);
const createUnknownWorkspaceItemTypeDefinition = (
itemType: WorkspaceItemTypeId,
): WorkspaceItemTypeDefinition => ({
id: itemType,
label: "Item",
shortLabel: "Item",
icon: FileText,
noun: "item",
actionPrefix: "item",
defaultCreateLabel: "New item",
description: "Fallback definition for unknown or future workspace item types.",
});
export const getWorkspaceItemTypeDefinition = (itemType: WorkspaceItemTypeId): WorkspaceItemTypeDefinition => {
return workspaceItemTypeMap.get(itemType) ?? createUnknownWorkspaceItemTypeDefinition(itemType);
};
export const getWorkspaceNodeIcon = (node: WorkspaceTreeNode): ShellIcon =>
node.kind === "folder" ? node.icon : getWorkspaceItemTypeDefinition(node.itemType).icon;
const getWorkspaceCreateActions = (): readonly WorkspaceContextMenuAction[] => [
{ id: "new-folder", label: "New folder", shortcut: { modifiers: ["alt"], key: "f" } },
...firstPartyWorkspaceItemTypes
.filter((definition) => definition.includeInWorkspaceCreate)
.map((definition) => ({
id: `create-${definition.actionPrefix}`,
label: definition.defaultCreateLabel,
shortcut:
definition.id === "core.board.kanban"
? ({ modifiers: ["alt"], key: "b" } as const)
: definition.id === "core.doc"
? ({ modifiers: ["alt"], key: "d" } as const)
: undefined,
})),
];
export const getWorkspaceContextMenuEyebrow = (target: WorkspaceContextMenuTarget): string => {
switch (target.kind) {
case "workspace":
@@ -155,10 +270,8 @@ export const getWorkspaceContextMenuEyebrow = (target: WorkspaceContextMenuTarge
return "Configuration";
case "folder":
return "Folder";
case "board":
return "Board";
case "doc":
return "Doc";
case "item":
return getWorkspaceItemTypeDefinition(target.itemType).shortLabel;
}
};
@@ -177,7 +290,12 @@ export const createWorkspaceStaticTarget = (item: WorkspaceStaticItem): Workspac
export const createWorkspaceTreeTarget = (node: WorkspaceTreeNode): WorkspaceContextMenuTarget => ({
id: node.id,
label: node.label,
kind: node.kind,
...(node.kind === "folder"
? { kind: "folder" as const }
: {
kind: "item" as const,
itemType: node.itemType,
}),
});
export type NotificationItem = {
@@ -264,7 +382,8 @@ export const workspaceStaticItems: readonly WorkspaceStaticItem[] = [
{ id: "workspace-settings", label: "Settings", icon: Settings, contextKind: "settings" },
] as const;
// Freeform workspace tree scaffold: folders, boards, and docs are first-class siblings.
// Freeform workspace tree scaffold: folders are structural, while non-folder
// nodes already flow through the future-safe itemType registry seam.
export const workspaceTree: readonly WorkspaceTreeNode[] = [
{
id: "product-workspace",
@@ -272,16 +391,16 @@ export const workspaceTree: readonly WorkspaceTreeNode[] = [
kind: "folder",
icon: Folder,
children: [
{ id: "roadmap-board", label: "Roadmap", kind: "board", icon: LayoutGrid, active: true },
{ id: "launch-brief", label: "Launch Brief", kind: "doc", icon: FileText },
{ id: "roadmap-board", label: "Roadmap", kind: "item", itemType: "core.board.kanban", active: true },
{ id: "launch-brief", label: "Launch Brief", kind: "item", itemType: "core.doc" },
{
id: "research-folder",
label: "Research",
kind: "folder",
icon: Folder,
children: [
{ id: "interviews-doc", label: "Interviews", kind: "doc", icon: FileText },
{ id: "signals-board", label: "Signals", kind: "board", icon: LayoutGrid, meta: "2" },
{ id: "interviews-doc", label: "Interviews", kind: "item", itemType: "core.doc" },
{ id: "signals-board", label: "Signals", kind: "item", itemType: "core.board.kanban", meta: "2" },
],
},
],
@@ -292,11 +411,11 @@ export const workspaceTree: readonly WorkspaceTreeNode[] = [
kind: "folder",
icon: Folder,
children: [
{ id: "system-doc", label: "Design System", kind: "doc", icon: FileText },
{ id: "review-board", label: "Review Queue", kind: "board", icon: LayoutGrid },
{ id: "system-doc", label: "Design System", kind: "item", itemType: "core.doc" },
{ id: "review-board", label: "Review Queue", kind: "item", itemType: "core.board.kanban" },
],
},
{ id: "general-notes", label: "General Notes", kind: "doc", icon: FileText },
{ id: "general-notes", label: "General Notes", kind: "item", itemType: "core.doc" },
] as const;
export const workspaceSidebarHeaderActions: readonly SidebarHeaderAction[] = [
@@ -314,11 +433,7 @@ export const mobileBottomNavItems: readonly MobileBottomNavItem[] = [
export const getWorkspaceContextMenuSections = (
target: WorkspaceContextMenuTarget,
): readonly WorkspaceContextMenuSection[] => {
const createActions = [
{ id: "new-folder", label: "New folder", shortcut: { modifiers: ["alt"], key: "f" } },
{ id: "new-board", label: "New board", shortcut: { modifiers: ["alt"], key: "b" } },
{ id: "new-doc", label: "New doc", shortcut: { modifiers: ["alt"], key: "d" } },
] as const;
const createActions = getWorkspaceCreateActions();
const createSubmenuAction = {
id: "create",
@@ -391,44 +506,30 @@ export const getWorkspaceContextMenuSections = (
],
},
] as const;
case "board":
case "item": {
const definition = getWorkspaceItemTypeDefinition(target.itemType);
const actionPrefix = definition.actionPrefix;
const nounLabel = definition.noun;
return [
{
id: "board",
id: `${actionPrefix}-primary`,
items: [
{ id: "open-board", label: "Open board", shortcut: { key: "enter" } },
{ id: "rename-board", label: "Rename", shortcut: { modifiers: ["meta"], key: "r" } },
{ id: `open-${actionPrefix}`, label: `Open ${nounLabel}`, shortcut: { key: "enter" } },
{ id: `rename-${actionPrefix}`, label: "Rename", shortcut: { modifiers: ["meta"], key: "r" } },
],
},
{
id: "organize",
label: undefined,
items: [
{ id: "duplicate-board", label: "Duplicate", shortcut: { modifiers: ["meta"], key: "d" } },
{ id: "move-board", label: "Move", shortcut: { modifiers: ["meta"], key: "m" } },
{ id: "delete-board", label: "Delete", shortcut: { modifiers: ["meta"], key: "delete" }, tone: "danger" },
],
},
] as const;
case "doc":
return [
{
id: "doc",
items: [
{ id: "open-doc", label: "Open doc", shortcut: { key: "enter" } },
{ id: "rename-doc", label: "Rename", shortcut: { modifiers: ["meta"], key: "r" } },
],
},
{
id: "organize",
label: undefined,
items: [
{ id: "duplicate-doc", label: "Duplicate", shortcut: { modifiers: ["meta"], key: "d" } },
{ id: "move-doc", label: "Move", shortcut: { modifiers: ["meta"], key: "m" } },
{ id: "delete-doc", label: "Delete", shortcut: { modifiers: ["meta"], key: "delete" }, tone: "danger" },
{ id: `duplicate-${actionPrefix}`, label: "Duplicate", shortcut: { modifiers: ["meta"], key: "d" } },
{ id: `move-${actionPrefix}`, label: "Move", shortcut: { modifiers: ["meta"], key: "m" } },
{ id: `delete-${actionPrefix}`, label: "Delete", shortcut: { modifiers: ["meta"], key: "delete" }, tone: "danger" },
],
},
] as const;
}
}
};