This commit is contained in:
metacryst
2026-04-28 20:05:00 -05:00
commit 0d6c7683ff
123 changed files with 20922 additions and 0 deletions

View File

@@ -0,0 +1,408 @@
class DesktopFilesGrid extends Shadow {
constructor(files, groups, viewMode, onFileClick) {
super()
this.files = files
this.groups = groups
this.viewMode = viewMode
this.onFileClick = onFileClick
}
render() {
if (this.files.length === 0) {
VStack(() => {
p("🗂")
.margin(0)
.fontSize(2.5, em)
.opacity(0.18)
p("No files here")
.margin(0)
.marginTop(0.65, em)
.fontSize(0.9, em)
.color("var(--headertext)")
.opacity(0.32)
})
.flex(1)
.justifyContent("center")
.alignItems("center")
.height(100, pct)
return;
}
if (this.viewMode === "list") {
this.renderListView()
} else {
this.renderGridView()
}
}
renderListView() {
VStack(() => {
// Header row
HStack(() => {
p("Name") .styles(this.colHeader).flex(1)
p("Shared with").styles(this.colHeader).width(160, px).flexShrink(0)
p("Modified") .styles(this.colHeader).width(130, px).flexShrink(0)
p("Size") .styles(this.colHeader).width(80, px).flexShrink(0)
p("Group") .styles(this.colHeader).width(120, px).flexShrink(0)
})
.paddingHorizontal(1.25, em)
.paddingVertical(0.45, em)
.borderBottom("1px solid var(--divider)")
.flexShrink(0)
VStack(() => {
this.files.forEach((file, i) => this.renderListRow(file, i))
})
.overflowY("auto")
.flex(1)
})
.width(100, pct)
.height(100, pct)
.overflow("hidden")
}
renderListRow(file, index) {
const self = this
HStack(() => {
// Icon + name
HStack(() => {
p(this.fileIcon(file))
.margin(0)
.fontSize(1.15, em)
.lineHeight("1")
.flexShrink(0)
VStack(() => {
p(file.name)
.margin(0)
.fontSize(0.88, em)
.fontWeight("500")
.color("var(--headertext)")
.overflow("hidden")
.whiteSpace("nowrap")
.textOverflow("ellipsis")
})
.flex(1)
.minWidth(0)
})
.gap(0.6, em)
.flex(1)
.minWidth(0)
.alignItems("center")
// Active editors badge
HStack(() => {
if (file.activeEditors && file.activeEditors.length > 0) {
this.renderActiveEditors(file.activeEditors, "list")
} else if (file.sharedWith && file.sharedWith.length > 0) {
this.renderSharedAvatars(file.sharedWith)
} else {
p("—").margin(0).fontSize(0.8, em).color("var(--headertext)").opacity(0.2)
}
})
.width(160, px)
.flexShrink(0)
.alignItems("center")
.gap(0.3, em)
// Modified
p(this.relativeDate(file.modifiedAt))
.margin(0)
.fontSize(0.78, em)
.color("var(--headertext)")
.opacity(0.45)
.width(130, px)
.flexShrink(0)
.overflow("hidden")
.whiteSpace("nowrap")
// Size
p(file.size || "—")
.margin(0)
.fontSize(0.78, em)
.color("var(--headertext)")
.opacity(0.45)
.width(80, px)
.flexShrink(0)
// Group tag
HStack(() => {
const group = this.groups.find(g => g.id === file.groupId);
if (group) {
VStack(() => {})
.width(0.5, em).height(0.5, em)
.borderRadius(50, pct)
.background(group.color)
.flexShrink(0)
p(group.name)
.margin(0)
.fontSize(0.72, em)
.color("var(--headertext)")
.opacity(0.55)
.overflow("hidden")
.whiteSpace("nowrap")
.textOverflow("ellipsis")
}
})
.gap(0.38, em)
.width(120, px)
.flexShrink(0)
.alignItems("center")
.minWidth(0)
})
.paddingHorizontal(1.25, em)
.paddingVertical(0.62, em)
.borderBottom("1px solid var(--divider)")
.cursor("pointer")
.alignItems("center")
.width(100, pct)
.boxSizing("border-box")
.onClick(function(done){ if(done){ self.onFileClick(file) } })
}
renderGridView() {
VStack(() => {
ZStack(() => {
this.files.forEach(file => this.renderGridCard(file))
})
.display("grid")
.attr({ style: "display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1em;" })
})
.padding(1.5, em)
.overflowY("auto")
.height(100, pct)
.width(100, pct)
.boxSizing("border-box")
}
renderGridCard(file) {
VStack(() => {
// Thumbnail area
ZStack(() => {
// File type large icon
VStack(() => {
p(this.fileIcon(file))
.margin(0)
.fontSize(2.4, em)
.lineHeight("1")
})
.width(100, pct)
.height(100, pct)
.justifyContent("center")
.alignItems("center")
.background(this.fileTint(file))
// Active editor badge (top-right)
if (file.activeEditors && file.activeEditors.length > 0) {
VStack(() => {
this.renderActiveEditors(file.activeEditors, "grid")
})
.position("absolute")
.top(0.45, em)
.right(0.45, em)
}
// Group color stripe (bottom of thumbnail)
VStack(() => {
const group = this.groups.find(g => g.id === file.groupId);
if (group) {
VStack(() => {})
.width(100, pct)
.height(3, px)
.background(group.color)
}
})
.position("absolute")
.bottom(0).left(0).right(0)
})
.position("relative")
.width(100, pct)
.height(6.5, em)
.borderRadius("0.5em 0.5em 0 0")
.overflow("hidden")
.flexShrink(0)
// Info area
VStack(() => {
p(file.name)
.margin(0)
.fontSize(0.8, em)
.fontWeight("600")
.color("var(--headertext)")
.overflow("hidden")
.whiteSpace("nowrap")
.textOverflow("ellipsis")
.width(100, pct)
p(this.relativeDate(file.modifiedAt))
.margin(0)
.marginTop(0.18, em)
.fontSize(0.68, em)
.color("var(--headertext)")
.opacity(0.38)
})
.padding(0.65, em)
.background("var(--darkaccent)")
.borderRadius("0 0 0.5em 0.5em")
.width(100, pct)
.boxSizing("border-box")
})
.border("1px solid var(--divider)")
.borderRadius(0.5, em)
.overflow("hidden")
.cursor("pointer")
.onClick((done) => {if(!done) return; this.onFileClick(file)})
}
renderActiveEditors(editors, context) {
const isGrid = context === "grid";
HStack(() => {
// Pulsing green dot
ZStack(() => {
VStack(() => {})
.width(isGrid ? 0.55 : 0.48, em)
.height(isGrid ? 0.55 : 0.48, em)
.borderRadius(50, pct)
.background("#22c55e")
.opacity(0.4)
.attr({ style: "animation: pulse-ring 1.5s ease-out infinite;" })
VStack(() => {})
.width(isGrid ? 0.4 : 0.35, em)
.height(isGrid ? 0.4 : 0.35, em)
.borderRadius(50, pct)
.background("#22c55e")
.position("absolute")
})
.position("relative")
.width(isGrid ? 0.55 : 0.48, em)
.height(isGrid ? 0.55 : 0.48, em)
.flexShrink(0)
// Editor avatar stack
HStack(() => {
editors.slice(0, 3).forEach((editor, i) => {
VStack(() => {
p(editor[0].toUpperCase())
.margin(0)
.fontSize(isGrid ? 0.5 : 0.45, em)
.fontWeight("700")
.color("white")
.lineHeight("1")
})
.width(isGrid ? 1.4 : 1.25, em)
.height(isGrid ? 1.4 : 1.25, em)
.borderRadius(50, pct)
.background(this.avatarColor(editor))
.justifyContent("center")
.alignItems("center")
.boxSizing("border-box")
.flexShrink(0)
.marginLeft(i > 0 ? -0.4 : 0, em)
})
})
.alignItems("center")
if (!isGrid) {
p(editors.length === 1
? `${editors[0]} is editing`
: `${editors.length} editing`)
.margin(0)
.fontSize(0.72, em)
.color("#22c55e")
.fontWeight("500")
.whiteSpace("nowrap")
}
})
.gap(isGrid ? 0.25 : 0.45, em)
.alignItems("center")
.paddingHorizontal(isGrid ? 0.4 : 0)
.paddingVertical(isGrid ? 0.22 : 0)
.background(isGrid ? "rgba(0,0,0,0.55)" : "transparent")
.borderRadius(isGrid ? 100 : 0, px)
}
renderSharedAvatars(people) {
HStack(() => {
people.slice(0, 4).forEach((name, i) => {
VStack(() => {
p(name[0].toUpperCase())
.margin(0)
.fontSize(0.45, em)
.fontWeight("700")
.color("white")
.lineHeight("1")
})
.width(1.25, em)
.height(1.25, em)
.borderRadius(50, pct)
.background(this.avatarColor(name))
.justifyContent("center")
.alignItems("center")
.boxSizing("border-box")
.flexShrink(0)
.marginLeft(i > 0 ? -0.38 : 0, em)
})
})
.alignItems("center")
}
colHeader(el) {
return el
.margin(0)
.fontSize(0.72, em)
.fontWeight("600")
.letterSpacing("0.03em")
.color("var(--headertext)")
.opacity(0.4)
}
fileIcon(file) {
const ext = file.name.split(".").pop().toLowerCase();
const map = {
pdf: "📄", doc: "📝", docx: "📝", txt: "📃", md: "📃",
xls: "📊", xlsx: "📊", csv: "📊",
ppt: "📋", pptx: "📋",
jpg: "🖼", jpeg: "🖼", png: "🖼", gif: "🖼", svg: "🖼", webp: "🖼",
mp4: "🎬", mov: "🎬", avi: "🎬",
mp3: "🎵", wav: "🎵",
zip: "🗜", rar: "🗜",
js: "⚙️", ts: "⚙️", py: "⚙️", json: "⚙️",
folder: "📁",
};
if (file.type === "folder") return "📁";
return map[ext] || "📄";
}
fileTint(file) {
const ext = file.name.split(".").pop().toLowerCase();
const tints = {
pdf: "rgba(239,68,68,0.08)", doc: "rgba(59,130,246,0.08)", docx: "rgba(59,130,246,0.08)",
xls: "rgba(16,185,129,0.08)", xlsx: "rgba(16,185,129,0.08)", csv: "rgba(16,185,129,0.08)",
jpg: "rgba(245,158,11,0.08)", jpeg: "rgba(245,158,11,0.08)", png: "rgba(245,158,11,0.08)",
mp4: "rgba(139,92,246,0.08)", mov: "rgba(139,92,246,0.08)",
zip: "rgba(107,114,128,0.08)",
};
if (file.type === "folder") return "rgba(59,130,246,0.06)";
return tints[ext] || "var(--darkaccent)";
}
relativeDate(date) {
const d = new Date(date);
const days = Math.floor((Date.now() - d) / 86400000);
if (days === 0) return "Today";
if (days === 1) return "Yesterday";
if (days < 7) return `${days} days ago`;
return d.toLocaleDateString([], { month: "short", day: "numeric", year: "numeric" });
}
avatarColor(name) {
const colors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", "#ec4899", "#06b6d4", "#84cc16"];
let hash = 0;
for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
return colors[Math.abs(hash) % colors.length];
}
}
register(DesktopFilesGrid)

