Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d27d730d2 | ||
|
|
b0ab6ee5c0 |
+20
-3
@@ -5,7 +5,17 @@
|
||||
# manager and no libc — the attack surface of the running container is the binary.
|
||||
|
||||
# --- 1. the web interface ----------------------------------------------------
|
||||
FROM node:22-alpine AS web
|
||||
#
|
||||
# Pinned to the *build* platform, not the target. What this stage produces is
|
||||
# HTML, CSS and JavaScript, which are the same bytes whatever the image will run
|
||||
# on — so building it once and copying it into both architectures is not an
|
||||
# optimisation, it is the correct thing.
|
||||
#
|
||||
# Without the pin, buildx runs this stage once per target, which means running
|
||||
# Node and esbuild under QEMU for arm64. That crashes: exit 132, SIGILL, an
|
||||
# instruction the emulator does not implement, roughly one release in three. It
|
||||
# looks like a broken build and is a broken emulator.
|
||||
FROM --platform=$BUILDPLATFORM node:22-alpine AS web
|
||||
|
||||
WORKDIR /src
|
||||
COPY web/ ./
|
||||
@@ -24,7 +34,12 @@ RUN if [ -f package.json ]; then \
|
||||
fi
|
||||
|
||||
# --- 2. the binary -----------------------------------------------------------
|
||||
FROM golang:1.26-alpine AS build
|
||||
#
|
||||
# Also pinned to the build platform, and cross-compiled instead. Go does that
|
||||
# natively and CGO is already off, so there is nothing to emulate — where
|
||||
# building *in* the target architecture means running the whole Go toolchain
|
||||
# under QEMU for one of the two.
|
||||
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS build
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
@@ -37,11 +52,13 @@ COPY . .
|
||||
COPY --from=web /src/build ./cmd/verdande/webbuild
|
||||
|
||||
ARG VERSION=dev
|
||||
# Supplied by buildx per target. The default keeps a plain `docker build` working.
|
||||
ARG TARGETARCH=amd64
|
||||
|
||||
# CGO off is what makes this binary static, and it is only possible because the
|
||||
# SQLite driver is pure Go. -trimpath and the empty buildid keep the output
|
||||
# reproducible; -s -w drop the symbol and DWARF tables, which is most of the size.
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build \
|
||||
-tags embedweb \
|
||||
-trimpath \
|
||||
-ldflags="-s -w -buildid= -X main.version=${VERSION}" \
|
||||
|
||||
@@ -804,6 +804,46 @@ test('en rolle kan rettes uden at fjerne personen', async ({ browser, page }) =>
|
||||
expect(trouble).toEqual([]);
|
||||
});
|
||||
|
||||
/**
|
||||
* The project page on a phone.
|
||||
*
|
||||
* Every other test here runs at desktop width, and this is what that misses: the
|
||||
* header is a flex row of a title and four controls, and 390px does not have room
|
||||
* for them. Without wrapping, the heading was squeezed to its min-content — which
|
||||
* with `overflow-wrap: anywhere` is one character — so a project called
|
||||
* "Skovvænget" rendered as a vertical column of letters while the buttons ran off
|
||||
* the right edge. It looked like a broken font.
|
||||
*/
|
||||
test('projektsiden er brugbar på en telefon', async ({ page }) => {
|
||||
const trouble = watchForTrouble(page);
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto('/');
|
||||
|
||||
// The sidebar is a drawer at this width, so the project is reached through it.
|
||||
await page.getByRole('button', { name: 'Vis menu' }).click();
|
||||
const sidebar = page.getByRole('navigation', { name: 'Hovedmenu' });
|
||||
await sidebar.getByRole('button', { name: 'Nyt projekt' }).click();
|
||||
await sidebar.getByLabel('Projektnavn').fill('Skovvænget');
|
||||
await sidebar.getByLabel('Projektnavn').press('Enter');
|
||||
|
||||
const heading = page.getByRole('heading', { name: 'Skovvænget' });
|
||||
await expect(heading).toBeVisible();
|
||||
|
||||
const box = await heading.boundingBox();
|
||||
// Wide enough to hold the word, and short enough not to be a column of
|
||||
// letters. One line of "Skovvænget" at --text-2xl is nowhere near 200px tall.
|
||||
expect(box.width, 'overskriften er klemt sammen').toBeGreaterThan(120);
|
||||
expect(box.height, 'overskriften er brudt op i én bogstavkolonne').toBeLessThan(120);
|
||||
|
||||
// And nothing sticks out past the right edge of the page.
|
||||
const overflow = await page.evaluate(
|
||||
() => document.documentElement.scrollWidth - document.documentElement.clientWidth
|
||||
);
|
||||
expect(overflow, 'siden kan scrolles vandret').toBeLessThanOrEqual(1);
|
||||
|
||||
expect(trouble).toEqual([]);
|
||||
});
|
||||
|
||||
/**
|
||||
* The MCP connector address must not fall through to the app shell.
|
||||
*
|
||||
|
||||
@@ -548,7 +548,10 @@
|
||||
ondrop={(e) => onSectionDrop(e, '')}
|
||||
>
|
||||
<TaskList tasks={unsectioned} projectId={project.id} sectionId="" {canEdit} />
|
||||
{#if !unsectioned.length}
|
||||
<!-- Only once there are sections to be "without": on a project with none,
|
||||
the label is a heading for the only list there is, and the empty
|
||||
state below already says the project is empty. -->
|
||||
{#if sections.length && !unsectioned.length}
|
||||
<p class="empty">Uden sektion</p>
|
||||
{/if}
|
||||
</section>
|
||||
@@ -664,17 +667,28 @@
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
/* Wraps, and the title keeps a floor.
|
||||
*
|
||||
* Without both, a phone squeezes the heading to its min-content — which with
|
||||
* `overflow-wrap: anywhere` is *one character*, so "Skovvænget" came out as a
|
||||
* vertical column of letters while the buttons ran off the right edge. Four
|
||||
* controls beside a title is more than 390px has, so they belong on the next
|
||||
* line rather than in a race for the same one.
|
||||
*/
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s4);
|
||||
gap: var(--s3);
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--s5);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-2xl);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
/* 8ch rather than 0: the basis is what stops flex shrinking it past the
|
||||
point where the wrapping below is the better answer. */
|
||||
flex: 1 1 8ch;
|
||||
min-width: 8ch;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@@ -694,8 +708,8 @@
|
||||
|
||||
/* Matches the heading it stands in for, so the swap does not move the page. */
|
||||
.title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex: 1 1 8ch;
|
||||
min-width: 8ch;
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 560;
|
||||
letter-spacing: -0.015em;
|
||||
|
||||
@@ -135,17 +135,21 @@
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
/* Wraps, for the same reason the project header does: a title and a switcher
|
||||
are more than a phone's width, and the switcher belongs underneath rather
|
||||
than squeezing the heading. */
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s4);
|
||||
gap: var(--s3);
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--s5);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-2xl);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex: 1 1 8ch;
|
||||
min-width: 8ch;
|
||||
}
|
||||
|
||||
.views {
|
||||
|
||||
Reference in New Issue
Block a user