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.
- package/README.md +85 -29
- package/SKIPPED.md +9 -6
- package/dist/controls/SkiaLabel.d.ts +2 -0
- package/dist/controls/SkiaLabel.js +9 -0
- package/dist/core/Canvas.d.ts +26 -1
- package/dist/core/Canvas.js +100 -3
- package/dist/core/Gestures.d.ts +28 -1
- package/dist/core/Gestures.js +27 -1
- package/dist/core/SkiaControl.d.ts +17 -1
- package/dist/core/SkiaControl.js +30 -0
- package/dist/core/Types.d.ts +7 -2
- package/dist/react/SkiaShell.js +5 -2
- package/dist/react/index.d.ts +13 -3
- package/dist/react/index.js +16 -1
- package/dist/vite/index.d.ts +74 -0
- package/dist/vite/index.js +204 -0
- package/package.json +31 -4
- package/PARITY.md +0 -287
package/dist/react/SkiaShell.js
CHANGED
|
@@ -440,9 +440,12 @@ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBa
|
|
|
440
440
|
}
|
|
441
441
|
})();
|
|
442
442
|
}
|
|
443
|
-
else
|
|
444
|
-
// forward
|
|
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; });
|
package/dist/react/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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";
|
package/dist/react/index.js
CHANGED
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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.
|
|
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": [
|
|
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": [
|
|
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",
|