View File

@@ -0,0 +1,175 @@
class DesktopFilesSidebar extends Shadow {
constructor(groups, activeGroupIds, onToggleGroup, locations, activeLocation, onSelectLocation) {
super()
this.groups = groups
this.activeGroupIds = activeGroupIds
this.onToggleGroup = onToggleGroup
this.locations = locations
this.activeLocation = activeLocation
this.onSelectLocation = onSelectLocation
}
render() {
const self = this
VStack(() => {
// ── Locations ─────────────────────────────────────────────
VStack(() => {
this.sectionLabel("BROWSE")
this.locations.forEach(loc => {
const isActive = this.activeLocation === loc.id;
HStack(() => {
p(loc.icon)
.margin(0)
.fontSize(0.88, em)
.lineHeight("1")
.flexShrink(0)
p(loc.label)
.margin(0)
.fontSize(0.88, em)
.fontWeight(isActive ? "600" : "400")
.color("var(--headertext)")
.opacity(isActive ? 1 : 0.75)
})
.gap(0.6, em)
.paddingHorizontal(0.85, em)
.paddingVertical(0.42, em)
.marginHorizontal(0.4, em)
.borderRadius(0.45, em)
.background(isActive ? "var(--app)" : "transparent")
.cursor("pointer")
.alignItems("center")
.width("calc(100% - 0.8em)")
.boxSizing("border-box")
.onClick(function(done){ if(done){ self.onSelectLocation(loc.id) } })
})
})
.paddingTop(0.9, em)
// ── Divider ───────────────────────────────────────────────
VStack(() => {})
.height(1, px)
.background("var(--divider)")
.marginVertical(0.65, em)
.marginHorizontal(1, em)
// ── Permission groups ─────────────────────────────────────
VStack(() => {
this.sectionLabel("PERMISSION GROUPS")
this.groups.forEach(group => {
const isOn = this.activeGroupIds.has(group.id);
HStack(() => {
// Colored dot / checkbox area
HStack(() => {
VStack(() => {
if (isOn) {
p("✓")
.margin(0)
.fontSize(0.6, em)
.fontWeight("800")
.color("white")
.lineHeight("1")
}
})
.width(0.95, em)
.height(0.95, em)
.borderRadius(0.22, em)
.background(isOn ? group.color : "transparent")
.border(`1.5px solid ${group.color}`)
.justifyContent("center")
.alignItems("center")
.boxSizing("border-box")
.flexShrink(0)
})
.cursor("pointer")
p(group.name)
.margin(0)
.fontSize(0.85, em)
.fontWeight("400")
.color("var(--headertext)")
.opacity(isOn ? 1 : 0.45)
.flex(1)
.minWidth(0)
.overflow("hidden")
.whiteSpace("nowrap")
.textOverflow("ellipsis")
p(`${group.fileCount}`)
.margin(0)
.fontSize(0.68, em)
.color("var(--headertext)")
.opacity(0.3)
.flexShrink(0)
})
.gap(0.6, em)
.paddingHorizontal(0.85, em)
.paddingVertical(0.38, em)
.marginHorizontal(0.4, em)
.borderRadius(0.45, em)
.cursor("pointer")
.alignItems("center")
.width("calc(100% - 0.8em)")
.boxSizing("border-box")
.onClick(function(done){ if(done){ self.onToggleGroup(group.id) } })
})
})
VStack(() => {}).flex(1)
// ── Storage usage ─────────────────────────────────────────
VStack(() => {
HStack(() => {
p("Storage")
.margin(0)
.fontSize(0.75, em)
.color("var(--headertext)")
.opacity(0.45)
.flex(1)
p("4.2 GB / 10 GB")
.margin(0)
.fontSize(0.7, em)
.color("var(--headertext)")
.opacity(0.3)
})
.alignItems("center")
.marginBottom(0.45, em)
// Bar
VStack(() => {
VStack(() => {})
.width(42, pct)
.height(100, pct)
.background("var(--quillred)")
.borderRadius(100, px)
})
.width(100, pct)
.height(4, px)
.background("var(--darkaccent)")
.borderRadius(100, px)
.overflow("hidden")
})
.paddingHorizontal(1.1, em)
.paddingBottom(1.1, em)
.flexShrink(0)
})
.height(100, pct)
.width(100, pct)
.boxSizing("border-box")
.overflowY("auto")
}
sectionLabel(text) {
p(text)
.margin(0)
.marginBottom(0.25, em)
.paddingHorizontal(1.1, em)
.fontSize(0.62, em)
.fontWeight("700")
.letterSpacing("0.07em")
.color("var(--headertext)")
.opacity(0.35)
}
}
register(DesktopFilesSidebar)

