chore: stage all in-progress work before repo split
CI / test (push) Has been cancelled

Web app: new entities (Image, RenderedAsset, SharedImage, Token,
DeviceImageHistory), enums, repositories, controllers, message handlers,
migrations, tests, frontend upload/library/sticker UI, Vue components.

Firmware: EPD background screen binaries + gen scripts, setup_bg header.

Infra: ddev config, test bundle, gitignore coverage dir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 12:11:31 -04:00
parent 062c52eec7
commit 12245759ac
149 changed files with 14846 additions and 92 deletions
+122
View File
@@ -0,0 +1,122 @@
<template>
<BaseBottomSheet :model-value="modelValue" label="Choose frames" @update:model-value="$emit('update:modelValue', $event)">
<h2 class="device-picker__title">Add to frames</h2>
<p class="device-picker__sub">Choose which frames will show this photo.</p>
<div class="device-picker__list">
<label
v-for="device in devices"
:key="device.id"
class="device-picker__row"
>
<input
type="checkbox"
class="device-picker__check"
:checked="selected.includes(device.id)"
@change="toggle(device.id)"
/>
<span class="device-picker__name">{{ device.name }}</span>
<span class="device-picker__orientation">{{ device.orientation }}</span>
</label>
</div>
<BaseButton
variant="primary"
class="device-picker__confirm"
:disabled="selected.length === 0 || uploading"
@click="$emit('confirm')"
>
{{ uploading ? 'Uploading…' : confirmLabel }}
</BaseButton>
</BaseBottomSheet>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import BaseBottomSheet from '@/components/BaseBottomSheet.vue'
import BaseButton from '@/components/BaseButton.vue'
import type { Device } from '@/types'
const props = defineProps<{
modelValue: boolean
devices: Device[]
selected: number[]
uploading?: boolean
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: boolean): void
(e: 'update:selected', v: number[]): void
(e: 'confirm'): void
}>()
function toggle(id: number) {
if (props.selected.includes(id)) {
emit('update:selected', props.selected.filter(d => d !== id))
} else {
emit('update:selected', [...props.selected, id])
}
}
const confirmLabel = computed(() => {
const n = props.selected.length
return n === 0 ? 'Add to frame' : `Add to ${n} frame${n > 1 ? 's' : ''}`
})
</script>
<style scoped lang="scss">
.device-picker {
&__title {
font-size: var(--text-md);
font-weight: 700;
margin-bottom: var(--space-2);
}
&__sub {
font-size: var(--text-sm);
color: var(--color-text-muted);
margin-bottom: var(--space-4);
}
&__list {
display: flex;
flex-direction: column;
gap: var(--space-1);
margin-bottom: var(--space-5);
}
&__row {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) 0;
border-bottom: 1px solid var(--color-border);
cursor: pointer;
min-height: var(--touch-min);
}
&__check {
width: 20px;
height: 20px;
accent-color: var(--color-primary);
cursor: pointer;
flex-shrink: 0;
}
&__name {
flex: 1;
font-size: var(--text-base);
font-weight: 600;
}
&__orientation {
font-size: var(--text-xs);
color: var(--color-text-muted);
text-transform: capitalize;
}
&__confirm {
width: 100%;
}
}
</style>