Fix webapp mobile layout

This commit is contained in:
daylily
2026-01-12 15:09:12 -06:00
parent 79b438623b
commit dfec24db24
11 changed files with 107 additions and 116 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ const unsupported = navigator.requestMIDIAccess === undefined
<ModeWatcher /> <ModeWatcher />
<Toaster position="bottom-center" duration={2000} /> <Toaster position="bottom-center" duration={2000} />
<div class="max-w-screen-xl min-h-dvh m-auto p-8 stack gap-4"> <div class="max-w-screen-xl min-h-dvh m-auto p-6 stack gap-4">
{#if unsupported} {#if unsupported}
<Unsupported /> <Unsupported />
{:else} {:else}
@@ -35,7 +35,7 @@ $effect(() => {
</script> </script>
<section <section
class="row gap-2 max-lg:stack max-lg:items-stretch" class="flex flex-col lg:flex-row lg:items-center gap-2"
aria-labelledby="connect-section-label" aria-labelledby="connect-section-label"
> >
<div class="grow"> <div class="grow">
@@ -1,15 +1,15 @@
<script lang="ts"> <script lang="ts">
import { Separator } from '$lib/components/ui/separator' import { Separator } from '$lib/components/ui/separator'
import PreviewSection from './preview/PreviewSection.svelte' import PreviewSection from './preview/PreviewSection.svelte'
import ControlsSection from './controls/ControlsSection.svelte' import ControlsSection from './controls/ControlsSection.svelte'
import { createMediaQuery } from '$lib/utils/media.svelte' import { MediaQuery } from 'svelte/reactivity'
const mobileMediaQuery = createMediaQuery('(max-width: 1024px)') const mediaLg = new MediaQuery('min-width: 1024px')
const separatorOrientation = $derived(mobileMediaQuery.matches ? 'horizontal' : 'vertical') const separatorOrientation = $derived(mediaLg.current ? 'vertical' : 'horizontal')
</script> </script>
<div class="grow stack-h max-lg:stack gap-4"> <div class="grow flex flex-col lg:flex-row gap-4">
<PreviewSection /> <PreviewSection />
<Separator decorative orientation={separatorOrientation} /> <Separator decorative orientation={separatorOrientation} />
@@ -0,0 +1,49 @@
<script lang="ts">
import { Label } from '$lib/components/ui/label'
import IconHideImage from '~icons/material-symbols/hide-image'
import { cn } from '$lib/utils'
import { drawQuantizedData, freshContext, makeAltText } from './common.svelte'
import { getRenderedContext } from '$lib/contexts/rendered.svelte'
import { DEVICE_HEIGHT, DEVICE_WIDTH } from '$lib/constants'
import { getFilesContext } from '$lib/contexts/files.svelte'
import { getConversionConfig } from '$lib/contexts/config.svelte'
import { getImageContext } from '$lib/contexts/image.svelte'
const filesCtx = getFilesContext()
const imageCtx = getImageContext()
const config = getConversionConfig()
const renderedCtx = getRenderedContext()
const hasRendered = $derived(renderedCtx.rendered !== null)
let canvasEl: HTMLCanvasElement = $state(undefined!)
const ctx = $derived(freshContext(canvasEl))
$effect(() => {
if (renderedCtx.rendered === null) return
drawQuantizedData(ctx, renderedCtx.rendered)
})
</script>
<div class="bg-[#ccc] shadow-md rounded-lg p-2 w-full aspect-square relative" role="group">
<div
class="shadow-sm shadow-[inset#888] p-1 size-full"
role="img"
aria-label={makeAltText(filesCtx, imageCtx, config)}
>
{#if hasRendered}
<canvas
class={cn('size-full', hasRendered || 'hidden')}
style:image-rendering="pixelated"
bind:this={canvasEl}
height={DEVICE_HEIGHT}
width={DEVICE_WIDTH}
></canvas>
{:else}
<div class="col justify-center text-[#333] size-full">
<IconHideImage class="text-5xl" aria-label="No image" />
</div>
{/if}
</div>
</div>
@@ -16,6 +16,12 @@
const renderedCtx = getRenderedContext() const renderedCtx = getRenderedContext()
const hasRendered = $derived(renderedCtx.rendered !== null) const hasRendered = $derived(renderedCtx.rendered !== null)
interface Props {
scale: number
}
const { scale }: Props = $props()
let canvasEl: HTMLCanvasElement = $state(undefined!) let canvasEl: HTMLCanvasElement = $state(undefined!)
const ctx = $derived(freshContext(canvasEl)) const ctx = $derived(freshContext(canvasEl))
@@ -27,25 +33,25 @@
</script> </script>
<div> <div>
<div class="bg-[#ccc] shadow-md rounded-lg p-2 w-fit relative" role="group" aria-labelledby="preview-1x-label"> <div class="bg-[#ccc] shadow-md rounded-lg p-2 w-fit relative" role="group" aria-labelledby="preview-{scale}x-label">
<div class="p-[3px] shadow-sm shadow-[inset#888]" role="img" aria-label={makeAltText(filesCtx, imageCtx, config)}> <div class="shadow-sm shadow-[inset#888]" style:padding="{scale * 3}px" role="img" aria-label={makeAltText(filesCtx, imageCtx, config)}>
{#if hasRendered} <div class="aspect-square" style:width="{DEVICE_WIDTH * scale}px">
<canvas {#if hasRendered}
class={cn(hasRendered || 'hidden')} <canvas
style:image-rendering="pixelated" class={cn('size-full', hasRendered || 'hidden')}
style:width="{DEVICE_WIDTH}px" style:image-rendering="pixelated"
style:height="{DEVICE_HEIGHT}px" bind:this={canvasEl}
bind:this={canvasEl} height={DEVICE_HEIGHT}
height={DEVICE_HEIGHT} width={DEVICE_WIDTH}
width={DEVICE_WIDTH} ></canvas>
></canvas> {:else}
{:else} <div class="col justify-center text-[#333] size-full">
<div class="col justify-center text-[#333]" style:width="{DEVICE_WIDTH}px" style:height="{DEVICE_HEIGHT}px"> <IconHideImage style="font-size: {30 * scale}px" aria-label="No image" />
<IconHideImage class="text-3xl" aria-label="No image" /> </div>
</div> {/if}
{/if} </div>
</div> </div>
</div> </div>
<Label id="preview-1x-label">1x Preview</Label> <Label id="preview-{scale}x-label">{scale}x Preview</Label>
</div> </div>
@@ -1,54 +0,0 @@
<script lang="ts">
import { Label } from '$lib/components/ui/label'
import IconHideImage from '~icons/material-symbols/hide-image'
import { cn } from '$lib/utils'
import { drawQuantizedData, freshContext, makeAltText } from './common.svelte'
import { getRenderedContext } from '$lib/contexts/rendered.svelte'
import { DEVICE_HEIGHT, DEVICE_WIDTH } from '$lib/constants'
import { getFilesContext } from '$lib/contexts/files.svelte'
import { getConversionConfig } from '$lib/contexts/config.svelte'
import { getImageContext } from '$lib/contexts/image.svelte'
const filesCtx = getFilesContext()
const imageCtx = getImageContext()
const config = getConversionConfig()
const renderedCtx = getRenderedContext()
const hasRendered = $derived(renderedCtx.rendered !== null)
let canvasEl: HTMLCanvasElement = $state(undefined!)
const ctx = $derived(freshContext(canvasEl))
$effect(() => {
if (renderedCtx.rendered === null) return
drawQuantizedData(ctx, renderedCtx.rendered)
})
</script>
<div>
<div class="bg-[#ccc] shadow-lg rounded-2xl p-4 w-fit relative" role="group" aria-labelledby="preview-2x-label">
<div class="p-[6px] shadow shadow-[inset#888]" role="img" aria-label={makeAltText(filesCtx, imageCtx, config)}>
{#if hasRendered}
<canvas
class={cn(hasRendered || 'hidden')}
style:image-rendering="pixelated"
style:width="{DEVICE_WIDTH * 2}px"
style:height="{DEVICE_HEIGHT * 2}px"
bind:this={canvasEl}
height={DEVICE_HEIGHT}
width={DEVICE_WIDTH}
></canvas>
{:else}
<div
class="col justify-center text-[#333]"
style:width="{DEVICE_WIDTH * 2}px"
style:height="{DEVICE_HEIGHT * 2}px"
>
<IconHideImage class="text-6xl" aria-label="No image" />
</div>
{/if}
</div>
</div>
<Label id="preview-2x-label">2x Preview</Label>
</div>
@@ -1,7 +1,10 @@
<script lang="ts"> <script lang="ts">
import FileSelect from './FileSelect.svelte' import { MediaQuery } from 'svelte/reactivity'
import PreviewCanvas1x from './PreviewCanvas1x.svelte' import FileSelect from './FileSelect.svelte'
import PreviewCanvas2x from './PreviewCanvas2x.svelte' import PreviewCanvas from './PreviewCanvas.svelte'
import AdaptivePreviewCanvas from './AdaptivePreviewCanvas.svelte'
const mediaSm = new MediaQuery('min-width: 640px', false)
</script> </script>
<section class="stack gap-4" aria-labelledby="preview-section-label"> <section class="stack gap-4" aria-labelledby="preview-section-label">
@@ -9,8 +12,12 @@
<FileSelect /> <FileSelect />
<div class="stack-h gap-4 max-md:stack"> <div class="stack-h flex-wrap gap-4">
<PreviewCanvas2x /> {#if mediaSm.current}
<PreviewCanvas1x /> <PreviewCanvas scale={2} />
<PreviewCanvas scale={1} />
{:else}
<AdaptivePreviewCanvas />
{/if}
</div> </div>
</section> </section>
@@ -18,7 +18,7 @@ export function makeAltText(
const file = filesCtx.files[0] const file = filesCtx.files[0]
const output = [`${DEVICE_WIDTH}-by-${DEVICE_HEIGHT}-pixels e-paper preview of "${file.name}"`] const output = [`${DEVICE_WIDTH}-by-${DEVICE_HEIGHT}-pixel e-paper preview of "${file.name}"`]
if (!imageIsCorrectRatio(imageCtx.image)) { if (!imageIsCorrectRatio(imageCtx.image)) {
if (config.scaleMode === 'fit') output.push('letterboxed') if (config.scaleMode === 'fit') output.push('letterboxed')
@@ -59,11 +59,11 @@ async function connectAndWrite() {
return return
} finally { } finally {
setTimeout(() => { setTimeout(() => {
toast.success('Wrote pattern to device')
inProgress = false inProgress = false
}, WRITE_TIME) }, WRITE_TIME)
} }
toast.success('Wrote pattern to device')
} }
$effect(() => { $effect(() => {
@@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
import IconPending from '~icons/material-symbols/pending' import IconPending from '~icons/material-symbols/pending'
import IconArrowUploadProgress from '~icons/material-symbols/arrow-upload-progress' import IconArrowUploadProgress from '~icons/material-symbols/arrow-upload-progress'
import IconWarning from '~icons/material-symbols/warning' import IconWarning from '~icons/material-symbols/warning'
import WriteButton from './WriteButton.svelte' import WriteButton from './WriteButton.svelte'
import { getImageContext } from '$lib/contexts/image.svelte' import { getImageContext } from '$lib/contexts/image.svelte'
const imageCtx = getImageContext() const imageCtx = getImageContext()
let inProgress = $state(false) let inProgress = $state(false)
</script> </script>
<section class="row gap-2 max-lg:stack max-lg:items-stretch" aria-labelledby="write-section-label"> <section class="flex flex-col lg:flex-row gap-2" aria-labelledby="write-section-label">
<div class="grow"> <div class="grow">
<h2 class="font-semibold text-xl/8" id="write-section-label">Write pattern to device</h2> <h2 class="font-semibold text-xl/8" id="write-section-label">Write pattern to device</h2>
@@ -19,7 +19,8 @@
{#if imageCtx.image === null} {#if imageCtx.image === null}
<IconPending aria-hidden /> Select an image file in order to write it onto your device. <IconPending aria-hidden /> Select an image file in order to write it onto your device.
{:else if !inProgress} {:else if !inProgress}
<IconArrowUploadProgress aria-hidden /> Write the pattern onto your device if you have finished editing the image. <IconArrowUploadProgress aria-hidden /> Write the pattern onto your device if you have finished
editing the image.
{:else} {:else}
<IconWarning aria-hidden /> Refresh in progress. Do not disconnect device. <IconWarning aria-hidden /> Refresh in progress. Do not disconnect device.
{/if} {/if}
-18
View File
@@ -1,18 +0,0 @@
import { onDestroy } from 'svelte'
export interface ReactiveMediaQuery {
readonly matches: boolean
}
export function createMediaQuery(query: string): ReactiveMediaQuery {
const queryList = matchMedia(query)
const queryState = $state({ matches: queryList.matches })
const updateQueryState = (e: MediaQueryListEvent) => (queryState.matches = e.matches)
queryList.addEventListener('change', updateQueryState)
onDestroy(() => queryList.removeEventListener('change', updateQueryState))
return queryState
}