Merge branch 'fix/v1475-mobile' into release/v1.47.5

# Conflicts:
#	src/__tests__/baselines/source-function-lines.json
#	src/__tests__/baselines/version-stamp-comments.json
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 22:51:30 +01:00
commit ad483fa4bc
52 files changed
+1838 -1027

No files matched your search

@@ -1,12 +1,12 @@
{
"$comment": "Production files over 800 lines and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
"count": 84,
"total": 93834,
"total": 93833,
"entries": {
"app/medications/page-client.tsx": 839,
"components/charts/mood-chart.tsx": 1166,
"components/cycle/log-day-sheet.tsx": 1639,
"components/insights/coach-panel/chat-bubble.tsx": 822,
"components/insights/coach-panel/chat-bubble.tsx": 821,
"components/insights/coach-panel/coach-conversation.tsx": 1278,
"components/insights/coach-panel/use-coach.ts": 1486,
"components/insights/derived/vitals-dashboard.tsx": 1033,
@@ -1,7 +1,7 @@
{
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
"count": 337,
"total": 109677,
"count": 336,
"total": 109345,
"entries": {
"app/achievements/page.tsx :: AchievementsPage": 206,
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 275,
@@ -53,29 +53,29 @@
"components/admin/api-token-overview-section.tsx :: ApiTokenOverviewSection": 212,
"components/admin/app-log-preview-section.tsx :: AppLogPreviewSection": 341,
"components/admin/central-codex-section.tsx :: CentralCodexSection": 228,
"components/admin/encryption-section.tsx :: EncryptionSection": 264,
"components/admin/encryption-section.tsx :: EncryptionSection": 261,
"components/admin/general-settings-section.tsx :: GeneralSettingsSection": 220,
"components/admin/glitchtip-section.tsx :: GlitchtipSection": 233,
"components/admin/glitchtip-section.tsx :: GlitchtipSection": 231,
"components/admin/host-metrics-chart.tsx :: HostMetricsChart": 232,
"components/admin/invite-tokens-section.tsx :: InviteTokensSection": 525,
"components/admin/login-overview-section.tsx :: LoginOverviewSection": 503,
"components/admin/pulse-day-mean-card.tsx :: PulseDayMeanCard": 305,
"components/admin/reminders-section.tsx :: RemindersSection": 309,
"components/admin/pulse-day-mean-card.tsx :: PulseDayMeanCard": 302,
"components/admin/reminders-section.tsx :: RemindersSection": 306,
"components/admin/user-management-section.tsx :: UserManagementSection": 630,
"components/admin/web-push-vapid-section.tsx :: WebPushVapidSection": 217,
"components/auth/claim-profile.tsx :: ClaimProfile": 429,
"components/auth/mfa-login-step.tsx :: MfaLoginStep": 217,
"components/charts/compliance-heatmap.tsx :: ComplianceHeatmap": 325,
"components/charts/health-chart.tsx :: HealthChart": 470,
"components/charts/health-chart.tsx :: HealthChart": 462,
"components/charts/medication-compliance-chart.tsx :: MedicationComplianceChart": 543,
"components/charts/mood-chart.tsx :: MoodChart": 896,
"components/charts/mood-heatmap.tsx :: MoodHeatmap": 296,
"components/charts/trend-card.tsx :: TrendCard": 365,
"components/charts/trend-card.tsx :: TrendCard": 351,
"components/clinician/clinician-view.tsx :: ClinicianView": 223,
"components/command-palette/command-palette.tsx :: PaletteBody": 333,
"components/custom-metrics/custom-metric-chart.tsx :: CustomMetricChart": 228,
"components/custom-metrics/custom-metric-detail.tsx :: CustomMetricDetail": 277,
"components/custom-metrics/custom-metric-form.tsx :: CustomMetricForm": 214,
"components/custom-metrics/custom-metric-form.tsx :: CustomMetricForm": 210,
"components/custom-metrics/custom-metric-history-list.tsx :: CustomMetricHistoryList": 298,
"components/cycle/bbt-chart.tsx :: BbtChart": 244,
"components/cycle/cycle-calendar.tsx :: CycleCalendar": 292,
@@ -99,10 +99,10 @@
"components/documents/sources/document-source-picker.tsx :: DocumentSourcePicker": 563,
"components/illness/log-day-sheet.tsx :: LogDaySheet": 265,
"components/illness/new-episode-sheet.tsx :: NewEpisodeSheet": 264,
"components/insights/coach-panel/chat-bubble.tsx :: ChatBubbleImpl": 419,
"components/insights/coach-panel/chat-bubble.tsx :: ChatBubbleImpl": 417,
"components/insights/coach-panel/coach-conversation.tsx :: CoachConversation": 1002,
"components/insights/coach-panel/coach-input.tsx :: CoachInput": 259,
"components/insights/coach-panel/coach-model-picker.tsx :: CoachModelPicker": 212,
"components/insights/coach-panel/coach-model-picker.tsx :: CoachModelPicker": 208,
"components/insights/coach-panel/conversations-panel.tsx :: ConversationsPanel": 317,
"components/insights/coach-panel/message-thread.tsx :: MessageThread": 390,
"components/insights/coach-panel/sources-rail.tsx :: SourcesRail": 203,
@@ -138,7 +138,6 @@
"components/labs/ocr-row-editor.tsx :: OcrRowEditor": 378,
"components/layout/auth-shell.tsx :: AuthShell": 518,
"components/layout/bottom-nav.tsx :: BottomNav": 262,
"components/layout/capture-picker.tsx :: CapturePicker": 235,
"components/layout/sidebar-nav.tsx :: SidebarNav": 356,
"components/layout/top-bar.tsx :: TopBar": 212,
"components/links/entity-link-picker.tsx :: EntityLinkPicker": 392,
@@ -147,28 +146,28 @@
"components/measurements/measurement-form.tsx :: MeasurementForm": 496,
"components/medications/detail/efficacy/efficacy-chart.tsx :: EfficacyChart": 361,
"components/medications/detail/medication-detail-tabs.tsx :: MedicationDetailTabs": 767,
"components/medications/dose-history-add-dialog.tsx :: LedgerAddDialog": 223,
"components/medications/dose-history-add-dialog.tsx :: LedgerAddDialog": 221,
"components/medications/dose-strength-curve.tsx :: DoseStrengthCurve": 266,
"components/medications/glp1-medication-card.tsx :: Glp1MedicationCard": 405,
"components/medications/injection-site-picker.tsx :: InjectionSitePicker": 287,
"components/medications/intake-history-list-v2.tsx :: IntakeHistoryListV2": 403,
"components/medications/intake-import-dialog.tsx :: IntakeImportDialog": 208,
"components/medications/intake-import-dialog.tsx :: IntakeImportDialog": 201,
"components/medications/log-intake-dialog.tsx :: LogIntakeDialog": 267,
"components/medications/medication-card.tsx :: MedicationCard": 413,
"components/medications/medication-table.tsx :: MedicationTableRowItem": 377,
"components/medications/scheduling/natural-language-extractor.tsx :: NaturalLanguageExtractor": 214,
"components/medications/scheduling/natural-language-extractor.tsx :: NaturalLanguageExtractor": 211,
"components/medications/scheduling/schedule-history-timeline.tsx :: EraDialog": 219,
"components/medications/scheduling/schedule-history-timeline.tsx :: ScheduleHistoryTimeline": 254,
"components/medications/sections/api-tokens-row.tsx :: ApiTokensRow": 296,
"components/medications/sections/api-tokens-row.tsx :: ApiTokensRow": 294,
"components/medications/sections/composition-section.tsx :: CompositionEditor": 362,
"components/medications/sections/inventory-dialogs.tsx :: AddInventoryDialog": 238,
"components/medications/sections/inventory-section.tsx :: InventorySection": 368,
"components/medications/side-effects-section.tsx :: SideEffectsSection": 518,
"components/medications/wizard/medication-wizard-dialog.tsx :: WizardDialogShell": 516,
"components/medications/wizard/medication-wizard-dialog.tsx :: WizardDialogShell": 514,
"components/medications/wizard/steps/step3-dose.tsx :: Step3Dose": 222,
"components/medications/wizard/steps/step8-summary.tsx :: Step8Summary": 230,
"components/mood/manage/tag-groups-card.tsx :: TagGroupsCard": 256,
"components/mood/manage/tag-manager-card.tsx :: TagManagerCard": 270,
"components/mood/manage/tag-groups-card.tsx :: TagGroupsCard": 252,
"components/mood/manage/tag-manager-card.tsx :: TagManagerCard": 261,
"components/mood/mood-form.tsx :: MoodForm": 514,
"components/mood/mood-list.tsx :: MoodList": 1109,
"components/mood/mood-tag-picker.tsx :: MoodTagPicker": 257,
@@ -186,8 +185,8 @@
"components/settings/access/managed-profile-edit-form.tsx :: ManagedProfileEditFields": 208,
"components/settings/access/managed-profile-guardians.tsx :: ManagedProfileGuardians": 234,
"components/settings/account-section/index.tsx :: AccountSection": 659,
"components/settings/ai/codex-provider-form.tsx :: CodexProviderForm": 280,
"components/settings/ai/fallback-chain-card.tsx :: FallbackChainCard": 228,
"components/settings/ai/codex-provider-form.tsx :: CodexProviderForm": 270,
"components/settings/ai/fallback-chain-card.tsx :: FallbackChainCard": 224,
"components/settings/ai/runtime-actions-row.tsx :: RuntimeActionsRow": 284,
"components/settings/api-section.tsx :: ApiTokensCard": 269,
"components/settings/coach-nudge-card.tsx :: CoachNudgeCard": 251,
@@ -195,7 +194,7 @@
"components/settings/environment-section.tsx :: EnvironmentSection": 666,
"components/settings/import-panel/apple-health-import-card.tsx :: AppleHealthImportCard": 298,
"components/settings/import-panel/csv-import-card.tsx :: CsvImportCard": 212,
"components/settings/import-panel/dose-history-import-card.tsx :: DoseHistoryImportCard": 225,
"components/settings/import-panel/dose-history-import-card.tsx :: DoseHistoryImportCard": 222,
"components/settings/import-panel/health-connect-import-card.tsx :: HealthConnectImportCard": 293,
"components/settings/import-panel/record-csv-import-card.tsx :: RecordCsvImportSheet": 458,
"components/settings/insights-pill-order-section.tsx :: InsightsPillOrderSection": 255,
@@ -210,7 +209,7 @@
"components/settings/report-selection/selection-panel.tsx :: HealthRecordExportPanel": 370,
"components/settings/score-section.tsx :: ScoreSection": 383,
"components/settings/security-section/passkey-list-section.tsx :: PasskeyListSection": 395,
"components/settings/security-section/security-keys-card.tsx :: SecurityKeysCard": 288,
"components/settings/security-section/security-keys-card.tsx :: SecurityKeysCard": 286,
"components/settings/security-section/totp-card.tsx :: TotpCard": 533,
"components/settings/security-sessions-card.tsx :: SecuritySessionsCard": 245,
"components/settings/settings-shell.tsx :: SettingsShell": 254,
@@ -221,8 +220,8 @@
"components/settings/web-push-card.tsx :: WebPushCard": 215,
"components/symptoms/symptom-entry-form.tsx :: SymptomEntryForm": 317,
"components/symptoms/symptoms-section.tsx :: SymptomsSection": 205,
"components/targets/range-bar.tsx :: RangeBar": 220,
"components/targets/target-edit-sheet.tsx :: TargetEditSheetBody": 474,
"components/targets/range-bar.tsx :: RangeBar": 215,
"components/targets/target-edit-sheet.tsx :: TargetEditSheetBody": 473,
"components/timeline/life-event-form.tsx :: LifeEventForm": 281,
"components/timeline/timeline-chart.tsx :: ChartBody": 236,
"components/timeline/timeline-chronicle.tsx :: TimelineChronicle": 246,
@@ -1,7 +1,7 @@
{
"$comment": "Comment lines that open with a release version, per file under src/. Maintained by src/__tests__/version-stamp-ratchet.test.ts.",
"count": 2884,
"total": 9191,
"count": 2883,
"total": 9186,
"entries": {
"__tests__/account-payload-consumer-guard.test.ts": 2,
"__tests__/acting-account-boundary-guard.test.ts": 7,
@@ -80,7 +80,6 @@
"app/__tests__/not-found-copy.test.ts": 1,
"app/__tests__/quick-add-labels.test.ts": 3,
"app/__tests__/root-loading-silhouette.test.tsx": 1,
"app/__tests__/sheet-dirty-check.test.ts": 1,
"app/about/page.tsx": 1,
"app/achievements/__tests__/page.test.tsx": 2,
"app/achievements/page.tsx": 4,
@@ -808,7 +807,7 @@
"components/dashboard/chart-reveal.tsx": 1,
"components/dashboard/dashboard-gates.ts": 9,
"components/dashboard/medication-intake-quick-add.tsx": 11,
"components/dashboard/quick-entry-sheets.tsx": 6,
"components/dashboard/quick-entry-sheets.tsx": 3,
"components/dashboard/range-display.tsx": 1,
"components/dashboard/recent-workouts-tile.tsx": 3,
"components/data-list/__tests__/selection.test.ts": 1,
@@ -1096,7 +1095,7 @@
"components/layout/account-switcher-menu.tsx": 1,
"components/layout/auth-shell.tsx": 14,
"components/layout/bottom-nav.tsx": 8,
"components/layout/capture-picker.tsx": 5,
"components/layout/capture-picker.tsx": 4,
"components/layout/nav-model.ts": 17,
"components/layout/offline-banner.tsx": 3,
"components/layout/record-session-fence-bridge.tsx": 1,
+197 -3
View File
@@ -9,17 +9,21 @@
* 5 px content box and pushed the logo across its bottom border).
* 2. The stylesheet carries the rules the shell and the keyboard bridge
* depend on.
* 3. Every form-field primitive keeps 16 px type on a touch screen: iOS
* Safari zooms the page into a focused field below 16 px, and a width
* 3. Every form field keeps 16 px type on a touch screen: iOS Safari
* zooms the page into a focused field below 16 px, and a width
* breakpoint alone (`md:text-sm`) handed 14 px to a phone held sideways
* and to every iPad. The small size is allowed only behind
* `pointer-fine:`.
* `pointer-fine:`. That holds for the primitives and for every call
* site, whose `text-sm` replaces the primitive's size, and for a raw
* `<textarea>` styled from a class constant.
*/
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import ts from "typescript";
import { describe, expect, it } from "vitest";
import { stripComments } from "./helpers/source-files";
import { parse, productionSources, readSrc } from "./helpers/source-metrics";
const ROOT = resolve(__dirname, "../..");
const read = (path: string) => readFileSync(resolve(ROOT, path), "utf8");
@@ -125,6 +129,24 @@ describe("the shell owns the safe area", () => {
expect(bottomClass).not.toMatch(/(?<![\w-])bottom-/);
});
it("the More sheet's grid scrolls inside the sheet's height cap", () => {
// Held sideways a phone keeps the bottom bar, and 90 dvh of a 393 px
// screen is shorter than the hub: the last rows sat below the edge.
const nav = code(read("src/components/layout/bottom-nav.tsx"));
const grid = nav.match(/"grid [^"]*grid-cols-2[^"]*"/)?.[0] ?? "";
expect(grid).toMatch(/\bmin-h-0\b/);
expect(grid).toMatch(/\boverflow-y-auto\b/);
expect(grid).toMatch(/\boverscroll-contain\b/);
});
it("the onboarding tour's bottom card clears the home indicator", () => {
const tour = code(read("src/components/onboarding/tour.tsx"));
const card = tour.match(/"[^"]*absolute inset-x-2 [^"]*"/)?.[0] ?? "";
expect(card).toMatch(
/\bbottom-\[calc\(env\(safe-area-inset-bottom,0px\)\+0\.5rem\)\]/,
);
});
it("the sheet primitives name their side for those rules", () => {
expect(read("src/components/ui/sheet.tsx")).toMatch(
/data-sheet-side=\{side\}/,
@@ -165,3 +187,175 @@ describe("form fields keep 16 px on a touch screen", () => {
},
);
});
/** Elements a person types into. */
const FIELD_TAGS = new Set([
"input",
"textarea",
"select",
"Input",
"Textarea",
"PasswordInput",
"NativeSelect",
]);
/** `type` values of an `<input>` nobody types into. */
const NOT_TYPED = new Set([
"checkbox",
"radio",
"range",
"file",
"hidden",
"color",
"button",
"submit",
"reset",
"image",
]);
/** A type size under 16 px that is not behind `pointer-fine:`. */
const SMALL_TYPE =
/(?<![\w:-])(?:(?:sm|md|lg|xl):)?text-(?:2xs|xs|sm)(?![\w-])/g;
/** Every string a `className` expression can produce, constants resolved. */
function classStrings(
node: ts.Node,
constants: ReadonlyMap<string, string>,
): string[] {
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
return [node.text];
}
if (ts.isTemplateExpression(node)) {
return [
node.head.text,
...node.templateSpans.flatMap((span) => [
...classStrings(span.expression, constants),
span.literal.text,
]),
];
}
if (ts.isIdentifier(node)) {
const value = constants.get(node.text);
return value === undefined ? [] : [value];
}
const out: string[] = [];
node.forEachChild((child) => {
out.push(...classStrings(child, constants));
});
return out;
}
/** `rel:line <Tag> class` for each field whose classes go under 16 px. */
function smallFields(
rel: string,
text: string,
): {
fields: number;
small: string[];
} {
const sf = parse(rel, text);
const constants = new Map<string, string>();
sf.forEachChild((statement) => {
if (!ts.isVariableStatement(statement)) return;
for (const decl of statement.declarationList.declarations) {
if (
ts.isIdentifier(decl.name) &&
decl.initializer &&
(ts.isStringLiteral(decl.initializer) ||
ts.isNoSubstitutionTemplateLiteral(decl.initializer))
) {
constants.set(decl.name.text, decl.initializer.text);
}
}
});
let fields = 0;
const small: string[] = [];
const visit = (node: ts.Node) => {
if (
(ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) &&
FIELD_TAGS.has(node.tagName.getText(sf))
) {
const attribute = (name: string) =>
node.attributes.properties.find(
(prop): prop is ts.JsxAttribute =>
ts.isJsxAttribute(prop) && prop.name.getText(sf) === name,
);
const type = attribute("type")?.initializer;
const typed = !(
type &&
ts.isStringLiteral(type) &&
NOT_TYPED.has(type.text)
);
const className = attribute("className")?.initializer;
if (typed && className) {
fields += 1;
const line = sf.getLineAndCharacterOfPosition(node.getStart()).line + 1;
for (const cls of classStrings(className, constants)) {
for (const match of cls.matchAll(SMALL_TYPE)) {
small.push(
`${rel}:${line} <${node.tagName.getText(sf)}> ${match[0]}`,
);
}
}
}
}
node.forEachChild(visit);
};
visit(sf);
return { fields, small };
}
describe("no field call site goes under 16 px on a touch screen", () => {
/**
* Call sites still under 16 px, fixed by a change of their own. An entry
* that stops matching fails, so the list only shrinks.
*/
const OPEN: Readonly<Record<string, string>> = {
"components/settings/coach-memory-section.tsx":
"/settings/coach memory editor",
"components/settings/access/managed-profile-handover.tsx":
"read-only handover link field",
"components/cycle/cycle-settings.tsx": "raw cycle-length number field",
"components/command-palette/command-palette.tsx":
"search field shrinks at `md:` without `pointer-fine:`",
};
const scan = productionSources()
.filter((rel) => rel.endsWith(".tsx"))
.map((rel) => ({ rel, ...smallFields(rel, readSrc(rel)) }));
it("reads the field call sites (floor)", () => {
const fields = scan.reduce((sum, file) => sum + file.fields, 0);
expect(fields).toBeGreaterThan(90);
});
it("finds a class constant on a raw field, and spares fine-pointer sizes and checkboxes", () => {
const { small } = smallFields(
"probe.tsx",
[
'const FIELD_CLASSES = "border-input w-full text-sm";',
"export const A = () => (",
" <>",
' <textarea className={cn(FIELD_CLASSES, "min-h-16")} />',
' <Textarea className="font-mono sm:pointer-fine:text-xs" />',
' <input type="checkbox" className="text-sm" />',
' <Input className={`h-9 ${wide ? "md:text-sm" : ""}`} />',
" </>",
");",
].join("\n"),
);
expect(small).toEqual([
"probe.tsx:4 <textarea> text-sm",
"probe.tsx:7 <Input> md:text-sm",
]);
});
it("no typed-into field sets a size under 16 px outside a fine pointer", () => {
const offenders = scan
.filter((file) => !(file.rel in OPEN))
.flatMap((file) => file.small);
expect(offenders).toEqual([]);
});
it.each(Object.keys(OPEN))("%s is still open", (rel) => {
expect(scan.find((file) => file.rel === rel)?.small ?? []).not.toEqual([]);
});
});
@@ -41,9 +41,6 @@ const TEXT_FIELD_FILES = [
"src/components/ui/calendar.tsx",
"src/components/insights/coach-panel/coach-input.tsx",
"src/components/cycle/cycle-settings.tsx",
"src/components/records/about-me-note-manager.tsx",
"src/components/records/conditions-manager.tsx",
"src/components/records/allergy-free-text-note.tsx",
];
const FOCUS_VARIANT = /(^|:)(focus|focus-visible|focus-within|has-focus)(:|$)/;
@@ -24,6 +24,8 @@ const ALLOWED: Record<string, string> = {
"lib/url-state.ts": "the helper itself",
"components/clinician/share-unlock-gate.tsx":
"scrubs a passphrase from the fragment synchronously, before the unlock request leaves; no query state",
"components/ui/sheet-history.ts":
"a sheet's own history entry: the page's URL unchanged, never query state",
"components/documents/vault/selection-history.ts":
"closeDocumentSelectionHistoryEntry takes a history-shaped argument for its tests; the page passes `urlStateHistory`",
};