# Sidenav Collapsible Sections, Drag-to-Upload & Blossom API Extraction

## Overview

Four enhancements to the sidenav on `www/blobs.html` (prototyped here before rolling out to all pages):

1. **Collapsible sections** — clicking a section title collapses/expands its content
2. **Generalized sidenav section pattern** — shared CSS and optional JS helper so relay and blossom sections follow the same collapsible structure
3. **Drag-and-drop upload onto blossom server names** — drag a blob file over a server name in the blossom section to upload directly to that specific server
4. **Extract `blossom-api.mjs`** — shared upload/delete/auth/hash module so any page can interact with blossom servers

---

## Current Architecture

### Sidenav Section Layout in `blobs.html`

```
#divSideNav
  ├── #divSideNavHeader
  ├── #divSideNavBody          (flex: 1, scrollable)
  ├── #divRelaySection         (fixed, rendered by relay-ui.mjs)
  │     ├── #divRelaySectionTitle   "リレー"
  │     └── #divRelayList
  ├── #divBlossomSection       (fixed, rendered by blossom-ui.mjs)
  │     ├── #divBlossomSectionTitle "ブロッサム"
  │     └── #divBlossomList
  └── #divVersionBar
```

### Key Files

- [`www/css/client.css`](www/css/client.css) — Global styles for `#divRelaySection`, `#divBlossomSection`, titles, lists
- [`www/js/relay-ui.mjs`](www/js/relay-ui.mjs) — Renders relay rows into `#divRelayList`
- [`www/js/blossom-ui.mjs`](www/js/blossom-ui.mjs) — Renders blossom server rows into `#divBlossomList`
- [`www/blobs.html`](www/blobs.html) — The page we are prototyping on

---

## Feature 1: Collapsible Sections

### Behavior

- Clicking the section title toggles visibility of the section content (the list div)
- Collapsed state persists via `localStorage` per section ID
- A small visual indicator shows collapsed/expanded state (CSS rotation of a caret or similar)
- Default state: expanded

### Implementation

#### CSS Changes in `client.css`

Add styles for the collapsible behavior on the shared section title selectors:

```css
#divRelaySectionTitle,
#divBlossomSectionTitle {
  cursor: pointer;
  user-select: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

/* Collapse indicator arrow appended via ::after */
#divRelaySectionTitle::after,
#divBlossomSectionTitle::after {
  content: '▾';
  font-size: 80%;
  transition: transform 0.3s ease;
  color: var(--muted-color);
}

/* When parent section has .collapsed class, rotate the arrow */
#divRelaySection.collapsed #divRelaySectionTitle::after,
#divBlossomSection.collapsed #divBlossomSectionTitle::after {
  transform: rotate(-90deg);
}

/* Hide the list when collapsed */
#divRelaySection.collapsed #divRelayList,
#divBlossomSection.collapsed #divBlossomList {
  display: none;
}
```

#### JS — Generalized Toggle Helper

Rather than duplicating toggle logic in each UI module, add a small shared function. This can live in a new utility or be inlined in each module. The simplest approach: add it directly to `relay-ui.mjs` and `blossom-ui.mjs` since they already own their respective sections.

**In `relay-ui.mjs`** — add to [`initSidenavRelaySection()`](www/js/relay-ui.mjs:543):

```javascript
export function initSidenavRelaySection() {
  // ... existing code ...
  
  // Collapsible section toggle
  const title = document.getElementById('divRelaySectionTitle');
  const section = document.getElementById('divRelaySection');
  if (title && section) {
    // Restore collapsed state
    if (localStorage.getItem('sidenav-relay-collapsed') === 'true') {
      section.classList.add('collapsed');
    }
    title.addEventListener('click', () => {
      section.classList.toggle('collapsed');
      localStorage.setItem('sidenav-relay-collapsed', section.classList.contains('collapsed'));
    });
  }
}
```

