plum-e2e 2.8.5 → 2.9.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.
- package/backend/_scaffold/utils/browser.ts +184 -30
- package/backend/_scaffold/utils/hooks.ts +37 -8
- package/backend/app.js +0 -5
- package/backend/config/scripts/generate-report.js +2 -2
- package/backend/constants/socketEvents.js +7 -4
- package/backend/lib/reportFilename.js +1 -2
- package/backend/lib/{screenshotPoller.js → rrwebPoller.js} +7 -4
- package/backend/lib/serverBootstrap.js +14 -0
- package/backend/logs/runner-cmtbz5b1l0000mr0110b27w5k.log +8 -0
- package/backend/mcp/server.js +3 -47
- package/backend/package-lock.json +199 -1
- package/backend/package.json +3 -1
- package/backend/prisma/migrations/20260828120000_add_recording_and_split_runner_worker_count/migration.sql +39 -0
- package/backend/prisma/migrations/20260828140000_add_recording_started_ended_at/migration.sql +5 -0
- package/backend/prisma/migrations/20260828150000_strip_screenshot_refs_from_reports/migration.sql +34 -0
- package/backend/prisma/migrations/20260828160000_add_backup_include_reports/migration.sql +4 -0
- package/backend/prisma/schema.prisma +97 -70
- package/backend/routes/backup.routes.js +47 -1
- package/backend/routes/reports.routes.js +23 -0
- package/backend/routes/test-cases.routes.js +7 -2
- package/backend/server.js +1 -1
- package/backend/services/backupCronService.js +1 -1
- package/backend/services/backupService.js +134 -44
- package/backend/services/cronService.js +23 -15
- package/backend/services/nodeExecutionService.js +41 -15
- package/backend/services/nodeStreamRegistry.js +24 -0
- package/backend/services/reportService.js +174 -84
- package/backend/services/runnerService.js +19 -7
- package/backend/services/settingsService.js +6 -3
- package/backend/services/testCaseService.js +3 -2
- package/backend/services/triggerService.js +20 -13
- package/backend/services/userService.js +1 -3
- package/backend/websockets/nodeSocketHandler.js +40 -0
- package/backend/websockets/socketHandler.js +9 -7
- package/frontend/.svelte-kit/adapter-node/.vite/manifest.json +1 -1
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.C1TbMUws.css +6 -0
- package/frontend/.svelte-kit/adapter-node/chunks/internal.js +1 -1
- package/frontend/.svelte-kit/adapter-node/entries/pages/_page.svelte.js +22 -22
- package/frontend/.svelte-kit/adapter-node/entries/pages/settings/_page.svelte.js +3 -3
- package/frontend/.svelte-kit/adapter-node/manifest-full.js +1 -1
- package/frontend/.svelte-kit/adapter-node/manifest.js +1 -1
- package/frontend/.svelte-kit/adapter-node/nodes/0.js +1 -1
- package/frontend/.svelte-kit/adapter-node/nodes/1.js +1 -1
- package/frontend/.svelte-kit/adapter-node/nodes/11.js +1 -1
- package/frontend/.svelte-kit/adapter-node/nodes/12.js +1 -1
- package/frontend/.svelte-kit/adapter-node/nodes/2.js +2 -2
- package/frontend/.svelte-kit/adapter-node/nodes/3.js +1 -1
- package/frontend/.svelte-kit/adapter-node/nodes/5.js +1 -1
- package/frontend/.svelte-kit/adapter-node/nodes/6.js +1 -1
- package/frontend/.svelte-kit/adapter-node/nodes/8.js +1 -1
- package/frontend/.svelte-kit/adapter-node/nodes/9.js +1 -1
- package/frontend/.svelte-kit/ambient.d.ts +4 -0
- package/frontend/.svelte-kit/generated/server/internal.js +1 -1
- package/frontend/.svelte-kit/output/client/.vite/manifest.json +35 -35
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/2.C1TbMUws.css +6 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.C1TbMUws.css +6 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/{DEKAA-Fm.js → DDZw0L0T.js} +1 -1
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/{Dhf4xQKo.js → kkA1mrsH.js} +2 -2
- package/frontend/.svelte-kit/output/client/_app/immutable/entry/{app.B0GH5ONo.js → app.KrtMZ3my.js} +2 -2
- package/frontend/.svelte-kit/output/client/_app/immutable/entry/{start.Bn25CW9X.js → start.4qxHHAcl.js} +1 -1
- package/frontend/.svelte-kit/output/client/_app/immutable/nodes/{0.-imXt0f5.js → 0.C8DodwXs.js} +1 -1
- package/frontend/{build/client/_app/immutable/nodes/1.Dom3CKWF.js → .svelte-kit/output/client/_app/immutable/nodes/1.Df9sLw8b.js} +1 -1
- package/frontend/{build/client/_app/immutable/nodes/11.DMlayLCK.js → .svelte-kit/output/client/_app/immutable/nodes/11.BViaDK2m.js} +1 -1
- package/frontend/{build/client/_app/immutable/nodes/12.Bu89A1eX.js → .svelte-kit/output/client/_app/immutable/nodes/12.BMSCSd88.js} +1 -1
- package/frontend/.svelte-kit/output/client/_app/immutable/nodes/2.BCPSg7H4.js +6 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/nodes/{3.B86rlHXU.js → 3.8BFAJoHT.js} +1 -1
- package/frontend/{build/client/_app/immutable/nodes/5.CqTXvadV.js → .svelte-kit/output/client/_app/immutable/nodes/5.DVeH2eQP.js} +1 -1
- package/frontend/.svelte-kit/output/client/_app/immutable/nodes/{6.BJZ62MCd.js → 6.DI7-IJhN.js} +1 -1
- package/frontend/{build/client/_app/immutable/nodes/8.B4fj2Xyu.js → .svelte-kit/output/client/_app/immutable/nodes/8.93Mb3ofT.js} +4 -4
- package/frontend/{build/client/_app/immutable/nodes/9.C57_2xq6.js → .svelte-kit/output/client/_app/immutable/nodes/9.CvfGx3Hu.js} +1 -1
- package/frontend/.svelte-kit/output/client/_app/version.json +1 -1
- package/frontend/.svelte-kit/output/server/.vite/manifest.json +1 -1
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.C1TbMUws.css +6 -0
- package/frontend/.svelte-kit/output/server/chunks/internal.js +1 -1
- package/frontend/.svelte-kit/output/server/entries/pages/_page.svelte.js +22 -22
- package/frontend/.svelte-kit/output/server/entries/pages/settings/_page.svelte.js +3 -3
- package/frontend/.svelte-kit/output/server/manifest-full.js +1 -1
- package/frontend/.svelte-kit/output/server/manifest.js +1 -1
- package/frontend/.svelte-kit/output/server/nodes/0.js +1 -1
- package/frontend/.svelte-kit/output/server/nodes/1.js +1 -1
- package/frontend/.svelte-kit/output/server/nodes/11.js +1 -1
- package/frontend/.svelte-kit/output/server/nodes/12.js +1 -1
- package/frontend/.svelte-kit/output/server/nodes/2.js +2 -2
- package/frontend/.svelte-kit/output/server/nodes/3.js +1 -1
- package/frontend/.svelte-kit/output/server/nodes/5.js +1 -1
- package/frontend/.svelte-kit/output/server/nodes/6.js +1 -1
- package/frontend/.svelte-kit/output/server/nodes/8.js +1 -1
- package/frontend/.svelte-kit/output/server/nodes/9.js +1 -1
- package/frontend/build/client/_app/immutable/assets/2.C1TbMUws.css +6 -0
- package/frontend/build/client/_app/immutable/assets/2.C1TbMUws.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/2.C1TbMUws.css.gz +0 -0
- package/frontend/build/client/_app/immutable/assets/_page.C1TbMUws.css +6 -0
- package/frontend/build/client/_app/immutable/assets/_page.C1TbMUws.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/_page.C1TbMUws.css.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/{DEKAA-Fm.js → DDZw0L0T.js} +1 -1
- package/frontend/build/client/_app/immutable/chunks/DDZw0L0T.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/DDZw0L0T.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/{Dhf4xQKo.js → kkA1mrsH.js} +2 -2
- package/frontend/build/client/_app/immutable/chunks/kkA1mrsH.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/kkA1mrsH.js.gz +0 -0
- package/frontend/build/client/_app/immutable/entry/{app.B0GH5ONo.js → app.KrtMZ3my.js} +2 -2
- package/frontend/build/client/_app/immutable/entry/app.KrtMZ3my.js.br +0 -0
- package/frontend/build/client/_app/immutable/entry/app.KrtMZ3my.js.gz +0 -0
- package/frontend/build/client/_app/immutable/entry/{start.Bn25CW9X.js → start.4qxHHAcl.js} +1 -1
- package/frontend/build/client/_app/immutable/entry/{start.Bn25CW9X.js.br → start.4qxHHAcl.js.br} +0 -0
- package/frontend/build/client/_app/immutable/entry/{start.Bn25CW9X.js.gz → start.4qxHHAcl.js.gz} +0 -0
- package/frontend/build/client/_app/immutable/nodes/{0.-imXt0f5.js → 0.C8DodwXs.js} +1 -1
- package/frontend/build/client/_app/immutable/nodes/0.C8DodwXs.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/0.C8DodwXs.js.gz +0 -0
- package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/1.Dom3CKWF.js → build/client/_app/immutable/nodes/1.Df9sLw8b.js} +1 -1
- package/frontend/build/client/_app/immutable/nodes/1.Df9sLw8b.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/1.Df9sLw8b.js.gz +0 -0
- package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/11.DMlayLCK.js → build/client/_app/immutable/nodes/11.BViaDK2m.js} +1 -1
- package/frontend/build/client/_app/immutable/nodes/11.BViaDK2m.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/11.BViaDK2m.js.gz +0 -0
- package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/12.Bu89A1eX.js → build/client/_app/immutable/nodes/12.BMSCSd88.js} +1 -1
- package/frontend/build/client/_app/immutable/nodes/12.BMSCSd88.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/{12.Bu89A1eX.js.gz → 12.BMSCSd88.js.gz} +0 -0
- package/frontend/build/client/_app/immutable/nodes/2.BCPSg7H4.js +6 -0
- package/frontend/build/client/_app/immutable/nodes/2.BCPSg7H4.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/2.BCPSg7H4.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/{3.B86rlHXU.js → 3.8BFAJoHT.js} +1 -1
- package/frontend/build/client/_app/immutable/nodes/3.8BFAJoHT.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/3.8BFAJoHT.js.gz +0 -0
- package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/5.CqTXvadV.js → build/client/_app/immutable/nodes/5.DVeH2eQP.js} +1 -1
- package/frontend/build/client/_app/immutable/nodes/5.DVeH2eQP.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/5.DVeH2eQP.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/{6.BJZ62MCd.js → 6.DI7-IJhN.js} +1 -1
- package/frontend/build/client/_app/immutable/nodes/6.DI7-IJhN.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/6.DI7-IJhN.js.gz +0 -0
- package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/8.B4fj2Xyu.js → build/client/_app/immutable/nodes/8.93Mb3ofT.js} +4 -4
- package/frontend/build/client/_app/immutable/nodes/8.93Mb3ofT.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/8.93Mb3ofT.js.gz +0 -0
- package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/9.C57_2xq6.js → build/client/_app/immutable/nodes/9.CvfGx3Hu.js} +1 -1
- package/frontend/build/client/_app/immutable/nodes/9.CvfGx3Hu.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/9.CvfGx3Hu.js.gz +0 -0
- package/frontend/build/client/_app/version.json +1 -1
- package/frontend/build/client/_app/version.json.br +0 -0
- package/frontend/build/client/_app/version.json.gz +0 -0
- package/frontend/build/server/chunks/{0-DG1Uv07a.js → 0-D5YWFygh.js} +2 -2
- package/frontend/build/server/chunks/{0-DG1Uv07a.js.map → 0-D5YWFygh.js.map} +1 -1
- package/frontend/build/server/chunks/{1-iB0ogJU_.js → 1-Kxn9d30Z.js} +2 -2
- package/frontend/build/server/chunks/{1-iB0ogJU_.js.map → 1-Kxn9d30Z.js.map} +1 -1
- package/frontend/build/server/chunks/{11-Df9vGZ_v.js → 11-CNLerze_.js} +2 -2
- package/frontend/build/server/chunks/{11-Df9vGZ_v.js.map → 11-CNLerze_.js.map} +1 -1
- package/frontend/build/server/chunks/{12-DqvjCln4.js → 12-De6Ol6pu.js} +2 -2
- package/frontend/build/server/chunks/{12-DqvjCln4.js.map → 12-De6Ol6pu.js.map} +1 -1
- package/frontend/build/server/chunks/{2-Cte30Dgy.js → 2-Dc_rKWoK.js} +4 -4
- package/frontend/build/server/chunks/{2-Cte30Dgy.js.map → 2-Dc_rKWoK.js.map} +1 -1
- package/frontend/build/server/chunks/{3-BYKCZi4U.js → 3-DrRqVrcX.js} +2 -2
- package/frontend/build/server/chunks/{3-BYKCZi4U.js.map → 3-DrRqVrcX.js.map} +1 -1
- package/frontend/build/server/chunks/{5-B6Jc1foX.js → 5-CFeE5dPZ.js} +2 -2
- package/frontend/build/server/chunks/{5-B6Jc1foX.js.map → 5-CFeE5dPZ.js.map} +1 -1
- package/frontend/build/server/chunks/{6-dXZKtN86.js → 6-DZk37MZH.js} +2 -2
- package/frontend/build/server/chunks/{6-dXZKtN86.js.map → 6-DZk37MZH.js.map} +1 -1
- package/frontend/build/server/chunks/{8-D-Bp7HzS.js → 8-CKPVFOqJ.js} +3 -3
- package/frontend/build/server/chunks/{8-D-Bp7HzS.js.map → 8-CKPVFOqJ.js.map} +1 -1
- package/frontend/build/server/chunks/{9-BynjuGUO.js → 9-DirZG_Lu.js} +2 -2
- package/frontend/build/server/chunks/{9-BynjuGUO.js.map → 9-DirZG_Lu.js.map} +1 -1
- package/frontend/build/server/chunks/{_page.svelte-CpB-fgJl.js → _page.svelte-04WxjOuH.js} +23 -23
- package/frontend/build/server/chunks/_page.svelte-04WxjOuH.js.map +5 -0
- package/frontend/build/server/chunks/{_page.svelte-wKyUmA2e.js → _page.svelte-8gELc_B1.js} +4 -4
- package/frontend/build/server/chunks/_page.svelte-8gELc_B1.js.map +5 -0
- package/frontend/build/server/index.js +1 -1
- package/frontend/build/server/index.js.map +1 -1
- package/frontend/build/server/manifest.js +11 -11
- package/frontend/build/server/manifest.js.map +1 -1
- package/frontend/package-lock.json +121 -32
- package/frontend/package.json +1 -0
- package/frontend/src/lib/api/reports.js +13 -4
- package/frontend/src/lib/api/repository.js +2 -2
- package/frontend/src/lib/api/settings.js +16 -1
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +9 -24
- package/frontend/src/lib/components/reports/ElementInspector.svelte +141 -0
- package/frontend/src/lib/components/reports/LiveReplayer.svelte +110 -0
- package/frontend/src/lib/components/reports/MultiTabTimeline.svelte +115 -0
- package/frontend/src/lib/components/reports/RecordingPlayer.svelte +786 -0
- package/frontend/src/lib/components/reports/StepsRail.svelte +109 -0
- package/frontend/src/lib/components/ui/CodeViewer.svelte +61 -0
- package/frontend/src/lib/constants.js +0 -1
- package/frontend/src/lib/copy/reports.js +18 -8
- package/frontend/src/lib/copy/repository.js +6 -0
- package/frontend/src/lib/copy/settings.js +25 -4
- package/frontend/src/lib/socketEvents.js +7 -4
- package/frontend/src/lib/stores/runner.js +26 -3
- package/frontend/src/lib/styles/tokens.css +7 -0
- package/frontend/src/lib/utils/format.js +108 -2
- package/frontend/src/lib/utils/inspectElement.js +34 -0
- package/frontend/src/routes/+page.svelte +6 -1
- package/frontend/src/routes/reports/+page.svelte +79 -1
- package/frontend/src/routes/reports/[id]/+page.svelte +304 -495
- package/frontend/src/routes/reports/live/+page.svelte +236 -260
- package/frontend/src/routes/settings/+page.svelte +246 -8
- package/frontend/src/routes/test-repository/suites/[id]/+page.svelte +116 -0
- package/package.json +1 -1
- package/backend/playwright.config.js +0 -85
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.DvMkmYA-.css +0 -6
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/2.DvMkmYA-.css +0 -6
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.DvMkmYA-.css +0 -6
- package/frontend/.svelte-kit/output/client/_app/immutable/nodes/2.BnHalSCi.js +0 -6
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.DvMkmYA-.css +0 -6
- package/frontend/build/client/_app/immutable/assets/2.DvMkmYA-.css +0 -6
- package/frontend/build/client/_app/immutable/assets/2.DvMkmYA-.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/2.DvMkmYA-.css.gz +0 -0
- package/frontend/build/client/_app/immutable/assets/_page.DvMkmYA-.css +0 -6
- package/frontend/build/client/_app/immutable/assets/_page.DvMkmYA-.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/_page.DvMkmYA-.css.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/DEKAA-Fm.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/DEKAA-Fm.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/Dhf4xQKo.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/Dhf4xQKo.js.gz +0 -0
- package/frontend/build/client/_app/immutable/entry/app.B0GH5ONo.js.br +0 -0
- package/frontend/build/client/_app/immutable/entry/app.B0GH5ONo.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/0.-imXt0f5.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/0.-imXt0f5.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/1.Dom3CKWF.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/1.Dom3CKWF.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/11.DMlayLCK.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/11.DMlayLCK.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/12.Bu89A1eX.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/2.BnHalSCi.js +0 -6
- package/frontend/build/client/_app/immutable/nodes/2.BnHalSCi.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/2.BnHalSCi.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/3.B86rlHXU.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/3.B86rlHXU.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/5.CqTXvadV.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/5.CqTXvadV.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/6.BJZ62MCd.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/6.BJZ62MCd.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/8.B4fj2Xyu.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/8.B4fj2Xyu.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/9.C57_2xq6.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/9.C57_2xq6.js.gz +0 -0
- package/frontend/build/server/chunks/_page.svelte-CpB-fgJl.js.map +0 -5
- package/frontend/build/server/chunks/_page.svelte-wKyUmA2e.js.map +0 -5
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
saveBackupConfig,
|
|
19
19
|
testBackupS3,
|
|
20
20
|
runBackupNow,
|
|
21
|
+
fetchS3Backups,
|
|
22
|
+
restoreFromS3,
|
|
21
23
|
fetchMcpConfig,
|
|
22
24
|
generateMcpKey as generateMcpKeyApi
|
|
23
25
|
} from '$lib/api/settings';
|
|
@@ -214,8 +216,10 @@
|
|
|
214
216
|
IMPORT_BLOCK_TITLE,
|
|
215
217
|
IMPORT_BLOCK_DESC,
|
|
216
218
|
CHOOSE_FILE_LABEL,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
+
INCLUDE_REPORTS_LABEL,
|
|
220
|
+
INCLUDE_REPORTS_HINT,
|
|
221
|
+
includeReportsDisclaimer,
|
|
222
|
+
saveIncludeReportsLabel,
|
|
219
223
|
S3_STORAGE_CARD_TITLE,
|
|
220
224
|
S3_STORAGE_DESC_PREFIX,
|
|
221
225
|
ENDPOINT_URL_LABEL,
|
|
@@ -240,6 +244,15 @@
|
|
|
240
244
|
S3_CONNECTION_FAILED,
|
|
241
245
|
BACKUP_CONFIG_SAVED_TOAST,
|
|
242
246
|
BACKUP_CONFIG_SAVE_FAILED,
|
|
247
|
+
RESTORE_FROM_S3_CARD_TITLE,
|
|
248
|
+
RESTORE_FROM_S3_DESC,
|
|
249
|
+
CONFIGURE_S3_FIRST_RESTORE_MESSAGE,
|
|
250
|
+
NO_S3_BACKUPS_MESSAGE,
|
|
251
|
+
RESTORE_CONFIRM_TITLE,
|
|
252
|
+
restoreConfirmBody,
|
|
253
|
+
RESTORE_SUCCESS_TOAST,
|
|
254
|
+
RESTORE_FAILED_FALLBACK,
|
|
255
|
+
LIST_S3_BACKUPS_FAILED,
|
|
243
256
|
SCHEDULED_BACKUP_CARD_TITLE,
|
|
244
257
|
CONFIGURE_S3_FIRST_MESSAGE,
|
|
245
258
|
ENABLE_SCHEDULED_BACKUP_LABEL,
|
|
@@ -264,7 +277,10 @@
|
|
|
264
277
|
saveS3ConfigLabel,
|
|
265
278
|
uploadedToLabel,
|
|
266
279
|
uploadS3NowLabel,
|
|
267
|
-
saveScheduleLabel
|
|
280
|
+
saveScheduleLabel,
|
|
281
|
+
restoreLabel,
|
|
282
|
+
refreshingLabel,
|
|
283
|
+
backupSizeLabel
|
|
268
284
|
} from '$lib/copy/settings';
|
|
269
285
|
|
|
270
286
|
/** @type {'project' | 'runners' | 'repository' | 'integrations' | 'mcp' | 'account' | 'users' | 'backup'} */
|
|
@@ -335,9 +351,11 @@
|
|
|
335
351
|
backupS3Bucket: '',
|
|
336
352
|
backupS3AccessKey: '',
|
|
337
353
|
backupS3SecretKey: '',
|
|
338
|
-
backupS3Prefix: ''
|
|
354
|
+
backupS3Prefix: '',
|
|
355
|
+
backupIncludeReports: false
|
|
339
356
|
};
|
|
340
357
|
let backupConfigSaving = false;
|
|
358
|
+
let includeReportsSaving = false;
|
|
341
359
|
let backupS3SecretKeySet = false;
|
|
342
360
|
let backupTestingS3 = false;
|
|
343
361
|
let backupRunningNow = false;
|
|
@@ -346,6 +364,13 @@
|
|
|
346
364
|
let backupLastRunAt = null;
|
|
347
365
|
let backupLastStatus = '';
|
|
348
366
|
|
|
367
|
+
let s3Backups = [];
|
|
368
|
+
let s3BackupsLoaded = false;
|
|
369
|
+
let loadingS3Backups = false;
|
|
370
|
+
let restoringKey = null;
|
|
371
|
+
let restoreConfirmOpen = false;
|
|
372
|
+
let restoreTarget = null;
|
|
373
|
+
|
|
349
374
|
let runners = [];
|
|
350
375
|
let runnerForm = { name: '', url: '', token: '', browser: 'chromium' };
|
|
351
376
|
let runnerFormError = '';
|
|
@@ -406,7 +431,8 @@
|
|
|
406
431
|
backupS3Bucket: bc.backupS3Bucket,
|
|
407
432
|
backupS3AccessKey: bc.backupS3AccessKey,
|
|
408
433
|
backupS3SecretKey: '',
|
|
409
|
-
backupS3Prefix: bc.backupS3Prefix
|
|
434
|
+
backupS3Prefix: bc.backupS3Prefix,
|
|
435
|
+
backupIncludeReports: bc.backupIncludeReports
|
|
410
436
|
};
|
|
411
437
|
} catch {}
|
|
412
438
|
if ($auth.user) {
|
|
@@ -802,6 +828,23 @@
|
|
|
802
828
|
}
|
|
803
829
|
}
|
|
804
830
|
|
|
831
|
+
async function handleSaveIncludeReports() {
|
|
832
|
+
includeReportsSaving = true;
|
|
833
|
+
try {
|
|
834
|
+
const payload = { ...backupConfig };
|
|
835
|
+
if (!payload.backupS3SecretKey) delete payload.backupS3SecretKey;
|
|
836
|
+
const result = await saveBackupConfig(payload);
|
|
837
|
+
if (result.error) throw new Error(result.error);
|
|
838
|
+
if (backupConfig.backupS3SecretKey) backupS3SecretKeySet = true;
|
|
839
|
+
backupConfig = { ...backupConfig, backupS3SecretKey: '' };
|
|
840
|
+
showToast('success', BACKUP_CONFIG_SAVED_TOAST);
|
|
841
|
+
} catch (e) {
|
|
842
|
+
showToast('error', e.message || BACKUP_CONFIG_SAVE_FAILED);
|
|
843
|
+
} finally {
|
|
844
|
+
includeReportsSaving = false;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
805
848
|
async function handleTestS3() {
|
|
806
849
|
backupTestingS3 = true;
|
|
807
850
|
backupS3TestResult = null;
|
|
@@ -845,6 +888,48 @@
|
|
|
845
888
|
backupS3SecretKeySet
|
|
846
889
|
);
|
|
847
890
|
|
|
891
|
+
// Fetch the list once, the first time S3 looks configured — the Refresh
|
|
892
|
+
// button covers reloading after a new upload or a bucket/prefix change.
|
|
893
|
+
$: if (s3Configured && !s3BackupsLoaded) {
|
|
894
|
+
s3BackupsLoaded = true;
|
|
895
|
+
loadS3Backups();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async function loadS3Backups() {
|
|
899
|
+
loadingS3Backups = true;
|
|
900
|
+
try {
|
|
901
|
+
const result = await fetchS3Backups();
|
|
902
|
+
if (result.error) throw new Error(result.error);
|
|
903
|
+
s3Backups = result.backups ?? [];
|
|
904
|
+
} catch (e) {
|
|
905
|
+
showToast('error', e.message || LIST_S3_BACKUPS_FAILED);
|
|
906
|
+
} finally {
|
|
907
|
+
loadingS3Backups = false;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function openRestoreConfirm(backup) {
|
|
912
|
+
restoreTarget = backup;
|
|
913
|
+
restoreConfirmOpen = true;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
async function handleRestoreFromS3() {
|
|
917
|
+
if (!restoreTarget) return;
|
|
918
|
+
restoringKey = restoreTarget.key;
|
|
919
|
+
try {
|
|
920
|
+
const result = await restoreFromS3(restoreTarget.key);
|
|
921
|
+
if (result.error) throw new Error(result.error);
|
|
922
|
+
showToast('success', RESTORE_SUCCESS_TOAST);
|
|
923
|
+
project = await fetchProject();
|
|
924
|
+
restoreConfirmOpen = false;
|
|
925
|
+
restoreTarget = null;
|
|
926
|
+
} catch (e) {
|
|
927
|
+
showToast('error', e.message || RESTORE_FAILED_FALLBACK);
|
|
928
|
+
} finally {
|
|
929
|
+
restoringKey = null;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
848
933
|
$: mcpConfigSnippet = JSON.stringify(
|
|
849
934
|
{
|
|
850
935
|
mcpServers: {
|
|
@@ -1822,10 +1907,36 @@
|
|
|
1822
1907
|
</div>
|
|
1823
1908
|
</div>
|
|
1824
1909
|
</div>
|
|
1910
|
+
|
|
1911
|
+
<div class="include-reports-row">
|
|
1912
|
+
<label class="field-label backup-toggle-label" for="include-reports">
|
|
1913
|
+
<span>
|
|
1914
|
+
{INCLUDE_REPORTS_LABEL}
|
|
1915
|
+
<span class="field-hint">{INCLUDE_REPORTS_HINT}</span>
|
|
1916
|
+
</span>
|
|
1917
|
+
<button
|
|
1918
|
+
id="include-reports"
|
|
1919
|
+
class="toggle-btn"
|
|
1920
|
+
class:active={backupConfig.backupIncludeReports}
|
|
1921
|
+
on:click={() =>
|
|
1922
|
+
(backupConfig.backupIncludeReports = !backupConfig.backupIncludeReports)}
|
|
1923
|
+
role="switch"
|
|
1924
|
+
aria-checked={backupConfig.backupIncludeReports}
|
|
1925
|
+
>
|
|
1926
|
+
<span class="toggle-thumb"></span>
|
|
1927
|
+
</button>
|
|
1928
|
+
</label>
|
|
1929
|
+
<Button
|
|
1930
|
+
variant="ghost"
|
|
1931
|
+
on:click={handleSaveIncludeReports}
|
|
1932
|
+
disabled={includeReportsSaving}
|
|
1933
|
+
>
|
|
1934
|
+
{saveIncludeReportsLabel(includeReportsSaving)}
|
|
1935
|
+
</Button>
|
|
1936
|
+
</div>
|
|
1937
|
+
|
|
1825
1938
|
<p class="backup-disclaimer">
|
|
1826
|
-
{
|
|
1827
|
-
<code>pg_dump</code>
|
|
1828
|
-
{BACKUP_DISCLAIMER_SUFFIX}
|
|
1939
|
+
{includeReportsDisclaimer(backupConfig.backupIncludeReports)}
|
|
1829
1940
|
</p>
|
|
1830
1941
|
</div>
|
|
1831
1942
|
|
|
@@ -1947,6 +2058,64 @@
|
|
|
1947
2058
|
{/if}
|
|
1948
2059
|
</div>
|
|
1949
2060
|
|
|
2061
|
+
<!-- Restore from S3 -->
|
|
2062
|
+
<ConfirmModal
|
|
2063
|
+
bind:open={restoreConfirmOpen}
|
|
2064
|
+
title={RESTORE_CONFIRM_TITLE}
|
|
2065
|
+
confirmLabel={restoreLabel(false)}
|
|
2066
|
+
loading={!!restoringKey}
|
|
2067
|
+
on:confirm={handleRestoreFromS3}
|
|
2068
|
+
>
|
|
2069
|
+
{#if restoreTarget}
|
|
2070
|
+
{restoreConfirmBody(restoreTarget.key)}
|
|
2071
|
+
{/if}
|
|
2072
|
+
</ConfirmModal>
|
|
2073
|
+
|
|
2074
|
+
<div class="card settings-card" class:card-disabled={!s3Configured}>
|
|
2075
|
+
<div class="card-title-row">
|
|
2076
|
+
<p class="card-title">{RESTORE_FROM_S3_CARD_TITLE}</p>
|
|
2077
|
+
{#if s3Configured}
|
|
2078
|
+
<Button variant="ghost" on:click={loadS3Backups} disabled={loadingS3Backups}>
|
|
2079
|
+
{refreshingLabel(loadingS3Backups)}
|
|
2080
|
+
</Button>
|
|
2081
|
+
{/if}
|
|
2082
|
+
</div>
|
|
2083
|
+
|
|
2084
|
+
{#if !s3Configured}
|
|
2085
|
+
<p class="backup-block-desc">{CONFIGURE_S3_FIRST_RESTORE_MESSAGE}</p>
|
|
2086
|
+
{:else}
|
|
2087
|
+
<p class="backup-block-desc" style="margin-bottom: 1rem;">{RESTORE_FROM_S3_DESC}</p>
|
|
2088
|
+
|
|
2089
|
+
{#if s3Backups.length === 0}
|
|
2090
|
+
<p class="backup-block-desc">
|
|
2091
|
+
{loadingS3Backups ? refreshingLabel(true) : NO_S3_BACKUPS_MESSAGE}
|
|
2092
|
+
</p>
|
|
2093
|
+
{:else}
|
|
2094
|
+
<ul class="s3-backup-list">
|
|
2095
|
+
{#each s3Backups as backup (backup.key)}
|
|
2096
|
+
<li class="s3-backup-row">
|
|
2097
|
+
<div class="s3-backup-info">
|
|
2098
|
+
<span class="s3-backup-key">{backup.key}</span>
|
|
2099
|
+
<span class="s3-backup-meta">
|
|
2100
|
+
{new Date(backup.lastModified).toLocaleString()} · {backupSizeLabel(
|
|
2101
|
+
backup.size
|
|
2102
|
+
)}
|
|
2103
|
+
</span>
|
|
2104
|
+
</div>
|
|
2105
|
+
<Button
|
|
2106
|
+
variant="ghost"
|
|
2107
|
+
on:click={() => openRestoreConfirm(backup)}
|
|
2108
|
+
disabled={restoringKey === backup.key}
|
|
2109
|
+
>
|
|
2110
|
+
{restoreLabel(restoringKey === backup.key)}
|
|
2111
|
+
</Button>
|
|
2112
|
+
</li>
|
|
2113
|
+
{/each}
|
|
2114
|
+
</ul>
|
|
2115
|
+
{/if}
|
|
2116
|
+
{/if}
|
|
2117
|
+
</div>
|
|
2118
|
+
|
|
1950
2119
|
<!-- Scheduled backup -->
|
|
1951
2120
|
<div class="card settings-card" class:card-disabled={!s3Configured}>
|
|
1952
2121
|
<p class="card-title">{SCHEDULED_BACKUP_CARD_TITLE}</p>
|
|
@@ -2390,6 +2559,23 @@
|
|
|
2390
2559
|
border-radius: 3px;
|
|
2391
2560
|
}
|
|
2392
2561
|
|
|
2562
|
+
.include-reports-row {
|
|
2563
|
+
display: flex;
|
|
2564
|
+
align-items: center;
|
|
2565
|
+
gap: 1.25rem;
|
|
2566
|
+
margin-top: 1.25rem;
|
|
2567
|
+
padding-top: 1.25rem;
|
|
2568
|
+
border-top: 1px solid var(--border);
|
|
2569
|
+
}
|
|
2570
|
+
.include-reports-row .backup-toggle-label {
|
|
2571
|
+
flex: 1;
|
|
2572
|
+
}
|
|
2573
|
+
.include-reports-row .field-label > span {
|
|
2574
|
+
display: flex;
|
|
2575
|
+
flex-direction: column;
|
|
2576
|
+
gap: 0.15rem;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2393
2579
|
.backup-disclaimer {
|
|
2394
2580
|
margin-top: 1rem;
|
|
2395
2581
|
padding: 0.625rem 0.875rem;
|
|
@@ -2422,6 +2608,58 @@
|
|
|
2422
2608
|
color: var(--fail);
|
|
2423
2609
|
}
|
|
2424
2610
|
|
|
2611
|
+
.card-title-row {
|
|
2612
|
+
display: flex;
|
|
2613
|
+
align-items: center;
|
|
2614
|
+
justify-content: space-between;
|
|
2615
|
+
gap: 1rem;
|
|
2616
|
+
margin-bottom: 0.75rem;
|
|
2617
|
+
}
|
|
2618
|
+
.card-title-row .card-title {
|
|
2619
|
+
margin-bottom: 0;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
.s3-backup-list {
|
|
2623
|
+
display: flex;
|
|
2624
|
+
flex-direction: column;
|
|
2625
|
+
gap: 0.5rem;
|
|
2626
|
+
list-style: none;
|
|
2627
|
+
margin: 0;
|
|
2628
|
+
padding: 0;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
.s3-backup-row {
|
|
2632
|
+
display: flex;
|
|
2633
|
+
align-items: center;
|
|
2634
|
+
justify-content: space-between;
|
|
2635
|
+
gap: 1rem;
|
|
2636
|
+
padding: 0.625rem 0.875rem;
|
|
2637
|
+
background: var(--bg-subtle);
|
|
2638
|
+
border: 1px solid var(--border);
|
|
2639
|
+
border-radius: var(--radius-sm);
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
.s3-backup-info {
|
|
2643
|
+
display: flex;
|
|
2644
|
+
flex-direction: column;
|
|
2645
|
+
gap: 0.2rem;
|
|
2646
|
+
min-width: 0;
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
.s3-backup-key {
|
|
2650
|
+
font-family: 'JetBrains Mono', monospace;
|
|
2651
|
+
font-size: 0.8125rem;
|
|
2652
|
+
color: var(--text);
|
|
2653
|
+
overflow: hidden;
|
|
2654
|
+
text-overflow: ellipsis;
|
|
2655
|
+
white-space: nowrap;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
.s3-backup-meta {
|
|
2659
|
+
font-size: 0.75rem;
|
|
2660
|
+
color: var(--text-muted);
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2425
2663
|
.backup-last-run {
|
|
2426
2664
|
font-size: 0.8125rem;
|
|
2427
2665
|
color: var(--text-muted);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { fly, fade } from 'svelte/transition';
|
|
10
10
|
import {
|
|
11
11
|
fetchSuite,
|
|
12
|
+
fetchAllSuitesWithCases,
|
|
12
13
|
updateSuite,
|
|
13
14
|
createTestCase,
|
|
14
15
|
fetchTestCase,
|
|
@@ -80,13 +81,20 @@
|
|
|
80
81
|
TEST_CASE_UPDATED_TOAST,
|
|
81
82
|
STEPS_SAVED_TOAST,
|
|
82
83
|
SUITE_UPDATED_TOAST,
|
|
84
|
+
MOVE_CASE_TITLE,
|
|
85
|
+
MOVE_TEST_CASE_MODAL_TITLE,
|
|
86
|
+
MOVE_TO_SUITE_LABEL,
|
|
87
|
+
NO_SUITES_AVAILABLE,
|
|
88
|
+
FAILED_TO_MOVE_CASE,
|
|
83
89
|
suiteDetailTitle,
|
|
84
90
|
createdByCapitalized,
|
|
85
91
|
caseCreatedToast,
|
|
86
92
|
caseDeletedToast,
|
|
93
|
+
caseMovedToast,
|
|
87
94
|
caseCount,
|
|
88
95
|
stepCount,
|
|
89
96
|
saveStepsLabel,
|
|
97
|
+
moveCaseLabel,
|
|
90
98
|
runLinkLabel
|
|
91
99
|
} from '$lib/copy/repository';
|
|
92
100
|
|
|
@@ -118,6 +126,13 @@
|
|
|
118
126
|
let confirmDeleteCase = null;
|
|
119
127
|
let confirmDeleteCaseOpen = false;
|
|
120
128
|
|
|
129
|
+
let allSuites = [];
|
|
130
|
+
let moveCaseOpen = false;
|
|
131
|
+
let moveCaseTarget = null;
|
|
132
|
+
let moveCaseSuiteId = '';
|
|
133
|
+
let moveCaseSaving = false;
|
|
134
|
+
$: otherSuites = allSuites.filter((s) => s.id !== suiteId);
|
|
135
|
+
|
|
121
136
|
let caseSearch = '';
|
|
122
137
|
let casesPage = 1;
|
|
123
138
|
let casesSort = (() => {
|
|
@@ -181,6 +196,9 @@
|
|
|
181
196
|
} finally {
|
|
182
197
|
loading = false;
|
|
183
198
|
}
|
|
199
|
+
try {
|
|
200
|
+
allSuites = await fetchAllSuitesWithCases();
|
|
201
|
+
} catch {}
|
|
184
202
|
});
|
|
185
203
|
|
|
186
204
|
async function handleCreateCase() {
|
|
@@ -287,6 +305,34 @@
|
|
|
287
305
|
confirmDeleteCaseOpen = false;
|
|
288
306
|
}
|
|
289
307
|
|
|
308
|
+
function openMoveCase(tc) {
|
|
309
|
+
moveCaseTarget = tc;
|
|
310
|
+
moveCaseSuiteId = otherSuites[0]?.id ?? '';
|
|
311
|
+
moveCaseOpen = true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function handleMoveCase() {
|
|
315
|
+
if (!moveCaseTarget || !moveCaseSuiteId) return;
|
|
316
|
+
moveCaseSaving = true;
|
|
317
|
+
try {
|
|
318
|
+
await updateTestCase(moveCaseTarget.id, { suiteId: moveCaseSuiteId });
|
|
319
|
+
const targetSuite = allSuites.find((s) => s.id === moveCaseSuiteId);
|
|
320
|
+
suite = {
|
|
321
|
+
...suite,
|
|
322
|
+
cases: suite.cases.filter((c) => c.id !== moveCaseTarget.id),
|
|
323
|
+
_count: { cases: suite._count.cases - 1 }
|
|
324
|
+
};
|
|
325
|
+
if (selectedCase?.id === moveCaseTarget.id) selectedCase = null;
|
|
326
|
+
showToast('success', caseMovedToast(moveCaseTarget.displayId, targetSuite?.name ?? ''));
|
|
327
|
+
moveCaseOpen = false;
|
|
328
|
+
moveCaseTarget = null;
|
|
329
|
+
} catch (e) {
|
|
330
|
+
showToast('error', e.message ?? FAILED_TO_MOVE_CASE);
|
|
331
|
+
} finally {
|
|
332
|
+
moveCaseSaving = false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
290
336
|
async function handleUpdateSuite() {
|
|
291
337
|
editSuiteSaving = true;
|
|
292
338
|
try {
|
|
@@ -409,6 +455,32 @@
|
|
|
409
455
|
</div>
|
|
410
456
|
</Modal>
|
|
411
457
|
|
|
458
|
+
<Modal bind:open={moveCaseOpen} title={MOVE_TEST_CASE_MODAL_TITLE}>
|
|
459
|
+
<div class="form-fields">
|
|
460
|
+
{#if moveCaseTarget}
|
|
461
|
+
<p class="move-case-subtitle">{moveCaseTarget.displayId} — {moveCaseTarget.title}</p>
|
|
462
|
+
{/if}
|
|
463
|
+
{#if otherSuites.length === 0}
|
|
464
|
+
<p class="form-error">{NO_SUITES_AVAILABLE}</p>
|
|
465
|
+
{:else}
|
|
466
|
+
<div class="field">
|
|
467
|
+
<label class="field-label" for="move-suite">{MOVE_TO_SUITE_LABEL}</label>
|
|
468
|
+
<select id="move-suite" class="field-input" bind:value={moveCaseSuiteId}>
|
|
469
|
+
{#each otherSuites as s}
|
|
470
|
+
<option value={s.id}>{s.displayId} — {s.name}</option>
|
|
471
|
+
{/each}
|
|
472
|
+
</select>
|
|
473
|
+
</div>
|
|
474
|
+
{/if}
|
|
475
|
+
<div class="modal-actions">
|
|
476
|
+
<Button on:click={handleMoveCase} disabled={moveCaseSaving || otherSuites.length === 0}>
|
|
477
|
+
{moveCaseLabel(moveCaseSaving)}
|
|
478
|
+
</Button>
|
|
479
|
+
<Button variant="ghost" on:click={() => (moveCaseOpen = false)}>{CANCEL_LABEL}</Button>
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
</Modal>
|
|
483
|
+
|
|
412
484
|
{#if loading}
|
|
413
485
|
<div class="loading-state">{LOADING_LABEL}</div>
|
|
414
486
|
{:else if suite}
|
|
@@ -548,6 +620,26 @@
|
|
|
548
620
|
<span class="auto-badge">{AUTOMATED_LABEL}</span>
|
|
549
621
|
{/if}
|
|
550
622
|
<div class="case-row-actions" on:click|stopPropagation>
|
|
623
|
+
<button
|
|
624
|
+
class="icon-btn small"
|
|
625
|
+
title={MOVE_CASE_TITLE}
|
|
626
|
+
on:click={() => openMoveCase(tc)}
|
|
627
|
+
>
|
|
628
|
+
<svg
|
|
629
|
+
width="11"
|
|
630
|
+
height="11"
|
|
631
|
+
viewBox="0 0 24 24"
|
|
632
|
+
fill="none"
|
|
633
|
+
stroke="currentColor"
|
|
634
|
+
stroke-width="2"
|
|
635
|
+
stroke-linecap="round"
|
|
636
|
+
stroke-linejoin="round"
|
|
637
|
+
>
|
|
638
|
+
<path
|
|
639
|
+
d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"
|
|
640
|
+
/>
|
|
641
|
+
</svg>
|
|
642
|
+
</button>
|
|
551
643
|
<button
|
|
552
644
|
class="icon-btn danger small"
|
|
553
645
|
on:click={() => {
|
|
@@ -606,6 +698,26 @@
|
|
|
606
698
|
{/if}
|
|
607
699
|
<div class="detail-header-actions">
|
|
608
700
|
{#if !editingCase}
|
|
701
|
+
<button
|
|
702
|
+
class="icon-btn"
|
|
703
|
+
title={MOVE_CASE_TITLE}
|
|
704
|
+
on:click={() => openMoveCase(selectedCase)}
|
|
705
|
+
>
|
|
706
|
+
<svg
|
|
707
|
+
width="13"
|
|
708
|
+
height="13"
|
|
709
|
+
viewBox="0 0 24 24"
|
|
710
|
+
fill="none"
|
|
711
|
+
stroke="currentColor"
|
|
712
|
+
stroke-width="2"
|
|
713
|
+
stroke-linecap="round"
|
|
714
|
+
stroke-linejoin="round"
|
|
715
|
+
>
|
|
716
|
+
<path
|
|
717
|
+
d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"
|
|
718
|
+
/>
|
|
719
|
+
</svg>
|
|
720
|
+
</button>
|
|
609
721
|
<button class="icon-btn" title={EDIT_CASE_TITLE} on:click={startEditCase}>
|
|
610
722
|
<svg
|
|
611
723
|
width="13"
|
|
@@ -1577,6 +1689,10 @@
|
|
|
1577
1689
|
font-size: 0.8125rem;
|
|
1578
1690
|
color: var(--fail);
|
|
1579
1691
|
}
|
|
1692
|
+
.move-case-subtitle {
|
|
1693
|
+
font-size: 0.8125rem;
|
|
1694
|
+
color: var(--text-muted);
|
|
1695
|
+
}
|
|
1580
1696
|
.modal-actions {
|
|
1581
1697
|
display: flex;
|
|
1582
1698
|
gap: 0.5rem;
|
package/package.json
CHANGED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* This file is part of Plum.
|
|
3
|
-
* Licensed under the MIT License. See LICENSE file in the project root for details.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// @ts-check
|
|
7
|
-
import { defineConfig, devices } from '@playwright/test';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Read environment variables from file.
|
|
11
|
-
* https://github.com/motdotla/dotenv
|
|
12
|
-
*/
|
|
13
|
-
import dotenv from 'dotenv';
|
|
14
|
-
import path from 'path';
|
|
15
|
-
dotenv.config({ path: path.resolve(__dirname, '.env') });
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* @see https://playwright.dev/docs/test-configuration
|
|
19
|
-
*/
|
|
20
|
-
export default defineConfig({
|
|
21
|
-
testDir: './tests',
|
|
22
|
-
/* Run tests in files in parallel */
|
|
23
|
-
fullyParallel: true,
|
|
24
|
-
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
25
|
-
forbidOnly: !!process.env.CI,
|
|
26
|
-
/* Retry on CI only */
|
|
27
|
-
retries: process.env.CI ? 2 : 0,
|
|
28
|
-
/* Opt out of parallel tests on CI. */
|
|
29
|
-
workers: process.env.CI ? 1 : undefined,
|
|
30
|
-
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
31
|
-
reporter: 'html',
|
|
32
|
-
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
33
|
-
use: {
|
|
34
|
-
/* Base URL to use in actions like `await page.goto('/')`. */
|
|
35
|
-
// baseURL: 'http://127.0.0.1:3000',
|
|
36
|
-
|
|
37
|
-
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
38
|
-
trace: 'on-first-retry'
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
/* Configure projects for major browsers */
|
|
42
|
-
projects: [
|
|
43
|
-
{
|
|
44
|
-
name: 'chromium',
|
|
45
|
-
use: { ...devices['Desktop Chrome'] }
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
{
|
|
49
|
-
name: 'firefox',
|
|
50
|
-
use: { ...devices['Desktop Firefox'] }
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
{
|
|
54
|
-
name: 'webkit',
|
|
55
|
-
use: { ...devices['Desktop Safari'] }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/* Test against mobile viewports. */
|
|
59
|
-
// {
|
|
60
|
-
// name: 'Mobile Chrome',
|
|
61
|
-
// use: { ...devices['Pixel 5'] },
|
|
62
|
-
// },
|
|
63
|
-
// {
|
|
64
|
-
// name: 'Mobile Safari',
|
|
65
|
-
// use: { ...devices['iPhone 12'] },
|
|
66
|
-
// },
|
|
67
|
-
|
|
68
|
-
/* Test against branded browsers. */
|
|
69
|
-
// {
|
|
70
|
-
// name: 'Microsoft Edge',
|
|
71
|
-
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
|
72
|
-
// },
|
|
73
|
-
// {
|
|
74
|
-
// name: 'Google Chrome',
|
|
75
|
-
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
|
76
|
-
// },
|
|
77
|
-
]
|
|
78
|
-
|
|
79
|
-
/* Run your local dev server before starting the tests */
|
|
80
|
-
// webServer: {
|
|
81
|
-
// command: 'npm run start',
|
|
82
|
-
// url: 'http://127.0.0.1:3000',
|
|
83
|
-
// reuseExistingServer: !process.env.CI,
|
|
84
|
-
// },
|
|
85
|
-
});
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* This file is part of Plum.
|
|
3
|
-
* Licensed under the MIT License. See LICENSE file in the project root for details.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
.page-header.svelte-ll28r2{margin-bottom:1.75rem;padding-bottom:1.5rem;border-bottom:1px solid var(--border)}.header-top.svelte-ll28r2{display:flex;align-items:flex-end;justify-content:space-between;gap:1rem;margin-bottom:1.25rem}h1.svelte-ll28r2{font-size:2.5rem;margin-bottom:.25rem}.subtitle.svelte-ll28r2{color:var(--text-muted);font-size:.875rem}.search-row.svelte-ll28r2{display:flex;gap:.75rem}.search-wrap.svelte-ll28r2{position:relative;flex:1;max-width:480px}.search-icon.svelte-ll28r2{position:absolute;left:.75rem;top:50%;transform:translateY(-50%);color:var(--text-muted);pointer-events:none}.search-input.svelte-ll28r2{width:100%;padding:.55rem .875rem .55rem 2.25rem;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-subtle);color:var(--text);font-family:var(--font-body);font-size:.875rem;font-weight:300;outline:none;transition:border-color var(--duration-fast),box-shadow var(--duration-fast)}.search-input.svelte-ll28r2:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);background:var(--bg-elevated)}.sort-bar.svelte-ll28r2{display:flex;align-items:center;gap:.375rem;margin:1.25rem 0;flex-wrap:wrap}.sort-label.svelte-ll28r2{font-size:.75rem;color:var(--text-muted);margin-right:.25rem}.sort-chip.svelte-ll28r2{display:flex;align-items:center;gap:.25rem;height:28px;padding:0 .625rem;font-family:var(--font-body);font-size:.8125rem;color:var(--text-muted);background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius-pill);cursor:pointer;transition:color var(--duration-fast),border-color var(--duration-fast),background var(--duration-fast)}.sort-chip.svelte-ll28r2:hover{color:var(--text);border-color:var(--accent)}.sort-chip.active.svelte-ll28r2{color:var(--accent);border-color:var(--accent);background:var(--accent-soft)}.sort-dir.svelte-ll28r2{font-size:.7rem;opacity:.8}.search-input.svelte-ll28r2::placeholder{color:var(--text-muted)}.search-clear.svelte-ll28r2{position:absolute;right:.625rem;top:50%;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;border:none;background:var(--border);color:var(--text-muted);cursor:pointer;transition:background var(--duration-fast),color var(--duration-fast)}.search-clear.svelte-ll28r2:hover{background:var(--text-muted);color:var(--bg)}.suites.svelte-ll28r2{display:flex;flex-direction:column;gap:1rem}.suite.svelte-ll28r2{border:1px solid var(--border);border-radius:var(--radius-lg);overflow:visible;background:var(--bg-elevated);animation:fadeUp .35s var(--ease-out) both}.suite-header.svelte-ll28r2{display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.875rem 1.25rem;background:var(--bg-subtle);border-bottom:1px solid var(--border);border-radius:var(--radius-lg) var(--radius-lg) 0 0}.suite-toggle.svelte-ll28r2{display:flex;align-items:center;justify-content:center;width:22px;height:22px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;flex-shrink:0;border-radius:var(--radius-sm);transition:background var(--duration-fast),color var(--duration-fast)}.suite-toggle.svelte-ll28r2:hover{background:var(--bg-elevated);color:var(--text)}.suite-toggle.svelte-ll28r2 svg:where(.svelte-ll28r2){transition:transform var(--duration-fast) var(--ease-out)}.suite-toggle.svelte-ll28r2 svg.rotated:where(.svelte-ll28r2){transform:rotate(90deg)}.suite-meta.svelte-ll28r2{display:flex;align-items:center;gap:.625rem;flex:1;min-width:0}.suite-badges.svelte-ll28r2{display:flex;gap:.25rem;flex-shrink:0}.suite-name.svelte-ll28r2{font-size:.9rem;font-weight:400;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.suite-count.svelte-ll28r2{font-size:.75rem;color:var(--text-muted);flex-shrink:0}.run-btn.svelte-ll28r2{display:inline-flex;align-items:center;gap:.35rem;font-family:var(--font-body);font-size:.75rem;font-weight:400;padding:.3rem .75rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-elevated);color:var(--text-muted);cursor:pointer;white-space:nowrap;flex-shrink:0;transition:background var(--duration-fast),color var(--duration-fast),border-color var(--duration-fast)}.run-btn.svelte-ll28r2:hover{background:var(--accent-soft);color:var(--accent);border-color:var(--accent)}.tests.svelte-ll28r2{display:flex;flex-direction:column}.test-row.svelte-ll28r2{border-bottom:1px solid var(--border);animation:fadeUp .3s var(--ease-out) both}.test-row.svelte-ll28r2:last-child{border-bottom:none}.test-main.svelte-ll28r2{display:flex;align-items:center;gap:.625rem;padding:.75rem 1.25rem}.run-icon-btn.svelte-ll28r2{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;border:1px solid var(--border);background:transparent;color:var(--text-muted);cursor:pointer;flex-shrink:0;transition:background var(--duration-fast),color var(--duration-fast),border-color var(--duration-fast)}.run-icon-btn.svelte-ll28r2:hover{background:var(--accent);color:var(--white);border-color:var(--accent)}.test-ids.svelte-ll28r2{display:flex;gap:.25rem;flex-shrink:0}.id-pill.svelte-ll28r2{position:relative;font-size:.68rem;font-weight:500;letter-spacing:.04em;color:var(--accent);background:var(--accent-soft);border:none;border-radius:var(--radius-pill);padding:.15rem .55rem;cursor:pointer;font-family:JetBrains Mono,monospace;transition:background var(--duration-fast),color var(--duration-fast),filter var(--duration-fast)}.id-pill.svelte-ll28r2:hover{filter:brightness(.92)}.id-pill.copied.svelte-ll28r2{color:var(--pass);background:var(--pass-soft)}.id-pill.copied.svelte-ll28r2:after{content:"Copied";position:absolute;left:50%;bottom:calc(100% + 6px);z-index:50;transform:translate(-50%);padding:.16rem .42rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-elevated);color:var(--pass);box-shadow:0 4px 14px #00000014;font-family:var(--font-body);font-size:.65rem;font-weight:500;letter-spacing:0;pointer-events:none;animation:svelte-ll28r2-copiedPop 1.4s var(--ease-out) both}.test-name.svelte-ll28r2{flex:1;font-size:.875rem;color:var(--text);min-width:0}.outline-badge.svelte-ll28r2{display:inline-block;font-size:.62rem;font-weight:500;letter-spacing:.06em;text-transform:uppercase;background:var(--warn-soft);color:var(--warn);padding:.1rem .4rem;border-radius:var(--radius-pill);margin-left:.4rem;vertical-align:middle}.steps-toggle.svelte-ll28r2{display:inline-flex;align-items:center;gap:.2rem;font-family:var(--font-body);font-size:.72rem;font-weight:400;color:var(--text-muted);background:transparent;border:1px solid var(--border);border-radius:var(--radius-sm);padding:.2rem .5rem;cursor:pointer;flex-shrink:0;transition:background var(--duration-fast),color var(--duration-fast)}.steps-toggle.svelte-ll28r2:hover{background:var(--bg-subtle);color:var(--text)}.steps-toggle.svelte-ll28r2 svg:where(.svelte-ll28r2){transition:transform var(--duration-fast) var(--ease-out)}.steps-toggle.svelte-ll28r2 svg.rotated:where(.svelte-ll28r2){transform:rotate(90deg)}.steps.svelte-ll28r2{padding:.75rem 1.25rem .875rem 3.75rem;background:var(--bg-subtle);border-top:1px solid var(--border)}.step-list.svelte-ll28r2{list-style:none;display:flex;flex-direction:column;gap:.3rem}.step.svelte-ll28r2{font-size:.8125rem;color:var(--text-muted);font-family:JetBrains Mono,monospace;line-height:1.5;padding-left:1rem;position:relative}.step.svelte-ll28r2:before{content:"›";position:absolute;left:0;color:var(--accent);font-family:var(--font-body)}.examples.svelte-ll28r2{margin-top:.75rem;padding-top:.75rem;border-top:1px dashed var(--border)}.examples-label.svelte-ll28r2{display:block;font-size:.68rem;font-weight:500;letter-spacing:.07em;text-transform:uppercase;color:var(--text-muted);margin-bottom:.5rem}.examples-table-wrap.svelte-ll28r2{overflow-x:auto}.examples-table.svelte-ll28r2{border-collapse:collapse;font-size:.78rem;font-family:JetBrains Mono,monospace;width:auto}.examples-table.svelte-ll28r2 th:where(.svelte-ll28r2){text-align:left;padding:.3rem .75rem;font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);border-bottom:1px solid var(--border);font-family:var(--font-body);font-weight:500}.examples-table.svelte-ll28r2 td:where(.svelte-ll28r2){padding:.3rem .75rem;color:var(--text-muted);border-bottom:1px solid var(--border)}.examples-table.svelte-ll28r2 tbody:where(.svelte-ll28r2) tr:where(.svelte-ll28r2):last-child td:where(.svelte-ll28r2){border-bottom:none}@keyframes svelte-ll28r2-copiedPop{0%{opacity:0;transform:translate(-50%,4px) scale(.96)}12%,78%{opacity:1;transform:translate(-50%) scale(1)}to{opacity:0;transform:translate(-50%,-2px) scale(.98)}}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* This file is part of Plum.
|
|
3
|
-
* Licensed under the MIT License. See LICENSE file in the project root for details.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
.page-header.svelte-ll28r2{margin-bottom:1.75rem;padding-bottom:1.5rem;border-bottom:1px solid var(--border)}.header-top.svelte-ll28r2{display:flex;align-items:flex-end;justify-content:space-between;gap:1rem;margin-bottom:1.25rem}h1.svelte-ll28r2{font-size:2.5rem;margin-bottom:.25rem}.subtitle.svelte-ll28r2{color:var(--text-muted);font-size:.875rem}.search-row.svelte-ll28r2{display:flex;gap:.75rem}.search-wrap.svelte-ll28r2{position:relative;flex:1;max-width:480px}.search-icon.svelte-ll28r2{position:absolute;left:.75rem;top:50%;transform:translateY(-50%);color:var(--text-muted);pointer-events:none}.search-input.svelte-ll28r2{width:100%;padding:.55rem .875rem .55rem 2.25rem;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-subtle);color:var(--text);font-family:var(--font-body);font-size:.875rem;font-weight:300;outline:none;transition:border-color var(--duration-fast),box-shadow var(--duration-fast)}.search-input.svelte-ll28r2:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);background:var(--bg-elevated)}.sort-bar.svelte-ll28r2{display:flex;align-items:center;gap:.375rem;margin:1.25rem 0;flex-wrap:wrap}.sort-label.svelte-ll28r2{font-size:.75rem;color:var(--text-muted);margin-right:.25rem}.sort-chip.svelte-ll28r2{display:flex;align-items:center;gap:.25rem;height:28px;padding:0 .625rem;font-family:var(--font-body);font-size:.8125rem;color:var(--text-muted);background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius-pill);cursor:pointer;transition:color var(--duration-fast),border-color var(--duration-fast),background var(--duration-fast)}.sort-chip.svelte-ll28r2:hover{color:var(--text);border-color:var(--accent)}.sort-chip.active.svelte-ll28r2{color:var(--accent);border-color:var(--accent);background:var(--accent-soft)}.sort-dir.svelte-ll28r2{font-size:.7rem;opacity:.8}.search-input.svelte-ll28r2::placeholder{color:var(--text-muted)}.search-clear.svelte-ll28r2{position:absolute;right:.625rem;top:50%;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;border:none;background:var(--border);color:var(--text-muted);cursor:pointer;transition:background var(--duration-fast),color var(--duration-fast)}.search-clear.svelte-ll28r2:hover{background:var(--text-muted);color:var(--bg)}.suites.svelte-ll28r2{display:flex;flex-direction:column;gap:1rem}.suite.svelte-ll28r2{border:1px solid var(--border);border-radius:var(--radius-lg);overflow:visible;background:var(--bg-elevated);animation:fadeUp .35s var(--ease-out) both}.suite-header.svelte-ll28r2{display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.875rem 1.25rem;background:var(--bg-subtle);border-bottom:1px solid var(--border);border-radius:var(--radius-lg) var(--radius-lg) 0 0}.suite-toggle.svelte-ll28r2{display:flex;align-items:center;justify-content:center;width:22px;height:22px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;flex-shrink:0;border-radius:var(--radius-sm);transition:background var(--duration-fast),color var(--duration-fast)}.suite-toggle.svelte-ll28r2:hover{background:var(--bg-elevated);color:var(--text)}.suite-toggle.svelte-ll28r2 svg:where(.svelte-ll28r2){transition:transform var(--duration-fast) var(--ease-out)}.suite-toggle.svelte-ll28r2 svg.rotated:where(.svelte-ll28r2){transform:rotate(90deg)}.suite-meta.svelte-ll28r2{display:flex;align-items:center;gap:.625rem;flex:1;min-width:0}.suite-badges.svelte-ll28r2{display:flex;gap:.25rem;flex-shrink:0}.suite-name.svelte-ll28r2{font-size:.9rem;font-weight:400;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.suite-count.svelte-ll28r2{font-size:.75rem;color:var(--text-muted);flex-shrink:0}.run-btn.svelte-ll28r2{display:inline-flex;align-items:center;gap:.35rem;font-family:var(--font-body);font-size:.75rem;font-weight:400;padding:.3rem .75rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-elevated);color:var(--text-muted);cursor:pointer;white-space:nowrap;flex-shrink:0;transition:background var(--duration-fast),color var(--duration-fast),border-color var(--duration-fast)}.run-btn.svelte-ll28r2:hover{background:var(--accent-soft);color:var(--accent);border-color:var(--accent)}.tests.svelte-ll28r2{display:flex;flex-direction:column}.test-row.svelte-ll28r2{border-bottom:1px solid var(--border);animation:fadeUp .3s var(--ease-out) both}.test-row.svelte-ll28r2:last-child{border-bottom:none}.test-main.svelte-ll28r2{display:flex;align-items:center;gap:.625rem;padding:.75rem 1.25rem}.run-icon-btn.svelte-ll28r2{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;border:1px solid var(--border);background:transparent;color:var(--text-muted);cursor:pointer;flex-shrink:0;transition:background var(--duration-fast),color var(--duration-fast),border-color var(--duration-fast)}.run-icon-btn.svelte-ll28r2:hover{background:var(--accent);color:var(--white);border-color:var(--accent)}.test-ids.svelte-ll28r2{display:flex;gap:.25rem;flex-shrink:0}.id-pill.svelte-ll28r2{position:relative;font-size:.68rem;font-weight:500;letter-spacing:.04em;color:var(--accent);background:var(--accent-soft);border:none;border-radius:var(--radius-pill);padding:.15rem .55rem;cursor:pointer;font-family:JetBrains Mono,monospace;transition:background var(--duration-fast),color var(--duration-fast),filter var(--duration-fast)}.id-pill.svelte-ll28r2:hover{filter:brightness(.92)}.id-pill.copied.svelte-ll28r2{color:var(--pass);background:var(--pass-soft)}.id-pill.copied.svelte-ll28r2:after{content:"Copied";position:absolute;left:50%;bottom:calc(100% + 6px);z-index:50;transform:translate(-50%);padding:.16rem .42rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-elevated);color:var(--pass);box-shadow:0 4px 14px #00000014;font-family:var(--font-body);font-size:.65rem;font-weight:500;letter-spacing:0;pointer-events:none;animation:svelte-ll28r2-copiedPop 1.4s var(--ease-out) both}.test-name.svelte-ll28r2{flex:1;font-size:.875rem;color:var(--text);min-width:0}.outline-badge.svelte-ll28r2{display:inline-block;font-size:.62rem;font-weight:500;letter-spacing:.06em;text-transform:uppercase;background:var(--warn-soft);color:var(--warn);padding:.1rem .4rem;border-radius:var(--radius-pill);margin-left:.4rem;vertical-align:middle}.steps-toggle.svelte-ll28r2{display:inline-flex;align-items:center;gap:.2rem;font-family:var(--font-body);font-size:.72rem;font-weight:400;color:var(--text-muted);background:transparent;border:1px solid var(--border);border-radius:var(--radius-sm);padding:.2rem .5rem;cursor:pointer;flex-shrink:0;transition:background var(--duration-fast),color var(--duration-fast)}.steps-toggle.svelte-ll28r2:hover{background:var(--bg-subtle);color:var(--text)}.steps-toggle.svelte-ll28r2 svg:where(.svelte-ll28r2){transition:transform var(--duration-fast) var(--ease-out)}.steps-toggle.svelte-ll28r2 svg.rotated:where(.svelte-ll28r2){transform:rotate(90deg)}.steps.svelte-ll28r2{padding:.75rem 1.25rem .875rem 3.75rem;background:var(--bg-subtle);border-top:1px solid var(--border)}.step-list.svelte-ll28r2{list-style:none;display:flex;flex-direction:column;gap:.3rem}.step.svelte-ll28r2{font-size:.8125rem;color:var(--text-muted);font-family:JetBrains Mono,monospace;line-height:1.5;padding-left:1rem;position:relative}.step.svelte-ll28r2:before{content:"›";position:absolute;left:0;color:var(--accent);font-family:var(--font-body)}.examples.svelte-ll28r2{margin-top:.75rem;padding-top:.75rem;border-top:1px dashed var(--border)}.examples-label.svelte-ll28r2{display:block;font-size:.68rem;font-weight:500;letter-spacing:.07em;text-transform:uppercase;color:var(--text-muted);margin-bottom:.5rem}.examples-table-wrap.svelte-ll28r2{overflow-x:auto}.examples-table.svelte-ll28r2{border-collapse:collapse;font-size:.78rem;font-family:JetBrains Mono,monospace;width:auto}.examples-table.svelte-ll28r2 th:where(.svelte-ll28r2){text-align:left;padding:.3rem .75rem;font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);border-bottom:1px solid var(--border);font-family:var(--font-body);font-weight:500}.examples-table.svelte-ll28r2 td:where(.svelte-ll28r2){padding:.3rem .75rem;color:var(--text-muted);border-bottom:1px solid var(--border)}.examples-table.svelte-ll28r2 tbody:where(.svelte-ll28r2) tr:where(.svelte-ll28r2):last-child td:where(.svelte-ll28r2){border-bottom:none}@keyframes svelte-ll28r2-copiedPop{0%{opacity:0;transform:translate(-50%,4px) scale(.96)}12%,78%{opacity:1;transform:translate(-50%) scale(1)}to{opacity:0;transform:translate(-50%,-2px) scale(.98)}}
|