From 1bdd99692cbfaafc883b1ad89e867ee98072566b Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Thu, 29 Oct 2026 14:17:08 +0100 Subject: [PATCH] feat(cards): apply the design rules to chips, rows, folders, FABs and selection Page count, code and favorite are one pill top-right (22 tall, hairlines between segments), the title chip is alone. Only the grid scales selected tiles and list rows get a tonal highlight. Rows are 72 tall with a 96x60 thumbnail, no chevron on Android, no subtitle for name-only cards. Folder rows are 52 tall with a 32 circle and a 20 outline icon. The Android FABs are a 40 add button above a 56 extended Scan button with the elevation of floating controls. While selecting, Android shows a themed top bar with Favorite, Move and Delete plus an overflow (the bottom toolbar is gone) and iOS keeps the system toolbar with Share, Favorite, Move, Delete. Co-Authored-By: Claude Sonnet 5.5 --- app/app.android.scss | 28 ++++++++-------- app/app.common.scss | 6 ++-- app/components/common/CardChip.svelte | 27 ++++++++++++---- app/components/list/CardListCell.svelte | 43 ++++++++++++++----------- app/components/list/CardsList.svelte | 4 +-- app/components/list/FolderCell.svelte | 14 ++++---- app/components/list/MainList.svelte | 40 +++++++++++++++++++---- 7 files changed, 105 insertions(+), 57 deletions(-) diff --git a/app/app.android.scss b/app/app.android.scss index bbe65211..6026ead7 100644 --- a/app/app.android.scss +++ b/app/app.android.scss @@ -17,7 +17,7 @@ button[variant='contained'] { .cardChip { background-color: var(--colorPrimaryContainer); border-color: transparent; - border-radius: 10; + border-radius: 11; border-width: 1; } @@ -40,27 +40,27 @@ textfield[variant='outline'] { stroke-inactive-color: var(--colorOutline); } -// the floating buttons are fully round +// the floating buttons: a small tonal add button above the extended Scan button, both with the elevation of floating controls .addFab { background-color: var(--colorSecondaryContainer); + border-radius: 12; color: var(--colorOnSecondaryContainer); - elevation: 2; + elevation: 6; font-size: 24; - height: 48; - margin: 8; + height: 40; + horizontal-align: right; + margin: 0 16 12 16; padding: 0; - shape: round; - vertical-align: center; - width: 48; + width: 40; } .scanFab { - elevation: 3; - font-size: 16; - font-weight: 600; + border-radius: 18; + elevation: 6; + font-size: 14; + font-weight: 500; height: 56; - margin: 16; + horizontal-align: right; + margin: 0 16 16 16; padding: 0 20; - shape: round; - vertical-align: center; } diff --git a/app/app.common.scss b/app/app.common.scss index 28cf3c1f..1bf7dfc2 100644 --- a/app/app.common.scss +++ b/app/app.common.scss @@ -322,11 +322,11 @@ textfield, } .cardChip { + height: 22; horizontal-align: left; - min-height: 24; - min-width: 24; + min-width: 22; + padding: 0; vertical-align: middle; - padding: 4 8; } .cardsEmptyIcon { diff --git a/app/components/common/CardChip.svelte b/app/components/common/CardChip.svelte index d4ea8933..14fdd1fa 100644 --- a/app/components/common/CardChip.svelte +++ b/app/components/common/CardChip.svelte @@ -2,22 +2,37 @@ import CardIcon from '~/components/common/CardIcon.svelte'; import { colors } from '~/variables'; + interface ChipSegment { + icon?: string; + text?: string; + } + export let text: string = null; export let icon: string = null; + /** several segments are one pill with a hairline between them */ + export let segments: ChipSegment[] = null; export let fontWeight: any = '600'; let { colorOnPrimaryContainer, colorOnSurface } = $colors; $: ({ colorOnPrimaryContainer, colorOnSurface } = $colors); $: color = __IOS__ ? colorOnSurface : colorOnPrimaryContainer; + $: parts = segments ?? [{ icon, text }]; - {#if icon} - - {/if} - {#if text} - diff --git a/app/components/list/CardListCell.svelte b/app/components/list/CardListCell.svelte index e0c7566d..2ab3b979 100644 --- a/app/components/list/CardListCell.svelte +++ b/app/components/list/CardListCell.svelte @@ -25,8 +25,8 @@ - + {#if !tile} diff --git a/app/components/list/MainList.svelte b/app/components/list/MainList.svelte index 013bbe8b..a055d942 100644 --- a/app/components/list/MainList.svelte +++ b/app/components/list/MainList.svelte @@ -1357,19 +1357,37 @@ $: primaryAction = actions.find((action) => action.primary); $: secondaryActions = actions.filter((action) => !action.primary); $: plusMenu = secondaryActions.map((action) => ({ title: action.text, icon: appIcon(action.icon), onSelect: action.onTap })); + // the main actions of the selection, in order: the rest goes to the more menu + function getSelectionActionIds() { + if (isTrash) { + return ['select_all', 'restore', 'delete_permanently']; + } + return CARD_APP ? (__IOS__ ? ['share', 'favorite', 'move_folder', 'delete'] : ['favorite', 'move_folder', 'delete']) : ['pdf', 'share', 'delete']; + } + function splitSelectionOptions(options: OptionType[]) { + const toolbarIds = getSelectionActionIds(); + return { + main: toolbarIds.map((id) => options.find((option) => option.id === id)).filter(Boolean), + more: options.filter((option) => !toolbarIds.includes(option.id)) + }; + } + const onSelectionAction = (option: OptionType) => handleSelectionAction({ object: collectionView?.nativeView }, option); + // Android: the actions are in the top bar while selecting, iOS: in the bottom toolbar + $: selectionOptions = nbSelected > 0 ? splitSelectionOptions(getSelectionToolbarOptions()) : { main: [], more: [] }; + $: selectionMoreMenu = optionsToMenuItems(selectionOptions.more, onSelectionAction); function getSelectionBottomItems(options: OptionType[]) { - const toolbarIds = isTrash ? options.map((option) => option.id) : ['pdf', 'share', 'delete']; - const toolbarOptions = options.filter((option) => toolbarIds.includes(option.id)); - const menuOptions = options.filter((option) => !toolbarIds.includes(option.id)); - const onAction = (option: OptionType) => handleSelectionAction({ object: collectionView?.nativeView }, option); - const items: any[] = toolbarOptions.flatMap((option) => [{ spacer: true }, { text: option.name, icon: appIcon(option.id), onTap: () => onAction(option) }]); - if (menuOptions.length) { + if (__ANDROID__) { + return []; + } + const { main, more } = splitSelectionOptions(options); + const items: any[] = main.flatMap((option) => [{ spacer: true }, { text: option.name, icon: appIcon(option.id), onTap: () => onSelectionAction(option) }]); + if (more.length) { items.push( { spacer: true }, { text: lc('more_options'), icon: appIcon('more'), - menu: optionsToMenuItems(menuOptions, onAction) + menu: optionsToMenuItems(more, onSelectionAction) } ); } @@ -1427,6 +1445,7 @@ 0 ? colorPrimaryContainer : undefined} scrollBehavior="lift" searchable={!isTrash && !onlyForImport && nbSelected === 0} scrollTargetId="documentsCollectionView" @@ -1476,6 +1495,13 @@ {/if} {:else if __IOS__} + {:else} + {#each selectionOptions.main as option (option.id)} + onSelectionAction(option)} /> + {/each} + {#if selectionMoreMenu.length} + + {/if} {/if}