Improve the primitives sidebar
This commit is contained in:
parent
dfeed5d1a6
commit
bfcacb184f
6 changed files with 209 additions and 36 deletions
8
TODO.md
8
TODO.md
|
|
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
103
src/components/PrimitivesSidebar.vue
Normal file
103
src/components/PrimitivesSidebar.vue
Normal 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>
|
||||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue