@checkstack/ui 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/package.json +20 -15
  3. package/src/components/Accordion.tsx +17 -9
  4. package/src/components/ActionCard.tsx +4 -4
  5. package/src/components/BrandIcon.tsx +57 -0
  6. package/src/components/CodeEditor/CodeEditor.tsx +71 -7
  7. package/src/components/CodeEditor/TypefoxEditor.tsx +266 -53
  8. package/src/components/CodeEditor/editorTheme.test.ts +41 -0
  9. package/src/components/CodeEditor/editorTheme.ts +26 -0
  10. package/src/components/CodeEditor/index.ts +3 -1
  11. package/src/components/CodeEditor/monacoGuard.ts +76 -0
  12. package/src/components/CodeEditor/monacoTsService.ts +5 -37
  13. package/src/components/CodeEditor/scriptContext.test.ts +15 -7
  14. package/src/components/CodeEditor/scriptContext.ts +12 -18
  15. package/src/components/CodeEditor/types.ts +20 -0
  16. package/src/components/CodeEditor/validateScripts.ts +53 -13
  17. package/src/components/CodeEditor/vscodeServicesSignal.ts +72 -0
  18. package/src/components/ConfirmationModal.tsx +7 -1
  19. package/src/components/DynamicForm/DynamicForm.tsx +101 -53
  20. package/src/components/DynamicForm/DynamicOptionsField.tsx +19 -14
  21. package/src/components/DynamicForm/FormField.tsx +84 -24
  22. package/src/components/DynamicForm/MultiTypeEditorField.tsx +11 -0
  23. package/src/components/DynamicForm/index.ts +14 -0
  24. package/src/components/DynamicForm/types.ts +63 -1
  25. package/src/components/DynamicForm/utils.test.ts +38 -0
  26. package/src/components/DynamicForm/utils.ts +22 -0
  27. package/src/components/DynamicForm/validation.logic.test.ts +255 -0
  28. package/src/components/DynamicForm/validation.logic.ts +210 -0
  29. package/src/components/DynamicIcon.tsx +39 -17
  30. package/src/components/Markdown.tsx +68 -2
  31. package/src/components/Spinner.tsx +56 -0
  32. package/src/components/StatusBadge.tsx +78 -0
  33. package/src/components/StrategyConfigCard.tsx +3 -3
  34. package/src/components/Tabs.tsx +7 -1
  35. package/src/components/UserMenu.logic.test.ts +37 -0
  36. package/src/components/UserMenu.logic.ts +30 -0
  37. package/src/components/UserMenu.tsx +40 -12
  38. package/src/components/iconRegistry.tsx +27 -0
  39. package/src/index.ts +3 -0
  40. package/stories/Introduction.mdx +1 -1
  41. package/stories/Markdown.stories.tsx +56 -0
  42. package/stories/Spinner.stories.tsx +90 -0
  43. package/tsconfig.json +3 -0