**In `blossom-ui.mjs`** — add to [`initBlossomSection()`](www/js/blossom-ui.mjs:242):

```javascript
export async function initBlossomSection() {
  // ... existing code ...
  
  // Collapsible section toggle
  const title = document.getElementById('divBlossomSectionTitle');
  const section = document.getElementById('divBlossomSection');
  if (title && section) {
    if (localStorage.getItem('sidenav-blossom-collapsed') === 'true') {
      section.classList.add('collapsed');
    }
    title.addEventListener('click', () => {
      section.classList.toggle('collapsed');
      localStorage.setItem('sidenav-blossom-collapsed', section.classList.contains('collapsed'));
    });
  }
}
```

---

## Feature 2: Generalized Sidenav Section Pattern

### Analysis

The relay and blossom sections already share a very similar structure:
- A container div with `border-top`, `padding`, `flex-shrink: 0`
- A title div with `pageKanji` font
- A list div with `font-size: 70%`

The CSS in [`client.css`](www/css/client.css:465) already groups them:

```css
#divRelaySection,
#divBlossomSection { ... }

#divRelaySectionTitle,
#divBlossomSectionTitle { ... }

#divRelayList,
#divBlossomList { ... }
```

### Approach: CSS Class-Based Generalization

Convert the ID-based styles to also support class-based selectors. This makes it trivial to add future sections without touching `client.css` again.

Add class-based equivalents alongside the existing ID selectors:

```css
/* Generalized sidenav section classes */
.sidenavSection {
  flex-shrink: 0;
  background-color: var(--secondary-color);
  border-top: 3px solid var(--primary-color);
  padding: 10px;
}

.sidenavSectionTitle {
  font-family: pageKanji;
  font-size: 100%;
  font-weight: bold;
  margin-bottom: 10px;
  padding-bottom: 5px;
  border-bottom: 1px solid var(--muted-color);
  cursor: pointer;
  user-select: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.sidenavSectionTitle::after {
  content: '▾';
  font-size: 80%;
  transition: transform 0.3s ease;
  color: var(--muted-color);
}

.sidenavSection.collapsed .sidenavSectionTitle::after {
  transform: rotate(-90deg);
}

.sidenavSectionList {
  font-size: 70%;
  font-family: var(--font-family);
}

.sidenavSection.collapsed .sidenavSectionList {
  display: none;
}
```

Then update the existing HTML to add these classes alongside the IDs:

```html
<div id="divRelaySection" class="sidenavSection">
  <div id="divRelaySectionTitle" class="sidenavSectionTitle">リレー</div>
  <div id="divRelayList" class="sidenavSectionList">Loading relays...</div>
</div>

<div id="divBlossomSection" class="sidenavSection">
  <div id="divBlossomSectionTitle" class="sidenavSectionTitle">ブロッサム</div>
  <div id="divBlossomList" class="sidenavSectionList">Loading blossom servers...</div>
</div>
```

The existing ID-based selectors remain for backward compatibility. The class-based selectors provide the collapsible behavior and make future sections easy.

### Why Not a Full JS Component?

The relay and blossom sections have fundamentally different internal rendering logic (relay-ui.mjs creates HamburgerMorphing indicators with relay-specific shapes; blossom-ui.mjs creates health indicators with blob counts). Abstracting the *content rendering* into a shared component would add complexity without real benefit. The shared parts are:

1. **CSS structure** — handled by the `.sidenavSection` classes above
2. **Collapse toggle** — a 5-line pattern repeated in each module's init function
3. **HTML skeleton** — the same 3-div pattern in each page

This is the right level of abstraction: shared CSS classes + a simple collapse pattern, with each module owning its own content rendering.

---

## Feature 3: Drag-and-Drop Upload onto Blossom Server Names

### Behavior

