ref: change ui
This commit is contained in:
+1
-1
@@ -7,7 +7,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@600;700;800&family=Inter:wght@400;500;600;700&display=swap"
|
||||
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500..800&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>My Workspace</title>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="flex min-h-0 flex-1 flex-col">
|
||||
<div v-if="$slots.filters" class="mb-4 flex flex-wrap items-end gap-3">
|
||||
<div v-if="$slots.filters" class="mb-5 flex flex-wrap items-end gap-3">
|
||||
<slot name="filters" />
|
||||
</div>
|
||||
<section class="panel flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<header
|
||||
class="flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3"
|
||||
class="flex flex-wrap items-center justify-between gap-3 border-b px-5 py-3.5"
|
||||
style="border-color: var(--hairline)"
|
||||
>
|
||||
<h2 class="table-title m-0">{{ title }}</h2>
|
||||
|
||||
@@ -1,32 +1,43 @@
|
||||
<template>
|
||||
<div class="panel flex h-full flex-col overflow-hidden">
|
||||
<div class="flex flex-col gap-3 border-b p-3 sm:flex-row sm:items-center" style="border-color: var(--hairline)">
|
||||
<IconField class="min-w-0 flex-1">
|
||||
<div class="flex h-full flex-col">
|
||||
<AppTableShell title="Documents">
|
||||
<template #filters>
|
||||
<IconField class="sm:w-[320px]">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText
|
||||
:model-value="searchTerm"
|
||||
placeholder="Search documents"
|
||||
placeholder="Search documents..."
|
||||
class="search-input w-full"
|
||||
@update:model-value="onSearch"
|
||||
/>
|
||||
</IconField>
|
||||
<div v-if="canCreate" class="flex shrink-0 gap-2">
|
||||
<Button label="Document" icon="pi pi-plus" size="small" @click="$emit('create', 'Document')" />
|
||||
<Button label="Folder" icon="pi pi-folder-open" severity="secondary" outlined size="small" @click="$emit('create', 'Folder')" />
|
||||
</div>
|
||||
</div>
|
||||
<Select v-model="filters.type" :options="typeOptions" optionLabel="label" optionValue="value" placeholder="Type" showClear class="w-[140px]" />
|
||||
<Select v-model="filters.owner" :options="members" optionLabel="displayName" optionValue="userId" placeholder="Owner" showClear class="w-[170px]" />
|
||||
<Select v-model="filters.updatedBy" :options="members" optionLabel="displayName" optionValue="userId" placeholder="Last edited by" showClear class="w-[180px]" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<template v-if="canCreate">
|
||||
<Button label="Document" icon="pi pi-plus" severity="secondary" outlined @click="$emit('create', 'Document')" />
|
||||
<Button label="Folder" icon="pi pi-folder-open" @click="$emit('create', 'Folder')" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<p v-if="canCreate" class="muted-note border-b px-3 py-2" style="border-color: var(--hairline)">
|
||||
<p v-if="canCreate" class="muted-note border-b px-4 py-2" style="border-color: var(--hairline)">
|
||||
{{ creatingLabel }}
|
||||
</p>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-auto">
|
||||
<TreeTable
|
||||
v-model:selectionKeys="selectionKeys"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
:value="nodes"
|
||||
:loading="loading"
|
||||
dataKey="key"
|
||||
selectionMode="single"
|
||||
scrollable
|
||||
scrollHeight="flex"
|
||||
class="min-h-0 min-w-[720px] flex-1"
|
||||
@node-select="onNodeSelect"
|
||||
@node-unselect="onNodeUnselect"
|
||||
>
|
||||
<Column field="title" header="Name" expander style="min-width: 240px">
|
||||
<template #body="{ node }">
|
||||
@@ -55,21 +66,46 @@
|
||||
<span class="muted-note">{{ formatDate(node.data.updatedAt) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="" style="width: 110px">
|
||||
<template #body="{ node }">
|
||||
<div v-if="canEdit || canDelete" class="flex justify-end gap-1">
|
||||
<Button
|
||||
v-if="canEdit"
|
||||
icon="pi pi-pencil"
|
||||
text
|
||||
severity="secondary"
|
||||
size="small"
|
||||
:aria-label="`Rename ${node.data.title}`"
|
||||
@click.stop="$emit('rename', node.data)"
|
||||
/>
|
||||
<Button
|
||||
v-if="canDelete"
|
||||
icon="pi pi-trash"
|
||||
text
|
||||
severity="danger"
|
||||
size="small"
|
||||
:aria-label="`Delete ${node.data.title}`"
|
||||
@click.stop="$emit('delete', node.data)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<template #empty>
|
||||
<div class="grid place-items-center gap-2 px-4 py-14 text-center">
|
||||
<i class="pi pi-file-edit text-2xl" style="color: var(--ink-muted)"></i>
|
||||
<p class="font-semibold" style="color: var(--ink)">No documents yet</p>
|
||||
<p class="font-semibold" style="color: var(--ink)">{{ hasActiveFilters ? 'No matching documents' : 'No documents yet' }}</p>
|
||||
<p class="muted-note max-w-[280px]">
|
||||
{{ canCreate ? 'Create a document to start writing, or a folder to group them.' : 'Ask a project owner for document access.' }}
|
||||
{{ hasActiveFilters ? 'Try clearing the filters.' : (canCreate ? 'Create a document to start writing, or a folder to group them.' : 'Ask a project owner for document access.') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</TreeTable>
|
||||
</div>
|
||||
</AppTableShell>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppTableShell from './AppTableShell.vue'
|
||||
import type { DocumentNode, DocumentType, ProjectMember } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -79,6 +115,8 @@ const props = defineProps<{
|
||||
searchTerm: string
|
||||
creatingLabel: string
|
||||
canCreate: boolean
|
||||
canEdit: boolean
|
||||
canDelete: boolean
|
||||
members: ProjectMember[]
|
||||
}>()
|
||||
|
||||
@@ -86,6 +124,8 @@ const emit = defineEmits<{
|
||||
'update:searchTerm': [value: string]
|
||||
select: [id: string]
|
||||
create: [type: DocumentType]
|
||||
rename: [node: DocumentNode]
|
||||
delete: [node: DocumentNode]
|
||||
}>()
|
||||
|
||||
function onSearch(value: unknown) {
|
||||
@@ -111,8 +151,39 @@ function toTreeTableNode(node: DocumentNode): DocumentTreeTableNode {
|
||||
return { key: node.id, data: node, children: sortNodes(node.children).map(toTreeTableNode) }
|
||||
}
|
||||
|
||||
const nodes = computed(() => sortNodes(props.tree).map(toTreeTableNode))
|
||||
const filters = reactive<{ type: DocumentType | null; owner: string | null; updatedBy: string | null }>({
|
||||
type: null,
|
||||
owner: null,
|
||||
updatedBy: null,
|
||||
})
|
||||
|
||||
const typeOptions = [
|
||||
{ label: 'Document', value: 'Document' },
|
||||
{ label: 'Folder', value: 'Folder' },
|
||||
]
|
||||
|
||||
const hasActiveFilters = computed(() => !!(filters.type || filters.owner || filters.updatedBy))
|
||||
|
||||
function matchesFilters(node: DocumentNode): boolean {
|
||||
if (filters.type && node.type !== filters.type) return false
|
||||
if (filters.owner && node.createdBy !== filters.owner) return false
|
||||
if (filters.updatedBy && node.updatedBy !== filters.updatedBy) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function filterNodes(nodes: DocumentNode[]): DocumentNode[] {
|
||||
const result: DocumentNode[] = []
|
||||
for (const node of nodes) {
|
||||
const children = filterNodes(node.children)
|
||||
if (matchesFilters(node) || children.length > 0) result.push({ ...node, children })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const filteredTree = computed(() => (hasActiveFilters.value ? filterNodes(props.tree) : props.tree))
|
||||
const nodes = computed(() => sortNodes(filteredTree.value).map(toTreeTableNode))
|
||||
const selectionKeys = ref<Record<string, boolean>>({})
|
||||
const expandedKeys = ref<Record<string, boolean>>({})
|
||||
|
||||
function resolveUserName(userId: string) {
|
||||
return props.members.find((m) => m.userId === userId)?.displayName ?? '—'
|
||||
@@ -123,7 +194,18 @@ function formatDate(v?: string | null) {
|
||||
return Number.isNaN(date.valueOf()) ? '—' : date.toLocaleDateString()
|
||||
}
|
||||
|
||||
function toggleFolder(node?: { key?: string; data?: DocumentNode }) {
|
||||
if (node?.key && node.data?.type === 'Folder') {
|
||||
expandedKeys.value = { ...expandedKeys.value, [node.key]: !expandedKeys.value[node.key] }
|
||||
}
|
||||
}
|
||||
|
||||
function onNodeSelect(node: { key?: string; data?: DocumentNode }) {
|
||||
if (node.key) emit('select', node.key)
|
||||
if (node.data?.type === 'Document' && node.key) emit('select', node.key)
|
||||
else toggleFolder(node)
|
||||
}
|
||||
|
||||
function onNodeUnselect(node: { key?: string; data?: DocumentNode }) {
|
||||
toggleFolder(node)
|
||||
}
|
||||
</script>
|
||||
|
||||
+20
-19
@@ -12,16 +12,16 @@
|
||||
<aside
|
||||
class="fixed inset-y-0 left-0 z-40 flex w-[236px] transform flex-col border-r px-3 py-4 transition-transform duration-200 lg:static"
|
||||
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:hidden'"
|
||||
style="background: var(--panel); border-color: var(--hairline)"
|
||||
style="background: var(--primary); border-color: var(--primary)"
|
||||
>
|
||||
<div class="mb-5 flex items-center justify-between px-2">
|
||||
<router-link to="/projects" class="flex items-center gap-2.5">
|
||||
<span
|
||||
class="grid h-8 w-8 place-items-center rounded-[10px] font-display text-[15px] font-extrabold text-white"
|
||||
style="background: var(--primary)"
|
||||
class="grid h-8 w-8 place-items-center rounded-[10px] font-display text-[15px] font-extrabold"
|
||||
style="background: #fff; color: var(--primary)"
|
||||
>M</span
|
||||
>
|
||||
<span class="font-display text-[15px] font-extrabold tracking-[-0.01em]" style="color: var(--ink)">
|
||||
<span class="font-display text-[15px] font-extrabold tracking-[-0.01em]" style="color: #fff">
|
||||
Workspace
|
||||
</span>
|
||||
</router-link>
|
||||
@@ -31,6 +31,7 @@
|
||||
rounded
|
||||
severity="secondary"
|
||||
class="lg:hidden"
|
||||
style="color: #fff"
|
||||
aria-label="Close menu"
|
||||
@click="sidebarOpen = false"
|
||||
/>
|
||||
@@ -54,7 +55,7 @@
|
||||
<i class="pi pi-cog"></i> Settings
|
||||
<i class="pi ml-auto text-xs" :class="settingsExpanded ? 'pi-chevron-down' : 'pi-chevron-right'"></i>
|
||||
</button>
|
||||
<div v-show="settingsExpanded" class="ml-3 mt-2 flex flex-col gap-0.5 border-l pl-2" style="border-color: var(--hairline)">
|
||||
<div v-show="settingsExpanded" class="ml-3 mt-2 flex flex-col gap-0.5 border-l pl-2" style="border-color: rgba(255, 255, 255, 0.3)">
|
||||
<router-link v-if="auth.canView('masterdata')" class="nav-link nav-link-sub submenu-link" to="/settings/masterdata" @click="handleNavClick">
|
||||
Master Data
|
||||
</router-link>
|
||||
@@ -100,7 +101,7 @@
|
||||
</header>
|
||||
|
||||
<main class="flex-1 overflow-auto">
|
||||
<div class="mx-auto h-full w-full max-w-[1280px] p-4 sm:p-6 lg:px-8 lg:py-7">
|
||||
<div class="mx-auto h-full w-full max-w-[1200px] p-5 sm:p-7 lg:px-9 lg:py-8">
|
||||
<router-view />
|
||||
</div>
|
||||
</main>
|
||||
@@ -149,23 +150,23 @@ watch(
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
border-radius: 12px;
|
||||
border-radius: 10px;
|
||||
padding: 0.5rem 0.875rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--ink-muted);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
.nav-link i {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.nav-link:hover {
|
||||
background-color: var(--primary-soft);
|
||||
color: var(--ink);
|
||||
background-color: rgba(255, 255, 255, 0.14);
|
||||
color: #fff;
|
||||
}
|
||||
.nav-link.router-link-active {
|
||||
background-color: var(--primary-soft);
|
||||
color: var(--primary);
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav-link-sub {
|
||||
@@ -175,8 +176,8 @@ watch(
|
||||
border-radius: 8px;
|
||||
}
|
||||
.nav-link-sub.router-link-active {
|
||||
background-color: var(--primary-soft);
|
||||
color: var(--primary);
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav-link-sub.router-link-active::before {
|
||||
@@ -187,10 +188,10 @@ watch(
|
||||
height: 60%;
|
||||
width: 3px;
|
||||
border-radius: 999px;
|
||||
background: var(--magenta);
|
||||
background: #fff;
|
||||
}
|
||||
.app-dark .nav-link.router-link-active {
|
||||
color: #9ba6f8;
|
||||
color: #fff;
|
||||
}
|
||||
.nav-link.router-link-active::before {
|
||||
content: '';
|
||||
@@ -200,11 +201,11 @@ watch(
|
||||
height: 60%;
|
||||
width: 3px;
|
||||
border-radius: 999px;
|
||||
background: var(--magenta);
|
||||
background: #fff;
|
||||
}
|
||||
.submenu-link.router-link-active {
|
||||
background: transparent !important;
|
||||
color: #6a7cd0 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.submenu-link.router-link-active::before {
|
||||
content: '';
|
||||
@@ -214,6 +215,6 @@ watch(
|
||||
height: 60%;
|
||||
width: 2px;
|
||||
border-radius: 999px;
|
||||
background: var(--magenta);
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
+15
-14
@@ -12,16 +12,16 @@
|
||||
<aside
|
||||
class="fixed inset-y-0 left-0 z-40 flex w-[236px] transform flex-col border-r px-3 py-4 transition-transform duration-200 lg:static"
|
||||
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:hidden'"
|
||||
style="background: var(--panel); border-color: var(--hairline)"
|
||||
style="background: var(--primary); border-color: var(--primary)"
|
||||
>
|
||||
<div class="mb-5 flex items-center justify-between px-2">
|
||||
<router-link :to="auth.isAdmin ? '/projects' : '/select-project'" class="flex items-center gap-2.5">
|
||||
<span
|
||||
class="grid h-8 w-8 place-items-center rounded-[10px] font-display text-[15px] font-extrabold text-white"
|
||||
style="background: var(--primary)"
|
||||
class="grid h-8 w-8 place-items-center rounded-[10px] font-display text-[15px] font-extrabold"
|
||||
style="background: #fff; color: var(--primary)"
|
||||
>M</span
|
||||
>
|
||||
<span class="font-display text-[15px] font-extrabold tracking-[-0.01em]" style="color: var(--ink)">
|
||||
<span class="font-display text-[15px] font-extrabold tracking-[-0.01em]" style="color: #fff">
|
||||
Workspace
|
||||
</span>
|
||||
</router-link>
|
||||
@@ -31,13 +31,14 @@
|
||||
rounded
|
||||
severity="secondary"
|
||||
class="lg:hidden"
|
||||
style="color: #fff"
|
||||
aria-label="Close menu"
|
||||
@click="sidebarOpen = false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<nav v-if="currentProject" class="flex flex-col gap-0.5">
|
||||
<div class="truncate px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wide" style="color: var(--ink-muted)">
|
||||
<div class="truncate px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wide" style="color: rgba(255, 255, 255, 0.75)">
|
||||
{{ currentProject.name }}
|
||||
</div>
|
||||
<router-link
|
||||
@@ -107,7 +108,7 @@
|
||||
</header>
|
||||
|
||||
<main class="flex-1 overflow-auto">
|
||||
<div class="mx-auto h-full w-full max-w-[1280px] p-4 sm:p-6 lg:px-8 lg:py-7">
|
||||
<div class="mx-auto h-full w-full max-w-[1200px] p-5 sm:p-7 lg:px-9 lg:py-8">
|
||||
<router-view />
|
||||
</div>
|
||||
</main>
|
||||
@@ -209,27 +210,27 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
border-radius: 12px;
|
||||
border-radius: 10px;
|
||||
padding: 0.5rem 0.875rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--ink-muted);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
.nav-link i {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.nav-link:hover {
|
||||
background-color: var(--primary-soft);
|
||||
color: var(--ink);
|
||||
background-color: rgba(255, 255, 255, 0.14);
|
||||
color: #fff;
|
||||
}
|
||||
.nav-link.router-link-active {
|
||||
background-color: var(--primary-soft);
|
||||
color: var(--primary);
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.app-dark .nav-link.router-link-active {
|
||||
color: #9ba6f8;
|
||||
color: #fff;
|
||||
}
|
||||
.nav-link.router-link-active::before {
|
||||
content: '';
|
||||
@@ -239,6 +240,6 @@ onMounted(() => {
|
||||
height: 60%;
|
||||
width: 3px;
|
||||
border-radius: 999px;
|
||||
background: var(--magenta);
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
+78
-58
@@ -3,9 +3,9 @@
|
||||
@custom-variant dark (&:where(.app-dark, .app-dark *));
|
||||
|
||||
@theme {
|
||||
/* Display = heavy geometric grotesque (DESIGN.md substitute for ABC Ginto Nord) */
|
||||
--font-sans: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
--font-display: 'Hanken Grotesk', 'Inter', Helvetica, Arial, sans-serif;
|
||||
/* Display = Bricolage Grotesque (characterful grotesque), body = IBM Plex Sans */
|
||||
--font-sans: 'IBM Plex Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
--font-display: 'Bricolage Grotesque', 'IBM Plex Sans', Helvetica, Arial, sans-serif;
|
||||
|
||||
/* Blurple — brand primary, remaps every indigo-* utility */
|
||||
--color-indigo-50: #eef0fe;
|
||||
@@ -51,42 +51,42 @@
|
||||
--color-green-900: #073a1f;
|
||||
--color-green-950: #042313;
|
||||
|
||||
/* Cool indigo-tinted neutrals — remaps every slate-* utility */
|
||||
--color-slate-50: #f6f7fb;
|
||||
--color-slate-100: #eef0f7;
|
||||
--color-slate-200: #dde0ed;
|
||||
--color-slate-300: #c1c6dd;
|
||||
--color-slate-400: #8e95b5;
|
||||
--color-slate-500: #666d92;
|
||||
--color-slate-600: #4a5177;
|
||||
--color-slate-700: #2d3358;
|
||||
--color-slate-800: #1e2353;
|
||||
--color-slate-900: #141840;
|
||||
--color-slate-950: #0a0d3a;
|
||||
/* Warm neutral stones — remaps every slate-* utility (no indigo tint) */
|
||||
--color-slate-50: #f8f7f4;
|
||||
--color-slate-100: #f0eee9;
|
||||
--color-slate-200: #e5e2db;
|
||||
--color-slate-300: #cfcbc2;
|
||||
--color-slate-400: #a3a09a;
|
||||
--color-slate-500: #6e6b66;
|
||||
--color-slate-600: #52504c;
|
||||
--color-slate-700: #3b3a37;
|
||||
--color-slate-800: #26262a;
|
||||
--color-slate-900: #17181c;
|
||||
--color-slate-950: #0e0f12;
|
||||
|
||||
--shadow-subtle: rgba(69, 42, 124, 0.1) 0px 3px 34px;
|
||||
--shadow-micro: rgba(20, 24, 64, 0.06) 0px 1px 3px;
|
||||
--shadow-subtle: rgba(28, 25, 20, 0.08) 0 8px 32px;
|
||||
--shadow-micro: rgba(28, 25, 20, 0.05) 0 1px 2px;
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-family: var(--font-sans);
|
||||
--ink: #141840;
|
||||
--ink-muted: #666d92;
|
||||
--ink: #17181c;
|
||||
--ink-muted: #6e6b66;
|
||||
--primary: #5865f2;
|
||||
--magenta: #ec48bd;
|
||||
--canvas: #f6f7fb;
|
||||
--canvas: #f8f7f4;
|
||||
--panel: #ffffff;
|
||||
--hairline: #dde0ed;
|
||||
--primary-soft: rgba(88, 101, 242, 0.12);
|
||||
--hairline: #e5e2db;
|
||||
--primary-soft: rgba(88, 101, 242, 0.1);
|
||||
}
|
||||
|
||||
.app-dark {
|
||||
--ink: #ffffff;
|
||||
--ink-muted: #8e95b5;
|
||||
--canvas: #0a0d3a;
|
||||
--panel: #141840;
|
||||
--hairline: #2d3358;
|
||||
--primary-soft: rgba(123, 136, 245, 0.2);
|
||||
--ink: #f3f2ef;
|
||||
--ink-muted: #a3a09a;
|
||||
--canvas: #0e0f12;
|
||||
--panel: #17191e;
|
||||
--hairline: #2c2f36;
|
||||
--primary-soft: rgba(123, 136, 245, 0.18);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -174,25 +174,25 @@
|
||||
}
|
||||
|
||||
.app-dark .ck.ck-editor {
|
||||
--ck-color-base-background: #0a0d3a;
|
||||
--ck-color-base-border: #2d3358;
|
||||
--ck-color-base-text: #ffffff;
|
||||
--ck-color-text: #ffffff;
|
||||
--ck-color-base-background: #0e0f12;
|
||||
--ck-color-base-border: #2c2f36;
|
||||
--ck-color-base-text: #f3f2ef;
|
||||
--ck-color-text: #f3f2ef;
|
||||
--ck-color-focus-border: #5865f2;
|
||||
--ck-color-toolbar-background: #141840;
|
||||
--ck-color-toolbar-border: #2d3358;
|
||||
--ck-color-dropdown-panel-background: #141840;
|
||||
--ck-color-panel-background: #141840;
|
||||
--ck-color-panel-border: #2d3358;
|
||||
--ck-color-button-default-hover-background: #2d3358;
|
||||
--ck-color-button-default-active-background: #2d3358;
|
||||
--ck-color-button-on-background: #2d3358;
|
||||
--ck-color-input-background: #0a0d3a;
|
||||
--ck-color-input-border: #2d3358;
|
||||
--ck-color-input-text: #ffffff;
|
||||
--ck-color-tooltip-background: #2d3358;
|
||||
--ck-color-tooltip-text: #ffffff;
|
||||
--ck-color-table-border: #4a5177;
|
||||
--ck-color-toolbar-background: #17191e;
|
||||
--ck-color-toolbar-border: #2c2f36;
|
||||
--ck-color-dropdown-panel-background: #17191e;
|
||||
--ck-color-panel-background: #17191e;
|
||||
--ck-color-panel-border: #2c2f36;
|
||||
--ck-color-button-default-hover-background: #2c2f36;
|
||||
--ck-color-button-default-active-background: #2c2f36;
|
||||
--ck-color-button-on-background: #2c2f36;
|
||||
--ck-color-input-background: #0e0f12;
|
||||
--ck-color-input-border: #2c2f36;
|
||||
--ck-color-input-text: #f3f2ef;
|
||||
--ck-color-tooltip-background: #2c2f36;
|
||||
--ck-color-tooltip-text: #f3f2ef;
|
||||
--ck-color-table-border: #52504c;
|
||||
--ck-color-link-default: #9ba6f8;
|
||||
}
|
||||
}
|
||||
@@ -203,28 +203,28 @@
|
||||
}
|
||||
|
||||
.field > label {
|
||||
@apply mb-1.5 block text-[13px] font-semibold uppercase tracking-[0.06em];
|
||||
@apply mb-1.5 block text-[12px] font-semibold uppercase tracking-[0.08em];
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(28px, 3vw, 38px);
|
||||
font-weight: 800;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.02em;
|
||||
font-size: clamp(26px, 2.6vw, 36px);
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.025em;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.muted-note {
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
line-height: 1.5;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
@@ -232,16 +232,16 @@
|
||||
.eyebrow {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.table-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
letter-spacing: -0.015em;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
@@ -249,13 +249,33 @@
|
||||
.panel {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 16px;
|
||||
border-radius: 14px;
|
||||
box-shadow: var(--shadow-micro);
|
||||
}
|
||||
|
||||
/* Table headers read as quiet eyebrows, not rows of body text */
|
||||
.p-datatable .p-datatable-header-cell,
|
||||
.p-treetable .p-treetable-header-cell {
|
||||
text-transform: uppercase;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.search-input.p-inputtext,
|
||||
.search-input.p-select {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
/* Numeric columns line up table to table */
|
||||
.p-datatable-table,
|
||||
.p-treetable-table {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Restrained "blurple" is an accent, not a wash */
|
||||
::selection {
|
||||
background: var(--primary-soft);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
+29
-29
@@ -32,32 +32,32 @@ const green = {
|
||||
|
||||
const lightSurface = {
|
||||
0: '#ffffff',
|
||||
50: '#f6f7fb',
|
||||
100: '#eef0f7',
|
||||
200: '#dde0ed',
|
||||
300: '#c1c6dd',
|
||||
400: '#8e95b5',
|
||||
500: '#666d92',
|
||||
600: '#4a5177',
|
||||
700: '#2d3358',
|
||||
800: '#1e2353',
|
||||
900: '#141840',
|
||||
950: '#0a0d3a',
|
||||
50: '#f8f7f4',
|
||||
100: '#f0eee9',
|
||||
200: '#e5e2db',
|
||||
300: '#cfcbc2',
|
||||
400: '#a3a09a',
|
||||
500: '#6e6b66',
|
||||
600: '#52504c',
|
||||
700: '#3b3a37',
|
||||
800: '#26262a',
|
||||
900: '#17181c',
|
||||
950: '#0e0f12',
|
||||
}
|
||||
|
||||
const darkSurface = {
|
||||
0: '#ffffff',
|
||||
50: '#f6f7fb',
|
||||
100: '#dde0ed',
|
||||
200: '#c1c6dd',
|
||||
300: '#8e95b5',
|
||||
400: '#666d92',
|
||||
500: '#4a5177',
|
||||
600: '#2d3358',
|
||||
700: '#242a4d',
|
||||
800: '#1e2353',
|
||||
900: '#141840',
|
||||
950: '#0a0d3a',
|
||||
50: '#f8f7f4',
|
||||
100: '#e5e2db',
|
||||
200: '#cfcbc2',
|
||||
300: '#a3a09a',
|
||||
400: '#6e6b66',
|
||||
500: '#52504c',
|
||||
600: '#2c2f36',
|
||||
700: '#26282e',
|
||||
800: '#1d1f24',
|
||||
900: '#17191e',
|
||||
950: '#0e0f12',
|
||||
}
|
||||
|
||||
export default definePreset(Aura, {
|
||||
@@ -113,34 +113,34 @@ export default definePreset(Aura, {
|
||||
background: '#ffffff',
|
||||
borderColor: '{surface.200}',
|
||||
color: '{text.color}',
|
||||
shadow: 'rgba(69, 42, 124, 0.14) 0px 12px 48px',
|
||||
shadow: 'rgba(24, 22, 18, 0.12) 0px 12px 48px',
|
||||
},
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
surface: darkSurface,
|
||||
text: {
|
||||
color: '#ffffff',
|
||||
color: '#f3f2ef',
|
||||
hoverColor: '#ffffff',
|
||||
mutedColor: '{surface.300}',
|
||||
hoverMutedColor: '{surface.200}',
|
||||
},
|
||||
primary: {
|
||||
color: '{primary.400}',
|
||||
contrastColor: '#0a0d3a',
|
||||
contrastColor: '#17191e',
|
||||
hoverColor: '{primary.300}',
|
||||
activeColor: '{primary.200}',
|
||||
},
|
||||
content: {
|
||||
background: '#141840',
|
||||
background: '#17191e',
|
||||
borderColor: '{surface.600}',
|
||||
},
|
||||
overlay: {
|
||||
modal: {
|
||||
background: '#141840',
|
||||
background: '#17191e',
|
||||
borderColor: '{surface.600}',
|
||||
color: '#ffffff',
|
||||
shadow: 'rgba(0, 0, 0, 0.5) 0px 12px 48px',
|
||||
color: '#f3f2ef',
|
||||
shadow: 'rgba(0, 0, 0, 0.55) 0px 12px 48px',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -13,13 +13,15 @@
|
||||
<!-- Left panel: login image -->
|
||||
<section class="brand-panel relative hidden overflow-hidden lg:flex lg:items-center lg:justify-center">
|
||||
<img src="/login.png" alt="Login" class="absolute inset-0 h-full w-full object-cover" />
|
||||
<h1 class="relative z-10 -mt-48 font-display text-5xl font-extrabold tracking-tight text-white drop-shadow-lg">MY WORKSPACE</h1>
|
||||
<h1 class="relative z-10 -mt-48 font-display text-5xl font-extrabold tracking-[-0.03em] text-white drop-shadow-lg">MY WORKSPACE</h1>
|
||||
</section>
|
||||
|
||||
<section class="flex items-center justify-center p-6" style="background: var(--canvas)">
|
||||
<section class="flex items-center justify-center p-6 sm:p-10" style="background: var(--canvas)">
|
||||
<div class="w-full max-w-[360px]">
|
||||
<div class="mb-7">
|
||||
<div class="mb-8">
|
||||
<span class="eyebrow">My Workspace</span>
|
||||
<h2 class="page-title mt-2">Sign in</h2>
|
||||
<p class="page-subtitle mt-2">Projects, documents and tasks in one place.</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
|
||||
@@ -7,9 +7,13 @@
|
||||
v-model:search-term="searchTerm"
|
||||
:creating-label="creatingLabel"
|
||||
:can-create="canCreate"
|
||||
:can-edit="canEdit"
|
||||
:can-delete="canDelete"
|
||||
:members="members"
|
||||
@select="selectDocument"
|
||||
@create="openCreate"
|
||||
@rename="openRowRename"
|
||||
@delete="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -118,6 +122,7 @@ const createPath = ref('/')
|
||||
const creating = ref(false)
|
||||
const renameDialog = ref(false)
|
||||
const renameTitle = ref('')
|
||||
const renameTarget = ref<{ id: string; title: string; type: DocumentType } | null>(null)
|
||||
const moveDialog = ref(false)
|
||||
const moveTarget = ref<string | null>(null)
|
||||
|
||||
@@ -294,16 +299,25 @@ async function onCreate() {
|
||||
|
||||
function openRename() {
|
||||
if (doc.value) {
|
||||
renameTarget.value = doc.value
|
||||
renameTitle.value = doc.value.title
|
||||
renameDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function openRowRename(node: DocumentNode) {
|
||||
renameTarget.value = node
|
||||
renameTitle.value = node.title
|
||||
renameDialog.value = true
|
||||
}
|
||||
|
||||
async function onRename() {
|
||||
if (!doc.value || !renameTitle.value) return
|
||||
const target = renameTarget.value
|
||||
if (!target || !renameTitle.value) return
|
||||
try {
|
||||
const updated = await updateDocument(doc.value.id, { title: renameTitle.value, content: doc.value.content })
|
||||
doc.value = updated
|
||||
const content = target.type === 'Document' ? (await getDocument(target.id)).content : undefined
|
||||
const updated = await updateDocument(target.id, { title: renameTitle.value, content })
|
||||
if (doc.value?.id === target.id) doc.value = updated
|
||||
renameDialog.value = false
|
||||
toast.add({ severity: 'success', summary: 'Renamed', life: 2000 })
|
||||
await loadTree()
|
||||
@@ -342,29 +356,31 @@ function findNode(nodes: DocumentNode[], id: string): DocumentNode | null {
|
||||
return null
|
||||
}
|
||||
|
||||
function deleteMessage(): string {
|
||||
if (!doc.value) return ''
|
||||
if (doc.value.type !== 'Folder') return `Delete "${doc.value.title}"?`
|
||||
const node = findNode(tree.value, doc.value.id)
|
||||
function deleteMessage(target: { id: string; title: string; type: DocumentType }): string {
|
||||
if (target.type !== 'Folder') return `Delete "${target.title}"?`
|
||||
const node = findNode(tree.value, target.id)
|
||||
const count = node ? countDescendants(node.children) : 0
|
||||
return count > 0
|
||||
? `Delete folder "${doc.value.title}" and the ${count} item${count === 1 ? '' : 's'} inside it? This cannot be undone.`
|
||||
: `Delete folder "${doc.value.title}"?`
|
||||
? `Delete folder "${target.title}" and the ${count} item${count === 1 ? '' : 's'} inside it? This cannot be undone.`
|
||||
: `Delete folder "${target.title}"?`
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
if (!doc.value) return
|
||||
function confirmDelete(target?: { id: string; title: string; type: DocumentType }) {
|
||||
const item = target ?? doc.value
|
||||
if (!item) return
|
||||
confirm.require({
|
||||
message: deleteMessage(),
|
||||
message: deleteMessage(item),
|
||||
header: 'Delete',
|
||||
acceptProps: { severity: 'danger' },
|
||||
rejectProps: { severity: 'secondary', outlined: true },
|
||||
accept: async () => {
|
||||
try {
|
||||
await deleteDocument(doc.value!.id)
|
||||
await deleteDocument(item.id)
|
||||
if (doc.value?.id === item.id) {
|
||||
doc.value = null
|
||||
selectedId.value = null
|
||||
viewerVisible.value = false
|
||||
}
|
||||
toast.add({ severity: 'success', summary: 'Deleted', life: 2000 })
|
||||
await loadTree()
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="mb-3 flex items-center justify-end">
|
||||
<div class="mb-3 flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<IconField class="sm:w-[280px]">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText v-model.trim="taskSearch" placeholder="Search tasks..." class="search-input w-full" />
|
||||
</IconField>
|
||||
<Select
|
||||
v-model="assigneeFilter"
|
||||
:options="assigneeOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Assignee"
|
||||
showClear
|
||||
class="w-[180px]"
|
||||
/>
|
||||
</div>
|
||||
<Button v-if="auth.can('tasks', 'create')" label="New Task" icon="pi pi-plus" @click="openCreate" />
|
||||
</div>
|
||||
|
||||
@@ -87,6 +102,26 @@ const members = ref<{ userId: string; displayName: string }[]>([])
|
||||
const draggingId = ref<string | null>(null)
|
||||
const dragOverStatus = ref<TaskStatus | null>(null)
|
||||
|
||||
const taskSearch = ref('')
|
||||
const assigneeFilter = ref<string | null>(null)
|
||||
|
||||
const assigneeOptions = computed(() =>
|
||||
members.value.map((m) => ({ label: m.displayName, value: m.userId })),
|
||||
)
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
const q = taskSearch.value.toLowerCase()
|
||||
return tasks.value.filter((t) => {
|
||||
if (assigneeFilter.value && t.assigneeId !== assigneeFilter.value) return false
|
||||
if (!q) return true
|
||||
return (
|
||||
t.title.toLowerCase().includes(q) ||
|
||||
(t.description ?? '').toLowerCase().includes(q) ||
|
||||
(t.assigneeName ?? '').toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const editingTask = ref<Task | null>(null)
|
||||
|
||||
@@ -103,7 +138,7 @@ const columns = [
|
||||
]
|
||||
|
||||
function tasksIn(status: TaskStatus) {
|
||||
return tasks.value.filter((t) => t.status === status)
|
||||
return filteredTasks.value.filter((t) => t.status === status)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
|
||||
Reference in New Issue
Block a user