@homepages/template-kit 0.8.1-dev-20260718020735 → 0.9.0-dev-20260719205406

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.
Files changed (97) hide show
  1. package/CHANGELOG.md +81 -1
  2. package/README.md +2 -2
  3. package/dist/canvas-core/chrome-css.d.ts +24 -0
  4. package/dist/canvas-core/chrome-css.js +403 -0
  5. package/dist/canvas-core/chrome.d.ts +49 -0
  6. package/dist/canvas-core/chrome.js +147 -0
  7. package/dist/canvas-core/frame.d.ts +20 -0
  8. package/dist/canvas-core/frame.js +23 -0
  9. package/dist/canvas-core/hidden-tabs.d.ts +16 -0
  10. package/dist/canvas-core/hidden-tabs.js +84 -0
  11. package/dist/canvas-core/resolve-target.js +55 -0
  12. package/dist/canvas-core/scroll.d.ts +10 -0
  13. package/dist/canvas-core/scroll.js +15 -0
  14. package/dist/canvas-core/section-dom.d.ts +6 -0
  15. package/dist/canvas-core/section-dom.js +43 -0
  16. package/dist/canvas-core/srcdoc.d.ts +8 -0
  17. package/dist/canvas-core/srcdoc.js +77 -0
  18. package/dist/canvas-core/types.d.ts +76 -0
  19. package/dist/canvas-core.d.ts +9 -0
  20. package/dist/canvas-core.js +9 -0
  21. package/dist/cli/check/css.js +2 -2
  22. package/dist/cli/check/loader.js +10 -7
  23. package/dist/cli/check/stages/deps.js +1 -1
  24. package/dist/cli/check/stages/size.js +2 -2
  25. package/dist/cli/check/stages/tree.js +1 -1
  26. package/dist/cli/check/stages/validate/index.js +1 -1
  27. package/dist/cli/check/stages/validate/nav-contract.js +1 -1
  28. package/dist/cli/check/stages/validate/orchestrator.js +1 -1
  29. package/dist/cli/check/workspace.js +2 -2
  30. package/dist/cli/dev/build-css.js +12 -6
  31. package/dist/cli/dev/compose-template.js +3 -2
  32. package/dist/cli/dev/diagnostics.js +68 -0
  33. package/dist/cli/dev/discover.js +2 -2
  34. package/dist/cli/dev/index.js +12 -2
  35. package/dist/cli/dev/inspect.js +1 -1
  36. package/dist/cli/dev/island-bootstrap.js +1 -1
  37. package/dist/cli/dev/island-canvas-bootstrap.js +204 -0
  38. package/dist/cli/dev/section-page.js +39 -2
  39. package/dist/cli/dev/server.js +35 -5
  40. package/dist/cli/dev/tailwind.js +1 -1
  41. package/dist/cli/dev/tsconfig-paths-plugin.js +90 -0
  42. package/dist/cli/dev/vite-server.js +7 -1
  43. package/dist/cli/dev/workspace.js +2 -2
  44. package/dist/cli/link/index.js +1 -1
  45. package/dist/cli/link/overlay.js +1 -1
  46. package/dist/cli/new/emit.js +1 -1
  47. package/dist/cli/new/index.js +1 -1
  48. package/dist/cli/new/scaffold-assets.js +2 -2
  49. package/dist/cli/pack/collect.js +1 -1
  50. package/dist/cli/pack/guards.js +1 -1
  51. package/dist/cli/pack/index.js +1 -1
  52. package/dist/cli/theme/generate.js +1 -1
  53. package/dist/cli/theme/load-theme.js +1 -1
  54. package/dist/dev-client/assets/index-CJa9G1Na.css +1 -0
  55. package/dist/dev-client/assets/index-C_1atZs4.js +450 -0
  56. package/dist/dev-client/index.html +2 -2
  57. package/dist/islands/client-directive.d.ts +7 -0
  58. package/dist/{eslint/is-client-file.js → islands/client-directive.js} +6 -3
  59. package/dist/islands/detect.d.ts +1 -1
  60. package/dist/islands/detect.js +1 -1
  61. package/dist/islands/discover.js +1 -1
  62. package/dist/islands/esbuild-plugin.js +1 -1
  63. package/dist/islands/wrap.js +1 -1
  64. package/dist/package.js +1 -1
  65. package/dist/rules.d.ts +176 -0
  66. package/dist/rules.js +209 -0
  67. package/dist/ssr.d.ts +2 -2
  68. package/dist/ssr.js +2 -2
  69. package/dist/tokens.css +21 -0
  70. package/guide/INDEX.md +2 -2
  71. package/guide/check.md +5 -5
  72. package/guide/dev.md +82 -30
  73. package/guide/eslint.md +13 -10
  74. package/guide/llms.txt +4 -3
  75. package/guide/quickstart.md +1 -1
  76. package/guide/rules/INDEX.md +3 -2
  77. package/guide/upgrading.md +18 -2
  78. package/package.json +8 -4
  79. package/tsconfig.json +1 -1
  80. package/dist/dev-client/assets/index-DfEfrp6P.css +0 -1
  81. package/dist/dev-client/assets/index-bpMP0b0W.js +0 -50
  82. package/dist/eslint/is-client-file.d.ts +0 -4
  83. package/dist/eslint/rules/image-bare-needs-reason.js +0 -43
  84. package/dist/eslint/rules/no-client-directive-in-contract.js +0 -31
  85. package/dist/eslint/rules/no-client-runtime-in-server.js +0 -103
  86. package/dist/eslint/rules/no-css-import-from-render-path.js +0 -29
  87. package/dist/eslint/rules/no-hex.js +0 -188
  88. package/dist/eslint/rules/no-inline-style.js +0 -27
  89. package/dist/eslint/rules/no-nondeterminism.js +0 -48
  90. package/dist/eslint/rules/no-raw-element.js +0 -38
  91. package/dist/eslint/rules/props-from-schema.js +0 -37
  92. package/dist/eslint/rules/require-island-editor.js +0 -74
  93. package/dist/eslint/rules/serializable-island-props.js +0 -112
  94. package/dist/eslint/rules/slot-marker-literal.js +0 -38
  95. package/dist/eslint.d.ts +0 -6
  96. package/dist/eslint.js +0 -80
  97. package/dist/rules/registry.js +0 -16
