@xenosystem/blocks 0.21.0 → 0.23.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.
@@ -252,6 +252,12 @@ interface XenoAuthGateState {
252
252
  message?: string;
253
253
  /** For `expired-offline`: when the licence was last confirmed, so the plate can say how long ago. */
254
254
  lastVerifiedAt?: number;
255
+ /**
256
+ * The host name the system browser opens for sign-in (`xenosystem.ai`), as the HOST knows it — the gate
257
+ * does not. Named on the sign-in plate only when it is a plain host name; absent, the plate names no domain
258
+ * rather than guess one (apps sign in at different origins while the domain moves).
259
+ */
260
+ signInHost?: string;
255
261
  /** Which intents the host implements. A missing one hides its affordance (COMPONENTS §1.7). */
256
262
  capabilities?: Partial<Record<XenoAuthGateIntentKind, boolean>>;
257
263
  }
@@ -131,6 +131,11 @@ import { useEffect, useState } from "react";
131
131
  import { AuthDoor, SignInForm } from "@xenosystem/components/auth";
132
132
 
133
133
  // src/auth/gate/behaviour.ts
134
+ var HOST = /^(?=.{3,253}$)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/i;
135
+ function signInNote(signInHost, productName) {
136
+ const where = signInHost && HOST.test(signInHost) ? `at ${signInHost.toLowerCase()}` : "to sign in";
137
+ return `Your browser opens ${where}, where you can continue with Google, GitHub or email. ${productName} never sees your password.`;
138
+ }
134
139
  function authGateView(state, productName, now = Date.now()) {
135
140
  const phase = deriveAuthGatePhase(state);
136
141
  const copy = plateCopy(phase, productName, state.message, formatSince(state.lastVerifiedAt, now));
@@ -159,7 +164,7 @@ function authGateView(state, productName, now = Date.now()) {
159
164
  tag: copy.tag,
160
165
  title: copy.title,
161
166
  body: copy.body,
162
- note: phase === "sign-in" && !state.pending ? `Your browser opens at xenostudio.ai, where you can continue with Google, GitHub or email. ${productName} never sees your password.` : void 0,
167
+ note: phase === "sign-in" && !state.pending ? signInNote(state.signInHost, productName) : void 0,
163
168
  warning: copy.note,
164
169
  identity: state.identity && phase !== "sign-in" ? state.identity.email ?? state.identity.name ?? state.identity.sub : void 0,
165
170
  actions
@@ -0,0 +1,435 @@
1
+ // src/edit/curve/scale.ts
2
+ var DEFAULT_CURVE_ZOOM = { min: 5e-4, max: 64 };
3
+ var XenoCurveScale = class _XenoCurveScale {
4
+ pxPerTick;
5
+ scrollTick;
6
+ minValue;
7
+ maxValue;
8
+ widthPx;
9
+ heightPx;
10
+ gutterPx;
11
+ rulerPx;
12
+ constructor(state, geometry) {
13
+ this.widthPx = Math.max(1, finiteOr(geometry.widthPx, 1));
14
+ this.heightPx = Math.max(1, finiteOr(geometry.heightPx, 1));
15
+ this.gutterPx = Math.max(0, Math.min(finiteOr(geometry.gutterPx, 0), this.widthPx - 1));
16
+ this.rulerPx = Math.max(0, Math.min(finiteOr(geometry.rulerPx, 0), this.heightPx - 1));
17
+ this.pxPerTick = Math.max(Number.MIN_VALUE, finiteOr(state.pxPerTick, 1));
18
+ this.scrollTick = finiteOr(state.scrollTick, 0);
19
+ const min = finiteOr(state.minValue, 0);
20
+ const max = finiteOr(state.maxValue, 1);
21
+ if (max - min < 1e-12) {
22
+ const centre = (max + min) / 2;
23
+ this.minValue = centre - 0.5;
24
+ this.maxValue = centre + 0.5;
25
+ } else {
26
+ this.minValue = min;
27
+ this.maxValue = max;
28
+ }
29
+ }
30
+ /** Width of the plot area. Always ≥ 1. */
31
+ get plotPx() {
32
+ return Math.max(1, this.widthPx - this.gutterPx);
33
+ }
34
+ /** Height of the plot area. Always ≥ 1. */
35
+ get plotHeightPx() {
36
+ return Math.max(1, this.heightPx - this.rulerPx);
37
+ }
38
+ /** Ticks visible. */
39
+ get spanTicks() {
40
+ return this.plotPx / this.pxPerTick;
41
+ }
42
+ /** Tick at the right edge. */
43
+ get endTick() {
44
+ return this.scrollTick + this.spanTicks;
45
+ }
46
+ /** Value units per pixel. */
47
+ get valuePerPx() {
48
+ return (this.maxValue - this.minValue) / this.plotHeightPx;
49
+ }
50
+ /** Container-relative x → tick. */
51
+ xToTick(x) {
52
+ return this.scrollTick + (finiteOr(x, 0) - this.gutterPx) / this.pxPerTick;
53
+ }
54
+ /** Tick → container-relative x. */
55
+ tickToX(tick) {
56
+ return this.gutterPx + (finiteOr(tick, 0) - this.scrollTick) * this.pxPerTick;
57
+ }
58
+ /**
59
+ * Container-relative y → value.
60
+ *
61
+ * **Inverted.** Screen y grows downward, value grows upward. Every sign error in a graph editor
62
+ * lives on this one line.
63
+ */
64
+ yToValue(y) {
65
+ const local = finiteOr(y, 0) - this.rulerPx;
66
+ return this.maxValue - local / this.plotHeightPx * (this.maxValue - this.minValue);
67
+ }
68
+ /** Value → container-relative y. */
69
+ valueToY(value) {
70
+ const fraction = (this.maxValue - finiteOr(value, 0)) / (this.maxValue - this.minValue);
71
+ return this.rulerPx + fraction * this.plotHeightPx;
72
+ }
73
+ /**
74
+ * A tick DURATION → a pixel width.
75
+ *
76
+ * Separate from {@link tickToX}, because rounding is not associative: mixing
77
+ * `durationToPx(b - a)` with `tickToX(b) - tickToX(a)` puts a curve's handle a pixel off its key
78
+ * at some zoom levels and not others.
79
+ */
80
+ durationToPx(ticks) {
81
+ return finiteOr(ticks, 0) * this.pxPerTick;
82
+ }
83
+ /** A value DELTA → a pixel height. Sign-preserving and NOT inverted. */
84
+ valueDeltaToPx(delta) {
85
+ return finiteOr(delta, 0) / (this.maxValue - this.minValue) * this.plotHeightPx;
86
+ }
87
+ /** A pixel distance → a tick distance. For screen-space thresholds. */
88
+ pxToTicks(px) {
89
+ return finiteOr(px, 0) / this.pxPerTick;
90
+ }
91
+ /** A pixel distance → a value distance. For screen-space thresholds. */
92
+ pxToValue(px) {
93
+ return finiteOr(px, 0) * this.valuePerPx;
94
+ }
95
+ /** Is this point inside the plot area? */
96
+ isInPlot(x, y) {
97
+ return x >= this.gutterPx && x <= this.widthPx && y >= this.rulerPx && y <= this.heightPx;
98
+ }
99
+ /** A copy with new state. */
100
+ with(state) {
101
+ return new _XenoCurveScale(
102
+ {
103
+ pxPerTick: state.pxPerTick ?? this.pxPerTick,
104
+ scrollTick: state.scrollTick ?? this.scrollTick,
105
+ minValue: state.minValue ?? this.minValue,
106
+ maxValue: state.maxValue ?? this.maxValue
107
+ },
108
+ {
109
+ widthPx: this.widthPx,
110
+ heightPx: this.heightPx,
111
+ gutterPx: this.gutterPx,
112
+ rulerPx: this.rulerPx
113
+ }
114
+ );
115
+ }
116
+ /** A copy with new geometry. */
117
+ resized(geometry) {
118
+ return new _XenoCurveScale(
119
+ {
120
+ pxPerTick: this.pxPerTick,
121
+ scrollTick: this.scrollTick,
122
+ minValue: this.minValue,
123
+ maxValue: this.maxValue
124
+ },
125
+ {
126
+ widthPx: geometry.widthPx ?? this.widthPx,
127
+ heightPx: geometry.heightPx ?? this.heightPx,
128
+ gutterPx: geometry.gutterPx ?? this.gutterPx,
129
+ rulerPx: geometry.rulerPx ?? this.rulerPx
130
+ }
131
+ );
132
+ }
133
+ };
134
+ function finiteOr(value, fallback) {
135
+ return Number.isFinite(value) ? value : fallback;
136
+ }
137
+ function zoomTimeAboutCursor(scale, x, factor, bounds = DEFAULT_CURVE_ZOOM) {
138
+ const anchorTick = scale.xToTick(x);
139
+ const requested = scale.pxPerTick * (Number.isFinite(factor) && factor > 0 ? factor : 1);
140
+ const pxPerTick = Math.min(bounds.max, Math.max(bounds.min, requested));
141
+ return {
142
+ pxPerTick,
143
+ scrollTick: anchorTick - (x - scale.gutterPx) / pxPerTick,
144
+ minValue: scale.minValue,
145
+ maxValue: scale.maxValue
146
+ };
147
+ }
148
+ function zoomValueAboutCursor(scale, y, factor) {
149
+ const anchorValue = scale.yToValue(y);
150
+ const safeFactor = Number.isFinite(factor) && factor > 0 ? factor : 1;
151
+ const span = (scale.maxValue - scale.minValue) / safeFactor;
152
+ const fraction = (scale.maxValue - anchorValue) / (scale.maxValue - scale.minValue);
153
+ const maxValue = anchorValue + span * fraction;
154
+ return {
155
+ pxPerTick: scale.pxPerTick,
156
+ scrollTick: scale.scrollTick,
157
+ minValue: maxValue - span,
158
+ maxValue
159
+ };
160
+ }
161
+ function wheelZoomFactor(deltaY, sensitivity = 0.01) {
162
+ const raw = Number.isFinite(deltaY) ? deltaY : 0;
163
+ const clamped = Math.max(-10, Math.min(10, raw));
164
+ return Math.pow(2, -clamped * sensitivity * 10);
165
+ }
166
+ function fitValueRange(extent, marginFraction = 0.1) {
167
+ if (!extent || !Number.isFinite(extent.min) || !Number.isFinite(extent.max)) {
168
+ return { minValue: 0, maxValue: 1 };
169
+ }
170
+ const span = extent.max - extent.min;
171
+ const margin = Math.max(span * marginFraction, 0.5);
172
+ return { minValue: extent.min - margin, maxValue: extent.max + margin };
173
+ }
174
+
175
+ // src/edit/nodegraph/scale.ts
176
+ var DEFAULT_GRAPH_ZOOM = { min: 0.05, max: 4 };
177
+ var XenoGraphScale = class _XenoGraphScale {
178
+ zoom;
179
+ x;
180
+ y;
181
+ widthPx;
182
+ heightPx;
183
+ constructor(state, geometry) {
184
+ this.widthPx = Math.max(1, finiteOr2(geometry.widthPx, 1));
185
+ this.heightPx = Math.max(1, finiteOr2(geometry.heightPx, 1));
186
+ this.zoom = Math.max(Number.MIN_VALUE, finiteOr2(state.zoom, 1));
187
+ this.x = finiteOr2(state.x, 0);
188
+ this.y = finiteOr2(state.y, 0);
189
+ }
190
+ /** Graph-space width of the viewport. */
191
+ get graphWidth() {
192
+ return this.widthPx / this.zoom;
193
+ }
194
+ /** Graph-space height of the viewport. */
195
+ get graphHeight() {
196
+ return this.heightPx / this.zoom;
197
+ }
198
+ /** The visible rectangle in graph space. */
199
+ get viewport() {
200
+ return { x: this.x, y: this.y, width: this.graphWidth, height: this.graphHeight };
201
+ }
202
+ /** Container-relative screen point → graph point. */
203
+ screenToGraph(px, py) {
204
+ return { x: this.x + finiteOr2(px, 0) / this.zoom, y: this.y + finiteOr2(py, 0) / this.zoom };
205
+ }
206
+ /** Graph point → container-relative screen point. */
207
+ graphToScreen(gx, gy) {
208
+ return { x: (finiteOr2(gx, 0) - this.x) * this.zoom, y: (finiteOr2(gy, 0) - this.y) * this.zoom };
209
+ }
210
+ /** A graph-space distance → pixels. */
211
+ toPx(distance) {
212
+ return finiteOr2(distance, 0) * this.zoom;
213
+ }
214
+ /** A pixel distance → graph space. Used for screen-space hit thresholds. */
215
+ toGraph(px) {
216
+ return finiteOr2(px, 0) / this.zoom;
217
+ }
218
+ /** A copy with new state. */
219
+ with(state) {
220
+ return new _XenoGraphScale(
221
+ { zoom: state.zoom ?? this.zoom, x: state.x ?? this.x, y: state.y ?? this.y },
222
+ { widthPx: this.widthPx, heightPx: this.heightPx }
223
+ );
224
+ }
225
+ /** A copy with new geometry. */
226
+ resized(geometry) {
227
+ return new _XenoGraphScale(
228
+ { zoom: this.zoom, x: this.x, y: this.y },
229
+ { widthPx: geometry.widthPx ?? this.widthPx, heightPx: geometry.heightPx ?? this.heightPx }
230
+ );
231
+ }
232
+ };
233
+ function finiteOr2(value, fallback) {
234
+ return Number.isFinite(value) ? value : fallback;
235
+ }
236
+ function zoomAboutCursor(scale, px, py, factor, bounds = DEFAULT_GRAPH_ZOOM) {
237
+ const anchor = scale.screenToGraph(px, py);
238
+ const requested = scale.zoom * (Number.isFinite(factor) && factor > 0 ? factor : 1);
239
+ const zoom = Math.min(bounds.max, Math.max(bounds.min, requested));
240
+ return { zoom, x: anchor.x - px / zoom, y: anchor.y - py / zoom };
241
+ }
242
+ function fitToBounds(bounds, widthPx, heightPx, zoomBounds = DEFAULT_GRAPH_ZOOM, paddingPx = 40) {
243
+ if (!bounds || bounds.width <= 0 || bounds.height <= 0) return { zoom: 1, x: 0, y: 0 };
244
+ const usableW = Math.max(1, widthPx - paddingPx * 2);
245
+ const usableH = Math.max(1, heightPx - paddingPx * 2);
246
+ const zoom = Math.min(
247
+ zoomBounds.max,
248
+ Math.max(zoomBounds.min, Math.min(usableW / bounds.width, usableH / bounds.height))
249
+ );
250
+ const x = bounds.x + bounds.width / 2 - widthPx / zoom / 2;
251
+ const y = bounds.y + bounds.height / 2 - heightPx / zoom / 2;
252
+ return { zoom, x, y };
253
+ }
254
+ function graphBounds(nodes) {
255
+ if (nodes.length === 0) return null;
256
+ let minX = Number.POSITIVE_INFINITY;
257
+ let minY = Number.POSITIVE_INFINITY;
258
+ let maxX = Number.NEGATIVE_INFINITY;
259
+ let maxY = Number.NEGATIVE_INFINITY;
260
+ for (const node of nodes) {
261
+ if (!Number.isFinite(node.x) || !Number.isFinite(node.y)) continue;
262
+ minX = Math.min(minX, node.x);
263
+ minY = Math.min(minY, node.y);
264
+ maxX = Math.max(maxX, node.x + Math.max(1, node.width));
265
+ maxY = Math.max(maxY, node.y + Math.max(1, node.height));
266
+ }
267
+ if (!Number.isFinite(minX)) return null;
268
+ return { x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) };
269
+ }
270
+
271
+ // src/edit/nodegraph/types.ts
272
+ function portRefId(ref) {
273
+ return `${ref.nodeId}${ref.portId}`;
274
+ }
275
+ function edgeKey(edge) {
276
+ return `${edge.fromNodeId}${edge.fromPortId}${edge.toNodeId}${edge.toPortId}`;
277
+ }
278
+ function findPort(node, portId) {
279
+ return node.inputs.find((p) => p.id === portId) ?? node.outputs.find((p) => p.id === portId) ?? null;
280
+ }
281
+ function portHasRoom(port, existingCount) {
282
+ if (port.multiple !== void 0) return port.multiple || existingCount === 0;
283
+ return port.direction === "out" ? true : existingCount === 0;
284
+ }
285
+ function nodeBounds(node) {
286
+ return { x: node.x, y: node.y, width: Math.max(1, node.width), height: Math.max(1, node.height) };
287
+ }
288
+ function boxesIntersect(a, b) {
289
+ return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
290
+ }
291
+
292
+ // src/edit/nodegraph/spatial.ts
293
+ var XenoNodeIndex = class {
294
+ cellSize;
295
+ cells = /* @__PURE__ */ new Map();
296
+ all;
297
+ constructor(nodes) {
298
+ this.all = nodes;
299
+ this.cellSize = chooseCellSize(nodes);
300
+ for (const node of nodes) {
301
+ const x0 = Math.floor(node.x / this.cellSize);
302
+ const y0 = Math.floor(node.y / this.cellSize);
303
+ const x1 = Math.floor((node.x + Math.max(1, node.width)) / this.cellSize);
304
+ const y1 = Math.floor((node.y + Math.max(1, node.height)) / this.cellSize);
305
+ for (let cx = x0; cx <= x1; cx += 1) {
306
+ for (let cy = y0; cy <= y1; cy += 1) {
307
+ const key = `${cx},${cy}`;
308
+ const bucket = this.cells.get(key);
309
+ if (bucket) bucket.push(node);
310
+ else this.cells.set(key, [node]);
311
+ }
312
+ }
313
+ }
314
+ }
315
+ /** Bucket size, for tests. */
316
+ get bucketSize() {
317
+ return this.cellSize;
318
+ }
319
+ /**
320
+ * Nodes intersecting a box.
321
+ *
322
+ * @param box - The query rectangle in graph space.
323
+ * @returns Matching nodes, each at most once.
324
+ */
325
+ query(box) {
326
+ const x0 = Math.floor(box.x / this.cellSize);
327
+ const y0 = Math.floor(box.y / this.cellSize);
328
+ const x1 = Math.floor((box.x + box.width) / this.cellSize);
329
+ const y1 = Math.floor((box.y + box.height) / this.cellSize);
330
+ const seen = /* @__PURE__ */ new Set();
331
+ const out = [];
332
+ for (let cx = x0; cx <= x1; cx += 1) {
333
+ for (let cy = y0; cy <= y1; cy += 1) {
334
+ for (const node of this.cells.get(`${cx},${cy}`) ?? []) {
335
+ if (seen.has(node.id)) continue;
336
+ seen.add(node.id);
337
+ if (boxesIntersect(node, { ...box, width: Math.max(0, box.width), height: Math.max(0, box.height) })) {
338
+ out.push(node);
339
+ }
340
+ }
341
+ }
342
+ }
343
+ return out;
344
+ }
345
+ /** The topmost node containing a point, or `null`. */
346
+ hit(x, y) {
347
+ const candidates = this.query({ x, y, width: 1e-3, height: 1e-3 });
348
+ if (candidates.length === 0) return null;
349
+ let best = null;
350
+ for (const node of candidates) {
351
+ if (x >= node.x && x <= node.x + node.width && y >= node.y && y <= node.y + node.height) {
352
+ if (!best || this.all.indexOf(node) > this.all.indexOf(best)) best = node;
353
+ }
354
+ }
355
+ return best;
356
+ }
357
+ };
358
+ function chooseCellSize(nodes) {
359
+ if (nodes.length === 0) return 256;
360
+ const sizes = [];
361
+ for (const node of nodes) {
362
+ const size = Math.max(node.width || 1, node.height || 1);
363
+ if (Number.isFinite(size) && size > 0) sizes.push(size);
364
+ }
365
+ if (sizes.length === 0) return 256;
366
+ sizes.sort((a, b) => a - b);
367
+ const median = sizes[Math.floor(sizes.length / 2)];
368
+ return Math.max(32, Math.min(4096, median * 2));
369
+ }
370
+ function edgeBounds(from, to, slack = 64) {
371
+ const x = Math.min(from.x, to.x) - slack;
372
+ const y = Math.min(from.y, to.y) - slack;
373
+ return {
374
+ x,
375
+ y,
376
+ width: Math.abs(to.x - from.x) + slack * 2,
377
+ height: Math.abs(to.y - from.y) + slack * 2
378
+ };
379
+ }
380
+ function visibleEdges(edges, portPosition, viewport) {
381
+ const out = [];
382
+ for (const edge of edges) {
383
+ const from = portPosition(edge.fromNodeId, edge.fromPortId);
384
+ const to = portPosition(edge.toNodeId, edge.toPortId);
385
+ if (!from || !to) continue;
386
+ if (!boxesIntersect(edgeBounds(from, to), viewport)) continue;
387
+ out.push({ edge, from, to });
388
+ }
389
+ return out;
390
+ }
391
+ function edgePath(from, to) {
392
+ const dx = Math.abs(to.x - from.x);
393
+ const reach = Math.max(40, Math.min(dx * 0.6, 240));
394
+ return `M${from.x},${from.y} C${from.x + reach},${from.y} ${to.x - reach},${to.y} ${to.x},${to.y}`;
395
+ }
396
+ function distanceToEdge(point, from, to, samples = 16) {
397
+ const dx = Math.abs(to.x - from.x);
398
+ const reach = Math.max(40, Math.min(dx * 0.6, 240));
399
+ const c1 = { x: from.x + reach, y: from.y };
400
+ const c2 = { x: to.x - reach, y: to.y };
401
+ let best = Number.POSITIVE_INFINITY;
402
+ for (let i = 0; i <= samples; i += 1) {
403
+ const t = i / samples;
404
+ const mt = 1 - t;
405
+ const x = mt * mt * mt * from.x + 3 * mt * mt * t * c1.x + 3 * mt * t * t * c2.x + t * t * t * to.x;
406
+ const y = mt * mt * mt * from.y + 3 * mt * mt * t * c1.y + 3 * mt * t * t * c2.y + t * t * t * to.y;
407
+ best = Math.min(best, Math.hypot(x - point.x, y - point.y));
408
+ }
409
+ return best;
410
+ }
411
+
412
+ export {
413
+ DEFAULT_CURVE_ZOOM,
414
+ XenoCurveScale,
415
+ zoomTimeAboutCursor,
416
+ zoomValueAboutCursor,
417
+ wheelZoomFactor,
418
+ fitValueRange,
419
+ DEFAULT_GRAPH_ZOOM,
420
+ XenoGraphScale,
421
+ zoomAboutCursor,
422
+ fitToBounds,
423
+ graphBounds,
424
+ portRefId,
425
+ edgeKey,
426
+ findPort,
427
+ portHasRoom,
428
+ nodeBounds,
429
+ boxesIntersect,
430
+ XenoNodeIndex,
431
+ edgeBounds,
432
+ visibleEdges,
433
+ edgePath,
434
+ distanceToEdge
435
+ };