Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ export const STORAGE_KEY_ACTIVE_ID = 'craftbot.dashboard.activeLayoutId'
* Stored layouts under any other version are discarded on read and the
* dashboard reseeds from the default; there is no migration code. Bumped
* for grid-unit changes AND for changes to the shipped default (v4 hid
* Recent Activity by default).
* Recent Activity by default; v5 introduced the hero default with the 2x2
* CraftBot intro).
*/
export const STORAGE_VERSION = 4
export const STORAGE_VERSION = 5

/**
* One grid cell IS one original dashboard card.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,15 @@ import { WIDGET_REGISTRY } from '../widgets/registry'
/**
* The seed/default arrangement — and what "Reset layout" restores.
*
* This is the ORIGINAL pre-revamp dashboard (the monolithic DashboardPage this
* branch replaced): eight equal-sized panels flowing five per row, in its
* exact source order — Task Statistics, Token Usage, System Resources, Usage
* Patterns, MCP Servers, Skills, Integrations, Model Information. One grid
* cell IS one of those panels (see WIDGET_MIN_CELLS), so every widget seeds at
* 1x1 and the lg row holds exactly the original five; narrower breakpoints
* reflow to fewer per row, same as the old CSS grid did.
* A hero composition: the CraftBot intro widget sits 2x2 at the top-left, with
* the stat cards (Token Usage / System Resources / Usage Patterns over Task
* Statistics / Skills / Model Information) filling the columns beside it, and
* a bottom row of MCP Servers, Integrations, and Living UI next to a 2-wide
* Mascot scene. Narrower breakpoints keep the hero on top and reflow the 1x1
* cards in pairs.
*
* The widgets this revamp introduced (Mascot, Recent Activity, Living UI)
* weren't part of that dashboard, so they trail after the original eight in
* the flow instead of being woven into it.
* Recent Activity deliberately isn't here: it's hidden by default and only
* appears when added via the Add Widget modal.
*
* Explicit coordinates, not shelf packing: packing by each widget's default
* size leaves holes wherever row heights disagree (a taller neighbor blocks
Expand All @@ -25,40 +23,46 @@ import { WIDGET_REGISTRY } from '../widgets/registry'
*/
type Placement = { id: string; x: number; y: number; w: number; h: number }

const ORIGINAL_ORDER = [
'taskStats',
'tokenUsage',
'systemResources',
'usagePatterns',
'mcpServers',
'skills',
'integrations',
'modelInfo',
]

// Recent Activity deliberately isn't here: it's hidden by default and only
// appears when added via the Add Widget modal. Living UI takes the slot it
// used to occupy.
const REVAMP_EXTRAS = ['mascot', 'livingUi']

// Uniform 1x1 cards flowed left-to-right, one per column — the same reflow
// the old dashboard's repeat(5, 1fr) / 4 / 2 container queries produced.
function flow(ids: string[], perRow: number): Placement[] {
return ids.map((id, i) => ({
id,
x: i % perRow,
y: Math.floor(i / perRow),
w: 1,
h: 1,
}))
}

const ALL_IDS = [...ORIGINAL_ORDER, ...REVAMP_EXTRAS]

