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)
|
||||
3
files/icons/files.svg
Normal file
3
files/icons/files.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="152" height="128" viewBox="0 0 152 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M135.189 18.0274H70.1495L56.0594 1.23569C55.7346 0.848813 55.3292 0.537769 54.8719 0.324324C54.4145 0.110879 53.9162 0.000189693 53.4117 1.17244e-07H3.45833C3.00415 -0.000118302 2.55438 0.0894701 2.13475 0.263644C1.71511 0.437819 1.33382 0.693166 1.01266 1.0151C0.6915 1.33703 0.436765 1.71924 0.26301 2.13989C0.0892542 2.56054 -0.000118018 3.01138 1.16963e-07 3.46667V111.766C0.00492427 115.986 1.67921 120.031 4.65558 123.014C7.63195 125.998 11.6674 127.676 15.8766 127.681H135.189C139.398 127.676 143.434 125.998 146.41 123.014C149.386 120.031 151.061 115.986 151.066 111.766V33.9405C151.061 29.7213 149.386 25.6764 146.41 22.6931C143.434 19.7099 139.398 18.0319 135.189 18.0274ZM6.91667 6.93333H51.8007L61.1119 18.0274H6.91667V6.93333ZM144.149 111.766C144.146 114.147 143.202 116.43 141.522 118.114C139.842 119.798 137.565 120.745 135.189 120.748H15.8766C13.501 120.745 11.2235 119.798 9.54377 118.114C7.864 116.43 6.9192 114.147 6.91667 111.766V24.9608H135.189C137.564 24.9633 139.842 25.9101 141.521 27.5935C143.201 29.2769 144.146 31.5595 144.149 33.9405V111.766Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
files/icons/fileslight.svg
Normal file
3
files/icons/fileslight.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="152" height="128" viewBox="0 0 152 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M135.189 18.0274H70.1495L56.0594 1.23569C55.7346 0.848813 55.3292 0.537769 54.8719 0.324324C54.4145 0.110879 53.9162 0.000189693 53.4117 1.17244e-07H3.45833C3.00415 -0.000118302 2.55438 0.0894701 2.13475 0.263644C1.71511 0.437819 1.33382 0.693166 1.01266 1.0151C0.6915 1.33703 0.436765 1.71924 0.26301 2.13989C0.0892542 2.56054 -0.000118018 3.01138 1.16963e-07 3.46667V111.766C0.00492427 115.986 1.67921 120.031 4.65558 123.014C7.63195 125.998 11.6674 127.676 15.8766 127.681H135.189C139.398 127.676 143.434 125.998 146.41 123.014C149.386 120.031 151.061 115.986 151.066 111.766V33.9405C151.061 29.7213 149.386 25.6764 146.41 22.6931C143.434 19.7099 139.398 18.0319 135.189 18.0274ZM6 5.5H53.4117L64.0654 18.0274H6V5.5ZM145.5 111.766C145.5 113 143.745 118.316 142.065 120C140.386 121.684 134.876 121.997 132.5 122L15.8766 122.5C13.501 122.497 10.2452 121.184 8.56543 119.5C6.88567 117.816 5.50254 114.147 5.5 111.766L6 23L70.1495 22.6931H135.189C137.564 22.6956 141.386 25.3166 143.065 27C144.745 28.6834 145.497 32.619 145.5 35V74V111.766Z" fill="#FEE8C8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
Reference in New Issue
Block a user