Commit Graph

13 Commits

Author SHA1 Message Date
xarmian 3bf1b60365 feat(attachments): editor image crop with aspect presets (TASK-880) (#298)
Adds a drag-to-crop modal on top of the AttachmentImage toolbar
introduced in TASK-879. The /transform endpoint already accepted
the "crop" operation shape from TASK-879 — this PR wires the editor
UI plus the supporting tests.

Editor:
  - attachment-crop-modal.ts (new): pure-DOM crop modal in the same
    style as the existing image lightbox. Returns a Promise that
    resolves to the crop rect in ORIGINAL-IMAGE pixel coordinates
    when the user clicks Apply, or null on cancel / dismiss /
    image-load failure.
    - Image fits to a centered <dialog> via flex layout; backdrop
      click and Esc both cancel cleanly.
    - Crop rectangle starts at 80% of the image, centered. Body is
      a "move" handle; four corner handles resize.
    - Aspect presets: Free, 1:1, 4:3, 16:9. Preset clicks snap the
      current rect to the new ratio while preserving its center;
      subsequent corner drags clamp to the locked ratio.
    - Pointer events (touch + mouse for free) with setPointerCapture
      so drag continues even if the cursor leaves the handle.
    - Coordinate translation: rect in preview-pixel space →
      naturalWidth / offsetWidth scale → original-image pixel
      space. Result is clamped to natural bounds so a fractional-
      rounding overrun doesn't push the rect off-image.

  - attachment-image.ts: extracts swapNodeUuid() helper from
    runRotate so runCrop can share the setNodeMarkup +
    invalidate-old-metadata flow. The toolbar gains a fourth
    button (⌶ Crop…) that opens the modal pointed at the original
    variant. Per-format gating (refreshToolbarState) treats the
    crop button identically to the rotate trio — both go through
    /transform, so a libvips-only format (e.g. WebP on the pure-Go
    build) disables the whole toolbar with the same explanatory
    tooltip.

  - app.css: full styling for the crop modal — header with aspect
    toolbar, image stage with shadow-cutout overlay around the
    crop rect, four corner handles, footer with Cancel + Apply.
    Uses the existing CSS-variable palette so light/dark mode
    track automatically.

Server tests (3 new):
  - TestTransform_CropProducesNewBlobAtRectDimensions: end-to-end
    PNG crop, verify the response dimensions AND that the served
    bytes decode at the same dimensions (guards against an
    encode-pipeline off-by-one).
  - TestTransform_CropClipsToImageBounds: rect that extends past
    the image boundary clips rather than 400ing — the editor's
    rounding can produce rect+1px past natural width/height in
    rare fractional-scale cases, and the processor's Crop
    intersects with image bounds for exactly this reason.
  - TestTransform_CropRejectsBadRect: missing rect, zero width,
    negative xy, rect entirely outside → 400.

Parent: PLAN-866. Closes the editor-side image-tools track on top of
TASK-878 (Processor) and TASK-879 (rotate / transform endpoint).
2026-04-29 15:02:07 -04:00
xarmian f93b0ee4ce feat(attachments): server-side rotate transform + editor toolbar (TASK-879) (#297)
* feat(attachments): server-side rotate transform + editor toolbar (TASK-879)

Adds the POST /transform endpoint and the editor's rotate toolbar
on top of TASK-878's Processor abstraction. Rotation produces a NEW
content-addressed attachment row; the editor swaps the AttachmentImage
node's UUID via setNodeMarkup and the original ages into orphan GC.

Server (internal/server/handlers_attachments_transform.go):
  POST /api/v1/workspaces/{slug}/attachments/{id}/transform with
  body {operation, ...params}. Phase 1 wires the "rotate" branch
  (degrees: 90 | 180 | 270 only — pixel-exact reorderings, no
  resampling, matches what the editor emits). The "crop" branch
  is parsed and validated but the transform path is wired in
  TASK-880; defining the wire format here keeps both PRs aligned.

  Auth: editor+ on the workspace. Cross-workspace and deleted-parent
  probes return 404 (not 403) so the new endpoint can't become a
  side-channel for ID enumeration. Unsupported MIME → 415; oversized
  image → 413; bad params → 400; missing processor → 503. Output
  format follows the same PNG-stays-PNG / else-JPEG policy as the
  thumbnail pipeline so derived blobs deduplicate cleanly.

  Tests (10): rotate 90 swaps WxH, rotate 180 keeps WxH, bad degrees
  → 400, unknown op → 400, non-existent attachment → 404, cross-
  workspace → 404, no processor → 503, derived row has fresh hash +
  inherits workspace/uploader/item, served bytes decode at the new
  dimensions, deleted-parent → 404.

Web client (web/src/lib/api/client.ts + types):
  api.attachments.transform(slug, id, payload) hits the new endpoint
  with a discriminated AttachmentTransformRequest type. New
  api.server.capabilities() reads the public capability profile
  added in TASK-878. Both surface PadApiError on failure so the
  editor can show actionable messages.

Editor:
  - attachment-metadata.ts (new): shared HEAD-probe cache extracted
    from attachment-chip.ts so AttachmentImage's toolbar can probe
    the image's MIME with the same zero-extra-network-cost
    deduplication. Adds mimeToFormat() — maps MIME to the canonical
    short format name the server's Capabilities reports.

  - attachment-chip.ts: swapped to use the shared cache. Behavior
    unchanged.

  - attachment-image.ts: NodeView now wraps the <img> in a
    positioned <span> and lazy-builds a 3-button rotate toolbar
    (rotate left 90°, rotate 180°, rotate right 90°). selectNode
    shows it; deselectNode hides it. On click → calls
    options.transform → setNodeMarkup with the returned UUID at
    getPos(); cached metadata for the OLD UUID is invalidated.

    Per-button gating via refreshToolbarState: empty
    supportedFormats list (degraded build) → all disabled with a
    "this build doesn't have image processing" tooltip. MIME
    probed and not in supportedFormats → disabled with a format-
    specific tooltip ("Image editing for image/webp requires
    libvips"). Otherwise → enabled with the action tooltip.

  - Editor.svelte: configures AttachmentImage with the workspace
    slug, the supportedFormats list (initially empty, populated
    asynchronously after capabilities resolve), and the transform
    callback wired to api.attachments.transform. Errors surface via
    console.error + window.alert — same fallback as the upload
    plugin until a centralized toast system lands.

  - app.css: wrapper + toolbar styles. Toolbar pinned top-right with
    absolute positioning; selected-state ring on the image; disabled
    button state at 40% opacity.

Parent: PLAN-866. Unblocks TASK-880 (crop) — the /transform endpoint
already accepts the crop op shape, the editor's toolbar pattern is
the same, and the supportedFormats gating composes cleanly.

* fix(attachments): rotate attribution + toolbar refresh per Codex review (round 1)

Two findings from the round-1 Codex review:

1. The transform handler set UploadedBy = currentUserOrSystem(r),
   contradicting the comment that said "inherit attribution from
   the parent" and creating an audit-attribution drift whenever a
   user rotated/cropped someone else's upload. Inherit
   parent.UploadedBy instead — same policy as the thumbnail
   pipeline. Added TestTransform_DerivedRowInheritsUploadedByFromParent
   to lock in the contract. Removed the now-unused
   currentUserOrSystem helper.

2. The rotate toolbar's per-format gating could permanently stick
   in "all-disabled" state if the user selected an image before
   the async capabilities fetch resolved. supportedFormats started
   as [] (matching "no processor"), refreshToolbarState ran once
   in that state, and the later mutation of ext.options.
   supportedFormats had no observer to push the change down to
   already-open toolbar DOM. Fix: module-level toolbarRefreshers
   set, populated by each NodeView at ensureToolbar() and torn
   down in destroy(); a new notifyAttachmentImageCapabilitiesChanged()
   export iterates the set and re-runs each toolbar's refresh
   hook. Editor.svelte calls it after the capabilities fetch
   updates ext.options.supportedFormats, so any toolbar opened
   during the in-flight request snaps to its correct state the
   moment caps arrive.

Verification: go test ./internal/server -run TestTransform passes
(11 cases now); npm run check passes with the existing 6 warnings.
2026-04-29 14:54:48 -04:00
xarmian 7e5b15722f feat(attachments): editor paste + drag-drop upload plugin (TASK-875) (#294)
Tiptap extension that intercepts paste/drop events with files, uploads
each through the attachment API, and replaces the placeholder with the
right node (attachmentImage for image MIMEs, attachmentChip for
everything else) at the position the user dropped.

The plugin's flow:
  1. Detect file payloads in clipboardData.items / dataTransfer.files
     (skip the event when there are none, so plain text paste / cursor
     drag still go through tiptap-markdown's transformPastedText path).
  2. Insert a position-tracked placeholder at the paste/drop position
     via a setMeta transaction. Placeholders are widget decorations
     (zero document width) so they never enter serialized markdown
     even if the user navigates away mid-upload.
  3. Race the network. The plugin's apply() handler maps every
     placeholder position through every intervening transaction, so
     continued editing doesn't strand the spinner.
  4. On success: schema-aware replacement — attachmentImage for
     category=image, attachmentChip otherwise. The placeholder is
     removed in the same transaction.
  5. On error: remove the placeholder and surface the failure via the
     injected onError callback. The upload bytes that did land become
     orphans; orphan-GC reclaims them after the grace period.
  6. If the placeholder has been deleted before the upload completes
     (user cancelled, navigated away, etc.) the upload is dropped
     silently — same orphan-GC outcome.

Multiple files in a single drop fan out as concurrent uploads; each
gets its own placeholder and replaces independently as the network
completes.

Editor.svelte wires:
  - upload  -> api.attachments.upload(workspaceSlug, file)
              (rejects with a clear message when no workspace context)
  - onError -> console.error + window.alert as a minimal fallback
              until a centralized toast system lands.

Styles (app.css) cover the placeholder bubble (dashed border, faded
colour) and a CSS spinner — kept inline via decoration widget DOM so
ProseMirror's selection ignores it (ignoreSelection: true).

Parent: PLAN-866. Closes the editor-input flow on top of TASK-874
(markdown resolver), TASK-876 (image node), TASK-877 (chip node).
2026-04-29 13:41:05 -04:00
xarmian 934794b606 feat(attachments): editor file chip node (TASK-877) (#293)
* feat(attachments): editor file chip node (TASK-877)

Custom Tiptap node `attachmentChip` for non-image `pad-attachment:UUID`
references — Notion-style chip rendering with type icon, filename, and
human-readable size.

Node shape:
  - uuid:     string — the attachments-row UUID
  - filename: string — display name; preserved across save/reload

Markdown round-trip:
  - Serialize: `[filename](pad-attachment:UUID)` — same standard link
    syntax the markdown resolver in TASK-874 understands. `]` and `\`
    in the filename are escaped to keep the link label balanced.
  - Parse: markdown-it's link token produces
    `<a href="pad-attachment:UUID">filename</a>`. Our parseHTML rule
    `a[href^="pad-attachment:"]` runs at priority 1000 to beat
    SafeLink's default mark rule (priority 50), so attachment refs
    become a chip Node instead of a Link Mark on plain text.

Editor display (NodeView):
  - <a class="file-chip"> with icon + name + optional size span
  - Icon: filename-extension heuristic on first paint, upgraded to a
    MIME-based icon once a single HEAD request resolves the canonical
    Content-Type. The HEAD goes against the existing GET handler — no
    new API endpoint required, and Go's net/http strips the body
    automatically for HEAD.
  - Size: rendered from Content-Length once HEAD resolves; hidden
    until then (CSS `:empty { display: none }`).
  - Module-level Promise cache keyed by `${ws}:${uuid}` deduplicates
    repeated chips for the same attachment and survives undo/redo
    without re-fetching.
  - target=_blank + download attribute so a click opens / saves the
    file with its canonical filename.
  - atom: true → Backspace/Delete remove the chip as a single unit.

Styles in app.css (not Editor.svelte) so they reach the read-only
markdown render path too — TASK-874's Go and TS resolvers emit the
same `.file-chip` / `.file-chip-icon` / `.file-chip-name` /
`.file-chip-size` markup.

Parent: PLAN-866. Unblocks TASK-875 — the upload plugin needs a chip
node to insert on successful non-image uploads.

* fix(attachments): chip HEAD route + chip click handler per Codex review (round 1)

Two findings from the round-1 Codex review:

1. chi router does not auto-route HEAD to GET handlers, so the chip's
   metadata HEAD probe was returning 405 and chip size + MIME-refined
   icons never loaded. Fix: register HEAD on the same path/handler;
   http.ServeContent already strips the body for HEAD on the seekable
   path, and the streaming fallback short-circuits before io.Copy so
   future S3-style backends don't burn GetObject bandwidth on HEAD.

   Tests added: HEAD returns 200 with Content-Type + Content-Length
   and an empty body; HEAD cross-workspace returns 404 (not 403) so
   the new endpoint can't become a side-channel for ID enumeration.

2. Editor.svelte installs a global anchor-click suppressor that
   preventDefaults every <a> inside the editor, so the chip looked
   clickable but did nothing in edit mode. Fix: the chip's NodeView
   now attaches an explicit click handler that calls window.open with
   the download URL and stops propagation before the global handler
   runs. Mirrors the AttachmentImage lightbox click pattern.
2026-04-29 13:35:15 -04:00
xarmian f1ce9ca24a feat(attachments): editor inline image node (TASK-876) (#292)
Custom Tiptap node for inline `pad-attachment:UUID` image references.
Stores the attachment UUID (not a backend URL) so item content survives
a storage-backend migration untouched. See DOC-865.

Node shape:
  - uuid: string  — the attachments-row UUID (required)
  - alt:  string  — preserved across save/reload for accessibility

Markdown round-trip:
  - Serialize:  ![alt](pad-attachment:UUID)  via tiptap-markdown's
    addStorage.markdown.serialize, with [/] in the alt text escaped so
    brackets stay balanced.
  - Parse: markdown-it's default image token already produces
    <img src="pad-attachment:UUID" alt="…">, captured by parseHTML
    rule img[src^="pad-attachment:"]. The alternate parseHTML rule
    img[data-attachment-id] catches editor-rendered HTML on copy/paste.

Editor display:
  - addNodeView renders <img class="attachment-image" loading="lazy">
    pointing at /api/v1/workspaces/{ws}/attachments/{id}?variant=thumb-md
    via an injected getDownloadUrl callback (Editor.svelte resolves the
    workspace slug from page.params at mount time, falling back to the
    workspace store).
  - Single-click opens a native <dialog> lightbox with the original-
    resolution variant; multi-click events fall through so users can
    drag-select around the image.
  - atom: true means Backspace/Delete remove the image as a single
    unit and the cursor never lands inside the node.

Lightbox styles live in app.css because the <dialog> is appended to
document.body, outside Editor.svelte's scoped style block.

The configure() default returns the literal `pad-attachment:UUID`
href — sufficient for markdown round-trip in headless / SSR contexts
and a clearly-broken render in any environment that hasn't wired the
URL builder, which is the right signal to fix.

Parent: PLAN-866. Unblocks TASK-875 (the upload plugin needs a node
to insert on success).
2026-04-29 13:22:13 -04:00
xarmian cf3ba5510d fix(web): drop repeating print-header, clean page-1 layout, skip empty rows (BUG-626) (#157)
* fix(web): drop repeating print-header, clean page-1 layout, skip empty rows (BUG-626)

Real-print testing after BUG-625 showed the repeating fixed-position
`.print-header` approach is fragile -- even with a generous @page top
margin, Chromium's handling of fixed elements during pagination can
overlap content on the first page, and there's no clean way to
coordinate the header with page-break behavior across browsers.

Replace the repeating header with a page-1 document header in normal
flow and simplify.

## Changes

### Template (+page.svelte)
- Remove `.print-header` entirely; drop the `workspaceStore` import
  (no longer needed in print).
- Tag non-computed field-rows with `class:print-empty={isFieldEmpty}`
  when the raw value is null / empty string / empty array. Flag at
  the template level because :empty can't see FieldEditor children.

### Styles (+page.svelte @media print)
- `.title-row` becomes a flex row: title on the left (20pt, wraps),
  item ref on the right (10pt, tabular-nums, nowrap), both aligned to
  the title's first-line baseline.
- `.meta-info` gets a 1px bottom border to separate the document
  header block from the properties card.
- `.field-row.print-empty { display: none !important; }`.
- Drop all `.print-header*` CSS (dead) and the padding/border shared
  rule between header+footer. `.print-footer` now stands alone.

### Global (app.css)
- Shrink @page top margin from 1.25in to 0.6in -- no reserved header
  strip means no clearance needed. Bottom margin stays 1in for the
  fixed footer + `@bottom-right` page number counter.

## Outcome

- No repeating header, no overlap, no workspace/collection context on
  subsequent pages (users who want it can leave browser headers
  enabled in the print dialog).
- Page 1 shows: title+ref header row, meta subtitle, border, properties
  (with empty rows skipped), body, relationships/children if present.
- Footer repeats on every page with Printed date, URL, and Page N.
- 116-line file net -16 lines smaller, app.css -2.

Verified locally via Ctrl+P preview in Chromium before committing.

* fix(web): flip print title-row order so title is left, ref is right (PR #157)

Address Codex P2: DOM order in the template is `[item-ref, title]`,
so `display: flex; justify-content: space-between` kept the ref on
the left and pushed the (flex:1) title to fill the remaining space on
the right — the opposite of the intended BUG-626 header layout.

Use the flex `order` property to reverse only the visual sequence in
print, keeping the template DOM untouched. `.title` gets `order: 1`,
`.item-ref` gets `order: 2` + `margin-left: auto` so the ref sits
baseline-aligned at the right edge and the title claims everything
to its left.
2026-04-18 19:04:32 -04:00
xarmian 7c29413685 fix(web): print title overlap, page number, and select chevrons (BUG-625) (#156)
* fix(web): print title overlap, page number, and select chevrons (BUG-625)

Address three issues surfaced by a real Ctrl/Cmd+P test on an Idea
detail page (PLAN-620 follow-up):

1. Title cut off at top of page 1. The `@page { margin: 1.1in ... }`
   rule was declared in +page.svelte's scoped style block, but Svelte
   scoped-CSS at-rule loading meant the 0.75in default from app.css
   (TASK-621) kept winning. The fixed print header was ~0.4in tall and
   the content area started at 0.75in, but layout timing left them
   overlapping. Consolidate to a single @page rule in app.css with a
   widened `margin: 1.25in 0.6in 1in 0.6in` -- guaranteed clearance.

2. Footer showed "Page 0" on every page. `counter(page)` inside the
   ::after pseudo-element of a fixed-positioned element is captured
   once at initial layout (before pagination) and reused, so it never
   increments. Move the page number into a `@page { @bottom-right {
   content: "Page " counter(page); } }` margin-box where the counter
   evaluates correctly per page. Remove the `.print-footer-page` span
   and its `.print-page-num::after` rule from the item detail page.
   Add `padding-right: 1.2in` to the fixed footer so its content
   doesn't overlap the new margin-box page number.

3. FieldEditor selects still showed a `∨` chevron in print output --
   the chevron is an inline <svg class="select-chevron">, not the
   native UA dropdown arrow, so `appearance: none` on the button had
   no effect. Hide `.select-chevron` and `.select-dropdown` explicitly
   in the global print block.

Bonus: skip empty `.field-row`s via `.field-row:has(.field-value:empty)`
so rows like an unset "Category" don't print as a label with no value.

* fix(web): drop dead empty-field-row print rules (PR #156)

Address Codex P2 review comment on BUG-625. The `:empty`-based rules
added as a bonus to hide label-only rows (e.g. unset "Category")
never actually match in this codebase:

- Non-computed fields wrap a `<FieldEditor>` child inside `.field-value`,
  so `.field-value` always has children and is never `:empty`.
- Computed fields call `formatFieldDisplay(value)`, which returns `"—"`
  for null / empty, so `.computed-value` is never `:empty` either.

Remove the rules rather than leaving dead selectors that suggest the
behavior exists. Hiding blank rows in print is worth revisiting with a
real signal (e.g. a `data-empty` attribute or a template `{#if}`
guard), but out of scope for BUG-625 -- the title / page-number /
chevron fixes are what this PR is about.
2026-04-18 16:27:40 -04:00
xarmian f9a248f4da feat(web): print-format the item detail page (TASK-622) (#153)
* feat(web): print-format the item detail page (TASK-622)

Layer item-page print formatting on top of the base stylesheet added in
TASK-621:

- Title row renders as plain text (large, serif-friendly, no button
  affordance); issue ref prefix stays as a subtle prefix.
- Meta info (created/updated + actor) keeps as small-print subtitle.
- Properties panel becomes a definition-list block (label / value grid)
  wrapped in a light card, with form widgets stripped so the selected
  value reads as plain text.
- Content layout stacks the fields panel above the markdown body (no
  side-by-side columns in print).
- Code context section, relationships list, and child items stay.
- Comments / activity / version timeline are hidden entirely.
- Action buttons, breadcrumb, share/move/delete controls, edit-mode
  toggle, add-relationship form, save-status chip, link-delete buttons
  are all stripped.

Rendered markdown (.prose) gets a print tune-up in app.css: 11pt body,
inline URL suffix on external links (skipped for wiki-links and fragment
links), break-inside guards on code / images / tables, light-palette
overrides for code blocks and blockquotes. Editor overlays (bubble menu,
link popover, slash menu, mobile toolbar, table toolbar, editor
toolbar) are hidden in print.

Parent: PLAN-620.

* fix(web): keep relationship status chips + print title during edit mode (PR #153)

Address Codex P2 review comments on TASK-622:

- Relationship rows: previously hid the entire `.link-row-actions`
  wrapper, which silently dropped the `.link-status` chip alongside the
  destructive delete button. Hide only `.link-delete-btn` so the status
  stays visible in print.

- Title during inline edit: the screen renders either a `.title` button
  (read) or a `.title-input` textarea (edit); previous rules displayed
  the button and hid the textarea, so printing while editing produced a
  title-less page. Apply the same print typography to both, turning the
  textarea into a non-interactive, borderless plain-text heading.

* fix(web): preserve checkbox field state in print output (PR #153)

Address Codex P2 review comment on TASK-622. The form-widget strip rule
`.field-value button { border: none; background: transparent; }` killed
the visual state of `.toggle` (the checkbox field's switch button),
since it renders state purely via styling — no text label. The printed
page would lose the on/off signal entirely.

Exempt `.toggle` from the strip rule via `:not(.toggle)` and add a
dedicated print style that renders the toggle as an outlined 11pt box;
when the field is on, overlay a check mark via `::after`. The toggle-knob
is hidden (it's the sliding switch visual, not useful in print).

* fix(web): print URL suffixes for SafeLink + print raw markdown legibly (PR #153)

Address Codex P2 review comments on TASK-622:

- Rich-editor links (Tiptap SafeLink extension) render with `data-href`
  instead of `href`, so the print suffix rule `.prose a[href]::after`
  never fired for the main document body. Add a parallel selector
  `.prose a[data-href]::after { content: " (" attr(data-href) ")"; }`
  plus matching skips for internal data-href wiki-links.

- The Markdown editor's raw textarea had no print styling. Printing
  while the Markdown tab was active either clipped the textarea to its
  screen height or rendered with dark-theme chrome. Add a @media print
  block to `RawMarkdownEditor.svelte` that flattens the textarea into a
  plain monospace flow: no border, no background, auto height, visible
  overflow, page-break-inside: auto. Content prints as markdown source
  -- not ideal, but readable and content-preserving.

* fix(web): hoist FieldEditor print strip rules to global scope (PR #153)

Address Codex P2: the `.field-value select / input / button / .toggle`
print overrides were defined inside the item detail page's scoped style
block. Svelte scoped selectors don't cross component boundaries, so the
form widgets rendered inside `FieldEditor` kept their interactive
styling in print preview -- selects rendered with their screen chrome,
toggles disappeared, etc.

Move these rules into app.css's @media print block (which applies
globally) and leave a note in +page.svelte explaining why. The
`.assignment-select` rule stays in +page.svelte because those selects
are inline in this template and correctly scoped.
2026-04-18 15:39:53 -04:00
xarmian e81da7a24f feat(web): add base @media print stylesheet for workspace layout (TASK-621) (#152)
Tune Ctrl/Cmd+P output so Pad pages can be saved as clean PDFs. This is the
first of four tasks under PLAN-620 (Print-friendly item detail pages) and
handles the layout-level chrome: hides the sidebar, top bar, floating expand
toggles, toasts, command palette, modals, and any [data-print-hide] opt-in
element; unlocks the 100vh / overflow:hidden app shell so content flows
across pages; forces a light color palette regardless of theme; strips
shadows and background images; sets a default 0.75in @page margin.

Item-level formatting (title, properties, markdown body), the rendered
print header / footer, and the child-item checklist ship in TASK-622,
TASK-623, and TASK-624 respectively.

Parent: PLAN-620.
2026-04-18 14:59:08 -04:00
xarmian 1d26c2b542 feat: add workspace top bar with drag-to-reorder (#80)
* feat: add workspace top bar with drag-to-reorder

Replace the sidebar WorkspaceSwitcher dropdown with a dedicated top bar
that provides fast workspace switching and a user menu.

Desktop:
- Horizontal bar above sidebar + content with workspace icons (colored
  first-letter circles) and names as real <a> links
- Drag-and-drop reorder via svelte-dnd-action
- User avatar on right with dropdown (settings, theme toggle, sign out)
- "+" button to create new workspaces

Mobile:
- Full-width fixed bar at top when sidebar opens (above sidebar/backdrop)
- Tap workspace to navigate and close sidebar
- Reorder button opens full-screen vertical list with drag handles
- Sidebar starts below the top bar with adjusted positioning

Backend:
- Migration 028: add sort_order to workspace_members (per-user ordering)
- GET /workspaces now returns workspaces in user's sort order
- PUT /workspaces/reorder endpoint for persisting order

Sidebar simplified:
- Removed WorkspaceSwitcher component, user section, theme toggle
- Theme initialization moved to root layout
- Cleaner footer with search, settings, and notification bell

Implements IDEA-129, relates to IDEA-126.

* fix: address codex review findings (P1+P2)

- Remove unsupported `direction` option from svelte-dnd-action dndzone
- Add Postgres migration 008 for workspace_members.sort_order
- Handle sql.ErrNoRows gracefully in reorder endpoint for admins who
  aren't members of all workspaces
- Restore mobile sign-out: add user name + logout button to sidebar
  footer on mobile (was only in desktop TopBar user menu)
2026-04-10 01:22:49 -04:00
xarmian a8059c5a0f Implement 8 ideas from the idea board
Quick wins:
- IDEA-31: URL autolink + link popover in editor (SafeLink with data-href
  prevents mobile navigation, popover shows open/edit/remove actions)
- IDEA-36: Focus title on new item creation, Enter moves to editor
- IDEA-38: Add `pad link` CLI command to link directory to existing workspace
- IDEA-34: Show checklist progress bar on item cards (parses markdown checkboxes)
- IDEA-28: Workspace rename (already existed in settings)

Medium effort:
- IDEA-33: Drag-and-drop task reordering in Phase documents via svelte-dnd-action
- IDEA-26: Archive collections (frontend wiring — backend already supported soft delete)
- IDEA-29: Archive workspaces with danger zone confirmation in settings
- IDEA-37: Raw markdown editor toggle + inline Mermaid diagram rendering
  (NodeView with ignoreMutation to prevent ProseMirror re-parse loops)
2026-03-27 23:41:20 +00:00
xarmian 7ba69abb88 Misc improvements: CLI field summaries, editor enhancements, CI and UI polish
Show field summary after create/update CLI commands. Make svelte-check
blocking in CI. Improve editor block handling, field editor layout,
conventions page, and minor UI consistency fixes across pages.
2026-03-27 01:13:07 +00:00
xarmian 81579847c6 Initial release
Pad — project management for developers and AI agents.
Single Go binary with embedded SvelteKit web UI, SQLite storage,
CLI, and Claude Code /pad skill integration.

https://getpad.dev
2026-03-26 01:52:36 +00:00