diff --git a/babel.config.mjs b/babel.config.mjs index 0b378e5..ad40e4b 100644 --- a/babel.config.mjs +++ b/babel.config.mjs @@ -8,6 +8,6 @@ export default { }, }, ], - "@babel/preset-typescript", + ["@babel/preset-typescript", { allowDeclareFields: true }], ], } diff --git a/src/components/FolderBreadcrumb.ts b/src/components/FolderBreadcrumb.ts new file mode 100644 index 0000000..aa4513f --- /dev/null +++ b/src/components/FolderBreadcrumb.ts @@ -0,0 +1,127 @@ +import { LitElement, html, css, svg, nothing } from 'lit' + +/** + * A single segment of the breadcrumb trail. + */ +export interface Crumb { + label: string + href?: string + /** + * `'grid'` renders the Figma dashboard glyph (a 2×2 of rounded rectangles) + * before the label. Used on the leading crumb to mirror the redesign. + */ + icon?: 'grid' +} + +/** + * — the location trail at the top of the folder view + * (Figma file eIjn2itV9Ma1nwxyW4Nk4f, node 1569:12509). + * + * Visual spec: a flex row with `gap: 5px`, every segment is a + * `Neue Einstellung Regular 14px #6a7282` label, segments are separated by + * a 5.751 × 12.334 px diagonal stroke (the "/" slash). The leading segment + * also shows the 12 px four-tile dashboard glyph. + */ +export class FolderBreadcrumb extends LitElement { + /** Crumbs to render, left-to-right. Set as a property. */ + crumbs: Crumb[] = [] + + static styles = css` + :host { + display: block; + font-family: 'Neue Einstellung', var(--font-family-base, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif); + } + + .breadcrumb { + display: flex; + align-items: center; + gap: 5px; + list-style: none; + margin: 0; + padding: 0; + } + + .crumb { + display: flex; + align-items: center; + gap: 4px; + text-decoration: none; + color: inherit; + cursor: pointer; + } + + .label { + font-size: 14px; + /* line-height: 1 keeps the visual centre aligned with the icon and + separator — otherwise the default line box pushes text slightly low. */ + line-height: 1; + font-weight: 400; + color: var(--gray-500, #6a7282); + white-space: nowrap; + } + + .grid-icon { + flex: 0 0 12px; + width: 12px; + height: 12px; + display: block; + } + + .grid-icon svg, + .sep svg { + display: block; + } + + .sep { + /* The Figma slash is a 13.609 px line rotated 115° inside a 5.751 × + 12.334 hit-box. Rendering it as a properly-scaled diagonal stroke + keeps the line endpoints flush with the box corners. */ + flex: 0 0 auto; + width: 5.751px; + height: 12.334px; + display: block; + } + ` + + private gridIconSvg () { + return svg` + + + + + ` + } + + /** + * A diagonal slash from bottom-left to top-right of a 5.751 × 12.334 box, + * matching Figma node 1569:12518 (a 13.609 px line rotated 115°). + */ + private separatorSvg () { + return svg` + + ` + } + + render () { + if (!this.crumbs.length) return nothing + return html` + + ` + } +} + +export const FOLDER_BREADCRUMB_TAG = 'folder-breadcrumb' + +if (!customElements.get(FOLDER_BREADCRUMB_TAG)) { + customElements.define(FOLDER_BREADCRUMB_TAG, FolderBreadcrumb) +} diff --git a/src/components/FolderCard.ts b/src/components/FolderCard.ts new file mode 100644 index 0000000..611ca2e --- /dev/null +++ b/src/components/FolderCard.ts @@ -0,0 +1,247 @@ +import { LitElement, html, css, svg } from 'lit' + +/** + * — a single folder/file tile in the folder pane. + * + * Visual spec ported 1:1 from the Solid OS Figma redesign + * (file eIjn2itV9Ma1nwxyW4Nk4f, node 1569:12231). The card is a fixed + * 192 × 145.824 px tile: folder glyph in a slate box, a ⋮ overflow menu, + * the resource name, a divider, and a footer with the child count plus + * optional "favorite" / "public" status badges. + * + * Regular (non-power, non-developer) users see only the cleaned-up name — + * no file extension, no MIME type, no URI — so `name` is expected to be + * pre-cleaned by the caller. + */ +export class FolderCard extends LitElement { + static properties = { + name: { type: String, reflect: true }, + href: { type: String, reflect: true }, + kind: { type: String, reflect: true }, + count: { type: Number, reflect: true }, + favorite: { type: Boolean, reflect: true }, + isPublic: { type: Boolean, attribute: 'is-public', reflect: true } + } + + declare name: string + declare href: string + /** 'folder' (default) or 'file' — selects the card glyph. */ + declare kind: 'folder' | 'file' + declare count: number + declare favorite: boolean + declare isPublic: boolean + + constructor () { + super() + this.name = '' + this.href = '' + this.kind = 'folder' + this.count = 0 + this.favorite = false + this.isPublic = false + } + + static styles = css` + :host { + display: block; + width: 192px; + height: 145.824px; + flex: 0 0 auto; + } + + .card { + position: relative; + box-sizing: border-box; + width: 192px; + height: 145.824px; + background: var(--white, #ffffff); + border: 0.932px solid var(--gray-200, #e5e7eb); + border-radius: 4.66px; + overflow: hidden; + text-decoration: none; + color: inherit; + display: block; + cursor: pointer; + } + + .icon-box { + position: absolute; + left: 13.05px; + top: 13.31px; + width: 37.282px; + height: 37.282px; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + padding: 9.32px; + border-radius: 3.883px; + background: var(--slate-100, #f1f5f9); + } + + .icon-box svg { + width: 18.641px; + height: 18.641px; + display: block; + } + + .menu { + position: absolute; + right: 13.71px; + top: 13.07px; + width: 15.36px; + height: 15.36px; + padding: 0; + margin: 0; + border: none; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + } + + .menu svg { + width: 2.4px; + height: 11.04px; + display: block; + } + + .name { + position: absolute; + left: 13.47px; + top: 61.77px; + margin: 0; + font-family: 'Neue Einstellung', var(--font-family-base, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif); + font-weight: 500; + font-size: 16px; + line-height: normal; + color: var(--gray-700, #364153); + white-space: nowrap; + max-width: 165px; + overflow: hidden; + text-overflow: ellipsis; + } + + .divider { + position: absolute; + left: 13.47px; + top: 107.61px; + width: 163.2px; + height: 0; + border-top: 0.96px solid var(--gray-200, #e5e7eb); + } + + .footer { + position: absolute; + left: 13.47px; + top: 116.76px; + width: 162.687px; + display: flex; + align-items: center; + justify-content: space-between; + } + + .count { + margin: 0; + font-family: 'Neue Einstellung', var(--font-family-base, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif); + font-weight: 500; + font-size: 12px; + line-height: normal; + color: var(--gray-500, #6a7282); + white-space: nowrap; + } + + .badges { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 9.32px; + } + + .badges svg { + width: 14.913px; + height: 14.913px; + display: block; + } + ` + + private folderIcon () { + return svg` + + ` + } + + // File glyph for non-container resources. The Figma frame only shows + // folder cards, so this reuses the folder icon's visual treatment + // (pale indigo fill, #6A7282 stroke) on a document outline. + private fileIcon () { + return svg` + + + ` + } + + private menuIcon () { + return svg` + + + + ` + } + + private favoriteIcon () { + return svg` + + ` + } + + private publicIcon () { + return svg` + + + + ` + } + + private onMenuClick (e: Event) { + e.preventDefault() + e.stopPropagation() + this.dispatchEvent(new CustomEvent('folder-card-menu', { + bubbles: true, + composed: true, + detail: { name: this.name, href: this.href } + })) + } + + render () { + return html` + + ${this.kind === 'file' ? this.fileIcon() : this.folderIcon()} + +

${this.name}

+
+ +
+ ` + } +} + +export const FOLDER_CARD_TAG = 'folder-card' + +if (!customElements.get(FOLDER_CARD_TAG)) { + customElements.define(FOLDER_CARD_TAG, FolderCard) +} diff --git a/src/components/FolderNavSidebar.ts b/src/components/FolderNavSidebar.ts new file mode 100644 index 0000000..53cb193 --- /dev/null +++ b/src/components/FolderNavSidebar.ts @@ -0,0 +1,424 @@ +import { LitElement, html, css, svg, nothing } from 'lit' + +/** + * A single navigable node in the folder tree. + */ +export interface NavNode { + name: string + href: string + expanded?: boolean + current?: boolean + children?: NavNode[] +} + +/** + * — the left navigation rail of the Solid OS folder + * redesign (Figma file eIjn2itV9Ma1nwxyW4Nk4f, node 1569:12232). + * + * Fixed 250px rail: a "Favorites" section, a divider, the main folder tree + * ("Home"), a divider, and a "Public" section. Tree rows carry a disclosure + * chevron, a bullet/glyph, and a label. The visual spec — paddings, the + * #e4dbfe current-row highlight, the #e5e7eb guide lines on nested groups — + * is ported 1:1 from the design. + */ +export class FolderNavSidebar extends LitElement { + static properties = { + homeLabel: { type: String, attribute: 'home-label' }, + homeHref: { type: String, attribute: 'home-href' }, + publicHref: { type: String, attribute: 'public-href' } + } + + declare homeLabel: string + declare homeHref: string + declare publicHref: string + /** Tree under "Home". Set as a property (not an attribute). */ + tree: NavNode[] = [] + /** Favorited resources shown under the "Favorites" section. */ + favorites: NavNode[] = [] + /** + * Per-row expand/collapse overrides, keyed by href. Survives parent-driven + * `tree` reassignments (it's component state, not tree data), so a user's + * disclosure clicks aren't wiped when folderPane re-syncs the tree. + */ + private _expanded = new Map() + /** Disclosure state for the top-level "Home" and "Favorites" sections. */ + private _homeExpanded = true + private _favExpanded = false + + constructor () { + super() + this.homeLabel = 'Home' + this.homeHref = '' + this.publicHref = '' + } + + static styles = css` + :host { + display: block; + width: 250px; + min-width: 250px; + align-self: stretch; + box-sizing: border-box; + background: var(--neutral-50, #fafafa); + border-right: 1px solid var(--gray-200, #e5e7eb); + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + font-family: 'Neue Einstellung', var(--font-family-base, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif); + } + + .inner { + box-sizing: border-box; + padding: 17px 10px 17px 10px; + display: flex; + flex-direction: column; + gap: 10px; + } + + .divider { + height: 0; + border-top: 1px solid var(--gray-200, #e5e7eb); + width: 100%; + } + + /* A top-level section row (Favorites / Home / Public). The disclosure + chevron is a sibling button — only the .row-link navigates. */ + .row { + display: flex; + align-items: center; + gap: 5px; + box-sizing: border-box; + width: 100%; + padding: 5px 15px 5px 5px; + border-radius: 5px; + } + + .row--current { + background: #e4dbfe; + } + + /* The navigating part of a row — chevron clicks never reach this. */ + .row-link, + .tree-row-link { + display: flex; + align-items: center; + flex: 1 1 auto; + min-width: 0; + text-decoration: none; + color: inherit; + cursor: pointer; + } + + .chev { + flex: 0 0 16px; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + padding: 0; + cursor: pointer; + } + + .chev svg { + width: 9px; + height: 5px; + display: block; + transform: rotate(-90deg); + transition: transform 0.12s ease; + } + + .chev[aria-expanded='true'] svg { + transform: rotate(0deg); + } + + .label-group { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + } + + .glyph { + flex: 0 0 16px; + width: 16px; + height: 16px; + display: block; + } + + .bullet { + flex: 0 0 6px; + width: 6px; + height: 6px; + display: flex; + align-items: center; + justify-content: center; + } + + /* Inline SVGs default to vertical-align: baseline, which leaves a + descender slot under them and visually shifts the glyph up against + neighbouring text. Force every nav svg to block so the icon box + contains the glyph exactly. */ + .chev svg, + .glyph svg, + .bullet svg { + display: block; + } + + .glyph svg { width: 16px; height: 16px; } + .bullet svg { width: 6px; height: 6px; } + + .label { + font-size: 14px; + /* line-height: 1 keeps the line box snug to the glyphs so flex + align-items: center actually centers against the visual text, + not against the (taller) default line box. */ + line-height: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .label--section { + font-weight: 500; + color: var(--gray-500, #6a7282); + } + + .label--home { + font-weight: 500; + color: var(--primary-royal-lavender, #7c4dff); + } + + .label--item { + font-weight: 400; + color: var(--gray-500, #6a7282); + } + + /* The "Home" tree and its nested groups. */ + .tree { + display: flex; + flex-direction: column; + gap: 4px; + width: 100%; + } + + .tree-children { + display: flex; + flex-direction: column; + gap: 4px; + width: 100%; + box-sizing: border-box; + padding-left: 10px; + } + + .tree-group { + display: flex; + flex-direction: column; + gap: 4px; + width: 100%; + box-sizing: border-box; + border-left: 1px solid var(--gray-200, #e5e7eb); + } + + /* A folder row in the tree. The chevron toggles in place; the .tree-row-link + beside it is the only navigating element. */ + .tree-row { + display: flex; + align-items: center; + gap: 5px; + box-sizing: border-box; + width: 100%; + padding: 5px 15px 5px 10px; + border-radius: 5px; + } + + .tree-row .label-group { + width: 125px; + } + ` + + private chevronIcon () { + return svg` + + ` + } + + private starIcon () { + return svg` + + ` + } + + private homeIcon () { + return svg` + + ` + } + + private globeIcon () { + return svg` + + + + ` + } + + private bulletIcon () { + // viewBox padded by 0.5 on every side so the 1px stroke (outer edge at + // ±3 from centre) never touches the svg's clip rect — otherwise the + // bottom/right of the circle gets shaved and the glyph looks off-centre. + return svg` + + ` + } + + /** Effective expand state for a node — user override wins over the default. */ + private isExpanded (node: NavNode): boolean { + return this._expanded.has(node.href) + ? !!this._expanded.get(node.href) + : !!node.expanded + } + + /** + * Disclosure-chevron click. Toggles the row in place — it must NOT navigate + * (the chevron lives next to, not inside, the row's link). Also emits + * `folder-nav-toggle` so the host can lazily load children if needed. + */ + private toggleNode (node: NavNode, e: Event) { + e.preventDefault() + e.stopPropagation() + const next = !this.isExpanded(node) + this._expanded.set(node.href, next) + this.requestUpdate() + this.dispatchEvent(new CustomEvent('folder-nav-toggle', { + bubbles: true, + composed: true, + detail: { href: node.href, expanded: next } + })) + } + + private renderNode (node: NavNode): unknown { + const hasChildren = !!(node.children && node.children.length) + const expanded = this.isExpanded(node) + return html` +
+ + + + ${this.bulletIcon()} + ${node.name} + + +
+ ${hasChildren && expanded + ? html`
+ ${node.children!.map(child => this.renderNode(child))} +
` + : nothing} + ` + } + + /** Toggle a top-level section's disclosure without navigating. */ + private toggleSection (which: 'home' | 'fav', e: Event) { + e.preventDefault() + e.stopPropagation() + if (which === 'home') this._homeExpanded = !this._homeExpanded + else this._favExpanded = !this._favExpanded + this.requestUpdate() + } + + render () { + return html` +
+ +
+ + + + ${this.starIcon()} + Favorites + + +
+ ${this._favExpanded && this.favorites.length + ? html`
+ ${this.favorites.map(fav => html` + + `)} +
` + : nothing} + +
+ + +
+
+ + + + ${this.homeIcon()} + ${this.homeLabel} + + +
+ ${this._homeExpanded && this.tree.length + ? html`
+ ${this.tree.map(node => this.renderNode(node))} +
` + : nothing} +
+ +
+ + +
+ + + + ${this.globeIcon()} + Public + + +
+
+ ` + } +} + +export const FOLDER_NAV_SIDEBAR_TAG = 'folder-nav-sidebar' + +if (!customElements.get(FOLDER_NAV_SIDEBAR_TAG)) { + customElements.define(FOLDER_NAV_SIDEBAR_TAG, FolderNavSidebar) +} diff --git a/src/folderPane.ts b/src/folderPane.ts index 13f8f72..adc8759 100644 --- a/src/folderPane.ts +++ b/src/folderPane.ts @@ -7,6 +7,27 @@ import { authn } from 'solid-logic' import * as UI from 'solid-ui' import './styles/folderPane.css' import './styles/utilities.css' +import { FOLDER_CARD_TAG, FolderCard } from './components/FolderCard' +import { FOLDER_NAV_SIDEBAR_TAG, FolderNavSidebar } from './components/FolderNavSidebar' +import { FOLDER_BREADCRUMB_TAG, FolderBreadcrumb } from './components/FolderBreadcrumb' + +// Display name for a contained resource, cleaned for regular (non-power, +// non-developer) users: trailing slash dropped for folders, file extension +// dropped for files. Power-user detail (URIs, MIME types) is intentionally +// not surfaced here. +function displayName (obj: { uri: string }): string { + let last = obj.uri.replace(/\/+$/, '') + last = last.slice(last.lastIndexOf('/') + 1) + try { + last = decodeURIComponent(last) + } catch { /* leave as-is if not decodable */ } + const isFolder = obj.uri.endsWith('/') + if (!isFolder) { + const dot = last.lastIndexOf('.') + if (dot > 0) last = last.slice(0, dot) // strip extension + } + return last +} export default { icon: UI.icons.iconBase + 'noun_973694_expanded.svg', @@ -56,11 +77,22 @@ export default { ) } + // Child containers whose contents we've already requested, so the lazy + // count-loading below fires at most once per folder. + const countLoadRequested = new Set() + + function isContainer (obj) { + return obj.uri.endsWith('/') || + kb.holds(obj, UI.ns.rdf('type'), UI.ns.ldp('Container')) || + kb.holds(obj, UI.ns.rdf('type'), UI.ns.ldp('BasicContainer')) + } + function refresh () { let objs = kb.each(subject, UI.ns.ldp('contains')).filter(noHiddenFiles) objs = objs.map(obj => [UI.utils.label(obj).toLowerCase(), obj]) objs.sort() // Sort by label case-insensitive objs = objs.map(pair => pair[1]) +<<<<<<< Updated upstream UI.utils.syncTableToArray(mainTable, objs, function (obj) { const st = kb.statementsMatching(subject, UI.ns.ldp('contains'), obj)[0] const defaultpropview = outliner.VIEWAS_boring_default @@ -73,11 +105,45 @@ export default { ) // UI.widgets.makeDraggable(tr, obj) return tr +======= + // mainTable is a
of + // web components — one per ldp:contains child. syncTableToArray works + // on any parent's children, so it manages the cards identically to rows. + UI.utils.syncTableToArray(mainTable, objs, function (obj) { + const st = kb.statementsMatching(subject, UI.ns.ldp('contains'), obj)[0] + const card = dom.createElement(FOLDER_CARD_TAG) as FolderCard + ;(card as any).AJAR_statement = st + card.name = displayName(obj) + card.href = obj.uri + card.kind = isContainer(obj) ? 'folder' : 'file' + // Child count: number of resources inside this folder. The parent + // listing doesn't carry it, so for sub-folders we lazily fetch the + // child container once and update this card when it lands. (We update + // the card directly rather than via refresh(), because syncTableToArray + // reuses existing card elements and won't re-run this callback.) + card.count = kb.each(obj, UI.ns.ldp('contains')).length + if (isContainer(obj) && card.count === 0 && !countLoadRequested.has(obj.uri)) { + countLoadRequested.add(obj.uri) + kb.fetcher + .load(obj) + .then(() => { + card.count = kb.each(obj, UI.ns.ldp('contains')).filter(noHiddenFiles).length + }) + .catch(() => { /* unreadable child — leave at 0 */ }) + } + // "Public" badge: anything living under a /public/ path segment is + // world-readable in a Solid pod — a cheap, accurate signal that needs + // no extra ACL fetch. + card.isPublic = /\/public\//.test(obj.uri) + // "Favorite" badge: driven by an optional ui:favorite triple, which + // the parent container's .meta can carry (loaded with the folder). + card.favorite = kb.holds(obj, UI.ns.ui('favorite'), true as any) + return card +>>>>>>> Stashed changes }) } const dom = context.dom - const outliner = context.getOutliner(dom) const kb = context.session.store let mainTable // This is a live synced table const div = dom.createElement('div') @@ -100,13 +166,125 @@ export default { }) return div } else { +<<<<<<< Updated upstream mainTable = div.appendChild(dom.createElement('table')) mainTable.classList.add('folderPaneMainTable') mainTable.refresh = refresh refresh() +======= + // Breadcrumb (Figma node 1569:12509) — sits above the white card. + // Always starts with "Dashboard" + "Home", then adds one crumb per + // path segment between the conventional public/ home root and the + // current container so deep folders show their full trail. + const breadcrumb = div.appendChild( + dom.createElement(FOLDER_BREADCRUMB_TAG) + ) as FolderBreadcrumb + const dashboardMatch = subject.uri.match(/^(https?:\/\/[^/]+\/[^/]+\/)/) + const homeMatch = subject.uri.match(/^(https?:\/\/[^/]+\/[^/]+\/public\/)/) + const homeRoot = homeMatch ? homeMatch[1] : (dashboardMatch ? dashboardMatch[1] : subject.uri) + const crumbs: any[] = [ + { label: 'Dashboard', icon: 'grid', href: dashboardMatch ? dashboardMatch[1] : subject.uri }, + { label: 'Home', href: homeRoot } + ] + if (subject.uri.startsWith(homeRoot) && subject.uri !== homeRoot) { + const tail = subject.uri.slice(homeRoot.length).replace(/\/+$/, '') + const parts = tail.split('/').filter(Boolean) + let acc = homeRoot + for (const part of parts) { + acc += part + '/' + let label = part + try { label = decodeURIComponent(part) } catch { /* keep raw */ } + crumbs.push({ label, href: acc }) + } + } + breadcrumb.crumbs = crumbs + + // Two-column layout (Figma node 1569:12231): a navigation rail on the + // left, the folder-card grid on the right. + const layout = div.appendChild(dom.createElement('div')) + layout.classList.add('folder-pane-layout') + + const sidebar = layout.appendChild( + dom.createElement(FOLDER_NAV_SIDEBAR_TAG) + ) as FolderNavSidebar + // "Home" is the navigation root label (Figma node 1569:12243) — a stable + // UX label for the tree root, not the literal folder name. + sidebar.homeLabel = 'Home' + sidebar.homeHref = subject.uri + const pubMatch = subject.uri.match(/^(.*\/public\/)/) + sidebar.publicHref = pubMatch ? pubMatch[1] : '' + + const mainCol = layout.appendChild(dom.createElement('div')) + mainCol.classList.add('folder-pane-main') + + // Listing of LDP contents rendered as a grid of tiles. + mainTable = mainCol.appendChild(dom.createElement('div')) + mainTable.classList.add('folder-card-grid') + mainTable.setAttribute('role', 'list') + mainTable.setAttribute('aria-label', 'Contents of ' + (UI.utils.label(subject) || 'folder')) + + // Keep the sidebar's tree + favorites in sync with the listing: the + // tree shows the current folder's sub-folders, favorites shows any + // child carrying a ui:favorite triple. + // The sidebar tree mirrors the pod folder hierarchy two levels deep + // (matching Figma node 1569:12242). syncTreeRequested tracks which + // sub-folders we've already kicked a load for. + const syncTreeRequested = new Set() + + // Sorted, de-duplicated child sub-folders of a container already in the + // store. Lazily loading a child re-asserts its ldp:contains triple in a + // second doc, so kb.each can return duplicates — dedup by URI. + const childFolders = function (container) { + const seen = new Set() + return kb.each(container, UI.ns.ldp('contains')) + .filter(noHiddenFiles) + .filter(c => (seen.has(c.uri) ? false : (seen.add(c.uri), true))) + .filter(isContainer) + .map(c => [UI.utils.label(c).toLowerCase(), c] as [string, any]) + .sort() + .map(pair => pair[1]) + } + + const syncSidebar = function () { + const seen = new Set() + const children = kb.each(subject, UI.ns.ldp('contains')) + .filter(noHiddenFiles) + .filter(c => (seen.has(c.uri) ? false : (seen.add(c.uri), true))) + + // Build a tree node for a sub-folder, recursing one level so the rail + // shows nested folders (Figma shows e.g. Marketing Materials expanded). + const buildNode = function (folder, depth) { + const grandchildren = depth < 2 ? childFolders(folder) : [] + // Kick a one-off load so a deeper level can appear once it lands. + if (depth < 2 && grandchildren.length === 0 && !syncTreeRequested.has(folder.uri)) { + syncTreeRequested.add(folder.uri) + kb.fetcher.load(folder).then(syncSidebar).catch(() => { /* unreadable */ }) + } + return { + name: displayName(folder), + href: folder.uri, + expanded: grandchildren.length > 0, // expand folders that hold folders + children: grandchildren.map(g => buildNode(g, depth + 1)) + } + } + + sidebar.tree = childFolders(subject).map(f => buildNode(f, 1)) + sidebar.favorites = children + .filter(c => kb.holds(c, UI.ns.ui('favorite'), true as any)) + .map(c => ({ name: displayName(c), href: c.uri })) + sidebar.requestUpdate() + } + + const refreshAll = function () { + refresh() + syncSidebar() + } + mainTable.refresh = refreshAll + refreshAll() +>>>>>>> Stashed changes // addDownstreamChangeListener is a high level function which when someone else changes the resource, // reloads it into the kb, then must call addDownstreamChangeListener to be able to update the folder pane. - kb.updater.addDownstreamChangeListener(subject, refresh) // Update store and call me if folder changes + kb.updater.addDownstreamChangeListener(subject, refreshAll) // Update store and call me if folder changes } // Allow user to create new things within the folder diff --git a/src/icons/breadcrumb-icon.svg b/src/icons/breadcrumb-icon.svg new file mode 100644 index 0000000..fde476d --- /dev/null +++ b/src/icons/breadcrumb-icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/icons/breadcrumb-slash.svg b/src/icons/breadcrumb-slash.svg new file mode 100644 index 0000000..674713d --- /dev/null +++ b/src/icons/breadcrumb-slash.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/bullet.svg b/src/icons/bullet.svg new file mode 100644 index 0000000..4fb819d --- /dev/null +++ b/src/icons/bullet.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/bullet2.svg b/src/icons/bullet2.svg new file mode 100644 index 0000000..4fb819d --- /dev/null +++ b/src/icons/bullet2.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/chevron.svg b/src/icons/chevron.svg new file mode 100644 index 0000000..b2087f1 --- /dev/null +++ b/src/icons/chevron.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/divider.svg b/src/icons/divider.svg new file mode 100644 index 0000000..e4d3100 --- /dev/null +++ b/src/icons/divider.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/favorites-alt.svg b/src/icons/favorites-alt.svg new file mode 100644 index 0000000..3315c6f --- /dev/null +++ b/src/icons/favorites-alt.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/icons/favorites.svg b/src/icons/favorites.svg new file mode 100644 index 0000000..a59be36 --- /dev/null +++ b/src/icons/favorites.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/icons/folder.svg b/src/icons/folder.svg new file mode 100644 index 0000000..f746bd4 --- /dev/null +++ b/src/icons/folder.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/icons/menu-dots.svg b/src/icons/menu-dots.svg new file mode 100644 index 0000000..1101c25 --- /dev/null +++ b/src/icons/menu-dots.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/icons/nav-globe.svg b/src/icons/nav-globe.svg new file mode 100644 index 0000000..838694f --- /dev/null +++ b/src/icons/nav-globe.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/icons/nav-home.svg b/src/icons/nav-home.svg new file mode 100644 index 0000000..a6731de --- /dev/null +++ b/src/icons/nav-home.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/icons/nav-star.svg b/src/icons/nav-star.svg new file mode 100644 index 0000000..a07159d --- /dev/null +++ b/src/icons/nav-star.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/icons/public.svg b/src/icons/public.svg new file mode 100644 index 0000000..bfa3d50 --- /dev/null +++ b/src/icons/public.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/styles/folderPane.css b/src/styles/folderPane.css index d0ef151..d799952 100644 --- a/src/styles/folderPane.css +++ b/src/styles/folderPane.css @@ -4,16 +4,48 @@ text-align: left; width: 100%; max-width: none; - margin-left: 0; - margin-right: 0; - border-top: solid 1px #777; - border-bottom: solid 1px #777; - margin-top: var(--spacing-xs, 0.5em); - margin-bottom: var(--spacing-xs, 0.5em); + margin: 0; + background: var(--white, #ffffff); overflow-x: auto; -webkit-overflow-scrolling: touch; - padding-top: 1rem; - padding-bottom: 1rem; + padding: 0; +} + +/* Breadcrumb row sits at the top of the pane, above the sidebar/grid card + (Figma node 1569:12509). Horizontal padding matches the card's inner + gutter; vertical padding leaves the row breathing room. */ +.folderPaneInstancePane > folder-breadcrumb { + display: block; + padding: 16px 24px; +} + +/* Two-column shell (Figma node 1569:12231): nav rail flush-left, card area + filling the rest. The carries its own 250px width + and right border. */ +.folder-pane-layout { + display: flex; + align-items: stretch; + width: 100%; + min-height: 360px; +} + +.folder-pane-main { + flex: 1 1 auto; + min-width: 0; + box-sizing: border-box; + /* Figma node 1569:12303: card grid at left 269 / top 24, sidebar is 250 + wide — a 19px gutter between the rail and the first card. */ + padding: 24px 24px 24px 19px; +} + +/* Grid of tiles — Figma node 1569:12303: a wrapping flex row + with a 20px gap. Cards are fixed-size so they tile left-to-right. */ +.folder-card-grid { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 20px; + width: 100%; } .folderPanePackageDiv {