uxml-preview 0.1.0 → 0.2.0

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.
@@ -42,9 +42,31 @@ export interface LayoutOptions {
42
42
  */
43
43
  measureText: MeasureText;
44
44
  }
45
+ /**
46
+ * A laid-out element the file never mentioned, built by a control.
47
+ *
48
+ * Separate from `boxes` rather than sharing its keys: `NodeId` identifies
49
+ * something in the document, and these have no document to be in. Giving them
50
+ * borrowed ids would make "which node is this?" answerable with a lie, and an
51
+ * editor asking that question needs to hear "this one is not yours".
52
+ */
53
+ export interface LayoutPart {
54
+ /** Unity's name, e.g. `unity-content-viewport`. */
55
+ name: string;
56
+ /** The element whose control built it. */
57
+ owner: NodeId;
58
+ box: LayoutBox;
59
+ /** Fixed styling, with `builtin-theme` provenance on every value. */
60
+ style: ComputedStyle;
61
+ }
45
62
  export interface LayoutTree {
46
63
  /** Computed box per element, in panel coordinates. */
47
64
  boxes: ReadonlyMap<NodeId, LayoutBox>;
65
+ /**
66
+ * Parts each element's control built, outermost first. The element's own
67
+ * children were laid out inside the last one.
68
+ */
69
+ parts: ReadonlyMap<NodeId, readonly LayoutPart[]>;
48
70
  /** Elements that were laid out, parents before children. */
49
71
  painted: readonly ElementNode[];
50
72
  warnings: readonly Warning[];
@@ -60,4 +82,15 @@ export interface LayoutTree {
60
82
  * Deps/Effects: creates Yoga nodes; the returned tree owns them until disposed.
61
83
  * Requires: `loadLayoutEngine()` must have resolved.
62
84
  */
63
- export declare function layoutDocument(root: ElementNode, styles: ReadonlyMap<NodeId, ComputedStyle>, options: LayoutOptions): LayoutTree;
85
+ export declare function layoutDocument(root: ElementNode, styles: ReadonlyMap<NodeId, ComputedStyle>,
86
+ /**
87
+ * Style per control part, from the cascade.
88
+ *
89
+ * Taken rather than computed. This module used to build part styles itself
90
+ * from the registry's fixed declarations, which meant author USS could never
91
+ * reach them — `#unity-content-container { flex-direction: row }` had nowhere
92
+ * to land, because the part did not exist until after the cascade had run.
93
+ * Resolving parts alongside elements is what fixes that, and computing them
94
+ * in two places would put the second cascade back.
95
+ */
96
+ partStyles: ReadonlyMap<NodeId, ReadonlyMap<string, ComputedStyle>>, options: LayoutOptions): LayoutTree;
@@ -251,8 +251,10 @@ export interface UxmlDocument {
251
251
  * Where a computed value came from.
252
252
  *
253
253
  * Phase 8 needs this to answer "write to the inline style, or edit the rule?".
254
- * Every variant points into the model, so the editor can jump straight to the
255
- * declaration's span and rewrite exactly that text.
254
+ * Every variant *except `builtin-theme`* points into the model, so the editor
255
+ * can jump straight to the declaration's span and rewrite exactly that text.
256
+ * `builtin-theme` is the one that does not, and it says so rather than
257
+ * inventing a location — an editor must branch on it, not follow it.
256
258
  *
257
259
  * Note the inline variant indexes declarations parsed on demand from the
258
260
  * element's `style` attribute — those declarations are not stored on the node.
@@ -268,11 +270,40 @@ export type StyleOrigin = {
268
270
  sheet: number;
269
271
  item: number;
270
272
  declIndex: number;
273
+ /**
274
+ * Pseudo-class states the matched selector required, e.g. `['hover']`.
275
+ *
276
+ * Absent when the rule applies unconditionally. Present, it means the
277
+ * value is only true while those states hold — an editor showing "this
278
+ * came from line 12" without it would be telling half the truth, and a
279
+ * user would edit the base appearance while looking at the hover one.
280
+ */
281
+ states?: readonly string[];
271
282
  } | {
272
283
  kind: 'inherited';
273
284
  from: NodeId;
274
285
  origin: StyleOrigin;
275
286
  }
287
+ /**
288
+ * A control default this renderer supplies, standing in for Unity's theme
289
+ * stylesheet. **There is no file and no span**: the declaration exists in this
290
+ * library, not in the user's project.
291
+ *
292
+ * Its own variant rather than a `rule` with an invented path, because an
293
+ * editor offering "jump to the declaration" must be able to tell that there
294
+ * is nowhere to jump — pointing at a file that does not exist is worse than
295
+ * saying the value is not editable.
296
+ *
297
+ * `unityVersion` records where the value was measured. Theme values move
298
+ * between Unity versions, and when one stops matching this is the first thing
299
+ * to check.
300
+ */
301
+ | {
302
+ kind: 'builtin-theme';
303
+ selector: string;
304
+ property: string;
305
+ unityVersion: string;
306
+ }
276
307
  /** USS default, which is not always the CSS default — `flex-direction` is `column`. */
277
308
  | {
278
309
  kind: 'default';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * XML entity decoding, for the moment a stored attribute becomes a real value.
3
+ *
4
+ * The parser deliberately keeps attribute values exactly as written — that is
5
+ * what makes serialization byte-exact — so `&amp;` survives in the model. Any
6
+ * consumer that means to *use* the value rather than re-emit it has to decode
7
+ * first, and there are two: painted text, and paths handed to the host's
8
+ * resolver hooks. A `<Style src="…?a=1&amp;b=2">` reaching `resolveImport`
9
+ * undecoded gives the host `amp;b=2` to parse, which is not a thing.
10
+ */
11
+ /**
12
+ * Purpose: turn stored attribute text into the value it stands for.
13
+ * Ensures: never throws. A numeric reference outside the Unicode range
14
+ * makes `String.fromCodePoint` raise a RangeError, and one bad
15
+ * character must not take down a render (CLAUDE.md rule 6).
16
+ * Unreadable references are left as written, which also
17
+ * round-trips.
18
+ */
19
+ export declare function decodeEntities(text: string): string;
@@ -1,8 +1,20 @@
1
1
  import { ElementNode, NodeId, Warning } from '../model/types';
2
2
  import { ComputedStyle } from '../style/resolve';
3
- import { LayoutBox } from '../layout/yoga';
3
+ import { LayoutBox, LayoutPart } from '../layout/yoga';
4
4
  import { CssMapOptions } from './css-map';
5
5
  export declare const NODE_ATTRIBUTE = "data-uxml-node";
6
+ /**
7
+ * Marks an element a control built rather than one the file asked for, and
8
+ * carries Unity's name for it.
9
+ *
10
+ * Deliberately not `data-uxml-node`: these have no node. An editor that traced
11
+ * a click back through the node attribute would get an id that belongs to
12
+ * something else, and would then offer to edit the wrong element. Here it finds
13
+ * a different attribute and can say "this part is not yours to change".
14
+ */
15
+ export declare const PART_ATTRIBUTE = "data-uxml-part";
16
+ /** The element whose control built a part. Lets a click land on its owner. */
17
+ export declare const PART_OWNER_ATTRIBUTE = "data-uxml-part-owner";
6
18
  export interface PaintResult {
7
19
  warnings: Warning[];
8
20
  /** Painted element per model node, for hit-testing and for tests. */
@@ -16,4 +28,4 @@ export interface PaintOptions extends CssMapOptions {
16
28
  * Deps/Effects: replaces the container's children. The caller owns disposal.
17
29
  * Requires: `boxes` must come from a layout of this same tree.
18
30
  */
19
- export declare function paint(root: ElementNode, boxes: ReadonlyMap<NodeId, LayoutBox>, styles: ReadonlyMap<NodeId, ComputedStyle>, container: HTMLElement, options: PaintOptions): PaintResult;
31
+ export declare function paint(root: ElementNode, boxes: ReadonlyMap<NodeId, LayoutBox>, parts: ReadonlyMap<NodeId, readonly LayoutPart[]>, styles: ReadonlyMap<NodeId, ComputedStyle>, container: HTMLElement, options: PaintOptions): PaintResult;
@@ -7,20 +7,68 @@ export interface ComputedValue {
7
7
  }
8
8
  export type ComputedStyle = ReadonlyMap<string, ComputedValue>;
9
9
  export interface ResolveOptions {
10
- /** Pseudo-classes to treat as active, e.g. `new Set(['hover'])`. */
10
+ /**
11
+ * Pseudo-classes to treat as active on every element, e.g. `new Set(['hover'])`.
12
+ *
13
+ * Blunt, and kept for the case where that is what you want. Per-element
14
+ * control is `states`.
15
+ */
11
16
  activeStates?: ReadonlySet<string>;
17
+ /**
18
+ * Pseudo-classes to activate per element, keyed by USS selector.
19
+ *
20
+ * ```ts
21
+ * { '#UseButton': ['hover'], '#DropButton': ['disabled'] }
22
+ * ```
23
+ *
24
+ * States are explicit input, never mouse events: a render that depends on
25
+ * where the pointer is cannot be compared to anything twice.
26
+ *
27
+ * Keys are matched against the tree **without** any state applied, so a key
28
+ * cannot switch itself on, and `:hover` in a key is meaningless.
29
+ */
30
+ states?: Readonly<Record<string, readonly string[]>>;
12
31
  }
13
32
  export interface ResolveResult {
14
33
  styles: ReadonlyMap<NodeId, ComputedStyle>;
34
+ /**
35
+ * Style per control part, keyed by the owning element and then Unity's name
36
+ * for the part.
37
+ *
38
+ * Parts are resolved here rather than in the layout, because that is what
39
+ * lets author USS reach them: `#unity-content-container { flex-direction: row }`
40
+ * is how a real project lays out a scroll region's contents, and a part built
41
+ * after the cascade had already run could never receive it.
42
+ */
43
+ partStyles: ReadonlyMap<NodeId, ReadonlyMap<string, ComputedStyle>>;
15
44
  warnings: readonly Warning[];
16
45
  }
46
+ /**
47
+ * Where a declaration sits in the cascade's outermost comparison.
48
+ *
49
+ * Unity's control defaults are not "a stylesheet that loaded first" — they are
50
+ * a lower origin, the way a browser's user-agent sheet is, so **an author rule
51
+ * beats them regardless of specificity**. Measured, not reasoned: the
52
+ * `states-disabled` case writes `Button { margin: 0 }` (a type selector, 0,0,1)
53
+ * against the theme's `.unity-button` (a class, 0,1,0), and Unity puts the
54
+ * button at x=0. Specificity alone would have put it at x=3.
55
+ *
56
+ * This is the axis the cascade was missing. Without it the theme wins ties it
57
+ * should lose, and every value added to `theme.ts` spreads the error.
58
+ */
59
+ export declare const enum Rank {
60
+ BuiltinTheme = 0,
61
+ Author = 1
62
+ }
17
63
  /** One declaration that competed, winner or not. */
18
64
  export interface Candidate {
19
65
  property: string;
20
66
  value: string;
21
67
  origin: StyleOrigin;
68
+ /** Compared before specificity. A built-in default never beats author USS. */
69
+ rank: Rank;
22
70
  specificity: Specificity;
23
- /** Position in cascade order; higher wins a specificity tie. */
71
+ /** Position in cascade order; higher wins a specificity tie within a rank. */
24
72
  order: number;
25
73
  winner: boolean;
26
74
  }
@@ -1,7 +1,21 @@
1
- import { ElementNode, Selector } from '../model/types';
2
- export interface MatchContext {
3
- /** Parent of every node, built once per document. */
4
- parentOf: (node: ElementNode) => ElementNode | null;
1
+ import { Selector } from '../model/types';
2
+ /**
3
+ * How to interrogate whatever is being matched.
4
+ *
5
+ * Deps: every function here is read during matching, including for ancestors —
6
+ * `.panel:hover .item` asks the ancestor for its states, not the element's.
7
+ */
8
+ export interface MatchContext<T> {
9
+ /**
10
+ * The style parent, which is not always the layout parent.
11
+ *
12
+ * Measured, and it decides the shape of this whole design: a slot inside a
13
+ * ScrollView sits physically inside `unity-content-container`, yet
14
+ * `#Grid > .slot` reaches it in Unity. So an element from the file keeps its
15
+ * file parent here, and the parts a control builds are transparent to it.
16
+ * See `child-combinator-through-parts` and the 2026-08-06 decision rows.
17
+ */
18
+ parentOf: (target: T) => T | null;
5
19
  /**
6
20
  * The element `:root` refers to.
7
21
  *
@@ -11,11 +25,38 @@ export interface MatchContext {
11
25
  * keeps the usual custom-property token pattern working, since values
12
26
  * declared there inherit down the whole tree.
13
27
  */
14
- root: ElementNode;
15
- /** Pseudo-classes currently active, e.g. `hover`. Empty in a static render. */
16
- activeStates: ReadonlySet<string>;
28
+ isRoot: (target: T) => boolean;
29
+ /** Pseudo-classes active on this target, e.g. `hover`. */
30
+ statesOf: (target: T) => ReadonlySet<string>;
31
+ /** What a type selector compares against — the C# class name, not a tag. */
32
+ typeNameOf: (target: T) => string;
33
+ /** What `#name` compares against. */
34
+ idOf: (target: T) => string | undefined;
35
+ /** Written classes plus any the control adds, e.g. `unity-button`. */
36
+ classesOf: (target: T) => readonly string[];
17
37
  }
18
- export declare function matchesSelector(selector: Selector, node: ElementNode, ctx: MatchContext): boolean;
38
+ export declare function matchesSelector<T>(selector: Selector, target: T, ctx: MatchContext<T>): boolean;
39
+ /**
40
+ * Purpose: the state pseudo-classes a selector depends on, in source order.
41
+ * Ensures: `:root` is excluded — it names a position in the tree, not a
42
+ * state, and cannot be switched on or off.
43
+ *
44
+ * Used for provenance: a value from `.unity-button:hover` is only true while
45
+ * hovering, and an editor that does not know this rewrites the wrong rule.
46
+ */
47
+ export declare function statesRequiredBy(selector: Selector): string[];
48
+ /**
49
+ * Purpose: names and classes in a selector that belong to Unity, not the author.
50
+ * Ensures: returns the `unity-` prefixed fragments only.
51
+ *
52
+ * An author writing `#unity-text-input` or `.unity-scroll-view__content-container`
53
+ * is aiming at a part a control builds. If nothing matches, that is not an
54
+ * unused rule — it is a rule that cannot work, and saying nothing about it is
55
+ * the silent loss CLAUDE.md rule 6 exists to prevent.
56
+ */
57
+ export declare function unityFragmentsIn(selectors: readonly Selector[]): string[];
19
58
  /** The first unsupported fragment in a selector group, if any. */
20
59
  export declare function unsupportedFragment(selectors: readonly Selector[]): string | null;
21
- export declare function buildParentMap(root: ElementNode): Map<ElementNode, ElementNode | null>;
60
+ export declare function buildParentMap<T extends {
61
+ children: readonly T[];
62
+ }>(root: T): Map<T, T | null>;
package/package.json CHANGED
@@ -1,59 +1,86 @@
1
- {
2
- "name": "uxml-preview",
3
- "version": "0.1.0",
4
- "description": "Preview Unity UI Toolkit UXML/USS in a browser without the Unity Editor. Uses Yoga (WebAssembly), the same layout engine UI Toolkit uses: 242 of 244 element coordinates identical to Unity 6. Parses and writes files back byte-for-byte.",
5
- "keywords": [
6
- "unity", "unity3d", "ui-toolkit", "uitk", "uielements", "uxml", "uss",
7
- "yoga", "yoga-layout", "flexbox", "webassembly", "renderer", "preview",
8
- "game-ui", "typescript", "editor-tooling", "unity-editor", "css-to-uss"
9
- ],
10
- "license": "Apache-2.0",
11
- "type": "module",
12
- "main": "./dist/index.js",
13
- "module": "./dist/index.js",
14
- "types": "./dist/index.d.ts",
15
- "exports": {
16
- ".": {
17
- "types": "./dist/index.d.ts",
18
- "import": "./dist/index.js"
19
- }
20
- },
21
- "files": ["dist", "README.md", "LICENSE", "THIRD-PARTY-NOTICES.md", "CHANGELOG.md"],
22
- "sideEffects": false,
23
- "engines": { "node": ">=18" },
24
- "publishConfig": { "access": "public" },
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/ReuHomi/uxml-preview.git"
28
- },
29
- "homepage": "https://reuhomi.github.io/uxml-preview/",
30
- "bugs": { "url": "https://github.com/ReuHomi/uxml-preview/issues" },
31
- "scripts": {
32
- "dev": "vite",
33
- "build": "tsc --noEmit && vite build",
34
- "build:site": "vite build --config vite.playground.config.ts",
35
- "preview:site": "vite preview --config vite.playground.config.ts",
36
- "test": "vitest run",
37
- "test:watch": "vitest",
38
- "test:roundtrip": "vitest run tests/roundtrip",
39
- "test:golden": "vitest run tests/golden --passWithNoTests",
40
- "golden:emit": "node --experimental-strip-types --no-warnings scripts/emit-golden-cases.mjs",
41
- "golden:update": "node scripts/update-golden.mjs",
42
- "typecheck": "tsc --noEmit",
43
- "prepublishOnly": "npm run typecheck && npm run test && npm run build"
44
- },
45
- "dependencies": {
46
- "yoga-layout": "^3.2.1"
47
- },
48
- "devDependencies": {
49
- "@types/node": "^22.0.0",
50
- "jsdom": "^30.0.1",
51
- "pixelmatch": "^6.0.0",
52
- "pngjs": "^7.0.0",
53
- "typescript": "^5.6.0",
54
- "vite": "^6.0.0",
55
- "vite-plugin-dts": "^4.3.0",
56
- "vitest": "^2.1.0"
57
- },
58
- "packageManager": "pnpm@9.12.0"
59
- }
1
+ {
2
+ "name": "uxml-preview",
3
+ "version": "0.2.0",
4
+ "description": "Preview Unity UI Toolkit UXML/USS in a browser without the Unity Editor. Uses Yoga (WebAssembly), the same layout engine UI Toolkit uses: 242 of 244 element coordinates identical to Unity 6. Parses and writes files back byte-for-byte.",
5
+ "keywords": [
6
+ "unity",
7
+ "unity3d",
8
+ "ui-toolkit",
9
+ "uitk",
10
+ "uielements",
11
+ "uxml",
12
+ "uss",
13
+ "yoga",
14
+ "yoga-layout",
15
+ "flexbox",
16
+ "webassembly",
17
+ "renderer",
18
+ "preview",
19
+ "game-ui",
20
+ "typescript",
21
+ "editor-tooling",
22
+ "unity-editor",
23
+ "css-to-uss"
24
+ ],
25
+ "license": "Apache-2.0",
26
+ "type": "module",
27
+ "main": "./dist/index.js",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "README.md",
39
+ "LICENSE",
40
+ "THIRD-PARTY-NOTICES.md",
41
+ "CHANGELOG.md"
42
+ ],
43
+ "sideEffects": false,
44
+ "engines": {
45
+ "node": ">=18"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/ReuHomi/uxml-preview.git"
53
+ },
54
+ "homepage": "https://reuhomi.github.io/uxml-preview/",
55
+ "bugs": {
56
+ "url": "https://github.com/ReuHomi/uxml-preview/issues"
57
+ },
58
+ "scripts": {
59
+ "dev": "vite",
60
+ "build": "tsc --noEmit && vite build",
61
+ "build:site": "vite build --config vite.playground.config.ts",
62
+ "preview:site": "vite preview --config vite.playground.config.ts",
63
+ "test": "vitest run",
64
+ "test:watch": "vitest",
65
+ "test:roundtrip": "vitest run tests/roundtrip",
66
+ "test:golden": "vitest run tests/golden --passWithNoTests",
67
+ "golden:emit": "node --experimental-strip-types --no-warnings scripts/emit-golden-cases.mjs",
68
+ "golden:update": "node scripts/update-golden.mjs",
69
+ "typecheck": "tsc --noEmit",
70
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
71
+ },
72
+ "dependencies": {
73
+ "yoga-layout": "^3.2.1"
74
+ },
75
+ "devDependencies": {
76
+ "@types/node": "^22.0.0",
77
+ "jsdom": "^30.0.1",
78
+ "pixelmatch": "^6.0.0",
79
+ "pngjs": "^7.0.0",
80
+ "typescript": "^5.6.0",
81
+ "vite": "^6.0.0",
82
+ "vite-plugin-dts": "^4.3.0",
83
+ "vitest": "^2.1.0"
84
+ },
85
+ "packageManager": "pnpm@9.12.0"
86
+ }