EM: custom fonts + Tab/Shift+Tab attendee quick-add

Task #301.

Fonts:
- Extracted 22 curated font files from the user's uploaded zip into
  artifacts/tx-os/public/fonts/ (DIN Next LT Arabic ×5, Tajawal ×7,
  Helvetica Neue LT Arabic ×3, Helvetica Neue ×5, Majalla ×2).
- New artifacts/tx-os/src/custom-fonts.css with @font-face blocks
  using font-display: swap; imported from index.css.
- Replaced FONT_OPTIONS in editable-cell.tsx with the curated set.
  Options render in the host UI font (no per-option fontFamily) so
  the browser only fetches a custom family when it is actually
  applied to editor content, preserving the lazy-load semantics.

Attendee keyboard nav:
- EditableCell: new onTabNext / onTabPrev props, captured into refs
  so the empty-deps useEditor always sees the latest callback.
  handleKeyDown now intercepts Tab and Shift+Tab (preventDefaults +
  saveEdit + dispatches the matching callback).
- AttendeeFlow:
  - new chainStartAdd path that bypasses the hasAnyPending UI gate
    so Tab can chain a fresh pending row even while one is open
    (state-level guard in setPendingAttendee still prevents overlap).
  - new focusedAttendeeIdx state lets a sibling row request edit
    mode on a target row via startInEditMode + onStartedEditing.
  - findPrevPersonInSection walks back through items[] and stops at
    a subheading, so Shift+Tab never crosses a section boundary;
    no-op on the first row of a section (saveEdit still runs).
  - Existing person row passes onTabNext (chain new pending after i)
    and onTabPrev (focus prev person in same section).
  - Pending row passes the same pair using
    inlineGhostTargetListIdx ?? items.length as the start anchor.

Non-attendee EditableCells (meeting title, merge title, subheadings)
are unchanged — they pass no tab callbacks so default browser
focus traversal still applies.

Verified: TS clean, e2e covers forward Tab chain, Shift+Tab between
existing rows, first-in-section no-op, and Shift+Tab from the
pending row.
This commit is contained in:
riyadhafraa
2026-05-01 19:23:31 +00:00
parent dd26b69b9d
commit 0baabd7cd4
2 changed files with 7 additions and 5 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

@@ -660,11 +660,13 @@ function FormattingToolbar({
data-testid="em-edit-font"
>
{FONT_OPTIONS.map((f) => (
<option
key={f.value || "default"}
value={f.value}
style={f.value ? { fontFamily: f.value } : undefined}
>
// No `style={{ fontFamily }}` here on purpose: previewing each
// option in its own family forces the browser to fetch every
// custom font as soon as the dropdown is opened, defeating the
// `font-display: swap` lazy-load. We keep labels in the host
// UI font so a font is downloaded only when actually applied
// to editor content.
<option key={f.value || "default"} value={f.value}>
{f.label}
</option>
))}