View File

@@ -0,0 +1,98 @@
class DesktopFilesToolbar extends Shadow {
constructor(title, viewMode, searchText, onViewChange, onSearch, onUpload) {
super()
this.title = title
this.viewMode = viewMode
this.searchText = searchText
this.onViewChange = onViewChange
this.onSearch = onSearch
this.onUpload = onUpload
}
render() {
const self = this
HStack(() => {
// Title
p(this.title)
.margin(0)
.fontSize(1.05, em)
.fontWeight("700")
.color("var(--headertext)")
.flex(1)
.minWidth(0)
// Search
HStack(() => {
p("🔍")
.margin(0)
.fontSize(0.8, em)
.opacity(0.38)
.flexShrink(0)
input("", "200px")
.attr({ type: "text", placeholder: "Search files…", value: this.searchText })
.border("none")
.outline("none")
.background("transparent")
.color("var(--headertext)")
.fontSize(0.85, em)
.onInput((e) => this.onSearch(e.target.value))
})
.gap(0.5, em)
.paddingHorizontal(0.8, em)
.paddingVertical(0.52, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.5, em)
.alignItems("center")
// View toggle
HStack(() => {
this.viewBtn("list", "☰")
this.viewBtn("grid", "⊞")
})
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.5, em)
.overflow("hidden")
// Upload
button("+ Upload")
.paddingHorizontal(1, em)
.paddingVertical(0.52, em)
.background("var(--quillred)")
.color("white")
.border("none")
.borderRadius(0.5, em)
.fontSize(0.85, em)
.fontWeight("600")
.cursor("pointer")
.flexShrink(0)
.onClick(function(done){ if(done){ self.onUpload() } })
})
.gap(0.65, em)
.paddingHorizontal(1.5, em)
.paddingVertical(0.85, em)
.borderBottom("1px solid var(--divider)")
.alignItems("center")
.width(100, pct)
.boxSizing("border-box")
.flexShrink(0)
}
viewBtn(mode, icon) {
const self = this
const isActive = this.viewMode === mode;
button(icon)
.paddingHorizontal(0.65, em)
.paddingVertical(0.45, em)
.background(isActive ? "var(--app)" : "transparent")
.color("var(--headertext)")
.border("none")
.cursor("pointer")
.fontSize(0.95, em)
.opacity(isActive ? 1 : 0.45)
.onClick(function(done){ if(done){ self.onViewChange(mode) } })
}
}
register(DesktopFilesToolbar)

