WIP jordan
This commit is contained in:
391
app/pages/settings/profile-layouts.vue
Normal file
391
app/pages/settings/profile-layouts.vue
Normal file
@@ -0,0 +1,391 @@
|
||||
<script setup lang="ts">
|
||||
import { useProfileLayouts } from '~/composables/useProfileLayouts'
|
||||
import type { ProfileLayout } from '~/composables/useProfileLayouts'
|
||||
|
||||
definePageMeta({ ssr: false })
|
||||
usePageTitle('Profile Layouts · Settings')
|
||||
|
||||
const {
|
||||
layouts,
|
||||
activeLayoutId,
|
||||
activeLayout,
|
||||
sortedSections,
|
||||
setActiveLayout,
|
||||
updateLayout,
|
||||
removeCustomLayout,
|
||||
resetToDefaults,
|
||||
} = useProfileLayouts()
|
||||
|
||||
/* ── Built-in vs custom ── */
|
||||
const builtInLayouts = computed(() => layouts.value.filter(l => !l.isCustom))
|
||||
const customLayouts = computed(() => layouts.value.filter(l => l.isCustom))
|
||||
|
||||
/* ── Toggle section visibility ── */
|
||||
function toggleSectionVisibility(sectionId: string) {
|
||||
const updated = activeLayout.value.sections.map(s =>
|
||||
s.id === sectionId ? { ...s, visible: !s.visible } : s
|
||||
)
|
||||
updateLayout(activeLayout.value.id, { sections: updated })
|
||||
}
|
||||
|
||||
/* ── Move section up/down ── */
|
||||
function moveSection(sectionId: string, direction: 'up' | 'down') {
|
||||
const sections = [...activeLayout.value.sections].sort((a, b) => a.order - b.order)
|
||||
const idx = sections.findIndex(s => s.id === sectionId)
|
||||
if (idx < 0) return
|
||||
const swapIdx = direction === 'up' ? idx - 1 : idx + 1
|
||||
if (swapIdx < 0 || swapIdx >= sections.length) return
|
||||
|
||||
const tempOrder = sections[idx].order
|
||||
sections[idx] = { ...sections[idx], order: sections[swapIdx].order }
|
||||
sections[swapIdx] = { ...sections[swapIdx], order: tempOrder }
|
||||
|
||||
updateLayout(activeLayout.value.id, { sections })
|
||||
}
|
||||
|
||||
/* ── Ordered sections for display ── */
|
||||
const orderedSections = computed(() =>
|
||||
[...activeLayout.value.sections].sort((a, b) => a.order - b.order)
|
||||
)
|
||||
|
||||
/* ── Delete custom layout ── */
|
||||
function handleDelete(id: string) {
|
||||
removeCustomLayout(id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pl-page">
|
||||
<!-- Back -->
|
||||
<NuxtLink to="/settings" class="inline-flex">
|
||||
<UButton color="neutral" variant="ghost" size="sm" icon="i-heroicons-arrow-left">Settings</UButton>
|
||||
</NuxtLink>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="max-w-xl">
|
||||
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-[var(--text-primary)]">Profile Layouts</h1>
|
||||
<p class="mt-2 text-[14px] leading-relaxed text-[var(--text-muted)]">
|
||||
Configure which sections appear and in what order based on your role.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Dev note -->
|
||||
<div class="pl-dev-note">
|
||||
<UIcon name="i-heroicons-beaker" style="width: 16px; height: 16px; flex-shrink: 0;" />
|
||||
<div>
|
||||
<p class="pl-dev-note-title">In development</p>
|
||||
<p class="pl-dev-note-text">
|
||||
Layout preferences are stored locally. Once the API is ready, layouts will sync across devices and can be assigned organization-wide by role.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════ Active Layout Selector ═══════════════ -->
|
||||
<section>
|
||||
<p class="pl-label">Active layout</p>
|
||||
<div class="pl-layout-grid">
|
||||
<button
|
||||
v-for="layout in builtInLayouts"
|
||||
:key="layout.id"
|
||||
type="button"
|
||||
class="pl-layout-card"
|
||||
:class="activeLayoutId === layout.id ? 'pl-layout-active' : 'pl-layout-inactive'"
|
||||
@click="setActiveLayout(layout.id)"
|
||||
>
|
||||
<div class="pl-layout-card-inner">
|
||||
<div class="pl-layout-icon" :class="activeLayoutId === layout.id ? 'pl-icon-active' : 'pl-icon-inactive'">
|
||||
<UIcon :name="layout.icon" style="width: 20px; height: 20px;" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="pl-layout-name">{{ layout.name }}</p>
|
||||
<UIcon
|
||||
v-if="activeLayoutId === layout.id"
|
||||
name="i-heroicons-check-circle-solid"
|
||||
class="pl-check-icon"
|
||||
/>
|
||||
</div>
|
||||
<p class="pl-layout-desc">{{ layout.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════ Section Order ═══════════════ -->
|
||||
<section>
|
||||
<p class="pl-label">Section order · {{ activeLayout.name }}</p>
|
||||
<p class="pl-sublabel">Reorder sections and toggle visibility for the active layout.</p>
|
||||
<div class="pl-section-list">
|
||||
<div
|
||||
v-for="(section, idx) in orderedSections"
|
||||
:key="section.id"
|
||||
class="pl-section-row"
|
||||
:class="{ 'pl-section-hidden': !section.visible }"
|
||||
>
|
||||
<div class="pl-drag-handle" title="Drag to reorder (coming soon)">
|
||||
<UIcon name="i-heroicons-bars-3" style="width: 16px; height: 16px;" />
|
||||
</div>
|
||||
|
||||
<span class="pl-section-order">{{ idx + 1 }}</span>
|
||||
|
||||
<span class="pl-section-label" :class="{ 'pl-section-label-hidden': !section.visible }">
|
||||
{{ section.label }}
|
||||
</span>
|
||||
|
||||
<div class="pl-section-spacer" />
|
||||
|
||||
<div class="pl-section-arrows">
|
||||
<button
|
||||
type="button"
|
||||
class="pl-arrow-btn"
|
||||
:disabled="idx === 0"
|
||||
title="Move up"
|
||||
@click="moveSection(section.id, 'up')"
|
||||
>
|
||||
<UIcon name="i-heroicons-chevron-up" style="width: 14px; height: 14px;" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pl-arrow-btn"
|
||||
:disabled="idx === orderedSections.length - 1"
|
||||
title="Move down"
|
||||
@click="moveSection(section.id, 'down')"
|
||||
>
|
||||
<UIcon name="i-heroicons-chevron-down" style="width: 14px; height: 14px;" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="pl-toggle"
|
||||
:class="section.visible ? 'pl-toggle-on' : 'pl-toggle-off'"
|
||||
:title="section.visible ? 'Hide section' : 'Show section'"
|
||||
@click="toggleSectionVisibility(section.id)"
|
||||
>
|
||||
<span class="pl-toggle-dot" :class="section.visible ? 'pl-dot-on' : 'pl-dot-off'" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="pl-hint">Default tab: <strong>{{ activeLayout.defaultTab }}</strong></p>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════ Custom Layouts ═══════════════ -->
|
||||
<section>
|
||||
<p class="pl-label">Custom layouts</p>
|
||||
<p class="pl-sublabel">Create personalized layouts beyond the built-in role presets.</p>
|
||||
|
||||
<div v-if="customLayouts.length" class="pl-section-list">
|
||||
<div
|
||||
v-for="layout in customLayouts"
|
||||
:key="layout.id"
|
||||
class="pl-custom-row"
|
||||
>
|
||||
<div class="pl-layout-icon pl-icon-inactive" style="width: 32px; height: 32px;">
|
||||
<UIcon :name="layout.icon" style="width: 16px; height: 16px;" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[13px] font-semibold text-[var(--text-primary)]">{{ layout.name }}</p>
|
||||
<p class="text-[11px] text-[var(--text-muted)]">{{ layout.description }}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="pl-action-btn"
|
||||
title="Use this layout"
|
||||
@click="setActiveLayout(layout.id)"
|
||||
>
|
||||
<UIcon name="i-heroicons-check" style="width: 14px; height: 14px;" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pl-action-btn pl-action-delete"
|
||||
title="Delete custom layout"
|
||||
@click="handleDelete(layout.id)"
|
||||
>
|
||||
<UIcon name="i-heroicons-trash" style="width: 14px; height: 14px;" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="pl-empty">
|
||||
<p>No custom layouts yet.</p>
|
||||
</div>
|
||||
|
||||
<button type="button" class="pl-btn-primary" style="margin-top: 12px;" disabled>
|
||||
<UIcon name="i-heroicons-plus" style="width: 16px; height: 16px;" />
|
||||
Create Custom Layout
|
||||
</button>
|
||||
<p class="pl-hint" style="margin-top: 6px;">Custom layout creation coming soon.</p>
|
||||
</section>
|
||||
|
||||
<!-- Reset -->
|
||||
<section class="pl-reset-section">
|
||||
<button type="button" class="pl-btn-ghost" @click="resetToDefaults">
|
||||
<UIcon name="i-heroicons-arrow-uturn-left" style="width: 14px; height: 14px;" />
|
||||
Reset all layouts to defaults
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ── Page shell ── */
|
||||
.pl-page {
|
||||
max-width: 56rem;
|
||||
margin: 0 auto;
|
||||
display: flex; flex-direction: column; gap: 28px;
|
||||
}
|
||||
|
||||
/* ── Dev note ── */
|
||||
.pl-dev-note {
|
||||
display: flex; gap: 10px; align-items: flex-start;
|
||||
padding: 12px 16px; border-radius: 10px;
|
||||
background: rgba(124, 58, 237, 0.06); color: #7c3aed;
|
||||
}
|
||||
.pl-dev-note-title { font-size: 12px; font-weight: 700; }
|
||||
.pl-dev-note-text { font-size: 12px; line-height: 1.5; opacity: 0.85; }
|
||||
|
||||
/* ── Labels ── */
|
||||
.pl-label {
|
||||
font-size: 11px; font-weight: 600; text-transform: uppercase;
|
||||
letter-spacing: 0.04em; color: #8a8a86; margin-bottom: 10px;
|
||||
}
|
||||
.pl-sublabel {
|
||||
font-size: 13px; color: var(--text-muted); margin-top: -4px; margin-bottom: 12px;
|
||||
}
|
||||
.pl-hint {
|
||||
font-size: 12px; color: var(--text-muted); margin-top: 8px;
|
||||
}
|
||||
|
||||
/* ── Layout selector grid ── */
|
||||
.pl-layout-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px;
|
||||
}
|
||||
.pl-layout-card {
|
||||
border-radius: 12px; padding: 14px; cursor: pointer;
|
||||
border: 1.5px solid transparent; background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
||||
transition: all 150ms ease; text-align: left;
|
||||
}
|
||||
.pl-layout-inactive {
|
||||
border-color: rgba(0,0,0,0.06);
|
||||
}
|
||||
.pl-layout-inactive:hover {
|
||||
border-color: rgba(0,0,0,0.12);
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.06);
|
||||
}
|
||||
.pl-layout-active {
|
||||
border-color: #01696f;
|
||||
box-shadow: 0 0 0 1px #01696f, 0 2px 8px rgba(1,105,111,0.10);
|
||||
}
|
||||
.pl-layout-card-inner {
|
||||
display: flex; align-items: flex-start; gap: 10px;
|
||||
}
|
||||
.pl-layout-icon {
|
||||
width: 40px; height: 40px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
}
|
||||
.pl-icon-active { background: rgba(1,105,111,0.10); color: #01696f; }
|
||||
.pl-icon-inactive { background: rgba(0,0,0,0.04); color: #8a8a86; }
|
||||
.pl-layout-name { font-size: 13px; font-weight: 600; color: var(--text-primary); }
|
||||
.pl-layout-desc { font-size: 11px; color: var(--text-muted); margin-top: 2px; line-height: 1.4; }
|
||||
.pl-check-icon { width: 16px; height: 16px; color: #01696f; flex-shrink: 0; }
|
||||
|
||||
/* ── Section list ── */
|
||||
.pl-section-list {
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
}
|
||||
.pl-section-row {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 10px 14px; border-radius: 10px;
|
||||
border: 1px solid rgba(0,0,0,0.06); background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
||||
transition: background 150ms ease;
|
||||
}
|
||||
.pl-section-row:hover { background: rgba(0,0,0,0.01); }
|
||||
.pl-section-hidden { opacity: 0.5; }
|
||||
|
||||
.pl-drag-handle {
|
||||
color: #c4c4c0; cursor: grab; display: flex; align-items: center;
|
||||
}
|
||||
.pl-section-order {
|
||||
font-size: 11px; font-weight: 700; color: #8a8a86;
|
||||
width: 20px; text-align: center; font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.pl-section-label { font-size: 13px; font-weight: 500; color: var(--text-primary); }
|
||||
.pl-section-label-hidden { text-decoration: line-through; color: var(--text-muted); }
|
||||
.pl-section-spacer { flex: 1; }
|
||||
|
||||
/* ── Arrows ── */
|
||||
.pl-section-arrows { display: flex; flex-direction: column; gap: 1px; }
|
||||
.pl-arrow-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 22px; height: 18px; border-radius: 4px; border: none; cursor: pointer;
|
||||
background: transparent; color: #8a8a86; transition: all 120ms ease;
|
||||
}
|
||||
.pl-arrow-btn:hover:not(:disabled) { background: rgba(0,0,0,0.06); color: var(--text-primary); }
|
||||
.pl-arrow-btn:disabled { opacity: 0.25; cursor: not-allowed; }
|
||||
|
||||
/* ── Toggle ── */
|
||||
.pl-toggle {
|
||||
width: 36px; height: 20px; border-radius: 10px; border: none;
|
||||
cursor: pointer; position: relative; transition: background 150ms ease; flex-shrink: 0;
|
||||
}
|
||||
.pl-toggle-on { background: #01696f; }
|
||||
.pl-toggle-off { background: rgba(0,0,0,0.12); }
|
||||
.pl-toggle-dot {
|
||||
display: block; width: 16px; height: 16px; border-radius: 8px;
|
||||
background: #fff; position: absolute; top: 2px; transition: left 150ms ease;
|
||||
}
|
||||
.pl-dot-on { left: 18px; }
|
||||
.pl-dot-off { left: 2px; }
|
||||
|
||||
/* ── Custom layout rows ── */
|
||||
.pl-custom-row {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 10px 14px; border-radius: 10px;
|
||||
border: 1px solid rgba(0,0,0,0.06); background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
||||
}
|
||||
.pl-action-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 28px; height: 28px; border-radius: 6px; border: none; cursor: pointer;
|
||||
background: rgba(0,0,0,0.03); color: #8a8a86; transition: all 150ms ease;
|
||||
}
|
||||
.pl-action-btn:hover { background: rgba(0,0,0,0.06); color: var(--text-primary); }
|
||||
.pl-action-delete:hover { background: rgba(193,56,56,0.08); color: #c13838; }
|
||||
|
||||
/* ── Empty state ── */
|
||||
.pl-empty {
|
||||
padding: 20px; border-radius: 10px; text-align: center;
|
||||
border: 1px dashed rgba(0,0,0,0.10); color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ── Buttons ── */
|
||||
.pl-btn-primary {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 8px 16px; border-radius: 8px;
|
||||
background: #01696f; color: #fff;
|
||||
font-size: 13px; font-weight: 500; border: none;
|
||||
cursor: pointer; transition: all 150ms ease; white-space: nowrap;
|
||||
}
|
||||
.pl-btn-primary:hover:not(:disabled) { background: #015458; }
|
||||
.pl-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.pl-btn-ghost {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 6px 12px; border-radius: 6px;
|
||||
background: transparent; color: var(--text-muted);
|
||||
font-size: 12px; font-weight: 500; border: 1px solid rgba(0,0,0,0.08);
|
||||
cursor: pointer; transition: all 150ms ease;
|
||||
}
|
||||
.pl-btn-ghost:hover { background: rgba(0,0,0,0.03); color: var(--text-primary); }
|
||||
|
||||
/* ── Reset section ── */
|
||||
.pl-reset-section {
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(0,0,0,0.06);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user