- User drags a file from their desktop (or from the blob grid) over a server name in the `#divBlossomList`
- The server name highlights to indicate it is a valid drop target
- On drop, the file is uploaded to *that specific server only* (not all upload servers)
- Visual feedback during upload (server name shows uploading state)

### Implementation

#### Changes to `blossom-ui.mjs`

The [`renderBlossomSection()`](www/js/blossom-ui.mjs:160) function creates server rows. We need to:

1. Add drag-and-drop event listeners to each server row's name element
2. Emit a custom event when a file is dropped, carrying the server URL and file data
3. Add CSS classes for drag-over visual feedback

**In `renderBlossomSection()`** — modify the row creation to add drag-drop handlers:

```javascript
// Make the server name a drop target
name.classList.add('blossomServerDropTarget');

name.addEventListener('dragover', (e) => {
  e.preventDefault();
  e.stopPropagation();
  name.classList.add('dragover');
});

name.addEventListener('dragleave', (e) => {
  e.stopPropagation();
  name.classList.remove('dragover');
});

name.addEventListener('drop', (e) => {
  e.preventDefault();
  e.stopPropagation();
  name.classList.remove('dragover');
  
  const files = Array.from(e.dataTransfer.files);
  if (files.length > 0) {
    // Dispatch custom event with server URL and files
    window.dispatchEvent(new CustomEvent('blossomServerDrop', {
      detail: { serverUrl: server.url, files }
    }));
  }
});
```

#### CSS for Drop Target Feedback

Add to `client.css`:

```css
.blossomServerDropTarget {
  transition: color 0.2s, border-color 0.2s;
  padding: 2px 4px;
  border-radius: var(--border-radius);
}

.blossomServerDropTarget.dragover {
  color: var(--accent-color) !important;
  font-weight: bold !important;
  outline: 1px dashed var(--accent-color);
  outline-offset: 2px;
}
```

#### Changes to `blobs.html`

In [`blobs.html`](www/blobs.html), listen for the custom event and perform the upload to the specific server:

```javascript
// Listen for drag-drop uploads to specific blossom servers
window.addEventListener('blossomServerDrop', async (event) => {
  const { serverUrl, files } = event.detail;
  console.log(`[blobs.html] Uploading ${files.length} file(s) to ${serverUrl}`);
  
  for (const file of files) {
    try {
      await uploadBlobToServer(file, serverUrl);
    } catch (error) {
      console.error(`Failed to upload ${file.name} to ${serverUrl}:`, error);
    }
  }
});
```

Add a new `uploadBlobToServer()` function that is a simplified version of `uploadBlob()` targeting a single server:

```javascript
const uploadBlobToServer = async (file, serverUrl) => {
  const sha256 = await calculateSHA256(file);
  
  const event = {
    kind: 24242,
    created_at: Math.floor(Date.now() / 1000),
    tags: [
      ['t', 'upload'],
      ['x', sha256],
      ['size', String(file.size)],
      ['expiration', String(Math.floor(Date.now() / 1000) + 3600)]
    ],
    content: `Upload ${file.name}`,
    pubkey: currentPubkey
  };
  
  const signedEvent = await window.nostr.signEvent(event);
  event.id = signedEvent.id;
  event.sig = signedEvent.sig;
  const authToken = btoa(JSON.stringify(event));
  
  const response = await fetch(`${serverUrl}/upload`, {
    method: 'PUT',
    headers: {
      'Content-Type': file.type || 'application/octet-stream',
      'Authorization': `Nostr ${authToken}`
    },
    body: file
  });
  
  if (response.ok) {
    const blobDescriptor = await response.json();
    USER_BLOBS.unshift(blobDescriptor);
    
    // Update server blob maps
    if (!window.SERVER_BLOB_MAPS[serverUrl]) {
      window.SERVER_BLOB_MAPS[serverUrl] = [];
    }
    if (!window.SERVER_BLOB_MAPS[serverUrl].includes(sha256)) {
      window.SERVER_BLOB_MAPS[serverUrl].push(sha256);
    }
    
    // Refresh UI
    if (BLOB_VIEW_MODE === 'list') updateBlobTable();
    else updateBlobGrid();
    
    showSuccess(`Uploaded ${file.name} to ${serverUrl}`);
    return blobDescriptor;
  } else {
    throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
  }
};
```

