824 lines
26 KiB
Vue
824 lines
26 KiB
Vue
<template>
|
|
<section class="asset-library-page">
|
|
<div class="page-header compact-header">
|
|
<div>
|
|
<span class="eyebrow">ASSET LIBRARY</span>
|
|
<h2>Deployment assets</h2>
|
|
<p>Store installers, support files, icons, configs, detection scripts, and package content for PSADT, Intune, and general PowerShell automation.</p>
|
|
</div>
|
|
<div class="page-actions">
|
|
<button class="ghost-button compact" type="button" @click="openFolderModal(null)">
|
|
<FolderPlus :size="16" />New folder
|
|
</button>
|
|
<button class="primary-action compact" type="button" @click="openUploadModal">
|
|
<Upload :size="16" />Upload asset
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="asset-stats-strip">
|
|
<article v-for="stat in assetStats" :key="stat.label" class="glass-panel asset-stat-card">
|
|
<span><component :is="stat.icon" :size="18" /></span>
|
|
<div>
|
|
<strong>{{ stat.value }}</strong>
|
|
<small>{{ stat.label }}</small>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
|
|
<div class="asset-library-shell">
|
|
<aside class="asset-folder-panel glass-panel">
|
|
<header>
|
|
<div>
|
|
<span class="eyebrow">FOLDERS</span>
|
|
<strong>Package tree</strong>
|
|
</div>
|
|
<button title="New folder" type="button" @click="openFolderModal(null)"><FolderPlus :size="15" /></button>
|
|
</header>
|
|
<div class="asset-folder-summary">
|
|
<span><strong>{{ assets.length }}</strong> assets</span>
|
|
<span><strong>{{ folders.length }}</strong> folders</span>
|
|
</div>
|
|
<AssetTree
|
|
:folders="folders"
|
|
:assets="assets"
|
|
:active-folder-id="activeFolderId"
|
|
:open-ids="openFolderIds"
|
|
@select="activeFolderId = $event"
|
|
@toggle="toggleFolder"
|
|
@create-child="openFolderModal(null, $event.id)"
|
|
@edit="openFolderModal"
|
|
@delete="deleteFolder"
|
|
/>
|
|
<div v-if="!folders.length" class="asset-tree-empty">
|
|
<FolderPlus :size="18" />
|
|
<strong>No folders yet</strong>
|
|
<button class="ghost-button compact" type="button" @click="openFolderModal(null)">Create folder</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="asset-table-region">
|
|
<ResourceTable
|
|
:columns="assetColumns"
|
|
:rows="pagedAssets"
|
|
:total="filteredAssets.length"
|
|
:search="assetFilters.search"
|
|
:page="assetFilters.page"
|
|
:page-count="assetPageCount"
|
|
:page-size="assetFilters.pageSize"
|
|
:sort="assetFilters.sort"
|
|
:direction="assetFilters.direction"
|
|
search-placeholder="Search assets, package paths, checksum, folders..."
|
|
item-label="assets"
|
|
empty-text="No assets match the current folder or filters."
|
|
@update:search="assetFilters.search = $event"
|
|
@update:page="assetFilters.page = $event"
|
|
@update:page-size="assetFilters.pageSize = $event"
|
|
@sort="setSort($event)"
|
|
>
|
|
<template #toolbar-actions>
|
|
<button class="ghost-button compact" type="button" @click="openFolderModal(null)">
|
|
<FolderPlus :size="14" />Folder
|
|
</button>
|
|
<button class="primary-action compact" type="button" @click="openUploadModal">
|
|
<Upload :size="14" />Upload
|
|
</button>
|
|
</template>
|
|
<template #cell-name="{ row }">
|
|
<button :class="['asset-row-title', { active: selectedAsset?.id === row.id }]" type="button" @click="selectAsset(row)">
|
|
<component :is="assetIcon(row)" :size="16" />
|
|
<span>{{ row.name }}</span>
|
|
</button>
|
|
</template>
|
|
<template #cell-kind="{ row }"><span :class="['status-pill', row.kind]">{{ row.kind }}</span></template>
|
|
<template #cell-sizeBytes="{ row }">{{ formatBytes(row.sizeBytes) }}</template>
|
|
<template #cell-folderName="{ row }">{{ row.folderName || 'Unfiled' }}</template>
|
|
<template #cell-packagePath="{ row }"><code>{{ row.packagePath || row.originalName }}</code></template>
|
|
<template #cell-actions="{ row }">
|
|
<div class="table-action-row">
|
|
<button class="ghost-button compact icon-button" type="button" aria-label="View asset" title="View asset" @click="selectAsset(row); viewAsset(row)"><Eye :size="15" /></button>
|
|
<button class="ghost-button compact icon-button" type="button" aria-label="Download asset" title="Download asset" @click="downloadAsset(row)"><Download :size="15" /></button>
|
|
<button class="ghost-button compact icon-button danger-text" type="button" aria-label="Delete asset" title="Delete asset" @click="deleteAsset(row)"><Trash2 :size="15" /></button>
|
|
</div>
|
|
</template>
|
|
<template #empty>
|
|
<section class="asset-empty-state">
|
|
<div class="asset-empty-orb"><PackageOpen :size="28" /></div>
|
|
<div>
|
|
<span class="eyebrow">PACKAGE CONTENT STARTS HERE</span>
|
|
<h3>{{ activeFolderId ? 'This folder is empty' : 'No deployment assets yet' }}</h3>
|
|
<p>Upload installers, scripts, detection files, icons, archives, configs, or PSADT support content. Assets can be linked into scripts, RunPlans, PSADT profiles, and Intune deployment plans.</p>
|
|
</div>
|
|
<div class="asset-empty-actions">
|
|
<button class="primary-action compact" type="button" @click="openUploadModal">
|
|
<Upload :size="15" />Upload asset
|
|
</button>
|
|
<button class="ghost-button compact" type="button" @click="openFolderModal(null)">
|
|
<FolderPlus :size="15" />New folder
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
</ResourceTable>
|
|
</main>
|
|
|
|
<AssetPreviewPanel
|
|
:asset="selectedAsset"
|
|
:preview-url="previewUrl"
|
|
:text-preview="textPreview"
|
|
@view="viewAsset"
|
|
@download="downloadAsset"
|
|
@insert-reference="insertReference"
|
|
@link="openLinkModal"
|
|
@refresh="$emit('refresh')"
|
|
/>
|
|
</div>
|
|
|
|
<BaseModal :open="folderModalOpen" :title="folderForm.id ? 'Edit folder' : 'New folder'" @close="folderModalOpen = false">
|
|
<form class="modal-form" @submit.prevent="saveFolder">
|
|
<label>
|
|
<span>Name</span>
|
|
<input v-model="folderForm.name" required placeholder="Installers" />
|
|
</label>
|
|
<label>
|
|
<span>Description</span>
|
|
<textarea v-model="folderForm.description" rows="3" placeholder="Optional package notes" />
|
|
</label>
|
|
<label>
|
|
<span>Parent folder</span>
|
|
<select v-model="folderForm.parentId">
|
|
<option :value="null">Library root</option>
|
|
<option v-for="folder in folders" :key="folder.id" :value="folder.id">{{ folder.name }}</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span>Visibility</span>
|
|
<select v-model="folderForm.visibility">
|
|
<option value="personal">Personal</option>
|
|
<option value="shared">Shared</option>
|
|
<option value="group">Group</option>
|
|
</select>
|
|
</label>
|
|
<label v-if="folderForm.visibility === 'group'">
|
|
<span>Group</span>
|
|
<select v-model="folderForm.groupId">
|
|
<option :value="null">Choose group</option>
|
|
<option v-for="group in groups" :key="group.id" :value="group.id">{{ group.name }}</option>
|
|
</select>
|
|
</label>
|
|
<button class="primary-action full-width" type="submit">Save folder</button>
|
|
</form>
|
|
</BaseModal>
|
|
|
|
<BaseModal :open="uploadModalOpen" title="Upload asset" @close="uploadModalOpen = false">
|
|
<form class="modal-form" @submit.prevent="submitUpload">
|
|
<label class="file-drop-zone">
|
|
<Upload :size="22" />
|
|
<span>{{ uploadFile?.name || 'Choose a package asset' }}</span>
|
|
<small>Installers, scripts, icons, configs, archives, or support files</small>
|
|
<input type="file" @change="handleFileSelect" />
|
|
</label>
|
|
<label>
|
|
<span>Name</span>
|
|
<input v-model="assetForm.name" required placeholder="Contoso VPN Installer" />
|
|
</label>
|
|
<label>
|
|
<span>Description</span>
|
|
<textarea v-model="assetForm.description" rows="3" placeholder="How this asset is used by scripts or packages" />
|
|
</label>
|
|
<label>
|
|
<span>Folder</span>
|
|
<select v-model="assetForm.folderId">
|
|
<option :value="null">Unfiled</option>
|
|
<option v-for="folder in folders" :key="folder.id" :value="folder.id">{{ folder.name }}</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span>Package path</span>
|
|
<input v-model="assetForm.packagePath" placeholder="Files/installer.exe or SupportFiles/config.json" />
|
|
</label>
|
|
<div class="form-grid two">
|
|
<label>
|
|
<span>Visibility</span>
|
|
<select v-model="assetForm.visibility">
|
|
<option value="personal">Personal</option>
|
|
<option value="shared">Shared</option>
|
|
<option value="group">Group</option>
|
|
</select>
|
|
</label>
|
|
<label v-if="assetForm.visibility === 'group'">
|
|
<span>Group</span>
|
|
<select v-model="assetForm.groupId">
|
|
<option :value="null">Choose group</option>
|
|
<option v-for="group in groups" :key="group.id" :value="group.id">{{ group.name }}</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
<button class="primary-action full-width" :disabled="!uploadFile" type="submit">Upload asset</button>
|
|
</form>
|
|
</BaseModal>
|
|
|
|
<BaseModal :open="linkModalOpen" title="Link asset to target" @close="linkModalOpen = false">
|
|
<form class="modal-form" @submit.prevent="saveLink">
|
|
<label>
|
|
<span>Target type</span>
|
|
<select v-model="linkForm.targetType">
|
|
<option value="script">Script</option>
|
|
<option value="runplan">RunPlan</option>
|
|
<option value="psadt_profile">PSADT profile</option>
|
|
<option value="intune_deployment">Intune deployment</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span>Target</span>
|
|
<select v-model="linkForm.targetId" required>
|
|
<option value="">Choose target</option>
|
|
<option v-for="target in linkTargets" :key="target.id" :value="target.id">{{ target.name }}</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span>Role</span>
|
|
<select v-model="linkForm.linkRole">
|
|
<option value="reference">Reference</option>
|
|
<option value="package-file">Package file</option>
|
|
<option value="installer">Installer</option>
|
|
<option value="support-file">Support file</option>
|
|
<option value="icon">Icon</option>
|
|
<option value="detection-script">Detection script</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span>Package path override</span>
|
|
<input v-model="linkForm.packagePath" placeholder="Files/setup.exe" />
|
|
</label>
|
|
<button class="primary-action full-width" type="submit">Create link</button>
|
|
</form>
|
|
</BaseModal>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, reactive, ref, watch } from 'vue';
|
|
import { Archive, Download, Eye, FileCog, FileText, FolderPlus, Image, Link2, Package, PackageOpen, ScrollText, Trash2, Upload } from '@lucide/vue';
|
|
import AssetPreviewPanel from '../components/assets/AssetPreviewPanel.vue';
|
|
import AssetTree from '../components/assets/AssetTree.vue';
|
|
import BaseModal from '../components/ui/BaseModal.vue';
|
|
import ResourceTable from '../components/ui/ResourceTable.vue';
|
|
|
|
const props = defineProps({
|
|
folders: { type: Array, default: () => [] },
|
|
assets: { type: Array, default: () => [] },
|
|
groups: { type: Array, default: () => [] },
|
|
scripts: { type: Array, default: () => [] },
|
|
runplans: { type: Array, default: () => [] },
|
|
psadtProfiles: { type: Array, default: () => [] },
|
|
intuneDeployments: { type: Array, default: () => [] },
|
|
token: { type: String, default: '' }
|
|
});
|
|
|
|
const emit = defineEmits(['refresh', 'save-folder', 'delete-folder', 'upload-asset', 'delete-asset', 'link-asset', 'insert-reference']);
|
|
|
|
const assetColumns = [
|
|
{ key: 'name', label: 'Asset' },
|
|
{ key: 'kind', label: 'Type' },
|
|
{ key: 'sizeBytes', label: 'Size' },
|
|
{ key: 'folderName', label: 'Folder' },
|
|
{ key: 'packagePath', label: 'Package path' },
|
|
{ key: 'actions', label: 'Actions', sortable: false, class: 'table-actions-cell' }
|
|
];
|
|
|
|
const activeFolderId = ref(null);
|
|
const selectedAssetId = ref('');
|
|
const openFolderIds = ref([]);
|
|
const folderModalOpen = ref(false);
|
|
const uploadModalOpen = ref(false);
|
|
const linkModalOpen = ref(false);
|
|
const uploadFile = ref(null);
|
|
const previewUrl = ref('');
|
|
const textPreview = ref('');
|
|
const assetFilters = reactive({ search: '', sort: 'name', direction: 'asc', page: 1, pageSize: 8 });
|
|
const folderForm = reactive({ id: null, parentId: null, name: '', description: '', visibility: 'personal', groupId: null });
|
|
const assetForm = reactive({ folderId: null, name: '', description: '', packagePath: '', visibility: 'personal', groupId: null });
|
|
const linkForm = reactive({ assetId: '', targetType: 'script', targetId: '', linkRole: 'reference', packagePath: '' });
|
|
|
|
const selectedAsset = computed(() => props.assets.find((asset) => asset.id === selectedAssetId.value) || props.assets[0] || null);
|
|
const assetStats = computed(() => [
|
|
{ label: 'Stored assets', value: props.assets.length, icon: PackageOpen },
|
|
{ label: 'Folder groups', value: props.folders.length, icon: FolderPlus },
|
|
{ label: 'Package paths', value: props.assets.filter((asset) => asset.packagePath).length, icon: Link2 },
|
|
{ label: 'Installers', value: props.assets.filter((asset) => asset.kind === 'installer').length, icon: Package }
|
|
]);
|
|
const filteredAssets = computed(() => {
|
|
const query = assetFilters.search.trim().toLowerCase();
|
|
return props.assets
|
|
.filter((asset) => !activeFolderId.value || asset.folderId === activeFolderId.value)
|
|
.filter((asset) => {
|
|
if (!query) return true;
|
|
return [asset.name, asset.originalName, asset.kind, asset.folderName, asset.packagePath, asset.checksum, asset.description]
|
|
.filter(Boolean)
|
|
.join(' ')
|
|
.toLowerCase()
|
|
.includes(query);
|
|
})
|
|
.sort((a, b) => compare(a, b, assetFilters.sort, assetFilters.direction));
|
|
});
|
|
const assetPageCount = computed(() => Math.max(1, Math.ceil(filteredAssets.value.length / assetFilters.pageSize)));
|
|
const pagedAssets = computed(() => {
|
|
const page = Math.min(assetFilters.page, assetPageCount.value);
|
|
return filteredAssets.value.slice((page - 1) * assetFilters.pageSize, page * assetFilters.pageSize);
|
|
});
|
|
const linkTargets = computed(() => ({
|
|
script: props.scripts,
|
|
runplan: props.runplans,
|
|
psadt_profile: props.psadtProfiles,
|
|
intune_deployment: props.intuneDeployments
|
|
}[linkForm.targetType] || []));
|
|
|
|
watch(() => props.assets, () => {
|
|
if (!selectedAssetId.value && props.assets.length) selectedAssetId.value = props.assets[0].id;
|
|
}, { immediate: true });
|
|
|
|
watch(() => [assetFilters.search, assetFilters.sort, assetFilters.direction, assetFilters.pageSize, activeFolderId.value], () => {
|
|
assetFilters.page = 1;
|
|
});
|
|
|
|
watch(() => linkForm.targetType, () => {
|
|
linkForm.targetId = '';
|
|
});
|
|
|
|
function compare(a, b, key, direction) {
|
|
const left = a?.[key] ?? '';
|
|
const right = b?.[key] ?? '';
|
|
const result = typeof left === 'number' || typeof right === 'number'
|
|
? Number(left || 0) - Number(right || 0)
|
|
: String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: 'base' });
|
|
return direction === 'desc' ? result * -1 : result;
|
|
}
|
|
|
|
function setSort(key) {
|
|
if (assetFilters.sort === key) assetFilters.direction = assetFilters.direction === 'asc' ? 'desc' : 'asc';
|
|
else {
|
|
assetFilters.sort = key;
|
|
assetFilters.direction = 'asc';
|
|
}
|
|
}
|
|
|
|
function toggleFolder(folderId) {
|
|
openFolderIds.value = openFolderIds.value.includes(folderId)
|
|
? openFolderIds.value.filter((id) => id !== folderId)
|
|
: [...openFolderIds.value, folderId];
|
|
}
|
|
|
|
function openFolderModal(folder = null, parentId = null) {
|
|
Object.assign(folderForm, {
|
|
id: folder?.id || null,
|
|
parentId: folder?.parentId ?? parentId,
|
|
name: folder?.name || '',
|
|
description: folder?.description || '',
|
|
visibility: folder?.visibility || 'personal',
|
|
groupId: folder?.groupId || null
|
|
});
|
|
folderModalOpen.value = true;
|
|
}
|
|
|
|
function saveFolder() {
|
|
emit('save-folder', { ...folderForm });
|
|
folderModalOpen.value = false;
|
|
}
|
|
|
|
function deleteFolder(folder) {
|
|
emit('delete-folder', folder);
|
|
}
|
|
|
|
function openUploadModal() {
|
|
uploadFile.value = null;
|
|
Object.assign(assetForm, {
|
|
folderId: activeFolderId.value,
|
|
name: '',
|
|
description: '',
|
|
packagePath: '',
|
|
visibility: 'personal',
|
|
groupId: null
|
|
});
|
|
uploadModalOpen.value = true;
|
|
}
|
|
|
|
function handleFileSelect(event) {
|
|
const [file] = event.target.files || [];
|
|
uploadFile.value = file || null;
|
|
if (!file) return;
|
|
assetForm.name = assetForm.name || file.name;
|
|
assetForm.packagePath = assetForm.packagePath || file.name;
|
|
}
|
|
|
|
async function submitUpload() {
|
|
if (!uploadFile.value) return;
|
|
emit('upload-asset', {
|
|
...assetForm,
|
|
file: uploadFile.value,
|
|
originalName: uploadFile.value.name,
|
|
mimeType: uploadFile.value.type || 'application/octet-stream'
|
|
});
|
|
uploadModalOpen.value = false;
|
|
}
|
|
|
|
function selectAsset(asset) {
|
|
selectedAssetId.value = asset.id;
|
|
clearPreview();
|
|
}
|
|
|
|
async function fetchAssetBlob(asset, route = 'view') {
|
|
const response = await fetch(route === 'download' ? asset.downloadUrl : asset.viewUrl, {
|
|
headers: props.token ? { Authorization: `Bearer ${props.token}` } : {}
|
|
});
|
|
if (!response.ok) throw new Error(`Asset request failed: ${response.status}`);
|
|
return response.blob();
|
|
}
|
|
|
|
async function viewAsset(asset) {
|
|
clearPreview();
|
|
const blob = await fetchAssetBlob(asset, 'view');
|
|
if (asset.mimeType?.startsWith('text/') || ['script', 'config', 'text'].includes(asset.kind)) {
|
|
textPreview.value = await blob.text();
|
|
return;
|
|
}
|
|
previewUrl.value = URL.createObjectURL(blob);
|
|
}
|
|
|
|
async function downloadAsset(asset) {
|
|
const blob = await fetchAssetBlob(asset, 'download');
|
|
const url = URL.createObjectURL(blob);
|
|
const anchor = document.createElement('a');
|
|
anchor.href = url;
|
|
anchor.download = asset.originalName || asset.name;
|
|
anchor.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function deleteAsset(asset) {
|
|
emit('delete-asset', asset);
|
|
if (selectedAssetId.value === asset.id) selectedAssetId.value = '';
|
|
}
|
|
|
|
function openLinkModal(asset) {
|
|
Object.assign(linkForm, {
|
|
assetId: asset.id,
|
|
targetType: 'script',
|
|
targetId: '',
|
|
linkRole: asset.kind === 'installer' ? 'installer' : 'reference',
|
|
packagePath: asset.packagePath || asset.originalName
|
|
});
|
|
linkModalOpen.value = true;
|
|
}
|
|
|
|
function saveLink() {
|
|
emit('link-asset', { ...linkForm });
|
|
linkModalOpen.value = false;
|
|
}
|
|
|
|
function insertReference(asset) {
|
|
emit('insert-reference', asset);
|
|
}
|
|
|
|
function clearPreview() {
|
|
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value);
|
|
previewUrl.value = '';
|
|
textPreview.value = '';
|
|
}
|
|
|
|
function assetIcon(asset) {
|
|
const map = {
|
|
archive: Archive,
|
|
config: FileCog,
|
|
document: FileText,
|
|
image: Image,
|
|
installer: Package,
|
|
script: ScrollText,
|
|
text: FileText
|
|
};
|
|
return map[asset.kind] || PackageOpen;
|
|
}
|
|
|
|
function formatBytes(bytes = 0) {
|
|
if (!bytes) return '0 B';
|
|
const units = ['B', 'KB', 'MB', 'GB'];
|
|
const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
return `${(bytes / (1024 ** index)).toFixed(index ? 1 : 0)} ${units[index]}`;
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.asset-library-page {
|
|
display: grid;
|
|
gap: 12px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.asset-library-page :deep(.page-header),
|
|
.asset-library-page .page-header {
|
|
min-height: 58px;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.asset-library-page .page-header p {
|
|
max-width: 760px;
|
|
}
|
|
|
|
.asset-stats-strip {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(140px, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.asset-stat-card {
|
|
min-height: 62px;
|
|
padding: 11px 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
border-radius: 15px;
|
|
background:
|
|
radial-gradient(circle at 12% 0, rgba(125, 232, 255, .16), transparent 38%),
|
|
linear-gradient(145deg, rgba(19, 27, 62, .88), rgba(6, 18, 45, .78));
|
|
}
|
|
|
|
.asset-stat-card > span {
|
|
width: 36px;
|
|
height: 36px;
|
|
flex: 0 0 36px;
|
|
display: grid;
|
|
place-items: center;
|
|
border: 1px solid rgba(125, 232, 255, .2);
|
|
border-radius: 12px;
|
|
color: #72e8ff;
|
|
background: rgba(95, 219, 255, .1);
|
|
}
|
|
|
|
.asset-stat-card strong {
|
|
display: block;
|
|
color: #fff;
|
|
font: 850 21px var(--display-font);
|
|
line-height: 1;
|
|
}
|
|
|
|
.asset-stat-card small {
|
|
display: block;
|
|
margin-top: 5px;
|
|
color: rgba(214, 226, 244, .64);
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: .08em;
|
|
}
|
|
|
|
.asset-library-shell {
|
|
display: grid;
|
|
grid-template-columns: minmax(220px, 260px) minmax(520px, 1fr) minmax(270px, 320px);
|
|
gap: 14px;
|
|
align-items: stretch;
|
|
min-width: 0;
|
|
}
|
|
|
|
.asset-folder-panel {
|
|
min-height: 430px;
|
|
padding: 16px;
|
|
border-radius: 18px;
|
|
background:
|
|
radial-gradient(circle at 0 0, rgba(125, 232, 255, .09), transparent 38%),
|
|
linear-gradient(145deg, rgba(31, 18, 58, .86), rgba(8, 15, 42, .8));
|
|
}
|
|
|
|
.asset-folder-panel header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.asset-folder-panel header button {
|
|
width: 34px;
|
|
height: 34px;
|
|
display: grid;
|
|
place-items: center;
|
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
border-radius: 10px;
|
|
color: rgba(255, 255, 255, 0.84);
|
|
background: rgba(255, 255, 255, 0.06);
|
|
}
|
|
|
|
.asset-folder-summary {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.asset-folder-summary span {
|
|
min-height: 50px;
|
|
padding: 10px;
|
|
border: 1px solid rgba(125, 232, 255, .11);
|
|
border-radius: 13px;
|
|
color: rgba(214, 226, 244, .58);
|
|
background: rgba(255, 255, 255, .035);
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: .07em;
|
|
}
|
|
|
|
.asset-folder-summary strong {
|
|
display: block;
|
|
color: #fff;
|
|
font-size: 17px;
|
|
line-height: 1.1;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.asset-tree-empty {
|
|
min-height: 170px;
|
|
margin-top: 12px;
|
|
padding: 18px;
|
|
display: grid;
|
|
place-items: center;
|
|
align-content: center;
|
|
gap: 10px;
|
|
border: 1px dashed rgba(125, 232, 255, .18);
|
|
border-radius: 16px;
|
|
color: rgba(214, 226, 244, .66);
|
|
background: rgba(4, 12, 31, .3);
|
|
text-align: center;
|
|
}
|
|
|
|
.asset-tree-empty svg {
|
|
color: #79eaff;
|
|
}
|
|
|
|
.asset-tree-empty strong {
|
|
color: #fff;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.asset-table-region {
|
|
min-width: 0;
|
|
}
|
|
|
|
.asset-table-region :deep(.resource-table-panel) {
|
|
padding: 14px;
|
|
border-radius: 18px;
|
|
min-height: 430px;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.asset-table-region :deep(.resource-toolbar) {
|
|
grid-template-columns: minmax(260px, 1fr) auto auto auto auto;
|
|
}
|
|
|
|
.asset-table-region :deep(.toolbar-count) {
|
|
margin-left: 0;
|
|
}
|
|
|
|
.asset-table-region :deep(.resource-empty-shell) {
|
|
flex: 1;
|
|
min-height: 270px;
|
|
}
|
|
|
|
.asset-table-region :deep(.pagination) {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.asset-row-title {
|
|
width: 100%;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
border: 0;
|
|
color: rgba(255, 255, 255, 0.86);
|
|
background: transparent;
|
|
text-align: left;
|
|
font-weight: 750;
|
|
}
|
|
|
|
.asset-row-title span {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.asset-row-title.active {
|
|
color: #8beaff;
|
|
}
|
|
|
|
.table-action-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.asset-empty-state {
|
|
width: min(620px, 100%);
|
|
display: grid;
|
|
justify-items: center;
|
|
gap: 14px;
|
|
padding: 28px 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.asset-empty-orb {
|
|
width: 66px;
|
|
height: 66px;
|
|
display: grid;
|
|
place-items: center;
|
|
border: 1px solid rgba(125, 232, 255, .22);
|
|
border-radius: 22px;
|
|
color: #79eaff;
|
|
background:
|
|
radial-gradient(circle at 28% 18%, rgba(255, 255, 255, .2), transparent 24%),
|
|
linear-gradient(135deg, rgba(88, 221, 255, .22), rgba(154, 134, 255, .18));
|
|
box-shadow: 0 18px 42px rgba(88, 221, 255, .11);
|
|
}
|
|
|
|
.asset-empty-state h3 {
|
|
margin: 5px 0 8px;
|
|
color: #fff;
|
|
font: 800 22px var(--display-font);
|
|
letter-spacing: -.03em;
|
|
}
|
|
|
|
.asset-empty-state p {
|
|
max-width: 560px;
|
|
margin: 0;
|
|
color: rgba(214, 226, 244, .72);
|
|
font-size: 12px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.asset-empty-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
gap: 9px;
|
|
}
|
|
|
|
.file-drop-zone {
|
|
min-height: 128px;
|
|
display: grid;
|
|
place-items: center;
|
|
gap: 6px;
|
|
padding: 18px;
|
|
border: 1px dashed rgba(125, 232, 255, 0.35);
|
|
border-radius: 16px;
|
|
color: rgba(255, 255, 255, 0.82);
|
|
background: rgba(95, 219, 255, 0.07);
|
|
text-align: center;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.file-drop-zone small {
|
|
color: rgba(255, 255, 255, 0.48);
|
|
}
|
|
|
|
.file-drop-zone input {
|
|
display: none;
|
|
}
|
|
|
|
.form-grid.two {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 12px;
|
|
}
|
|
|
|
@media (max-width: 1420px) {
|
|
.asset-stats-strip {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
|
|
.asset-library-shell {
|
|
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
|
|
}
|
|
|
|
.asset-library-shell :deep(.asset-preview) {
|
|
grid-column: 1 / -1;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 860px) {
|
|
.asset-stats-strip {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.asset-library-shell {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.asset-folder-panel {
|
|
min-height: auto;
|
|
}
|
|
|
|
.asset-table-region :deep(.resource-toolbar) {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|