feat(native): swipe rows, long-press menus, placeholders while loading, fading pictures, iOS bounce and app icon quick actions

Packing items, tasks and expenses swipe sideways to their actions, as in
Mail or Reminders: done or open one way, delete and edit the other, the
first one run by a full swipe. They are second ways to what each row
already offers; an expense still asks before it goes.

A long press on a trip card lifts it and opens its menu with the card's own
actions. Plain confirmations in MConfirmSheet now also ask through the
system where the app can.

Lists, the atlas and a journey show grey placeholders in their shape while
loading instead of a spinner in an empty screen (MSkeleton); buttons keep
theirs. A picture still loading fades in once it is there; one from the
cache shows at once. In the iOS app the page bounces at its ends again,
except on screens with pull-to-refresh, whose pull it would race; Android
keeps its stretch off.

On iOS a long press on the app icon offers "New trip" and the trip coming
up by its name, set by the UI in its language (TrekShell.setQuickActions,
feature quickActions), and a tap opens it, also from a cold start.

The app's own CSS moves out of index.css into native/nativeApp.css, which
had outgrown its size budget.
This commit is contained in:
Maurice
2026-11-09 09:37:36 +01:00
parent c9a91bbdad
commit 80c04df4fc
44 changed files with 1315 additions and 120 deletions
+83
View File
@@ -0,0 +1,83 @@
/*
* What the UI does differently inside the iOS and Android app, keyed on the
* attributes native/nativeApp.ts puts on <html> there (data-native-app, and
* data-touch-input while a finger drives it). A browser never has them, so
* none of this reaches it. Loaded after index.css (main.tsx).
*/
/* Inside the iOS and Android app (native/nativeApp.ts sets the attribute): a long
press on a label, card or image does nothing, as in a native app, instead of
starting a text selection, a magnifier or a "save image" callout. What the user
types into stays selectable, or editing, pasting and moving the caret would break;
text worth copying opts in with .trek-selectable. A browser keeps its default. */
html[data-native-app] body {
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
}
html[data-native-app] :is(input, textarea, select, [contenteditable]:not([contenteditable='false']), .trek-selectable),
html[data-native-app] :is([contenteditable]:not([contenteditable='false']), .trek-selectable) * {
-webkit-user-select: text;
user-select: text;
-webkit-touch-callout: default;
}
html[data-native-app] img {
-webkit-user-drag: none;
}
/* A picture still loading waits unseen and fades in once it is there
(native/imageFade.ts). An animation rather than a transition, so an image's
own transitions (a hover zoom, a crossfade) stay as they are. */
html[data-native-app] img[data-fading] {
opacity: 0;
}
@keyframes trek-image-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (prefers-reduced-motion: no-preference) {
html[data-native-app]:not([data-reduce-motion]) img[data-faded] {
animation: trek-image-in 0.24s ease-out;
}
}
/* The press a native button shows the moment a finger lands on it. Most of the
UI says nothing until the finger lifts, which reads as lag. Only what has no
press of its own (no active: class), only the innermost pressed control, not
the card around a pressed button, and after a 60 ms wait, so a finger that
lands on a row to scroll the list does not flash it. Draggable rows show
their own pick-up; data-no-press opts a control out. */
html[data-native-app]
:is(button, a[href], [role='button'], [role='tab'], [role='menuitem'], [role='option'], summary):not(:disabled):not(
[aria-disabled='true']
):not([class*='active:']):not([draggable='true']):not([data-no-press]):not([data-pressing]):not(
:has(:is(button, a[href], [role='button']):active)
):active {
opacity: 0.55;
transition: opacity 0s linear 60ms;
}
/* A card held for its menu lifts a little under the finger
(mobile/components/useLongPress.ts), as a native context menu does. */
html[data-native-app] [data-long-press] {
transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
}
html[data-native-app] [data-long-press][data-pressing] {
transform: scale(0.97);
}
/* A tap focuses a button on Android, and a focus: ring (rather than
focus-visible:) then stays round it, which no native control shows. Focus
from a hardware keyboard is focus-visible and keeps its ring. */
html[data-touch-input]
:is(button, a, [role='button'], [role='tab'], [role='menuitem'], [role='option'], [tabindex]):focus:not(
:focus-visible
) {
outline: none;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}