package/CHANGELOG.md CHANGED
@@ -1,14 +1,94 @@
1
1
  # @homepages/template-kit
2
2
 
3
- ## 0.8.1-dev-20260718020735
3
+ ## 0.9.0-dev-20260719205406
4
+
5
+ ### Minor Changes
6
+
7
+ - 5030122: The authoring ESLint rules move out of `@homepages/template-kit` into the new
8
+ `@homepages/eslint-plugin-template`, which takes the kit as a peerDependency.
9
+
10
+ The kit's `./eslint` subpath is removed with no shim. Replace
11
+
12
+ import kit from "@homepages/template-kit/eslint";
13
+
14
+ with
15
+
16
+ import kit from "@homepages/eslint-plugin-template";
17
+
18
+ and add `@homepages/eslint-plugin-template` to devDependencies. Rule ids are
19
+ unchanged — the namespace is still `template-kit/`, so any rule-specific
20
+ `eslint-disable` comment and any `rules` override keep working.
21
+
22
+ The kit also gains a `./rules` subpath exposing the rule-id registry, and its
23
+ `./ssr` entry now exports the `"use client"` resolver (`isClientFile`,
24
+ `resolveImportedFile`, `resetClientFileCache`) that the plugin's rules consume
25
+ over the peer edge.
4
26
 
5
27
  ### Patch Changes
6
28
 
29
+ - 10f53a7: `template-kit dev`'s canvas hover/select ring no longer overflows the page's
30
+ x-axis when hovering a full-bleed section (e.g. a hero or header flush with
31
+ both page edges). The ring's geometry inflated the target's bounding rect by
32
+ a fixed offset on every side with no bound, so a rect already touching the
33
+ document edge pushed the ring 2px past it, growing the iframe's horizontal
34
+ scroll extent. The ring box is now clamped to `[0, containerWidth]`.
35
+ - f3f6d70: `template-kit check` no longer fails on a tsconfig `paths` alias (e.g.
36
+ `"@/platform/*": ["../platform/*"]`) or on a section whose contract files
37
+ share a module large enough for esbuild to code-split. Previously the
38
+ loader's css-stub plugin decided "external vs. bundle" from the specifier's
39
+ own text, so a `paths`-resolved alias got re-emitted as a literal bare
40
+ import Node couldn't resolve; separately, the loader's build applied
41
+ `publicPath` to code-split `chunk-*.js` cross-imports the same way it does
42
+ to real asset files, producing an import path Node couldn't resolve either.
43
+ Both are now decided from the actual resolved file, and `publicPath` is no
44
+ longer set on the loader's build.
45
+ - 1c2c2db: `template-kit dev`'s preview server no longer 500s on `/runtime/base.css`.
46
+ The route resolved the file at a path the build never emits to
47
+ (`dist/css/base.css`; the build copies it flat to `dist/base.css`), so every
48
+ section rendered unstyled — Tailwind-derived colors and layout were silently
49
+ missing everywhere, only arbitrary-value element geometry survived. The
50
+ route now checks the built (flat) path first and falls back to the source
51
+ (`src/css/`) layout, so it resolves correctly both from an installed kit and
52
+ when running the CLI unbuilt via `tsx` during kit development.
53
+ - 989d23e: `template-kit dev` gains an optional `--diagnostics-cmd` flag: point it at any
54
+ command that prints `{"violations": [...]}` JSON for a `--template`/`--section`
55
+ pair, and the standalone section preview
56
+ (`/sections/<template>/<section>/<fixture>`) shows a dismissible banner when it
57
+ reports violations. Unset by default — no behavior change for a workspace that
58
+ doesn't configure it.
59
+ - e1d5792: `template-kit dev`'s preview server now compiles a template's theme-color
60
+ Tailwind utilities (`bg-primary`, `text-ink`, `bg-background`, etc.) instead
61
+ of silently dropping them. `assembleTemplateCss` used to concatenate the
62
+ compiled theme (`compileThemeToCss`, the `@theme inline` alias layer plus
63
+ its `:root` custom props) onto Tailwind's CLI output _after_ the CLI had
64
+ already run, so the theme's color tokens were never part of the build graph
65
+ Tailwind scanned — `@theme` outside that graph is inert, and silently so, so
66
+ any utility built on a template color never got generated. No error, no
67
+ warning: every section rendered with correct layout but default
68
+ black-on-white text and backgrounds. The theme partial is now written to the
69
+ dev cache and `@import`ed into the Tailwind entry ahead of the section
70
+ `@source` scans, mirroring how the registry's own publish pipeline
71
+ (`ci/build-css.ts`) has always assembled it. `fontFaces` is still prepended
72
+ to the final bundle separately, since a nested `@import url(...)` doesn't
73
+ hoist out of an imported partial.
74
+ - 2e36277: Escape `>` and `&` as well as `<` in the dev server's island map, matching the escaping used by published pages.
7
75
  - 1c4c7dd: Island hydration is now nesting-safe: `hydrateIslands` skips any island marker
