Improve the primitives sidebar

This commit is contained in:
Nettika 2026-01-29 01:06:48 -08:00
parent dfeed5d1a6
commit bfcacb184f
No known key found for this signature in database
6 changed files with 209 additions and 36 deletions

View file

@ -76,10 +76,10 @@ A step-by-step checklist for porting MatterControl's design features to a Vue +
- [x] Apply theme to all components
### Sidebar Behavior
- [ ] Create collapsible sidebar component
- [ ] Add toggle buttons for left/right sidebars
- [ ] Persist sidebar state to localStorage
- [ ] Handle responsive layout for narrow screens
- [x] Create collapsible sidebar component
- [x] Add toggle buttons for left/right sidebars
- [x] Persist sidebar state to localStorage
- [x] Handle responsive layout for narrow screens
---

View file

@ -1,4 +1,45 @@
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import PrimitivesSidebar from './PrimitivesSidebar.vue'
const STORAGE_KEY = 'rapidmodel-sidebar-state'
const leftCollapsed = ref(false)
const leftSidebarWidth = computed(() =>
leftCollapsed.value ? '0' : 'var(--sidebar-width)'
)
function loadSidebarState() {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
const state = JSON.parse(stored)
leftCollapsed.value = state.leftCollapsed ?? false
}
} catch {
// Ignore parse errors, use defaults
}
}
function saveSidebarState() {
try {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
leftCollapsed: leftCollapsed.value,
})
)
} catch {
// Ignore storage errors
}
}
watch(leftCollapsed, saveSidebarState)
onMounted(() => {
loadSidebarState()
})
</script>
<template>
@ -9,17 +50,23 @@
</slot>
</header>
<aside class="left-sidebar">
<slot name="left-sidebar" />
</aside>
<div class="main-area">
<main class="viewport">
<slot />
</main>
<PrimitivesSidebar
v-model:collapsed="leftCollapsed"
class="left-sidebar"
:style="{ width: leftSidebarWidth }"
>
<slot name="left-sidebar" />
</PrimitivesSidebar>
<aside class="right-sidebar">
<slot name="right-sidebar" />
</aside>
</div>
<footer class="status-bar">
<slot name="status-bar" />
@ -29,20 +76,16 @@
<style scoped>
.app-layout {
display: grid;
grid-template-areas:
'header header header'
'left-sidebar viewport right-sidebar'
'status-bar status-bar status-bar';
grid-template-columns: var(--sidebar-width) 1fr var(--sidebar-width);
grid-template-rows: var(--header-height) 1fr var(--status-bar-height);
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
overflow: hidden;
}
.header {
grid-area: header;
flex-shrink: 0;
height: var(--header-height);
display: flex;
align-items: center;
padding: 0 var(--space-md);
@ -56,27 +99,41 @@
color: var(--color-text-primary);
}
.left-sidebar {
grid-area: left-sidebar;
background: var(--color-bg-secondary);
border-right: 1px solid var(--color-border);
overflow-y: auto;
.main-area {
flex: 1;
position: relative;
}
.viewport {
grid-area: viewport;
position: absolute;
inset: 0;
overflow: hidden;
}
.left-sidebar {
position: absolute;
top: 0;
left: 0;
bottom: 0;
z-index: 1;
overflow: visible;
}
.right-sidebar {
grid-area: right-sidebar;
background: var(--color-bg-secondary);
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: var(--sidebar-width);
background: rgba(30, 30, 46, 0.85);
border-left: 1px solid var(--color-border);
overflow-y: auto;
z-index: 1;
}
.status-bar {
grid-area: status-bar;
flex-shrink: 0;
height: var(--status-bar-height);
display: flex;
align-items: center;
padding: 0 var(--space-md);
@ -85,4 +142,18 @@
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
/* Responsive: auto-collapse left sidebar on narrow screens */
@media (max-width: 900px) {
.left-sidebar {
width: 0 !important;
}
}
@media (max-width: 600px) {
.left-sidebar,
.right-sidebar {
display: none;
}
}
</style>

View file

@ -0,0 +1,103 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = withDefaults(
defineProps<{
collapsed?: boolean
}>(),
{
collapsed: false,
}
)
const emit = defineEmits<{
'update:collapsed': [value: boolean]
}>()
const isCollapsed = computed({
get: () => props.collapsed,
set: (value) => emit('update:collapsed', value),
})
function toggle() {
isCollapsed.value = !isCollapsed.value
}
</script>
<template>
<div class="primitives-sidebar-container">
<button
class="toggle"
:class="{ 'toggle--floating': isCollapsed }"
:title="isCollapsed ? 'Expand primitives toolbox' : 'Collapse primitives toolbox'"
@click="toggle"
>
<span class="toggle-icon">{{ isCollapsed ? '»' : '«' }}</span>
</button>
<aside v-show="!isCollapsed" class="primitives-sidebar">
<div class="content">
<slot />
</div>
</aside>
</div>
</template>
<style scoped>
.primitives-sidebar-container {
position: relative;
height: 100%;
overflow: visible;
}
.primitives-sidebar {
height: 100%;
display: flex;
flex-direction: column;
background: rgba(30, 30, 46, 0.85);
border-right: 1px solid var(--color-border);
overflow: hidden;
}
.toggle {
position: absolute;
top: var(--space-xs);
right: var(--space-xs);
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
z-index: 2;
}
.toggle--floating {
top: var(--space-sm);
left: var(--space-sm);
right: auto;
background: rgba(30, 30, 46, 0.85);
border: 1px solid var(--color-border);
}
.toggle:hover {
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
}
.toggle-icon {
font-size: var(--font-size-lg);
line-height: 1;
}
.content {
flex: 1;
overflow-y: auto;
padding: var(--space-sm);
padding-top: calc(var(--space-xs) + 24px + var(--space-xs));
}
</style>

View file

@ -189,8 +189,8 @@ describe('useSceneStore', () => {
const list = store.objectList
expect(list).toHaveLength(2)
expect(list[0].name).toBe('A')
expect(list[1].name).toBe('B')
expect(list[0]?.name).toBe('A')
expect(list[1]?.name).toBe('B')
})
})
})

View file

@ -59,10 +59,9 @@ export const useSceneStore = defineStore('scene', () => {
}
function removeObject(id: string): boolean {
const index = objects.value.findIndex((o) => o.id === id)
if (index === -1) return false
const obj = objects.value.find((o) => o.id === id)
if (!obj) return false
const obj = objects.value[index]
if (threeScene) {
threeScene.remove(obj.mesh)
}

View file

@ -130,7 +130,7 @@ button {
background-color: var(--color-bg-tertiary);
color: var(--color-text-primary);
cursor: pointer;
transition: all var(--transition-fast);
transition: background-color var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}
button:hover {