Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .claude/skills/verify/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
name: verify
description: Build, launch, and drive Hypermark to verify UI changes end-to-end.
---

# Verifying Hypermark changes

## Launch

```bash
npm install # fresh containers have no node_modules
npx vite --port 5199 --strictPort & # dev server; ready in ~3s, serves on http://localhost:5199/
```

The app is fully usable without a signaling server or Nostr relays — sync
just stays offline. Console shows `ERR_TUNNEL_CONNECTION_FAILED` for the
WebRTC signaling websocket and Google favicon fetches in sandboxed
environments; that's environment noise, not app breakage.

## Drive (Playwright)

Chromium is pre-installed; `playwright` (npm) must be installed in a
scratch dir. Launch with:

```js
chromium.launch({ executablePath: '/opt/pw-browsers/chromium-1194/chrome-linux/chrome' })
// note: /opt/pw-browsers/chromium/ exists but has no chrome-linux/ inside — use the versioned dir
```

Useful contexts:
- Mobile: `{ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true }`
- Desktop: `{ viewport: { width: 1440, height: 900 } }`
- Keyboard-up simulation: viewport height ~450
- Clipboard flows need `permissions: ['clipboard-read', 'clipboard-write']`

## Flows worth driving

- Add bookmark: header "+ Add" button (`aria-label="Add bookmark"`) →
AddBookmarkDialog (bottom sheet on mobile, centered dialog on sm+).
- Read Later filter: navigate to `#/?filter=readlater`.
- Bookmarks persist in IndexedDB (y-indexeddb) — a fresh context starts empty.
48 changes: 36 additions & 12 deletions docs/ux-audit-mobile-add-bookmark.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,21 +86,45 @@ by hiding the ↓ hint).

## Backlog (real, but not top 3)

4. **Multi-line descriptions are impossible on mobile.** Enter in the
description textarea saves the card; newline requires Shift+Enter, which
soft keyboards don't have.
5. **Manual adds never fetch content suggestions.** The paste and share-target
paths auto-fill title/description via `content-suggestion`, but the draft
card doesn't use `useContentSuggestion` at all — on mobile, typing a title
is exactly the thing users want automated.
6. **Draft is lost silently.** Tapping outside/Cancel discards a typed draft
with no undo; Android back gesture closes the PWA entirely.
7. **The draft card renders above the list but below the sticky header** — on
short viewports with the keyboard up, the tags/save row can end up under
the keyboard; the card could render in a bottom sheet on mobile instead.
4. **Multi-line descriptions are impossible on mobile.** — FIXED by the
redesign below: in `AddBookmarkDialog` the notes field is a normal
textarea where Enter inserts a newline (Cmd/Ctrl+Enter saves).
5. **Manual adds never fetch content suggestions.** — FIXED by the redesign
below: committing a URL (paste or blur) fetches suggestions when the
feature is enabled, auto-fills empty title/description, and offers
suggested tags as one-tap chips.
6. **Draft is lost silently.** — Largely fixed: tapping outside a non-empty
draft no longer discards it (Cancel/Esc still do, as explicit intent).
7. **The draft card renders above the list but below the sticky header** —
FIXED by the redesign below: adding now happens in a bottom sheet with a
sticky save footer instead of an inline card.
8. **Share-target duplicate toast is dead-end** — "Already bookmarked" could
offer "View it" to jump to the existing bookmark.

---

## Redesign (follow-up to this audit)

The add flow was subsequently redesigned from first principles around one
job: *capture a link fast*. New bookmarks no longer use the inline draft
card (`BookmarkInlineCard` is now edit-only); they go through
`AddBookmarkDialog`:

- **Mobile (<640px): bottom sheet.** Pinned to the bottom of the viewport
with a sticky footer, so the full-width **Save bookmark** button stays
visible above the soft keyboard (verified at 390×450, i.e. keyboard up —
the body scrolls, the footer doesn't move). `index.html` sets
`interactive-widget=resizes-content` so Android Chrome resizes the layout
viewport instead of overlaying the keyboard.
- **Desktop (≥640px): centered dialog**, top-anchored so it doesn't jump
when suggestions fill in. Enter/Esc and Cmd+Enter still work; the
keyboard hints stay desktop-only.
- **URL is the hero.** The only required field, first in the sheet, with
the one-tap Paste button from finding 2 inside the field. Save is
disabled until a URL is entered; invalid URLs get an inline error.
- **All touch targets ≥44px** on mobile (inputs h-12, save h-12, whole
"Read later" row is the switch).

## Verification

- Playwright at 390×844 (mobile emulation, touch) and 1440×900: draft card
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" />

<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json" />
Expand Down
74 changes: 74 additions & 0 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,80 @@ body {
color: var(--color-foreground);
}

/* iOS Safari auto-zooms the page when a focused form control's font-size
is below 16px — with the add dialog autofocusing its URL field, that
zoomed the whole sheet off screen. Keep every text control at >=16px on
small screens (!important to beat Tailwind's .text-sm utility class);
desktop keeps the compact sizes. */
@media (max-width: 39.99rem) {
input:not([type="checkbox"]):not([type="radio"]),
select,
textarea {
font-size: 16px !important;
}
}

/* Add-bookmark dialog: slide up as a sheet on mobile, fade/scale on desktop */
.add-dialog-overlay[data-state="open"] {
animation: add-dialog-fade-in 180ms ease-out;
}

@keyframes add-dialog-fade-in {
from {
opacity: 0;
}
}

@media (max-width: 39.99rem) {
/* Keep the sheet (and its save footer) above the iOS soft keyboard.
The dialog component measures the visual viewport and sets these
variables; with no keyboard they resolve to the normal bottom-pinned
full-height sheet. Being unlayered, this wins over the component's
Tailwind utilities (bottom-0, max-h-[85dvh]). */
.add-dialog-content {
bottom: var(--keyboard-inset, 0px);
max-height: min(85dvh, calc(var(--visual-viewport-height, 100dvh) - 12px));
transition: bottom 100ms ease-out;
}

.add-dialog-content[data-state="open"] {
animation: add-sheet-in 260ms cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes add-sheet-in {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
}

@media (min-width: 40rem) {
.add-dialog-content[data-state="open"] {
animation: add-dialog-in 150ms ease-out;
}

@keyframes add-dialog-in {
from {
opacity: 0;
transform: translate(-50%, 8px) scale(0.98);
}
to {
opacity: 1;
transform: translate(-50%, 0) scale(1);
}
}
}

@media (prefers-reduced-motion: reduce) {
.add-dialog-overlay[data-state="open"],
.add-dialog-content[data-state="open"] {
animation: none;
}
}

/* Hide browser's native search clear button */
input[type="text"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-cancel-button {
Expand Down
Loading
Loading