@@ -0,0 +1,76 @@
1
+ /// <reference types="vite/client" />
2
+ // Guarded accessor for the monaco-vscode editor API. ALL runtime monaco access
3
+ // in this package goes through here; direct value imports of
4
+ // `@codingame/monaco-vscode-editor-api` elsewhere are banned by lint
5
+ // (`no-restricted-imports`) so the guard cannot be bypassed.
6
+ //
7
+ // Why: `monaco.editor.*` / `monaco.languages.*` functions resolve services via
8
+ // `StandaloneServices.get()`, which AUTO-INITIALIZES the monaco-vscode services
9
+ // on first use (CodinGame `standaloneServices.js` — `get()` calls `initialize`
10
+ // when not yet initialized). `@typefox/monaco-editor-react` tracks its OWN,
11
+ // independent init flags; if any `monaco.*` call runs BEFORE the wrapper's init,
12
+ // it trips CodinGame's `servicesInitialized` flag and the wrapper's later
13
+ // `initialize()` throws "Services are already initialized" — the editor then
14
+ // never renders. This is dev-only (production's single mount masked it once),
15
+ // and it is exactly the bug this guard prevents from regressing.
16
+ //
17
+ // In dev, calling any `monaco.editor.*` / `monaco.languages.*` FUNCTION before
18
+ // `areVscodeServicesReady()` throws immediately, at the exact call site, with a
19
+ // clear message. In production the raw API is returned (zero overhead).
20
+
21
+ import * as monacoApi from "@codingame/monaco-vscode-editor-api";
22
+ import { areVscodeServicesReady } from "./vscodeServicesSignal";
23
+
24
+ // The namespaces whose functions auto-initialize the services on first use.
25
+ const GUARDED_NAMESPACES = new Set(["editor", "languages"]);
26
+
27
+ // Wrap a namespace so calling any of its functions before services are ready
28
+ // throws. Non-function members (enums like `MarkerSeverity`, classes) pass
29
+ // through untouched. Returns the same type `T` (the Proxy is invisible to types,
30
+ // so call sites still type-check against the real monaco signatures).
31
+ const guardNamespace = <T extends object>(ns: T, nsName: string): T =>
32
+ new Proxy(ns, {
33
+ get(target, prop, receiver): unknown {
34
+ const value: unknown = Reflect.get(target, prop, receiver);
35
+ if (typeof value !== "function" || typeof prop !== "string") {
36
+ return value;
37
+ }
38
+ const fn = value;
39
+ return (...args: unknown[]): unknown => {
40
+ if (!areVscodeServicesReady()) {
41
+ throw new Error(
42
+ `monaco.${nsName}.${prop}() was called before the monaco-vscode ` +
43
+ `services were initialized. That call auto-initializes the ` +
44
+ `services and breaks the editor's @typefox init ("Services are ` +
45
+ `already initialized"), so the editor never renders. Gate it ` +
46
+ `behind \`apiReady\` (the editor's onEditorStartDone) or ` +
47
+ `\`areVscodeServicesReady()\`. See ` +
48
+ `core/ui/src/components/CodeEditor/monacoGuard.ts.`,
49
+ );
50
+ }
51
+ return Reflect.apply(fn, ns, args);
52
+ };
53
+ },
54
+ });
55
+
56
+ const guardedMonaco: typeof monacoApi = import.meta.env.DEV
57
+ ? new Proxy(monacoApi, {
58
+ get(target, prop, receiver): unknown {
59
+ const value: unknown = Reflect.get(target, prop, receiver);
60
+ if (
61
+ typeof prop === "string" &&
62
+ GUARDED_NAMESPACES.has(prop) &&
63
+ typeof value === "object" &&
64
+ value !== null
65
+ ) {
66
+ return guardNamespace(value, prop);
67
+ }
68
+ return value;
69
+ },
70
+ })
71
+ : monacoApi;
72
+
73
+ // The guarded runtime value. Consumers import this for runtime monaco access
74
+ // and import the TYPE namespace separately, type-only, from
75
+ // `@codingame/monaco-vscode-editor-api` (which the lint rule allows).
76
+ export { guardedMonaco as monaco };
@@ -65,43 +65,11 @@ const tsWorkerLoader: WorkerLoader = () =>
65
65
  const jsonWorkerLoader: WorkerLoader = () =>
66
66
  new Worker(jsonWorkerUrl, { type: "module" });
67
67
 