8
76
  nested inside another marker, so an island can render another island without the
9
77
  child being hydrated twice (once by its parent root, once independently). Enables
10
78
  interactive islands (accordion, card-slider, carousel) to wrap existing leaf
11
79
  islands (expandable-text) on published pages.
80
+ - ff1d6de: `template-kit dev`'s canvas mirror now shows the Islands inspector panel for a
81
+ section-level selection, not just a slot selection. A `fill-parent`/`fill-self`
82
+ island with no owning slot of its own only ever gets its shield dropped by a
83
+ section-level click, so the panel previously could show it as `shielded` but
84
+ never `interactive`. Existing slot-selection behavior (edit panel + Islands
85
+ list) is unchanged.
86
+ - 41d0e88: `template-kit dev` now honors a workspace's tsconfig `paths` when resolving
87
+ imports, the same way `check` already does via esbuild's native tsconfig
88
+ lookup. Previously the dev server's Vite instance had no `paths` awareness at
89
+ all, so a workspace mapping (e.g. `"@/lib/*": ["../lib/*"]`) that made `check`
90
+ pass left `dev` 500ing with `Cannot find module`. A workspace with no `paths`
91
+ mapping is unaffected.
12
92
 
13
93
  ## 0.8.0
14
94
 
package/README.md CHANGED
@@ -17,7 +17,7 @@ The fastest way to start is the workspace scaffolder — it creates a workspace
17
17
  working starter template and the full toolchain already wired:
18
18
 
