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)