@ultrst-armada/frnlts 0.0.0-stage → 0.2.2

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.
@@ -0,0 +1,1125 @@
1
+ import {
2
+ applyFrnlTheme,
3
+ getFrnlComponentHandle,
4
+ getFrnlEvent,
5
+ getFrnlLibrary,
6
+ registerFrnlTheme,
7
+ setFrnlVisualParameter,
8
+ validateFrnlThemeAgainstDefinition
9
+ } from "./chunk-LG2AFOLX.js";
10
+
11
+ // src/runtime/core.ts
12
+ import { z } from "zod";
13
+ var lifecyclePhaseSchema = z.enum([
14
+ "created",
15
+ "initializing",
16
+ "mounted",
17
+ "active",
18
+ "suspended",
19
+ "resuming",
20
+ "disposing",
21
+ "disposed",
22
+ "failed"
23
+ ]);
24
+ var allowedTransitions = {
25
+ created: ["initializing", "disposing"],
26
+ initializing: ["mounted", "failed", "disposing"],
27
+ mounted: ["active", "suspended", "disposing", "failed"],
28
+ active: ["suspended", "disposing", "failed"],
29
+ suspended: ["resuming", "disposing", "failed"],
30
+ resuming: ["active", "failed", "disposing"],
31
+ disposing: ["disposed"],
32
+ disposed: [],
33
+ failed: ["initializing", "disposing", "disposed"]
34
+ };
35
+ function canTransition(from, to) {
36
+ return allowedTransitions[from]?.includes(to) ?? false;
37
+ }
38
+ var LifecycleError = class extends Error {
39
+ constructor(from, to) {
40
+ super(`Invalid FrnlTS lifecycle transition: ${from} -> ${to}`);
41
+ this.from = from;
42
+ this.to = to;
43
+ this.name = "LifecycleError";
44
+ }
45
+ from;
46
+ to;
47
+ };
48
+ function createLifecycle(initial = "created") {
49
+ lifecyclePhaseSchema.parse(initial);
50
+ let phase = initial;
51
+ const listeners = /* @__PURE__ */ new Set();
52
+ return {
53
+ get phase() {
54
+ return phase;
55
+ },
56
+ transition(to) {
57
+ lifecyclePhaseSchema.parse(to);
58
+ if (to === phase) {
59
+ return;
60
+ }
61
+ if (!canTransition(phase, to)) {
62
+ throw new LifecycleError(phase, to);
63
+ }
64
+ const transitionEvent = { from: phase, to };
65
+ phase = to;
66
+ for (const listener of [...listeners]) {
67
+ listener(transitionEvent);
68
+ }
69
+ },
70
+ subscribe(listener) {
71
+ listeners.add(listener);
72
+ return () => {
73
+ listeners.delete(listener);
74
+ };
75
+ }
76
+ };
77
+ }
78
+ var contractSchema = z.object({
79
+ id: z.string().min(1),
80
+ version: z.number().int().positive(),
81
+ inputs: z.record(z.string(), z.unknown()).optional(),
82
+ outputs: z.record(z.string(), z.unknown()).optional(),
83
+ events: z.array(z.string().min(1)).optional(),
84
+ commands: z.array(z.string().min(1)).optional(),
85
+ capabilities: z.array(z.string().min(1)).optional()
86
+ });
87
+ function defineContract(contract) {
88
+ contractSchema.parse(contract);
89
+ return Object.freeze({ ...contract });
90
+ }
91
+ var capabilitySchema = z.object({
92
+ id: z.string().min(1),
93
+ version: z.number().int().positive(),
94
+ provides: z.array(z.string().min(1)).optional(),
95
+ requires: z.array(z.string().min(1)).optional()
96
+ });
97
+ function defineCapability(capability) {
98
+ capabilitySchema.parse(capability);
99
+ return Object.freeze({ ...capability });
100
+ }
101
+ var FrnlFault = class extends Error {
102
+ code;
103
+ recoverable;
104
+ retryable;
105
+ cause;
106
+ constructor(options) {
107
+ super(options.message);
108
+ this.name = "FrnlFault";
109
+ this.code = options.code;
110
+ this.recoverable = options.recoverable ?? false;
111
+ this.retryable = options.retryable ?? false;
112
+ this.cause = options.cause;
113
+ }
114
+ };
115
+ function defineEffect(id, run) {
116
+ z.string().min(1).parse(id);
117
+ if (typeof run !== "function") {
118
+ throw new TypeError("Effect run must be a function");
119
+ }
120
+ return Object.freeze({ id, run });
121
+ }
122
+ function defineCommand(id, payload) {
123
+ z.string().min(1).parse(id);
124
+ return Object.freeze({ id, payload });
125
+ }
126
+
127
+ // src/runtime/resources.ts
128
+ function defineResource(resource) {
129
+ if (!resource || !resource.id || typeof resource.id !== "string") {
130
+ throw new TypeError("Invalid Frnl resource: missing valid id");
131
+ }
132
+ if (typeof resource.acquire !== "function") {
133
+ throw new TypeError(`Invalid Frnl resource '${resource.id}': acquire must be a function`);
134
+ }
135
+ return Object.freeze({ ...resource });
136
+ }
137
+ function createResourceController(resource, scopeId) {
138
+ const controller = new AbortController();
139
+ const state = {
140
+ status: "idle",
141
+ value: void 0,
142
+ error: void 0,
143
+ inFlightPromise: void 0,
144
+ inFlightReleasePromise: void 0
145
+ };
146
+ const context = Object.freeze({
147
+ signal: controller.signal,
148
+ scopeId
149
+ });
150
+ return {
151
+ id: resource.id,
152
+ get status() {
153
+ return state.status;
154
+ },
155
+ get value() {
156
+ return state.value;
157
+ },
158
+ get error() {
159
+ return state.error;
160
+ },
161
+ async load() {
162
+ if (state.status === "ready") {
163
+ return state.value;
164
+ }
165
+ if (state.status === "loading" && state.inFlightPromise) {
166
+ return state.inFlightPromise;
167
+ }
168
+ if (state.status === "released") {
169
+ throw new FrnlFault({
170
+ code: "RESOURCE_RELEASED",
171
+ message: `Resource '${resource.id}' has already been released`
172
+ });
173
+ }
174
+ if (controller.signal.aborted) {
175
+ throw new FrnlFault({
176
+ code: "RESOURCE_ABORTED",
177
+ message: `Resource '${resource.id}' acquisition aborted`
178
+ });
179
+ }
180
+ state.status = "loading";
181
+ state.error = void 0;
182
+ state.inFlightPromise = (async () => {
183
+ try {
184
+ const acquired = await Promise.resolve(resource.acquire(context));
185
+ if (controller.signal.aborted || state.status === "released") {
186
+ if (resource.release && acquired !== void 0) {
187
+ try {
188
+ await Promise.resolve(resource.release(acquired, context));
189
+ } catch {
190
+ }
191
+ }
192
+ throw new FrnlFault({
193
+ code: "RESOURCE_ABORTED",
194
+ message: `Resource '${resource.id}' acquired after abort or release`
195
+ });
196
+ }
197
+ state.value = acquired;
198
+ state.status = "ready";
199
+ return acquired;
200
+ } catch (err) {
201
+ if (state.status !== "released") {
202
+ state.status = "error";
203
+ state.error = err instanceof FrnlFault ? err : new FrnlFault({
204
+ code: "RESOURCE_ACQUIRE_FAILED",
205
+ message: err instanceof Error ? err.message : String(err),
206
+ retryable: true,
207
+ cause: err
208
+ });
209
+ }
210
+ throw state.error ?? err;
211
+ } finally {
212
+ state.inFlightPromise = void 0;
213
+ }
214
+ })();
215
+ return state.inFlightPromise;
216
+ },
217
+ async release() {
218
+ if (state.status === "released") {
219
+ return;
220
+ }
221
+ if (state.inFlightReleasePromise) {
222
+ return state.inFlightReleasePromise;
223
+ }
224
+ state.inFlightReleasePromise = (async () => {
225
+ state.status = "released";
226
+ controller.abort();
227
+ if (state.inFlightPromise) {
228
+ try {
229
+ await state.inFlightPromise;
230
+ } catch {
231
+ }
232
+ }
233
+ const previousValue = state.value;
234
+ state.value = void 0;
235
+ if (previousValue !== void 0 && resource.release) {
236
+ try {
237
+ await Promise.resolve(resource.release(previousValue, context));
238
+ } catch (releaseErr) {
239
+ state.error = releaseErr instanceof FrnlFault ? releaseErr : new FrnlFault({
240
+ code: "RESOURCE_RELEASE_FAILED",
241
+ message: releaseErr instanceof Error ? releaseErr.message : String(releaseErr),
242
+ cause: releaseErr
243
+ });
244
+ throw state.error;
245
+ }
246
+ }
247
+ })();
248
+ return state.inFlightReleasePromise;
249
+ }
250
+ };
251
+ }
252
+ function createScope(id, parent) {
253
+ if (parent && (parent.signal.aborted || parent.isDisposed)) {
254
+ throw new FrnlFault({
255
+ code: "SCOPE_DISPOSED",
256
+ message: `Cannot create child scope from disposed parent '${parent.id}'`
257
+ });
258
+ }
259
+ const controller = new AbortController();
260
+ const resources = /* @__PURE__ */ new Set();
261
+ const childScopes = /* @__PURE__ */ new Set();
262
+ let isDisposed = false;
263
+ let inFlightDisposePromise;
264
+ const scope = {
265
+ id,
266
+ parent,
267
+ signal: controller.signal,
268
+ get isDisposed() {
269
+ return isDisposed;
270
+ },
271
+ _registerChild(child) {
272
+ if (isDisposed) {
273
+ void child.dispose();
274
+ } else {
275
+ childScopes.add(child);
276
+ }
277
+ },
278
+ _removeChild(child) {
279
+ childScopes.delete(child);
280
+ },
281
+ add(resource) {
282
+ if (controller.signal.aborted || isDisposed) {
283
+ throw new FrnlFault({
284
+ code: "SCOPE_DISPOSED",
285
+ message: `Scope '${id}' has been disposed`
286
+ });
287
+ }
288
+ const resCtrl = createResourceController(resource, id);
289
+ resources.add(resCtrl);
290
+ return resCtrl;
291
+ },
292
+ createChildScope(childId) {
293
+ if (controller.signal.aborted || isDisposed) {
294
+ throw new FrnlFault({
295
+ code: "SCOPE_DISPOSED",
296
+ message: `Cannot create child scope from disposed parent '${id}'`
297
+ });
298
+ }
299
+ return createScope(childId, scope);
300
+ },
301
+ async dispose() {
302
+ if (isDisposed) {
303
+ return;
304
+ }
305
+ if (inFlightDisposePromise) {
306
+ return inFlightDisposePromise;
307
+ }
308
+ inFlightDisposePromise = (async () => {
309
+ isDisposed = true;
310
+ if (!controller.signal.aborted) {
311
+ controller.abort();
312
+ }
313
+ if (parent?._removeChild) {
314
+ parent._removeChild(scope);
315
+ }
316
+ const childPromises = [...childScopes].map((child) => child.dispose());
317
+ const resourcePromises = [...resources].map((res) => res.release());
318
+ childScopes.clear();
319
+ resources.clear();
320
+ await Promise.allSettled([...childPromises, ...resourcePromises]);
321
+ })();
322
+ return inFlightDisposePromise;
323
+ }
324
+ };
325
+ if (parent?._registerChild) {
326
+ parent._registerChild(scope);
327
+ }
328
+ if (parent) {
329
+ parent.signal.addEventListener("abort", () => void scope.dispose(), { once: true });
330
+ }
331
+ return scope;
332
+ }
333
+
334
+ // src/runtime/dependencies.ts
335
+ var DependencyGraph = class _DependencyGraph {
336
+ nodes = /* @__PURE__ */ new Map();
337
+ providers = /* @__PURE__ */ new Map();
338
+ // capability -> set of nodeIds
339
+ register(node) {
340
+ if (!node || !node.id || typeof node.id !== "string") {
341
+ throw new TypeError("Invalid dependency node: id must be a non-empty string");
342
+ }
343
+ if (this.nodes.has(node.id)) {
344
+ throw new Error(`Duplicate dependency node: ${node.id}`);
345
+ }
346
+ const frozen = Object.freeze({ ...node });
347
+ this.nodes.set(node.id, frozen);
348
+ for (const prov of node.provides ?? []) {
349
+ if (!this.providers.has(prov)) {
350
+ this.providers.set(prov, /* @__PURE__ */ new Set());
351
+ }
352
+ this.providers.get(prov).add(node.id);
353
+ }
354
+ }
355
+ registerCapability(capability) {
356
+ this.register({
357
+ id: capability.id,
358
+ version: capability.version,
359
+ requires: capability.requires,
360
+ provides: capability.provides
361
+ });
362
+ }
363
+ get(id) {
364
+ return this.nodes.get(id);
365
+ }
366
+ list() {
367
+ return [...this.nodes.values()];
368
+ }
369
+ resolveDependencyNodeId(dependencyName) {
370
+ if (this.nodes.has(dependencyName)) {
371
+ return dependencyName;
372
+ }
373
+ const provSet = this.providers.get(dependencyName);
374
+ if (provSet && provSet.size > 0) {
375
+ return [...provSet][0];
376
+ }
377
+ return void 0;
378
+ }
379
+ validate() {
380
+ const errors = [];
381
+ for (const node of this.nodes.values()) {
382
+ for (const req of node.requires ?? []) {
383
+ const resolvedId = this.resolveDependencyNodeId(req);
384
+ if (!resolvedId) {
385
+ errors.push(`${node.id} requires missing dependency ${req}`);
386
+ }
387
+ }
388
+ }
389
+ return errors;
390
+ }
391
+ topologicalOrder() {
392
+ const visiting = /* @__PURE__ */ new Set();
393
+ const visited = /* @__PURE__ */ new Set();
394
+ const result = [];
395
+ const visit = (id, path = []) => {
396
+ if (visiting.has(id)) {
397
+ const cycle = [...path, id].join(" -> ");
398
+ throw new Error(`Dependency cycle detected at ${id}: ${cycle}`);
399
+ }
400
+ if (visited.has(id)) {
401
+ return;
402
+ }
403
+ visiting.add(id);
404
+ const node = this.nodes.get(id);
405
+ for (const req of node?.requires ?? []) {
406
+ const resolvedId = this.resolveDependencyNodeId(req);
407
+ if (resolvedId && this.nodes.has(resolvedId)) {
408
+ visit(resolvedId, [...path, id]);
409
+ }
410
+ }
411
+ visiting.delete(id);
412
+ visited.add(id);
413
+ result.push(id);
414
+ };
415
+ for (const id of this.nodes.keys()) {
416
+ visit(id);
417
+ }
418
+ return result;
419
+ }
420
+ clear() {
421
+ this.nodes.clear();
422
+ this.providers.clear();
423
+ }
424
+ static fromCapabilities(capabilities) {
425
+ const graph = new _DependencyGraph();
426
+ for (const cap of capabilities) {
427
+ graph.registerCapability(cap);
428
+ }
429
+ return graph;
430
+ }
431
+ };
432
+
433
+ // src/runtime/registry.ts
434
+ var FrnlRegistry = class {
435
+ entries = /* @__PURE__ */ new Map();
436
+ register(entry) {
437
+ if (!entry || !entry.id || typeof entry.id !== "string") {
438
+ throw new TypeError("Invalid registry entry: id must be a non-empty string");
439
+ }
440
+ if (!Number.isInteger(entry.version) || entry.version < 1) {
441
+ throw new TypeError(
442
+ `Invalid registry entry '${entry.id}': version must be a positive integer, received ${entry.version}`
443
+ );
444
+ }
445
+ const current = this.entries.get(entry.id);
446
+ if (current && current.version > entry.version) {
447
+ throw new Error(
448
+ `Cannot register older version (${entry.version}) of '${entry.id}'. Active version is ${current.version}.`
449
+ );
450
+ }
451
+ this.entries.set(entry.id, Object.freeze({ ...entry }));
452
+ }
453
+ has(id) {
454
+ return this.entries.has(id);
455
+ }
456
+ get(id) {
457
+ return this.entries.get(id);
458
+ }
459
+ resolve(id) {
460
+ const entry = this.entries.get(id);
461
+ if (!entry) {
462
+ throw new Error(`Frnl registration not found: ${id}`);
463
+ }
464
+ return entry.value;
465
+ }
466
+ list() {
467
+ return [...this.entries.values()];
468
+ }
469
+ requireCapabilities(id, available) {
470
+ const entry = this.entries.get(id);
471
+ if (!entry) {
472
+ throw new Error(`Frnl registration not found: ${id}`);
473
+ }
474
+ const availableSet = new Set(available);
475
+ const required = [];
476
+ if (entry.contract?.capabilities) {
477
+ required.push(...entry.contract.capabilities);
478
+ }
479
+ if (entry.capabilities) {
480
+ for (const cap of entry.capabilities) {
481
+ if (cap.requires) {
482
+ required.push(...cap.requires);
483
+ }
484
+ }
485
+ }
486
+ for (const cap of required) {
487
+ if (!availableSet.has(cap)) {
488
+ throw new Error(`Missing capability '${cap}' for registration '${id}'`);
489
+ }
490
+ }
491
+ }
492
+ toDependencyGraph() {
493
+ const graph = new DependencyGraph();
494
+ for (const entry of this.entries.values()) {
495
+ const requires = [];
496
+ if (entry.contract?.capabilities) {
497
+ requires.push(...entry.contract.capabilities);
498
+ }
499
+ const provides = [];
500
+ if (entry.capabilities) {
501
+ for (const cap of entry.capabilities) {
502
+ if (cap.provides) {
503
+ provides.push(...cap.provides);
504
+ }
505
+ }
506
+ }
507
+ graph.register({
508
+ id: entry.id,
509
+ version: entry.version,
510
+ requires,
511
+ provides
512
+ });
513
+ }
514
+ return graph;
515
+ }
516
+ clear() {
517
+ this.entries.clear();
518
+ }
519
+ };
520
+
521
+ // src/runtime/routing.ts
522
+ function escapeRegex(str) {
523
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
524
+ }
525
+ function normalizePattern(pattern) {
526
+ if (!pattern || pattern.trim() === "") {
527
+ return "/";
528
+ }
529
+ const trimmed = pattern.trim();
530
+ const withLeading = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
531
+ return withLeading.length > 1 && withLeading.endsWith("/") ? withLeading.slice(0, -1) : withLeading;
532
+ }
533
+ function compileRoute(pattern) {
534
+ const norm = normalizePattern(pattern);
535
+ const paramNames = [];
536
+ let wildcardName;
537
+ const parts = norm.split("/");
538
+ const regexParts = parts.map((part) => {
539
+ if (part.startsWith(":")) {
540
+ const name = part.slice(1);
541
+ paramNames.push(name);
542
+ return "([^/]+)";
543
+ }
544
+ if (part === "*" || part.startsWith("*")) {
545
+ const name = part.length > 1 ? part.slice(1) : "wildcard";
546
+ wildcardName = name;
547
+ paramNames.push(name);
548
+ return "(.*)";
549
+ }
550
+ return escapeRegex(part);
551
+ });
552
+ const source = regexParts.join("/");
553
+ return {
554
+ regex: new RegExp(`^${source}/?$`),
555
+ paramNames,
556
+ wildcardName
557
+ };
558
+ }
559
+ function matchRoute(route, url) {
560
+ const parsed = new URL(url, "http://frnl.local");
561
+ const compiled = compileRoute(route.pattern);
562
+ const match = compiled.regex.exec(parsed.pathname);
563
+ if (!match) {
564
+ return null;
565
+ }
566
+ const params = {};
567
+ compiled.paramNames.forEach((name, index) => {
568
+ const rawVal = match[index + 1] ?? "";
569
+ params[name] = decodeURIComponent(rawVal);
570
+ });
571
+ const query = {};
572
+ parsed.searchParams.forEach((value, key) => {
573
+ query[key] = value;
574
+ });
575
+ return {
576
+ id: route.id,
577
+ params: Object.freeze(params),
578
+ query: Object.freeze(query),
579
+ path: parsed.pathname
580
+ };
581
+ }
582
+ function buildRouteUrl(route, params = {}, query = {}, options = {}) {
583
+ const norm = normalizePattern(route.pattern);
584
+ const parts = norm.split("/");
585
+ const consumedParams = /* @__PURE__ */ new Set();
586
+ const builtParts = parts.map((part) => {
587
+ if (part.startsWith(":")) {
588
+ const name = part.slice(1);
589
+ consumedParams.add(name);
590
+ const value = params[name];
591
+ if (value === void 0 || value === null || value === "") {
592
+ throw new Error(`Missing route parameter: ${name}`);
593
+ }
594
+ return encodeURIComponent(value);
595
+ }
596
+ if (part === "*" || part.startsWith("*")) {
597
+ const name = part.length > 1 ? part.slice(1) : "wildcard";
598
+ consumedParams.add(name);
599
+ consumedParams.add("wildcard");
600
+ const value = params[name] ?? params.wildcard ?? "";
601
+ return value.split("/").map((segment) => encodeURIComponent(segment)).join("/");
602
+ }
603
+ return part;
604
+ });
605
+ const path = builtParts.join("/") || "/";
606
+ const unconsumed = {};
607
+ for (const [key, value] of Object.entries(params)) {
608
+ if (!consumedParams.has(key)) {
609
+ if (options.strict) {
610
+ throw new Error(`Unknown route parameter: ${key}`);
611
+ }
612
+ unconsumed[key] = value;
613
+ }
614
+ }
615
+ const finalQuery = {
616
+ ...options.appendUnmatchedParamsToQuery ? unconsumed : {},
617
+ ...query
618
+ };
619
+ const searchParams = new URLSearchParams();
620
+ for (const [key, value] of Object.entries(finalQuery)) {
621
+ if (value !== void 0 && value !== null) {
622
+ searchParams.set(key, value);
623
+ }
624
+ }
625
+ const queryString = searchParams.toString();
626
+ return queryString ? `${path}?${queryString}` : path;
627
+ }
628
+ var FrnlRouter = class {
629
+ routeMap = /* @__PURE__ */ new Map();
630
+ patternMap = /* @__PURE__ */ new Map();
631
+ // pattern -> routeId
632
+ constructor(routes = []) {
633
+ for (const route of routes) {
634
+ this.addRoute(route);
635
+ }
636
+ }
637
+ addRoute(route) {
638
+ if (!route || !route.id || typeof route.id !== "string") {
639
+ throw new TypeError("Invalid route: id must be a non-empty string");
640
+ }
641
+ if (this.routeMap.has(route.id)) {
642
+ throw new Error(`Duplicate route id: ${route.id}`);
643
+ }
644
+ const norm = normalizePattern(route.pattern);
645
+ const existingRouteId = this.patternMap.get(norm);
646
+ if (existingRouteId) {
647
+ throw new Error(
648
+ `Conflicting route pattern '${route.pattern}' already registered by route '${existingRouteId}'`
649
+ );
650
+ }
651
+ this.routeMap.set(route.id, Object.freeze({ ...route }));
652
+ this.patternMap.set(norm, route.id);
653
+ }
654
+ getRoute(id) {
655
+ return this.routeMap.get(id);
656
+ }
657
+ match(url) {
658
+ for (const route of this.routeMap.values()) {
659
+ const match = matchRoute(route, url);
660
+ if (match) {
661
+ return match;
662
+ }
663
+ }
664
+ return null;
665
+ }
666
+ async resolve(url) {
667
+ const match = this.match(url);
668
+ if (!match) {
669
+ return null;
670
+ }
671
+ const route = this.routeMap.get(match.id);
672
+ const data = route?.load ? await route.load() : void 0;
673
+ return { match, data };
674
+ }
675
+ list() {
676
+ return [...this.routeMap.values()];
677
+ }
678
+ clear() {
679
+ this.routeMap.clear();
680
+ this.patternMap.clear();
681
+ }
682
+ };
683
+
684
+ // src/runtime/accessibility.ts
685
+ function validateAccessibility(contract, attributes) {
686
+ const issues = [];
687
+ if (contract.nameRequired) {
688
+ const hasLabel = typeof attributes["aria-label"] === "string" && attributes["aria-label"].trim().length > 0;
689
+ const hasLabelledBy = typeof attributes["aria-labelledby"] === "string" && attributes["aria-labelledby"].trim().length > 0;
690
+ if (!hasLabel && !hasLabelledBy) {
691
+ issues.push({
692
+ code: "ACCESSIBLE_NAME_REQUIRED",
693
+ message: "An accessible name is required via aria-label or aria-labelledby."
694
+ });
695
+ }
696
+ }
697
+ for (const attr of contract.requiredAttributes ?? []) {
698
+ if (!(attr in attributes) || attributes[attr] === void 0 || attributes[attr] === null || attributes[attr] === "") {
699
+ issues.push({
700
+ code: "REQUIRED_ATTRIBUTE_MISSING",
701
+ message: `Missing required attribute: ${attr}`
702
+ });
703
+ }
704
+ }
705
+ if (contract.role && attributes.role !== contract.role) {
706
+ issues.push({
707
+ code: "ROLE_MISMATCH",
708
+ message: `Expected role '${contract.role}', received '${String(attributes.role)}'`
709
+ });
710
+ }
711
+ if (contract.focusable) {
712
+ if (attributes.tabIndex === -1 || attributes["aria-hidden"] === true) {
713
+ issues.push({
714
+ code: "FOCUS_RESTRICTED",
715
+ message: "Element is marked as focusable but focus is suppressed via tabIndex or aria-hidden."
716
+ });
717
+ }
718
+ }
719
+ return issues;
720
+ }
721
+
722
+ // src/runtime/store.ts
723
+ import { createStore } from "zustand/vanilla";
724
+ import { z as z3 } from "zod";
725
+
726
+ // src/runtime/validation.ts
727
+ import { z as z2 } from "zod";
728
+ var entityIdSchema = z2.string().min(1);
729
+ var stateNameSchema = z2.string().min(1);
730
+ var propertyKeySchema = z2.string().min(1);
731
+ var propertyValueSchema = z2.unknown();
732
+ var propertiesSchema = z2.record(z2.string(), propertyValueSchema);
733
+ var frnlThemeStateSchema = z2.object({
734
+ visualParameters: propertiesSchema.optional()
735
+ }).strict();
736
+ var frnlThemeComponentSchema = z2.object({
737
+ states: z2.record(z2.string().min(1), frnlThemeStateSchema).optional()
738
+ }).strict();
739
+ var frnlThemeDefinitionSchema = z2.record(
740
+ z2.string().min(1),
741
+ frnlThemeComponentSchema
742
+ );
743
+ var entityStateSchema = z2.object({
744
+ state: stateNameSchema,
745
+ properties: propertiesSchema
746
+ });
747
+ var entityStateChangedPayloadSchema = z2.object({
748
+ entityId: entityIdSchema,
749
+ previousState: stateNameSchema,
750
+ state: stateNameSchema
751
+ });
752
+ var entityPropertyChangedPayloadSchema = z2.object({
753
+ entityId: entityIdSchema,
754
+ key: propertyKeySchema,
755
+ value: propertyValueSchema
756
+ });
757
+ var flowTriggeredPayloadSchema = z2.object({
758
+ flowId: z2.string().min(1),
759
+ componentId: entityIdSchema,
760
+ trigger: z2.string().min(1),
761
+ payload: propertyValueSchema.optional()
762
+ });
763
+ var frnlEventSchema = z2.discriminatedUnion("type", [
764
+ z2.object({ type: z2.literal("entity.stateChanged"), payload: entityStateChangedPayloadSchema }),
765
+ z2.object({ type: z2.literal("entity.propertyChanged"), payload: entityPropertyChangedPayloadSchema }),
766
+ z2.object({ type: z2.literal("flow.triggered"), payload: flowTriggeredPayloadSchema })
767
+ ]);
768
+ function parseFrnlInput(schema, input) {
769
+ return schema.parse(input);
770
+ }
771
+ function validateFrnlInput(schema, input) {
772
+ return schema.safeParse(input).success;
773
+ }
774
+
775
+ // src/runtime/store.ts
776
+ function createFrnlStore(initial) {
777
+ const initialEntities = z3.record(z3.string(), entityStateSchema).parse(initial?.entities ?? {});
778
+ return createStore((set, get) => ({
779
+ entities: initialEntities,
780
+ events: [],
781
+ componentDefinitions: {},
782
+ eventHandlers: {},
783
+ componentParents: {},
784
+ visualOverrides: {},
785
+ themes: {},
786
+ componentThemes: {},
787
+ libraries: {},
788
+ registerComponentDefinition: (componentId, definition) => {
789
+ const id = entityIdSchema.parse(componentId);
790
+ for (const theme of Object.values(get().themes)) {
791
+ validateFrnlThemeAgainstDefinition(theme, definition);
792
+ }
793
+ set((current) => ({ componentDefinitions: { ...current.componentDefinitions, [id]: definition } }));
794
+ },
795
+ registerTheme: (name, theme) => {
796
+ const themeName = z3.string().min(1).parse(name);
797
+ const validatedTheme = frnlThemeDefinitionSchema.parse(theme);
798
+ for (const definition of Object.values(get().componentDefinitions)) {
799
+ validateFrnlThemeAgainstDefinition(validatedTheme, definition);
800
+ }
801
+ set((current) => ({
802
+ themes: { ...current.themes, [themeName]: validatedTheme }
803
+ }));
804
+ },
805
+ applyTheme: (componentId, themeName) => {
806
+ const id = entityIdSchema.parse(componentId);
807
+ const name = z3.string().min(1).parse(themeName);
808
+ set((current) => {
809
+ if (!current.entities[id]) {
810
+ throw new Error(`Unknown Frnl component: ${id}`);
811
+ }
812
+ if (!current.themes[name]) {
813
+ throw new Error(`Unknown Frnl theme: ${name}`);
814
+ }
815
+ const visualOverrides = { ...current.visualOverrides };
816
+ delete visualOverrides[id];
817
+ return {
818
+ componentThemes: { ...current.componentThemes, [id]: name },
819
+ visualOverrides
820
+ };
821
+ });
822
+ },
823
+ registerEventHandlers: (componentId, handlers) => {
824
+ const id = entityIdSchema.parse(componentId);
825
+ set((current) => ({ eventHandlers: { ...current.eventHandlers, [id]: handlers } }));
826
+ },
827
+ unregisterEventHandlers: (componentId) => {
828
+ const id = entityIdSchema.parse(componentId);
829
+ set((current) => {
830
+ const next = { ...current.eventHandlers };
831
+ delete next[id];
832
+ return { eventHandlers: next };
833
+ });
834
+ },
835
+ unregisterEntity: (entityId) => {
836
+ const id = entityIdSchema.parse(entityId);
837
+ set((current) => {
838
+ const entities = { ...current.entities };
839
+ const componentDefinitions = { ...current.componentDefinitions };
840
+ const eventHandlers = { ...current.eventHandlers };
841
+ const componentParents = { ...current.componentParents };
842
+ const visualOverrides = { ...current.visualOverrides };
843
+ const componentThemes = { ...current.componentThemes };
844
+ delete entities[id];
845
+ delete componentDefinitions[id];
846
+ delete eventHandlers[id];
847
+ delete componentParents[id];
848
+ delete visualOverrides[id];
849
+ delete componentThemes[id];
850
+ return { entities, componentDefinitions, eventHandlers, componentParents, visualOverrides, componentThemes };
851
+ });
852
+ },
853
+ setVisualOverride: (componentId, parameter, value) => {
854
+ const id = entityIdSchema.parse(componentId);
855
+ const key = propertyKeySchema.parse(parameter);
856
+ set((current) => ({
857
+ visualOverrides: {
858
+ ...current.visualOverrides,
859
+ [id]: { ...current.visualOverrides[id] ?? {}, [key]: value }
860
+ }
861
+ }));
862
+ },
863
+ clearVisualOverrides: (componentId) => {
864
+ const id = entityIdSchema.parse(componentId);
865
+ set((current) => {
866
+ const visualOverrides = { ...current.visualOverrides };
867
+ delete visualOverrides[id];
868
+ return { visualOverrides };
869
+ });
870
+ },
871
+ registerLibrary: (address, library) => {
872
+ const key = z3.string().min(1).parse(address);
873
+ set((current) => ({ libraries: { ...current.libraries, [key]: library } }));
874
+ },
875
+ unregisterLibrary: (address) => {
876
+ const key = z3.string().min(1).parse(address);
877
+ set((current) => {
878
+ const libraries = { ...current.libraries };
879
+ delete libraries[key];
880
+ return { libraries };
881
+ });
882
+ },
883
+ registerComponentParent: (componentId, parentId) => {
884
+ const id = entityIdSchema.parse(componentId);
885
+ const parent = parentId === void 0 ? void 0 : entityIdSchema.parse(parentId);
886
+ if (parent === id) throw new Error(`Frnl component cannot be its own parent: ${id}`);
887
+ set((current) => ({
888
+ componentParents: { ...current.componentParents, [id]: parent }
889
+ }));
890
+ },
891
+ emit: (event) => {
892
+ const validated = frnlEventSchema.parse(event);
893
+ set((current) => ({ events: [...current.events, validated] }));
894
+ },
895
+ /**
896
+ * registerEntity performs an idempotent upsert of declarative metadata.
897
+ *
898
+ * Semantic Authority Model:
899
+ * - Declarative Metadata (authoritative from registration / props):
900
+ * `properties`.
901
+ * - Runtime State (authoritative from runtime / interactions):
902
+ * `state` (active state is preserved across re-registrations),
903
+ * `events` (event history is preserved).
904
+ */
905
+ registerEntity: (entityId, entity) => {
906
+ const id = entityIdSchema.parse(entityId);
907
+ const validated = entityStateSchema.parse(entity);
908
+ set((current) => {
909
+ const existing = current.entities[id];
910
+ const nextEntity = existing ? {
911
+ // Runtime state is preserved if already set
912
+ state: existing.state,
913
+ // Declarative metadata is merged / updated
914
+ properties: { ...existing.properties, ...validated.properties }
915
+ } : validated;
916
+ return {
917
+ entities: { ...current.entities, [id]: nextEntity }
918
+ };
919
+ });
920
+ },
921
+ setEntityState: (entityId, state) => {
922
+ const id = entityIdSchema.parse(entityId);
923
+ const nextState = stateNameSchema.parse(state);
924
+ set((current) => {
925
+ const entity = current.entities[id];
926
+ if (!entity) return current;
927
+ if (entity.state === nextState) return current;
928
+ const previousState = entity.state;
929
+ const entityPayload = entityStateChangedPayloadSchema.parse({
930
+ entityId: id,
931
+ previousState,
932
+ state: nextState
933
+ });
934
+ const newEvents = [
935
+ { type: "entity.stateChanged", payload: entityPayload }
936
+ ];
937
+ const visualOverrides = { ...current.visualOverrides };
938
+ delete visualOverrides[id];
939
+ return {
940
+ entities: { ...current.entities, [id]: { ...entity, state: nextState } },
941
+ visualOverrides,
942
+ events: [...current.events, ...newEvents]
943
+ };
944
+ });
945
+ },
946
+ setEntityProperty: (entityId, key, value) => {
947
+ const id = entityIdSchema.parse(entityId);
948
+ const property = propertyKeySchema.parse(key);
949
+ set((current) => {
950
+ const entity = current.entities[id];
951
+ if (!entity) return current;
952
+ const payload = entityPropertyChangedPayloadSchema.parse({
953
+ entityId: id,
954
+ key: property,
955
+ value
956
+ });
957
+ return {
958
+ entities: {
959
+ ...current.entities,
960
+ [id]: { ...entity, properties: { ...entity.properties, [property]: value } }
961
+ },
962
+ events: [...current.events, { type: "entity.propertyChanged", payload }]
963
+ };
964
+ });
965
+ },
966
+ setEntityProperties: (entityId, properties) => {
967
+ const id = entityIdSchema.parse(entityId);
968
+ const validated = propertiesSchema.parse(properties);
969
+ set((current) => {
970
+ const entity = current.entities[id];
971
+ if (!entity) return current;
972
+ const newEvents = [];
973
+ for (const [key, value] of Object.entries(validated)) {
974
+ if (entity.properties[key] !== value) {
975
+ newEvents.push({
976
+ type: "entity.propertyChanged",
977
+ payload: entityPropertyChangedPayloadSchema.parse({
978
+ entityId: id,
979
+ key,
980
+ value
981
+ })
982
+ });
983
+ }
984
+ }
985
+ return {
986
+ entities: {
987
+ ...current.entities,
988
+ [id]: {
989
+ ...entity,
990
+ properties: { ...entity.properties, ...validated }
991
+ }
992
+ },
993
+ events: [...current.events, ...newEvents]
994
+ };
995
+ });
996
+ },
997
+ triggerFlow: (flowId, componentId, trigger, payload) => {
998
+ const fId = z3.string().min(1).parse(flowId);
999
+ const cId = entityIdSchema.parse(componentId);
1000
+ const trig = z3.string().min(1).parse(trigger);
1001
+ const eventPayload = flowTriggeredPayloadSchema.parse({
1002
+ flowId: fId,
1003
+ componentId: cId,
1004
+ trigger: trig,
1005
+ payload
1006
+ });
1007
+ set((current) => ({
1008
+ events: [
1009
+ ...current.events,
1010
+ { type: "flow.triggered", payload: eventPayload }
1011
+ ]
1012
+ }));
1013
+ }
1014
+ }));
1015
+ }
1016
+
1017
+ // src/runtime/facade.ts
1018
+ var FrnlFacade = class {
1019
+ constructor(store) {
1020
+ this.store = store;
1021
+ }
1022
+ store;
1023
+ component(componentId) {
1024
+ return getFrnlComponentHandle(this.store, componentId);
1025
+ }
1026
+ event(componentId, eventName) {
1027
+ return getFrnlEvent(this.store, componentId, eventName);
1028
+ }
1029
+ library(address) {
1030
+ return getFrnlLibrary(this.store, address);
1031
+ }
1032
+ setVisualParameter(address, value) {
1033
+ setFrnlVisualParameter(this.store, address, value);
1034
+ }
1035
+ registerTheme(name, theme) {
1036
+ registerFrnlTheme(this.store, name, theme);
1037
+ }
1038
+ applyThemeToAllComponents(themeName) {
1039
+ if (!this.store.getState().themes[themeName]) {
1040
+ throw new Error(`Unknown Frnl theme: ${themeName}`);
1041
+ }
1042
+ for (const componentId of Object.keys(this.store.getState().entities)) {
1043
+ applyFrnlTheme(this.store, componentId, themeName);
1044
+ }
1045
+ }
1046
+ };
1047
+ function createFrnlFacade(store) {
1048
+ return new FrnlFacade(store);
1049
+ }
1050
+
1051
+ // src/runtime/component-definition.ts
1052
+ import { z as z4 } from "zod";
1053
+ var frnlVisualDefinitionSchema = z4.object({
1054
+ parameters: z4.record(z4.string(), z4.unknown())
1055
+ });
1056
+ var frnlPhaseDefinitionSchema = z4.object({
1057
+ visualDefinition: frnlVisualDefinitionSchema
1058
+ });
1059
+ var frnlEventDefinitionSchema = z4.object({
1060
+ functionId: z4.string().min(1)
1061
+ });
1062
+ var frnlStateDefinitionSchema = z4.object({
1063
+ name: z4.string().min(1),
1064
+ events: z4.record(z4.string().min(1), frnlEventDefinitionSchema),
1065
+ visualParameters: z4.record(z4.string(), z4.unknown()).default({}),
1066
+ phases: z4.object({
1067
+ attackPhase: frnlPhaseDefinitionSchema.optional(),
1068
+ sustainPhase: frnlPhaseDefinitionSchema,
1069
+ releasePhase: frnlPhaseDefinitionSchema.optional()
1070
+ })
1071
+ });
1072
+ var frnlComponentDefinitionSchema = z4.object({
1073
+ name: z4.string().min(1),
1074
+ states: z4.record(z4.string().min(1), frnlStateDefinitionSchema)
1075
+ });
1076
+ function defineFrnlComponent(definition) {
1077
+ return frnlComponentDefinitionSchema.parse(definition);
1078
+ }
1079
+
1080
+ export {
1081
+ lifecyclePhaseSchema,
1082
+ canTransition,
1083
+ LifecycleError,
1084
+ createLifecycle,
1085
+ contractSchema,
1086
+ defineContract,
1087
+ capabilitySchema,
1088
+ defineCapability,
1089
+ FrnlFault,
1090
+ defineEffect,
1091
+ defineCommand,
1092
+ defineResource,
1093
+ createResourceController,
1094
+ createScope,
1095
+ DependencyGraph,
1096
+ FrnlRegistry,
1097
+ matchRoute,
1098
+ buildRouteUrl,
1099
+ FrnlRouter,
1100
+ validateAccessibility,
1101
+ entityIdSchema,
1102
+ stateNameSchema,
1103
+ propertyKeySchema,
1104
+ propertyValueSchema,
1105
+ propertiesSchema,
1106
+ frnlThemeStateSchema,
1107
+ frnlThemeComponentSchema,
1108
+ frnlThemeDefinitionSchema,
1109
+ entityStateSchema,
1110
+ entityStateChangedPayloadSchema,
1111
+ entityPropertyChangedPayloadSchema,
1112
+ flowTriggeredPayloadSchema,
1113
+ frnlEventSchema,
1114
+ parseFrnlInput,
1115
+ validateFrnlInput,
1116
+ createFrnlStore,
1117
+ FrnlFacade,
1118
+ createFrnlFacade,
1119
+ frnlVisualDefinitionSchema,
1120
+ frnlPhaseDefinitionSchema,
1121
+ frnlEventDefinitionSchema,
1122
+ frnlStateDefinitionSchema,
1123
+ frnlComponentDefinitionSchema,
1124
+ defineFrnlComponent
1125
+ };