@june-ai/june-ui 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,24 +1,1086 @@
1
- // src/JuneUI.tsx
1
+ // src/renderer/renderer.tsx
2
2
  import { Renderer } from "@openuidev/react-lang";
3
- import { openuiLibrary } from "@openuidev/react-ui";
3
+ import {
4
+ Fragment,
5
+ useCallback,
6
+ useMemo as useMemo2,
7
+ useRef
8
+ } from "react";
9
+
10
+ // src/errors/errors.ts
11
+ var JuneError = class _JuneError extends Error {
12
+ constructor(code, message, options = {}) {
13
+ super(message);
14
+ Object.setPrototypeOf(this, _JuneError.prototype);
15
+ this.name = "JuneError";
16
+ this.code = code;
17
+ this.details = options.details;
18
+ if (options.cause !== void 0) {
19
+ this.cause = options.cause;
20
+ }
21
+ }
22
+ };
23
+ function isJuneError(value) {
24
+ return value instanceof JuneError;
25
+ }
26
+ function toJuneError(error, fallbackCode, message) {
27
+ if (error instanceof JuneError) return error;
28
+ const text = message ?? (error instanceof Error ? error.message : String(error));
29
+ return new JuneError(fallbackCode, text, { cause: error });
30
+ }
31
+
32
+ // src/actions/action-handler.ts
33
+ function firstString(...values) {
34
+ for (const v of values) if (typeof v === "string" && v.length > 0) return v;
35
+ return null;
36
+ }
37
+ var defaultParseActionEvent = (event) => {
38
+ if (!event || typeof event !== "object") return null;
39
+ const e = event;
40
+ const name = firstString(e.type, e.name, e.action);
41
+ if (!name) return null;
42
+ return { name, payload: e.params ?? e.payload ?? e.data };
43
+ };
44
+ async function runAction(event, options) {
45
+ const { actions, parseEvent = defaultParseActionEvent, confirm, onError, onUnhandled } = options;
46
+ const parsed = parseEvent(event);
47
+ const action = parsed && Object.prototype.hasOwnProperty.call(actions, parsed.name) ? actions[parsed.name] : void 0;
48
+ if (!parsed || !action) {
49
+ onUnhandled?.(event);
50
+ return "unhandled";
51
+ }
52
+ const { name } = parsed;
53
+ let payload = parsed.payload;
54
+ if (action.schema) {
55
+ const result = action.schema.safeParse(payload);
56
+ if (!result.success) {
57
+ onError?.(
58
+ new JuneError("ACTION_VALIDATION_ERROR", `Invalid payload for action "${name}"`, {
59
+ details: result.error
60
+ })
61
+ );
62
+ return "failed";
63
+ }
64
+ payload = result.data;
65
+ }
66
+ try {
67
+ if (action.sensitive || action.confirm) {
68
+ const ask = action.confirm ?? confirm;
69
+ if (!ask) {
70
+ onError?.(
71
+ new JuneError(
72
+ "ACTION_REJECTED",
73
+ `Action "${name}" requires confirmation but no confirm handler was provided`
74
+ )
75
+ );
76
+ return "rejected";
77
+ }
78
+ const approved = await ask({ name, payload, description: action.description });
79
+ if (!approved) return "rejected";
80
+ }
81
+ await action.handler(payload, { name, event });
82
+ return "handled";
83
+ } catch (error) {
84
+ onError?.(toJuneError(error, "ACTION_ERROR"));
85
+ return "failed";
86
+ }
87
+ }
88
+ function createActionHandler(options) {
89
+ return (event) => {
90
+ void runAction(event, options);
91
+ };
92
+ }
93
+
94
+ // src/themes/theme-provider.tsx
95
+ import {
96
+ useMemo,
97
+ useSyncExternalStore
98
+ } from "react";
99
+
100
+ // src/core/context.ts
101
+ import { createContext, useContext } from "react";
102
+ var JuneContext = createContext({
103
+ mode: "light",
104
+ theme: "light"
105
+ });
106
+ function useJuneTheme() {
107
+ return useContext(JuneContext);
108
+ }
109
+
110
+ // src/themes/dark.ts
111
+ var darkTheme = {
112
+ "--june-background": "#0b0f19",
113
+ "--june-foreground": "#f3f4f6",
114
+ "--june-muted": "#9ca3af",
115
+ "--june-border": "#1f2937",
116
+ "--june-accent": "#818cf8",
117
+ "--june-danger": "#f87171",
118
+ "--june-radius": "8px",
119
+ "--june-font-family": "system-ui, -apple-system, 'Segoe UI', sans-serif"
120
+ };
121
+
122
+ // src/themes/light.ts
123
+ var lightTheme = {
124
+ "--june-background": "#ffffff",
125
+ "--june-foreground": "#111827",
126
+ "--june-muted": "#6b7280",
127
+ "--june-border": "#e5e7eb",
128
+ "--june-accent": "#4f46e5",
129
+ "--june-danger": "#b91c1c",
130
+ "--june-radius": "8px",
131
+ "--june-font-family": "system-ui, -apple-system, 'Segoe UI', sans-serif"
132
+ };
133
+
134
+ // src/themes/theme-provider.tsx
4
135
  import { jsx } from "react/jsx-runtime";