---

## Feature 4: Extract `blossom-api.mjs` — Shared Upload/Delete Module

### Problem

Currently all blossom upload/delete/auth/hash logic lives directly in `blobs.html` as page-level functions. Other pages (post.html for image uploads, msg.html for attachments, music.html for album art, etc.) will need the same capabilities. Duplicating this code would be unmaintainable.

### Solution

Create `www/js/blossom-api.mjs` — a shared module containing the operational side of blossom interactions. This complements `blossom-ui.mjs` which handles the display side.

```mermaid
graph LR
    subgraph Shared Modules
        A[blossom-ui.mjs] -->|getBlossomServers| B[Server list + health state]
        C[blossom-api.mjs] -->|upload delete mirror| D[Blossom operations]
        C -->|imports getBlossomServers| A
    end

    subgraph Pages
        E[blobs.html] -->|imports| A
        E -->|imports| C
        F[post.html - future] -->|imports| C
        G[msg.html - future] -->|imports| C
        H[music.html - future] -->|imports| C
    end
```

### Exports

| Function | Signature | Purpose |
|----------|-----------|---------|
| `uploadToServer` | `file, serverUrl => Promise of BlobDescriptor` | Upload a single file to a specific server |
| `uploadToAllServers` | `file => Promise of BlobDescriptor` | Upload to all configured upload servers + mirror to mirror servers |
| `deleteFromAllServers` | `sha256, filename? => Promise of boolean` | Delete from all healthy servers |
| `mirrorBlobToServers` | `sha256, filename? => Promise of void` | Mirror an existing blob to configured mirror servers |
| `createBlossomAuth` | `action, sha256?, content? => Promise of string` | Create and sign a BUD-02 auth event, return base64 token |
| `calculateSHA256` | `file => Promise of string` | Hash a file and return hex string |
| `listUserBlobs` | `serverUrl, pubkey, since? => Promise of Array` | List blobs for a user on a specific server |

### Internal Dependencies

- Imports `getBlossomServers()` from `blossom-ui.mjs` for server list
- Imports `getPubkey()` from `init-ndk.mjs` for current user pubkey
- Uses `window.nostr.signEvent()` for Nostr auth signing (same as current code)

### Functions Extracted from `blobs.html`

These functions move from `blobs.html` into `blossom-api.mjs`:

| Current function in blobs.html | New export in blossom-api.mjs |
|-------------------------------|-------------------------------|
| `calculateSHA256()` line 1174 | `calculateSHA256()` |
| `createAuthEvent()` line 1187 | `createBlossomAuth()` |
| `listUserBlobs()` line 1232 | `listUserBlobs()` |
| `uploadBlob()` line 1280 | `uploadToAllServers()` |
| `mirrorBlob()` line 1558 | `mirrorBlobToServers()` |
| `deleteBlob()` line 1640 | `deleteFromAllServers()` |
| *new* | `uploadToServer()` — simplified single-server upload |

### What Stays in `blobs.html`

- `USER_BLOBS` array and blob pagination state
- `refreshBlobs()` — orchestrates listing from all servers, deduplication, UI update
- `syncBlobs()` — orchestrates cross-server synchronization
- `updateBlobGrid()` / `updateBlobTable()` — UI rendering
- `window.SERVER_BLOB_MAPS` — tracking which blobs are on which servers
- Event listeners for buttons, drag-drop handler for `blossomServerDrop`

### Callback Pattern for UI Updates

`uploadToAllServers()` and `deleteFromAllServers()` currently update `USER_BLOBS` and call UI update functions directly. In the extracted module, they return results and let the calling page handle UI updates:

```javascript
// In blossom-api.mjs — returns the blob descriptor, does not touch UI
export async function uploadToAllServers(file) {
  // ... upload logic ...
  return { blobDescriptor, serverResults };
}

// In blobs.html — handles UI
const result = await uploadToAllServers(file);
if (result.blobDescriptor) {
  USER_BLOBS.unshift(result.blobDescriptor);
  updateBlobGrid();
}
```

---

## Architecture Diagram

```mermaid
graph TD
    subgraph CSS - client.css
        A[.sidenavSection] --> B[.sidenavSectionTitle]
        A --> C[.sidenavSectionList]
        A --> D[.sidenavSection.collapsed]
        E[.blossomServerDropTarget]
        E --> F[.blossomServerDropTarget.dragover]
    end

    subgraph relay-ui.mjs
        G[initSidenavRelaySection] --> H[Add collapse toggle listener]
        G --> I[Restore collapsed state from localStorage]
    end

    subgraph blossom-ui.mjs
        J[initBlossomSection] --> K[Add collapse toggle listener]
        J --> L[Restore collapsed state from localStorage]
        M[renderBlossomSection] --> N[Add dragover/drop listeners to server names]
        N --> O[Dispatch blossomServerDrop custom event]
    end

    subgraph blossom-api.mjs - NEW
        S[uploadToServer] --> T[createBlossomAuth + fetch PUT]
        U[uploadToAllServers] --> V[Local/remote server partitioning + mirror]
        W[deleteFromAllServers] --> X[Auth + DELETE to all healthy servers]
        Y[calculateSHA256] --> Z[crypto.subtle.digest]
    end

    subgraph blobs.html
        P[Listen for blossomServerDrop event]
        P --> S
        Q[refreshBlobs] --> R[listUserBlobs from blossom-api.mjs]
        AA[uploadBlob button] --> U
        BB[deleteBlob button] --> W
    end
```

---

## File Change Summary

| File | Changes |
|------|---------|
| [`www/css/client.css`](www/css/client.css) | Add `.sidenavSection`, `.sidenavSectionTitle`, `.sidenavSectionList` classes with collapse behavior; add `.blossomServerDropTarget` styles |
| [`www/js/relay-ui.mjs`](www/js/relay-ui.mjs) | Add collapse toggle logic in `initSidenavRelaySection()` |
| [`www/js/blossom-ui.mjs`](www/js/blossom-ui.mjs) | Add collapse toggle logic in `initBlossomSection()`; add drag-drop event listeners in `renderBlossomSection()` |
| **`www/js/blossom-api.mjs`** *(new)* | New shared module: `uploadToServer`, `uploadToAllServers`, `deleteFromAllServers`, `mirrorBlobToServers`, `createBlossomAuth`, `calculateSHA256`, `listUserBlobs` |
| [`www/blobs.html`](www/blobs.html) | Add section classes to HTML; import from `blossom-api.mjs`; remove extracted functions; add `blossomServerDrop` event listener; refactor upload/delete/mirror calls to use shared module |

---

## Notes

- This is prototyped on `blobs.html` only. Once validated, the collapsible section classes and HTML class additions can be rolled out to all 20+ pages.
- The drag-drop upload feature only makes sense on `blobs.html` (the event listener). The drag-drop *target* setup in `blossom-ui.mjs` is harmless on other pages — the custom event simply won't have a listener.
- The `blossomServerDrop` custom event pattern keeps `blossom-ui.mjs` decoupled from upload logic. Any page can listen for this event and handle it differently.
- `blossom-api.mjs` is designed so future pages can `import { uploadToAllServers } from './js/blossom-api.mjs'` and immediately have full blossom upload capability without duplicating any code.
- The `isLocalServer()` helper and local/remote server partitioning logic moves into `blossom-api.mjs` since it is part of the upload strategy.