const DEFAULT_PLACEMENT: Record<Breakpoint, Placement[]> = {
lg: flow(ALL_IDS, 5), // 5 per row, the original full-width look
md: flow(ALL_IDS, 4),
sm: flow(ALL_IDS, 2),
lg: [
{ id: 'craftBotIntro', x: 0, y: 0, w: 2, h: 2 },
{ id: 'tokenUsage', x: 2, y: 0, w: 1, h: 1 },
{ id: 'systemResources', x: 3, y: 0, w: 1, h: 1 },
{ id: 'usagePatterns', x: 4, y: 0, w: 1, h: 1 },
{ id: 'taskStats', x: 2, y: 1, w: 1, h: 1 },
{ id: 'skills', x: 3, y: 1, w: 1, h: 1 },
{ id: 'modelInfo', x: 4, y: 1, w: 1, h: 1 },
{ id: 'mcpServers', x: 0, y: 2, w: 1, h: 1 },
{ id: 'integrations', x: 1, y: 2, w: 1, h: 1 },
{ id: 'livingUi', x: 2, y: 2, w: 1, h: 1 },
{ id: 'mascot', x: 3, y: 2, w: 2, h: 1 },
],
md: [
{ id: 'craftBotIntro', x: 0, y: 0, w: 2, h: 2 },
{ id: 'tokenUsage', x: 2, y: 0, w: 1, h: 1 },
{ id: 'systemResources', x: 3, y: 0, w: 1, h: 1 },
{ id: 'taskStats', x: 2, y: 1, w: 1, h: 1 },
{ id: 'skills', x: 3, y: 1, w: 1, h: 1 },
{ id: 'usagePatterns', x: 0, y: 2, w: 1, h: 1 },
{ id: 'modelInfo', x: 1, y: 2, w: 1, h: 1 },
{ id: 'mcpServers', x: 2, y: 2, w: 1, h: 1 },
{ id: 'integrations', x: 3, y: 2, w: 1, h: 1 },
{ id: 'livingUi', x: 0, y: 3, w: 1, h: 1 },
{ id: 'mascot', x: 1, y: 3, w: 2, h: 1 },
],
sm: [
{ id: 'craftBotIntro', x: 0, y: 0, w: 2, h: 2 },
{ id: 'tokenUsage', x: 0, y: 2, w: 1, h: 1 },
{ id: 'systemResources', x: 1, y: 2, w: 1, h: 1 },
{ id: 'taskStats', x: 0, y: 3, w: 1, h: 1 },
{ id: 'skills', x: 1, y: 3, w: 1, h: 1 },
{ id: 'usagePatterns', x: 0, y: 4, w: 1, h: 1 },
{ id: 'modelInfo', x: 1, y: 4, w: 1, h: 1 },
{ id: 'mcpServers', x: 0, y: 5, w: 1, h: 1 },
{ id: 'integrations', x: 1, y: 5, w: 1, h: 1 },
{ id: 'livingUi', x: 0, y: 6, w: 2, h: 1 },
{ id: 'mascot', x: 0, y: 7, w: 2, h: 1 },
],
}

function clamp(value: number, min: number, max: number): number {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,26 @@ function normalizeBreakpoint(items: unknown, widgetIds: string[], bp: Breakpoint
* bounds is pulled back to the nearest one rather than left as an odd shape.
*/
export function normalizeLayout(layout: NamedLayout): NamedLayout {
const widgetIds = (Array.isArray(layout.widgetIds) ? layout.widgetIds : [])
const baseWidgetIds = (Array.isArray(layout.widgetIds) ? layout.widgetIds : [])
.filter(id => typeof id === 'string' && !!WIDGET_REGISTRY[id])

// Ensure craftBotIntro is ALWAYS included at top-left by default if missing
const widgetIds = baseWidgetIds.includes('craftBotIntro')
? baseWidgetIds
: ['craftBotIntro', ...baseWidgetIds]

const stored = (layout.layouts ?? {}) as Partial<BreakpointLayouts>

const layouts = BREAKPOINT_KEYS.reduce((acc, bp) => {
acc[bp] = normalizeBreakpoint(stored[bp], widgetIds, bp)
const bpItems = normalizeBreakpoint(stored[bp], widgetIds, bp)
const introIndex = bpItems.findIndex(item => item.i === 'craftBotIntro')
if (introIndex !== -1) {
bpItems[introIndex] = { ...bpItems[introIndex], x: 0, y: 0 }
} else {
const bounds = boundsFor(bp, 'craftBotIntro')
bpItems.unshift({ i: 'craftBotIntro', x: 0, y: 0, w: 1, h: 1, ...bounds })
}
acc[bp] = bpItems
return acc
}, {} as BreakpointLayouts)

Expand Down
Loading