5
- function JuneUI({
6
- response,
7
- isStreaming = false,
8
- onAction,
9
- onError
136
+ var QUERY = "(prefers-color-scheme: dark)";
137
+ function subscribe(onChange) {
138
+ if (typeof window === "undefined" || !window.matchMedia) return () => {
139
+ };
140
+ const mq = window.matchMedia(QUERY);
141
+ mq.addEventListener("change", onChange);
142
+ return () => mq.removeEventListener("change", onChange);
143
+ }
144
+ function getSnapshot() {
145
+ if (typeof window === "undefined") return "light";
146
+ return window.matchMedia?.(QUERY)?.matches ? "dark" : "light";
147
+ }
148
+ function getServerSnapshot() {
149
+ return "light";
150
+ }
151
+ function useResolvedTheme(mode) {
152
+ const system = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
153
+ return mode === "system" ? system : mode;
154
+ }
155
+ function JuneThemeProvider({
156
+ mode = "light",
157
+ vars,
158
+ className,
159
+ style,
160
+ children
10
161
  }) {
11
- return /* @__PURE__ */ jsx(
12
- Renderer,
162
+ const resolved = useResolvedTheme(mode);
163
+ const mergedStyle = useMemo(() => {
164
+ const base = resolved === "dark" ? darkTheme : lightTheme;
165
+ return { ...base, ...vars, colorScheme: resolved, ...style };
166
+ }, [resolved, vars, style]);
167
+ const context = useMemo(() => ({ mode, theme: resolved }), [mode, resolved]);
168
+ return /* @__PURE__ */ jsx(JuneContext.Provider, { value: context, children: /* @__PURE__ */ jsx(
169
+ "div",
13
170
  {
14
- library: openuiLibrary,
15
- response,
16
- isStreaming,
17
- onAction,
18
- onError
171
+ className: className ? `june-ui-root ${className}` : "june-ui-root",
172
+ "data-june-theme": resolved,
173
+ style: mergedStyle,
174
+ children
175
+ }
176
+ ) });
177
+ }
178
+
179
+ // src/utils/normalize-program.ts
180
+ var ATTACHABLE = /* @__PURE__ */ new Set([
181
+ "Accordion",
182
+ "AreaChart",
183
+ "BarChart",
184
+ "Buttons",
185
+ "Button",
186
+ "Callout",
187
+ "Card",
188
+ "CardHeader",
189
+ "Carousel",
190
+ "CodeBlock",
191
+ "CompositeCardBlock",
192
+ "ContextCardBlock",
193
+ "EditableTable",
194
+ "EntityList",
195
+ "Form",
196
+ "HorizontalBarChart",
197
+ "IconButton",
198
+ "Image",
199
+ "ImageBlock",
200
+ "ImageGallery",
201
+ "InlineHeader",
202
+ "LineChart",
203
+ "ListBlock",
204
+ "MarkDownRenderer",
205
+ "Modal",
206
+ "OverviewCardBlock",
207
+ "PieChart",
208
+ "RadarChart",
209
+ "RadialChart",
210
+ "ScatterChart",
211
+ "Separator",
212
+ "SingleStackedBarChart",
213
+ "SnippetCardBlock",
214
+ "Stack",
215
+ "Steps",
216
+ "Table",
217
+ "Tabs",
218
+ "TagBlock",
219
+ "TextCallout",
220
+ "TextContent",
221
+ "VisualCardBlock"
222
+ ]);
223
+ function normalizeProgram(source) {
224
+ const rewritten = rewriteCalls(source);
225
+ return attachOrphans(rewritten);
226
+ }
227
+ function rewriteCalls(source) {
228
+ let out = "";
229
+ let i = 0;
230
+ let quote = false;
231
+ while (i < source.length) {
232
+ const ch = source[i];
233
+ if (quote) {
234
+ out += ch;
235
+ if (ch === "\\") {
236
+ i += 1;
237
+ if (i < source.length) out += source[i];
238
+ } else if (ch === '"') {
239
+ quote = false;
240
+ }
241
+ i += 1;
242
+ continue;
243
+ }
244
+ if (ch === '"') {
245
+ quote = true;
246
+ out += ch;
247
+ i += 1;
248
+ continue;
249
+ }
250
+ const call = readCall(source, i);
251
+ if (call) {
252
+ const next = rewriteKnownCall(call);
253
+ if (next !== null) {
254
+ out += next;
255
+ i = call.end;
256
+ continue;
257
+ }
258
+ }
259
+ out += ch;
260
+ i += 1;
261
+ }
262
+ return out === source ? source : out;
263
+ }
264
+ function rewriteKnownCall(call) {
265
+ if (call.callee === "Grid") {
266
+ const args = call.args.map((arg) => rewriteCalls(arg));
267
+ const children = args.length === 0 ? "[]" : args.length === 1 ? args[0] : null;
268
+ if (children !== null) {
269
+ return `Stack(${children}, "row", "m", "stretch", "start", true)`;
19
270
  }
271
+ return `Stack(${args.join(", ")})`;
272
+ }
273
+ if (call.callee === "Card") {
274
+ const args = call.args.map((arg) => rewriteCalls(arg));
275
+ if (args.length >= 2 && isStringLiteral(args[0]) && isStringLiteral(args[1])) {
276
+ const children = `[TextContent(${args[0]}, "small"), TextContent(${args[1]}, "large-heavy")]`;
277
+ return `Card(${[children, ...args.slice(2)].join(", ")})`;
278
+ }
279
+ if (args.some((arg, index) => arg !== call.args[index])) {
280
+ return `Card(${args.join(", ")})`;
281
+ }
282
+ }
283
+ return null;
284
+ }
285
+ function readCall(source, index) {
286
+ if (index > 0 && /[A-Za-z0-9_$]/.test(source[index - 1])) return null;
287
+ const match = /^(Grid|Card)\s*\(/.exec(source.slice(index));
288
+ if (!match) return null;
289
+ const open = index + match[0].length - 1;
290
+ const close = findMatchingParen(source, open);
291
+ if (close < 0) return null;
292
+ const args = splitArgs(source.slice(open + 1, close));
293
+ if (!args) return null;
294
+ return { callee: match[1], args, end: close + 1 };
295
+ }
296
+ function findMatchingParen(source, open) {
297
+ let depth = 0;
298
+ let quote = false;
299
+ for (let i = open; i < source.length; i += 1) {
300
+ const ch = source[i];
301
+ if (quote) {
302
+ if (ch === "\\") i += 1;
303
+ else if (ch === '"') quote = false;
304
+ continue;
305
+ }
306
+ if (ch === '"') quote = true;
307
+ else if (ch === "(") depth += 1;
308
+ else if (ch === ")") {
309
+ depth -= 1;
310
+ if (depth === 0) return i;
311
+ }
312
+ }
313
+ return -1;
314
+ }
315
+ function splitArgs(inner) {
316
+ const args = [];
317
+ let start = 0;
318
+ let depth = 0;
319
+ let quote = false;
320
+ for (let i = 0; i < inner.length; i += 1) {
321
+ const ch = inner[i];
322
+ if (quote) {
323
+ if (ch === "\\") i += 1;
324
+ else if (ch === '"') quote = false;
325
+ continue;
326
+ }
327
+ if (ch === '"') quote = true;
328
+ else if (ch === "(" || ch === "[" || ch === "{") depth += 1;
329
+ else if (ch === ")" || ch === "]" || ch === "}") depth -= 1;
330
+ else if (ch === "," && depth === 0) {
331
+ args.push(inner.slice(start, i).trim());
332
+ start = i + 1;
333
+ }
334
+ }
335
+ if (quote || depth !== 0) return null;
336
+ const last = inner.slice(start).trim();
337
+ if (last) args.push(last);
338
+ return args;
339
+ }
340
+ function isStringLiteral(value) {
341
+ return /^"(?:\\.|[^"\\])*"$/.test(value.trim());
342
+ }
343
+ function attachOrphans(source) {
344
+ let text = source;
345
+ let stmts = parseStatements(text);
346
+ if (!stmts) return source;
347
+ const taken = new Set(stmts.map((stmt) => stmt.name));
348
+ const extraRoots = stmts.filter((stmt) => stmt.name === "root").slice(1);
349
+ if (extraRoots.length > 0) {
350
+ const edits = extraRoots.map((stmt) => {
351
+ const body2 = fresh(taken, "rootBlock");
352
+ const stmtText = text.slice(stmt.start, stmt.end);
353
+ return {
354
+ start: stmt.start,
355
+ end: stmt.end,
356
+ text: stmtText.replace(/^(\s*)root\b/, `$1${body2}`)
357
+ };
358
+ });
359
+ text = applyEdits(text, edits);
360
+ stmts = parseStatements(text);
361
+ if (!stmts) return source;
362
+ }
363
+ const orphans = stmts.filter(
364
+ (stmt) => stmt.name !== "root" && ATTACHABLE.has(calleeOf(stmt.rhs) ?? "") && !referencedByOther(stmt.name, stmts)
365
+ );
366
+ if (orphans.length === 0) return source;
367
+ const names = orphans.map((stmt) => stmt.name);
368
+ const root = stmts.find((stmt) => stmt.name === "root");
369
+ if (!root) return `root = Stack([${names.join(", ")}])
370
+ ${text}`;
371
+ const rawRhs = text.slice(root.rhsStart, root.end);
372
+ const lead = /^\s*/.exec(rawRhs)?.[0] ?? "";
373
+ const spliced = spliceFirstArray(rawRhs.trim(), names);
374
+ if (spliced !== null) {
375
+ return text.slice(0, root.rhsStart) + lead + spliced + text.slice(root.end);
376
+ }
377
+ const body = fresh(new Set(stmts.map((stmt) => stmt.name)), "rootBody");
378
+ const wrapped = `root = Stack([${body}, ${names.join(", ")}])
379
+ ${body} = ${rawRhs.trim()}`;
380
+ return text.slice(0, root.start) + wrapped + text.slice(root.end);
381
+ }
382
+ function spliceFirstArray(rhs, names) {
383
+ const match = /^([A-Za-z_]\w*)\s*\(/.exec(rhs);
384
+ if (!match || match[1] !== "Stack" && match[1] !== "Card") return null;
385
+ const openParen = match[0].length - 1;
386
+ let paren = 0;
387
+ let quote = false;
388
+ let arrayStart = -1;
389
+ let arrayDepth = 0;
390
+ let arrayEnd = -1;
391
+ for (let i = openParen; i < rhs.length; i += 1) {
392
+ const ch = rhs[i];
393
+ if (quote) {
394
+ if (ch === "\\") i += 1;
395
+ else if (ch === '"') quote = false;
396
+ continue;
397
+ }
398
+ if (ch === '"') quote = true;
399
+ else if (ch === "(") paren += 1;
400
+ else if (ch === ")") {
401
+ paren -= 1;
402
+ if (paren === 0) break;
403
+ } else if (ch === "[") {
404
+ if (paren === 1 && arrayStart < 0) {
405
+ arrayStart = i;
406
+ arrayDepth = 1;
407
+ } else if (arrayStart >= 0 && arrayEnd < 0) {
408
+ arrayDepth += 1;
409
+ }
410
+ } else if (ch === "]" && arrayStart >= 0 && arrayEnd < 0) {
411
+ arrayDepth -= 1;
412
+ if (arrayDepth === 0) arrayEnd = i;
413
+ }
414
+ }
415
+ if (arrayStart < 0 || arrayEnd < 0) return null;
416
+ const inner = rhs.slice(arrayStart + 1, arrayEnd).trim();
417
+ const merged = inner === "" ? names.join(", ") : `${inner.replace(/,\s*$/, "")}, ${names.join(", ")}`;
418
+ return rhs.slice(0, arrayStart + 1) + merged + rhs.slice(arrayEnd);
419
+ }
420
+ function referencedByOther(name, stmts) {
421
+ const pattern = new RegExp(
422
+ `(?<![A-Za-z0-9_$.])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_])`
20
423
  );
424
+ return stmts.some((stmt) => stmt.name !== name && pattern.test(stripStrings(stmt.rhs)));
425
+ }
426
+ function calleeOf(rhs) {
427
+ const match = /^([A-Za-z_]\w*)\s*\(/.exec(rhs.trim());
428
+ return match ? match[1] : null;
429
+ }
430
+ function stripStrings(text) {
431
+ return text.replace(/"(?:\\.|[^"\\])*"/g, '""');
432
+ }
433
+ function fresh(taken, base) {
434
+ let name = base;
435
+ let n = 2;
436
+ while (taken.has(name)) {
437
+ name = `${base}${n}`;
438
+ n += 1;
439
+ }
440
+ taken.add(name);
441
+ return name;
442
+ }
443
+ function applyEdits(source, edits) {
444
+ const ordered = [...edits].sort((a, b) => b.start - a.start);
445
+ let text = source;
446
+ for (const edit of ordered) {
447
+ text = text.slice(0, edit.start) + edit.text + text.slice(edit.end);
448
+ }
449
+ return text;
450
+ }
451
+ function parseStatements(source) {
452
+ const lines = source.split("\n");
453
+ const stmts = [];
454
+ let offset = 0;
455
+ let depth = 0;
456
+ let quote = false;
457
+ let current = null;
458
+ const scan2 = (line) => {
459
+ for (let i = 0; i < line.length; i += 1) {
460
+ const ch = line[i];
461
+ if (quote) {
462
+ if (ch === "\\") i += 1;
463
+ else if (ch === '"') quote = false;
464
+ } else if (ch === '"') {
465
+ quote = true;
466
+ } else if (ch === "(" || ch === "[" || ch === "{") {
467
+ depth += 1;
468
+ } else if (ch === ")" || ch === "]" || ch === "}") {
469
+ depth = Math.max(0, depth - 1);
470
+ }
471
+ }
472
+ };
473
+ for (let li = 0; li < lines.length; li += 1) {
474
+ const line = lines[li];
475
+ const lineStart = offset;
476
+ const lineEnd = offset + line.length;
477
+ offset = lineEnd + (li < lines.length - 1 ? 1 : 0);
478
+ if (current && depth > 0) {
479
+ scan2(line);
480
+ if (depth === 0) {
481
+ stmts.push({
482
+ name: current.name,
483
+ rhs: source.slice(current.rhsStart, lineEnd).trim(),
484
+ start: current.start,
485
+ end: lineEnd,
486
+ rhsStart: current.rhsStart
487
+ });
488
+ current = null;
489
+ }
490
+ continue;
491
+ }
492
+ const trimmed = line.trim();
493
+ if (trimmed === "" || trimmed.startsWith("```")) continue;
494
+ const match = /^(\s*)(\$?[A-Za-z_]\w*)\s*=/.exec(line);
495
+ if (!match) return null;
496
+ const eq = line.indexOf("=");
497
+ const rhsStart = lineStart + eq + 1;
498
+ depth = 0;
499
+ quote = false;
500
+ scan2(line);
501
+ if (depth === 0) {
502
+ stmts.push({
503
+ name: match[2],
504
+ rhs: line.slice(eq + 1).trim(),
505
+ start: lineStart,
506
+ end: lineEnd,
507
+ rhsStart
508
+ });
509
+ } else {
510
+ current = { name: match[2], start: lineStart, rhsStart };
511
+ }
512
+ }
513
+ if (current || depth !== 0) return null;
514
+ return stmts;
515
+ }
516
+
517
+ // src/utils/split-response.ts
518
+ var STATEMENT_START = /^\$?[A-Za-z_]\w*\s*=/;
519
+ var PENDING_IDENTIFIER = /^\s*\$?[A-Za-z_]\w*\s*$/;
520
+ function scan(line, state) {
521
+ for (let i = 0; i < line.length; i++) {
522
+ const ch = line[i];
523
+ if (state.quote) {
524
+ if (ch === "\\") i++;
525
+ else if (ch === state.quote) state.quote = null;
526
+ } else if (ch === '"') {
527
+ state.quote = ch;
528
+ } else if (ch === "(" || ch === "[" || ch === "{") {
529
+ state.depth++;
530
+ } else if (ch === ")" || ch === "]" || ch === "}") {
531
+ state.depth = Math.max(0, state.depth - 1);
532
+ }
533
+ }
534
+ state.quote = null;
535
+ }
536
+ function splitResponse(source, options = {}) {
537
+ let input = source;
538
+ if (options.streaming && !input.endsWith("\n")) {
539
+ const cut = input.lastIndexOf("\n");
540
+ if (PENDING_IDENTIFIER.test(input.slice(cut + 1))) input = input.slice(0, cut + 1);
541
+ }
542
+ const before = [];
543
+ const after = [];
544
+ const ui = [];
545
+ const state = { depth: 0, quote: null };
546
+ let text = [];
547
+ let seenUi = false;
548
+ let sawFence = false;
549
+ const flushText = () => {
550
+ const joined = text.join(" ").trim();
551
+ text = [];
552
+ if (joined) (seenUi ? after : before).push(joined);
553
+ };
554
+ for (const line of input.split(/\r?\n/)) {
555
+ const trimmed = line.trim();
556
+ if (trimmed.startsWith("```")) {
557
+ sawFence = true;
558
+ continue;
559
+ }
560
+ if (state.depth > 0) {
561
+ ui.push(line);
562
+ scan(line, state);
563
+ continue;
564
+ }
565
+ if (STATEMENT_START.test(trimmed)) {
566
+ flushText();
567
+ seenUi = true;
568
+ ui.push(line);
569
+ scan(line, state);
570
+ continue;
571
+ }
572
+ if (trimmed === "") {
573
+ flushText();
574
+ continue;
575
+ }
576
+ text.push(trimmed);
577
+ }
578
+ flushText();
579
+ return {
580
+ before,
581
+ ui: ui.join("\n"),
582
+ after,
583
+ pure: !sawFence && before.length === 0 && after.length === 0
584
+ };
585
+ }
586
+
587
+ // src/renderer/error-boundary.tsx
588
+ import { Component } from "react";
589
+ import { jsx as jsx2 } from "react/jsx-runtime";
590
+ function keysChanged(a = [], b = []) {
591
+ return a.length !== b.length || a.some((v, i) => !Object.is(v, b[i]));
592
+ }
593
+ var JuneErrorBoundary = class extends Component {
594
+ constructor() {
595
+ super(...arguments);
596
+ this.state = { error: null };
597
+ this.reset = () => {
598
+ this.setState({ error: null });
599
+ };
600
+ }
601
+ static getDerivedStateFromError(error) {
602
+ return { error };
603
+ }
604
+ componentDidCatch(error, info) {
605
+ this.props.onError?.(error, info);
606
+ }
607
+ componentDidUpdate(prev) {
608
+ if (this.state.error && keysChanged(prev.resetKeys, this.props.resetKeys)) {
609
+ this.reset();
610
+ }
611
+ }
612
+ render() {
613
+ const { error } = this.state;
614
+ if (!error) return this.props.children;
615
+ const { fallback } = this.props;
616
+ if (typeof fallback === "function") return fallback({ error, reset: this.reset });
617
+ if (fallback !== void 0) return fallback;
618
+ return /* @__PURE__ */ jsx2("div", { role: "alert", "data-june-error": "", children: "Something went wrong while rendering this UI." });
619
+ }
620
+ };
621
+
622
+ // src/renderer/renderer.tsx
623
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
624
+ function JuneRenderer(props) {
625
+ const {
626
+ library,
627
+ response,
628
+ isStreaming = false,
629
+ mixedContent = true,
630
+ renderText,
631
+ theme = "light",
632
+ themeVars,
633
+ className,
634
+ style,
635
+ actions,
636
+ confirmAction,
637
+ parseActionEvent,
638
+ onAction,
639
+ onError,
640
+ onJuneError,
641
+ loadingFallback = null,
642
+ emptyFallback = null,
643
+ errorFallback,
644
+ resetKeys,
645
+ ...rendererProps
646
+ } = props;
647
+ const latest = useRef({
648
+ actions,
649
+ confirmAction,
650
+ parseActionEvent,
651
+ onAction,
652
+ onError,
653
+ onJuneError
654
+ });
655
+ latest.current = {
656
+ actions,
657
+ confirmAction,
658
+ parseActionEvent,
659
+ onAction,
660
+ onError,
661
+ onJuneError
662
+ };
663
+ const handleAction = useCallback((event) => {
664
+ const l = latest.current;
665
+ l.onAction?.(event);
666
+ if (l.actions) {
667
+ void runAction(event, {
668
+ actions: l.actions,
669
+ parseEvent: l.parseActionEvent,
670
+ confirm: l.confirmAction,
671
+ onError: (e) => l.onJuneError?.(e)
672
+ });
673
+ }
674
+ }, []);
675
+ const handleParserErrors = useCallback((errors) => {
676
+ const list = Array.isArray(errors) ? errors : [];
677
+ const l = latest.current;
678
+ l.onError?.(list);
679
+ const real = list.filter((e) => e?.severity !== "warning");
680
+ if (real.length > 0) {
681
+ l.onJuneError?.(
682
+ new JuneError("PARSE_ERROR", `OpenUI reported ${real.length} error(s)`, { details: real })
683
+ );
684
+ }
685
+ if (list.length > 0 && !l.onError && !l.onJuneError) {
686
+ console.warn("[june-ui] OpenUI reported errors:", list);
687
+ }
688
+ }, []);
689
+ const handleBoundaryError = useCallback((error, info) => {
690
+ latest.current.onJuneError?.(
691
+ new JuneError("RENDER_ERROR", error.message, {
692
+ cause: error,
693
+ details: { componentStack: info.componentStack }
694
+ })
695
+ );
696
+ }, []);
697
+ const parts = useMemo2(
698
+ () => mixedContent && response ? splitResponse(response, { streaming: isStreaming }) : null,
699
+ [mixedContent, response, isStreaming]
700
+ );
701
+ const uiText = useMemo2(() => {
702
+ const raw = parts && !parts.pure ? parts.ui : response;
703
+ if (!mixedContent || isStreaming || typeof raw !== "string" || raw.length === 0) return raw;
704
+ return normalizeProgram(raw);
705
+ }, [parts, response, mixedContent, isStreaming]);
706
+ const hasUi = typeof uiText === "string" && uiText.trim().length > 0;
707
+ const before = parts?.before ?? [];
708
+ const after = parts?.after ?? [];
709
+ const hasText = before.length + after.length > 0;
710
+ const text = (value, index, key) => /* @__PURE__ */ jsx3(Fragment, { children: renderText ? renderText(value, index) : /* @__PURE__ */ jsx3("p", { className: "june-text", children: value }) }, key);
711
+ return /* @__PURE__ */ jsx3(JuneThemeProvider, { mode: theme, vars: themeVars, className, style, children: /* @__PURE__ */ jsxs(
712
+ JuneErrorBoundary,
713
+ {
714
+ fallback: errorFallback,
715
+ onError: handleBoundaryError,
716
+ resetKeys: resetKeys ?? [response],
717
+ children: [
718
+ before.map((t, i) => text(t, i, `b${i}`)),
719
+ hasUi && /* @__PURE__ */ jsx3(
720
+ Renderer,
721
+ {
722
+ ...rendererProps,
723
+ library,
724
+ response: uiText,
725
+ isStreaming,
726
+ onAction: handleAction,
727
+ onError: handleParserErrors
728
+ }
729
+ ),
730
+ after.map((t, i) => text(t, before.length + i, `a${i}`)),
731
+ !hasUi && !hasText && (isStreaming ? loadingFallback : emptyFallback)
732
+ ]
733
+ }
734
+ ) });
735
+ }
736
+
737
+ // src/components/library.ts
738
+ import { createLibrary, defineComponent } from "@openuidev/react-lang";
739
+ var defineJuneComponent = defineComponent;
740
+ function createJuneLibrary(options) {
741
+ const { allow, deny, ...rest } = options;
742
+ const input = rest;
743
+ const raw = input;
744
+ const components = raw.components ?? [];
745
+ const names = /* @__PURE__ */ new Set();
746
+ for (const component of components) {
747
+ if (typeof component?.name !== "string" || component.name.length === 0) {
748
+ throw new JuneError("LIBRARY_ERROR", "Every component needs a non-empty string name");
749
+ }
750
+ if (names.has(component.name)) {
751
+ throw new JuneError("LIBRARY_ERROR", `Duplicate component name "${component.name}"`);
752
+ }
753
+ names.add(component.name);
754
+ }
755
+ const allowSet = allow ? new Set(allow) : null;
756
+ const denySet = deny ? new Set(deny) : null;
757
+ if (allowSet) {
758
+ for (const n of allowSet) {
759
+ if (!names.has(n)) throw new JuneError("LIBRARY_ERROR", `Allowed component "${n}" is not registered`);
760
+ }
761
+ }
762
+ const keep = components.filter((c) => {
763
+ const n = c.name;
764
+ return (!allowSet || allowSet.has(n)) && !(denySet && denySet.has(n));
765
+ });
766
+ if (raw.root !== void 0 && !keep.some((c) => c.name === raw.root)) {
767
+ throw new JuneError("LIBRARY_ERROR", `Root component "${raw.root}" is missing after filtering`);
768
+ }
769
+ try {
770
+ const unfiltered = keep.length === components.length;
771
+ return createLibrary(
772
+ unfiltered ? input : { ...input, components: keep }
773
+ );
774
+ } catch (error) {
775
+ throw new JuneError("LIBRARY_ERROR", "OpenUI rejected the component library", { cause: error });
776
+ }
777
+ }
778
+
779
+ // src/actions/define-action.ts
780
+ function defineJuneAction(definition) {
781
+ if (typeof definition.handler !== "function") {
782
+ throw new JuneError("ACTION_VALIDATION_ERROR", "defineJuneAction requires a handler function");
783
+ }
784
+ return Object.freeze({ ...definition, __june: "action" });
785
+ }
786
+
787
+ // src/streaming/use-june-stream.ts
788
+ import { useCallback as useCallback2, useEffect, useMemo as useMemo3, useRef as useRef2, useState } from "react";
789
+
790
+ // src/streaming/stream-handler.ts
791
+ function isReadableStream(source) {
792
+ return typeof source.getReader === "function";
793
+ }
794
+ async function* iterateReadable(stream, signal) {
795
+ const reader = stream.getReader();
796
+ const onAbort = () => {
797
+ void reader.cancel().catch(() => {
798
+ });
799
+ };
800
+ signal?.addEventListener("abort", onAbort, { once: true });
801
+ try {
802
+ while (true) {
803
+ const { done, value } = await reader.read();
804
+ if (done) return;
805
+ yield value;
806
+ }
807
+ } finally {
808
+ signal?.removeEventListener("abort", onAbort);
809
+ try {
810
+ reader.releaseLock();
811
+ } catch {
812
+ }
813
+ }
814
+ }
815
+ function toAsyncIterable(source, signal) {
816
+ return isReadableStream(source) ? iterateReadable(source, signal) : source;
817
+ }
818
+ function abortedError(partial) {
819
+ return new JuneError("STREAM_ABORTED", "Stream was aborted", { details: { partial } });
820
+ }
821
+ async function consumeStream(source, options = {}) {
822
+ const { signal, onText, allowEmpty = false } = options;
823
+ if (signal?.aborted) throw abortedError("");
824
+ const decoder = new TextDecoder();
825
+ let text = "";
826
+ let received = false;
827
+ const push = (delta) => {
828
+ if (!delta) return;
829
+ received = true;
830
+ text += delta;
831
+ onText?.(delta, text);
832
+ };
833
+ try {
834
+ for await (const chunk of toAsyncIterable(source, signal)) {
835
+ if (signal?.aborted) break;
836
+ push(typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true }));
837
+ }
838
+ push(decoder.decode());
839
+ } catch (error) {
840
+ if (signal?.aborted) throw abortedError(text);
841
+ if (error instanceof JuneError) throw error;
842
+ throw new JuneError("STREAM_ERROR", "Stream was interrupted", {
843
+ cause: error,
844
+ details: { partial: text }
845
+ });
846
+ }
847
+ if (signal?.aborted) throw abortedError(text);
848
+ if (!received && !allowEmpty) {
849
+ throw new JuneError("STREAM_EMPTY", "Stream completed without any content");
850
+ }
851
+ return text;
852
+ }
853
+
854
+ // src/streaming/sse-adapter.ts
855
+ var BOUNDARY = /\r\n\r\n|\n\n|\r\r/;
856
+ function parseFrame(raw) {
857
+ let event = "message";
858
+ const data = [];
859
+ for (const line of raw.split(/\r\n|\n|\r/)) {
860
+ if (!line || line.startsWith(":")) continue;
861
+ const i = line.indexOf(":");
862
+ const field = i === -1 ? line : line.slice(0, i);
863
+ let value = i === -1 ? "" : line.slice(i + 1);
864
+ if (value.startsWith(" ")) value = value.slice(1);
865
+ if (field === "event") event = value;
866
+ else if (field === "data") data.push(value);
867
+ }
868
+ if (data.length === 0 && event === "message") return null;
869
+ return { event, data: data.join("\n") };
870
+ }
871
+ async function* sseToText(source, options = {}) {
872
+ const { extract, doneSentinel = "[DONE]", signal } = options;
873
+ const decoder = new TextDecoder();
874
+ let buffer = "";
875
+ const handle = (raw) => {
876
+ const frame = parseFrame(raw);
877
+ if (!frame) return { done: false, text: null };
878
+ if (frame.event === "error") {
879
+ throw new JuneError("STREAM_ERROR", frame.data || "Server sent an error event", {
880
+ details: frame
881
+ });
882
+ }
883
+ if (doneSentinel !== null && frame.data === doneSentinel) {
884
+ return { done: true, text: null };
885
+ }
886
+ const text = extract ? extract(frame.data, frame.event) : frame.data;
887
+ return { done: false, text: text ? text : null };
888
+ };
889
+ for await (const chunk of toAsyncIterable(source, signal)) {
890
+ buffer += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
891
+ let match;
892
+ while ((match = BOUNDARY.exec(buffer)) !== null) {
893
+ const raw = buffer.slice(0, match.index);
894
+ buffer = buffer.slice(match.index + match[0].length);
895
+ const result = handle(raw);
896
+ if (result.done) return;
897
+ if (result.text) yield result.text;
898
+ }
899
+ }
900
+ buffer += decoder.decode();
901
+ if (buffer.trim()) {
902
+ const result = handle(buffer);
903
+ if (!result.done && result.text) yield result.text;
904
+ }
905
+ }
906
+ function jsonFieldExtractor(path) {
907
+ const keys = path.split(".");
908
+ return (data) => {
909
+ try {
910
+ let current = JSON.parse(data);
911
+ for (const key of keys) {
912
+ if (current === null || typeof current !== "object") return null;
913
+ current = current[key];
914
+ }
915
+ return typeof current === "string" ? current : null;
916
+ } catch {
917
+ return null;
918
+ }
919
+ };
920
+ }
921
+
922
+ // src/streaming/fetch-adapter.ts
923
+ async function fetchStream(input, options = {}) {
924
+ const { sse, ...init } = options;
925
+ let response;
926
+ try {
927
+ response = await fetch(input, init);
928
+ } catch (error) {
929
+ if (init.signal?.aborted) {
930
+ throw new JuneError("STREAM_ABORTED", "Request was aborted", { cause: error });
931
+ }
932
+ throw new JuneError("NETWORK_ERROR", "Network request failed", { cause: error });
933
+ }
934
+ if (!response.ok) {
935
+ throw new JuneError("NETWORK_ERROR", `Request failed with status ${response.status}`, {
936
+ details: { status: response.status, statusText: response.statusText }
937
+ });
938
+ }
939
+ if (!response.body) {
940
+ throw new JuneError("STREAM_EMPTY", "Response has no body");
941
+ }
942
+ const body = response.body;
943
+ if (sse) {
944
+ return sseToText(body, {
945
+ ...sse === true ? {} : sse,
946
+ signal: init.signal ?? void 0
947
+ });
948
+ }
949
+ return body;
950
+ }
951
+
952
+ // src/streaming/use-june-stream.ts
953
+ var FLUSH_MS = 16;
954
+ var INITIAL = {
955
+ response: "",
956
+ isStreaming: false,
957
+ status: "idle",
958
+ error: null
959
+ };
960
+ function useJuneStream(options = {}) {
961
+ const [state, setState] = useState(INITIAL);
962
+ const optionsRef = useRef2(options);
963
+ optionsRef.current = options;
964
+ const controllerRef = useRef2(null);
965
+ const runIdRef = useRef2(0);
966
+ const textRef = useRef2("");
967
+ const timerRef = useRef2(null);
968
+ const clearTimer = useCallback2(() => {
969
+ if (timerRef.current !== null) {
970
+ clearTimeout(timerRef.current);
971
+ timerRef.current = null;
972
+ }
973
+ }, []);
974
+ const schedule = useCallback2(() => {
975
+ if (timerRef.current !== null) return;
976
+ timerRef.current = setTimeout(() => {
977
+ timerRef.current = null;
978
+ const text = textRef.current;
979
+ setState(
980
+ (s) => s.status !== "streaming" || s.response === text ? s : { ...s, response: text }
981
+ );
982
+ }, FLUSH_MS);
983
+ }, []);
984
+ const start = useCallback2(
985
+ async (input) => {
986
+ controllerRef.current?.abort();
987
+ const controller = new AbortController();
988
+ controllerRef.current = controller;
989
+ const runId = ++runIdRef.current;
990
+ const stale = () => runId !== runIdRef.current;
991
+ textRef.current = "";
992
+ clearTimer();
993
+ setState({ response: "", isStreaming: true, status: "streaming", error: null });
994
+ try {
995
+ const source = typeof input === "function" ? await input(controller.signal) : input;
996
+ if (stale()) return null;
997
+ const text = await consumeStream(source, {
998
+ signal: controller.signal,
999
+ allowEmpty: optionsRef.current.allowEmpty,
1000
+ onText: (_delta, full) => {
1001
+ if (stale()) return;
1002
+ textRef.current = full;
1003
+ schedule();
1004
+ }
1005
+ });
1006
+ if (stale()) return null;
1007
+ clearTimer();
1008
+ setState({ response: text, isStreaming: false, status: "done", error: null });
1009
+ optionsRef.current.onComplete?.(text);
1010
+ return text;
1011
+ } catch (caught) {
1012
+ if (stale()) return null;
1013
+ clearTimer();
1014
+ const error = toJuneError(caught, "STREAM_ERROR");
1015
+ const aborted = controller.signal.aborted || error.code === "STREAM_ABORTED";
1016
+ setState({
1017
+ response: textRef.current,
1018
+ // keep partial output
1019
+ isStreaming: false,
1020
+ status: aborted ? "aborted" : "error",
1021
+ error: aborted ? null : error
1022
+ });
1023
+ if (!aborted) optionsRef.current.onError?.(error);
1024
+ return null;
1025
+ }
1026
+ },
1027
+ [clearTimer, schedule]
1028
+ );
1029
+ const startFetch = useCallback2(
1030
+ (input, fetchOptions) => start((signal) => fetchStream(input, { ...fetchOptions, signal })),
1031
+ [start]
1032
+ );
1033
+ const abort = useCallback2(() => {
1034
+ controllerRef.current?.abort();
1035
+ }, []);
1036
+ const reset = useCallback2(() => {
1037
+ controllerRef.current?.abort();
1038
+ runIdRef.current++;
1039
+ textRef.current = "";
1040
+ clearTimer();
1041
+ setState(INITIAL);
1042
+ }, [clearTimer]);
1043
+ useEffect(
1044
+ () => () => {
1045
+ runIdRef.current++;
1046
+ controllerRef.current?.abort();
1047
+ clearTimer();
1048
+ },
1049
+ [clearTimer]
1050
+ );
1051
+ return useMemo3(
1052
+ () => ({ ...state, start, startFetch, abort, reset }),
1053
+ [state, start, startFetch, abort, reset]
1054
+ );
1055
+ }
1056
+
1057
+ // src/core/JuneUI.tsx
1058
+ import { openuiLibrary } from "@openuidev/react-ui";
1059
+ import { jsx as jsx4 } from "react/jsx-runtime";
1060
+ function JuneUI({ library = openuiLibrary, ...props }) {
1061
+ return /* @__PURE__ */ jsx4(JuneRenderer, { library, ...props });
21
1062
  }
22
1063
  export {
23
- JuneUI
1064
+ JuneError,
1065
+ JuneErrorBoundary,
1066
+ JuneRenderer,
1067
+ JuneThemeProvider,
1068
+ JuneUI,
1069
+ consumeStream,
1070
+ createActionHandler,
1071
+ createJuneLibrary,
1072
+ darkTheme,
1073
+ defaultParseActionEvent,
1074
+ defineJuneAction,
1075
+ defineJuneComponent,
1076
+ fetchStream,
1077
+ isJuneError,
1078
+ jsonFieldExtractor,
1079
+ lightTheme,
1080
+ runAction,
1081
+ sseToText,
1082
+ toJuneError,
1083
+ useJuneStream,
1084
+ useJuneTheme,
1085
+ useResolvedTheme
24
1086
  };