Commit Graph

25 Commits

Author SHA1 Message Date
xarmian 74589e98bb fix(attachments): make the live chip a button, not a link (TASK-2424)
From the orchestrator's pass on TASK-2424. The chip opens the options
panel — it does not navigate — but it was still an <a href download> that
only intercepted primary click. Middle-click and aux-click therefore
opened or downloaded the file straight past the panel, which is the exact
accidental download the panel exists to prevent, and screen readers
announced a link for a control that opens a menu.

A button has no URL to activate, so the bypass cannot exist rather than
being intercepted, and it matches the strip's file tile — the same
control, now with the same semantics. renderHTML stays an <a download>:
that is the clipboard / read-only shape, where a link is honest and there
is no panel to open.

Deleted chips now use `disabled` rather than a dropped href: a disabled
button receives no click or keydown at all, where the anchor's handlers
still ran and had to swallow events. It is also blurred explicitly first,
since a chip the user is focused on when another surface deletes the row
would otherwise strand focus on an inert element. The keydown suppression
moved ahead of the deleted bail so a stray Enter can never reach
ProseMirror's keymap and split the paragraph the chip sits in.

.file-chip carries the button reset explicitly (font: inherit), since the
UA's 13.33px Arial would otherwise shrink the live chip away from the
read-only one.
2026-08-04 13:09:07 +00:00
xarmian 6f8105b01d refactor(attachments): consolidate icon helpers onto an SVG set (TASK-2417)
Replaces the three independent emoji icon helpers on the live attachment
surfaces with one mapper and one monochrome SVG icon set (PLAN-2392 DR-3,
DR-3a, DR-3b).

- display.ts: categoryIcon -> iconForAttachment(mime, filename), returning
  an icon identifier rather than an emoji. MIME first, filename extension
  second, generic file last -- never a question mark. isImage and
  formatBytes keep their signatures; StorageTab imports all three.
- attachments/icons/: one currentColor-driven icon per format family, with
  TWO render paths over one path table -- AttachmentIcon.svelte for Svelte
  call sites, iconSvg() for the editor chip, which builds DOM imperatively
  and cannot mount a component.
- attachment-chip.ts: iconForMime, iconForFilename and its local formatBytes
  deleted. The call site keeps its hide-zero/unknown-size conditional; the
  shared formatter renders "0 B" and does not grow a mode (DR-3b).
- mime-families.json: the shared MIME -> family map, inside the web root
  because vitest cannot read outside it. A Go test asserts the server upload
  allowlist is fully covered by it (and carries no strays), so the two lists
  cannot drift silently; the web test covers one representative MIME per
  family plus the unknown-MIME and no-extension cases.

CopyItemDialog and markdown/attachments.ts are deliberately untouched.