19
19
  ```bash
20
- npm create homepages-workspace <name>
20
+ npm create @homepages/workspace <name>
21
21
  cd <name>
22
22
  npm install
23
23
  npm run dev # preview the starter; the island hydrates
@@ -97,7 +97,7 @@ Full contract: **[guide/primitives.md](guide/primitives.md)**.
97
97
  | `@homepages/template-kit/browser` | Browser helpers for published pages (site config, signed ingest) |
98
98
  | `@homepages/template-kit/styles.css` | The global Tailwind preamble: the breakpoint ladder, the spacing base unit, the motion defaults, kit source registration |
99
99
  | `@homepages/template-kit/base.css` | Cascade-layer order, the three reset gaps Tailwind's preflight leaves, `.tr-section`, and the motion tokens |
100
- | `@homepages/template-kit/eslint` | ESLint flat-config preset |
100
+ | `@homepages/eslint-plugin-template` | ESLint flat-config preset (its own package) |
101
101
  | `@homepages/template-kit/tsconfig` | tsconfig preset (`"extends"`) |
102
102
 
103
103
  One specifier for everything you write a section against. The exceptions are
@@ -0,0 +1,24 @@
1
+ import { MarkerAttrs } from "./types.js";
2
+ //#region src/canvas-core/chrome-css.d.ts
3
+ /**
4
+ * Custom properties the chrome CSS reads but deliberately does not declare —
5
+ * the host must set them on the iframe document's root element. This is the
6
+ * host's obligation, not a suggestion: an adapter that skips one leaves the
7
+ * declared inline fallback in force, which renders plausibly and silently
8
+ * wrong rather than failing.
9
+ *
10
+ * - `--we-panel-w`: the floating panel's measured width, mirrored onto the
11
+ * iframe root so the page card's right padding clears it. Unset, the card
12
+ * lays out against the 436px base-width fallback and drifts out of alignment
13
+ * whenever the real panel is any other width.
14
+ * - `--we-content`: the host's interior scaling unit, mirrored in so hidden-tab
15
+ * chrome scales with the rest of the UI. Unset, it pins to the 1px fallback
16
+ * and the tabs stay laptop-sized while every surrounding surface scales.
17
+ *
18
+ * Anything added here must actually be read by the kernel; the token test
19
+ * fails on a stale entry.
20
+ */
21
+ declare const HOST_INJECTED_PROPERTIES: readonly ["--we-panel-w", "--we-content"];
22
+ declare function buildChromeCss(markers: MarkerAttrs): string;
23
+ //#endregion
24
+ export { HOST_INJECTED_PROPERTIES, buildChromeCss };
@@ -0,0 +1,403 @@
1
+ import { TAB_TUCK } from "./hidden-tabs.js";
2
+
3
+ //#region src/canvas-core/chrome-css.ts
4
+ /**
5
+ * Custom properties the chrome CSS reads but deliberately does not declare —
6
+ * the host must set them on the iframe document's root element. This is the
7
+ * host's obligation, not a suggestion: an adapter that skips one leaves the
8
+ * declared inline fallback in force, which renders plausibly and silently
9
+ * wrong rather than failing.
10
+ *
11
+ * - `--we-panel-w`: the floating panel's measured width, mirrored onto the
12
+ * iframe root so the page card's right padding clears it. Unset, the card
13
+ * lays out against the 436px base-width fallback and drifts out of alignment
14
+ * whenever the real panel is any other width.
15
+ * - `--we-content`: the host's interior scaling unit, mirrored in so hidden-tab
16
+ * chrome scales with the rest of the UI. Unset, it pins to the 1px fallback
17
+ * and the tabs stay laptop-sized while every surrounding surface scales.
18
+ *
19
+ * Anything added here must actually be read by the kernel; the token test
20
+ * fails on a stale entry.
21
+ */
22
+ const HOST_INJECTED_PROPERTIES = ["--we-panel-w", "--we-content"];
23
+ function buildChromeCss(markers) {
24
+ return `
25
+ html, body { background: transparent; }
26
+ body {
27
+ --we-page-radius: 14px;
28
+ margin: 0;
29
+ /* Padding aligns the page card with the floating panel and centers
30
+ it in the visible canvas area. Top/bottom match the panel's
31
+ top-5/bottom-5 inset (20px). Left is 20px. The right constant
32
+ (40px) covers the panel's right-5 inset (20px) plus an equal
33
+ 20px gap, giving symmetric breathing room on both sides of the
34
+ card between the canvas's left edge and the panel's left edge.
35
+ Fallback 436px matches the panel's base CSS width. */
36
+ padding: 20px calc(var(--we-panel-w, 436px) + 40px) 20px 20px;
37
+ /* Body is the container for the auto-fit calc on #we-zoom. With
38
+ container-type: inline-size, descendants resolve 100cqw to the
39
+ body's content-box width — exactly the width we want the card
40
+ to scale into. */
41
+ container-type: inline-size;
42
+ position: relative;
43
+ display: flex;
44
+ justify-content: center;
45
+ align-items: flex-start;
46
+ min-height: 100%;
47
+ overflow: auto;
48
+ color: #14180F;
49
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
50
+ /* Dotted canvas surface. background-attachment: local makes the grid
51
+ scroll together with the page card so the canvas + website read as
52
+ one scene being panned, not a card sliding over a fixed grid. */
53
+ background-image: radial-gradient(rgba(20, 18, 13, 0.13) 1px, transparent 1.2px);
54
+ background-size: 20px 20px;
55
+ background-position: 10px 10px;
56
+ background-attachment: local;
57
+ }
58
+ /* Hide the native scrollbar on whichever element ends up being the
59
+ scroll container. Some browsers route the scroll to the root
60
+ element (html) rather than body, so cover both. Wheel / trackpad /
61
+ keyboard and programmatic scrollTo still work. */
62
+ html, body { scrollbar-width: none; -ms-overflow-style: none; }
63
+ html::-webkit-scrollbar,
64
+ body::-webkit-scrollbar { width: 0; height: 0; display: none; }
65
+ #we-zoom {
66
+ /* Auto-fit + manual zoom, declarative.
67
+ Two-slope formula: below ~1472px canvas (≤1536px viewport) slope 1
68
+ (0.8×) keeps the card at 80% of canvas width — the original
69
+ breathing-room factor. Above that breakpoint slope 2 (0.214×+0.600)
70
+ takes over, growing sub-linearly so the percentage of canvas the
71
+ card occupies shrinks on wider monitors while absolute pixel width
72
+ keeps growing. At a 1920px monitor this yields ~12.5% smaller than
73
+ the old native-1440px cap, restoring the birds-eye view. The outer
74
+ min(1,...) still prevents upscaling beyond native width.
75
+
76
+ --user-zoom is the manual multiplier the toolbar writes (1.0 = at
77
+ fit / "100%"). The browser recomputes on every resize — no host JS
78
+ needed.
79
+
80
+ Scale via the zoom property, NOT transform: scale(). A transform
81
+ only shrinks the PAINT — the layout box keeps the full unscaled
82
+ 1440px footprint, so the body (scroll container) reserves space the
83
+ shrunken page no longer fills and the canvas scrolls far past the
84
+ bottom. zoom scales the layout box too, so scrollHeight tracks the
85
+ visible page exactly. getBoundingClientRect() reports visual coords
86
+ under either, so overlay-ring + scroll-into-view math in
87
+ iframe-canvas.tsx is unaffected; flex justify-content:center
88
+ re-centers the smaller box, so no transform-origin is needed. */
89
+ --user-zoom: 1;
90
+ --fit-zoom: min(0.9, min(
91
+ calc(0.72 * 100cqw / 1440px),
92
+ calc(0.1926 * 100cqw / 1440px + 0.540)
93
+ ));
94
+ zoom: calc(var(--fit-zoom) * var(--user-zoom));
95
+ transition: zoom 150ms cubic-bezier(0.4, 0, 0.2, 1);
96
+ flex-shrink: 0;
97
+ /* Positioned + stacked above #we-htabs (z-index:1) so the page's
98
+ drop-shadow falls over the hidden-tab's inner edge, nestling it against
99
+ the frame. #we-chrome-overlay (z-index:100) stays above. */
100
+ position: relative;
101
+ z-index: 2;
102
+ }
103
+ #we-page {
104
+ width: 1440px;
105
+ overflow: hidden;
106
+ border-radius: var(--we-page-radius);
107
+ background: #f5f3ee;
108
+ padding: 0;
109
+ box-shadow:
110
+ 0 1px 0 rgba(20,18,13,0.06),
111
+ 0 2px 6px -2px rgba(20,18,13,0.10),
112
+ 0 12px 28px -8px rgba(20,18,13,0.18),
113
+ 0 32px 64px -20px rgba(20,18,13,0.28);
114
+ }
115
+
116
+ /* Section wrapper -- structure only. No selection chrome here;
117
+ rings are painted by #we-chrome-overlay (see below). Zero margin
118
+ + padding means sections butt edge-to-edge, matching publisher. */
119
+ [${markers.sectionInstanceId}] {
120
+ position: relative;
121
+ margin: 0;
122
+ padding: 0;
123
+ cursor: pointer;
124
+ display: grid;
125
+ grid-template-rows: 1fr;
126
+ /* Reveal transition — governs the hidden→visible direction because
127
+ CSS uses the destination state's transition property. Spring easing
128
+ gives a snappy entrance. */
129
+ transition:
130
+ grid-template-rows 200ms cubic-bezier(0.16, 1, 0.3, 1),
131
+ opacity 180ms cubic-bezier(0.16, 1, 0.3, 1);
132
+ }
133
+
134
+ /* Section title pill -- lives in #we-chrome-overlay above the rings
135
+ so it straddles the top border without being covered. Sections
136
+ only; slots get the outline ring but no on-canvas label. */
137
+ .we-chrome-label {
138
+ position: absolute;
139
+ pointer-events: none;
140
+ z-index: 1;
141
+ background: rgba(229,184,50,0.95);
142
+ color: #282c32;
143
+ padding: 3px 8px;
144
+ border-radius: 4px;
145
+ font-size: 10.5px;
146
+ font-weight: 600;
147
+ line-height: 1;
148
+ letter-spacing: 0.02em;
149
+ text-transform: uppercase;
150
+ white-space: nowrap;
151
+ opacity: 0;
152
+ transform: translateY(calc(-50% - 4px)) scale(0.96);
153
+ transition:
154
+ opacity 60ms ease-in,
155
+ transform 60ms ease-in;
156
+ }
157
+ .we-chrome-label[data-active="true"] {
158
+ opacity: 1;
159
+ transform: translateY(-50%) scale(1);
160
+ transition:
161
+ opacity 130ms cubic-bezier(0.16, 1, 0.3, 1) 50ms,
162
+ transform 130ms cubic-bezier(0.16, 1, 0.3, 1) 50ms;
163
+ }
164
+
165
+ /* Hidden state -- collapse the section out of the page so it reflows shut.
166
+ The 0-height wrapper stays in flow as the anchor for the section's
167
+ floating edge tab (built in #we-htabs by iframe-canvas.tsx). See
168
+ docs/superpowers/specs/2026-06-02-hidden-section-edge-tabs-design.md. */
169
+ [${markers.sectionInstanceId}][data-hidden="true"] {
170
+ grid-template-rows: 0fr;
171
+ opacity: 0;
172
+ pointer-events: none;
173
+ /* Collapse transition — governs the visible→hidden direction.
174
+ Faster + accelerating so the hide feels decisive. */
175
+ transition:
176
+ grid-template-rows 150ms cubic-bezier(0.4, 0, 1, 1),
177
+ opacity 120ms cubic-bezier(0.4, 0, 1, 1);
178
+ }
179
+
180
+ /* Slot wrapper -- structure only. No outline here; the overlay
181
+ ring handles slot hover + selection too. */
182
+ [${markers.slotId}] {
183
+ position: relative;
184
+ cursor: pointer;
185
+ }
186
+
187
+ /* Slot-group container -- the gaps between grouped members (dividers,
188
+ padding) carry no slot id, so give the group container itself
189
+ the pointer cursor: the whole group reads as one clickable unit, and
190
+ the overlay ring outlines this container (see iframe-canvas.tsx). */
191
+ [${markers.slotGroup}],
192
+ [${markers.slotItem}] {
193
+ position: relative;
194
+ cursor: pointer;
195
+ }
196
+
197
+ /* Per-section body holds the API HTML.
198
+ 1. position:relative gives the body a positioning context for any
199
+ body-scoped chrome or future body-only effects.
200
+ 2. isolation:isolate SEALS the section's z-index hierarchy
201
+ inside this stacking context. Without it, any positioned
202
+ descendant with z-index > 0 (the header's sticky+z-50, a
203
+ dropdown's z-40, a sticky CTA) creates a stacking context
204
+ that escapes upward. Sections never need to know the
205
+ editor chrome exists -- internal z-index is unconstrained. */
206
+ /* overflow:hidden clips section content as the grid row collapses;
207
+ min-height:0 lets the grid child shrink below its content size.
208
+ Constraint: overflow:hidden is permanent (not just during animation), so
209
+ section content must not overflow its section-body boundary — sticky
210
+ elements, absolutely-positioned dropdowns, or shadows that bleed outside
211
+ the section edge will be silently clipped. Template sections are
212
+ self-contained blocks; this is safe in practice. */
213
+ .we-section-body { position: relative; isolation: isolate; overflow: hidden; min-height: 0; }
214
+
215
+ /* Chrome overlay -- sibling of #we-zoom inside the iframe body.
216
+ Sized to body's padding box; rings inside are absolutely
217
+ positioned by JS using getBoundingClientRect + window.scrollY/X.
218
+ Lives OUTSIDE #we-page so rings ride above the page's rounded
219
+ clip and any section's overflow:hidden. pointer-events:none so
220
+ clicks pass through to the section/slot underneath -- the
221
+ click resolver remains on doc.body. */
222
+ #we-chrome-overlay {
223
+ position: absolute;
224
+ inset: 0;
225
+ pointer-events: none;
226
+ z-index: 100;
227
+ }
228
+ .we-chrome-ring {
229
+ position: absolute;
230
+ pointer-events: none;
231
+ border: 2px solid transparent;
232
+ border-radius: 6px;
233
+ opacity: 0;
234
+ box-sizing: border-box;
235
+ }
236
+ /* Hover ring -- instant on cursor arrival, fades out on leave.
237
+ Position updates instantly (no top/left transition) so dragging
238
+ across elements never flickers. Ink stroke + white drop-shadow
239
+ halo (same trick as crop handles in globals.css) keeps the ring
240
+ readable on both light sections and dark ones like footer. */
241
+ .we-chrome-ring--hover {
242
+ border-color: rgba(15,42,34,0.85);
243
+ filter: drop-shadow(0 0 1px rgba(255,255,255,0.95));
244
+ transition: opacity 70ms ease-in;
245
+ }
246
+ .we-chrome-ring--hover[data-active="true"] {
247
+ opacity: 1;
248
+ transition: opacity 100ms cubic-bezier(0.16, 1, 0.3, 1);
249
+ }
250
+ /* Selection ring -- appears instantly (continuous with hover),
251
+ then morphs ink → gold. No position transition so resize
252
+ tracking stays tight. Same ink + white halo as hover. */
253
+ .we-chrome-ring--selection {
254
+ border-color: rgba(15,42,34,0.85);
255
+ filter: drop-shadow(0 0 1px rgba(255,255,255,0.95));
256
+ transform: scale(1);
257
+ box-shadow: none;
258
+ transition:
259
+ opacity 50ms ease-in,
260
+ border-color 160ms cubic-bezier(0.2, 0, 0, 1),
261
+ box-shadow 160ms cubic-bezier(0.2, 0, 0, 1);
262
+ }
263
+ .we-chrome-ring--selection[data-active="true"] {
264
+ opacity: 1;
265
+ /* Instant show on select; only border + glow animate */
266
+ transition:
267
+ opacity 0ms,
268
+ border-color 180ms cubic-bezier(0.2, 0, 0, 1),
269
+ box-shadow 180ms cubic-bezier(0.2, 0, 0, 1);
270
+ }
271
+ .we-chrome-ring--selection[data-kind="section"],
272
+ .we-chrome-ring--selection[data-kind="slot"] {
273
+ border-color: #E5B832;
274
+ filter: drop-shadow(0 0 1px rgba(255,255,255,0.95));
275
+ box-shadow: 0 0 0 1px rgba(229,184,50,0.25), 0 8px 24px -10px rgba(229,184,50,0.35);
276
+ }
277
+
278
+ @media (prefers-reduced-motion: reduce) {
279
+ .we-chrome-ring,
280
+ .we-chrome-label,
281
+ [${markers.sectionInstanceId}] {
282
+ transition: none !important;
283
+ }
284
+ }
285
+
286
+ /* Hidden-section edge tabs -------------------------------------
287
+ One dim chip-tab per hidden section, pinned to the page's left edge at
288
+ the collapsed wrapper's reflow seam. Built + positioned by
289
+ iframe-canvas.tsx (positionHiddenTabs); de-collided via placeTabs. Lives
290
+ in its own layer (sibling of #we-zoom) so it rides with page scroll and
291
+ stays a constant size at any zoom — like the rings. Colors are literals
292
+ (the iframe doesn't inherit dashboard Tailwind tokens). */
293
+ #we-htabs {
294
+ position: absolute;
295
+ inset: 0;
296
+ pointer-events: none;
297
+ z-index: 1;
298
+ }
299
+ .we-htab {
300
+ position: absolute;
301
+ transform: translateY(-50%);
302
+ display: flex;
303
+ align-items: center;
304
+ gap: calc(9 * var(--we-content, 1px));
305
+ height: calc(34 * var(--we-content, 1px));
306
+ padding: 0 calc(${12 + 16} * var(--we-content, 1px)) 0 calc(13 * var(--we-content, 1px));
307
+ background: #ffffff;
308
+ border: 1px solid #ebebe6;
309
+ border-radius: calc(9 * var(--we-content, 1px)) 0 0 calc(9 * var(--we-content, 1px));
310
+ box-shadow: 0 10px 24px -12px rgba(20,18,13,0.34);
311
+ color: #4a4842;
312
+ font: inherit;
313
+ cursor: pointer;
314
+ opacity: 1;
315
+ pointer-events: auto;
316
+ transition: transform 0.2s cubic-bezier(0.2,0.7,0.3,1),
317
+ border-color 0.18s, box-shadow 0.18s;
318
+ }
319
+ .we-htab-eye {
320
+ display: grid;
321
+ place-items: center;
322
+ flex: 0 0 auto;
323
+ color: #8a877e;
324
+ transition: color 0.18s;
325
+ }
326
+ .we-htab-eye svg { width: calc(14 * var(--we-content, 1px)); height: calc(14 * var(--we-content, 1px)); }
327
+ .we-htab-name {
328
+ font-size: calc(13 * var(--we-content, 1px));
329
+ font-weight: 600;
330
+ letter-spacing: -0.01em;
331
+ color: #14180F;
332
+ white-space: nowrap;
333
+ }
334
+ .we-htab:hover {
335
+ transform: translateY(-50%) scale(1.07);
336
+ border-color: rgba(15,42,34,0.5);
337
+ box-shadow: 0 13px 30px -12px rgba(15,42,34,0.45);
338
+ }
339
+ .we-htab:hover .we-htab-eye { color: #0F2A22; }
340
+ .we-htab:focus-visible {
341
+ outline: none;
342
+ transform: translateY(-50%) scale(1.07);
343
+ border-color: #0F2A22;
344
+ box-shadow: 0 0 0 3px #F3F6EF, 0 13px 30px -12px rgba(15,42,34,0.45);
345
+ }
346
+ /* Hover/focus swaps the eye-off glyph to the eye glyph in place — no width
347
+ change, no sliding label. Only one glyph is in flow at a time, so the eye
348
+ slot's grid keeps the shown glyph centered. */
349
+ .we-htab-eye-on { display: none; }
350
+ .we-htab:hover .we-htab-eye-off,
351
+ .we-htab:focus-visible .we-htab-eye-off { display: none; }
352
+ .we-htab:hover .we-htab-eye-on,
353
+ .we-htab:focus-visible .we-htab-eye-on { display: block; }
354
+
355
+ /* Render-error placeholder ------------------------------------- */
356
+ .we-render-error {
357
+ margin: 8px 0;
358
+ padding: 24px 16px;
359
+ border-radius: 6px;
360
+ border: 1px dashed #ebebe6;
361
+ background: #ffffff;
362
+ color: #8a877e;
363
+ font-size: 12.5px;
364
+ }
365
+ .we-render-error__title {
366
+ margin: 0;
367
+ color: #4a4842;
368
+ font-weight: 500;
369
+ }
370
+ .we-render-error__detail {
371
+ margin: 4px 0 0 0;
372
+ color: #b9b6ab;
373
+ font-size: 11.5px;
374
+ }
375
+
376
+ /* Island interaction shield: a transparent click layer over an
377
+ island's live surface (editor.liveSurface) (e.g. the neighborhood map canvas). Present until
378
+ the surface's owner (slot/section) is the live selection; a click on it
379
+ falls through to the editor's resolver, which selects the owner. Once the
380
+ owner carries data-we-live the shield is removed, so the live surface (map
381
+ pan/zoom/markers) is interactive. z-index 1 sits on the map canvas's plane
382
+ (later in DOM than the canvas -> above it) but below the floating
383
+ .tr-nbhd__card (z-index 2), so the card's slots stay clickable. */
384
+ .we-enh-shield {
385
+ position: absolute;
386
+ inset: 0;
387
+ z-index: 1;
388
+ cursor: pointer;
389
+ background: transparent;
390
+ }
391
+ [data-we-live] .we-enh-shield { display: none; }
392
+
393
+ /* Image slider: until the slider's \`images\` slot is the live selection, its
394
+ z-index:2 nav arrows would paint above the z-index:1 shield and stay
395
+ clickable, escaping the select-first gate. Disable them so the shield
396
+ catches the first click; restored once .tr-slider carries data-we-live.
397
+ Editor-only — deliberately not in published tokens.css. */
398
+ .tr-slider:not([data-we-live]) .tr-slider__nav { pointer-events: none; }
399
+ `;
400
+ }
401
+
402
+ //#endregion
403
+ export { HOST_INJECTED_PROPERTIES, buildChromeCss };
@@ -0,0 +1,49 @@
1
+ //#region src/canvas-core/chrome.d.ts
2
+ /** How far outside the target the ring sits, in CSS px. With a 2px border-box
3
+ * border, offset = 1 centers the stroke on the element's edge (1px outside,
4
+ * 1px overlapping inside) so the ring reads as flush. */
5
+ declare const RING_OFFSET = 1;
6
+ /** Inset of the section title pill from the target's left edge, in CSS px. */
7
+ declare const LABEL_LEFT_INDENT = 12;
8
+ interface RingRect {
9
+ top: number;
10
+ left: number;
11
+ width: number;
12
+ height: number;
13
+ }
14
+ /**
15
+ * Ring geometry in document space: shift by scroll, inflate by `offset`.
16
+ *
17
+ * `containerWidth` clamps the horizontal edges. A section flush with both page
18
+ * edges would otherwise inflate `offset` px past each one and grow the
19
+ * document's horizontal scroll extent. Omitting it means unclamped: the app's
20
+ * overlay is wider than its page card and never overflows, so clamping it would
21
+ * silently shift every full-bleed ring. A consumer whose overlay is page-width
22
+ * opts in by passing one.
23
+ *
24
+ * Absence — not a sentinel value — is what selects the unclamped branch. A
25
+ * numeric stand-in for "unbounded" would make NaN (plausible out of a detached
26
+ * document's clientWidth arithmetic) take the clamped branch and yield NaN
27
+ * geometry, and would let Number.MAX_SAFE_INTEGER quietly enable the left floor.
28
+ */
29
+ declare function ringBox(rect: RingRect, scroll: {
30
+ x: number;
31
+ y: number;
32
+ }, offset: number, containerWidth?: number): RingRect;
33
+ /** Whether an element has a box a ring can actually wrap. */
34
+ declare function hasMeasurableRingBox(el: HTMLElement): boolean;
35
+ declare function measureRingRect(el: HTMLElement): DOMRect;
36
+ /**
37
+ * Move a ring in #we-chrome-overlay to sit `offset` px outside `target`.
38
+ *
39
+ * `kind` flips a data attribute so CSS can pick a color (gold for sections and
40
+ * slots). The hover ring leaves it unset and uses its own selector.
41
+ */
42
+ declare function positionRing(ring: HTMLElement | null, target: HTMLElement | null, offset: number, kind?: "section" | "slot", containerWidth?: number): void;
43
+ declare function applySelectionKind(ring: HTMLElement, kind: "section" | "slot"): void;
44
+ declare function clearRing(ring: HTMLElement | null): void;
45
+ declare function positionLabel(label: HTMLElement | null, target: HTMLElement | null, text: string): void;
46
+ declare function clearLabel(label: HTMLElement | null): void;
47
+ declare function cssEscape(value: string): string;
48
+ //#endregion
49
+ export { LABEL_LEFT_INDENT, RING_OFFSET, RingRect, applySelectionKind, clearLabel, clearRing, cssEscape, hasMeasurableRingBox, measureRingRect, positionLabel, positionRing, ringBox };