everyscreen 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -2
  3. package/dist/cli.d.mts +1 -0
  4. package/dist/cli.mjs +2958 -0
  5. package/dist/commands-logic-DVWfSe3O.mjs +217 -0
  6. package/dist/dashboard/assets/button-N-Nucq_K.js +1 -0
  7. package/dist/dashboard/assets/cn-DMYzmXtt.js +1 -0
  8. package/dist/dashboard/assets/device-status-logic-Silf3YXA.js +1 -0
  9. package/dist/dashboard/assets/devices-5pEUg6rA.js +1 -0
  10. package/dist/dashboard/assets/dist-CnPFbxes.js +1 -0
  11. package/dist/dashboard/assets/focus._deviceId-DGel-kWw.js +1 -0
  12. package/dist/dashboard/assets/frame-layout-logic-H_x71NvW.js +1 -0
  13. package/dist/dashboard/assets/index-DNHZ-i1B.css +2 -0
  14. package/dist/dashboard/assets/index-DwPQ0eY_.js +10 -0
  15. package/dist/dashboard/assets/modal-dialog-BaO9xkRr.js +1 -0
  16. package/dist/dashboard/assets/rolldown-runtime-WkwZ5CU-.js +1 -0
  17. package/dist/dashboard/assets/routes-CUFa9tL0.js +1 -0
  18. package/dist/dashboard/assets/run-origin-badge-BeFqg0AC.js +1 -0
  19. package/dist/dashboard/assets/runs-queries-BYBE7R9S.js +2 -0
  20. package/dist/dashboard/assets/runs._runId-CokpJ1Rr.js +1 -0
  21. package/dist/dashboard/assets/runs.index-ZsynzkKU.js +1 -0
  22. package/dist/dashboard/assets/schemas-BPFqmBjG.js +55 -0
  23. package/dist/dashboard/assets/screen-boundary-BauyD9pu.js +1 -0
  24. package/dist/dashboard/assets/status-chip-yqTTTuyZ.js +1 -0
  25. package/dist/dashboard/assets/tile-status-chip-DisgbNnH.js +1 -0
  26. package/dist/dashboard/assets/toggle-nciug_di.js +1 -0
  27. package/dist/dashboard/assets/use-run-totals-B3G4yamQ.js +1 -0
  28. package/dist/dashboard/assets/useNavigate-8mkM-ffY.js +1 -0
  29. package/dist/dashboard/assets/useParams-D2lK5BEg.js +1 -0
  30. package/dist/dashboard/index.html +29 -0
  31. package/dist/devices-store-edScJDie.mjs +1228 -0
  32. package/dist/playwright-logic-QFlBFspE.d.mts +138 -0
  33. package/dist/playwright.d.mts +2 -0
  34. package/dist/playwright.mjs +18 -0
  35. package/dist/read-hub-file-N4rZ-6Wr.mjs +180 -0
  36. package/dist/report-hub-trouble-BjbcuK21.d.mts +2 -0
  37. package/dist/report-hub-trouble-CaUiyteI.mjs +41 -0
  38. package/dist/reporter.d.mts +47 -0
  39. package/dist/reporter.mjs +250 -0
  40. package/dist/run-one-at-a-time-BeV_a3Fm.mjs +48 -0
  41. package/dist/runs-store-Bd_ITPFj.mjs +283 -0
  42. package/dist/test.d.mts +11 -0
  43. package/dist/test.mjs +69 -0
  44. package/package.json +106 -4
  45. package/src/dashboard/ui/theme.css +28 -0
  46. package/src/skill/SKILL.md +15 -0
