WIP jordan

This commit is contained in:
Jordan Weingarten
2026-04-16 11:11:44 -05:00
parent ff2d7b18b5
commit 67482f6629
163 changed files with 50627 additions and 728 deletions

View 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 &middot; {{ 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>