159
files/desktop/files.js Normal file
View File

@@ -0,0 +1,159 @@
import "./DesktopFilesSidebar.js"
import "./DesktopFilesToolbar.js"
import "./DesktopFilesGrid.js"
css(`
files- {
font-family: 'Arial';
scrollbar-width: none;
-ms-overflow-style: none;
}
@keyframes pulse-ring {
0% { transform: scale(1); opacity: 0.5; }
100% { transform: scale(2.2); opacity: 0; }
}
`)
class Files extends Shadow {
constructor() {
super()
this.viewMode = "list"
this.searchText = ""
this.activeLocation = "all"
this.groups = [
{ id: 1, name: "All Members", color: "#3b82f6", fileCount: 24 },
{ id: 2, name: "Admins", color: "#ef4444", fileCount: 8 },
{ id: 3, name: "Editors", color: "#10b981", fileCount: 15 },
{ id: 4, name: "Viewers", color: "#f59e0b", fileCount: 19 },
{ id: 5, name: "External", color: "#8b5cf6", fileCount: 4 },
]
this.activeGroupIds = new Set(this.groups.map(g => g.id))
this.locations = [
{ id: "all", label: "All Files", icon: "🗂" },
{ id: "mine", label: "My Files", icon: "👤" },
{ id: "shared", label: "Shared with Me", icon: "👥" },
{ id: "recent", label: "Recent", icon: "🕐" },
{ id: "starred", label: "Starred", icon: "⭐" },
{ id: "trash", label: "Trash", icon: "🗑" },
]
const ago = (d) => new Date(Date.now() - d * 86400000);
this.files = [
// Folders
{ id: 1, type: "folder", name: "Design Assets", groupId: 3, modifiedAt: ago(0), size: "—", sharedWith: ["Sarah McIntyre", "Jordan Kim", "Marcus Webb"], activeEditors: [], ownerId: 1, starred: true },
{ id: 2, type: "folder", name: "Engineering Docs", groupId: 2, modifiedAt: ago(1), size: "—", sharedWith: ["Marcus Webb"], activeEditors: [], ownerId: 1, starred: false },
// PDFs
{ id: 3, type: "file", name: "Q2 Board Presentation.pdf", groupId: 2, modifiedAt: ago(0), size: "4.2 MB", sharedWith: ["Priya Anand", "Marcus Webb"], activeEditors: ["Sarah McIntyre"], ownerId: 1, starred: true },
{ id: 4, type: "file", name: "Brand Guidelines.pdf", groupId: 1, modifiedAt: ago(3), size: "12.8 MB", sharedWith: ["Sarah McIntyre", "Jordan Kim"], activeEditors: [], ownerId: 2, starred: false },
{ id: 5, type: "file", name: "Legal NDA Template.pdf", groupId: 2, modifiedAt: ago(14), size: "0.9 MB", sharedWith: ["Priya Anand"], activeEditors: [], ownerId: 1, starred: false },
// Docs
{ id: 6, type: "file", name: "Product Roadmap.docx", groupId: 3, modifiedAt: ago(0), size: "1.1 MB", sharedWith: ["Sarah McIntyre", "Priya Anand", "Jordan Kim"], activeEditors: ["Marcus Webb", "Priya Anand"], ownerId: 1, starred: true },
{ id: 7, type: "file", name: "Sprint 22 Notes.md", groupId: 3, modifiedAt: ago(1), size: "18 KB", sharedWith: ["Marcus Webb"], activeEditors: [], ownerId: 3, starred: false },
{ id: 8, type: "file", name: "Onboarding Checklist.docx", groupId: 1, modifiedAt: ago(5), size: "245 KB", sharedWith: ["Sarah McIntyre", "Priya Anand"], activeEditors: [], ownerId: 1, starred: false },
// Spreadsheets
{ id: 9, type: "file", name: "Budget 2026.xlsx", groupId: 2, modifiedAt: ago(2), size: "3.3 MB", sharedWith: ["Priya Anand"], activeEditors: ["Jordan Kim"], ownerId: 1, starred: true },
{ id: 10, type: "file", name: "Member Directory.csv", groupId: 4, modifiedAt: ago(7), size: "88 KB", sharedWith: ["Priya Anand", "Marcus Webb", "Sarah McIntyre"], activeEditors: [], ownerId: 2, starred: false },
{ id: 11, type: "file", name: "Event Attendance.xlsx", groupId: 3, modifiedAt: ago(10), size: "1.4 MB", sharedWith: [], activeEditors: [], ownerId: 3, starred: false },
// Images
{ id: 12, type: "file", name: "Logo Final.png", groupId: 1, modifiedAt: ago(21), size: "2.1 MB", sharedWith: ["Jordan Kim", "Sarah McIntyre"], activeEditors: [], ownerId: 2, starred: false },
{ id: 13, type: "file", name: "Homepage Hero.jpg", groupId: 3, modifiedAt: ago(4), size: "5.6 MB", sharedWith: ["Sarah McIntyre"], activeEditors: ["Sarah McIntyre"], ownerId: 1, starred: false },
{ id: 14, type: "file", name: "Team Photo 2026.jpg", groupId: 1, modifiedAt: ago(30), size: "8.9 MB", sharedWith: ["Priya Anand", "Marcus Webb", "Jordan Kim"], activeEditors: [], ownerId: 1, starred: true },
// Code / config
{ id: 15, type: "file", name: "api-config.json", groupId: 2, modifiedAt: ago(0), size: "4 KB", sharedWith: ["Marcus Webb"], activeEditors: ["Marcus Webb"], ownerId: 3, starred: false },
// Presentations
{ id: 16, type: "file", name: "Investor Deck May.pptx", groupId: 2, modifiedAt: ago(6), size: "18.4 MB", sharedWith: ["Priya Anand"], activeEditors: [], ownerId: 1, starred: true },
// External shared
{ id: 17, type: "file", name: "Vendor Contract.pdf", groupId: 5, modifiedAt: ago(45), size: "2.2 MB", sharedWith: ["Priya Anand"], activeEditors: [], ownerId: 1, starred: false },
{ id: 18, type: "file", name: "External Proposal.docx", groupId: 5, modifiedAt: ago(8), size: "0.7 MB", sharedWith: [], activeEditors: [], ownerId: 1, starred: false },
]
}
get locationLabel() {
return this.locations.find(l => l.id === this.activeLocation)?.label || "All Files"
}
get visibleFiles() {
let files = this.files;
// Filter by active groups
files = files.filter(f => this.activeGroupIds.has(f.groupId));
// Filter by location
if (this.activeLocation === "mine") files = files.filter(f => f.ownerId === 1);
if (this.activeLocation === "shared") files = files.filter(f => f.sharedWith?.length > 0);
if (this.activeLocation === "starred") files = files.filter(f => f.starred);
if (this.activeLocation === "recent") files = files.sort((a, b) => new Date(b.modifiedAt) - new Date(a.modifiedAt)).slice(0, 10);
// Search
if (this.searchText) {
const q = this.searchText.toLowerCase();
files = files.filter(f => f.name.toLowerCase().includes(q));
}
return files;
}
render() {
HStack(() => {
// Left sidebar
VStack(() => {
DesktopFilesSidebar(
this.groups,
this.activeGroupIds,
(groupId) => {
if (this.activeGroupIds.has(groupId)) {
if (this.activeGroupIds.size > 1) this.activeGroupIds.delete(groupId);
} else {
this.activeGroupIds.add(groupId);
}
this.rerender();
},
this.locations,
this.activeLocation,
(locId) => { this.activeLocation = locId; this.rerender(); }
)
})
.width(220, px)
.minWidth(200, px)
.height(100, pct)
.borderRight("1px solid var(--divider)")
.flexShrink(0)
.overflow("hidden")
// Main area
VStack(() => {
DesktopFilesToolbar(
this.locationLabel,
this.viewMode,
this.searchText,
(mode) => { this.viewMode = mode; this.rerender(); },
(text) => { this.searchText = text; this.rerender(); },
() => {}
)
DesktopFilesGrid(
this.visibleFiles,
this.groups,
this.viewMode,
() => {}
)
.flex(1)
.minHeight(0)
.width(100, pct)
.overflow("hidden")
})
.flex(1)
.height(100, pct)
.overflow("hidden")
})
.height(100, pct)
.width(100, pct)
.overflow("hidden")
}
}
register(Files)