drawnui-react 0.1.0-preview.1 → 0.1.0-preview.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.
@@ -440,9 +440,12 @@ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBa
440
440
  }
441
441
  })();
442
442
  }
443
- else if (depth > history.current.length) {
444
- // forward: the hash names the pages, rebuild the stack without animation (overlays cannot be restored)
443
+ else {
444
+ // forward (depth grew), or the same depth with another hash = a plain <a href="#/route"> link or a typed hash:
445
+ // the hash names the pages, rebuild the stack without animation (overlays cannot be restored)
445
446
  const target = parseHash();
447
+ if (depth === history.current.length && target.join("/") === live.current.stack.join("/"))
448
+ return;
446
449
  history.current = target.map(() => "page");
447
450
  window.history.replaceState({ [HISTORY_KEY]: history.current.length }, "", hashFor(target));
448
451
  setStacks((all) => { const next = all.slice(); next[live.current.selectedTab] = target; return next; });
@@ -31,9 +31,17 @@ import { SkiaScroll as SkiaScrollCtrl } from "../controls/SkiaScroll";
31
31
  import type { SkiaShape as SkiaShapeCtrl } from "../controls/SkiaShape";
32
32
  import type { Color, RenderingModeType } from "../core/Types";
33
33
  import type { GesturesMode } from "../core/Gestures";
34
- /** Public settable properties of a control become its JSX props, same PascalCase names as C#. */
34
+ /**
35
+ * Public settable properties of a control become its JSX props, same PascalCase names as C#.
36
+ *
37
+ * Function-typed members split in two: a METHOD (`Update(): void`) is not a prop and is dropped,
38
+ * while an EVENT is declared optional (`Tapped?: (sender, e) => void`) and is exactly the thing a
39
+ * caller passes in JSX. Optionality is the discriminator; without it every handler was missing
40
+ * from the props type, so `<SkiaButton Tapped={…} />` worked at runtime but failed to typecheck
41
+ * and never appeared in completion.
42
+ */
35
43
  type PropsOf<T> = Partial<{
36
- [K in keyof T as T[K] extends Function ? never : K extends "Children" | "Views" | "Parent" | "Spans" | "GridStructure" | "AccessibilityId" | "IsAccessibilityElement" | "HasTransform" | "RenderTransformMatrix" | "RenderObjectPrevious" | "LastMeasuredIndex" | "ItemsInsertedAtStart" | "UsingControlStyle" | "Track" | "Thumb" | "FrameOn" | "FrameOff" | "ViewCheckOn" | "ViewOn" | "ViewText" | "Ratio" | "StartThumbX" | "EndThumbX" | "SnapPoints" | "CurrentPosition" | "CurrentSnap" | "ContentOffsetBounds" | "InTransition" | "CanAnimate" | "MaxIndex" | "ChildrenTotal" | "IsAtStart" | "IsAtEnd" | "ScrollProgress" | "ScrollAmount" | "TransitionProgress" | "LastIndex" | "ChildrenCount" | "Horizontal" | "Animator" | "Animation" | "IsPlaying" | "PlayWhenAvailable" | "TotalFrames" | "HasEffects" | "IsDisposed" | "Label" | "IsMultiline" | "HasSelection" | "LayoutVersion" | "LinesCount" | "MeasuredLineHeight" | "Focused" | "SpriteSheet" | "FrameWidth" | "FrameHeight" | "DurationMs" | "FrameDurationMs" | "CurrentSprite" | "Rects" | "HasTapHandler" | "HasDecorations" | "LinesCount" | "Superview" | "DrawingRect" | "MeasuredSize" | "RenderingScale" | "NeedMeasure" | "_superview" | "HitBoxAuto" | "TotalDown" | "TotalTapped" | "TouchDown" | "PostAnimators" | "LoadedSource" | "IsLoading" | "DisplayRect" | "AspectScale" | "Content" | "ContentSize" | "ContentOffsetBounds" | "OverscrollDistance" | "OverScrolled" | "IsUserPanning" | "IsUserFocused" | "IsScrolling" | "IsTemplated" | "FirstVisibleIndex" | "LastVisibleIndex" | "DebugString" | "ChildrenFactory" | "ContextIndex" | "RenderObject" | "UsingCacheType" | "TransitionEffect" | "TransitionFromIndex" | "TransitionToIndex" | "EffectPostRenderers" | "CachedImage" | "ImageBitmap" | "ShouldSubmitOnEnter" | "Header" | "Footer" | "RefreshIndicator" | "ScrollBar" | "ScrollBarHorizontal" | "IsRunning" | "VisibleRatio" | "CurrentIndex" | "LastCompositeRecord" | "DirtyChildrenInternal" | "IsRenderingWithComposition" ? never : K]: T[K];
44
+ [K in keyof T as T[K] extends Function ? (undefined extends T[K] ? K : never) : K extends "Children" | "Views" | "Parent" | "Spans" | "GridStructure" | "AccessibilityId" | "IsAccessibilityElement" | "HasTransform" | "RenderTransformMatrix" | "RenderObjectPrevious" | "LastMeasuredIndex" | "ItemsInsertedAtStart" | "UsingControlStyle" | "Track" | "Thumb" | "FrameOn" | "FrameOff" | "ViewCheckOn" | "ViewOn" | "ViewText" | "Ratio" | "StartThumbX" | "EndThumbX" | "SnapPoints" | "CurrentPosition" | "CurrentSnap" | "ContentOffsetBounds" | "InTransition" | "CanAnimate" | "MaxIndex" | "ChildrenTotal" | "IsAtStart" | "IsAtEnd" | "ScrollProgress" | "ScrollAmount" | "TransitionProgress" | "LastIndex" | "ChildrenCount" | "Horizontal" | "Animator" | "Animation" | "IsPlaying" | "PlayWhenAvailable" | "TotalFrames" | "HasEffects" | "IsDisposed" | "Label" | "IsMultiline" | "HasSelection" | "LayoutVersion" | "LinesCount" | "MeasuredLineHeight" | "Focused" | "SpriteSheet" | "FrameWidth" | "FrameHeight" | "DurationMs" | "FrameDurationMs" | "CurrentSprite" | "Rects" | "HasTapHandler" | "HasDecorations" | "LinesCount" | "Superview" | "DrawingRect" | "MeasuredSize" | "RenderingScale" | "NeedMeasure" | "_superview" | "HitBoxAuto" | "TotalDown" | "TotalTapped" | "TouchDown" | "PostAnimators" | "LoadedSource" | "IsLoading" | "DisplayRect" | "AspectScale" | "Content" | "ContentSize" | "ContentOffsetBounds" | "OverscrollDistance" | "OverScrolled" | "IsUserPanning" | "IsUserFocused" | "IsScrolling" | "IsTemplated" | "FirstVisibleIndex" | "LastVisibleIndex" | "DebugString" | "ChildrenFactory" | "ContextIndex" | "RenderObject" | "UsingCacheType" | "TransitionEffect" | "TransitionFromIndex" | "TransitionToIndex" | "EffectPostRenderers" | "CachedImage" | "ImageBitmap" | "ShouldSubmitOnEnter" | "Header" | "Footer" | "RefreshIndicator" | "ScrollBar" | "ScrollBarHorizontal" | "IsRunning" | "VisibleRatio" | "CurrentIndex" | "LastCompositeRecord" | "DirtyChildrenInternal" | "IsRenderingWithComposition" ? never : K]: T[K];
37
45
  }>;
38
46
  /** `ref` receives the engine control instance (react-reconciler getPublicInstance). */
39
47
  type LeafProps<T> = PropsOf<T> & {
@@ -99,12 +107,14 @@ export interface CanvasProps {
99
107
  className?: string;
100
108
  /** Receives the engine Canvas (FPS, FrameTime, RenderingScale...). */
101
109
  ref?: Ref<CanvasView>;
110
+ /** Context-menu request (right click / long press / Menu key) no control handled; return true to suppress the browser menu. */
111
+ ContextMenu?: CanvasView["ContextMenu"];
102
112
  }
103
113
  /**
104
114
  * Mirrors DrawnUi Canvas: the bridge between the DOM (react-dom) and the drawn tree (DrawnUi reconciler).
105
115
  * Requires Super.UseDrawnUi()...BuildAsync() to have completed.
106
116
  */
107
- export declare function Canvas({ BackgroundColor, RenderingMode, Gestures, children, style, className, ref: viewRef }: CanvasProps): import("react").JSX.Element;
117
+ export declare function Canvas({ BackgroundColor, RenderingMode, Gestures, children, style, className, ref: viewRef, ContextMenu }: CanvasProps): import("react").JSX.Element;
108
118
  export type { SkiaControl };
109
119
  export * from "../index";
110
120
  export * from "./SkiaShell";
@@ -49,11 +49,24 @@ export const SkiaCarousel = "SkiaCarousel";
49
49
  /** Carousel whose slide changes are rendered by an SkSL transition (C# SkiaShaderCarousel); slides need UseCache="Image". */
50
50
  export const SkiaShaderCarousel = "SkiaShaderCarousel";
51
51
  export const SkiaDrawer = "SkiaDrawer";
52
+ /**
53
+ * One-shot, at the first Canvas mount: removes the HTML the `drawnUiStatic` Vite plugin generated for crawlers
54
+ * (`[data-drawnui-static]`, placed after the mount element). A querySelectorAll once per page, never again; an
55
+ * app built without the plugin finds nothing.
56
+ */
57
+ let staticRemoved = false;
58
+ function removeStaticContent() {
59
+ if (staticRemoved || typeof document === "undefined")
60
+ return;
61
+ staticRemoved = true;
62
+ for (const el of document.querySelectorAll("[data-drawnui-static]"))
63
+ el.remove();
64
+ }
52
65
  /**
53
66
  * Mirrors DrawnUi Canvas: the bridge between the DOM (react-dom) and the drawn tree (DrawnUi reconciler).
54
67
  * Requires Super.UseDrawnUi()...BuildAsync() to have completed.
55
68
  */
56
- export function Canvas({ BackgroundColor, RenderingMode, Gestures, children, style, className, ref: viewRef }) {
69
+ export function Canvas({ BackgroundColor, RenderingMode, Gestures, children, style, className, ref: viewRef, ContextMenu }) {
57
70
  const ref = useRef(null);
58
71
  const view = useRef(null);
59
72
  const root = useRef(null);
@@ -65,6 +78,7 @@ export function Canvas({ BackgroundColor, RenderingMode, Gestures, children, sty
65
78
  view.current = v;
66
79
  root.current = createDrawnRoot(v);
67
80
  setEngine(v);
81
+ removeStaticContent(); // the constructor drew frame 1: the crawlable block (drawnUiStatic) is duplicate content from here on
68
82
  return () => { root.current?.unmount(); v.Dispose(); view.current = null; root.current = null; setEngine(null); };
69
83
  // RenderingMode is read once at surface creation, like DrawnUi.
70
84
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -78,6 +92,7 @@ export function Canvas({ BackgroundColor, RenderingMode, Gestures, children, sty
78
92
  v.Update();
79
93
  }
80
94
  v.Gestures = Gestures ?? "Disabled";
95
+ v.ContextMenu = ContextMenu;
81
96
  root.current.render(children);
82
97
  });
83
98
  // The drawn surface is aria-hidden; the overlay is the accessibility tree (DrawnUi.Blazor pattern).
@@ -0,0 +1,74 @@
1
+ import type { Plugin } from "vite";
2
+ export interface DrawnUiStaticOptions {
3
+ /**
4
+ * Pages to generate, as URL hashes / paths appended to the site root ("" = the root page, "#/shapes"...).
5
+ * Default: the root page plus every page a button on the root page navigates to (discovered by clicking it
6
+ * in the headless browser and reading the new location).
7
+ */
8
+ routes?: string[];
9
+ /** Id of the element the app mounts into; the generated HTML is placed right after it (or at `<!-- drawnui-static -->` if present). Default "root". */
10
+ container?: string;
11
+ /** Chrome executable. Default: the installed Google Chrome (playwright-core `channel: "chrome"`), or $CHROME_PATH. */
12
+ executablePath?: string;
13
+ /** Milliseconds the accessibility tree must stay unchanged before a page is read (the engine rebuilds it at most every 300 ms). Default 800. */
14
+ settleMs?: number;
15
+ /** Text of the generated top heading. Default: the document <title>. */
16
+ heading?: string;
17
+ /** Look of the block: `style` (CSS, default DEFAULT_STATIC_STYLE, `false` = none). */
18
+ render?: RenderStaticOptions;
19
+ }
20
+ /** One accessible control as read from the running app (the engine's accessibility snapshot, exposed as DOM). */
21
+ export interface StaticNode {
22
+ Role: string;
23
+ Label: string;
24
+ Hint?: string;
25
+ Href?: string;
26
+ }
27
+ /**
28
+ * Generates visible, semantic HTML for crawlers and AI agents from the app's own accessibility tree, at build time.
29
+ *
30
+ * After `vite build` it serves the build (`vite preview`), opens it in headless Chrome, lets the engine measure,
31
+ * arrange and draw, reads the accessibility snapshot of every page (root + the pages its buttons open, or the
32
+ * `routes` you list) and writes ordinary HTML into `dist/index.html`: a heading becomes a heading, a button becomes
33
+ * a link to the page it opened, a label becomes a paragraph, a form control becomes a list item naming it. The
34
+ * overlay's own markup (transparent text over an `aria-hidden` canvas) is deliberately NOT copied — in a static
35
+ * file that reads as hidden text; this output is plain flow content, nothing hidden, nothing collapsed.
36
+ *
37
+ * Placement: right AFTER the mount element (`#root` by default), or wherever you put `<!-- drawnui-static -->` in
38
+ * index.html. With the usual layout (the mount element filling the viewport) it sits below the fold: a visitor
39
+ * sees the app area loading, the text is a scroll away and genuinely visible. Removal: the first `<Canvas>` that
40
+ * mounts removes every `[data-drawnui-static]` element once its engine has drawn frame 1 (one querySelectorAll,
41
+ * nothing in the frame loop; an app built without the plugin finds nothing). If the app never boots it stays.
42
+ * Removing it only shrinks the document below the fold; the mount element keeps its size, so nothing above the
43
+ * fold moves — except that a page which scrolled only because of the block loses its scrollbar (classic scrollbars
44
+ * on Windows/Linux then give the canvas those ~15 px back — set `html { scrollbar-gutter: stable }` so the
45
+ * canvas has the same width before and after boot; the demo does). If the mount element is a box inside a longer page, your own content below it moves
46
+ * up by the block's height when it goes; place the marker comment at the end of the body to avoid that.
47
+ *
48
+ * Two things to know:
49
+ * - Verify with `curl` or view-source, not DevTools after boot: the app removed it.
50
+ * - Crawlers that do not run JavaScript (GPTBot, ClaudeBot, CCBot, curl, link previews) read this HTML.
51
+ * Googlebot renders JavaScript: it sees the booted page, i.e. the accessibility overlay (roles, labels, text).
52
+ * Both come from the same accessibility tree, so they cannot drift — but a control without an
53
+ * `AccessibilityRole` is invisible to both.
54
+ *
55
+ * Needs `playwright-core` (dev dependency of your app) and a Chrome to drive; GitHub's ubuntu runners have one.
56
+ * Usage: `plugins: [react(), drawnUiStatic()]`.
57
+ */
58
+ export declare function drawnUiStatic(options?: DrawnUiStaticOptions): Plugin;
59
+ /**
60
+ * Default look of the generated block, shipped inline so every app gets a calm "loading its contents" page out of
61
+ * the box. Everything inherits the page's font and text color (set `body { color }` for a dark site); surfaces are
62
+ * translucent so they read on light and dark backgrounds. Override by styling `.drawnui-static` (your stylesheet
63
+ * comes later in the document and wins), or pass `style: false` to ship no default style at all.
64
+ */
65
+ export declare const DEFAULT_STATIC_STYLE = "\n.drawnui-static{max-width:820px;margin:0 auto;padding:32px 24px 48px;line-height:1.5;color:inherit}\n.drawnui-static h1{font-size:28px;font-weight:600;margin:0 0 4px}\n.drawnui-static h2{font-size:20px;font-weight:600;margin:28px 0 8px}\n.drawnui-static h3{font-size:16px;font-weight:600;margin:16px 0 4px;opacity:.9}\n.drawnui-static p{margin:0 0 8px;opacity:.85}\n.drawnui-static ul{list-style:none;margin:0;padding:0;display:grid;gap:8px}\n.drawnui-static li{padding:10px 14px;border-radius:8px;background:rgba(128,128,128,.14)}\n.drawnui-static li a{color:inherit;text-decoration:none;display:block}\n.drawnui-static li b{font-weight:600;margin-right:6px}\n";
66
+ export interface RenderStaticOptions {
67
+ /** CSS placed before the block; `false` ships none. Default DEFAULT_STATIC_STYLE. */
68
+ style?: string | false;
69
+ }
70
+ /**
71
+ * Accessibility nodes -> visible semantic HTML: one <section> per page (id = route), roles map to ordinary elements.
72
+ * Everything is open flow content; the block and its <style> carry data-drawnui-static so the app can remove them.
73
+ */
74
+ export declare function renderStatic(heading: string, pages: Map<string, StaticNode[]>, options?: RenderStaticOptions): string;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Vite helpers for DrawnUi.React apps: `import { drawnUiStatic } from "drawnui-react/vite"`.
3
+ * Build-time only; the runtime bundle never imports this module and the frame loop knows nothing about it.
4
+ */
5
+ import { readFileSync, writeFileSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ /**
8
+ * Generates visible, semantic HTML for crawlers and AI agents from the app's own accessibility tree, at build time.
9
+ *
10
+ * After `vite build` it serves the build (`vite preview`), opens it in headless Chrome, lets the engine measure,
11
+ * arrange and draw, reads the accessibility snapshot of every page (root + the pages its buttons open, or the
12
+ * `routes` you list) and writes ordinary HTML into `dist/index.html`: a heading becomes a heading, a button becomes
13
+ * a link to the page it opened, a label becomes a paragraph, a form control becomes a list item naming it. The
14
+ * overlay's own markup (transparent text over an `aria-hidden` canvas) is deliberately NOT copied — in a static
15
+ * file that reads as hidden text; this output is plain flow content, nothing hidden, nothing collapsed.
16
+ *
17
+ * Placement: right AFTER the mount element (`#root` by default), or wherever you put `<!-- drawnui-static -->` in
18
+ * index.html. With the usual layout (the mount element filling the viewport) it sits below the fold: a visitor
19
+ * sees the app area loading, the text is a scroll away and genuinely visible. Removal: the first `<Canvas>` that
20
+ * mounts removes every `[data-drawnui-static]` element once its engine has drawn frame 1 (one querySelectorAll,
21
+ * nothing in the frame loop; an app built without the plugin finds nothing). If the app never boots it stays.
22
+ * Removing it only shrinks the document below the fold; the mount element keeps its size, so nothing above the
23
+ * fold moves — except that a page which scrolled only because of the block loses its scrollbar (classic scrollbars
24
+ * on Windows/Linux then give the canvas those ~15 px back — set `html { scrollbar-gutter: stable }` so the
25
+ * canvas has the same width before and after boot; the demo does). If the mount element is a box inside a longer page, your own content below it moves
26
+ * up by the block's height when it goes; place the marker comment at the end of the body to avoid that.
27
+ *
28
+ * Two things to know:
29
+ * - Verify with `curl` or view-source, not DevTools after boot: the app removed it.
30
+ * - Crawlers that do not run JavaScript (GPTBot, ClaudeBot, CCBot, curl, link previews) read this HTML.
31
+ * Googlebot renders JavaScript: it sees the booted page, i.e. the accessibility overlay (roles, labels, text).
32
+ * Both come from the same accessibility tree, so they cannot drift — but a control without an
33
+ * `AccessibilityRole` is invisible to both.
34
+ *
35
+ * Needs `playwright-core` (dev dependency of your app) and a Chrome to drive; GitHub's ubuntu runners have one.
36
+ * Usage: `plugins: [react(), drawnUiStatic()]`.
37
+ */
38
+ export function drawnUiStatic(options = {}) {
39
+ const container = options.container ?? "root";
40
+ const settleMs = options.settleMs ?? 800;
41
+ let config;
42
+ return {
43
+ name: "drawnui-static",
44
+ apply: "build",
45
+ configResolved(c) { config = c; },
46
+ async closeBundle() {
47
+ const file = resolve(config.root, config.build.outDir, "index.html");
48
+ const html = readFileSync(file, "utf8");
49
+ const marker = "<!-- drawnui-static -->";
50
+ const slot = html.includes(marker) ? marker : new RegExp(`<div\\b[^>]*\\bid="${container}"[^>]*>\\s*</div>`).exec(html)?.[0];
51
+ if (!slot) {
52
+ config.logger.warn(`drawnui-static: neither ${marker} nor an empty <div id="${container}"> in ${file}, nothing generated`);
53
+ return;
54
+ }
55
+ const { preview } = await import("vite");
56
+ const server = await preview({ configFile: config.configFile, root: config.root, mode: config.mode, logLevel: "warn", preview: { port: 0, strictPort: false, open: false } });
57
+ const origin = server.resolvedUrls?.local[0]?.replace(/\/$/, "") ?? "";
58
+ const base = config.base.replace(/\/$/, "");
59
+ const { chromium } = await import("playwright-core");
60
+ const browser = await chromium.launch(options.executablePath ?? process.env.CHROME_PATH ? { executablePath: options.executablePath ?? process.env.CHROME_PATH } : { channel: "chrome" });
61
+ try {
62
+ const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
63
+ const errors = [];
64
+ page.on("pageerror", (e) => errors.push(String(e)));
65
+ const url = (route) => `${origin}${base}/${route}`;
66
+ // the engine rebuilds the accessibility snapshot at most every 300 ms and only after a drawn frame:
67
+ // wait until the overlay has stopped changing
68
+ const readTree = async () => {
69
+ let last = "", stableSince = Date.now(), started = Date.now();
70
+ for (;;) {
71
+ const nodes = await page.evaluate(() => [...document.querySelectorAll(".drawnui-a11y-node")].map((e) => ({
72
+ Role: e.getAttribute("role") ?? "", Label: e.getAttribute("aria-label") ?? e.textContent ?? "", Hint: e.getAttribute("title") ?? undefined,
73
+ })));
74
+ const key = JSON.stringify(nodes);
75
+ if (key !== last) {
76
+ last = key;
77
+ stableSince = Date.now();
78
+ }
79
+ else if (nodes.length && Date.now() - stableSince >= settleMs)
80
+ return nodes;
81
+ if (Date.now() - started > 20000)
82
+ return nodes;
83
+ await page.waitForTimeout(150);
84
+ }
85
+ };
86
+ await page.goto(url(""));
87
+ const root = await readTree();
88
+ const pages = new Map([["", root]]);
89
+ if (options.routes) {
90
+ for (const r of options.routes) {
91
+ if (r === "")
92
+ continue;
93
+ await page.goto(url(r));
94
+ pages.set(r, await readTree());
95
+ }
96
+ }
97
+ else {
98
+ // a button becomes a link to wherever it takes the user: click it, read the new location, come back
99
+ for (let i = 0; i < root.length; i++) {
100
+ if (root[i].Role !== "button")
101
+ continue;
102
+ await page.goto(url(""));
103
+ await readTree();
104
+ // activate through the overlay node (routed to the control as a Tapped), so buttons below the fold work too
105
+ const activated = await page.evaluate((i) => { const el = document.querySelectorAll(".drawnui-a11y-node")[i]; if (!el)
106
+ return false; el.click(); return true; }, i);
107
+ if (!activated)
108
+ continue;
109
+ await page.waitForTimeout(400);
110
+ const route = await page.evaluate(() => location.hash);
111
+ if (!route || pages.has(route))
112
+ continue;
113
+ root[i].Href = route;
114
+ pages.set(route, await readTree());
115
+ }
116
+ }
117
+ if (errors.length)
118
+ config.logger.warn(`drawnui-static: page errors while generating: ${errors.join("; ")}`);
119
+ const heading = options.heading ?? /<title>([^<]*)<\/title>/.exec(html)?.[1] ?? "";
120
+ const markup = renderStatic(heading, pages, options.render);
121
+ writeFileSync(file, html.replace(slot, () => (slot === marker ? markup : `${slot}\n${markup}`)));
122
+ config.logger.info(`drawnui-static: ${pages.size} page(s), ${[...pages.values()].reduce((n, p) => n + p.length, 0)} nodes after #${container} in ${file}`);
123
+ }
124
+ finally {
125
+ await browser.close();
126
+ await server.close();
127
+ }
128
+ },
129
+ };
130
+ }
131
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
132
+ /**
133
+ * Default look of the generated block, shipped inline so every app gets a calm "loading its contents" page out of
134
+ * the box. Everything inherits the page's font and text color (set `body { color }` for a dark site); surfaces are
135
+ * translucent so they read on light and dark backgrounds. Override by styling `.drawnui-static` (your stylesheet
136
+ * comes later in the document and wins), or pass `style: false` to ship no default style at all.
137
+ */
138
+ export const DEFAULT_STATIC_STYLE = `
139
+ .drawnui-static{max-width:820px;margin:0 auto;padding:32px 24px 48px;line-height:1.5;color:inherit}
140
+ .drawnui-static h1{font-size:28px;font-weight:600;margin:0 0 4px}
141
+ .drawnui-static h2{font-size:20px;font-weight:600;margin:28px 0 8px}
142
+ .drawnui-static h3{font-size:16px;font-weight:600;margin:16px 0 4px;opacity:.9}
143
+ .drawnui-static p{margin:0 0 8px;opacity:.85}
144
+ .drawnui-static ul{list-style:none;margin:0;padding:0;display:grid;gap:8px}
145
+ .drawnui-static li{padding:10px 14px;border-radius:8px;background:rgba(128,128,128,.14)}
146
+ .drawnui-static li a{color:inherit;text-decoration:none;display:block}
147
+ .drawnui-static li b{font-weight:600;margin-right:6px}
148
+ `;
149
+ /**
150
+ * Accessibility nodes -> visible semantic HTML: one <section> per page (id = route), roles map to ordinary elements.
151
+ * Everything is open flow content; the block and its <style> carry data-drawnui-static so the app can remove them.
152
+ */
153
+ export function renderStatic(heading, pages, options = {}) {
154
+ const style = options.style === undefined ? DEFAULT_STATIC_STYLE : options.style;
155
+ const out = [];
156
+ if (style)
157
+ out.push(`<style data-drawnui-static>${style}</style>`);
158
+ out.push(`<div class="drawnui-static" data-drawnui-static>`);
159
+ if (heading)
160
+ out.push(`<h1>${esc(heading)}</h1>`);
161
+ for (const [route, nodes] of pages) {
162
+ const titleNode = nodes.find((n) => n.Role === "heading" && n.Label.trim());
163
+ const title = titleNode?.Label.trim() ?? route.replace(/^#\/?/, "");
164
+ const id = route ? ` id="${esc(route.replace(/^#\/?/, ""))}"` : "";
165
+ out.push(`<section${id}>`);
166
+ if (route)
167
+ out.push(`<h2>${esc(title)}</h2>`);
168
+ let list = [];
169
+ const flush = () => { if (list.length) {
170
+ out.push(`<ul>${list.join("")}</ul>`);
171
+ list = [];
172
+ } };
173
+ for (const n of nodes) {
174
+ const label = n.Label.trim();
175
+ if (!label || (n === titleNode && route))
176
+ continue;
177
+ const hint = n.Hint?.trim();
178
+ switch (n.Role) {
179
+ case "heading":
180
+ flush();
181
+ out.push(route ? `<h3>${esc(label)}</h3>` : `<h2>${esc(label)}</h2>`);
182
+ break;
183
+ case "button":
184
+ case "link":
185
+ case "tab":
186
+ case "menuitem":
187
+ list.push(`<li>${n.Href ? `<a href="${esc(n.Href)}"><b>${esc(label)}</b>` : `<b>${esc(label)}</b>`}${hint ? ` ${esc(hint)}` : ""}${n.Href ? "</a>" : ""}</li>`);
188
+ break;
189
+ case "text":
190
+ case "status":
191
+ case "alert":
192
+ flush();
193
+ out.push(`<p>${esc(label)}</p>`);
194
+ break;
195
+ case "img": break;
196
+ default: list.push(`<li>${esc(label)}${hint ? ` — ${esc(hint)}` : ""} <small>(${esc(n.Role)})</small></li>`);
197
+ }
198
+ }
199
+ flush();
200
+ out.push(`</section>`);
201
+ }
202
+ out.push(`</div>`);
203
+ return out.join("\n");
204
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drawnui-react",
3
- "version": "0.1.0-preview.1",
3
+ "version": "0.1.0-preview.2",
4
4
  "description": "DrawnUi for React: the DrawnUi (.NET) drawn-UI engine in TypeScript on CanvasKit (Skia WASM). Same controls, same PascalCase API as DrawnUi.Net.",
5
5
  "license": "MIT",
6
6
  "author": "Nick Kovalsky (AppoMobi)",
@@ -12,7 +12,16 @@
12
12
  "bugs": {
13
13
  "url": "https://github.com/DrawnUi/DrawnUi.React/issues"
14
14
  },
15
- "keywords": ["drawnui", "skia", "canvaskit", "react", "canvas", "ui", "drawn-ui", "react-reconciler"],
15
+ "keywords": [
16
+ "drawnui",
17
+ "skia",
18
+ "canvaskit",
19
+ "react",
20
+ "canvas",
21
+ "ui",
22
+ "drawn-ui",
23
+ "react-reconciler"
24
+ ],
16
25
  "type": "module",
17
26
  "main": "./dist/react/index.js",
18
27
  "module": "./dist/react/index.js",
@@ -26,21 +35,38 @@
26
35
  "types": "./dist/index.d.ts",
27
36
  "import": "./dist/index.js"
28
37
  },
38
+ "./vite": {
39
+ "types": "./dist/vite/index.d.ts",
40
+ "import": "./dist/vite/index.js"
41
+ },
29
42
  "./package.json": "./package.json"
30
43
  },
31
- "files": ["dist", "README.md", "LICENSE", "SKIPPED.md", "PARITY.md"],
44
+ "files": [
45
+ "dist",
46
+ "README.md",
47
+ "LICENSE",
48
+ "SKIPPED.md"
49
+ ],
32
50
  "sideEffects": false,
33
51
  "scripts": {
34
52
  "dev": "vite samples/demo",
35
53
  "typecheck": "tsc --noEmit",
36
54
  "build": "tsc --noEmit && node dev/build-samples.mjs",
55
+ "sync:skills": "node dev/sync-skills.mjs",
56
+ "site:extras": "node dev/site-extras.mjs dist/demo",
37
57
  "build:lib": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
38
58
  "prepublishOnly": "npm run build:lib",
39
59
  "preview": "vite preview samples/demo"
40
60
  },
41
61
  "peerDependencies": {
62
+ "playwright-core": ">=1.40",
42
63
  "react": "^18.2.0 || ^19.0.0",
43
- "react-dom": "^18.2.0 || ^19.0.0"
64
+ "react-dom": "^18.2.0 || ^19.0.0",
65
+ "vite": ">=5"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "playwright-core": { "optional": true },
69
+ "vite": { "optional": true }
44
70
  },
45
71
  "dependencies": {
46
72
  "canvaskit-wasm": "^0.42.0",
@@ -52,6 +78,7 @@
52
78
  "@types/react-dom": "^19.2.0",
53
79
  "@types/react-reconciler": "^0.32.0",
54
80
  "@vitejs/plugin-react": "^5.0.0",
81
+ "playwright-core": "^1.62.1",
55
82
  "react": "^19.2.0",
56
83
  "react-dom": "^19.2.0",
57
84
  "typescript": "^5.9.0",