drawnui-react 0.1.0-preview.1 → 0.1.0-preview.11
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 +98 -29
- package/SKIPPED.md +23 -16
- package/dist/controls/DrawnGame.d.ts +37 -0
- package/dist/controls/DrawnGame.js +60 -0
- package/dist/controls/SkiaButton.d.ts +4 -1
- package/dist/controls/SkiaButton.js +20 -7
- package/dist/controls/SkiaCarousel.d.ts +6 -0
- package/dist/controls/SkiaCarousel.js +7 -1
- package/dist/controls/SkiaCheckbox.d.ts +0 -1
- package/dist/controls/SkiaCheckbox.js +0 -6
- package/dist/controls/SkiaEditor.js +8 -0
- package/dist/controls/SkiaLabel.d.ts +2 -0
- package/dist/controls/SkiaLabel.js +9 -0
- package/dist/controls/SkiaLayout.d.ts +10 -0
- package/dist/controls/SkiaLayout.js +64 -1
- package/dist/controls/SkiaRadioButton.js +1 -2
- package/dist/controls/SkiaScroll.d.ts +9 -0
- package/dist/controls/SkiaScroll.js +120 -13
- package/dist/controls/SkiaScrollBar.d.ts +27 -0
- package/dist/controls/SkiaScrollBar.js +51 -2
- package/dist/controls/SkiaShaderCarousel.d.ts +5 -0
- package/dist/controls/SkiaShaderCarousel.js +5 -0
- package/dist/controls/SkiaSwitch.d.ts +0 -1
- package/dist/controls/SkiaSwitch.js +0 -6
- package/dist/controls/SkiaToggle.d.ts +18 -4
- package/dist/controls/SkiaToggle.js +45 -3
- package/dist/core/Animators.d.ts +9 -0
- package/dist/core/Animators.js +26 -1
- package/dist/core/Canvas.d.ts +82 -4
- package/dist/core/Canvas.js +261 -10
- package/dist/core/Gestures.d.ts +47 -1
- package/dist/core/Gestures.js +43 -1
- package/dist/core/SkiaControl.d.ts +38 -2
- package/dist/core/SkiaControl.js +76 -3
- package/dist/core/Styles.d.ts +51 -0
- package/dist/core/Styles.js +92 -0
- package/dist/core/Super.d.ts +10 -1
- package/dist/core/Super.js +20 -3
- package/dist/core/Types.d.ts +7 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/react/SkiaShell.js +5 -2
- package/dist/react/index.d.ts +22 -4
- package/dist/react/index.js +25 -4
- package/dist/react/reconciler.js +2 -0
- package/dist/vite/index.d.ts +74 -0
- package/dist/vite/index.js +204 -0
- package/package.json +35 -4
- package/PARITY.md +0 -287
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> & {
|
|
@@ -92,19 +100,29 @@ export declare const SkiaDrawer: FC<LayoutProps<SkiaDrawerCtrl>>;
|
|
|
92
100
|
export interface CanvasProps {
|
|
93
101
|
BackgroundColor?: Color;
|
|
94
102
|
RenderingMode?: RenderingModeType;
|
|
95
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Disabled (default) / Enabled / Lock, like DrawnUi Canvas.Gestures. Enabled shares input with the page like MAUI's
|
|
105
|
+
* Enabled inside a native scroll view: a touch pan along an axis the page can scroll scrolls the page, and the wheel
|
|
106
|
+
* scrolls it unless a control used the wheel. A page that cannot scroll keeps every touch. Lock keeps all input.
|
|
107
|
+
*/
|
|
96
108
|
Gestures?: GesturesMode;
|
|
97
109
|
children?: ReactNode;
|
|
98
110
|
style?: CSSProperties;
|
|
99
111
|
className?: string;
|
|
100
112
|
/** Receives the engine Canvas (FPS, FrameTime, RenderingScale...). */
|
|
101
113
|
ref?: Ref<CanvasView>;
|
|
114
|
+
/** Context-menu request (right click / long press / Menu key) no control handled; return true to suppress the browser menu. */
|
|
115
|
+
ContextMenu?: CanvasView["ContextMenu"];
|
|
116
|
+
/** DrawnView.WillFirstTimeDraw: once, right before the first frame is drawn, with its drawing context. */
|
|
117
|
+
WillFirstTimeDraw?: CanvasView["WillFirstTimeDraw"];
|
|
118
|
+
/** DrawnView.WasDrawn: after every drawn frame. */
|
|
119
|
+
WasDrawn?: CanvasView["WasDrawn"];
|
|
102
120
|
}
|
|
103
121
|
/**
|
|
104
122
|
* Mirrors DrawnUi Canvas: the bridge between the DOM (react-dom) and the drawn tree (DrawnUi reconciler).
|
|
105
123
|
* Requires Super.UseDrawnUi()...BuildAsync() to have completed.
|
|
106
124
|
*/
|
|
107
|
-
export declare function Canvas({ BackgroundColor, RenderingMode, Gestures, children, style, className, ref: viewRef }: CanvasProps): import("react").JSX.Element;
|
|
125
|
+
export declare function Canvas({ BackgroundColor, RenderingMode, Gestures, children, style, className, ref: viewRef, ContextMenu, WillFirstTimeDraw, WasDrawn }: CanvasProps): import("react").JSX.Element;
|
|
108
126
|
export type { SkiaControl };
|
|
109
127
|
export * from "../index";
|
|
110
128
|
export * from "./SkiaShell";
|
package/dist/react/index.js
CHANGED
|
@@ -49,22 +49,40 @@ 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, WillFirstTimeDraw, WasDrawn }) {
|
|
57
70
|
const ref = useRef(null);
|
|
58
71
|
const view = useRef(null);
|
|
59
72
|
const root = useRef(null);
|
|
60
73
|
const [engine, setEngine] = useState(null);
|
|
61
74
|
useLayoutEffect(() => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
// the engine draws frame 1 inside its constructor: the draw events and the rendering mode must be on it before that
|
|
76
|
+
const v = new CanvasView(ref.current, (c) => {
|
|
77
|
+
if (RenderingMode)
|
|
78
|
+
c.RenderingMode = RenderingMode;
|
|
79
|
+
c.WillFirstTimeDraw = WillFirstTimeDraw;
|
|
80
|
+
c.WasDrawn = WasDrawn;
|
|
81
|
+
});
|
|
65
82
|
view.current = v;
|
|
66
83
|
root.current = createDrawnRoot(v);
|
|
67
84
|
setEngine(v);
|
|
85
|
+
removeStaticContent(); // the constructor drew frame 1: the crawlable block (drawnUiStatic) is duplicate content from here on
|
|
68
86
|
return () => { root.current?.unmount(); v.Dispose(); view.current = null; root.current = null; setEngine(null); };
|
|
69
87
|
// RenderingMode is read once at surface creation, like DrawnUi.
|
|
70
88
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -78,6 +96,9 @@ export function Canvas({ BackgroundColor, RenderingMode, Gestures, children, sty
|
|
|
78
96
|
v.Update();
|
|
79
97
|
}
|
|
80
98
|
v.Gestures = Gestures ?? "Disabled";
|
|
99
|
+
v.ContextMenu = ContextMenu;
|
|
100
|
+
v.WillFirstTimeDraw = WillFirstTimeDraw;
|
|
101
|
+
v.WasDrawn = WasDrawn;
|
|
81
102
|
root.current.render(children);
|
|
82
103
|
});
|
|
83
104
|
// The drawn surface is aria-hidden; the overlay is the accessibility tree (DrawnUi.Blazor pattern).
|
package/dist/react/reconciler.js
CHANGED
|
@@ -85,6 +85,8 @@ const hostConfig = {
|
|
|
85
85
|
if (!ctor)
|
|
86
86
|
throw new Error(`DrawnUi: unknown control <${type}>`);
|
|
87
87
|
const inst = new ctor();
|
|
88
|
+
// ConfigureStyles defaults first, the JSX props below override them (a TextSpan is not a control, it has none)
|
|
89
|
+
inst.ApplyInitialStyles?.(true);
|
|
88
90
|
applyProps(inst, null, props);
|
|
89
91
|
return inst;
|
|
90
92
|
},
|
|
@@ -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.11",
|
|
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,42 @@
|
|
|
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": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"vite": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
44
74
|
},
|
|
45
75
|
"dependencies": {
|
|
46
76
|
"canvaskit-wasm": "^0.42.0",
|
|
@@ -52,6 +82,7 @@
|
|
|
52
82
|
"@types/react-dom": "^19.2.0",
|
|
53
83
|
"@types/react-reconciler": "^0.32.0",
|
|
54
84
|
"@vitejs/plugin-react": "^5.0.0",
|
|
85
|
+
"playwright-core": "^1.62.1",
|
|
55
86
|
"react": "^19.2.0",
|
|
56
87
|
"react-dom": "^19.2.0",
|
|
57
88
|
"typescript": "^5.9.0",
|