68
- // The monaco-vscode API initializes globally exactly ONCE, and that init is
69
- // owned by the editor wrapper (`MonacoEditorReactComp`) - it throws "Services
70
- // are already initialized" if anything else inits first. So the headless
71
- // validator must NOT touch the worker / models until an editor has brought the
72
- // services up. The editor flips this flag from its `onEditorStartDone`; the
73
- // validator checks it and otherwise no-ops. Net effect: scripts validate once
74
- // any script editor has been opened this session (covering collapsed cards
75
- // from then on); a never-opened, all-collapsed automation is left to the
76
- // deferred backend typecheck.
77
- let vscodeServicesReady = false;
78
- const servicesReadyListeners = new Set<() => void>();
79
-
80
- /** Called by the editor once the monaco-vscode services have initialized. */
81
- export const markVscodeServicesReady = (): void => {
82
- if (vscodeServicesReady) return;
83
- vscodeServicesReady = true;
84
- for (const listener of servicesReadyListeners) listener();
85
- servicesReadyListeners.clear();
86
- };
87
-
88
- /** True once an editor has initialized the monaco-vscode services. */
89
- export const areVscodeServicesReady = (): boolean => vscodeServicesReady;
90
-
91
- /**
92
- * Subscribe to the one-time "services ready" transition. Fires immediately if
93
- * already ready. Returns an unsubscribe. Lets the headless validator re-run the
94
- * moment the first editor brings the services up (otherwise a never-edited
95
- * definition would not re-validate just because a card was opened).
96
- */
97
- export const onVscodeServicesReady = (listener: () => void): (() => void) => {
98
- if (vscodeServicesReady) {
99
- listener();
100
- return () => {};
101
- }
102
- servicesReadyListeners.add(listener);
103
- return () => servicesReadyListeners.delete(listener);
104
- };
68
+ // The "monaco-vscode services ready" signal (`markVscodeServicesReady` /
69
+ // `areVscodeServicesReady` / `onVscodeServicesReady`) lives in the Monaco-free
70
+ // `vscodeServicesSignal` module so the barrel and the automation editor can
71
+ // observe readiness without importing this Monaco-heavy module. See that file
72
+ // for the full rationale.
105
73
 
106
74
  let workerFactoryRegistered = false;
107
75
 
