diff --git a/artifacts/tx-os/public/fonts/DINNextLTArabic-Black.ttf b/artifacts/tx-os/public/fonts/DINNextLTArabic-Black.ttf new file mode 100644 index 00000000..8775a09e Binary files /dev/null and b/artifacts/tx-os/public/fonts/DINNextLTArabic-Black.ttf differ diff --git a/artifacts/tx-os/public/fonts/DINNextLTArabic-Bold.ttf b/artifacts/tx-os/public/fonts/DINNextLTArabic-Bold.ttf new file mode 100644 index 00000000..147360b9 Binary files /dev/null and b/artifacts/tx-os/public/fonts/DINNextLTArabic-Bold.ttf differ diff --git a/artifacts/tx-os/public/fonts/DINNextLTArabic-Medium.ttf b/artifacts/tx-os/public/fonts/DINNextLTArabic-Medium.ttf new file mode 100644 index 00000000..dc614c2c Binary files /dev/null and b/artifacts/tx-os/public/fonts/DINNextLTArabic-Medium.ttf differ diff --git a/artifacts/tx-os/public/fonts/DINNextLTArabic-Regular.ttf b/artifacts/tx-os/public/fonts/DINNextLTArabic-Regular.ttf new file mode 100644 index 00000000..8753857f Binary files /dev/null and b/artifacts/tx-os/public/fonts/DINNextLTArabic-Regular.ttf differ diff --git a/artifacts/tx-os/public/fonts/DINNextLTArabic-UltraLight.ttf b/artifacts/tx-os/public/fonts/DINNextLTArabic-UltraLight.ttf new file mode 100644 index 00000000..29a4c176 Binary files /dev/null and b/artifacts/tx-os/public/fonts/DINNextLTArabic-UltraLight.ttf differ diff --git a/artifacts/tx-os/public/fonts/HelveticaNeue-Black.otf b/artifacts/tx-os/public/fonts/HelveticaNeue-Black.otf new file mode 100644 index 00000000..981ea5a1 Binary files /dev/null and b/artifacts/tx-os/public/fonts/HelveticaNeue-Black.otf differ diff --git a/artifacts/tx-os/public/fonts/HelveticaNeue-Bold.ttf b/artifacts/tx-os/public/fonts/HelveticaNeue-Bold.ttf new file mode 100644 index 00000000..a9189019 Binary files /dev/null and b/artifacts/tx-os/public/fonts/HelveticaNeue-Bold.ttf differ diff --git a/artifacts/tx-os/public/fonts/HelveticaNeue-Light.otf b/artifacts/tx-os/public/fonts/HelveticaNeue-Light.otf new file mode 100644 index 00000000..7a08ea08 Binary files /dev/null and b/artifacts/tx-os/public/fonts/HelveticaNeue-Light.otf differ diff --git a/artifacts/tx-os/public/fonts/HelveticaNeue-Medium.otf b/artifacts/tx-os/public/fonts/HelveticaNeue-Medium.otf new file mode 100644 index 00000000..4b231139 Binary files /dev/null and b/artifacts/tx-os/public/fonts/HelveticaNeue-Medium.otf differ diff --git a/artifacts/tx-os/public/fonts/HelveticaNeue-Regular.ttf b/artifacts/tx-os/public/fonts/HelveticaNeue-Regular.ttf new file mode 100644 index 00000000..4c2cfa4b Binary files /dev/null and b/artifacts/tx-os/public/fonts/HelveticaNeue-Regular.ttf differ diff --git a/artifacts/tx-os/public/fonts/HelveticaNeueLTArabic-Bold.ttf b/artifacts/tx-os/public/fonts/HelveticaNeueLTArabic-Bold.ttf new file mode 100644 index 00000000..2d211807 Binary files /dev/null and b/artifacts/tx-os/public/fonts/HelveticaNeueLTArabic-Bold.ttf differ diff --git a/artifacts/tx-os/public/fonts/HelveticaNeueLTArabic-Light.ttf b/artifacts/tx-os/public/fonts/HelveticaNeueLTArabic-Light.ttf new file mode 100644 index 00000000..c84e1af9 Binary files /dev/null and b/artifacts/tx-os/public/fonts/HelveticaNeueLTArabic-Light.ttf differ diff --git a/artifacts/tx-os/public/fonts/HelveticaNeueLTArabic-Roman.ttf b/artifacts/tx-os/public/fonts/HelveticaNeueLTArabic-Roman.ttf new file mode 100644 index 00000000..88c2a2ac Binary files /dev/null and b/artifacts/tx-os/public/fonts/HelveticaNeueLTArabic-Roman.ttf differ diff --git a/artifacts/tx-os/public/fonts/Majalla-Bold.ttf b/artifacts/tx-os/public/fonts/Majalla-Bold.ttf new file mode 100644 index 00000000..c01a8820 Binary files /dev/null and b/artifacts/tx-os/public/fonts/Majalla-Bold.ttf differ diff --git a/artifacts/tx-os/public/fonts/Majalla-Regular.ttf b/artifacts/tx-os/public/fonts/Majalla-Regular.ttf new file mode 100644 index 00000000..f4586ec0 Binary files /dev/null and b/artifacts/tx-os/public/fonts/Majalla-Regular.ttf differ diff --git a/artifacts/tx-os/public/fonts/Tajawal-Black.ttf b/artifacts/tx-os/public/fonts/Tajawal-Black.ttf new file mode 100644 index 00000000..f9a40b54 Binary files /dev/null and b/artifacts/tx-os/public/fonts/Tajawal-Black.ttf differ diff --git a/artifacts/tx-os/public/fonts/Tajawal-Bold.ttf b/artifacts/tx-os/public/fonts/Tajawal-Bold.ttf new file mode 100644 index 00000000..081ac580 Binary files /dev/null and b/artifacts/tx-os/public/fonts/Tajawal-Bold.ttf differ diff --git a/artifacts/tx-os/public/fonts/Tajawal-ExtraBold.ttf b/artifacts/tx-os/public/fonts/Tajawal-ExtraBold.ttf new file mode 100644 index 00000000..2904c3c1 Binary files /dev/null and b/artifacts/tx-os/public/fonts/Tajawal-ExtraBold.ttf differ diff --git a/artifacts/tx-os/public/fonts/Tajawal-ExtraLight.ttf b/artifacts/tx-os/public/fonts/Tajawal-ExtraLight.ttf new file mode 100644 index 00000000..a3e2686a Binary files /dev/null and b/artifacts/tx-os/public/fonts/Tajawal-ExtraLight.ttf differ diff --git a/artifacts/tx-os/public/fonts/Tajawal-Light.ttf b/artifacts/tx-os/public/fonts/Tajawal-Light.ttf new file mode 100644 index 00000000..dfc9d93f Binary files /dev/null and b/artifacts/tx-os/public/fonts/Tajawal-Light.ttf differ diff --git a/artifacts/tx-os/public/fonts/Tajawal-Medium.ttf b/artifacts/tx-os/public/fonts/Tajawal-Medium.ttf new file mode 100644 index 00000000..4176ac15 Binary files /dev/null and b/artifacts/tx-os/public/fonts/Tajawal-Medium.ttf differ diff --git a/artifacts/tx-os/public/fonts/Tajawal-Regular.ttf b/artifacts/tx-os/public/fonts/Tajawal-Regular.ttf new file mode 100644 index 00000000..747f56cf Binary files /dev/null and b/artifacts/tx-os/public/fonts/Tajawal-Regular.ttf differ diff --git a/artifacts/tx-os/src/components/editable-cell.tsx b/artifacts/tx-os/src/components/editable-cell.tsx index f743c4f6..a159a866 100644 --- a/artifacts/tx-os/src/components/editable-cell.tsx +++ b/artifacts/tx-os/src/components/editable-cell.tsx @@ -40,12 +40,13 @@ const COLOR_SWATCHES = [ const FONT_OPTIONS = [ { value: "", label: "default" }, - { value: "system-ui", label: "system-ui" }, - { value: "Cairo", label: "Cairo" }, + { value: "DIN Next LT Arabic", label: "DIN Next LT Arabic" }, { value: "Tajawal", label: "Tajawal" }, - { value: "Amiri", label: "Amiri" }, - { value: "Noto Naskh Arabic", label: "Noto" }, + { value: "Helvetica Neue LT Arabic", label: "Helvetica Neue Arabic" }, + { value: "Helvetica Neue", label: "Helvetica Neue" }, + { value: "Majalla", label: "Majalla" }, { value: "IBM Plex Sans Arabic", label: "IBM Plex" }, + { value: "system-ui", label: "system-ui" }, ]; const FONT_SIZE_OPTIONS = [ @@ -136,6 +137,7 @@ export function EditableCell({ onStartedEditing, forceSaveOnBlur = false, onCancel, + onTabNext, }: { value: string; onSave: (html: string) => Promise | void; @@ -162,12 +164,29 @@ export function EditableCell({ * the synthetic row from the cell. */ onCancel?: () => void; + /** + * If provided, plain `Tab` (no Shift) inside the editor commits the + * current value and then invokes this callback. The callback is + * responsible for what should happen "next" — typically opening a + * new pending attendee row immediately after this one. We swallow + * the default Tab behaviour so the editor never inserts a tab + * character. `Shift+Tab` is left to the browser/TipTap default. + */ + onTabNext?: () => void; }) { const [editing, setEditing] = useState(false); const wrapperRef = useRef(null); const toolbarRef = useRef(null); const blurTimerRef = useRef | null>(null); + // `useEditor` below is created once (deps `[]`), so its `handleKeyDown` + // closes over the first render's `onTabNext`. Stash the latest in a + // ref so Tab always invokes the current callback. + const onTabNextRef = useRef(onTabNext); + useEffect(() => { + onTabNextRef.current = onTabNext; + }, [onTabNext]); + useEffect(() => { if (startInEditMode && !editing && !disabled) { setEditing(true); @@ -220,6 +239,20 @@ export function EditableCell({ saveEdit(); return true; } + // Plain Tab → commit and run the "what's next" callback (e.g. + // open a fresh pending attendee row right after this one). + // Without an `onTabNext` handler we leave Tab to the browser + // so focus moves naturally to the next focusable control. + if ( + event.key === "Tab" && + !event.shiftKey && + onTabNextRef.current + ) { + event.preventDefault(); + const next = onTabNextRef.current; + void Promise.resolve(saveEdit()).then(() => next()); + return true; + } return false; }, }, @@ -614,7 +647,11 @@ function FormattingToolbar({ data-testid="em-edit-font" > {FONT_OPTIONS.map((f) => ( - ))} diff --git a/artifacts/tx-os/src/custom-fonts.css b/artifacts/tx-os/src/custom-fonts.css new file mode 100644 index 00000000..ef87bc14 --- /dev/null +++ b/artifacts/tx-os/src/custom-fonts.css @@ -0,0 +1,176 @@ +/* + * Custom font families for the rich-text editor (attendee names, + * meeting titles, notes, etc.). Files live in /public/fonts/ and are + * served by Vite at /fonts/. Each declaration uses + * `font-display: swap` so the browser only fetches a weight when the + * font-family is actually applied to a rendered element — this keeps + * the initial page load lean even though there are ~22 files here. + * + * The `font-family` strings below MUST exactly match the `value` + * field of FONT_OPTIONS in `editable-cell.tsx`, otherwise TipTap's + * setFontFamily(value) will not match these declarations. + */ + +/* ── DIN Next LT Arabic ─────────────────────────────────────────── */ +@font-face { + font-family: "DIN Next LT Arabic"; + src: url("/fonts/DINNextLTArabic-UltraLight.ttf") format("truetype"); + font-weight: 200; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "DIN Next LT Arabic"; + src: url("/fonts/DINNextLTArabic-Regular.ttf") format("truetype"); + font-weight: 400; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "DIN Next LT Arabic"; + src: url("/fonts/DINNextLTArabic-Medium.ttf") format("truetype"); + font-weight: 500; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "DIN Next LT Arabic"; + src: url("/fonts/DINNextLTArabic-Bold.ttf") format("truetype"); + font-weight: 700; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "DIN Next LT Arabic"; + src: url("/fonts/DINNextLTArabic-Black.ttf") format("truetype"); + font-weight: 900; + font-style: normal; + font-display: swap; +} + +/* ── Tajawal ────────────────────────────────────────────────────── */ +@font-face { + font-family: "Tajawal"; + src: url("/fonts/Tajawal-ExtraLight.ttf") format("truetype"); + font-weight: 200; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Tajawal"; + src: url("/fonts/Tajawal-Light.ttf") format("truetype"); + font-weight: 300; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Tajawal"; + src: url("/fonts/Tajawal-Regular.ttf") format("truetype"); + font-weight: 400; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Tajawal"; + src: url("/fonts/Tajawal-Medium.ttf") format("truetype"); + font-weight: 500; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Tajawal"; + src: url("/fonts/Tajawal-Bold.ttf") format("truetype"); + font-weight: 700; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Tajawal"; + src: url("/fonts/Tajawal-ExtraBold.ttf") format("truetype"); + font-weight: 800; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Tajawal"; + src: url("/fonts/Tajawal-Black.ttf") format("truetype"); + font-weight: 900; + font-style: normal; + font-display: swap; +} + +/* ── Helvetica Neue LT Arabic ───────────────────────────────────── */ +@font-face { + font-family: "Helvetica Neue LT Arabic"; + src: url("/fonts/HelveticaNeueLTArabic-Light.ttf") format("truetype"); + font-weight: 300; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Helvetica Neue LT Arabic"; + src: url("/fonts/HelveticaNeueLTArabic-Roman.ttf") format("truetype"); + font-weight: 400; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Helvetica Neue LT Arabic"; + src: url("/fonts/HelveticaNeueLTArabic-Bold.ttf") format("truetype"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +/* ── Helvetica Neue (Latin) ─────────────────────────────────────── */ +@font-face { + font-family: "Helvetica Neue"; + src: url("/fonts/HelveticaNeue-Light.otf") format("opentype"); + font-weight: 300; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Helvetica Neue"; + src: url("/fonts/HelveticaNeue-Regular.ttf") format("truetype"); + font-weight: 400; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Helvetica Neue"; + src: url("/fonts/HelveticaNeue-Medium.otf") format("opentype"); + font-weight: 500; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Helvetica Neue"; + src: url("/fonts/HelveticaNeue-Bold.ttf") format("truetype"); + font-weight: 700; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Helvetica Neue"; + src: url("/fonts/HelveticaNeue-Black.otf") format("opentype"); + font-weight: 900; + font-style: normal; + font-display: swap; +} + +/* ── Majalla ────────────────────────────────────────────────────── */ +@font-face { + font-family: "Majalla"; + src: url("/fonts/Majalla-Regular.ttf") format("truetype"); + font-weight: 400; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Majalla"; + src: url("/fonts/Majalla-Bold.ttf") format("truetype"); + font-weight: 700; + font-style: normal; + font-display: swap; +} diff --git a/artifacts/tx-os/src/index.css b/artifacts/tx-os/src/index.css index 46d84140..5a6f6a16 100644 --- a/artifacts/tx-os/src/index.css +++ b/artifacts/tx-os/src/index.css @@ -1,5 +1,6 @@ @import "tailwindcss"; @import "tw-animate-css"; +@import "./custom-fonts.css"; @plugin "@tailwindcss/typography"; @custom-variant dark (&:is(.dark *)); diff --git a/artifacts/tx-os/src/pages/executive-meetings.tsx b/artifacts/tx-os/src/pages/executive-meetings.tsx index 844570c7..0d83c7ce 100644 --- a/artifacts/tx-os/src/pages/executive-meetings.tsx +++ b/artifacts/tx-os/src/pages/executive-meetings.tsx @@ -3683,6 +3683,7 @@ function AttendeesCell({ // in commitAddAttendee lands in the same place. pendingInsertAtIndex: pendingAttendee?.insertAtIndex, onStartAdd: hasAnyPending ? undefined : startAdd, + chainStartAdd: startAdd, onCommitAdd: commitAdd, onCancelAdd: cancelAdd, addLabel: t("executiveMeetings.schedule.addAttendee"), @@ -3872,6 +3873,18 @@ type AttendeeFlowSharedProps = { kind?: "person" | "subheading", insertAtIndex?: number, ) => void; + /** + * Same shape as `onStartAdd`, but always defined regardless of whether + * a pending row is currently open. Used by the Tab quick-add flow so + * the user can chain new attendees one after another even while a + * pending row is in flight (the parent's `hasAnyPending` guard would + * otherwise hide `onStartAdd` and break the chain). + */ + chainStartAdd?: ( + type: Attendee["attendanceType"], + kind?: "person" | "subheading", + insertAtIndex?: number, + ) => void; onCommitAdd?: ( type: Attendee["attendanceType"], html: string, @@ -3928,6 +3941,7 @@ function AttendeeFlow({ pendingKey, pendingInsertAtIndex, onStartAdd, + chainStartAdd, onCommitAdd, onCancelAdd, addLabel, @@ -4101,6 +4115,25 @@ function AttendeeFlow({ startInEditMode forceSaveOnBlur onCancel={onCancelAdd} + // Tab inside the pending row → commit it, then immediately + // open another pending row right after it. We use + // `chainStartAdd` (not `onStartAdd`) because the parent + // gates `onStartAdd` away while a pending row exists; the + // chain prop bypasses that guard. When `pendingInsertAtIndex` + // is undefined (legacy trailing slot) we leave it undefined + // so the next pending also opens at the trailing slot. + onTabNext={ + chainStartAdd + ? () => + chainStartAdd( + addType, + "person", + pendingInsertAtIndex !== undefined + ? pendingInsertAtIndex + 1 + : undefined, + ) + : undefined + } /> ); @@ -4226,6 +4259,16 @@ function AttendeeFlow({ // creates a real new paragraph inside the editor. className="inline-block align-baseline min-w-[3rem] min-h-[1.5rem] px-0.5 py-0.5 [&>span_p]:inline [&>span_p]:m-0 border-b border-dashed border-gray-400/70 hover:border-blue-500 data-[editing=true]:border-b-0 print:border-b-0 print:py-0 print:min-h-0" testId={`em-edit-attendee-${i}`} + // Tab → commit current name then open a pending person + // row immediately after this one in the same cell, so + // the user can keep typing names without mousing. + // We use `chainStartAdd` so the chain is reachable even + // when the parent has just moved into a pending state. + onTabNext={ + canMutate && chainStartAdd + ? () => chainStartAdd(addType, "person", i + 1) + : undefined + } /> ) : (