init
This commit is contained in:
408
files/desktop/DesktopFilesGrid.js
Normal file
408
files/desktop/DesktopFilesGrid.js
Normal 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)
|
||||
175
files/desktop/DesktopFilesSidebar.js
Normal file
175
files/desktop/DesktopFilesSidebar.js
Normal 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)
|
||||
98
files/desktop/DesktopFilesToolbar.js
Normal file
98
files/desktop/DesktopFilesToolbar.js
Normal 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
159
files/desktop/files.js
Normal 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)
|
||||
Reference in New Issue
Block a user