mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-10 22:31:01 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 322d5d5209 | |||
| 0346f9c13d | |||
| dfd87d413e | |||
| 27aab5c783 | |||
| a7cc1b9e57 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"src": {
|
||||
"@typescript-eslint/no-non-null-assertion": 375,
|
||||
"@typescript-eslint/no-misused-promises": 847,
|
||||
"@typescript-eslint/no-misused-promises": 845,
|
||||
"no-console": 88,
|
||||
"complexity": 218,
|
||||
"@typescript-eslint/no-explicit-any": 144,
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"tests": {
|
||||
"@typescript-eslint/no-explicit-any": 489,
|
||||
"@typescript-eslint/no-non-null-assertion": 1912,
|
||||
"@typescript-eslint/no-non-null-assertion": 1911,
|
||||
"@typescript-eslint/no-misused-promises": 22,
|
||||
"@typescript-eslint/no-unused-vars": 51,
|
||||
"@typescript-eslint/no-floating-promises": 33,
|
||||
|
||||
@@ -79,7 +79,6 @@
|
||||
"hooks/useAnchoredPosition.ts": 3,
|
||||
"hooks/useElementRect.ts": 3,
|
||||
"mobile/components/MSegmented.tsx": 1,
|
||||
"mobile/components/MSheet.tsx": 1,
|
||||
"mobile/screens/journey/MJourneyDetail.tsx": 3,
|
||||
"mobile/screens/journey/MJourneyEntrySheet.tsx": 1,
|
||||
"mobile/screens/trip/map/MMapArea.tsx": 3,
|
||||
|
||||
@@ -211,7 +211,15 @@ vi.mock('../../services/photoService', () => ({
|
||||
const shell = vi.hoisted(() => ({ phone: undefined as boolean | undefined }))
|
||||
vi.mock('../../mobile/useIsPhone', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../mobile/useIsPhone')>()
|
||||
return { ...actual, useIsPhone: () => shell.phone ?? actual.useIsPhone() }
|
||||
// The real hook runs every time, so a test that changes the answer never changes the
|
||||
// number of hooks a mounted map has (React throws on that).
|
||||
return {
|
||||
...actual,
|
||||
useIsPhone: () => {
|
||||
const real = actual.useIsPhone()
|
||||
return shell.phone ?? real
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('./gcj02Crs', () => ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChevronLeft, ChevronRight, FolderSync, List, Plus, Rows3 } from 'lucide-react';
|
||||
import { useLayoutEffect, useState, type ReactNode } from 'react';
|
||||
import { useCallback, useLayoutEffect, useRef, useState, type ReactNode } from 'react';
|
||||
import { canManageDocSync } from '../../../components/Files/docsync/useDocSync';
|
||||
import { useDocSyncOffered } from '../../../components/Files/docsync/useDocSyncOffered';
|
||||
import { useIsWide } from '../../../hooks/sizeClass';
|
||||
@@ -26,14 +26,21 @@ import { collabBoardFor, collabSideBySide } from './tabs/collabPanes';
|
||||
export function GlassSegment({ children, activeKey }: { children: ReactNode; activeKey?: string }) {
|
||||
const [region, setRegion] = useState<HTMLDivElement | null>(null);
|
||||
const [track, setTrack] = useState<HTMLDivElement | null>(null);
|
||||
// The element is also kept in a ref: the fit below writes to it, and state is not to be written.
|
||||
const trackRef = useRef<HTMLDivElement | null>(null);
|
||||
const attachTrack = useCallback((el: HTMLDivElement | null) => {
|
||||
trackRef.current = el;
|
||||
setTrack(el);
|
||||
}, []);
|
||||
const fade = useEdgeFade(track);
|
||||
useLayoutEffect(() => {
|
||||
if (!region || !track) return;
|
||||
const el = trackRef.current;
|
||||
if (!region || !el) return;
|
||||
const fit = () => {
|
||||
track.dataset.tight = 'false';
|
||||
track.dataset.tight = track.scrollWidth > track.clientWidth + 0.5 ? 'true' : 'false';
|
||||
const active = track.querySelector<HTMLElement>('[aria-pressed="true"]');
|
||||
if (active && track.scrollWidth > track.clientWidth + 0.5) {
|
||||
el.dataset.tight = 'false';
|
||||
el.dataset.tight = el.scrollWidth > el.clientWidth + 0.5 ? 'true' : 'false';
|
||||
const active = el.querySelector<HTMLElement>('[aria-pressed="true"]');
|
||||
if (active && el.scrollWidth > el.clientWidth + 0.5) {
|
||||
// 'nearest' on both axes: the shell root is fixed, anything else scrolls the page behind it.
|
||||
active.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
}
|
||||
@@ -49,7 +56,7 @@ export function GlassSegment({ children, activeKey }: { children: ReactNode; act
|
||||
className="pointer-events-none absolute inset-y-0 end-12 start-12 flex items-center justify-center"
|
||||
>
|
||||
<div className="pointer-events-auto flex min-w-0 max-w-full rounded-full border border-[color:var(--m-gbr)] bg-[color:var(--m-glass)] p-[3px] backdrop-blur-[24px] backdrop-saturate-[1.7]">
|
||||
<div ref={setTrack} className={`m-seg flex min-w-0 overflow-x-auto rounded-full ${edgeFadeClass(fade)}`}>
|
||||
<div ref={attachTrack} className={`m-seg flex min-w-0 overflow-x-auto rounded-full ${edgeFadeClass(fade)}`}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -182,7 +182,7 @@ export function AddPaymentSheet({
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
placeholder={zeroAmountInput(currency)}
|
||||
aria-label={t('costs.amount')}
|
||||
className="min-w-0 flex-1 border-0 bg-transparent p-0 text-[0.84375rem] font-semibold text-m-ink outline-none [font-variant-numeric:tabular-nums] placeholder:font-normal placeholder:text-m-faint"
|
||||
className="min-w-0 flex-1 border-0 bg-transparent p-0 text-body font-semibold text-m-ink outline-none [font-variant-numeric:tabular-nums] placeholder:font-normal placeholder:text-m-faint"
|
||||
/>
|
||||
<span className="flex-none font-geist text-caption font-bold text-m-faint">{currency}</span>
|
||||
</div>
|
||||
|
||||
@@ -123,7 +123,10 @@ export function FileTile({ file, selected, canStar, onPick, onStar, onMenu, t }:
|
||||
)}
|
||||
</span>
|
||||
<span className="flex flex-1 flex-col gap-[6px] px-3 pb-[10px] pt-2">
|
||||
<span dir="auto" className="line-clamp-2 break-words text-[length:calc(12.5px*var(--fs-scale-body,1))] font-bold leading-[1.3] text-m-ink">
|
||||
<span
|
||||
dir="auto"
|
||||
className="line-clamp-2 break-words text-[length:calc(12.5px*var(--fs-scale-body,1))] font-bold leading-[1.3] text-m-ink"
|
||||
>
|
||||
{file.original_name}
|
||||
</span>
|
||||
<span className="mt-auto flex flex-wrap items-center gap-1">
|
||||
@@ -166,7 +169,10 @@ export function FileTile({ file, selected, canStar, onPick, onStar, onMenu, t }:
|
||||
function Badge({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
// A size, a date or a name, each in its own direction ("46.1 KB" stays so in Arabic).
|
||||
<span dir="auto" className="whitespace-nowrap rounded-full bg-[color:var(--m-ic)] px-[7px] py-[2px] font-geist text-[length:calc(9px*var(--fs-scale-caption,1))] font-bold text-m-muted">
|
||||
<span
|
||||
dir="auto"
|
||||
className="whitespace-nowrap rounded-full bg-[color:var(--m-ic)] px-[7px] py-[2px] font-geist text-[length:calc(9px*var(--fs-scale-caption,1))] font-bold text-m-muted"
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
@@ -181,6 +187,43 @@ interface FilePreviewPaneProps {
|
||||
onMenu: () => void;
|
||||
}
|
||||
|
||||
/** The pane's heading: the type's tile when nothing is shown in place, the name, and what the file is. */
|
||||
function FileHeading({ file, shownInPlace }: { file: TripFile; shownInPlace: boolean }) {
|
||||
const { locale } = useTranslation();
|
||||
const meta = getFileTypeMeta(file);
|
||||
return (
|
||||
<div className="flex items-start gap-3">
|
||||
{!shownInPlace && (
|
||||
<span className="relative block h-[92px] w-[74px] flex-none overflow-hidden rounded-xl border border-[color:var(--m-rowbr)] bg-m-sheetop">
|
||||
<TypeArt file={file} />
|
||||
</span>
|
||||
)}
|
||||
<div className={`min-w-0 flex-1 ${shownInPlace ? '' : 'pt-1'}`}>
|
||||
<h3 className="break-words text-[length:calc(16px*var(--fs-scale-subtitle,1))] font-extrabold leading-[1.3] text-m-ink">
|
||||
<bdi>{file.original_name}</bdi>
|
||||
</h3>
|
||||
<div className="mt-[6px] flex flex-wrap items-center gap-1">
|
||||
<span
|
||||
className="rounded-full px-[7px] py-[2px] font-geist text-[length:calc(9px*var(--fs-scale-caption,1))] font-extrabold tracking-[.06em]"
|
||||
style={{ background: `${meta.color}1f`, color: meta.color }}
|
||||
>
|
||||
{meta.label}
|
||||
</span>
|
||||
{isWalletPass(file.mime_type, file.original_name) && (
|
||||
<span className="flex items-center gap-[3px] rounded-full bg-m-act px-[7px] py-[2px] font-geist text-[length:calc(9px*var(--fs-scale-caption,1))] font-bold text-m-actfg">
|
||||
<Wallet size={9} strokeWidth={2.4} aria-hidden="true" />
|
||||
{APPLE_WALLET}
|
||||
</span>
|
||||
)}
|
||||
{!!file.file_size && <Badge>{formatSize(file.file_size)}</Badge>}
|
||||
<Badge>{formatFileDate(file.created_at, locale)}</Badge>
|
||||
{file.uploaded_by_name && <Badge>{file.uploaded_by_name}</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The file picked in the grid, shown beside it once the window has room for
|
||||
* two panes, the way Files on an iPad previews a document: the picture or the
|
||||
@@ -191,8 +234,6 @@ interface FilePreviewPaneProps {
|
||||
*/
|
||||
export function FilePreviewPane({ file, planner, onOpen, onStar, onLinks, onMenu }: FilePreviewPaneProps) {
|
||||
const { t } = planner;
|
||||
const { locale } = useTranslation();
|
||||
const meta = getFileTypeMeta(file);
|
||||
const image = isImage(file.mime_type);
|
||||
const pdf = isPdf(file) && pdfInPage();
|
||||
const src = useSignedUrl(file.url, image || pdf);
|
||||
@@ -229,35 +270,7 @@ export function FilePreviewPane({ file, planner, onOpen, onStar, onLinks, onMenu
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
{!shownInPlace && (
|
||||
<span className="relative block h-[92px] w-[74px] flex-none overflow-hidden rounded-xl border border-[color:var(--m-rowbr)] bg-m-sheetop">
|
||||
<TypeArt file={file} />
|
||||
</span>
|
||||
)}
|
||||
<div className={`min-w-0 flex-1 ${shownInPlace ? '' : 'pt-1'}`}>
|
||||
<h3 className="break-words text-[length:calc(16px*var(--fs-scale-subtitle,1))] font-extrabold leading-[1.3] text-m-ink">
|
||||
<bdi>{file.original_name}</bdi>
|
||||
</h3>
|
||||
<div className="mt-[6px] flex flex-wrap items-center gap-1">
|
||||
<span
|
||||
className="rounded-full px-[7px] py-[2px] font-geist text-[length:calc(9px*var(--fs-scale-caption,1))] font-extrabold tracking-[.06em]"
|
||||
style={{ background: `${meta.color}1f`, color: meta.color }}
|
||||
>
|
||||
{meta.label}
|
||||
</span>
|
||||
{isWalletPass(file.mime_type, file.original_name) && (
|
||||
<span className="flex items-center gap-[3px] rounded-full bg-m-act px-[7px] py-[2px] font-geist text-[length:calc(9px*var(--fs-scale-caption,1))] font-bold text-m-actfg">
|
||||
<Wallet size={9} strokeWidth={2.4} aria-hidden="true" />
|
||||
{APPLE_WALLET}
|
||||
</span>
|
||||
)}
|
||||
{!!file.file_size && <Badge>{formatSize(file.file_size)}</Badge>}
|
||||
<Badge>{formatFileDate(file.created_at, locale)}</Badge>
|
||||
{file.uploaded_by_name && <Badge>{file.uploaded_by_name}</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FileHeading file={file} shownInPlace={shownInPlace} />
|
||||
|
||||
{(linked.places.length > 0 || linked.reservations.length > 0 || file.note_id != null) && (
|
||||
<div className="flex flex-wrap gap-[6px]">
|
||||
|
||||
@@ -374,7 +374,9 @@ export function useAtlas(options: { chromeInsets?: () => AtlasChromeInsets } = {
|
||||
|
||||
mapInstance.current = map
|
||||
// The opening view centres the world beside the screen's chrome, not behind the rail and under the header.
|
||||
const pan = chromePan(chromeInsetsRef.current?.(), map.latLngToContainerPoint([NORTH_EDGE_LAT, 0]).y)
|
||||
// Measured only where there is chrome to clear: the desktop atlas opens exactly as before.
|
||||
const insets = chromeInsetsRef.current?.()
|
||||
const pan = insets ? chromePan(insets, map.latLngToContainerPoint([NORTH_EDGE_LAT, 0]).y) : null
|
||||
if (pan) map.panBy(pan, { animate: false })
|
||||
// Both renderers live as long as the map does (see the note on the refs); map.remove()
|
||||
// disposes them along with every other layer it holds.
|
||||
|
||||
@@ -162,7 +162,8 @@ describe('useJourneyDetail', () => {
|
||||
setup({ search: '?create=entry' });
|
||||
await waitFor(() => expect(latest.editingEntry).not.toBeNull());
|
||||
expect(latest.editingEntry).toMatchObject({ id: 0, journey_id: 7, type: 'entry', visibility: 'private' });
|
||||
expect(screen.getByTestId('search')).toHaveTextContent('');
|
||||
// The parameter goes in the same effect but reaches the URL a render later.
|
||||
await waitFor(() => expect(screen.getByTestId('search')).toBeEmptyDOMElement());
|
||||
});
|
||||
|
||||
it('FE-JRN-DETHOOK-004: a viewer never gets the draft editor', async () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { useRef, useState } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { sizeClassOf } from '../../../src/hooks/sizeClass';
|
||||
import { useAutoGrow } from '../../../src/hooks/useAutoGrow';
|
||||
import MSheet from '../../../src/mobile/components/MSheet';
|
||||
import { edgeFadeClass, useEdgeFade } from '../../../src/mobile/components/useEdgeFade';
|
||||
@@ -115,7 +116,8 @@ describe('useEdgeFade on the vertical axis', () => {
|
||||
Object.defineProperty(el, 'scrollHeight', { configurable: true, value: 800 });
|
||||
await act(async () => {
|
||||
el.appendChild(document.createElement('div'));
|
||||
await Promise.resolve();
|
||||
// Measured on the next frame, so a burst of changes is measured once.
|
||||
await new Promise((resolve) => setTimeout(resolve, 40));
|
||||
});
|
||||
expect(result.current.end).toBe(true);
|
||||
});
|
||||
@@ -162,7 +164,12 @@ describe('form sheet stylesheet', () => {
|
||||
|
||||
it('FE-MOB-FORMSHEET-010: the short-window variant is the other side of the wide size class height bound', () => {
|
||||
expect(tailwind).toContain("addVariant('short', '@media (max-height: 500px)')");
|
||||
expect(tailwind).toContain("addVariant('wide', '@media (min-width: 600px) and (min-height: 501px)')");
|
||||
// `wide:` follows the size class the shell writes (data-size), whose height bound starts at 501.
|
||||
expect(tailwind).toContain(
|
||||
`addVariant('wide', ':where(html:has(.m-shell[data-size="wide"], .m-shell[data-size="split"])) &')`
|
||||
);
|
||||
expect(sizeClassOf(1024, 500)).toBe('compact');
|
||||
expect(sizeClassOf(1024, 501)).not.toBe('compact');
|
||||
});
|
||||
|
||||
it('FE-MOB-FORMSHEET-011: on a short window the actions sit beside the title and the fields take the rest', () => {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="node" />
|
||||
import type { TranslationStrings } from './types';
|
||||
|
||||
import { existsSync, readdirSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
@@ -11,7 +14,18 @@ import { describe, expect, it } from 'vitest';
|
||||
* reads, and the style is held here because a re-translation can bring a dash
|
||||
* back without any other check noticing.
|
||||
*/
|
||||
const nativeModules = import.meta.glob<{ default: TranslationStrings }>('./*/native.ts', { eager: true });
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const locales = readdirSync(here, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && existsSync(join(here, entry.name, 'native.ts')))
|
||||
.map((entry) => entry.name);
|
||||
const nativeModules: Record<string, { default: TranslationStrings }> = Object.fromEntries(
|
||||
await Promise.all(
|
||||
locales.map(async (locale) => [
|
||||
`./${locale}/native.ts`,
|
||||
(await import(`./${locale}/native.ts`)) as { default: TranslationStrings },
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
const DASH = /[‒–—―]| - /;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user