Claude-Session: https://claude.ai/code/session_01LmbFxQFDjcYKBLcTnor6DC
2026-08-03 00:46:24 +00:00
xarmian f994509289 feat(web): card anatomy — Chip status/priority, card tokens, violet ring, lane accents (TASK-2293) (#1023)
* feat(web): card anatomy per the refresh mock — Chip status/priority, card tokens, violet selection ring, lane accents (TASK-2293)

PLAN-2290 Phase 3, PR A. ItemCard (shared by Board/List/starred/tags/roles):

- Skin: --card-bg/--card-border/--radius-lg/--shadow-card; hover = border-strong
  (no transform — svelte-dnd-action owns card transforms); .focused becomes the
  mock's violet ring + glow (e2e asserts the CLASS, which is unchanged).
- Anatomy: ref stays top-left; star moves to the right cluster before the
  kebab (ONE auto margin on the star — competing autos split the gap).
- Status/priority render as Chip primitives (tinted pills; status keeps
  click-cycle + pulse via Chip props; labels Title Case, no more uppercase).
- Tags become purple-tinted pills; leading separator before parent chip
  dropped (chips separate visually).
- Dead CSS removed (meta-status family, status-pulse keyframes).

Lane accents: columnAccentClassFor (shareView — shared with the public fork
by construction) gains col-open for open/new/todo/planned; BoardView +
PublicBoardView underline it --status-blue. Default underline unchanged for
custom vocabularies.

Gates: svelte-check 0 errors, 488 tests; board+pane screenshots verified in
both themes.

* fix(web): consolidate BoardView lane accents onto shared mapper + AA chip text in light theme

Codex findings on #1023: (1) BoardView had its OWN columnCssClass — a fifth
parallel status-color-ish map, so only public boards got col-open; it now
delegates to shareView.columnAccentClassFor (in-app and public boards can't
drift, and custom terminal lanes now read as done in-app too). (2) New
--chip-text-mix token (100% dark / 72% light) darkens chip text on light
surfaces — all chip colors verified >=5.8:1 on white (computed).

* refactor(web): columnAccentClassFor moves to $lib/utils/fieldColors (Codex — dependency direction); shareView re-exports
2026-07-24 16:27:09 -04:00
xarmian 01a94d93a8 feat(web): Menu/MenuItem primitive — 3 menus migrated, escape-stack + portal + pointerdown dismissal (TASK-2292) (#1022)
* feat(web): Menu/MenuItem primitive — escape-stack ESC, portal mode, pointerdown outside-click (TASK-2292)

PLAN-2290 Phase 2, PR 4 (final primitive). New shared machinery:

- lib/components/common/Menu.svelte — anchored + portal modes (portal =
  fixed coords + flip/clamp, escapes card content-visibility containment),
  instance-scoped POINTERDOWN outside-click (structurally removes the
  BUG-2281 stopPropagation-on-rows detach workaround), ESC via the shared
  escapeStack at new priority menu=40 (one ESC closes menu before
  pane/drawer), roving keyboard nav, focus-in/focus-return, BottomSheet
  swap at 768px, --bg-raised panel skin.
- lib/components/common/MenuItem.svelte — icon/hint/danger/menuitemradio rows.
- lib/utils/clickOutside.ts + lib/utils/portalAction.ts — extracted from
  the hand-copied per-menu versions.
- app.css: --bg-raised token (dark = tertiary; light = white).

Migrated: ItemActionsMenu (portal mode, entire hand-rolled machinery
deleted), QuickActionsMenu (anchored + sheetOnMobile, EmojiPicker exemption
via exempt(), BUG-2281 workarounds removed), TopBar user menu (desktop +
mobile branches deduped into one snippet; gains aria-haspopup/expanded +
keyboard nav it never had). E2E locators updated to accessible-name form.

Documented leave-alones: TopBar workspace-overflow menu (it IS a dndzone —
conditional mount / focus-steal / pointerdown-close each break
drag-reorder; in-file comments), LaneActionsMenu drill-down +
WorkspaceSwitcher (Phase 3 / later).

Gates: svelte-check 0 errors, 488 tests, make check green; runtime-verified
via Playwright: user menu roving nav (ArrowDown x2 -> Admin), ESC closes via
stack, kebab portal placement + edge-aware rows.

* fix(web): Menu review fixes — form focus hand-off, drag suppression, scroll-close without refocus, resize close

Codex findings on #1022: (1) QuickActions create-form now receives focus
when it swaps in (the focused MenuItem unmounts on the flip); (2)
clickOutside gains suppress() and TopBar's user menu passes
isDragging||dragArmed so pill drags can't slam it shut (parity with the
old drag guard); (3) portal scroll/resize dismissal calls onclose()
directly — no trigger refocus fighting the user's scroll (parity with the
old returnFocus=false); (4) resize now also closes portal menus (stale
fixed coords).
2026-07-24 16:11:32 -04:00
xarmian 6422324edd feat(web): Button primitive + dark text-on-fill AA — 95 sites migrated (TASK-2292) (#1020)
* feat(web): Button primitive + dark text-on-fill AA fix; migrate 95 button sites (TASK-2292)

PLAN-2290 Phase 2, PR 2. lib/components/common/Button.svelte — variants
primary (filled --accent-primary-strong #7c4ff0, the violet band where white
text passes AA 4.96:1 while staying >=3:1 vs surface — pays off the PR #1018
deferral) / secondary / ghost / danger (red tint, AA both themes); size sm/md;
full attr passthrough (type=submit preserved at form sites).

95 usages across 14 files migrated (settings, conventions, playbooks x2,
workspace home, console suite, modals, comment composer, EmptyState); dead
scoped .btn* CSS deleted (net -291 lines). Deliberate leave-alones per file:
ItemDetail action-bar strip (Phase 4 owns the pane), anchors styled as
buttons, segmented controls, icon-only buttons, dashed low-emphasis
affordances.

Gates: svelte-check 0 errors (dead-selector warnings down 8->7), 488 web
tests, make check green; screenshots reviewed both themes.

* fix(web): Button class-prop merge + danger-solid variant for final confirms

Codex findings on #1020: (1) caller-supplied class no longer clobbers the
primitive's classes — class is destructured and merged, rest spread moved
first; (2) new danger-solid variant (filled --accent-red-strong #dc2626,
white text 4.83:1 AA both themes) restores destructive emphasis on the two
final-confirm flows that had gone pale (OpenChildrenDialog override,
conventions delete Confirm); entry-level destructive buttons keep the tint.
2026-07-24 15:04:48 -04:00
xarmian a335033415 feat(web): Chip primitive + canonical fieldColors util — 48 badge sites migrated (TASK-2292) (#1019)
* feat(web): Chip primitive + canonical fieldColors util; migrate 48 badge sites (TASK-2292)

PLAN-2290 Phase 2, PR 1. Extracts the first shared primitives:

- lib/utils/fieldColors.ts — ONE statusColor/priorityColor (+ hasCanonicalStatus,
  formatFieldLabel), replacing four drifted implementations (ItemCard,
  fields/FieldEditor, CommandPalette, workspace home); shareView.ts re-exports
  it so public shares stay in lockstep. Deliberate unifications: open/new/todo/
  planned -> --status-blue (was text-secondary on cards); active -> green (was
  cyan in palette/home); draft -> muted (was blue); rejected/cancelled/wontfix
  -> gray; priority medium -> text-secondary.
- lib/components/common/Chip.svelte — tinted-pill primitive per the refresh
  mock (color-mix tint via new --chip-alpha token, colored text, dot/size/
  onclick/pulse props); svelte-autofixer clean.
- 48 badge usages across 15 files migrated to Chip; scoped .badge CSS deleted
  (net -355 lines). Deliberate leave-alones: GraphToolbar count bubble +
  filter toggles, stat tiles, timeline rail markers, avatars.

Gates: svelte-check 0 errors, 488 web tests, make check green; board/settings
screenshots verified in both themes.

* fix(web): Chip button variant always preventDefaults (never navigates a parent <a>)

Codex finding on #1019: an onclick Chip inside a link card would activate
the link after the callback. preventDefault always (a chip is never a
link); propagation intentionally continues so click-outside closers work —
callers in interactive cards stopPropagation per the house pattern.
2026-07-24 14:34:54 -04:00
xarmian 841a2cb4ea feat(web): violet retheme — accent-primary alias, neutral scale, card tokens, radius, AA text (TASK-2291) (#1018)
* feat(web): violet retheme — accent-primary, neutral scale, card tokens, radius, AA text (TASK-2291)

PLAN-2290 Phase 1, PR B. Values-only retheme in app.css + theme-color meta:

- --accent-primary #9268f8 dark / #7c3aed light; --accent-blue aliased to it
  (~95% of its 462 sites are brand usage; categorical sites moved to
  --status-blue in PR A and stay blue). Dark value chosen by contrast math:
  AA as link text (4.85:1) while improving white-on-fill from 2.75 to 3.78
  (>=3:1 UI threshold; full text-on-fill AA lands with the Phase 2 Button
  primitive via --text-on-accent).
- Violet-biased neutral scale both themes; light mode inverts to off-white
  canvas (#f5f5f9) with white surfaces per the mock.
- Muted/secondary text re-picked: >=5:1 on every bg token in both themes
  (closes TASK-2262 C9 app-wide).
- --border-strong/--card-bg/--card-border/--shadow-card defined both themes
  (consumed from Phase 3).
- Radius scale 6/4/8 -> 8/5/12; light-mode danger tuned to #dc2626 (4.83:1).
- theme-color meta #4a9eff -> #8b5cf6.

Verified: make check green; screenshots on 4 surfaces x 2 themes reviewed;
contrast ratios computed for all text-token pairs.

* fix(web): violet PWA branding (manifest/icon) + pin light accents in print block

Codex review findings on #1018: manifest theme_color/background and icon.svg
still carried the blue brand; print block now pins light-theme accents so
dark-theme printing doesn't put the bright violet on white paper. Dark
button text-on-fill AA is explicitly deferred to the Phase 2 Button
primitive (tracked in TASK-2292).

* fix(web): regenerate apple-touch-icon.png from the violet icon.svg (180x180)

* fix(web): regenerate remaining brand rasters from violet icon.svg

favicon-16/32, favicon.ico (single PNG-encoded 48px entry), icon-192 (was
0 bytes), icon-512, padicon.png (OG image, 701x701) all regenerated from
the canonical icon.svg 'P' mark — the old rasters were a blue calendar
design inconsistent with the linked SVG. site.webmanifest colors updated
to the violet scheme.
2026-07-24 14:03:53 -04:00
xarmian 563371ee9b feat(web): define missing token families + zero-change drift sweep (TASK-2291) (#1017)
PLAN-2290 Phase 1, PR A. Defines --accent-red, --status-blue, --text-on-accent,
--shadow-sm/md/lg, --modal-shadow, --scrim in app.css (values matching the
long-standing inline fallbacks), then mechanically sweeps:

- var(--accent-red, #hex) fallback forms collapsed (52+4+1 sites); 3 bare
  var(--accent-red) sites that previously resolved to NOTHING now render
- phantom var(--color-danger, #dc2626) repointed to --accent-red
- bare #ef4444/#dc2626 danger literals -> var(--accent-red) (57 files);
  #c0392b/#e53e3e/#dc2626 outliers unify to #ef4444 (deliberate)
- shadow fallback forms collapsed to the now-defined tokens
- 10 categorical literally-blue sites (status maps, burndown chart, info
  badge) repointed --accent-blue -> --status-blue so PR B's violet accent
  flip won't drag status colors

Verified: svelte-check 0 errors, 488 web tests, make check green; Playwright
before/after pixel diff on 4 surfaces x 2 themes — identical except the
sidebar build-id string.
2026-07-24 13:48:56 -04:00
xarmian b76abdd66b fix(web): full ARIA-modal isolation for the mobile detail pane (TASK-2131) (#1007)
* feat(web): full ARIA-modal isolation for the mobile detail pane (TASK-2131)

Follow-up to TASK-2122. The mobile full-screen detail-pane overlay had a
JS focus trap + an inert list column, but the app-shell chrome behind it
stayed in the a11y tree and the pane was still just an <aside>. Complete
the modal:

- The mobile `.item-pane` becomes role="dialog" aria-modal="true"; the
  desktop split stays a bare <aside> (complementary landmark, non-modal).
- MobileContextBar + BottomNav (rendered in the workspace +layout, ABOVE
  the pane host) are marked `inert` while a mobile overlay is up, so they
  leave the focus order AND the screen-reader tree. A JS trap can't
  constrain an SR virtual cursor and aria-modal is unevenly honored, so
  the background chrome must physically drop out.

The chrome is a layout sibling the host can't reach by prop, so PaneHost
hoists "a mobile overlay is active" into a small ref-counted store
(paneOverlay.svelte.ts) the layout reads — one-way writer/reader split
per CONVE-1688. Ref-counted so an overlapping route-change remount can't
clear the signal early. The layout carries `inert` on display:contents
wrappers (cascades to the fixed chrome, adds no box).

Verified in a real browser (Playwright): mobile → dialog role + aria-modal
+ inert descendants unfocusable; desktop → bare aside + chrome interactive.

Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra

* fix(web): exclude the pane's dialog role from foreign-modal guards (TASK-2131)

The new mobile role="dialog" on the pane collided with three places that
treat ANY [role="dialog"] as a *foreign* modal that owns its own ESC /
focus — regressions the pane's <aside>-not-a-dialog invariant had been
silently relying on:

- paneFocus.ts PANE_EXEMPT_SURFACE_SELECTOR: any in-pane element matched
  closest('[role="dialog"]') → the whole pane read as an "exempt surface",
  killing the mobile Tab trap and confusing the focus-follows classifier.
- The collection + item-page ESC guards querySelector('[role="dialog"]')
  → the pane matched itself → ESC was swallowed instead of closing/popping
  the pane on mobile.

Fix: exclude the pane via [role="dialog"]:not(.item-pane) at all three
sites (a genuinely nested dialog/menu opened FROM the pane still matches).
Adds inExemptSurface unit coverage for the pane-not-exempt case.

Caught by the independent Codex review pass.

Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra

* fix(web): complete mobile modal isolation — banners + print (TASK-2131)

Two more members of the same [role="dialog"] collision class, from the
independent Codex pass:

- Print: app.css @media print hides [role="dialog"] to strip overlays.
  The mobile pane now matches, so printing at <=768px with the pane open
  dropped the whole item from the printout. Exclude via :not(.item-pane) —
  the pane is the content being printed, not a transient overlay.
- Banners: VerifyEmailBanner + ConnectBanner rendered OUTSIDE the inert
  wrappers, so their controls (Resend / Connect) stayed reachable by an SR
  virtual cursor behind the aria-modal pane — the same gap the inert of
  MobileContextBar/BottomNav closes. Fold them into the top inert wrapper
  so ALL app-shell siblings behind the overlay leave the a11y tree; only
  the pane (in children()) stays interactive.

Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra
2026-07-22 18:13:37 -04:00
xarmian 6d08b72f5d fix(web): thin horizontal scrollbars — set ::-webkit-scrollbar height (BUG-2127 follow-up) (#950)
The global `::-webkit-scrollbar` rule set `width: 6px` but no `height`.
`width` only sizes vertical scrollbars; a horizontal scrollbar's thickness
is `height`, which was unset — so horizontal scrollbars (notably the
board-view lane track) fell back to the chunky ~15px browser default while
every vertical scrollbar was a thin 6px. That "weirdly large horizontal
scrollbar" is the real defect PR #946 papered over by making the lanes
shrink-to-fill so horizontal overflow never occurred. Add `height: 6px`
so horizontal and vertical scrollbars match.

Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra
2026-07-16 22:08:54 -04:00
xarmian 9664cd6ee5 feat(web): shared native-dialog Modal primitive + migrate form modals (TASK-2023) (#881)
* feat(web): shared native-dialog Modal primitive + migrate form modals (TASK-2023)

* fix(web): hide closed native-dialog Modal (specificity vs UA rule) — Codex P1
2026-07-09 15:33:37 -04:00
xarmian 5c491b78b5 feat(web): mobile bottom navigation (PLAN-1694) (#692)
* feat(web): mobile bottom navigation + quick-capture (PLAN-1694)

Add a mobile-only persistent bottom navigation bar that coexists with the
existing TopBar. Five slots: Dashboard, Search, Quick-capture (center +),
Activity, More. The "More" slot opens a BottomSheet with the sidebar's
overflow destinations + collections; the center action opens a quick-capture
sheet that creates an item via the existing API.

- New shared nav source (lib/nav/destinations.ts) consumed by both the
  desktop Sidebar and the new BottomNav/More sheet so the two can't drift;
  Sidebar refactored to derive active-state from it (lossless).
- BottomNav mounts in the workspace layout (workspace-only), shows only on
  mobile (uiStore.isMobile, ≤768px), and toggles body.has-bottom-nav so
  app.css reflows .main-content above the fixed bar.
- iOS: app.html viewport-fit=cover + env(safe-area-inset-bottom) padding.
- z-index 40 (above TopBar/sidebar, below sheets).

Header retirement + full "You" (workspace/account) consolidation are
deliberately deferred to a human design checkpoint (DR-3).

Verify: web npm run check (0 errors) + npm run build (✓). Codex review loop
clean (1 P2 fixed: quick-capture re-defaults a stale collection slug).

Refs PLAN-1694 (TASK-1695, TASK-1696, TASK-1697, TASK-1698); parent ROADM-28.

* feat(web): retire mobile header in-workspace, add You sheet + contextual bar (PLAN-1694)

Complete the mobile nav vision on top of the bottom bar: inside a workspace
the global mobile header is gone, replaced by the BottomNav + a "You" sheet +
a contextual back/title bar on detail screens.

- BottomNav 5th slot More → You (👤). New YouSheet composes the workspace
  switcher, the nav overflow, and the full account menu carried over from the
  retired TopBar (Workspaces/Settings/Billing/Admin/theme/Resources/Connect/
  Sign out) so nothing is lost.
- Root layout gates <TopBar mobile /> on !inWorkspace (page.params.workspace),
  keeping it for the non-workspace picker/home which has no BottomNav; the
  .app-layout top offset now applies only via body.has-mobile-topbar.
- New MobileContextBar: fixed back+title bar shown only on detail screens
  (path depth ≥2); body.has-context-bar reflows .main-content top. Root/tab
  screens render full-height (reclaimed space).

Codex consensus + review clean (no P1). P2s fixed: safe in-app-history check
via afterNavigate (deep-link fallback to parent URL), humanized title fallback
for pages that don't wire titleStore, and the You sheet closes on navigation
so a workspace switch dismisses it.

Verify: web npm run check (0 errors) + npm run build (✓).

Refs PLAN-1694 (TASK-1699, TASK-1700); parent ROADM-28.

* feat(web): redesign mobile nav into Workspace + You docked sheets (PLAN-1694)

Address design feedback: the previous "You" sheet reused common/BottomSheet
(covered the nav bar) and embedded the old WorkspaceSwitcher (plain, unadapted).
Replace it with two purpose-built surfaces that dock ABOVE the nav.

- DockedSheet (new): bottom sheet anchored above the bottom nav — backdrop
  stops at the nav's top edge so the bar stays visible + tappable and the
  active slot stays lit. ~2/3 height, grab handle, slide-up, swipe-down /
  tap-out / Escape to dismiss. No more full-screen overlay covering the nav.
- Bottom-nav slot 1 Dashboard → Workspace (icon = current workspace avatar),
  opening WorkspaceSheet: a designed switcher card (inline-expand list) +
  Navigate tile grid + Collections.
- "You" slot → YouSheet, now account-only: profile header, theme toggle,
  account Settings / Workspaces / Billing / Admin / Connect / Resources /
  Sign out. Search / + / Activity unchanged.
- New avatar util (avatarColor/avatarInitial) for workspace + user avatars.

Splits the overstuffed single sheet into two focused surfaces (where-am-I vs
me) and removes the reused WorkspaceSwitcher/BottomSheet. Codex review CLEAN.

Verify: web npm run check (0 errors) + npm run build (✓).

Refs PLAN-1694 (TASK-1701); parent ROADM-28.

* feat(web): toggle Workspace/You sheets on repeat nav tap (PLAN-1694)

Tapping the Workspace or You nav slot a second time now closes its open
sheet (tap to open, tap again to close). Opening one still closes the other.
The docked backdrop stops above the nav, so the slot button stays tappable
while its sheet is open.

Refs PLAN-1694 (TASK-1701).

* feat(web): dock mobile search to match the nav sheets (PLAN-1694)

The CommandPalette was the last mobile surface that didn't match — a
full-screen takeover (square, no handle, hid the nav). Give its mobile
presentation the same docked treatment as the Workspace/You sheets;
desktop ⌘K is untouched.

- Mobile: dock the palette above the bottom nav (backdrop stops at the nav's
  top edge so the bar stays visible), rounded top, grab handle with
  swipe-down-to-dismiss, tap-outside to close, no X. Lift above the nav only
  when it's present (:global(body.has-bottom-nav)); flush to bottom on the
  non-workspace picker. Keyboard handling preserved: input stays at the sheet
  top, .results is the sole scroll area, min-height keeps the input clear of
  the keyboard when empty.
- BottomNav: Search slot toggles (tap to open, tap again to close) and lights
  up while open; the three surfaces (Workspace / You / Search) are now
  mutually exclusive.

Codex review CLEAN. Verify: web npm run check (0 errors) + build (✓).
Note: on-screen-keyboard feel should get an on-device smoke test.

Refs PLAN-1694 (TASK-1701); parent ROADM-28.
2026-06-02 08:41:54 -04:00
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