feat(walkthrough): introduce the rack canvas, and keep Next inside the card
The Getting Started tour stopped at the logical canvas, so the third design type was invisible to anyone who had not gone looking for New Canvas → Kind. A step between grouping and styling now points at the canvas switcher and says what a rack canvas is for. It is `mode: 'all'` — a rack needs no backend, it persists to localStorage in standalone like any other canvas — and it anchors on the switcher rather than a rack control, since the tour runs on a network canvas where none of the rack sidebar items exist. That twelfth step also broke the card footer. The progress strip grows with the step count and neither footer child could shrink (`min-width: auto`), so at twelve dots the strip pushed the counter and both buttons past the right edge of the 320px card — Next ended up outside it. The strip is now the part that gives: it shrinks and clips, the controls hold their size, and Next stays anchored whatever the step count. ha-relevant: maybe The overlay fix applies to any build of the frontend; the rack step itself is pointless until the rack canvas is ported.
This commit is contained in:
committed by
Pouzor - Rémy Jardient
parent
ff740f7466
commit
a70fc248bf
@@ -172,6 +172,7 @@ export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbee
|
||||
<div className="px-2 pt-2 pb-1 border-b border-border relative">
|
||||
<button
|
||||
onClick={() => setDesignSwitcherOpen((o) => !o)}
|
||||
data-tour="canvas-switcher"
|
||||
className="flex items-center gap-2 w-full px-2 py-1.5 rounded-md text-xs font-medium bg-[#21262d] border border-border hover:border-[#30363d] transition-colors cursor-pointer"
|
||||
>
|
||||
{activeDesignId ? (() => {
|
||||
|
||||
@@ -284,6 +284,38 @@ describe('Sidebar', () => {
|
||||
expect(mockLogout).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
// ── Walkthrough anchor ─────────────────────────────────────────────────────
|
||||
|
||||
it('carries the walkthrough anchor the rack tour step spotlights', async () => {
|
||||
const { useDesignStore } = await import('@/stores/designStore')
|
||||
const { STEPS } = await import('@/walkthrough/steps')
|
||||
const before = useDesignStore.getState()
|
||||
useDesignStore.setState({
|
||||
designs: [{
|
||||
id: 'd1', name: 'Main', design_type: 'network', icon: null,
|
||||
created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z',
|
||||
}],
|
||||
activeDesignId: 'd1',
|
||||
loaded: true,
|
||||
})
|
||||
|
||||
const { unmount } = render(<Sidebar {...defaultProps} />)
|
||||
|
||||
const step = STEPS.find((s) => s.id === 'rack')
|
||||
expect(step?.anchor).toBe('[data-tour="canvas-switcher"]')
|
||||
// The overlay resolves that selector against the DOM — an anchor the sidebar
|
||||
// stops carrying leaves the step spotlighting nothing.
|
||||
expect(document.querySelector(step!.anchor!)).toBeInTheDocument()
|
||||
|
||||
// Unmount before restoring, so the store reset never re-renders the sidebar.
|
||||
unmount()
|
||||
useDesignStore.setState({
|
||||
designs: before.designs,
|
||||
activeDesignId: before.activeDesignId,
|
||||
loaded: before.loaded,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ── Standalone mode ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -207,18 +207,20 @@ export function WalkthroughOverlay() {
|
||||
)}
|
||||
|
||||
<div className="mt-3 flex items-center justify-between gap-2">
|
||||
{/* Progress dots */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* Progress dots. The strip grows with the step count, so it is the
|
||||
part that gives: it shrinks and clips rather than pushing Next out
|
||||
of the fixed-width card. */}
|
||||
<div data-testid="walkthrough-progress" className="flex min-w-0 flex-1 items-center gap-1 overflow-hidden">
|
||||
{steps.map((s, i) => (
|
||||
<span
|
||||
key={s.id}
|
||||
className={`h-1.5 rounded-full transition-all ${
|
||||
className={`h-1.5 shrink-0 rounded-full transition-all ${
|
||||
i === stepIndex ? 'w-4 bg-[#00d4ff]' : 'w-1.5 bg-[#30363d]'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
<span className="mr-1 text-[10px] font-mono text-muted-foreground">
|
||||
{stepIndex + 1}/{steps.length}
|
||||
</span>
|
||||
|
||||
@@ -36,6 +36,20 @@ describe('WalkthroughOverlay', () => {
|
||||
expect(screen.getByText('Welcome to Homelable')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('lets the progress strip give way, not the Next button', () => {
|
||||
renderWithProviders(<WalkthroughOverlay />)
|
||||
startTour()
|
||||
|
||||
const dots = screen.getByTestId('walkthrough-progress')
|
||||
expect(dots.children).toHaveLength(TOTAL)
|
||||
// The card is a fixed 320px, so one dot per step eventually outgrows the
|
||||
// footer. The strip shrinks and clips; the counter and buttons hold their
|
||||
// size, or Next lands outside the card.
|
||||
expect(dots.className).toContain('min-w-0')
|
||||
expect(dots.className).toContain('overflow-hidden')
|
||||
expect(screen.getByText('Next').closest('div')?.className).toContain('shrink-0')
|
||||
})
|
||||
|
||||
it('skip closes the overlay', () => {
|
||||
renderWithProviders(<WalkthroughOverlay />)
|
||||
startTour()
|
||||
|
||||
@@ -12,13 +12,26 @@ describe('getSteps', () => {
|
||||
expect(standalone.length).toBeLessThan(STEPS.length)
|
||||
const ids = standalone.map((s) => s.id)
|
||||
// Canvas-only steps survive; backend-only steps are filtered out.
|
||||
expect(ids).toEqual(expect.arrayContaining(['welcome', 'nodes', 'grouping', 'style', 'end']))
|
||||
expect(ids).toEqual(expect.arrayContaining(['welcome', 'nodes', 'grouping', 'rack', 'style', 'end']))
|
||||
expect(ids).not.toContain('scan')
|
||||
expect(ids).not.toContain('scan-history')
|
||||
expect(ids).not.toContain('inventory')
|
||||
expect(ids).not.toContain('imports')
|
||||
})
|
||||
|
||||
it('keeps the rack step in both modes — a rack canvas needs no backend', () => {
|
||||
const rack = STEPS.find((s) => s.id === 'rack')
|
||||
expect(rack?.mode).toBe('all')
|
||||
expect(rack?.anchor).toBe('[data-tour="canvas-switcher"]')
|
||||
expect(getSteps(true).map((s) => s.id)).toContain('rack')
|
||||
})
|
||||
|
||||
it('introduces the rack canvas before the styling step', () => {
|
||||
const ids = STEPS.map((s) => s.id)
|
||||
expect(ids.indexOf('rack')).toBeGreaterThan(ids.indexOf('nodes'))
|
||||
expect(ids.indexOf('rack')).toBeLessThan(ids.indexOf('style'))
|
||||
})
|
||||
|
||||
it('ends on a step with a GitHub link (the full-mode recap in standalone)', () => {
|
||||
const last = getSteps(true).at(-1)
|
||||
expect(last?.id).toBe('end')
|
||||
|
||||
@@ -87,6 +87,14 @@ export const STEPS: TourStep[] = [
|
||||
action: 'selectTwoNodes',
|
||||
mode: 'all',
|
||||
},
|
||||
{
|
||||
id: 'rack',
|
||||
title: 'Map the physical side',
|
||||
body: 'A canvas can also be a Rack: real racks, the gear mounted in them, and the patch cables between their ports. Create one from the canvas switcher (New Canvas, Kind: Rack), drop a rack, mount devices with + Device, then hit Patch and drag from one port to another.',
|
||||
anchor: '[data-tour="canvas-switcher"]',
|
||||
placement: 'right',
|
||||
mode: 'all',
|
||||
},
|
||||
{
|
||||
id: 'style',
|
||||
title: 'Make it yours',
|
||||
|
||||
Reference in New Issue
Block a user