Skip to content

Commit 152a2fe

Browse files
committed
Consolidate shared sidebar and UI components
1 parent 30e6814 commit 152a2fe

25 files changed

Lines changed: 1145 additions & 1813 deletions
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<script>
2+
import { buildProgressSegments, calculatePercentComplete, PROGRESS_CHART_RADIUS } from '../utils/progressChart.js';
3+
import { objectDisplayName } from '../utils/systemLabels.js';
4+
5+
let {
6+
progress,
7+
ariaLabel,
8+
completeLabel,
9+
noItemsLabel,
10+
summaryLabel,
11+
totalLabel,
12+
completedLabel,
13+
remainingLabel,
14+
statusLabel,
15+
noStatusDataLabel,
16+
} = $props();
17+
18+
const radius = PROGRESS_CHART_RADIUS;
19+
const segments = $derived(buildProgressSegments(progress.status_breakdown, progress.total_items));
20+
const percentComplete = $derived(
21+
calculatePercentComplete(progress.completed_items, progress.total_items, progress.percent_complete),
22+
);
23+
</script>
24+
25+
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
26+
<div class="rounded-xl border p-6 flex flex-col items-center" style="background-color: var(--ds-surface-raised); border-color: var(--ds-border);">
27+
<div class="relative">
28+
{#if progress.total_items > 0}
29+
<svg viewBox="0 0 140 140" class="w-36 h-36" role="img" aria-label={ariaLabel}>
30+
<circle cx="70" cy="70" r={radius} fill="transparent" stroke="var(--ds-border)" stroke-width="16" />
31+
{#each segments as segment (segment.category_name)}
32+
<circle
33+
cx="70"
34+
cy="70"
35+
r={radius}
36+
fill="transparent"
37+
stroke={segment.color}
38+
stroke-width="16"
39+
stroke-linecap="butt"
40+
stroke-dasharray={segment.dasharray}
41+
stroke-dashoffset={segment.offset}
42+
transform="rotate(-90 70 70)"
43+
/>
44+
{/each}
45+
<text class="text-2xl font-bold" x="70" y="68" text-anchor="middle" fill="var(--ds-text)">
46+
{percentComplete}%
47+
</text>
48+
<text class="text-xs uppercase" x="70" y="86" text-anchor="middle" fill="var(--ds-text-subtle)">
49+
{completeLabel}
50+
</text>
51+
</svg>
52+
{:else}
53+
<div class="w-36 h-36 rounded-full border-2 border-dashed flex items-center justify-center" style="border-color: var(--ds-border);">
54+
<span class="text-sm" style="color: var(--ds-text-subtlest);">{noItemsLabel}</span>
55+
</div>
56+
{/if}
57+
</div>
58+
</div>
59+
60+
<div class="rounded-xl border p-6" style="background-color: var(--ds-surface-raised); border-color: var(--ds-border);">
61+
<h3 class="text-sm font-medium mb-4" style="color: var(--ds-text-subtle);">{summaryLabel}</h3>
62+
<div class="space-y-3">
63+
<div class="flex justify-between items-center">
64+
<span style="color: var(--ds-text-subtle);">{totalLabel}</span>
65+
<span class="font-semibold" style="color: var(--ds-text);">{progress.total_items}</span>
66+
</div>
67+
<div class="flex justify-between items-center">
68+
<span style="color: var(--ds-text-subtle);">{completedLabel}</span>
69+
<span class="font-semibold" style="color: var(--ds-text-success);">{progress.completed_items}</span>
70+
</div>
71+
<div class="flex justify-between items-center">
72+
<span style="color: var(--ds-text-subtle);">{remainingLabel}</span>
73+
<span class="font-semibold" style="color: var(--ds-text);">{progress.total_items - progress.completed_items}</span>
74+
</div>
75+
</div>
76+
</div>
77+
78+
<div class="rounded-xl border p-6" style="background-color: var(--ds-surface-raised); border-color: var(--ds-border);">
79+
<h3 class="text-sm font-medium mb-4" style="color: var(--ds-text-subtle);">{statusLabel}</h3>
80+
<div class="space-y-2">
81+
{#if progress.status_breakdown?.length > 0}
82+
{#each progress.status_breakdown as breakdown}
83+
<div class="flex items-center justify-between">
84+
<div class="flex items-center gap-2">
85+
<div class="w-3 h-3 rounded-full" style="background-color: {breakdown.category_color || '#9ca3af'};"></div>
86+
<span class="text-sm" style="color: var(--ds-text);">
87+
{objectDisplayName({
88+
name: breakdown.category_name,
89+
builtin_key: breakdown.category_builtin_key,
90+
}, 'status_category')}
91+
</span>
92+
</div>
93+
<span class="text-sm font-medium" style="color: var(--ds-text-subtle);">{breakdown.item_count}</span>
94+
</div>
95+
{/each}
96+
{:else}
97+
<p class="text-sm" style="color: var(--ds-text-subtlest);">{noStatusDataLabel}</p>
98+
{/if}
99+
</div>
100+
</div>
101+
</div>

frontend/src/lib/features/assets/AssetBrowser.svelte

Lines changed: 14 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import Label from '../../components/Label.svelte';
1111
import Textarea from '../../components/Textarea.svelte';
1212
import PageHeader from '../../layout/PageHeader.svelte';
13+
import SidebarResizeHandle from '../../layout/SidebarResizeHandle.svelte';
1314
import Modal from '../../dialogs/Modal.svelte';
1415
import ModalHeader from '../../dialogs/ModalHeader.svelte';
1516
import EmptyState from '../../components/EmptyState.svelte';
@@ -22,12 +23,12 @@
2223
import AssetImportWizard from './import/AssetImportWizard.svelte';
2324
import AssetSubFilterBar from './AssetSubFilterBar.svelte';
2425
import CustomFieldRenderer from '../items/CustomFieldRenderer.svelte';
26+
import AssetDetailContent from './AssetDetailContent.svelte';
2527
import { retainValuesForType } from './assetFormValues.js';
2628
import { isBooleanCustomFieldType } from '../../utils/customFieldTypes.js';
2729
import { toHotkeyString } from '../../utils/keyboardShortcuts.js';
2830
import { formatDateSimple } from '../../utils/dateFormatter.js';
2931
import { fetchAssetCategories, fetchAssetStatuses, flattenCategories } from './shared/assetSetUtils.js';
30-
import { useEventListener } from 'runed';
3132
3233
// Props for detail view
3334
let { assetId = null } = $props();
@@ -71,29 +72,6 @@
7172
7273
// Asset detail panel resize state
7374
let assetPanelWidth = $state(320);
74-
let isResizingAssetPanel = $state(false);
75-
let assetResizeStartX = 0;
76-
let assetResizeStartWidth = 0;
77-
78-
function startAssetPanelResize(event) {
79-
event.preventDefault();
80-
assetResizeStartX = event.clientX;
81-
assetResizeStartWidth = assetPanelWidth;
82-
isResizingAssetPanel = true;
83-
}
84-
85-
function onAssetResizeMove(e) {
86-
const deltaX = assetResizeStartX - e.clientX;
87-
assetPanelWidth = Math.max(280, Math.min(600, assetResizeStartWidth + deltaX));
88-
}
89-
90-
function onAssetResizeEnd() {
91-
isResizingAssetPanel = false;
92-
}
93-
94-
useEventListener(() => (isResizingAssetPanel ? document : null), 'mousemove', onAssetResizeMove);
95-
useEventListener(() => (isResizingAssetPanel ? document : null), 'mouseup', onAssetResizeEnd);
96-
9775
// Filter state
9876
let selectedCategoryId = $state(null);
9977
let searchMode = $state('simple'); // 'simple' or 'ql'
@@ -607,91 +585,7 @@
607585
608586
<!-- Detail content -->
609587
<div class="rounded-lg p-6" style="background: var(--ds-surface-raised); border: 1px solid var(--ds-border);">
610-
{#if directAsset.description}
611-
<div class="mb-6">
612-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{t('common.description')}</h4>
613-
<p class="text-sm" style="color: var(--ds-text);">{directAsset.description}</p>
614-
</div>
615-
{/if}
616-
<div class="grid grid-cols-2 gap-4">
617-
{#if directAsset.asset_type_name}
618-
<div>
619-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{t('common.type')}</h4>
620-
<span class="inline-flex items-center gap-1" style="color: var(--ds-text);">
621-
<ColorDot color={directAsset.asset_type_color || '#6b7280'} />
622-
{directAsset.asset_type_name}
623-
</span>
624-
</div>
625-
{/if}
626-
{#if directAsset.category_name}
627-
<div>
628-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{t('common.category')}</h4>
629-
<span class="inline-flex items-center gap-1" style="color: var(--ds-text);">
630-
<IconFolder class="w-4 h-4 text-yellow-500" />
631-
{directAsset.category_name}
632-
</span>
633-
</div>
634-
{/if}
635-
{#if directAsset.status_name}
636-
<div>
637-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{t('common.status')}</h4>
638-
<span class="inline-flex items-center gap-1.5" style="color: var(--ds-text);">
639-
<span class="w-2 h-2 rounded-full" style="background-color: {directAsset.status_color || '#6b7280'};"></span>
640-
{directAsset.status_name}
641-
</span>
642-
</div>
643-
{/if}
644-
{#if directAsset.asset_tag}
645-
<div>
646-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">Asset Tag</h4>
647-
<span class="text-sm font-mono" style="color: var(--ds-text);">{directAsset.asset_tag}</span>
648-
</div>
649-
{/if}
650-
{#if directAsset.creator_name}
651-
<div>
652-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{t('common.createdBy')}</h4>
653-
<span class="text-sm" style="color: var(--ds-text);">{directAsset.creator_name}</span>
654-
</div>
655-
{/if}
656-
<div>
657-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{t('common.created')}</h4>
658-
<span class="text-sm" style="color: var(--ds-text);">{formatDateSimple(directAsset.created_at)}</span>
659-
</div>
660-
<div>
661-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{t('common.updated')}</h4>
662-
<span class="text-sm" style="color: var(--ds-text);">{formatDateSimple(directAsset.updated_at)}</span>
663-
</div>
664-
{#if directAsset.linked_item_count > 0}
665-
<div>
666-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">Linked Items</h4>
667-
<span class="text-sm" style="color: var(--ds-text);">{directAsset.linked_item_count}</span>
668-
</div>
669-
{/if}
670-
</div>
671-
{#if directAsset.custom_field_values && Object.keys(directAsset.custom_field_values).length > 0}
672-
<div class="border-t pt-4 mt-4" style="border-color: var(--ds-border);">
673-
<h4 class="text-xs font-medium uppercase mb-3" style="color: var(--ds-text-subtlest);">Custom Fields</h4>
674-
{#each Object.entries(directAsset.custom_field_values) as [fieldId, value]}
675-
{@const fieldDef = displayTypeFields.find(f => String(f.custom_field_id) === String(fieldId))}
676-
{#if fieldDef && value !== null && value !== ''}
677-
<div class="mb-3">
678-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{fieldDef.field_name}</h4>
679-
<CustomFieldRenderer
680-
field={{
681-
id: fieldDef.custom_field_id,
682-
name: fieldDef.field_name,
683-
field_type: fieldDef.field_type,
684-
options: fieldDef.options
685-
}}
686-
value={value}
687-
readonly={true}
688-
noPadding={true}
689-
/>
690-
</div>
691-
{/if}
692-
{/each}
693-
</div>
694-
{/if}
588+
<AssetDetailContent asset={directAsset} fieldDefinitions={displayTypeFields} layout="grid" />
695589
</div>
696590
{:else}
697591
<EmptyState
@@ -947,15 +841,16 @@
947841
<!-- Right sidebar: Asset detail (when selected) -->
948842
{#if selectedAsset}
949843
<div class="flex-shrink-0 flex flex-col relative" style="width: {assetPanelWidth}px; min-width: 280px; max-width: 600px; border-left: 1px solid var(--ds-border);">
950-
<!-- Resize handle -->
951-
<!-- svelte-ignore a11y_no_static_element_interactions -->
952-
<div
953-
class="absolute left-0 top-0 bottom-0 w-1 cursor-ew-resize transition-colors z-10"
954-
style="background-color: transparent;"
955-
onmouseenter={(e) => e.currentTarget.style.backgroundColor = '#3b82f6'}
956-
onmouseleave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
957-
onmousedown={startAssetPanelResize}
958-
></div>
844+
<SidebarResizeHandle
845+
width={assetPanelWidth}
846+
minWidth={280}
847+
maxWidth={600}
848+
defaultWidth={320}
849+
edge="left"
850+
label="Resize asset details"
851+
title="Drag to resize, double-click to reset"
852+
onresize={(width) => assetPanelWidth = width}
853+
/>
959854
<div class="p-4 flex items-center justify-between" style="border-bottom: 1px solid var(--ds-border);">
960855
<h2 class="font-semibold truncate" style="color: var(--ds-text);">{selectedAsset.title}</h2>
961856
<div class="flex items-center gap-1">
@@ -994,91 +889,7 @@
994889
</div>
995890
</div>
996891
<div class="flex-1 overflow-auto p-4">
997-
{#if selectedAsset.description}
998-
<div class="mb-4">
999-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{t('common.description')}</h4>
1000-
<p class="text-sm" style="color: var(--ds-text);">{selectedAsset.description}</p>
1001-
</div>
1002-
{/if}
1003-
<div class="space-y-3">
1004-
{#if selectedAsset.asset_type_name}
1005-
<div>
1006-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{t('common.type')}</h4>
1007-
<span class="inline-flex items-center gap-1" style="color: var(--ds-text);">
1008-
<ColorDot color={selectedAsset.asset_type_color || '#6b7280'} />
1009-
{selectedAsset.asset_type_name}
1010-
</span>
1011-
</div>
1012-
{/if}
1013-
{#if selectedAsset.category_name}
1014-
<div>
1015-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{t('common.category')}</h4>
1016-
<span class="inline-flex items-center gap-1" style="color: var(--ds-text);">
1017-
<IconFolder class="w-4 h-4 text-yellow-500" />
1018-
{selectedAsset.category_name}
1019-
</span>
1020-
</div>
1021-
{/if}
1022-
{#if selectedAsset.status_name}
1023-
<div>
1024-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{t('common.status')}</h4>
1025-
<span class="inline-flex items-center gap-1.5" style="color: var(--ds-text);">
1026-
<span class="w-2 h-2 rounded-full" style="background-color: {selectedAsset.status_color || '#6b7280'};"></span>
1027-
{selectedAsset.status_name}
1028-
</span>
1029-
</div>
1030-
{/if}
1031-
{#if selectedAsset.asset_tag}
1032-
<div>
1033-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">Asset Tag</h4>
1034-
<span class="text-sm font-mono" style="color: var(--ds-text);">{selectedAsset.asset_tag}</span>
1035-
</div>
1036-
{/if}
1037-
{#if selectedAsset.creator_name}
1038-
<div>
1039-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{t('common.createdBy')}</h4>
1040-
<span class="text-sm" style="color: var(--ds-text);">{selectedAsset.creator_name}</span>
1041-
</div>
1042-
{/if}
1043-
<div>
1044-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{t('common.created')}</h4>
1045-
<span class="text-sm" style="color: var(--ds-text);">{formatDateSimple(selectedAsset.created_at)}</span>
1046-
</div>
1047-
<div>
1048-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{t('common.updated')}</h4>
1049-
<span class="text-sm" style="color: var(--ds-text);">{formatDateSimple(selectedAsset.updated_at)}</span>
1050-
</div>
1051-
{#if selectedAsset.linked_item_count > 0}
1052-
<div>
1053-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">Linked Items</h4>
1054-
<span class="text-sm" style="color: var(--ds-text);">{selectedAsset.linked_item_count}</span>
1055-
</div>
1056-
{/if}
1057-
</div>
1058-
{#if selectedAsset.custom_field_values && Object.keys(selectedAsset.custom_field_values).length > 0}
1059-
<div class="border-t pt-4 mt-4" style="border-color: var(--ds-border);">
1060-
<h4 class="text-xs font-medium uppercase mb-3" style="color: var(--ds-text-subtlest);">Custom Fields</h4>
1061-
{#each Object.entries(selectedAsset.custom_field_values) as [fieldId, value]}
1062-
{@const fieldDef = displayTypeFields.find(f => String(f.custom_field_id) === String(fieldId))}
1063-
{#if fieldDef && value !== null && value !== ''}
1064-
<div class="mb-3">
1065-
<h4 class="text-xs font-medium uppercase mb-1" style="color: var(--ds-text-subtlest);">{fieldDef.field_name}</h4>
1066-
<CustomFieldRenderer
1067-
field={{
1068-
id: fieldDef.custom_field_id,
1069-
name: fieldDef.field_name,
1070-
field_type: fieldDef.field_type,
1071-
options: fieldDef.options
1072-
}}
1073-
value={value}
1074-
readonly={true}
1075-
noPadding={true}
1076-
/>
1077-
</div>
1078-
{/if}
1079-
{/each}
1080-
</div>
1081-
{/if}
892+
<AssetDetailContent asset={selectedAsset} fieldDefinitions={displayTypeFields} />
1082893
</div>
1083894
</div>
1084895
{/if}

0 commit comments

Comments
 (0)