@@ -0,0 +1,48 @@
1
+ import { dirname } from "node:path";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
+ // src/hub/stored-files.ts
5
+ const DEFAULT_FILE_MODE = 438;
6
+ const isMissingFileError = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
7
+ const readStoredBytes = async (filePath) => {
8
+ try {
9
+ return await readFile(filePath);
10
+ } catch (error) {
11
+ if (isMissingFileError(error)) return null;
12
+ throw error;
13
+ }
14
+ };
15
+ const readStoredFile = async (filePath) => {
16
+ const fileBytes = await readStoredBytes(filePath);
17
+ if (fileBytes === null) return null;
18
+ return fileBytes.toString("utf8");
19
+ };
20
+ const writeStoredFile = async (filePath, fileBytes, fileMode = DEFAULT_FILE_MODE) => {
21
+ const folderPath = dirname(filePath);
22
+ const draftPath = `${filePath}.${randomUUID()}.draft`;
23
+ await mkdir(folderPath, { recursive: true });
24
+ await writeFile(draftPath, fileBytes, { mode: fileMode });
25
+ try {
26
+ await rename(draftPath, filePath);
27
+ } catch (error) {
28
+ await rm(draftPath, { force: true });
29
+ throw error;
30
+ }
31
+ };
32
+ // src/hub/run-one-at-a-time.ts
33
+ const queuedTasks = /* @__PURE__ */ new Map();
34
+ const runAfter = async (previousTask, task) => {
35
+ await Promise.allSettled([previousTask]);
36
+ return task();
37
+ };
38
+ const runOneAtATime = async (queueKey, task) => {
39
+ const previousTask = queuedTasks.get(queueKey) ?? null;
40
+ const currentTask = runAfter(previousTask, task);
41
+ queuedTasks.set(queueKey, currentTask);
42
+ try {
43
+ return await currentTask;
44
+ } finally {
45
+ if (queuedTasks.get(queueKey) === currentTask) queuedTasks.delete(queueKey);
46
+ }
47
+ };
48
+ export { writeStoredFile as i, readStoredBytes as n, readStoredFile as r, runOneAtATime as t };
@@ -0,0 +1,283 @@
1
+ import { m as countDeviceStatuses, w as STORE_FOLDER } from "./commands-logic-DVWfSe3O.mjs";
2
+ import { i as writeStoredFile, r as readStoredFile, t as runOneAtATime } from "./run-one-at-a-time-BeV_a3Fm.mjs";
3
+ import { HTTPException } from "hono/http-exception";
4
+ import { z } from "zod";
5
+ import { basename, join } from "node:path";
6
+ import { glob } from "node:fs/promises";
7
+ // src/checks/checks-logic.ts
8
+ const MIN_TAP_SIZE = 44;
9
+ const REQUEST_FAILURE_ECHO = "Failed to load resource";
10
+ const FAVICON_PATH = "/favicon.ico";
11
+ const SEVERITIES = ["failed", "warn"];
12
+ const findOverflow = (measurements, checkDevice) => {
13
+ const { widestPastEdge } = measurements;
14
+ if (widestPastEdge === null) return [];
15
+ const { selector, overflowPx } = widestPastEdge;
16
+ return [{
17
+ kind: "overflow",
18
+ severity: "failed",
19
+ deviceId: checkDevice.id,
20
+ selector,
21
+ message: `${selector} is ${overflowPx}px past the edge`
22
+ }];
23
+ };
24
+ const findClippedLabels = (measurements, checkDevice) => measurements.clippedSelectors.map((selector) => ({
25
+ kind: "clipped-text",
26
+ severity: "warn",
27
+ deviceId: checkDevice.id,
28
+ selector,
29
+ message: `${selector} cuts its text off`
30
+ }));
31
+ const listSafeAreaFindings = (measurements, checkDevice) => {
32
+ if (!measurements.isViewportCover) return [];
33
+ const { pinnedBoxes, viewportHeight } = measurements;
34
+ const { safeAreaInsets } = checkDevice;
35
+ const homeBarTop = viewportHeight - safeAreaInsets.bottom;
36
+ return pinnedBoxes.filter((pinnedBox) => {
37
+ const { top, bottom } = safeAreaInsets;
38
+ const { cssSafeAreaSides } = pinnedBox;
39
+ const isTopCovered = pinnedBox.paddingTop >= top || cssSafeAreaSides.top;
40
+ const isBottomCovered = pinnedBox.paddingBottom >= bottom || cssSafeAreaSides.bottom;
41
+ const isUnderNotch = pinnedBox.top < top && !isTopCovered;
42
+ const isUnderHomeBar = pinnedBox.bottom > homeBarTop && !isBottomCovered;
43
+ return isUnderNotch || isUnderHomeBar;
44
+ }).map((pinnedBox) => ({
45
+ kind: "safe-area",
46
+ severity: "warn",
47
+ deviceId: checkDevice.id,
48
+ selector: pinnedBox.selector,
49
+ message: `${pinnedBox.selector} sits under the notch or home bar`
50
+ }));
51
+ };
52
+ const findSmallTapTargets = (measurements, checkDevice) => {
53
+ if (!checkDevice.hasTouch) return [];
54
+ return measurements.tapTargets.filter(({ width, height }) => Math.min(width, height) < MIN_TAP_SIZE).map(({ selector, width, height }) => ({
55
+ kind: "tap-target",
56
+ severity: "warn",
57
+ deviceId: checkDevice.id,
58
+ selector,
59
+ message: `${selector} is ${Math.round(width)}x${Math.round(height)}, under ${MIN_TAP_SIZE}x${MIN_TAP_SIZE}`
60
+ }));
61
+ };
62
+ const listFindings = (measurements, checkDevice) => [
63
+ ...findOverflow(measurements, checkDevice),
64
+ ...findClippedLabels(measurements, checkDevice),
65
+ ...listSafeAreaFindings(measurements, checkDevice),
66
+ ...findSmallTapTargets(measurements, checkDevice)
67
+ ];
68
+ const isRequestFailureEcho = (consoleMessage) => consoleMessage.startsWith(REQUEST_FAILURE_ECHO);
69
+ const isFaviconUrl = (requestUrl) => new URL(requestUrl).pathname === FAVICON_PATH;
70
+ const createConsoleFinding = (deviceId, consoleMessage) => ({
71
+ kind: "console-error",
72
+ severity: "failed",
73
+ deviceId,
74
+ selector: null,
75
+ message: consoleMessage
76
+ });
77
+ const createRequestFinding = (deviceId, requestUrl, failureReason) => {
78
+ const { pathname } = new URL(requestUrl);
79
+ return {
80
+ kind: "failed-request",
81
+ severity: "failed",
82
+ deviceId,
83
+ selector: null,
84
+ message: `${pathname} ${failureReason ?? "failed"}`
85
+ };
86
+ };
87
+ // src/runs/runs-logic.ts
88
+ const RUN_ID_PATTERN = /^run-(\d+)$/;
89
+ const RUN_ID_DIGITS = 4;
90
+ const NOT_FOUND_STATUS = 404;
91
+ const CONFLICT_STATUS = 409;
92
+ const readRunNumber = (runId) => {
93
+ const idParts = RUN_ID_PATTERN.exec(runId);
94
+ if (idParts === null) return null;
95
+ return Number(idParts[1]);
96
+ };
97
+ const pickNextRunId = (runIds) => {
98
+ const runNumbers = runIds.map(readRunNumber).filter((runNumber) => runNumber !== null);
99
+ const latestNumber = Math.max(0, ...runNumbers);
100
+ return `run-${String(latestNumber + 1).padStart(RUN_ID_DIGITS, "0")}`;
101
+ };
102
+ const pickRunStatus = (steps) => {
103
+ const { failedCount } = countDeviceStatuses(steps);
104
+ return failedCount > 0 ? "failed" : "ok";
105
+ };
106
+ const checkRunEvent = (savedRun, runEvent) => {
107
+ if (savedRun === null) throw new HTTPException(NOT_FOUND_STATUS, { message: "No run with that id" });
108
+ if (savedRun.status !== "running" && runEvent.kind === "step-end") throw new HTTPException(CONFLICT_STATUS, { message: "The run has already ended" });
109
+ return savedRun;
110
+ };
111
+ const mergeRunEvent = (run, runEvent) => {
112
+ switch (runEvent.kind) {
113
+ case "run-start": return {
114
+ ...run,
115
+ origin: runEvent.origin,
116
+ runKind: runEvent.runKind,
117
+ status: "running"
118
+ };
119
+ case "step-end": return {
120
+ ...run,
121
+ steps: [...run.steps, runEvent.step]
122
+ };
123
+ case "run-end": {
124
+ const isReportedFailed = runEvent.status === "failed";
125
+ const stepsStatus = pickRunStatus(run.steps);
126
+ const status = isReportedFailed ? "failed" : stepsStatus;
127
+ return {
128
+ ...run,
129
+ status
130
+ };
131
+ }
132
+ }
133
+ };
134
+ const createStepId = (stepNumber) => `step-${stepNumber}`;
135
+ const pickStepId = (steps) => createStepId(steps.length + 1);
136
+ const isRunOpen = (run) => {
137
+ return run !== null && run.status === "running";
138
+ };
139
+ const pickLeftOpenRuns = (runs) => runs.filter(({ origin, status }) => origin === "cli" && status === "running");
140
+ // src/runs/run-schema.ts
141
+ const runOriginSchema = z.enum([
142
+ "cli",
143
+ "flow",
144
+ "playwright"
145
+ ]);
146
+ const runKindSchema = z.enum(["steps", "sweep"]);
147
+ const deviceAnswerSchema = z.discriminatedUnion("status", [z.object({
148
+ status: z.literal("ok"),
149
+ deviceId: z.string(),
150
+ durationMs: z.number(),
151
+ shotPath: z.string().nullable().default(null)
152
+ }), z.object({
153
+ status: z.literal("failed"),
154
+ deviceId: z.string(),
155
+ durationMs: z.number(),
156
+ message: z.string(),
157
+ shotPath: z.string().nullable()
158
+ })]);
159
+ const findingSchema = z.object({
160
+ kind: z.enum([
161
+ "overflow",
162
+ "clipped-text",
163
+ "safe-area",
164
+ "tap-target",
165
+ "console-error",
166
+ "failed-request"
167
+ ]),
168
+ severity: z.enum(SEVERITIES),
169
+ deviceId: z.string(),
170
+ selector: z.string().nullable(),
171
+ message: z.string()
172
+ });
173
+ const stepProofSchema = z.object({
174
+ shotPath: z.string().nullable(),
175
+ errorMessage: z.string(),
176
+ codeSnippet: z.string().nullable(),
177
+ codeLocation: z.string().nullable()
178
+ });
179
+ const runStepSchema = z.object({
180
+ stepId: z.string(),
181
+ title: z.string(),
182
+ startedAt: z.number(),
183
+ deviceAnswers: z.array(deviceAnswerSchema),
184
+ findings: z.array(findingSchema),
185
+ proof: stepProofSchema.nullable()
186
+ });
187
+ const runSchema = z.object({
188
+ runId: z.string(),
189
+ origin: runOriginSchema,
190
+ runKind: runKindSchema.default("steps"),
191
+ status: z.enum([
192
+ "running",
193
+ "ok",
194
+ "failed"
195
+ ]),
196
+ startedAt: z.number(),
197
+ steps: z.array(runStepSchema)
198
+ });
199
+ const runEventSchema = z.discriminatedUnion("kind", [
200
+ z.object({
201
+ kind: z.literal("run-start"),
202
+ origin: runOriginSchema,
203
+ runKind: runKindSchema
204
+ }),
205
+ z.object({
206
+ kind: z.literal("step-end"),
207
+ step: runStepSchema
208
+ }),
209
+ z.object({
210
+ kind: z.literal("run-end"),
211
+ status: z.enum(["ok", "failed"])
212
+ })
213
+ ]);
214
+ // src/runs/runs-store.ts
215
+ const RUNS_SUBFOLDER = "runs";
216
+ const RUN_FILE_EXTENSION = ".json";
217
+ const RUN_FILE_PATTERN = `*${RUN_FILE_EXTENSION}`;
218
+ const pickRunsFolder = (rootFolder) => join(rootFolder, STORE_FOLDER, RUNS_SUBFOLDER);
219
+ const pickRunFilePath = (rootFolder, runId) => join(pickRunsFolder(rootFolder), `${runId}${RUN_FILE_EXTENSION}`);
220
+ const listRunIds = async (rootFolder) => {
221
+ const runIds = [];
222
+ const runFileNames = glob(RUN_FILE_PATTERN, { cwd: pickRunsFolder(rootFolder) });
223
+ for await (const runFileName of runFileNames) runIds.push(basename(runFileName, RUN_FILE_EXTENSION));
224
+ return runIds.sort();
225
+ };
226
+ const writeRun = async (rootFolder, run) => {
227
+ const serializedRun = JSON.stringify(run, null, 2);
228
+ await writeStoredFile(pickRunFilePath(rootFolder, run.runId), serializedRun);
229
+ };
230
+ const readRun = async (rootFolder, runId) => {
231
+ const serializedRun = await readStoredFile(pickRunFilePath(rootFolder, runId));
232
+ if (serializedRun === null) return null;
233
+ return runSchema.parse(JSON.parse(serializedRun));
234
+ };
235
+ const listRuns = async (rootFolder) => {
236
+ const runIds = await listRunIds(rootFolder);
237
+ const runs = [];
238
+ for (const runId of runIds) try {
239
+ const run = await readRun(rootFolder, runId);
240
+ if (run !== null) runs.push(run);
241
+ } catch (error) {
242
+ console.error({
243
+ runId,
244
+ error
245
+ });
246
+ }
247
+ return runs;
248
+ };
249
+ const createRun = async (rootFolder, origin, runKind) => {
250
+ return runOneAtATime(pickRunsFolder(rootFolder), async () => {
251
+ const runIds = await listRunIds(rootFolder);
252
+ const run = {
253
+ runId: pickNextRunId(runIds),
254
+ origin,
255
+ runKind,
256
+ status: "running",
257
+ startedAt: Date.now(),
258
+ steps: []
259
+ };
260
+ await writeRun(rootFolder, run);
261
+ return run;
262
+ });
263
+ };
264
+ const saveRunEvent = async (rootFolder, runId, runEvent) => {
265
+ const runFilePath = pickRunFilePath(rootFolder, runId);
266
+ return runOneAtATime(runFilePath, async () => {
267
+ const savedRun = await readRun(rootFolder, runId);
268
+ const checkedRun = checkRunEvent(savedRun, runEvent);
269
+ const mergedRun = mergeRunEvent(checkedRun, runEvent);
270
+ await writeRun(rootFolder, mergedRun);
271
+ return mergedRun;
272
+ });
273
+ };
274
+ const closeRun = (rootFolder, runId) => saveRunEvent(rootFolder, runId, {
275
+ kind: "run-end",
276
+ status: "ok"
277
+ });
278
+ const closeLeftOpenRuns = async (rootFolder) => {
279
+ const runs = await listRuns(rootFolder);
280
+ const leftOpenRuns = pickLeftOpenRuns(runs);
281
+ for (const leftOpenRun of leftOpenRuns) await closeRun(rootFolder, leftOpenRun.runId);
282
+ };
283
+ export { listFindings as _, readRun as a, runOriginSchema as c, isRunOpen as d, pickStepId as f, isRequestFailureEcho as g, isFaviconUrl as h, listRuns as i, RUN_ID_PATTERN as l, createRequestFinding as m, closeRun as n, saveRunEvent as o, createConsoleFinding as p, createRun as r, runEventSchema as s, closeLeftOpenRuns as t, createStepId as u };
@@ -0,0 +1,11 @@
1
+ import { f as HubFile, n as DeviceProjectOptions } from "./playwright-logic-QFlBFspE.mjs";
2
+ import { t as ReportHubTrouble } from "./report-hub-trouble-BjbcuK21.mjs";
3
+ interface WorkerHub {
4
+ hubFile: HubFile | null;
5
+ reportTrouble: ReportHubTrouble;
6
+ }
7
+ export declare const test: import("playwright/test").TestType<import("playwright/test").PlaywrightTestArgs & import("playwright/test").PlaywrightTestOptions & DeviceProjectOptions, import("playwright/test").PlaywrightWorkerArgs & import("playwright/test").PlaywrightWorkerOptions & {
8
+ everyscreenHub: WorkerHub;
9
+ }>;
10
+ export declare const expect: import("playwright/test").Expect<{}>;
11
+ export {};
package/dist/test.mjs ADDED
@@ -0,0 +1,69 @@
1
+ import { C as SCREENCAST } from "./commands-logic-DVWfSe3O.mjs";
2
+ import { n as readHubFileSafely, v as pickScreenHubUrl } from "./read-hub-file-N4rZ-6Wr.mjs";
3
+ import { n as postToHub, t as createHubTroubleReporter } from "./report-hub-trouble-CaUiyteI.mjs";
4
+ import { test as test$1 } from "@playwright/test";
5
+ // src/playwright/playwright-fixture.ts
6
+ const SCREEN_INTERVAL_MS = 300;
7
+ const SCREENSHOT_TIMEOUT_MS = 2e3;
8
+ const sendScreenImage = async (page, deviceId, hubTarget) => {
9
+ const screenImage = await page.screenshot({
10
+ type: SCREENCAST.format,
11
+ quality: SCREENCAST.quality,
12
+ scale: "css",
13
+ timeout: SCREENSHOT_TIMEOUT_MS
14
+ });
15
+ await postToHub(hubTarget, "/screens", {
16
+ deviceId,
17
+ imageBase64: screenImage.toString("base64")
18
+ });
19
+ };
20
+ const startSendingScreens = (page, deviceId, hubTarget) => {
21
+ const sending = { screenSend: null };
22
+ const sendSafely = async () => {
23
+ try {
24
+ await sendScreenImage(page, deviceId, hubTarget);
25
+ } catch (error) {
26
+ if (!page.isClosed()) hubTarget.reportTrouble(error);
27
+ } finally {
28
+ sending.screenSend = null;
29
+ }
30
+ };
31
+ const sendOnTick = () => {
32
+ if (sending.screenSend !== null) return;
33
+ sending.screenSend = sendSafely();
34
+ };
35
+ const tickTimer = setInterval(sendOnTick, SCREEN_INTERVAL_MS);
36
+ return async () => {
37
+ clearInterval(tickTimer);
38
+ await sending.screenSend;
39
+ };
40
+ };
41
+ const test = test$1.extend({
42
+ everyscreenDeviceId: [null, { option: true }],
43
+ everyscreenHub: [async ({}, use) => {
44
+ const reportTrouble = createHubTroubleReporter();
45
+ await use({
46
+ hubFile: readHubFileSafely(reportTrouble),
47
+ reportTrouble
48
+ });
49
+ }, { scope: "worker" }],
50
+ page: async ({ page, everyscreenDeviceId, everyscreenHub }, use) => {
51
+ const { hubFile, reportTrouble } = everyscreenHub;
52
+ const screenHubUrl = pickScreenHubUrl(hubFile, everyscreenDeviceId);
53
+ if (screenHubUrl !== null && everyscreenDeviceId !== null) {
54
+ const stopSendingScreens = startSendingScreens(page, everyscreenDeviceId, {
55
+ hubUrl: screenHubUrl,
56
+ reportTrouble
57
+ });
58
+ try {
59
+ await use(page);
60
+ } finally {
61
+ await stopSendingScreens();
62
+ }
63
+ return;
64
+ }
65
+ await use(page);
66
+ }
67
+ });
68
+ const expect = test$1.expect;
69
+ export { expect, test };
package/package.json CHANGED
@@ -1,6 +1,108 @@
1
1
  {
2
2
  "name": "everyscreen",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Local CLI and live dashboard that runs your web app on many device sizes at once, with layout checks and one-line results for coding agents.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/YosefHayim/everyscreen.git"
9
+ },
10
+ "homepage": "https://everyscreen.jtslabs.dev",
11
+ "type": "module",
12
+ "bin": {
13
+ "everyscreen": "./dist/cli.mjs"
14
+ },
15
+ "exports": {
16
+ "./playwright": {
17
+ "types": "./dist/playwright.d.mts",
18
+ "default": "./dist/playwright.mjs"
19
+ },
20
+ "./test": {
21
+ "types": "./dist/test.d.mts",
22
+ "default": "./dist/test.mjs"
23
+ },
24
+ "./reporter": {
25
+ "types": "./dist/reporter.d.mts",
26
+ "default": "./dist/reporter.mjs"
27
+ },
28
+ "./theme.css": "./src/dashboard/ui/theme.css"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "!dist/site",
33
+ "src/skill/SKILL.md",
34
+ "src/dashboard/ui/theme.css"
35
+ ],
36
+ "engines": {
37
+ "node": ">=22.17"
38
+ },
39
+ "packageManager": "pnpm@10.14.0",
40
+ "scripts": {
41
+ "dev": "pnpm run \"/^dev:(hub|dashboard)$/\"",
42
+ "dev:hub": "tsx watch src/cli/cli.ts start --foreground",
43
+ "dev:dashboard": "vite",
44
+ "dev:site": "vite --config vite.site.config.ts",
45
+ "prod": "pnpm build && node dist/cli.mjs start",
46
+ "build": "tsdown && vite build",
47
+ "build:site": "vite build --config vite.site.config.ts",
48
+ "deploy:site": "pnpm build:site && wrangler deploy",
49
+ "test": "vitest run",
50
+ "test:watch": "vitest",
51
+ "test:e2e": "playwright test",
52
+ "lint": "biome check && knip",
53
+ "lint:fix": "biome check --write",
54
+ "typecheck": "tsc --noEmit",
55
+ "prepare": "husky",
56
+ "check": "turbo run lint typecheck test test:e2e build build:site --output-logs=errors-only"
57
+ },
58
+ "dependencies": {
59
+ "@hono/node-server": "2.1.5",
60
+ "@hono/zod-validator": "0.9.1",
61
+ "hono": "4.13.14",
62
+ "playwright": "1.64.1",
63
+ "ws": "8.22.0",
64
+ "yaml": "2.9.1",
65
+ "zod": "4.6.7"
66
+ },
67
+ "peerDependencies": {
68
+ "@playwright/test": "~1.64.0"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "@playwright/test": {
72
+ "optional": true
73
+ }
74
+ },
75
+ "devDependencies": {
76
+ "@biomejs/biome": "2.6.3",
77
+ "@hookform/resolvers": "6.0.2",
78
+ "@playwright/test": "1.64.1",
79
+ "@tailwindcss/vite": "4.3.3",
80
+ "@tanstack/react-query": "5.105.0",
81
+ "@tanstack/react-router": "1.170.46",
82
+ "@tanstack/router-plugin": "1.168.48",
83
+ "@types/node": "24.19.2",
84
+ "@types/react": "19.3.0",
85
+ "@types/react-dom": "19.3.0",
86
+ "@types/ws": "8.18.2",
87
+ "@vitejs/plugin-react": "6.1.2",
88
+ "@vitest/coverage-v8": "5.0.5",
89
+ "class-variance-authority": "0.7.1",
90
+ "clsx": "2.1.1",
91
+ "husky": "9.1.7",
92
+ "i18next": "26.5.11",
93
+ "knip": "6.43.0",
94
+ "react": "19.3.1",
95
+ "react-dom": "19.3.1",
96
+ "react-hook-form": "7.91.0",
97
+ "react-i18next": "17.1.9",
98
+ "tailwind-merge": "3.7.0",
99
+ "tailwindcss": "4.3.3",
100
+ "tsdown": "0.23.3",
101
+ "tsx": "4.23.15",
102
+ "turbo": "2.11.8",
103
+ "typescript": "7.0.2",
104
+ "vite": "8.3.5",
105
+ "vitest": "5.0.5",
106
+ "wrangler": "4.154.0"
107
+ }
108
+ }
@@ -0,0 +1,28 @@
1
+ @theme {
2
+ --color-*: initial;
3
+ --color-paper: #eef0f4;
4
+ --color-card: #ffffff;
5
+ --color-line: #d7dbe5;
6
+ --color-soft: #f6f7fa;
7
+ --color-ink: #0e1222;
8
+ --color-ink-2: #4e5468;
9
+ --color-ink-3: #5f6579;
10
+ --color-muted: #8b91a8;
11
+ --color-frame-detail: #2e3450;
12
+ --color-window-close: #ff5f57;
13
+ --color-window-minimize: #febc2e;
14
+ --color-window-maximize: #28c840;
15
+ --color-pink: #ff4f9a;
16
+ --color-cobalt: #2b35e0;
17
+ --color-cobalt-hover: #1c24b0;
18
+ --color-ok: #0b7a4b;
19
+ --color-ok-soft: #e6f4ec;
20
+ --color-warn: #a35a00;
21
+ --color-warn-soft: #fff3e0;
22
+ --color-failed: #c8361b;
23
+ --color-failed-soft: #fdece8;
24
+ --font-sans: "Geist", system-ui, sans-serif;
25
+ --font-mono: "Geist Mono", ui-monospace, monospace;
26
+ --shadow-lift:
27
+ 0 18px 40px -16px rgb(14 18 34 / 0.32), 0 2px 6px rgb(14 18 34 / 0.06);
28
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: everyscreen
3
+ description: Check a web app on many device sizes at once with the Everyscreen CLI. Use when the user asks to test a page on phones, tablets or other screen sizes, or to find layout bugs.
4
+ ---
5
+
6
+ # Everyscreen
7
+
8
+ Everyscreen runs your web app on many devices at once and answers each command with one line.
9
+
10
+ 1. Start the hub with `npx everyscreen start`. The dashboard is at http://127.0.0.1:4100.
11
+ 2. Add `--json` to every command. The reply is then one JSON object instead of the line.
12
+ 3. Read the reply. Do not open shots unless a step fails.
13
+ 4. When a step fails, read `failures` in the JSON reply. Each failed device has `deviceId`, `message` (the error, so read its first line) and `shotPath`. Open only the shots of the failed devices. For the full report, give the user `http://127.0.0.1:4100/runs/<runId>`, using `runId` from the reply.
14
+ 5. To check every route of the app on every device, run `npx everyscreen sweep --url <url>`. Add `--before <flow.yaml>` to run a login flow first on each device.
15
+ 6. To run the user's Playwright tests on every device at once, import `test` from `everyscreen/test` and use the `everyscreen/reporter` reporter. See the README.