Polish: imperial, clear-bed, keyboard nudging, empty states (#18)
Build image / build-and-push (push) Successful in 4s
Build image / build-and-push (push) Successful in 4s
Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #37.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { Modal } from '@/components/ui/Modal'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { useClearObject } from '@/lib/objects'
|
||||
|
||||
/** Confirm clearing every active plop from a focused bed (soft-remove — the rows
|
||||
* are kept with removed_at, so history survives). */
|
||||
export function ClearBedModal({
|
||||
objectName,
|
||||
plops,
|
||||
gardenId,
|
||||
onClose,
|
||||
}: {
|
||||
objectName: string
|
||||
plops: { id: number; version: number }[]
|
||||
gardenId: number
|
||||
onClose: () => void
|
||||
}) {
|
||||
const clear = useClearObject(gardenId)
|
||||
const n = plops.length
|
||||
return (
|
||||
<Modal title="Clear bed" onClose={onClose} busy={clear.isPending}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-sm text-muted">
|
||||
Remove all <span className="font-medium text-fg">{n}</span> {n === 1 ? 'plant' : 'plants'} from{' '}
|
||||
<span className="font-medium text-fg">{objectName}</span>? They're marked removed but kept in history.
|
||||
</p>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose} disabled={clear.isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="danger"
|
||||
disabled={clear.isPending || n === 0}
|
||||
onClick={() => clear.mutate(plops, { onSuccess: onClose })}
|
||||
>
|
||||
{clear.isPending ? 'Clearing…' : 'Clear bed'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@/lib/cn'
|
||||
|
||||
/** A non-interactive hint overlaid on the canvas (empty-state guidance). */
|
||||
export function EditorHint({ position = 'center', children }: { position?: 'center' | 'top'; children: ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute flex p-6 text-center',
|
||||
position === 'top' ? 'inset-x-0 top-16 justify-center' : 'inset-0 items-center justify-center',
|
||||
)}
|
||||
>
|
||||
<p className="max-w-xs rounded-lg bg-surface/85 px-4 py-3 text-sm text-muted shadow-sm backdrop-blur">
|
||||
{children}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -37,6 +37,9 @@ export const PlopMarker = memo(function PlopMarker({
|
||||
const showIcon = scale >= SEMANTIC_FAR && !!plant?.icon
|
||||
const showText = scale >= SEMANTIC_NEAR && !!plant
|
||||
const count = plop.count ?? (plant ? computeDerivedCount(plop.radiusCm, plant.spacingCm) : plop.derivedCount)
|
||||
// Keep the tap target at least ~44px across even when the plop draws tiny at
|
||||
// low zoom (fill=transparent still receives pointer events, unlike fill=none).
|
||||
const hitR = Math.max(r, 22 / scale)
|
||||
|
||||
function down(e: PointerEvent) {
|
||||
e.stopPropagation()
|
||||
@@ -45,6 +48,7 @@ export const PlopMarker = memo(function PlopMarker({
|
||||
|
||||
return (
|
||||
<g transform={`translate(${plop.xCm} ${plop.yCm})`} onPointerDown={down} style={{ cursor: 'pointer' }}>
|
||||
<circle cx={0} cy={0} r={hitR} fill="transparent" />
|
||||
<circle
|
||||
cx={0}
|
||||
cy={0}
|
||||
|
||||
@@ -27,3 +27,8 @@ export const OBJECT_KINDS: KindDef[] = [
|
||||
export function kindDef(kind: string): KindDef | undefined {
|
||||
return OBJECT_KINDS.find((k) => k.kind === kind)
|
||||
}
|
||||
|
||||
/** A human label for an object: its name, else its kind's label, else "Object". */
|
||||
export function objectDisplayName(o: { name: string; kind: string }): string {
|
||||
return o.name || kindDef(o.kind)?.label || 'Object'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user