chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { readCollapsedCookie } from './store'
|
||||
|
||||
function setCookie(value: string) {
|
||||
document.cookie = `sidebar_collapsed=${value}; path=/`
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
document.cookie = 'sidebar_collapsed=; path=/; max-age=0'
|
||||
})
|
||||
|
||||
describe('readCollapsedCookie', () => {
|
||||
it('is true only for an exact value of 1', () => {
|
||||
setCookie('1')
|
||||
expect(readCollapsedCookie()).toBe(true)
|
||||
})
|
||||
|
||||
it('is false for 0', () => {
|
||||
setCookie('0')
|
||||
expect(readCollapsedCookie()).toBe(false)
|
||||
})
|
||||
|
||||
it('does not treat a substring value like 10 as collapsed', () => {
|
||||
setCookie('10')
|
||||
expect(readCollapsedCookie()).toBe(false)
|
||||
})
|
||||
|
||||
it('is false when the cookie is absent', () => {
|
||||
expect(readCollapsedCookie()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,110 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { SIDEBAR_WIDTH } from '@/stores/constants'
|
||||
import type { SidebarState } from './types'
|
||||
|
||||
/**
|
||||
* Clamps an expanded sidebar width into the valid range for the current
|
||||
* viewport. The upper bound can never drop below {@link SIDEBAR_WIDTH.MIN}, so a
|
||||
* narrow window (where `innerWidth * MAX_PERCENTAGE < MIN`) still yields a width
|
||||
* at or above the minimum instead of collapsing the sidebar to nothing.
|
||||
*/
|
||||
function clampSidebarWidth(width: number): number {
|
||||
if (!Number.isFinite(width)) return SIDEBAR_WIDTH.DEFAULT
|
||||
const max =
|
||||
typeof window === 'undefined'
|
||||
? Number.POSITIVE_INFINITY
|
||||
: Math.max(SIDEBAR_WIDTH.MIN, window.innerWidth * SIDEBAR_WIDTH.MAX_PERCENTAGE)
|
||||
return Math.min(Math.max(width, SIDEBAR_WIDTH.MIN), max)
|
||||
}
|
||||
|
||||
function applySidebarWidth(width: number) {
|
||||
if (typeof window === 'undefined') return
|
||||
const value = Number.isFinite(width) ? width : SIDEBAR_WIDTH.DEFAULT
|
||||
document.documentElement.style.setProperty('--sidebar-width', `${value}px`)
|
||||
}
|
||||
|
||||
/**
|
||||
* The `sidebar_collapsed` cookie is the single source of truth for collapse: the
|
||||
* server layout reads it to render the correct structure on the first paint
|
||||
* (it can't read `localStorage`), and the client seeds its initial state from it
|
||||
* below. Width is the only field persisted to `localStorage`.
|
||||
*/
|
||||
function applyCollapsedCookie(collapsed: boolean) {
|
||||
if (typeof document === 'undefined') return
|
||||
document.cookie = `sidebar_collapsed=${collapsed ? '1' : '0'}; path=/; max-age=31536000; samesite=lax`
|
||||
}
|
||||
|
||||
/** Reads the collapse state the server saw, so the client store seeds identically. Matches the
|
||||
* cookie value strictly (`=1`) so `sidebar_collapsed=10` and the like aren't read as collapsed. */
|
||||
export function readCollapsedCookie(): boolean {
|
||||
if (typeof document === 'undefined') return false
|
||||
return document.cookie.match(/(?:^|;\s*)sidebar_collapsed=([^;]*)/)?.[1] === '1'
|
||||
}
|
||||
|
||||
export const useSidebarStore = create<SidebarState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
workspaceDropdownOpen: false,
|
||||
sidebarWidth: SIDEBAR_WIDTH.DEFAULT,
|
||||
isCollapsed: readCollapsedCookie(),
|
||||
_hasHydrated: false,
|
||||
setWorkspaceDropdownOpen: (isOpen) => set({ workspaceDropdownOpen: isOpen }),
|
||||
setSidebarWidth: (width) => {
|
||||
if (get().isCollapsed) return
|
||||
const clampedWidth = clampSidebarWidth(width)
|
||||
set({ sidebarWidth: clampedWidth })
|
||||
applySidebarWidth(clampedWidth)
|
||||
},
|
||||
toggleCollapsed: () => {
|
||||
const { isCollapsed, sidebarWidth } = get()
|
||||
const nextCollapsed = !isCollapsed
|
||||
set({ isCollapsed: nextCollapsed })
|
||||
applyCollapsedCookie(nextCollapsed)
|
||||
applySidebarWidth(nextCollapsed ? SIDEBAR_WIDTH.COLLAPSED : clampSidebarWidth(sidebarWidth))
|
||||
},
|
||||
syncWidth: () => {
|
||||
const { isCollapsed, sidebarWidth } = get()
|
||||
if (isCollapsed) {
|
||||
applySidebarWidth(SIDEBAR_WIDTH.COLLAPSED)
|
||||
return
|
||||
}
|
||||
const clampedWidth = clampSidebarWidth(sidebarWidth)
|
||||
if (clampedWidth !== sidebarWidth) set({ sidebarWidth: clampedWidth })
|
||||
applySidebarWidth(clampedWidth)
|
||||
},
|
||||
setHasHydrated: (hasHydrated) => set({ _hasHydrated: hasHydrated }),
|
||||
}),
|
||||
{
|
||||
name: 'sidebar-state',
|
||||
/**
|
||||
* Width is hydrated manually from a client-only effect (see Sidebar) so
|
||||
* `_hasHydrated` is deterministically `false` during SSR and the first
|
||||
* client render — both of which read collapse from the cookie-seeded prop.
|
||||
* This is zustand's documented SSR pattern; it avoids relying on auto
|
||||
* hydration's behavior when `localStorage` is absent on the server.
|
||||
*/
|
||||
skipHydration: true,
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state) {
|
||||
state.setHasHydrated(true)
|
||||
const width = state.isCollapsed
|
||||
? SIDEBAR_WIDTH.COLLAPSED
|
||||
: clampSidebarWidth(state.sidebarWidth)
|
||||
applySidebarWidth(width)
|
||||
}
|
||||
},
|
||||
/** Only width is persisted; collapse lives in the cookie. */
|
||||
partialize: (state) => ({ sidebarWidth: state.sidebarWidth }),
|
||||
/**
|
||||
* Never lets a legacy persisted `isCollapsed` override the cookie-seeded
|
||||
* value — the cookie is the source of truth (handles migration cleanly).
|
||||
*/
|
||||
merge: (persisted, current) => ({
|
||||
...current,
|
||||
...(persisted as Partial<SidebarState>),
|
||||
isCollapsed: current.isCollapsed,
|
||||
}),
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Sidebar state interface
|
||||
*/
|
||||
export interface SidebarState {
|
||||
workspaceDropdownOpen: boolean
|
||||
sidebarWidth: number
|
||||
/** Whether the sidebar is collapsed to icon-only mode */
|
||||
isCollapsed: boolean
|
||||
_hasHydrated: boolean
|
||||
setWorkspaceDropdownOpen: (isOpen: boolean) => void
|
||||
setSidebarWidth: (width: number) => void
|
||||
/** Toggles sidebar between collapsed and expanded states */
|
||||
toggleCollapsed: () => void
|
||||
/**
|
||||
* Re-applies the `--sidebar-width` CSS variable from current state, clamped to
|
||||
* the viewport. Self-heals cases where the variable was left at its `0px`
|
||||
* default (e.g. soft navigation) or grew wider than a now-smaller window.
|
||||
*/
|
||||
syncWidth: () => void
|
||||
setHasHydrated: (hasHydrated: boolean) => void
|
||||
}
|
||||
Reference in New Issue
Block a user