@@ -7,13 +7,17 @@ import {
7
7
  import type { JsonSchemaProperty } from "../DynamicForm/types";
8
8
 
9
9
  describe("healthcheckScriptContext", () => {
10
- it("emits both the global and module-export declarations of defineHealthCheck", () => {
10
+ it("emits the global helper + context types, but NOT a bare-name module block", () => {
11
11
  const ctx = healthcheckScriptContext({});
12
- // Module declaration is what `import { defineHealthCheck } from
13
- // "@checkstack/healthcheck"` resolves to.
14
- expect(ctx.typeDefinitions).toContain('declare module "@checkstack/healthcheck"');
12
+ // The bare-name `@checkstack/healthcheck` module block is REMOVED (plan
13
+ // §6.2 / §6.4): the subpath module `@checkstack/sdk/healthcheck` is now
14
+ // resolved from the injected @checkstack/sdk editor bundle, not from
15
+ // scriptContext. scriptContext must NOT declare ANY package module block.
16
+ expect(ctx.typeDefinitions).not.toContain('declare module "@checkstack/healthcheck"');
17
+ expect(ctx.typeDefinitions).not.toContain('declare module "@checkstack/sdk/healthcheck"');
18
+ expect(ctx.typeDefinitions).not.toContain("declare module");
15
19
  expect(ctx.typeDefinitions).toContain("HealthCheckScriptResult");
16
- // Global declaration is what makes Monaco autocomplete `defineHea…`
20
+ // The global declaration stays it makes Monaco autocomplete `defineHea…`
17
21
  // _without_ requiring the user to type the import first (Monaco 0.55
18
22
  // doesn't expose `includeCompletionsForModuleExports`).
19
23
  expect(ctx.typeDefinitions).toMatch(/declare function defineHealthCheck/);
@@ -143,9 +147,13 @@ describe("healthcheckScriptContext", () => {
143
147
  });
144
148
 
145
149
  describe("integrationScriptContext", () => {
146
- it("emits both the global and module-export declarations of defineIntegration", () => {
150
+ it("emits the global helper + context types, but NOT a bare-name module block", () => {
147
151
  const ctx = integrationScriptContext({});
148
- expect(ctx.typeDefinitions).toContain('declare module "@checkstack/integration"');
152
+ // Bare-name `@checkstack/integration` block removed (§6.2/§6.4); the
153
+ // subpath module resolves from the injected @checkstack/sdk editor bundle.
154
+ expect(ctx.typeDefinitions).not.toContain('declare module "@checkstack/integration"');
155
+ expect(ctx.typeDefinitions).not.toContain('declare module "@checkstack/sdk/integration"');
156
+ expect(ctx.typeDefinitions).not.toContain("declare module");
149
157
  expect(ctx.typeDefinitions).toContain("IntegrationScriptResult");
150
158
  // Global form — analogous to defineHealthCheck.
151
159
  expect(ctx.typeDefinitions).toMatch(/declare function defineIntegration/);
@@ -8,8 +8,9 @@ import { jsonSchemaToTypeScript } from "./generateTypeDefinitions";
8
8
  *
9
9
  * - `healthcheckInlineContext` — inline TS/JS health checks.
10
10
  * `context.config` is typed from the collector's own config schema.
11
- * A virtual `@checkstack/healthcheck` module exposes the required
12
- * return shape so users get a type _error_ when the shape is wrong.
11
+ * The `@checkstack/sdk/healthcheck` module (resolved from the injected
12
+ * @checkstack/sdk editor bundle) exposes the required return shape so
13
+ * users get a type _error_ when the shape is wrong.
13
14
  *
14
15
  * - `healthcheckShellContext` — shell health checks. No platform-injected
15
16
  * env vars today (the user supplies `env` themselves), so we surface
@@ -18,7 +19,8 @@ import { jsonSchemaToTypeScript } from "./generateTypeDefinitions";
18
19
  *
19
20
  * - `integrationInlineContext` — TS/JS integration scripts.
20
21
  * `context.event.payload` is typed from the event's payload schema.
21
- * A virtual `@checkstack/integration` module exposes the result shape.
22
+ * The `@checkstack/sdk/integration` module (resolved from the injected
23
+ * @checkstack/sdk editor bundle) exposes the result shape.
22
24
  *
23
25
  * - `integrationShellContext` — shell integration scripts. The platform
24
26
  * injects `EVENT_ID`, `DELIVERY_ID`, `SUBSCRIPTION_ID`,
@@ -119,9 +121,10 @@ interface HealthCheckScriptContext {
119
121
  * to be so mistakes are caught before the script ever runs.
120
122
  *
121
123
  * Available both as a global (this declaration) and as a named export
122
- * from \`@checkstack/healthcheck\` (below). The global form means the
123
- * editor can autocomplete it without the user typing the import first;
124
- * the module form is for explicit, IDE-style imports.
124
+ * from \`@checkstack/sdk/healthcheck\`. The global form means the editor
125
+ * can autocomplete it without the user typing the import first; the
126
+ * named-export form (resolved from the injected @checkstack/sdk editor
127
+ * bundle, not declared here) is for explicit, IDE-style imports.
125
128
  */
126
129
  declare function defineHealthCheck<
127
130
  T extends
@@ -132,11 +135,6 @@ declare function defineHealthCheck<
132
135
  >(value: T): T;
133
136
 
134
137
  declare const context: HealthCheckScriptContext;
135
-
136
- declare module "@checkstack/healthcheck" {
137
- export type { HealthCheckScriptResult, HealthCheckScriptContext };
138
- export { defineHealthCheck };
139
- }
140
138
  `;
141
139
  }
142
140
 
@@ -187,7 +185,8 @@ interface IntegrationScriptContext {
187
185
  * Helper that asserts the return shape of an integration script at the
188
186
  * type level. See \`defineHealthCheck\` for the analogous health-check
189
187
  * helper. Available both as a global and as a named export from
190
- * \`@checkstack/integration\`.
188
+ * \`@checkstack/sdk/integration\` (resolved from the injected
189
+ * @checkstack/sdk editor bundle, not declared here).
191
190
  */
192
191
  declare function defineIntegration<
193
192
  T extends
@@ -200,11 +199,6 @@ declare function defineIntegration<
200
199
  >(value: T): T;
201
200
 
202
201
  declare const context: IntegrationScriptContext;
203
-
204
- declare module "@checkstack/integration" {
205
- export type { IntegrationScriptResult, IntegrationScriptContext };
206
- export { defineIntegration };
207
- }
208
202
  `;
209
203
  }
210
204
 
@@ -221,7 +215,7 @@ declare module "@checkstack/integration" {
221
215
  * `defineHealthCheck` is available without an import (declared as an
222
216
  * ambient global in the editor's type defs and injected onto
223
217
  * `globalThis` by the runner). The named-import form
224
- * `import { defineHealthCheck } from "@checkstack/healthcheck"` also
218
+ * `import { defineHealthCheck } from "@checkstack/sdk/healthcheck"` also
225
219
  * works if users prefer it.
226
220
  */
227
221
  const HEALTHCHECK_INLINE_TS_STARTER = `import { loadavg } from "node:os";
@@ -144,6 +144,18 @@ export interface CodeEditorProps {
144
144
  * refresh against the new install.
145
145
  */
146
146
  acquireResetKey?: string;
147
+ /**
148
+ * The running release's `@checkstack/sdk` editor bundle as virtual `.d.ts`
149
+ * files (TS/JS editors). Makes `import { defineHealthCheck } from
150
+ * "@checkstack/sdk/healthcheck"` resolve with real, version-matched types.
151
+ * Fetched live by the consumer so `@checkstack/ui` stays network-agnostic.
152
+ */
153
+ sdkTypes?: ReadonlyArray<AcquiredTypeFile>;
154
+ /**
155
+ * Release-version reset key for `sdkTypes`. When it changes, the mounted SDK
156
+ * libs reset so the editor never serves stale SDK types after an upgrade.
157
+ */
158
+ sdkTypesResetKey?: string;
147
159
  /**
148
160
  * Importable installed package NAMES (TS/JS editors). When provided, the
149
161
  * editor suggests these while the cursor is inside an import specifier
@@ -165,4 +177,12 @@ export interface CodeEditorProps {
165
177
  * keeping `@checkstack/ui` plugin-agnostic.
166
178
  */
167
179
  title?: string;
180
+ /**
181
+ * When `true`, this editor never CLAIMS the one-time monaco-vscode cold init -
182
+ * it waits for another (visible) editor to bring the services up, then mounts.
183
+ * Set this for OFFSCREEN/hidden editors (e.g. the automation
184
+ * `ScriptServicesBooter`): a hidden editor's init may never complete, so it
185
+ * must not be the sole initializer. Defaults to `false`.
186
+ */
187
+ deferInit?: boolean;
168
188
  }
@@ -15,15 +15,16 @@
15
15
  // global to the shared service). Prepending the type defs onto each validated
16
16
  // source keeps `context` scoped to that one off-screen file. See
17
17
  // `buildValidationSource`.
18
- import * as monaco from "@codingame/monaco-vscode-editor-api";
19
- import {
20
- getJavaScriptWorker,
21
- getTypeScriptWorker,
22
- } from "@codingame/monaco-vscode-standalone-typescript-language-features";
23
- import {
24
- areVscodeServicesReady,
25
- ensureStandaloneStdlib,
26
- } from "./monacoTsService";
18
+ //
19
+ // The Monaco editor API, the standalone TS worker accessors, and the shared
20
+ // `monacoTsService` setup are imported LAZILY (in-body `await import(...)`)
21
+ // rather than at module scope. This keeps the entire `@codingame/*` stack off
22
+ // the `@checkstack/ui` barrel: `validateTypeScriptSources` is re-exported from
23
+ // the barrel, so a static Monaco import here would ship Monaco to every page
24
+ // that touches the barrel (e.g. the login page). The lazy imports only resolve
25
+ // once an editor has already brought the services up, so they hit an
26
+ // already-loaded chunk and add no extra cost.
27
+ import { areVscodeServicesReady } from "./vscodeServicesSignal";
27
28
  import {
28
29
  buildValidationSource,
29
30
  mapWorkerDiagnostics,
@@ -31,6 +32,10 @@ import {
31
32
  type ScriptDiagnostic,
32
33
  } from "./scriptDiagnostics";
33
34
 
35
+ type MonacoEditorApi = typeof import("@codingame/monaco-vscode-editor-api");
36
+ type TsLanguageFeatures =
37
+ typeof import("@codingame/monaco-vscode-standalone-typescript-language-features");
38
+
34
39
  export type { ScriptDiagnostic } from "./scriptDiagnostics";
35
40
 
36
41
  export interface ScriptValidationInput {
@@ -72,7 +77,26 @@ export async function validateTypeScriptSources({
72
77
  if (!areVscodeServicesReady()) {
73
78
  return results;
74
79
  }
80
+
81
+ // Lazy-load the Monaco stack only now that an editor has brought the services
82
+ // up (keeps these heavy `@codingame/*` modules off the barrel - see the
83
+ // module header). Because services are ready, these chunks are already
84
+ // resolved, so the imports are effectively free here.
85
+ let monaco: MonacoEditorApi;
86
+ let getJavaScriptWorker: TsLanguageFeatures["getJavaScriptWorker"];
87
+ let getTypeScriptWorker: TsLanguageFeatures["getTypeScriptWorker"];
75
88
  try {
89
+ const [editorApi, tsLanguageFeatures, { ensureStandaloneStdlib }] =
90
+ await Promise.all([
91
+ import("@codingame/monaco-vscode-editor-api"),
92
+ import(
93
+ "@codingame/monaco-vscode-standalone-typescript-language-features"
94
+ ),
95
+ import("./monacoTsService"),
96
+ ]);
97
+ monaco = editorApi;
98
+ getJavaScriptWorker = tsLanguageFeatures.getJavaScriptWorker;
99
+ getTypeScriptWorker = tsLanguageFeatures.getTypeScriptWorker;
76
100
  await ensureStandaloneStdlib();
77
101
  } catch {
78
102
  return results;
@@ -80,7 +104,15 @@ export async function validateTypeScriptSources({
80
104
 
81
105
  for (const input of sources) {
82
106
  try {
83
- results.set(input.id, await validateOne(input));
107
+ results.set(
108
+ input.id,
109
+ await validateOne({
110
+ input,
111
+ monaco,
112
+ getJavaScriptWorker,
113
+ getTypeScriptWorker,
114
+ }),
115
+ );
84
116
  } catch {
85
117
  results.set(input.id, []);
86
118
  }
@@ -88,9 +120,17 @@ export async function validateTypeScriptSources({
88
120
  return results;
89
121
  }
90
122
 
91
- async function validateOne(
92
- input: ScriptValidationInput,
93
- ): Promise<ScriptDiagnostic[]> {
123
+ async function validateOne({
124
+ input,
125
+ monaco,
126
+ getJavaScriptWorker,
127
+ getTypeScriptWorker,
128
+ }: {
129
+ input: ScriptValidationInput;
130
+ monaco: MonacoEditorApi;
131
+ getJavaScriptWorker: TsLanguageFeatures["getJavaScriptWorker"];
132
+ getTypeScriptWorker: TsLanguageFeatures["getTypeScriptWorker"];
133
+ }): Promise<ScriptDiagnostic[]> {
94
134
  const { text, prependedLineCount } = buildValidationSource({
95
135
  typeDefinitions: input.typeDefinitions,
96
136
  source: input.source,
@@ -0,0 +1,72 @@
1
+ // Lightweight, Monaco-free signal for the one-time "monaco-vscode services
2
+ // ready" transition.
3
+ //
4
+ // Extracted from `monacoTsService` so consumers that only need to OBSERVE
5
+ // readiness (the `@checkstack/ui` barrel re-export, the automation editor's
6
+ // `ScriptServicesBooter` + headless validator) do NOT transitively pull the
7
+ // entire `@codingame/*` Monaco stack into their bundle. Importing this module
8
+ // loads zero Monaco code, so pages that never mount an editor (e.g. the login
9
+ // page) stay Monaco-free.
10
+ //
11
+ // Why a global flag at all: the monaco-vscode API initializes globally exactly
12
+ // ONCE, and that init is owned by the editor wrapper (`MonacoEditorReactComp`),
13
+ // which throws "Services are already initialized" if anything else inits first.
14
+ // So the headless validator must NOT touch the worker / models until an editor
15
+ // has brought the services up. The editor flips this flag from its
16
+ // `onEditorStartDone` (via `markVscodeServicesReady`); the validator checks
17
+ // `areVscodeServicesReady()` and otherwise no-ops. Net effect: scripts validate
18
+ // once any script editor has been opened this session (covering collapsed cards
19
+ // from then on); a never-opened, all-collapsed automation is left to the
20
+ // deferred backend typecheck.
21
+
22
+ let vscodeServicesReady = false;
23
+ const servicesReadyListeners = new Set<() => void>();
24
+
25
+ /** Called by the editor once the monaco-vscode services have initialized. */
26
+ export const markVscodeServicesReady = (): void => {
27
+ if (vscodeServicesReady) return;
28
+ vscodeServicesReady = true;
29
+ for (const listener of servicesReadyListeners) listener();
30
+ servicesReadyListeners.clear();
31
+ };
32
+
33
+ /** True once an editor has initialized the monaco-vscode services. */
34
+ export const areVscodeServicesReady = (): boolean => vscodeServicesReady;
35
+
36
+ /**
37
+ * Subscribe to the one-time "services ready" transition. Fires immediately if
38
+ * already ready. Returns an unsubscribe. Lets the headless validator re-run the
39
+ * moment the first editor brings the services up (otherwise a never-edited
40
+ * definition would not re-validate just because a card was opened).
41
+ */
42
+ export const onVscodeServicesReady = (listener: () => void): (() => void) => {
43
+ if (vscodeServicesReady) {
44
+ listener();
45
+ return () => {};
46
+ }
47
+ servicesReadyListeners.add(listener);
48
+ return () => servicesReadyListeners.delete(listener);
49
+ };
50
+
51
+ // ─── Cold-init serialization ─────────────────────────────────────────────────
52
+ // monaco-vscode services initialize globally exactly once. @typefox's React
53
+ // wrapper performs that init when given a `vscodeApiConfig`, and it is
54
+ // StrictMode-safe for a SINGLE editor - but two editors mounting at once both
55
+ // race the init and corrupt it. So exactly ONE editor claims the cold init and
56
+ // mounts (with `vscodeApiConfig`); every other editor waits for
57
+ // `areVscodeServicesReady()` before mounting (it then attaches to the
58
+ // already-initialized services). The claim is released if the claiming editor
59
+ // unmounts before services come up, so a sibling can take over.
60
+ let coldInitClaimed = false;
61
+
62
+ /** First caller (while not ready and unclaimed) gets the cold-init role. */
63
+ export const claimColdInit = (): boolean => {
64
+ if (vscodeServicesReady || coldInitClaimed) return false;
65
+ coldInitClaimed = true;
66
+ return true;
67
+ };
68
+
69
+ /** Release the claim if services never came up (claimer unmounted early). */
70
+ export const releaseColdInit = (): void => {
71
+ if (!vscodeServicesReady) coldInitClaimed = false;
72
+ };
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { cn } from "../utils";
3
3
  import { Button } from "./Button";
4
+ import { usePerformance } from "./PerformanceProvider";
4
5
  import { AlertTriangle, X } from "lucide-react";
5
6
 
6
7
  export interface ConfirmationModalProps {
@@ -26,6 +27,8 @@ export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
26
27
  variant = "danger",
27
28
  isLoading = false,
28
29
  }) => {
30
+ const { isLowPower } = usePerformance();
31
+
29
32
  if (!isOpen) return;
30
33
 
31
34
  const handleConfirm = () => {
@@ -65,7 +68,10 @@ export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
65
68
  onClick={handleBackdropClick}
66
69
  >
67
70
  <div
68
- className="bg-background rounded-lg shadow-xl max-w-md w-full mx-4 my-4 max-h-[calc(100dvh-2rem)] overflow-y-auto animate-in fade-in zoom-in duration-200 pointer-events-auto"
71
+ className={cn(
72
+ "bg-background rounded-lg shadow-xl max-w-md w-full mx-4 my-4 max-h-[calc(100dvh-2rem)] overflow-y-auto pointer-events-auto",
73
+ !isLowPower && "animate-in fade-in zoom-in duration-200",
74
+ )}
69
75
  role="dialog"
70
76
  aria-modal="true"
71
77
  aria-labelledby="modal-title"