mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 08:56:41 +00:00
feat(screenshots): polish app-store capture pipeline
Iterates the store-screenshot pipeline to ship-ready quality. Squashed from a series of capture/build/UX changes that all live in e2e/store-screenshots and form a coherent set. Pipeline UX - Print master capture path via Playwright globalTeardown. - Declare sharp as devDep (was imported by build-store-assets but never listed) so a fresh install runs the build cleanly. - Open dist/ folder when build finishes; opt out via SP_SCREENSHOTS_NO_OPEN=1. - Emit dist/screenshots/_preview.html contact sheet for one-click QA across every per-store layout. Capture content - New slot 00 hero across desktop / mobile / tablet specs: showMarketingOverlay paints a gradient caption strip on top of the live app. Position is orientation-aware (bottom for landscape, top for portrait). Copy lives in marketing-copy.ts as a single source of truth. - Desktop slot 05 captures the running focus timer instead of the duration picker (skip the rocket countdown via clock.runFor). - Desktop slot 07 = plain dark project view (no wallpaper) instead of catppuccin; slot 08 = desktop planner. - Mobile slots 02 and 04 use signal-based planner expansion (the component is gesture-only, so flip isExpanded via ng.getComponent). - Side nav collapsed for desktop slots 01 (schedule day-panel) and 04 (notes panel) so the right panel can breathe. - Mobile hides the per-task play column via appFeatures.isTimeTrackingEnabled. - Seed: drop Morning yoga (the only top-level done task on today), flip the remaining done subtask back to undone, trim the PR review tag chips down to the two EM tags that drive Eisenhower. Viewports - desktopMaster: 1280x800 CSS at 2x -> 2560x1600 (smaller MAS Retina size = more apparent zoom than the prior 1440x900 baseline). - androidPhone: 412x915 CSS at 3x -> 1236x2745, matching modern flagships (Pixel 7/8, Galaxy S22+). - Tablet rotations: 7" -> landscape, 10" -> portrait. android7Tablet moves from PHONE_VIEWPORTS to TABLET_VIEWPORTS. Test infra - Bump per-test timeout to 8 minutes for multi-locale single-session specs (12+ captures per locale overran the 180s default on slow viewports). - Scope LOCALES to ['en'] for now until German strings catch up. - ImportPage race tolerates slow imports without an encryption dialog: catch the 15s waitFor rejection so the 60s import-complete promise is the real ceiling. A cold dev server would otherwise abort here at 15s.
This commit is contained in:
parent
8689197571
commit
7157ea62df
15 changed files with 969 additions and 93 deletions
|
|
@ -141,13 +141,16 @@ export class ImportPage extends BasePage {
|
|||
// Handle encryption warning dialog if it appears.
|
||||
// The dialog may appear several seconds after setInputFiles because FileReader is async.
|
||||
// Race: either the dialog appears (handle it) or the import completes without it.
|
||||
// Catch the dialog timeout so a slow import (cold dev server, large seed) doesn't
|
||||
// reject the race before importCompletePromise has a chance to resolve.
|
||||
const encryptionWarning = this.page
|
||||
.locator('dialog-import-encryption-warning')
|
||||
.first();
|
||||
const dialogOrImport = await Promise.race([
|
||||
encryptionWarning
|
||||
.waitFor({ state: 'visible', timeout: 15000 })
|
||||
.then(() => 'dialog' as const),
|
||||
.then(() => 'dialog' as const)
|
||||
.catch(() => 'no_dialog' as const),
|
||||
importCompletePromise.then(() => 'import_complete' as const),
|
||||
]);
|
||||
|
||||
|
|
@ -158,6 +161,10 @@ export class ImportPage extends BasePage {
|
|||
await encryptionWarning.waitFor({ state: 'hidden', timeout: 10000 });
|
||||
// Now wait for the import to actually complete
|
||||
await importCompletePromise;
|
||||
} else if (dialogOrImport === 'no_dialog') {
|
||||
// Dialog didn't appear within 15s — import is slow but may still complete.
|
||||
// Wait for the import-complete signal (its own 60s timeout will catch a true hang).
|
||||
await importCompletePromise;
|
||||
}
|
||||
// If 'import_complete', the import finished without showing the dialog
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
*/
|
||||
export default defineConfig({
|
||||
testDir: path.join(__dirname, 'store-screenshots', 'scenarios'),
|
||||
globalTeardown: path.join(__dirname, 'store-screenshots', 'print-output-path.ts'),
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
// Screenshots aren't flaky like e2e tests — failures should be investigated, not retried.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { VIEWPORTS, type ViewportSpec } from './store-screenshots/matrix';
|
|||
*/
|
||||
export default defineConfig({
|
||||
testDir: path.join(__dirname, 'store-screenshots', 'scenarios', 'desktop'),
|
||||
globalTeardown: path.join(__dirname, 'store-screenshots', 'print-output-path.ts'),
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: 0,
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ npm run screenshots:capture:electron # Electron build → .tmp/screenshots/_mast
|
|||
npm run screenshots:electron # capture:electron + build (lands in dist/)
|
||||
npm run screenshots:build # rebuild dist/ layout from existing masters
|
||||
|
||||
# One group while iterating (pattern matches `desktop dark|light|catppuccin` etc.)
|
||||
# One group while iterating
|
||||
npx playwright test --config e2e/playwright.store-screenshots.config.ts \
|
||||
--project=desktopMaster --grep "desktop dark \(en\)"
|
||||
--project=desktopMaster --grep "desktop all"
|
||||
```
|
||||
|
||||
## Environment overrides
|
||||
|
|
@ -38,6 +38,9 @@ Per-store assets land in `dist/screenshots/<store>/<locale>/NN-name.png` (and th
|
|||
|
||||
| Slot | Platform | Theme | What it shows |
|
||||
| ---- | -------- | ----- | ------------- |
|
||||
| mobile-00 | mobile | dark | Cover/hero — Today list with marketing caption overlay |
|
||||
| desktop-00 | desktop | dark | Cover/hero — Today list with marketing caption overlay |
|
||||
| tablet-00 | tablet | dark | Cover/hero — Today list with marketing caption overlay |
|
||||
| mobile-01 | mobile | dark | Planner |
|
||||
| mobile-02 | mobile | dark | Planner with calendar nav expanded |
|
||||
| mobile-03 | mobile | dark | Eisenhower matrix board |
|
||||
|
|
@ -50,9 +53,10 @@ Per-store assets land in `dist/screenshots/<store>/<locale>/NN-name.png` (and th
|
|||
| desktop-04 | desktop | light | Project (Work) + notes panel populated |
|
||||
| desktop-05 | desktop | dark | Focus mode |
|
||||
| desktop-06 | desktop | light | Schedule (light variant) |
|
||||
| desktop-07 | desktop | catppuccin-mocha | Today list with custom theme |
|
||||
| desktop-07 | desktop | dark | Project (Work) view, no wallpaper — regular palette reads cleanly |
|
||||
| desktop-08 | desktop | dark | Planner |
|
||||
|
||||
Specs are platform-grouped: `scenarios/desktop/all.spec.ts` and `scenarios/mobile/all.spec.ts` each capture every dark+light slot in a single session, flipping `DARK_MODE` between groups via `applyTheme()` (Playwright `addInitScript` is append-only, so a later script wins on each reload). The catppuccin slot stays in its own spec because changing `customTheme` requires a different seed file. Each spec runs once per locale (en + de).
|
||||
Specs are platform-grouped: `scenarios/desktop/all.spec.ts` and `scenarios/mobile/all.spec.ts` each capture every slot in a single session, flipping `DARK_MODE` between groups via `applyTheme()` (Playwright `addInitScript` is append-only, so a later script wins on each reload). Each spec runs once per locale (en + de).
|
||||
|
||||
## Files
|
||||
|
||||
|
|
@ -62,11 +66,12 @@ Specs are platform-grouped: `scenarios/desktop/all.spec.ts` and `scenarios/mobil
|
|||
| `seed/seed.template.json` | Curated dataset with date offsets and `@@PLANNER_OFFSET_+N` placeholders |
|
||||
| `seed/build-seed.ts` | Materializes offsets to absolute dates, injects `locale` + `customTheme` |
|
||||
| `fixture.ts` | Pins clock, applies dark-mode, imports seed via UI flow, exposes `screenshotMaster` (which reads live `DARK_MODE` so light/dark scenes land in the right directory) |
|
||||
| `helpers.ts` | `gotoAndSettle`, `openNotesPanel`, `openSchedulePanel`, `resetView`, `applyTheme` |
|
||||
| `scenarios/desktop/all.spec.ts` | 6 desktop slots (dark + light, single session) |
|
||||
| `scenarios/desktop/catppuccin.spec.ts` | desktop-07 (different seed → standalone) |
|
||||
| `scenarios/mobile/all.spec.ts` | 6 mobile slots (dark + light, single session) |
|
||||
| `build-store-assets.ts` | Renames + copies masters into per-store directory layouts; JPEG re-encode for `maxBytes`-capped stores (Snap) |
|
||||
| `helpers.ts` | `gotoAndSettle`, `openNotesPanel`, `openSchedulePanel`, `resetView`, `applyTheme`, `applyLocale`, `applyTimeTrackingEnabled`, `applySideNavCollapsed`, `setPlannerCalendarExpanded`, `showMarketingOverlay` |
|
||||
| `marketing-copy.ts` | Headline + subline shown on the slot-00 hero overlay |
|
||||
| `scenarios/desktop/all.spec.ts` | 9 desktop slots: hero + 8 scenes |
|
||||
| `scenarios/mobile/all.spec.ts` | 7 mobile slots: hero + 6 scenes |
|
||||
| `scenarios/tablet/all.spec.ts` | 6 tablet slots: hero + 5 scenes |
|
||||
| `build-store-assets.ts` | Renames + copies masters into per-store layouts; JPEG re-encode for `maxBytes`-capped stores (Snap); emits `_preview.html` contact sheet |
|
||||
| `../playwright.store-screenshots.config.ts` | Separate Playwright config; one project per viewport |
|
||||
|
||||
## How it works
|
||||
|
|
@ -134,7 +139,8 @@ The Mac App Store store rule has `masterDir: 'electron'` in `STORE_RULES`, so th
|
|||
## Status
|
||||
|
||||
- ✅ Foundation: matrix, seed builder, fixture (web + electron modes), helpers, post-processor
|
||||
- ✅ 13 scenarios (6 mobile + 7 desktop) covering planner, boards, schedule, focus, notes, custom theme
|
||||
- ✅ 22 scenarios (3 hero + 6 mobile + 8 desktop + 5 tablet) covering planner, boards, schedule, focus, notes, project view
|
||||
- ✅ Per-build `_preview.html` contact sheet under `dist/screenshots/` for one-click QA
|
||||
- ✅ Electron-mode pipeline with OS-level chrome capture (`screencapture` / `grim` / `import`)
|
||||
- ✅ Mac App Store wired to source from `_master_electron/`
|
||||
- ✅ Flathub STORE_RULE (single-gallery, sourced from `_master_electron/`)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
* Run: npm run screenshots:build
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import sharp from 'sharp';
|
||||
|
|
@ -214,7 +215,142 @@ const main = async (): Promise<void> => {
|
|||
);
|
||||
}
|
||||
console.log(`\nTotal files written: ${total}`);
|
||||
const previewPath = writePreviewSheet();
|
||||
console.log(`Output: ${OUT_DIR}`);
|
||||
console.log(`Preview: ${previewPath}`);
|
||||
openFolder(OUT_DIR);
|
||||
};
|
||||
|
||||
type PreviewEntry = { relPath: string; group: string; label: string };
|
||||
|
||||
/** Walk OUT_DIR and collect every emitted png/jpg as a preview entry. */
|
||||
const collectPreviewEntries = (root: string, base = ''): PreviewEntry[] => {
|
||||
const out: PreviewEntry[] = [];
|
||||
if (!fs.existsSync(root)) return out;
|
||||
for (const entry of fs.readdirSync(root)) {
|
||||
const absPath = path.join(root, entry);
|
||||
const relPath = base ? `${base}/${entry}` : entry;
|
||||
const stat = fs.statSync(absPath);
|
||||
if (stat.isDirectory()) {
|
||||
out.push(...collectPreviewEntries(absPath, relPath));
|
||||
continue;
|
||||
}
|
||||
if (!/\.(png|jpe?g)$/i.test(entry)) continue;
|
||||
// Group is everything up to the last directory separator; label is filename.
|
||||
const lastSlash = relPath.lastIndexOf('/');
|
||||
const group = lastSlash === -1 ? '(root)' : relPath.slice(0, lastSlash);
|
||||
out.push({ relPath, group, label: entry });
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* Emit `_preview.html` — a single-page contact sheet of every capture in
|
||||
* `dist/screenshots/`, grouped by store + locale. Open it in a browser to
|
||||
* eyeball the whole batch instead of drilling through ~14 subfolders.
|
||||
*/
|
||||
const writePreviewSheet = (): string => {
|
||||
const entries = collectPreviewEntries(OUT_DIR).sort((a, b) =>
|
||||
a.relPath.localeCompare(b.relPath),
|
||||
);
|
||||
const grouped = new Map<string, PreviewEntry[]>();
|
||||
for (const e of entries) {
|
||||
const list = grouped.get(e.group) ?? [];
|
||||
list.push(e);
|
||||
grouped.set(e.group, list);
|
||||
}
|
||||
const escape = (s: string): string =>
|
||||
s.replace(/[&<>"']/g, (c) => {
|
||||
switch (c) {
|
||||
case '&':
|
||||
return '&';
|
||||
case '<':
|
||||
return '<';
|
||||
case '>':
|
||||
return '>';
|
||||
case '"':
|
||||
return '"';
|
||||
default:
|
||||
return ''';
|
||||
}
|
||||
});
|
||||
const renderCard = (it: PreviewEntry): string => {
|
||||
const href = escape(it.relPath);
|
||||
const label = escape(it.label);
|
||||
return [
|
||||
`<a class="card" href="${href}" target="_blank" rel="noopener">`,
|
||||
`<img loading="lazy" src="${href}" alt="${label}">`,
|
||||
`<div class="label">${label}</div>`,
|
||||
`</a>`,
|
||||
].join('');
|
||||
};
|
||||
const sections = Array.from(grouped.entries())
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([group, items]) => {
|
||||
const cards = items.map(renderCard).join('');
|
||||
const heading =
|
||||
`<h2>${escape(group)} ` + `<span class="count">${items.length}</span></h2>`;
|
||||
return `<section>${heading}<div class="grid">${cards}</div></section>`;
|
||||
})
|
||||
.join('');
|
||||
const html = `<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8">
|
||||
<title>Screenshots preview</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; background: #111; color: #eee; margin: 0; padding: 24px; }
|
||||
h1 { margin: 0 0 4px; font-size: 22px; }
|
||||
.summary { color: #888; font-size: 13px; margin-bottom: 24px; }
|
||||
h2 {
|
||||
font-size: 14px; font-weight: 600; color: #ccc; margin: 28px 0 12px;
|
||||
letter-spacing: 0.02em; text-transform: uppercase;
|
||||
border-bottom: 1px solid #2a2a2a; padding-bottom: 6px;
|
||||
}
|
||||
h2 .count { color: #666; font-weight: 400; margin-left: 8px; }
|
||||
.grid {
|
||||
display: grid; gap: 14px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
}
|
||||
.card {
|
||||
display: block; background: #1a1a1a; border: 1px solid #262626;
|
||||
border-radius: 8px; overflow: hidden; text-decoration: none; color: inherit;
|
||||
transition: transform 0.12s, border-color 0.12s;
|
||||
}
|
||||
.card:hover { transform: translateY(-2px); border-color: #444; }
|
||||
.card img { display: block; width: 100%; height: 280px; object-fit: contain; background: #000; }
|
||||
.label { padding: 8px 10px; font-size: 12px; color: #aaa; word-break: break-all; }
|
||||
</style>
|
||||
</head><body>
|
||||
<h1>Store screenshots preview</h1>
|
||||
<div class="summary">${entries.length} captures across ${grouped.size} groups · ${OUT_DIR}</div>
|
||||
${sections}
|
||||
</body></html>
|
||||
`;
|
||||
const target = path.join(OUT_DIR, '_preview.html');
|
||||
fs.writeFileSync(target, html, 'utf-8');
|
||||
return target;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reveal `dir` in the OS file manager. Detached + unref'd so the build script
|
||||
* exits immediately. Silently no-ops on headless / CI environments where the
|
||||
* platform opener isn't available. Opt out with `SP_SCREENSHOTS_NO_OPEN=1`.
|
||||
*/
|
||||
const openFolder = (dir: string): void => {
|
||||
if (process.env.SP_SCREENSHOTS_NO_OPEN) return;
|
||||
const isWin = process.platform === 'win32';
|
||||
const isMac = process.platform === 'darwin';
|
||||
const cmd = isWin ? 'cmd' : isMac ? 'open' : 'xdg-open';
|
||||
const args = isWin ? ['/c', 'start', '', dir] : [dir];
|
||||
try {
|
||||
const child = spawn(cmd, args, { detached: true, stdio: 'ignore' });
|
||||
child.on('error', () => {
|
||||
/* opener not available on this system */
|
||||
});
|
||||
child.unref();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
main().catch((err) => {
|
||||
|
|
|
|||
|
|
@ -124,22 +124,132 @@ export const applyLocale = async (page: Page, lng: string): Promise<void> => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Switch customTheme without re-importing the seed. CustomThemeService
|
||||
* subscribes to `globalConfig.misc.customTheme` and lazy-loads the matching
|
||||
* stylesheet — give the chunk a beat to land before capturing.
|
||||
* Inject a marketing caption overlay over the current scene. Used for the
|
||||
* cover/hero slot — translucent gradient strip with a bold headline + thin
|
||||
* subline; app remains visible behind as the proof point. Position is
|
||||
* orientation-aware: landscape captures get the strip at the bottom (typical
|
||||
* desktop hero), portrait captures get it at the top (where mobile users
|
||||
* already track their gaze). Removed automatically on the next reload.
|
||||
*/
|
||||
export const applyCustomTheme = async (page: Page, themeId: string): Promise<void> => {
|
||||
export const showMarketingOverlay = async (
|
||||
page: Page,
|
||||
headline: string,
|
||||
subline?: string,
|
||||
): Promise<void> => {
|
||||
await page.evaluate(
|
||||
({ h, s }) => {
|
||||
const id = '__sp-marketing-overlay';
|
||||
document.getElementById(id)?.remove();
|
||||
const isLandscape = window.innerWidth >= window.innerHeight;
|
||||
const edgeRule = isLandscape ? 'bottom:0' : 'top:0';
|
||||
const gradientDir = isLandscape ? 'to top' : 'to bottom';
|
||||
// Slightly more padding on the gradient's "fade away" side so the text
|
||||
// sits visually anchored to the screen edge.
|
||||
const padding = isLandscape ? '8vh 6vw 6vh 6vw' : '6vh 6vw 8vh 6vw';
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = id;
|
||||
overlay.style.cssText = [
|
||||
'position:fixed',
|
||||
edgeRule,
|
||||
'left:0',
|
||||
'right:0',
|
||||
`padding:${padding}`,
|
||||
`background:linear-gradient(${gradientDir},` +
|
||||
'rgba(0,0,0,0.92) 0%,rgba(0,0,0,0.7) 60%,rgba(0,0,0,0) 100%)',
|
||||
'color:#fff',
|
||||
'z-index:99999',
|
||||
'pointer-events:none',
|
||||
'font-family:system-ui,-apple-system,"Segoe UI",sans-serif',
|
||||
'text-align:center',
|
||||
].join(';');
|
||||
const head = document.createElement('div');
|
||||
head.textContent = h;
|
||||
head.style.cssText =
|
||||
'font-size:clamp(28px,4.5vw,72px);font-weight:700;letter-spacing:-0.02em;line-height:1.1';
|
||||
overlay.appendChild(head);
|
||||
if (s) {
|
||||
const sub = document.createElement('div');
|
||||
sub.textContent = s;
|
||||
sub.style.cssText =
|
||||
'font-size:clamp(15px,1.8vw,28px);font-weight:400;opacity:0.85;margin-top:0.6em';
|
||||
overlay.appendChild(sub);
|
||||
}
|
||||
document.body.appendChild(overlay);
|
||||
},
|
||||
{ h: headline, s: subline },
|
||||
);
|
||||
await page.waitForTimeout(150);
|
||||
};
|
||||
|
||||
/**
|
||||
* Flip `globalConfig.appFeatures.isTimeTrackingEnabled`. Disabling hides the
|
||||
* per-task play / pause buttons (`task-hover-controls` gates them on this
|
||||
* flag), giving the task rows a slicker, less control-heavy look. Useful for
|
||||
* mobile captures where the play column adds visual noise.
|
||||
*/
|
||||
export const applyTimeTrackingEnabled = async (
|
||||
page: Page,
|
||||
enabled: boolean,
|
||||
): Promise<void> => {
|
||||
await dispatchInPage(page, {
|
||||
type: '[Global Config] Update Global Config Section',
|
||||
sectionKey: 'misc',
|
||||
sectionCfg: { customTheme: themeId },
|
||||
sectionKey: 'appFeatures',
|
||||
sectionCfg: { isTimeTrackingEnabled: enabled },
|
||||
isSkipSnack: true,
|
||||
meta: {
|
||||
isPersistent: true,
|
||||
entityType: 'GLOBAL_CONFIG',
|
||||
entityId: 'misc',
|
||||
entityId: 'appFeatures',
|
||||
opType: 'UPDATE',
|
||||
},
|
||||
});
|
||||
await page.waitForTimeout(800);
|
||||
await page.waitForTimeout(200);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the desktop side nav between full ("expanded") and collapsed
|
||||
* ("icons-only") modes. The side nav reads `SUP_NAV_SIDEBAR_EXPANDED` from
|
||||
* localStorage on init, so we set the key and reload — hash navigation alone
|
||||
* does not re-initialize the component. Use before scenes that open a right
|
||||
* side panel to keep the main canvas readable.
|
||||
*/
|
||||
export const applySideNavCollapsed = async (
|
||||
page: Page,
|
||||
collapsed: boolean,
|
||||
): Promise<void> => {
|
||||
await page.evaluate((isCollapsed) => {
|
||||
localStorage.setItem('SUP_NAV_SIDEBAR_EXPANDED', (!isCollapsed).toString());
|
||||
}, collapsed);
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await waitForAppReady(page, { ensureRoute: false });
|
||||
await page.waitForTimeout(200);
|
||||
};
|
||||
|
||||
/**
|
||||
* Force the planner calendar nav into its expanded (multi-week) state.
|
||||
* The component only responds to touch gestures (no click handler), so we
|
||||
* flip its `isExpanded` signal directly via Angular's dev-mode debug
|
||||
* helpers. Requires ngDevMode (enabled by `startFrontend:e2e` / ng serve).
|
||||
*/
|
||||
export const setPlannerCalendarExpanded = async (
|
||||
page: Page,
|
||||
expanded: boolean,
|
||||
): Promise<void> => {
|
||||
await page.evaluate((isExpanded) => {
|
||||
const nav = document.querySelector('planner-calendar-nav');
|
||||
if (!nav) throw new Error('planner-calendar-nav not found');
|
||||
type NgDebug = { getComponent?: (el: Element) => unknown };
|
||||
const ng = (window as unknown as { ng?: NgDebug }).ng;
|
||||
const cmp = ng?.getComponent?.(nav) as
|
||||
| { isExpanded?: { set: (v: boolean) => void } }
|
||||
| undefined;
|
||||
if (!cmp?.isExpanded?.set) {
|
||||
throw new Error(
|
||||
'planner-calendar-nav isExpanded signal not accessible — run against an ngDevMode build',
|
||||
);
|
||||
}
|
||||
cmp.isExpanded.set(isExpanded);
|
||||
}, expanded);
|
||||
// Settle frame so layout reflects the new state.
|
||||
await page.waitForTimeout(150);
|
||||
};
|
||||
|
|
|
|||
10
e2e/store-screenshots/marketing-copy.ts
Normal file
10
e2e/store-screenshots/marketing-copy.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* Single source of truth for the cover/hero slot caption shown on the lead
|
||||
* app-store screenshot. Keep it punchy — large type at the top of the
|
||||
* capture, with the actual app visible below as proof.
|
||||
*
|
||||
* If you want to A/B different copy, change here and re-run capture; all
|
||||
* spec classes (desktop / mobile / tablet) read these constants.
|
||||
*/
|
||||
export const MARKETING_HEADLINE = 'Plan it. Do it. Done.';
|
||||
export const MARKETING_SUBLINE = 'Tasks, time, focus – all in one place.';
|
||||
|
|
@ -12,7 +12,8 @@
|
|||
export type Theme = 'light' | 'dark';
|
||||
export type Locale = 'en' | 'de';
|
||||
|
||||
export const LOCALES: readonly Locale[] = ['en', 'de'] as const;
|
||||
// 'de' temporarily disabled — re-add once translations are caught up.
|
||||
export const LOCALES: readonly Locale[] = ['en'] as const;
|
||||
export const THEMES: readonly Theme[] = ['light', 'dark'] as const;
|
||||
|
||||
/**
|
||||
|
|
@ -32,20 +33,27 @@ export type ViewportSpec = {
|
|||
* The key is also the on-disk directory name under `_master/<viewport>/`.
|
||||
*/
|
||||
export const VIEWPORTS = {
|
||||
/** Shared desktop master (16:10). 1440×900 CSS @2x → 2880×1800. Source for
|
||||
* Mac App Store, MS Store, Snap, and the marketing site — MS Store accepts
|
||||
* ≥1366×768 so this exceeds spec. */
|
||||
desktopMaster: { width: 2880, height: 1800, deviceScaleFactor: 2 },
|
||||
/** Shared desktop master (16:10). 1280×800 CSS @2x → 2560×1600. The smaller
|
||||
* of MAS's two listed Retina sizes — gives more apparent UI zoom than the
|
||||
* 1440×900 baseline while still being a native MAS submission size. MS
|
||||
* Store min is 1366×768 so this still exceeds spec. */
|
||||
desktopMaster: { width: 2560, height: 1600, deviceScaleFactor: 2 },
|
||||
/** Apple App Store iPhone 6.9" portrait. CSS 430×932 @3x → 1290×2796. */
|
||||
iphone69: { width: 1290, height: 2796, deviceScaleFactor: 3, isMobile: true },
|
||||
/** Apple App Store iPad 13" portrait. CSS 1032×1376 @2x → 2064×2752. */
|
||||
ipad13: { width: 2064, height: 2752, deviceScaleFactor: 2, isMobile: true },
|
||||
/** Google Play phone portrait. CSS 360×640 @3x → 1080×1920. */
|
||||
androidPhone: { width: 1080, height: 1920, deviceScaleFactor: 3, isMobile: true },
|
||||
/** Google Play 7" tablet portrait. CSS 600×960 @2x → 1200×1920. */
|
||||
android7Tablet: { width: 1200, height: 1920, deviceScaleFactor: 2, isMobile: true },
|
||||
/** Google Play 10" tablet landscape. CSS 960×600 @2x → 1920×1200. */
|
||||
android10Tablet: { width: 1920, height: 1200, deviceScaleFactor: 2, isMobile: true },
|
||||
/** Google Play phone portrait. CSS 412×915 @3x → 1236×2745.
|
||||
* Matches modern flagships (Pixel 7/8, Galaxy S22+, OnePlus 11). 412 CSS
|
||||
* stays below the 768px breakpoint so SP still renders its phone layout. */
|
||||
androidPhone: { width: 1236, height: 2745, deviceScaleFactor: 3, isMobile: true },
|
||||
/** Google Play 7" tablet landscape. CSS 960×600 @2x → 1920×1200.
|
||||
* Portrait (600 CSS) was below the 768 desktop breakpoint and rendered
|
||||
* the phone layout, which felt cramped — landscape stays in tablet
|
||||
* layout and reads cleanly. */
|
||||
android7Tablet: { width: 1920, height: 1200, deviceScaleFactor: 2, isMobile: true },
|
||||
/** Google Play 10" tablet portrait. CSS 800×1280 @2x → 1600×2560.
|
||||
* Standard 10" tablet portrait dimensions. */
|
||||
android10Tablet: { width: 1600, height: 2560, deviceScaleFactor: 2, isMobile: true },
|
||||
} as const satisfies Record<string, ViewportSpec>;
|
||||
|
||||
export type ViewportName = keyof typeof VIEWPORTS;
|
||||
|
|
@ -69,25 +77,22 @@ export const MOBILE_VIEWPORTS = [
|
|||
/**
|
||||
* "Phone-class": viewports < 768 CSS px wide where SP renders its mobile
|
||||
* layout (bottom-nav, no side panels). These run `scenarios/mobile/`.
|
||||
*
|
||||
* Note that `android7Tablet` is 600 CSS wide in portrait — below the 768
|
||||
* mobile-breakpoint — so it really is phone-class for layout purposes.
|
||||
*/
|
||||
export const PHONE_VIEWPORTS = [
|
||||
'iphone69',
|
||||
'androidPhone',
|
||||
'android7Tablet',
|
||||
] as const satisfies readonly ViewportName[];
|
||||
|
||||
/**
|
||||
* "Tablet-class": viewports ≥ 768 CSS px wide where SP renders the desktop
|
||||
* layout (sidenav, panels) but the canvas is taller-than-wide (iPad portrait)
|
||||
* or narrower than the desktopMaster (Android 10" landscape). Routing these
|
||||
* layout (sidenav, panels) but the canvas is taller-than-wide (iPad / 10"
|
||||
* portrait) or narrower than the desktopMaster (7" landscape). Routing these
|
||||
* to the desktop spec would push side panels off-screen; routing them to the
|
||||
* mobile spec wastes the canvas. They run `scenarios/tablet/` instead.
|
||||
*/
|
||||
export const TABLET_VIEWPORTS = [
|
||||
'ipad13',
|
||||
'android7Tablet',
|
||||
'android10Tablet',
|
||||
] as const satisfies readonly ViewportName[];
|
||||
|
||||
|
|
|
|||
13
e2e/store-screenshots/print-output-path.ts
Normal file
13
e2e/store-screenshots/print-output-path.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import * as fs from 'fs';
|
||||
import { MASTER_DIR_ELECTRON, MASTER_DIR_WEB } from './matrix';
|
||||
|
||||
const printOutputPath = (): void => {
|
||||
const dir =
|
||||
process.env.SCREENSHOT_MODE === 'electron' ? MASTER_DIR_ELECTRON : MASTER_DIR_WEB;
|
||||
const exists = fs.existsSync(dir);
|
||||
console.log(
|
||||
`\nMaster captures: ${dir}${exists ? '' : ' (not created — no scenarios ran)'}`,
|
||||
);
|
||||
};
|
||||
|
||||
export default printOutputPath;
|
||||
|
|
@ -1,40 +1,45 @@
|
|||
/**
|
||||
* Single-session capture of every desktop scenario across both locales,
|
||||
* both themes, and the catppuccin custom theme. Switches are driven via
|
||||
* NgRx dispatch through `window.__e2eTestHelpers.store` (exposed in dev/
|
||||
* stage builds — see src/main.ts), so we don't relaunch the
|
||||
* browser / Electron app or re-import the seed between variants.
|
||||
* Single-session capture of every desktop scenario across both locales and
|
||||
* both themes. Switches are driven via NgRx dispatch through
|
||||
* `window.__e2eTestHelpers.store` (exposed in dev/stage builds — see
|
||||
* src/main.ts), so we don't relaunch the browser / Electron app or
|
||||
* re-import the seed between variants.
|
||||
*
|
||||
* Order:
|
||||
* en × dark (slots 01, 02, 03, 05)
|
||||
* en × light (slots 04, 06)
|
||||
* de × dark (slots 01, 02, 03, 05)
|
||||
* de × light (slots 04, 06)
|
||||
* en × dark × catppuccin-mocha (slot 07)
|
||||
* en × dark (slot 07 — project view, no wallpaper)
|
||||
*/
|
||||
import { test } from '../../fixture';
|
||||
import { LOCALES, type Locale } from '../../matrix';
|
||||
import {
|
||||
applyCustomTheme,
|
||||
applyLocale,
|
||||
applySideNavCollapsed,
|
||||
applyTheme,
|
||||
gotoAndSettle,
|
||||
openNotesPanel,
|
||||
openSchedulePanel,
|
||||
resetView,
|
||||
showMarketingOverlay,
|
||||
} from '../../helpers';
|
||||
import { MARKETING_HEADLINE, MARKETING_SUBLINE } from '../../marketing-copy';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
const captureDarkScenes = async (
|
||||
page: Page,
|
||||
shoot: (scenario: string, name: string) => Promise<void>,
|
||||
): Promise<void> => {
|
||||
// 01 — Today + schedule day-panel open
|
||||
// 01 — Today + schedule day-panel open. Side nav collapsed so the right
|
||||
// panel doesn't squeeze the task list.
|
||||
await applySideNavCollapsed(page, true);
|
||||
await gotoAndSettle(page, '/#/tag/TODAY/tasks');
|
||||
await page.locator('task').first().waitFor({ state: 'visible' });
|
||||
await openSchedulePanel(page);
|
||||
await page.waitForTimeout(500);
|
||||
await shoot('desktop-01-list-with-schedule', 'list-with-schedule');
|
||||
await applySideNavCollapsed(page, false);
|
||||
await resetView(page);
|
||||
|
||||
// 02 — Eisenhower board
|
||||
|
|
@ -53,7 +58,7 @@ const captureDarkScenes = async (
|
|||
await shoot('desktop-03-schedule-dark', 'schedule-dark');
|
||||
await resetView(page);
|
||||
|
||||
// 05 — Focus mode (set current task first so the title renders).
|
||||
// 05 — Focus mode, running timer (not duration-selection).
|
||||
await gotoAndSettle(page, '/#/tag/TODAY/tasks');
|
||||
const firstTask = page.locator('task').first();
|
||||
await firstTask.waitFor({ state: 'visible' });
|
||||
|
|
@ -67,20 +72,41 @@ const captureDarkScenes = async (
|
|||
.locator('focus-mode-overlay, focus-mode-main')
|
||||
.first()
|
||||
.waitFor({ state: 'visible', timeout: 10_000 });
|
||||
await page.waitForTimeout(500);
|
||||
// Start the session, then skip the rocket countdown (5s + 900ms launch).
|
||||
// The fixture pinned the clock with `page.clock.install`, so we have to
|
||||
// advance simulated time to fire the rocket's RxJS timer + setTimeout.
|
||||
await page.locator('focus-mode-main .play-button').click();
|
||||
await page.clock.runFor(6500);
|
||||
await page
|
||||
.locator('focus-mode-main .pause-resume-btn')
|
||||
.waitFor({ state: 'visible', timeout: 10_000 });
|
||||
// Tick a minute of session time so the clock face shows progress and the
|
||||
// remaining time isn't exactly the starting duration.
|
||||
await page.clock.runFor(60_000);
|
||||
await page.waitForTimeout(300);
|
||||
await shoot('desktop-05-focus-mode', 'focus-mode');
|
||||
await resetView(page);
|
||||
|
||||
// 08 — Planner (desktop layout). Mobile already has planner-default and
|
||||
// planner-expanded; desktop deserves its own canvas.
|
||||
await gotoAndSettle(page, '/#/planner');
|
||||
await page.locator('planner, planner-day').first().waitFor({ state: 'visible' });
|
||||
await shoot('desktop-08-planner', 'planner');
|
||||
};
|
||||
|
||||
const captureLightScenes = async (
|
||||
page: Page,
|
||||
shoot: (scenario: string, name: string) => Promise<void>,
|
||||
): Promise<void> => {
|
||||
// 04 — Project (Work) + notes panel
|
||||
// 04 — Project (Work) + notes panel. Side nav collapsed so the notes
|
||||
// panel can breathe.
|
||||
await applySideNavCollapsed(page, true);
|
||||
await gotoAndSettle(page, '/#/project/work/tasks');
|
||||
await page.locator('task').first().waitFor({ state: 'visible' });
|
||||
await openNotesPanel(page);
|
||||
await page.waitForTimeout(500);
|
||||
await shoot('desktop-04-list-with-notes', 'list-with-notes');
|
||||
await applySideNavCollapsed(page, false);
|
||||
await resetView(page);
|
||||
|
||||
// 06 — Schedule (light variant)
|
||||
|
|
@ -97,8 +123,20 @@ test.describe('@screenshot desktop all', () => {
|
|||
seededPage,
|
||||
screenshotMaster,
|
||||
}) => {
|
||||
// Two locales × multiple scenarios can exceed the 180s default.
|
||||
test.setTimeout(8 * 60 * 1000);
|
||||
const page = seededPage;
|
||||
|
||||
// 00 — Cover/hero. Today list with a marketing caption strip overlaid
|
||||
// on top so the lead app-store gallery shot is branded but still shows
|
||||
// the real app underneath.
|
||||
await applyTheme(page, 'dark');
|
||||
await gotoAndSettle(page, '/#/tag/TODAY/tasks');
|
||||
await page.locator('task').first().waitFor({ state: 'visible' });
|
||||
await showMarketingOverlay(page, MARKETING_HEADLINE, MARKETING_SUBLINE);
|
||||
await screenshotMaster('desktop-00-hero', 'hero');
|
||||
await resetView(page);
|
||||
|
||||
for (let i = 0; i < LOCALES.length; i += 1) {
|
||||
const lng = LOCALES[i] as Locale;
|
||||
// The seed already starts on `en`; only dispatch a switch when we move
|
||||
|
|
@ -112,13 +150,14 @@ test.describe('@screenshot desktop all', () => {
|
|||
await captureLightScenes(page, screenshotMaster);
|
||||
}
|
||||
|
||||
// 07 — Catppuccin Mocha. Routed through a project view so the global
|
||||
// background image is suppressed and the catppuccin palette reads clearly.
|
||||
// 07 — Project view, dark theme. Project contexts don't carry a
|
||||
// background image (see seed/build-seed.ts: `applyBg` only runs on
|
||||
// tag themes), so the regular palette reads cleanly without a
|
||||
// wallpaper competing for attention.
|
||||
await applyLocale(page, 'en');
|
||||
await applyTheme(page, 'dark');
|
||||
await applyCustomTheme(page, 'catppuccin-mocha');
|
||||
await gotoAndSettle(page, '/#/project/work/tasks');
|
||||
await page.locator('task').first().waitFor({ state: 'visible' });
|
||||
await screenshotMaster('desktop-07-project-catppuccin', 'project-catppuccin');
|
||||
await screenshotMaster('desktop-07-project-dark', 'project-dark');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,16 @@
|
|||
*/
|
||||
import { test } from '../../fixture';
|
||||
import { LOCALES, type Locale } from '../../matrix';
|
||||
import { applyLocale, applyTheme, gotoAndSettle, resetView } from '../../helpers';
|
||||
import {
|
||||
applyLocale,
|
||||
applyTheme,
|
||||
applyTimeTrackingEnabled,
|
||||
gotoAndSettle,
|
||||
resetView,
|
||||
setPlannerCalendarExpanded,
|
||||
showMarketingOverlay,
|
||||
} from '../../helpers';
|
||||
import { MARKETING_HEADLINE, MARKETING_SUBLINE } from '../../marketing-copy';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
const captureDarkScenes = async (
|
||||
|
|
@ -23,14 +32,11 @@ const captureDarkScenes = async (
|
|||
await shoot('mobile-01-planner', 'planner');
|
||||
await resetView(page);
|
||||
|
||||
// 02 — Planner expanded (calendar nav header click)
|
||||
// 02 — Planner with calendar nav expanded (multi-week day picker)
|
||||
await gotoAndSettle(page, '/#/planner');
|
||||
await page.locator('planner, planner-day').first().waitFor({ state: 'visible' });
|
||||
const navHeader = page
|
||||
.locator('planner-calendar-nav .header, planner-calendar-nav button')
|
||||
.first();
|
||||
await navHeader.click({ trial: false }).catch(() => undefined);
|
||||
await page.waitForTimeout(400);
|
||||
await page.locator('planner-calendar-nav').waitFor({ state: 'visible' });
|
||||
await setPlannerCalendarExpanded(page, true);
|
||||
await shoot('mobile-02-planner-expanded', 'planner-expanded');
|
||||
await resetView(page);
|
||||
|
||||
|
|
@ -60,14 +66,11 @@ const captureLightScenes = async (
|
|||
page: Page,
|
||||
shoot: (scenario: string, name: string) => Promise<void>,
|
||||
): Promise<void> => {
|
||||
// 04 — Planner expanded (light variant)
|
||||
// 04 — Planner with calendar nav expanded (light variant)
|
||||
await gotoAndSettle(page, '/#/planner');
|
||||
await page.locator('planner, planner-day').first().waitFor({ state: 'visible' });
|
||||
const navHeader = page
|
||||
.locator('planner-calendar-nav .header, planner-calendar-nav button')
|
||||
.first();
|
||||
await navHeader.click({ trial: false }).catch(() => undefined);
|
||||
await page.waitForTimeout(400);
|
||||
await page.locator('planner-calendar-nav').waitFor({ state: 'visible' });
|
||||
await setPlannerCalendarExpanded(page, true);
|
||||
await shoot('mobile-04-planner-expanded-light', 'planner-expanded-light');
|
||||
};
|
||||
|
||||
|
|
@ -78,8 +81,21 @@ test.describe('@screenshot mobile all', () => {
|
|||
seededPage,
|
||||
screenshotMaster,
|
||||
}) => {
|
||||
// 12 captures × ~15–20s each on a mobile viewport overruns the 180s default.
|
||||
test.setTimeout(8 * 60 * 1000);
|
||||
const page = seededPage;
|
||||
|
||||
// Mobile rows look cleaner without the per-task play button column.
|
||||
await applyTimeTrackingEnabled(page, false);
|
||||
|
||||
// 00 — Cover/hero. Today list with a marketing caption strip overlaid.
|
||||
await applyTheme(page, 'dark');
|
||||
await gotoAndSettle(page, '/#/tag/TODAY/tasks');
|
||||
await page.locator('task').first().waitFor({ state: 'visible' });
|
||||
await showMarketingOverlay(page, MARKETING_HEADLINE, MARKETING_SUBLINE);
|
||||
await screenshotMaster('mobile-00-hero', 'hero');
|
||||
await resetView(page);
|
||||
|
||||
for (let i = 0; i < LOCALES.length; i += 1) {
|
||||
const lng = LOCALES[i] as Locale;
|
||||
if (i > 0) await applyLocale(page, lng);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/**
|
||||
* Tablet capture spec — runs against ipad13 (1032×1376 CSS portrait) and
|
||||
* android10Tablet (960×600 CSS landscape). Both render SP's desktop layout
|
||||
* (≥ 768 CSS px) but neither has the room for the right-hand notes /
|
||||
* schedule day-panel that the desktopMaster scenes rely on, so we keep the
|
||||
* scenes panel-free.
|
||||
* Tablet capture spec — runs against ipad13 (1032×1376 CSS portrait),
|
||||
* android7Tablet (960×600 CSS landscape) and android10Tablet (800×1280 CSS
|
||||
* portrait). All render SP's desktop layout (≥ 768 CSS px) but none has the
|
||||
* room for the right-hand notes / schedule day-panel that the desktopMaster
|
||||
* scenes rely on, so we keep the scenes panel-free.
|
||||
*
|
||||
* Touch is emulated (`hasTouch: true` because the matrix marks these as
|
||||
* `isMobile`), so any hover-revealed action (e.g. desktop's
|
||||
|
|
@ -14,7 +14,13 @@
|
|||
* localized task content.
|
||||
*/
|
||||
import { test } from '../../fixture';
|
||||
import { applyTheme, gotoAndSettle, resetView } from '../../helpers';
|
||||
import {
|
||||
applyTheme,
|
||||
gotoAndSettle,
|
||||
resetView,
|
||||
showMarketingOverlay,
|
||||
} from '../../helpers';
|
||||
import { MARKETING_HEADLINE, MARKETING_SUBLINE } from '../../marketing-copy';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
const captureDarkScenes = async (
|
||||
|
|
@ -69,6 +75,14 @@ test.describe('@screenshot tablet all', () => {
|
|||
}) => {
|
||||
const page = seededPage;
|
||||
await applyTheme(page, 'dark');
|
||||
|
||||
// 00 — Cover/hero. Today list with a marketing caption strip overlaid.
|
||||
await gotoAndSettle(page, '/#/tag/TODAY/tasks');
|
||||
await page.locator('task').first().waitFor({ state: 'visible' });
|
||||
await showMarketingOverlay(page, MARKETING_HEADLINE, MARKETING_SUBLINE);
|
||||
await screenshotMaster('tablet-00-hero', 'hero');
|
||||
await resetView(page);
|
||||
|
||||
await captureDarkScenes(page, screenshotMaster);
|
||||
await applyTheme(page, 'light');
|
||||
await captureLightScenes(page, screenshotMaster);
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
"id": "personal",
|
||||
"title": "Personal",
|
||||
"isHiddenFromMenu": false,
|
||||
"taskIds": ["t-yoga", "t-call-mom"],
|
||||
"taskIds": ["t-call-mom"],
|
||||
"backlogTaskIds": ["t-book-flight", "t-renew-passport"],
|
||||
"noteIds": ["n-ideas", "n-week-goals"],
|
||||
"theme": {
|
||||
|
|
@ -269,7 +269,6 @@
|
|||
"t-quarterly-plan",
|
||||
"t-quarterly-plan-sub-1",
|
||||
"t-quarterly-plan-sub-2",
|
||||
"t-yoga",
|
||||
"t-call-mom",
|
||||
"t-book-flight",
|
||||
"t-renew-passport",
|
||||
|
|
@ -304,7 +303,7 @@
|
|||
"timeEstimate": 2700000,
|
||||
"isDone": false,
|
||||
"notes": "",
|
||||
"tagIds": ["tag-deep-work", "EM_URGENT", "EM_IMPORTANT"],
|
||||
"tagIds": ["EM_URGENT", "EM_IMPORTANT"],
|
||||
"created": 0,
|
||||
"attachments": [],
|
||||
"dueDayOffset": 0,
|
||||
|
|
@ -332,7 +331,7 @@
|
|||
"timeSpentOnDay": {},
|
||||
"timeSpent": 0,
|
||||
"timeEstimate": 1800000,
|
||||
"isDone": true,
|
||||
"isDone": false,
|
||||
"notes": "",
|
||||
"tagIds": [],
|
||||
"parentId": "t-quarterly-plan",
|
||||
|
|
@ -355,22 +354,6 @@
|
|||
"attachments": [],
|
||||
"projectId": "work"
|
||||
},
|
||||
"t-yoga": {
|
||||
"id": "t-yoga",
|
||||
"title": "Morning yoga",
|
||||
"subTaskIds": [],
|
||||
"timeSpentOnDayKeysAreOffsets": true,
|
||||
"timeSpentOnDayOffsets": { "0": 1500000 },
|
||||
"timeSpent": 1500000,
|
||||
"timeEstimate": 1800000,
|
||||
"isDone": true,
|
||||
"notes": "",
|
||||
"tagIds": ["tag-home"],
|
||||
"created": 0,
|
||||
"attachments": [],
|
||||
"dueDayOffset": 0,
|
||||
"projectId": "personal"
|
||||
},
|
||||
"t-call-mom": {
|
||||
"id": "t-call-mom",
|
||||
"title": "Call mom",
|
||||
|
|
@ -508,7 +491,6 @@
|
|||
"id": "TODAY",
|
||||
"title": "Today",
|
||||
"taskIds": [
|
||||
"t-yoga",
|
||||
"t-pr-review",
|
||||
"t-call-mom",
|
||||
"t-design-review",
|
||||
|
|
@ -608,7 +590,7 @@
|
|||
"tag-home": {
|
||||
"id": "tag-home",
|
||||
"title": "home",
|
||||
"taskIds": ["t-yoga"],
|
||||
"taskIds": [],
|
||||
"icon": "home",
|
||||
"created": 0,
|
||||
"theme": {
|
||||
|
|
|
|||
537
package-lock.json
generated
537
package-lock.json
generated
|
|
@ -135,6 +135,7 @@
|
|||
"pretty-quick": "^4.2.2",
|
||||
"query-string": "^7.1.3",
|
||||
"rxjs": "^7.8.2",
|
||||
"sharp": "^0.34.5",
|
||||
"shepherd.js": "^11.2.0",
|
||||
"spark-md5": "^3.0.2",
|
||||
"stacktrace-js": "^2.0.2",
|
||||
|
|
@ -3921,7 +3922,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
|
|
@ -5122,6 +5122,496 @@
|
|||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/ansi": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
|
||||
|
|
@ -24392,6 +24882,51 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.0.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
"semver": "^7.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.5",
|
||||
"@img/sharp-darwin-x64": "0.34.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||
"@img/sharp-linux-arm": "0.34.5",
|
||||
"@img/sharp-linux-arm64": "0.34.5",
|
||||
"@img/sharp-linux-ppc64": "0.34.5",
|
||||
"@img/sharp-linux-riscv64": "0.34.5",
|
||||
"@img/sharp-linux-s390x": "0.34.5",
|
||||
"@img/sharp-linux-x64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||
"@img/sharp-wasm32": "0.34.5",
|
||||
"@img/sharp-win32-arm64": "0.34.5",
|
||||
"@img/sharp-win32-ia32": "0.34.5",
|
||||
"@img/sharp-win32-x64": "0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -276,6 +276,7 @@
|
|||
"pretty-quick": "^4.2.2",
|
||||
"query-string": "^7.1.3",
|
||||
"rxjs": "^7.8.2",
|
||||
"sharp": "^0.34.5",
|
||||
"shepherd.js": "^11.2.0",
|
||||
"spark-md5": "^3.0.2",
|
||||
"stacktrace-js": "^2.0.2",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue