diff --git a/CONTEXT.md b/CONTEXT.md index 712cd7a..4809491 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -38,7 +38,7 @@ One stage of preparing an Order Item, requiring a Machine carrying a specific Ma _Avoid_: Stage (use Step consistently) **Delay**: -Extra time a Machine reports on top of a claimed Preparation Step's estimated duration. Delays add to the Order Item's time to completion and are shown to the customer as the item running late. +Extra time a Machine reports on top of a claimed Preparation Step's estimated duration. Delays add to the Order Item's time to completion; the customer only sees the updated time, never a delay notice. **Machine**: A physical cooking hardware unit at a Kitchen (e.g. an oven, a grill), carrying one or more Machine Tags. A Kitchen may have multiple Machines carrying the same Tag, working in parallel. diff --git a/src/components/orders/EtaMessage.module.scss b/src/components/orders/EtaMessage.module.scss new file mode 100644 index 0000000..47e5531 --- /dev/null +++ b/src/components/orders/EtaMessage.module.scss @@ -0,0 +1,19 @@ +@use '../../styles/variables' as *; + +.eta { + margin: 0 0 $space-2; + font-size: 0.9rem; + color: $color-success; +} + +.value { + position: relative; + display: inline-flex; + overflow: hidden; + vertical-align: bottom; +} + +.valueText { + display: inline-block; + white-space: nowrap; +} diff --git a/src/components/orders/EtaMessage.tsx b/src/components/orders/EtaMessage.tsx new file mode 100644 index 0000000..bb6a435 --- /dev/null +++ b/src/components/orders/EtaMessage.tsx @@ -0,0 +1,30 @@ +import { AnimatePresence, motion, useReducedMotion } from 'framer-motion' + +import { formatEtaParts } from '#/lib/format.ts' + +import styles from './EtaMessage.module.scss' + +export const EtaMessage = ({ etaSeconds }: { etaSeconds: number }) => { + const reduceMotion = useReducedMotion() + const { prefix, value } = formatEtaParts(etaSeconds) + + return ( +

+ {prefix}{' '} + + + + {value} + + + +

+ ) +} diff --git a/src/components/orders/ItemStatusBanner.module.scss b/src/components/orders/ItemStatusBanner.module.scss index 872bd80..047cd29 100644 --- a/src/components/orders/ItemStatusBanner.module.scss +++ b/src/components/orders/ItemStatusBanner.module.scss @@ -9,10 +9,6 @@ &[data-variant='cancelled'] { color: $color-danger; } - - &[data-variant='delayed'] { - color: $color-warning; - } } .icon { diff --git a/src/components/orders/ItemStatusBanner.tsx b/src/components/orders/ItemStatusBanner.tsx index 0f42fe1..9f6e8ac 100644 --- a/src/components/orders/ItemStatusBanner.tsx +++ b/src/components/orders/ItemStatusBanner.tsx @@ -2,14 +2,12 @@ import { motion, useReducedMotion } from 'framer-motion' import styles from './ItemStatusBanner.module.scss' -export type ItemStatusBannerVariant = - 'ready' | 'pickedUp' | 'cancelled' | 'delayed' +export type ItemStatusBannerVariant = 'ready' | 'pickedUp' | 'cancelled' const iconPaths = { ready: ['M7.5 12.5l3 3 6-6.5'], pickedUp: ['M7.5 12.5l3 3 6-6.5'], cancelled: ['M8.5 8.5l7 7', 'M15.5 8.5l-7 7'], - delayed: ['M12 7v5', 'M12 12l3.5 2'], } satisfies Record> type ItemStatusBannerProps = { diff --git a/src/components/orders/OrderStatusView.module.scss b/src/components/orders/OrderStatusView.module.scss index 4cb434d..f74a5ef 100644 --- a/src/components/orders/OrderStatusView.module.scss +++ b/src/components/orders/OrderStatusView.module.scss @@ -1,13 +1,5 @@ @use '../../styles/variables' as *; -@position-try --cancel-hint-flip { - position-area: bottom; - --hint-arrow-top: auto; - --hint-arrow-bottom: 100%; - --hint-arrow-color-top: transparent; - --hint-arrow-color-bottom: #{$color-text}; -} - .page { max-width: 720px; margin: 0 auto; @@ -277,12 +269,6 @@ opacity: 0.6; } -.eta { - margin: 0 0 $space-2; - font-size: 0.9rem; - color: $color-success; -} - .actions { display: flex; flex-wrap: wrap; @@ -291,7 +277,10 @@ button, a { - padding: $space-1 $space-3; + display: inline-flex; + align-items: center; + gap: $space-1; + padding: $space-2 $space-4; border: 1px solid $color-border; border-radius: $radius; background: $color-background; @@ -310,66 +299,12 @@ } } - .infoButton { - display: inline-flex; - align-items: center; - flex-shrink: 0; - gap: $space-1; - padding: $space-1 $space-2; - color: $color-text-muted; - font-size: 0.85rem; - white-space: nowrap; - - &:hover:not(:disabled) { - color: $color-primary; - border-color: $color-primary; - } - } - - .infoIcon { + .actionIcon { width: 1rem; height: 1rem; flex-shrink: 0; } - .cancelHint { - position: fixed; - position-area: top; - position-try-fallbacks: --cancel-hint-flip; - inset: auto; - overflow: visible; - height: auto; - margin: $space-2; - padding: $space-2 $space-3; - width: max-content; - max-width: 260px; - border: none; - border-radius: $radius; - background: $color-text; - color: $color-background; - font-size: 0.8rem; - line-height: 1.4; - text-align: left; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - - --hint-arrow-top: 100%; - --hint-arrow-bottom: auto; - --hint-arrow-color-top: #{$color-text}; - --hint-arrow-color-bottom: transparent; - - &::after { - content: ''; - position: absolute; - left: 50%; - top: var(--hint-arrow-top); - bottom: var(--hint-arrow-bottom); - transform: translateX(-50%); - border: 6px solid transparent; - border-top-color: var(--hint-arrow-color-top); - border-bottom-color: var(--hint-arrow-color-bottom); - } - } - .cancelButton { padding: $space-1 $space-2; border: none; diff --git a/src/components/orders/OrderStatusView.tsx b/src/components/orders/OrderStatusView.tsx index 53f9a7f..281ff27 100644 --- a/src/components/orders/OrderStatusView.tsx +++ b/src/components/orders/OrderStatusView.tsx @@ -4,7 +4,6 @@ import { useState } from 'react' import type { OrderStatus } from '#/db/schema.ts' import type { OrderItemStatusView } from '#/domain/ordering.ts' -import { formatDuration, formatEtaMessage } from '#/lib/format.ts' import { advanceSimulatedKitchen, cancelOrderItem, @@ -13,6 +12,7 @@ import { } from '#/server/orders.ts' import styles from './OrderStatusView.module.scss' +import { EtaMessage } from './EtaMessage.tsx' import { ItemStatusBanner } from './ItemStatusBanner.tsx' const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) @@ -55,14 +55,6 @@ const getItemBanner = (item: OrderItemStatusView) => { detail: "You've been refunded for this item.", } as const) } - if (item.delaySeconds > 0) { - return { - animationKey: 'delayed', - variant: 'delayed', - label: 'Running late', - detail: `About ${formatDuration(item.delaySeconds)} behind schedule`, - } as const - } return null } @@ -71,20 +63,6 @@ const bannerKey = (item: OrderItemStatusView) => { return banner ? `${item.id}:${banner.animationKey}` : null } -const cancelBlockedReason = (item: OrderItemStatusView) => { - if ( - item.cancellable || - item.status === 'cancelled' || - item.status === 'picked_up' - ) { - return null - } - if (item.status === 'ready') { - return "This item is already done cooking, so it's too late to cancel. If something's wrong with it, use Get help below." - } - return "This item has already started cooking, so the kitchen can't stop or undo it. If something's wrong, use Get help below." -} - const BackArrowIcon = () => ( ( ) -const InfoIcon = () => ( +const actionIconPaths = { + buyAgain: ['M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8', 'M3 3v5h5'], + pickedUp: [ + 'M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z', + 'M3 6h18', + 'M16 10a4 4 0 0 1-8 0', + ], + help: [ + 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z', + 'M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3', + 'M12 17h.01', + ], + cancel: ['M18 6 6 18', 'M6 6l12 12'], +} + +const ActionIcon = ({ name }: { name: keyof typeof actionIconPaths }) => ( ( strokeLinejoin="round" aria-hidden="true" > - - - + {actionIconPaths[name].map((d) => ( + + ))} ) @@ -262,7 +255,6 @@ export const OrderStatusView = ({ {error &&

{error}

}