code-coordinator 0.5.46__py3-none-any.whl
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.
- code_coordinator-0.5.46.dist-info/METADATA +625 -0
- code_coordinator-0.5.46.dist-info/RECORD +295 -0
- code_coordinator-0.5.46.dist-info/WHEEL +5 -0
- code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
- code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
- code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
- coord/__init__.py +176 -0
- coord/_board_mapping.py +229 -0
- coord/acceptance.py +468 -0
- coord/acceptance_drivers.py +632 -0
- coord/agent.py +7517 -0
- coord/agent_app.py +1555 -0
- coord/agent_update.py +417 -0
- coord/agents/opencode/.gitignore +13 -0
- coord/agents/opencode/agents/work.md +129 -0
- coord/agents/opencode/routing.jsonc +49 -0
- coord/audit.py +301 -0
- coord/auto_loop.py +1440 -0
- coord/board_bool_guard.py +72 -0
- coord/board_service.py +141 -0
- coord/board_wire.py +309 -0
- coord/brain.py +581 -0
- coord/branch_model.py +214 -0
- coord/cargo_cache.py +258 -0
- coord/ci_github.py +386 -0
- coord/ci_store.py +560 -0
- coord/claim.py +353 -0
- coord/cli.py +454 -0
- coord/client.py +610 -0
- coord/commands/__init__.py +1 -0
- coord/commands/_common.py +329 -0
- coord/commands/acceptance.py +916 -0
- coord/commands/agent_ops.py +1339 -0
- coord/commands/audit.py +131 -0
- coord/commands/chat.py +320 -0
- coord/commands/dispatch.py +1780 -0
- coord/commands/dispatch_workers.py +4894 -0
- coord/commands/drive.py +616 -0
- coord/commands/drive_queue.py +1203 -0
- coord/commands/gate_a.py +217 -0
- coord/commands/gates.py +89 -0
- coord/commands/issues.py +681 -0
- coord/commands/lifecycle.py +513 -0
- coord/commands/merge.py +1900 -0
- coord/commands/milestone.py +2081 -0
- coord/commands/plan_followup.py +1243 -0
- coord/commands/plans.py +156 -0
- coord/commands/release.py +2232 -0
- coord/commands/report.py +341 -0
- coord/commands/review.py +1523 -0
- coord/commands/scorecard.py +252 -0
- coord/commands/sessions.py +1930 -0
- coord/commands/setup.py +576 -0
- coord/commands/status.py +2089 -0
- coord/commands/terminal.py +385 -0
- coord/commands/test_gate.py +775 -0
- coord/commands/tui.py +288 -0
- coord/comments.py +718 -0
- coord/config.py +3032 -0
- coord/conflict_fix.py +633 -0
- coord/dao.py +483 -0
- coord/dashboard/__init__.py +0 -0
- coord/dashboard/fixture.py +376 -0
- coord/dashboard/index.html +658 -0
- coord/dashboard/server.py +1894 -0
- coord/dashboard/terminal.py +382 -0
- coord/dashboard/webapp/.gitignore +9 -0
- coord/dashboard/webapp/components.json +17 -0
- coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
- coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
- coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
- coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
- coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
- coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
- coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
- coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
- coord/dashboard/webapp/dist/icons/icon.svg +5 -0
- coord/dashboard/webapp/dist/index.html +38 -0
- coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
- coord/dashboard/webapp/dist/sw.js +1 -0
- coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
- coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
- coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
- coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
- coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
- coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
- coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
- coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
- coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
- coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
- coord/dashboard/webapp/eslint.config.js +20 -0
- coord/dashboard/webapp/index.html +37 -0
- coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
- coord/dashboard/webapp/package-lock.json +10584 -0
- coord/dashboard/webapp/package.json +63 -0
- coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
- coord/dashboard/webapp/playwright.config.ts +93 -0
- coord/dashboard/webapp/postcss.config.js +6 -0
- coord/dashboard/webapp/public/icons/icon-192.png +0 -0
- coord/dashboard/webapp/public/icons/icon-512.png +0 -0
- coord/dashboard/webapp/public/icons/icon.svg +5 -0
- coord/dashboard/webapp/src/App.tsx +140 -0
- coord/dashboard/webapp/src/api/client.ts +199 -0
- coord/dashboard/webapp/src/api/generated.ts +176 -0
- coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
- coord/dashboard/webapp/src/components/Detail.tsx +800 -0
- coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
- coord/dashboard/webapp/src/components/Home.tsx +435 -0
- coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
- coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
- coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
- coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
- coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
- coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
- coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
- coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
- coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
- coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
- coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
- coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
- coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
- coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
- coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
- coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
- coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
- coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
- coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
- coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
- coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
- coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
- coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
- coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
- coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
- coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
- coord/dashboard/webapp/src/index.css +210 -0
- coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
- coord/dashboard/webapp/src/lib/utils.ts +6 -0
- coord/dashboard/webapp/src/main.tsx +46 -0
- coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
- coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
- coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
- coord/dashboard/webapp/src/realtime/connection.ts +227 -0
- coord/dashboard/webapp/src/realtime/events.ts +100 -0
- coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
- coord/dashboard/webapp/src/routes/paths.ts +92 -0
- coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
- coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
- coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
- coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
- coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
- coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
- coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
- coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
- coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
- coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
- coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
- coord/dashboard/webapp/src/shell/railItems.ts +105 -0
- coord/dashboard/webapp/src/shell/shellState.ts +174 -0
- coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
- coord/dashboard/webapp/src/test-setup.ts +41 -0
- coord/dashboard/webapp/src/vite-env.d.ts +2 -0
- coord/dashboard/webapp/tailwind.config.js +140 -0
- coord/dashboard/webapp/tsconfig.json +25 -0
- coord/dashboard/webapp/tsconfig.node.json +11 -0
- coord/dashboard/webapp/vite.config.ts +71 -0
- coord/db.py +1076 -0
- coord/dead_end.py +332 -0
- coord/deploy/README.md +33 -0
- coord/deploy/coord-agent.service +89 -0
- coord/deploy/coord-db-backup.service +60 -0
- coord/deploy/coord-db-backup.sh +74 -0
- coord/deploy/coord-db-backup.timer +18 -0
- coord/deploy/coord-drive-queue.service +117 -0
- coord/deploy/coord-drive-queue.timer +39 -0
- coord/deploy/coord-notify.service +48 -0
- coord/deploy/coord-notify.timer +24 -0
- coord/deploy/coord-release-propagate.service +83 -0
- coord/deploy/coord-release-propagate.timer +38 -0
- coord/deploy/coord-release-window.service +119 -0
- coord/deploy/coord-release-window.timer +36 -0
- coord/deploy/coord-serve.service +82 -0
- coord/deploy/coord-web-dist-build.service +43 -0
- coord/deploy/coord-web-dist-build.timer +36 -0
- coord/deploy/coord-web.service +125 -0
- coord/deploy_manifest.py +80 -0
- coord/deploy_units.py +384 -0
- coord/deps.py +115 -0
- coord/diagnose.py +1623 -0
- coord/dispatch.py +1009 -0
- coord/dist_name.py +123 -0
- coord/drive.py +3101 -0
- coord/drive_queue.py +2298 -0
- coord/drive_state.py +870 -0
- coord/events.py +381 -0
- coord/failure_class.py +914 -0
- coord/filelock.py +168 -0
- coord/fleet_config_health.py +300 -0
- coord/freshness.py +206 -0
- coord/gate_a.py +469 -0
- coord/gate_b.py +411 -0
- coord/gate_snapshot.py +385 -0
- coord/gates.py +582 -0
- coord/github_ops.py +1954 -0
- coord/goal.py +125 -0
- coord/graph_health.py +348 -0
- coord/health/__init__.py +69 -0
- coord/health/aggregate.py +129 -0
- coord/health/checks/__init__.py +13 -0
- coord/health/checks/agent_install.py +280 -0
- coord/health/checks/cargo_targets.py +171 -0
- coord/health/checks/claude_binary.py +65 -0
- coord/health/checks/deploy_lane_facts.py +458 -0
- coord/health/checks/disk.py +99 -0
- coord/health/checks/fleet_board.py +89 -0
- coord/health/checks/fleet_deploy_lanes.py +469 -0
- coord/health/checks/fleet_phantom.py +69 -0
- coord/health/checks/fleet_unit_drift.py +151 -0
- coord/health/checks/graph.py +192 -0
- coord/health/checks/plan_usage.py +88 -0
- coord/health/checks/repo_state.py +161 -0
- coord/health/checks/spawned_coord.py +465 -0
- coord/health/checks/timer_active.py +254 -0
- coord/health/checks/toolchain.py +547 -0
- coord/health/checks/unit_drift.py +648 -0
- coord/health/checks/unit_enablement.py +171 -0
- coord/health/checks/worktrees.py +96 -0
- coord/health/cli.py +121 -0
- coord/health/context.py +106 -0
- coord/health/fleet_snapshot.py +477 -0
- coord/health/models.py +250 -0
- coord/health/pypi.py +231 -0
- coord/health/registry.py +240 -0
- coord/health/render.py +82 -0
- coord/health/units.py +60 -0
- coord/hooks.py +106 -0
- coord/housekeeping.py +204 -0
- coord/interactive.py +4286 -0
- coord/issue_store.py +1496 -0
- coord/liveness_auditor.py +293 -0
- coord/machine_pause.py +755 -0
- coord/merge_queue.py +4681 -0
- coord/milestone_chat.py +600 -0
- coord/milestone_dispatch.py +943 -0
- coord/milestone_gate.py +709 -0
- coord/milestone_order.py +840 -0
- coord/mock_author.py +334 -0
- coord/models.py +891 -0
- coord/network.py +269 -0
- coord/new_issue_chat.py +229 -0
- coord/notify.py +3226 -0
- coord/openapi.py +404 -0
- coord/overlap_fence.py +133 -0
- coord/parentage.py +200 -0
- coord/parentage_github.py +58 -0
- coord/pipeline.py +481 -0
- coord/plan_parser.py +266 -0
- coord/plans.py +543 -0
- coord/platform_paths.py +43 -0
- coord/pr_body_lint.py +67 -0
- coord/prereqs.py +533 -0
- coord/progress.py +425 -0
- coord/providers/__init__.py +683 -0
- coord/providers/base.py +218 -0
- coord/providers/claude.py +284 -0
- coord/providers/claude_pty.py +610 -0
- coord/providers/opencode.py +896 -0
- coord/reconcile.py +2233 -0
- coord/refine_chat.py +485 -0
- coord/release_cordon.py +525 -0
- coord/release_propagate.py +1176 -0
- coord/release_verify.py +777 -0
- coord/release_window.py +322 -0
- coord/reports.py +1643 -0
- coord/revalidate.py +1101 -0
- coord/review.py +3317 -0
- coord/scorecard.py +484 -0
- coord/serve_app.py +7192 -0
- coord/skills/update-issue/SKILL.md +93 -0
- coord/smoke.py +1030 -0
- coord/split_work.py +210 -0
- coord/stage_projection.py +650 -0
- coord/state.py +5720 -0
- coord/test_author.py +1064 -0
- coord/test_chat.py +352 -0
- coord/test_orchestrator.py +494 -0
- coord/test_report.py +178 -0
- coord/tui_release.py +271 -0
- coord/usage.py +753 -0
- coord/usage_limits.py +358 -0
- coord/usage_rollup.py +709 -0
- coord/worker_events.py +954 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme coverage for the acceptance slice (#1551, M-W1's exit gate).
|
|
3
|
+
*
|
|
4
|
+
* Nothing in the repo exercised dark/light before this file: `#1546` shipped
|
|
5
|
+
* the tokens, the provider and the toggle, but no E2E spec ever clicked it.
|
|
6
|
+
* "Both themes, including the toggle and its persistence" is one of #1551's
|
|
7
|
+
* named acceptance criteria precisely because a chassis milestone can ship
|
|
8
|
+
* looking done while a whole theme (or a whole breakpoint's access to the
|
|
9
|
+
* toggle) is quietly unreachable -- which is exactly what this file found:
|
|
10
|
+
* `ActivityRail`'s "Theme" button is `{!narrow && ...}` (wide/medium only,
|
|
11
|
+
* `ActivityRail.tsx`), and nothing stood in for it at narrow width. Fixed
|
|
12
|
+
* alongside this spec in `PanelHeader.tsx` -- a `md:hidden` `<ThemeToggle/>`
|
|
13
|
+
* next to the panel title, matching `docs/mocks/web/pipeline-narrow.html`'s
|
|
14
|
+
* per-view topbar (`id="theme"`), which already puts it there for exactly
|
|
15
|
+
* this reason (the bottom nav has no room for it -- see that file's own
|
|
16
|
+
* comment). Both controls share the same accessible name
|
|
17
|
+
* (`Switch to dark/light theme`), so `themeToggle()` below finds whichever
|
|
18
|
+
* one is actually in the accessibility tree at the current viewport --
|
|
19
|
+
* `md:hidden` is a real `display:none`, which a11y-tree role queries already
|
|
20
|
+
* exclude, so there is never a strict-mode ambiguity between the two.
|
|
21
|
+
*
|
|
22
|
+
* Runs at both breakpoints as distinct Playwright projects ('wide' /
|
|
23
|
+
* 'narrow', see playwright.config.ts) rather than a single project's
|
|
24
|
+
* `test.use({ viewport })` override, so a theme regression pinned to one
|
|
25
|
+
* breakpoint shows up as its own failing project/line rather than being
|
|
26
|
+
* folded into a single generic run.
|
|
27
|
+
*
|
|
28
|
+
* Run: npm run test:e2e
|
|
29
|
+
*/
|
|
30
|
+
import { test, expect, type Page } from '@playwright/test'
|
|
31
|
+
|
|
32
|
+
const SEEDED_PIPELINE = [
|
|
33
|
+
{
|
|
34
|
+
assignment_id: 'work-1',
|
|
35
|
+
issue_number: 42,
|
|
36
|
+
issue_title: 'Fix the dashboard rendering',
|
|
37
|
+
repo_name: 'api',
|
|
38
|
+
machine_name: 'laptop',
|
|
39
|
+
current_stage: 'coding',
|
|
40
|
+
stages: [
|
|
41
|
+
{ name: 'coding', status: 'active', is_current: true },
|
|
42
|
+
{ name: 'review', status: 'waiting', is_current: false },
|
|
43
|
+
{ name: 'merge', status: 'waiting', is_current: false },
|
|
44
|
+
],
|
|
45
|
+
available_gates: [],
|
|
46
|
+
progress_pct: 20,
|
|
47
|
+
review_findings_pending: false,
|
|
48
|
+
review_verdict: null,
|
|
49
|
+
review_findings_body: null,
|
|
50
|
+
test_verdict: null,
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
async function mockApi(page: Page): Promise<void> {
|
|
55
|
+
await page.route('**/api/pipeline', (route) =>
|
|
56
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(SEEDED_PIPELINE) }),
|
|
57
|
+
)
|
|
58
|
+
await page.route('**/api/sessions', (route) =>
|
|
59
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: '[]' }),
|
|
60
|
+
)
|
|
61
|
+
await page.route('**/api/board', (route) =>
|
|
62
|
+
route.fulfill({
|
|
63
|
+
status: 200,
|
|
64
|
+
contentType: 'application/json',
|
|
65
|
+
body: JSON.stringify({ round_number: 1, active: [], completed: [] }),
|
|
66
|
+
}),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Whichever theme toggle is actually in the accessibility tree at this
|
|
71
|
+
* viewport -- the rail's (wide/medium) or PanelHeader's (narrow). Exactly
|
|
72
|
+
* one exists at a time; see this file's header comment. */
|
|
73
|
+
function themeToggle(page: Page) {
|
|
74
|
+
return page.getByRole('button', { name: /Switch to (dark|light) theme/ })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function htmlTheme(page: Page): Promise<string | null> {
|
|
78
|
+
return page.evaluate(() => document.documentElement.getAttribute('data-theme'))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function storedTheme(page: Page): Promise<string | null> {
|
|
82
|
+
return page.evaluate(() => window.localStorage.getItem('coord-web-theme'))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Pin the emulated OS preference so "no stored theme yet" resolves
|
|
86
|
+
// deterministically to dark (the documented default, docs/mocks/web/README.md)
|
|
87
|
+
// rather than depending on Playwright's own default colour scheme.
|
|
88
|
+
test.use({ colorScheme: 'dark' })
|
|
89
|
+
|
|
90
|
+
test.describe('theme (#1551)', () => {
|
|
91
|
+
test('defaults to dark, and the toggle flips data-theme on <html> to light and back', async ({ page }) => {
|
|
92
|
+
await mockApi(page)
|
|
93
|
+
await page.goto('/')
|
|
94
|
+
|
|
95
|
+
await expect.poll(() => htmlTheme(page)).toBe('dark')
|
|
96
|
+
await expect(themeToggle(page)).toHaveAccessibleName('Switch to light theme')
|
|
97
|
+
|
|
98
|
+
await themeToggle(page).click()
|
|
99
|
+
await expect.poll(() => htmlTheme(page)).toBe('light')
|
|
100
|
+
await expect(themeToggle(page)).toHaveAccessibleName('Switch to dark theme')
|
|
101
|
+
|
|
102
|
+
await themeToggle(page).click()
|
|
103
|
+
await expect.poll(() => htmlTheme(page)).toBe('dark')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('switching theme visibly changes the rendered surface, not just the attribute', async ({ page }) => {
|
|
107
|
+
await mockApi(page)
|
|
108
|
+
await page.goto('/')
|
|
109
|
+
|
|
110
|
+
const surface = page.locator('body')
|
|
111
|
+
const darkBg = await surface.evaluate((el) => getComputedStyle(el).backgroundColor)
|
|
112
|
+
|
|
113
|
+
await themeToggle(page).click()
|
|
114
|
+
await expect.poll(() => htmlTheme(page)).toBe('light')
|
|
115
|
+
const lightBg = await surface.evaluate((el) => getComputedStyle(el).backgroundColor)
|
|
116
|
+
|
|
117
|
+
expect(lightBg).not.toEqual(darkBg)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('the choice persists to localStorage and survives a reload with no flash back to dark', async ({
|
|
121
|
+
page,
|
|
122
|
+
}) => {
|
|
123
|
+
await mockApi(page)
|
|
124
|
+
await page.goto('/')
|
|
125
|
+
|
|
126
|
+
await themeToggle(page).click()
|
|
127
|
+
await expect.poll(() => htmlTheme(page)).toBe('light')
|
|
128
|
+
await expect.poll(() => storedTheme(page)).toBe('light')
|
|
129
|
+
|
|
130
|
+
await page.reload()
|
|
131
|
+
|
|
132
|
+
// Asserted immediately after load, not just eventually -- index.html's
|
|
133
|
+
// blocking inline script is what's supposed to prevent a dark flash
|
|
134
|
+
// before React hydrates; a poll alone wouldn't catch a one-frame flash.
|
|
135
|
+
await expect.poll(() => htmlTheme(page), { timeout: 2_000 }).toBe('light')
|
|
136
|
+
await expect(themeToggle(page)).toHaveAccessibleName('Switch to dark theme')
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import tsPlugin from '@typescript-eslint/eslint-plugin'
|
|
2
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
3
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
{ ignores: ['dist'] },
|
|
7
|
+
...tsPlugin.configs['flat/recommended'],
|
|
8
|
+
{
|
|
9
|
+
files: ['**/*.{ts,tsx}'],
|
|
10
|
+
plugins: {
|
|
11
|
+
'react-hooks': reactHooks,
|
|
12
|
+
'react-refresh': reactRefresh,
|
|
13
|
+
},
|
|
14
|
+
rules: {
|
|
15
|
+
'react-hooks/rules-of-hooks': 'error',
|
|
16
|
+
'react-hooks/exhaustive-deps': 'warn',
|
|
17
|
+
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/icons/icon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
|
7
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
8
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
9
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
10
|
+
<meta name="theme-color" content="#101418" />
|
|
11
|
+
<title>coord dashboard</title>
|
|
12
|
+
<script>
|
|
13
|
+
// Blocking, pre-paint theme resolution (#1546) — must mirror
|
|
14
|
+
// src/components/ui/theme-provider.tsx's `initialTheme()` exactly, or
|
|
15
|
+
// the first React render disagrees with what's already on screen and
|
|
16
|
+
// flashes. Inlined (not a src="") so nothing external delays it.
|
|
17
|
+
(function () {
|
|
18
|
+
try {
|
|
19
|
+
var stored = localStorage.getItem('coord-web-theme');
|
|
20
|
+
var theme =
|
|
21
|
+
stored === 'light' || stored === 'dark'
|
|
22
|
+
? stored
|
|
23
|
+
: window.matchMedia('(prefers-color-scheme: light)').matches
|
|
24
|
+
? 'light'
|
|
25
|
+
: 'dark';
|
|
26
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
31
|
+
</script>
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<div id="root"></div>
|
|
35
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# ISC License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2018-2025, Andrea Giammarchi, @WebReflection
|
|
4
|
+
#
|
|
5
|
+
# Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
# purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
# copyright notice and this permission notice appear in all copies.
|
|
8
|
+
#
|
|
9
|
+
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
|
14
|
+
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
# PERFORMANCE OF THIS SOFTWARE.
|
|
16
|
+
|
|
17
|
+
import json as _json
|
|
18
|
+
|
|
19
|
+
class _Known:
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.key = []
|
|
22
|
+
self.value = []
|
|
23
|
+
|
|
24
|
+
class _String:
|
|
25
|
+
def __init__(self, value):
|
|
26
|
+
self.value = value
|
|
27
|
+
|
|
28
|
+
def _array_keys(value):
|
|
29
|
+
for i in range(len(value)):
|
|
30
|
+
yield i
|
|
31
|
+
|
|
32
|
+
def _object_keys(value):
|
|
33
|
+
for key in value:
|
|
34
|
+
yield key
|
|
35
|
+
|
|
36
|
+
def _is_array(value):
|
|
37
|
+
return isinstance(value, (list, tuple))
|
|
38
|
+
|
|
39
|
+
def _is_object(value):
|
|
40
|
+
return isinstance(value, dict)
|
|
41
|
+
|
|
42
|
+
def _is_string(value):
|
|
43
|
+
return isinstance(value, str)
|
|
44
|
+
|
|
45
|
+
def _index(known, input, value):
|
|
46
|
+
input.append(value)
|
|
47
|
+
index = str(len(input) - 1)
|
|
48
|
+
known.key.append(value)
|
|
49
|
+
known.value.append(index)
|
|
50
|
+
return index
|
|
51
|
+
|
|
52
|
+
def _relate(known, input, value):
|
|
53
|
+
if _is_string(value) or _is_array(value) or _is_object(value):
|
|
54
|
+
try:
|
|
55
|
+
return known.value[known.key.index(value)]
|
|
56
|
+
except:
|
|
57
|
+
return _index(known, input, value)
|
|
58
|
+
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
def _resolver(input, lazy, parsed):
|
|
62
|
+
def resolver(output):
|
|
63
|
+
keys = _array_keys(output) if _is_array(output) else _object_keys(output) if _is_object(output) else []
|
|
64
|
+
for key in keys:
|
|
65
|
+
value = output[key]
|
|
66
|
+
if isinstance(value, _String):
|
|
67
|
+
tmp = input[int(value.value)]
|
|
68
|
+
output[key] = tmp
|
|
69
|
+
if (_is_array(tmp) or _is_object(tmp)) and tmp not in parsed:
|
|
70
|
+
parsed.append(tmp)
|
|
71
|
+
lazy.append([output, key])
|
|
72
|
+
|
|
73
|
+
return output
|
|
74
|
+
|
|
75
|
+
return resolver
|
|
76
|
+
|
|
77
|
+
def _transform(known, input, value):
|
|
78
|
+
if _is_array(value):
|
|
79
|
+
output = []
|
|
80
|
+
for val in value:
|
|
81
|
+
output.append(_relate(known, input, val))
|
|
82
|
+
return output
|
|
83
|
+
|
|
84
|
+
if _is_object(value):
|
|
85
|
+
obj = {}
|
|
86
|
+
for key in value:
|
|
87
|
+
obj[key] = _relate(known, input, value[key])
|
|
88
|
+
return obj
|
|
89
|
+
|
|
90
|
+
return value
|
|
91
|
+
|
|
92
|
+
def _wrap(value):
|
|
93
|
+
if _is_string(value):
|
|
94
|
+
return _String(value)
|
|
95
|
+
|
|
96
|
+
if _is_array(value):
|
|
97
|
+
i = 0
|
|
98
|
+
for val in value:
|
|
99
|
+
value[i] = _wrap(val)
|
|
100
|
+
i += 1
|
|
101
|
+
|
|
102
|
+
elif _is_object(value):
|
|
103
|
+
for key in value:
|
|
104
|
+
value[key] = _wrap(value[key])
|
|
105
|
+
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
def parse(value, *args, **kwargs):
|
|
109
|
+
json = _json.loads(value, *args, **kwargs)
|
|
110
|
+
wrapped = []
|
|
111
|
+
for value in json:
|
|
112
|
+
wrapped.append(_wrap(value))
|
|
113
|
+
|
|
114
|
+
input = []
|
|
115
|
+
for value in wrapped:
|
|
116
|
+
if isinstance(value, _String):
|
|
117
|
+
input.append(value.value)
|
|
118
|
+
else:
|
|
119
|
+
input.append(value)
|
|
120
|
+
|
|
121
|
+
value = input[0]
|
|
122
|
+
lazy = []
|
|
123
|
+
revive = _resolver(input, lazy, [value])
|
|
124
|
+
|
|
125
|
+
value = revive(value)
|
|
126
|
+
|
|
127
|
+
i = 0
|
|
128
|
+
while i < len(lazy):
|
|
129
|
+
o, k = lazy[i]
|
|
130
|
+
i += 1
|
|
131
|
+
o[k] = revive(o[k])
|
|
132
|
+
|
|
133
|
+
return value
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def stringify(value, *args, **kwargs):
|
|
137
|
+
known = _Known()
|
|
138
|
+
input = []
|
|
139
|
+
output = []
|
|
140
|
+
i = int(_index(known, input, value))
|
|
141
|
+
while i < len(input):
|
|
142
|
+
output.append(_transform(known, input, input[i]))
|
|
143
|
+
i += 1
|
|
144
|
+
return _json.dumps(output, *args, **kwargs)
|