| new file mode 100644 |
| index 0000000..54e3c84 |
| --- /dev/null |
| +++ b/web/static/apps-disabled/notepad.js |
| @@ -0,0 +1,396 @@ |
| +import { defineApp, addItem, updateItem, removeItem } from "../apps.js"; |
| + |
| +export const app = defineApp({ |
| + name: "notepad", |
| + title: "Notepad", |
| + glyph: "π", |
| + places: ["you"], |
| + |
| + initialState: () => ({ |
| + items: [], |
| + selected: null, |
| + }), |
| + |
| + events: { |
| + "notepad.created": (s, e) => { |
| + if (!e.id || typeof e.text !== "string") return s; |
| + return { |
| + ...s, |
| + items: addItem(s.items, { |
| + id: e.id, |
| + title: "", |
| + content: e.text, |
| + ts: e.ts || 0, |
| + }), |
| + selected: e.id, |
| + }; |
| + }, |
| + "notepad.updated": (s, e) => { |
| + if (!e.id) return s; |
| + const patch = {}; |
| + if (typeof e.title === "string") patch.title = e.title; |
| + if (typeof e.content === "string") patch.content = e.content; |
| + if (Object.keys(patch).length === 0) return s; |
| + return { |
| + ...s, |
| + items: updateItem(s.items, e.id, patch), |
| + }; |
| + }, |
| + "notepad.deleted": (s, e) => { |
| + if (!e.id) return s; |
| + return { |
| + ...s, |
| + items: removeItem(s.items, e.id), |
| + selected: s.selected === e.id ? null : s.selected, |
| + }; |
| + }, |
| + "notepad.select": (s, e) => { |
| + if (!e.id) return s; |
| + return { ...s, selected: e.id }; |
| + }, |
| + "notepad.deselect": (s) => { |
| + return { ...s, selected: null }; |
| + }, |
| + }, |
| + |
| + styles: ` |
| +.notepad-container { |
| + display: flex; |
| + height: 100%; |
| + gap: 0; |
| +} |
| +.notepad-list { |
| + flex: 0 0 280px; |
| + border-right: 1px solid var(--border); |
| + overflow-y: auto; |
| + display: flex; |
| + flex-direction: column; |
| +} |
| +.notepad-detail { |
| + flex: 1; |
| + display: flex; |
| + flex-direction: column; |
| + overflow: hidden; |
| +} |
| +.notepad-empty { |
| + flex: 1; |
| + display: flex; |
| + align-items: center; |
| + justify-content: center; |
| + color: var(--muted); |
| + text-align: center; |
| + padding: 20px; |
| +} |
| +.notepad-toolbar { |
| + padding: 12px; |
| + border-bottom: 1px solid var(--border); |
| + display: flex; |
| + gap: 8px; |
| +} |
| +.notepad-toolbar button { |
| + flex: 1; |
| + padding: 8px 12px; |
| + background: var(--accent); |
| + color: var(--accent-ink); |
| + border: none; |
| + border-radius: var(--radius); |
| + cursor: var(--ui-cursor, pointer); |
| + font-weight: 600; |
| +} |
| +.notepad-toolbar button:hover { |
| + opacity: 0.9; |
| +} |
| +.notepad-list-item { |
| + padding: 12px; |
| + border-bottom: 1px solid var(--border); |
| + cursor: var(--ui-cursor, pointer); |
| + transition: background 0.15s; |
| +} |
| +.notepad-list-item:hover { |
| + background: var(--panel); |
| +} |
| +.notepad-list-item.active { |
| + background: var(--accent); |
| + color: var(--accent-ink); |
| +} |
| +.notepad-list-item-title { |
| + font-weight: 600; |
| + margin-bottom: 4px; |
| + overflow-wrap: anywhere; |
| + max-height: 1.2em; |
| + overflow: hidden; |
| + text-overflow: ellipsis; |
| +} |
| +.notepad-list-item-preview { |
| + font-size: 12px; |
| + opacity: 0.7; |
| + overflow-wrap: anywhere; |
| + max-height: 2.4em; |
| + overflow: hidden; |
| + text-overflow: ellipsis; |
| + white-space: pre-wrap; |
| +} |
| +.notepad-detail-header { |
| + padding: 12px; |
| + border-bottom: 1px solid var(--border); |
| + display: flex; |
| + gap: 8px; |
| + align-items: center; |
| +} |
| +.notepad-detail-header button { |
| + padding: 6px 12px; |
| + background: var(--panel); |
| + border: 1px solid var(--border); |
| + border-radius: var(--radius); |
| + cursor: var(--ui-cursor, pointer); |
| + color: var(--ink); |
| +} |
| +.notepad-detail-header button:hover { |
| + background: var(--border); |
| +} |
| +.notepad-detail-header button.back { |
| + flex: 0 0 auto; |
| +} |
| +.notepad-detail-content { |
| + flex: 1; |
| + display: flex; |
| + flex-direction: column; |
| + overflow: hidden; |
| + padding: 12px; |
| + gap: 12px; |
| +} |
| +.notepad-detail-title { |
| + padding: 8px 10px; |
| + border: 1px solid var(--border); |
| + border-radius: var(--radius); |
| + background: var(--panel); |
| + color: var(--ink); |
| + font: inherit; |
| + resize: none; |
| + overflow-y: auto; |
| + min-height: 32px; |
| +} |
| +.notepad-detail-title:focus { |
| + outline: none; |
| + border-color: var(--accent); |
| + box-shadow: 0 0 0 2px var(--accent); |
| +} |
| +.notepad-detail-body { |
| + flex: 1; |
| + display: flex; |
| + flex-direction: column; |
| + overflow: hidden; |
| +} |
| +.notepad-detail-textarea { |
| + flex: 1; |
| + padding: 10px; |
| + border: 1px solid var(--border); |
| + border-radius: var(--radius); |
| + background: var(--panel); |
| + color: var(--ink); |
| + font: inherit; |
| + resize: none; |
| + overflow-y: auto; |
| +} |
| +.notepad-detail-textarea:focus { |
| + outline: none; |
| + border-color: var(--accent); |
| + box-shadow: 0 0 0 2px var(--accent); |
| +} |
| +.notepad-detail-footer { |
| + display: flex; |
| + gap: 8px; |
| +} |
| +.notepad-detail-footer button { |
| + flex: 1; |
| + padding: 8px 12px; |
| + background: var(--danger); |
| + color: white; |
| + border: none; |
| + border-radius: var(--radius); |
| + cursor: var(--ui-cursor, pointer); |
| +} |
| +.notepad-detail-footer button:hover { |
| + opacity: 0.9; |
| +} |
| +@media (max-width: 640px) { |
| + .notepad-container { |
| + flex-direction: column; |
| + } |
| + .notepad-list { |
| + flex: 0 0 auto; |
| + border-right: none; |
| + border-bottom: 1px solid var(--border); |
| + } |
| + .notepad-detail { |
| + display: none; |
| + } |
| + .notepad-detail.mobile-show { |
| + display: flex; |
| + } |
| + .notepad-list.mobile-hide { |
| + display: none; |
| + } |
| +} |
| +`, |
| + |
| + render(state, el, ctx) { |
| + const s = state || { items: [], selected: null }; |
| + const items = s.items || []; |
| + const selected = s.selected; |
| + |
| + const isMobile = window.innerWidth <= 640; |
| + const isDetail = !!selected; |
| + |
| + const container = document.createElement("div"); |
| + container.className = "notepad-container"; |
| + |
| + // ============ LIST PANE ============ |
| + const listPane = document.createElement("div"); |
| + listPane.className = "notepad-list"; |
| + if (isMobile && isDetail) listPane.classList.add("mobile-hide"); |
| + |
| + const listToolbar = document.createElement("div"); |
| + listToolbar.className = "notepad-toolbar"; |
| + const newBtn = document.createElement("button"); |
| + newBtn.textContent = "+ New"; |
| + newBtn.onclick = () => { |
| + ctx.emit({ |
| + type: "notepad.created", |
| + id: ctx.id(), |
| + text: "", |
| + }); |
| + }; |
| + listToolbar.appendChild(newBtn); |
| + listPane.appendChild(listToolbar); |
| + |
| + if (items.length === 0) { |
| + const empty = document.createElement("div"); |
| + empty.className = "notepad-empty"; |
| + empty.textContent = "No notes yet β tap + to start writing."; |
| + listPane.appendChild(empty); |
| + } else { |
| + const sorted = [...items].sort((a, b) => (b.ts || 0) - (a.ts || 0)); |
| + for (const note of sorted) { |
| + const row = document.createElement("div"); |
| + row.className = "notepad-list-item"; |
| + if (selected === note.id) row.classList.add("active"); |
| + |
| + const title = document.createElement("div"); |
| + title.className = "notepad-list-item-title"; |
| + const noteTitle = note.title && note.title.trim() ? note.title : "Untitled"; |
| + title.textContent = noteTitle; |
| + |
| + const preview = document.createElement("div"); |
| + preview.className = "notepad-list-item-preview"; |
| + const content = (note.content || "").slice(0, 100); |
| + preview.textContent = content; |
| + |
| + row.appendChild(title); |
| + row.appendChild(preview); |
| + row.onclick = () => { |
| + ctx.emit({ type: "notepad.select", id: note.id }); |
| + }; |
| + |
| + listPane.appendChild(row); |
| + } |
| + } |
| + |
| + container.appendChild(listPane); |
| + |
| + // ============ DETAIL PANE ============ |
| + const detailPane = document.createElement("div"); |
| + detailPane.className = "notepad-detail"; |
| + if (isMobile && !isDetail) detailPane.classList.add("mobile-hide"); |
| + if (isMobile && isDetail) detailPane.classList.add("mobile-show"); |
| + |
| + if (selected) { |
| + const note = items.find((n) => n.id === selected); |
| + if (note) { |
| + const header = document.createElement("div"); |
| + header.className = "notepad-detail-header"; |
| + |
| + const backBtn = document.createElement("button"); |
| + backBtn.className = "back"; |
| + backBtn.textContent = isMobile ? "Back" : "Γ"; |
| + backBtn.onclick = () => { |
| + ctx.emit({ type: "notepad.deselect" }); |
| + }; |
| + header.appendChild(backBtn); |
| + |
| + detailPane.appendChild(header); |
| + |
| + const content = document.createElement("div"); |
| + content.className = "notepad-detail-content"; |
| + |
| + const titleInput = document.createElement("textarea"); |
| + titleInput.className = "notepad-detail-title"; |
| + titleInput.placeholder = "Title"; |
| + titleInput.value = note.title || ""; |
| + let titleTimeout; |
| + titleInput.oninput = () => { |
| + clearTimeout(titleTimeout); |
| + titleTimeout = setTimeout(() => { |
| + ctx.emit({ |
| + type: "notepad.updated", |
| + id: note.id, |
| + title: titleInput.value, |
| + }); |
| + }, 500); |
| + }; |
| + content.appendChild(titleInput); |
| + |
| + const body = document.createElement("div"); |
| + body.className = "notepad-detail-body"; |
| + |
| + const textarea = document.createElement("textarea"); |
| + textarea.className = "notepad-detail-textarea"; |
| + textarea.placeholder = "Start typingβ¦"; |
| + textarea.value = note.content || ""; |
| + let contentTimeout; |
| + textarea.oninput = () => { |
| + clearTimeout(contentTimeout); |
| + contentTimeout = setTimeout(() => { |
| + ctx.emit({ |
| + type: "notepad.updated", |
| + id: note.id, |
| + content: textarea.value, |
| + }); |
| + }, 500); |
| + }; |
| + body.appendChild(textarea); |
| + |
| + const footer = document.createElement("div"); |
| + footer.className = "notepad-detail-footer"; |
| + |
| + const delBtn = document.createElement("button"); |
| + delBtn.textContent = "Delete note"; |
| + delBtn.onclick = () => { |
| + if (confirm("Really delete this note?")) { |
| + ctx.emit({ type: "notepad.deleted", id: note.id }); |
| + } |
| + }; |
| + footer.appendChild(delBtn); |
| + |
| + body.appendChild(footer); |
| + content.appendChild(body); |
| + |
| + detailPane.appendChild(content); |
| + |
| + setTimeout(() => titleInput.focus(), 0); |
| + } |
| + } else { |
| + const empty = document.createElement("div"); |
| + empty.className = "notepad-empty"; |
| + if (items.length === 0) { |
| + empty.textContent = "No notes yet"; |
| + } else { |
| + empty.textContent = "Select a note to view and edit"; |
| + } |
| + detailPane.appendChild(empty); |
| + } |
| + |
| + container.appendChild(detailPane); |
| + el.appendChild(container); |
| + }, |
| +}); |