@geastack/core 0.1.25 → 0.1.27

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/events.cpp CHANGED
@@ -50,6 +50,8 @@ const char *PointerEvent::typeName() const
50
50
  return "keydown";
51
51
  case PointerEventType::Rotary:
52
52
  return "rotary";
53
+ case PointerEventType::Scroll:
54
+ return "scroll";
53
55
  }
54
56
  return "";
55
57
  }
package/include/canvas.h CHANGED
@@ -95,6 +95,11 @@ public:
95
95
  int width() const;
96
96
  int height() const;
97
97
  int strideBytes() const;
98
+ // Read or replace one logical framebuffer pixel in native format. Reads are
99
+ // independent of the current clip; writes obey the clip and mark the logical
100
+ // pixel dirty. Writes replace exactly (they do not apply global alpha).
101
+ pixel::native_t readPixelNative(int x, int y) const;
102
+ void writePixelNativeExact(int x, int y, pixel::native_t value);
98
103
 
99
104
  // Software-scroll register: rotates the logical → physical row mapping
100
105
  // inside a scroll region without moving any pixels. A pixel write at
package/include/events.h CHANGED
@@ -14,7 +14,8 @@ enum class PointerEventType {
14
14
  Click,
15
15
  Input,
16
16
  KeyDown,
17
- Rotary
17
+ Rotary,
18
+ Scroll
18
19
  };
19
20
 
20
21
  enum class EventPhase {
package/include/image.h CHANGED
@@ -124,11 +124,9 @@ private:
124
124
  void materializeDeferred(int id) const;
125
125
  int decodeGif(ImageSlot &image, const std::uint8_t *data, int length);
126
126
  int decodeStatic(ImageSlot &image, const std::uint8_t *data, int length, bool opaqueHint);
127
- #if defined(GEA_EMBEDDED_ROM_TJPGD) && GEA_EMBEDDED_ROM_TJPGD
128
- // Low-memory JPEG decode via the ESP32 ROM tinyjpeg decompressor (descales
129
- // large covers so they fit the fragmented PSRAM). Fills `image`, returns 0/-1.
127
+ // Native JPEG backends, selected automatically when provided by the target.
128
+ int decodeJpegEsp(ImageSlot &image, const std::uint8_t *data, int length);
130
129
  int decodeJpegTjpgd(ImageSlot &image, const std::uint8_t *data, int length);
131
- #endif
132
130
  int decodeNextGifFrame(ImageSlot &image);
133
131
  // dispose() body without taking slotMutex_ (callers hold it).
134
132
  void disposeLocked(int id);
package/index.d.ts CHANGED
@@ -185,6 +185,8 @@ export type StyleLength =
185
185
  | `max(${string})`
186
186
  | `clamp(${string})`
187
187
  export type StyleBox = StyleLength | string
188
+ export type StylePreferredSize = StyleLength | 'auto' | 'min-content' | 'max-content' | 'fit-content'
189
+ export type StyleOverflow = 'visible' | 'hidden' | 'clip' | 'scroll' | 'auto' | 'overlay'
188
190
  export type ClassMap = Record<string, string | number | boolean | null | undefined>
189
191
  export type ClassValue = string | ClassMap
190
192
 
@@ -213,15 +215,27 @@ export interface TouchHost {
213
215
  export declare const touch: TouchHost
214
216
 
215
217
  export interface Style {
218
+ font?: string
216
219
  display?: 'block' | 'flex' | 'grid' | 'none'
217
- flexDirection?: 'row' | 'column'
218
- flexWrap?: 'nowrap' | 'wrap'
220
+ visibility?: 'visible' | 'hidden' | 'collapse'
221
+ flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse'
222
+ flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse'
223
+ flexFlow?: string
224
+ boxSizing?: 'content-box' | 'border-box'
225
+ float?: 'none' | 'left' | 'right'
226
+ clear?: 'none' | 'left' | 'right' | 'both'
227
+ writingMode?: 'horizontal-tb' | 'vertical-lr' | 'vertical-rl' | 'sideways-lr' | 'sideways-rl'
228
+ direction?: 'ltr' | 'rtl'
229
+ rowGap?: StyleLength
230
+ columnGap?: StyleLength
231
+ order?: number
232
+ alignContent?: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'space-between' | 'space-around'
219
233
  justifyContent?: 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around'
220
234
  alignItems?: 'flex-start' | 'center' | 'flex-end' | 'stretch'
221
235
  alignSelf?: 'auto' | 'flex-start' | 'center' | 'flex-end' | 'stretch'
222
236
  gap?: StyleLength
223
- width?: StyleLength
224
- height?: StyleLength
237
+ width?: StylePreferredSize
238
+ height?: StylePreferredSize
225
239
  minWidth?: StyleLength
226
240
  minHeight?: StyleLength
227
241
  maxWidth?: StyleLength
@@ -257,9 +271,10 @@ export interface Style {
257
271
  fontFamily?: string
258
272
  fontSize?: StyleLength
259
273
  textAlign?: 'left' | 'center' | 'right'
260
- overflow?: 'visible' | 'hidden' | 'scroll' | 'auto'
261
- overflowX?: 'visible' | 'hidden' | 'scroll' | 'auto'
262
- overflowY?: 'visible' | 'hidden' | 'scroll' | 'auto'
274
+ whiteSpace?: 'normal' | 'nowrap' | 'pre' | 'pre-wrap' | 'pre-line' | 'break-spaces'
275
+ overflow?: StyleOverflow | `${StyleOverflow} ${StyleOverflow}`
276
+ overflowX?: StyleOverflow
277
+ overflowY?: StyleOverflow
263
278
  transform?: string | number
264
279
  rotate?: string | number
265
280
  scale?: string | number
@@ -445,6 +460,7 @@ export interface GeaEventMap {
445
460
  input: InputEvent
446
461
  keydown: KeyEvent
447
462
  rotary: RotaryEvent
463
+ scroll: Event
448
464
  }
449
465
 
450
466
  export type TouchEventHandler = (event: TouchEvent) => void
@@ -1523,6 +1539,7 @@ export interface NativeButtonProps extends NativeEventAttributes {
1523
1539
  }
1524
1540
 
1525
1541
  export interface NativeVirtualListProps extends NativeEventAttributes {
1542
+ onScroll?: (event: Event) => void
1526
1543
  id?: string
1527
1544
  class?: ClassValue
1528
1545
  style?: Style
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geastack/core",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "GeaStack app API, reactivity headers, runtime sources, and type declarations.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -60,10 +60,10 @@
60
60
  "@babel/parser": "^7.23.0",
61
61
  "@babel/traverse": "^7.23.0",
62
62
  "@babel/types": "^7.23.0",
63
- "@geajs/core": "1.4.3",
64
- "@geajs/vite-plugin": "1.4.3",
63
+ "@geajs/core": "1.5.1",
64
+ "@geajs/vite-plugin": "1.5.0",
65
65
  "@geastack/compiler": "^1.0.15",
66
- "@geastack/geatsc-plugin-gea": "^0.1.6",
66
+ "@geastack/geatsc-plugin-gea": "^0.1.8",
67
67
  "opentype.js": "^2.0.0",
68
68
  "vite": "^8.0.0"
69
69
  }
package/runtime.cpp CHANGED
@@ -35,6 +35,13 @@
35
35
  #define GEA_EMBEDDED_WIFI_EARLY_CONNECT 0
36
36
  #endif
37
37
 
38
+ // UI/audio applications can keep touch event-driven rather than spending CPU
39
+ // on back-to-back no-op frames between samples. Touch events still request an
40
+ // immediate frame; animations continue on the configured frame timer.
41
+ #ifndef GEA_EMBEDDED_CONTINUOUS_TOUCH_FRAMES
42
+ #define GEA_EMBEDDED_CONTINUOUS_TOUCH_FRAMES 1
43
+ #endif
44
+
38
45
  #include "css/declarative.h"
39
46
  #include "css/engine.h"
40
47
  #include "ui/style.h"
@@ -190,6 +197,7 @@ void dispatch_event(const events::Event &event, const RuntimeOptions &options)
190
197
  for (;;) {
191
198
  const bool catchUp = services::FrameScheduler::takeCatchUpRequest();
192
199
  const bool activeDrag =
200
+ GEA_EMBEDDED_CONTINUOUS_TOUCH_FRAMES &&
193
201
  events::TouchRuntime::gestureActiveWithin(services::FrameScheduler::nowMs(), 50);
194
202
  // TE single-clock: never run a frame back-to-back here — the panel TE posts the
195
203
  // next frame exactly one VBlank later, which is the whole point. Back-to-back
@@ -493,6 +493,7 @@ function writeCompatViteConfig({ configPath, compatSrcDir, appDir, entry, viteOu
493
493
  .join('\n')
494
494
  const config = `import { resolve } from 'node:path'
495
495
  import { geaPlugin } from ${q(geaPluginResolved)}
496
+ import { geaNativeStylePlugin } from ${q(path.join(coreRoot, 'scripts/gea-native-style-plugin.mjs'))}
496
497
  import { geaAppleNativeModuleAliases, geaModuleGraphPlugins } from ${q(moduleGraphPluginPath)}
497
498
 
498
499
  const root = ${q(compatSrcDir)}
@@ -510,7 +511,7 @@ export default {
510
511
  define: {
511
512
  ${defineEntries}
512
513
  },
513
- plugins: [geaPlugin({ ir: { enabled: true, outFile: ${q(geaIrPath)} } }), ...geaModuleGraph],
514
+ plugins: [geaNativeStylePlugin({ runtimeEntry: ${q(compilerRuntimeEntry)} }), geaPlugin({ ir: { enabled: true, outFile: ${q(geaIrPath)} } }), ...geaModuleGraph],
514
515
  resolve: {
515
516
  alias: [
516
517
  ${compilerRuntimeAlias}
@@ -1491,6 +1492,7 @@ function parseStaticSimpleSelectorSpec(rawSimple) {
1491
1492
  wantsRoot: false,
1492
1493
  wantsFirstChild: false,
1493
1494
  wantsLastChild: false,
1495
+ wantsHover: false,
1494
1496
  }
1495
1497
  let i = 0
1496
1498
  if (simple[i] !== '.' && simple[i] !== '#' && simple[i] !== ':') {
@@ -1514,13 +1516,14 @@ function parseStaticSimpleSelectorSpec(rawSimple) {
1514
1516
  if (token === 'root') spec.wantsRoot = true
1515
1517
  else if (token === 'first-child') spec.wantsFirstChild = true
1516
1518
  else if (token === 'last-child') spec.wantsLastChild = true
1519
+ else if (token === 'hover') spec.wantsHover = true
1517
1520
  else return null
1518
1521
  } else {
1519
1522
  return null
1520
1523
  }
1521
1524
  }
1522
1525
  if (!spec.tag && !spec.id && spec.classes.length === 0 &&
1523
- !spec.wantsRoot && !spec.wantsFirstChild && !spec.wantsLastChild) {
1526
+ !spec.wantsRoot && !spec.wantsFirstChild && !spec.wantsLastChild && !spec.wantsHover) {
1524
1527
  return null
1525
1528
  }
1526
1529
  return spec
@@ -1555,7 +1558,7 @@ function staticSimpleSelectorSpecCode(simple) {
1555
1558
  const tag = simple.tag ? JSON.stringify(simple.tag) : 'nullptr'
1556
1559
  const id = simple.id ? JSON.stringify(simple.id) : 'nullptr'
1557
1560
  const classes = simple.classes.map((className) => JSON.stringify(className)).join(', ')
1558
- return `{${tag}, ${id}, {${classes}}, ${simple.wantsRoot ? 'true' : 'false'}, ${simple.wantsFirstChild ? 'true' : 'false'}, ${simple.wantsLastChild ? 'true' : 'false'}}`
1561
+ return `{${tag}, ${id}, {${classes}}, ${simple.wantsRoot ? 'true' : 'false'}, ${simple.wantsFirstChild ? 'true' : 'false'}, ${simple.wantsLastChild ? 'true' : 'false'}, ${simple.wantsHover ? 'true' : 'false'}}`
1559
1562
  }
1560
1563
 
1561
1564
  function staticSelectorPartSpecCode(part) {
@@ -1872,8 +1875,8 @@ function parseStaticCssLengthSpec(rawValue) {
1872
1875
 
1873
1876
  function cssAlignKeywordValue(value) {
1874
1877
  const align = new Map([
1875
- ['flex-start', 0],
1876
- ['start', 0],
1878
+ ['flex-start', 6],
1879
+ ['start', 6],
1877
1880
  ['stretch', 0],
1878
1881
  ['normal', 0],
1879
1882
  ['center', 1],
@@ -1939,8 +1942,9 @@ function cssStaticColorTarget(property) {
1939
1942
  case 'color':
1940
1943
  return 'Color'
1941
1944
  case 'background':
1942
- case 'background-color':
1943
1945
  return 'Background'
1946
+ case 'background-color':
1947
+ return 'BackgroundColor'
1944
1948
  case 'active-background':
1945
1949
  case 'active-background-color':
1946
1950
  return 'ActiveBackground'
@@ -1997,9 +2001,6 @@ function parseStaticCssColorRef(rawValue) {
1997
2001
  function cssIgnoredProperty(property) {
1998
2002
  return new Set([
1999
2003
  'color-scheme',
2000
- 'background-position',
2001
- 'box-sizing',
2002
- 'font',
2003
2004
  'grid-column',
2004
2005
  'isolation',
2005
2006
  'letter-spacing',
@@ -2008,7 +2009,6 @@ function cssIgnoredProperty(property) {
2008
2009
  'scroll-snap-type',
2009
2010
  'scrollbar-width',
2010
2011
  'text-shadow',
2011
- 'transform-style',
2012
2012
  'transition',
2013
2013
  'cursor',
2014
2014
  '-webkit-tap-highlight-color',
@@ -2047,6 +2047,10 @@ function cssStaticPropertyValue(property, rawValue) {
2047
2047
  const values = new Map([['block', 0], ['none', 1], ['grid', 2], ['inline-grid', 2], ['flex', 3], ['inline-flex', 3]])
2048
2048
  return values.has(value) ? direct('Display', values.get(value)) : null
2049
2049
  }
2050
+ case 'box-sizing':
2051
+ if (value === 'content-box') return direct('BoxSizing', 0)
2052
+ if (value === 'border-box') return direct('BoxSizing', 1)
2053
+ return null
2050
2054
  case 'flex-direction':
2051
2055
  if (value === 'column') return direct('FlexDirection', 0)
2052
2056
  if (value === 'row') return direct('FlexDirection', 1)
@@ -2134,10 +2138,6 @@ function cssStaticPropertyValue(property, rawValue) {
2134
2138
  if (lower === 'none') return direct('ImageFit', 3)
2135
2139
  if (lower === 'scale-down') return direct('ImageFit', 4)
2136
2140
  return null
2137
- case 'rotate': {
2138
- const angle = parseStaticAngleTenths(value)
2139
- return angle === null ? null : direct('TransformRotate', angle)
2140
- }
2141
2141
  case 'text-align':
2142
2142
  if (value === 'left' || value === 'start') return direct('TextAlign', 0)
2143
2143
  if (value === 'center') return direct('TextAlign', 1)
@@ -2208,11 +2208,8 @@ function cssStaticPropertyValues(property, rawValue) {
2208
2208
  }
2209
2209
  const boxLength = directLengthValue(length)
2210
2210
  if (boxLength !== null) {
2211
- if (property === 'border') return [direct('BorderWidth', boxLength)]
2212
- if (property === 'border-top') return [direct('BorderTopWidth', boxLength)]
2213
- if (property === 'border-right') return [direct('BorderRightWidth', boxLength)]
2214
- if (property === 'border-bottom') return [direct('BorderBottomWidth', boxLength)]
2215
- if (property === 'border-left') return [direct('BorderLeftWidth', boxLength)]
2211
+ const border = {border:'Border','border-top':'BorderTop','border-right':'BorderRight','border-bottom':'BorderBottom','border-left':'BorderLeft'}[property]
2212
+ if (border) return [direct(`${border}Width`, boxLength), direct(`${border}Relief`, 0), direct(`${border}ColorCurrent`, 1)]
2216
2213
  if (property === 'margin') {
2217
2214
  return [
2218
2215
  direct('MarginTop', boxLength),
@@ -2250,14 +2247,37 @@ function cssStaticPropertyValues(property, rawValue) {
2250
2247
  }
2251
2248
  }
2252
2249
 
2253
- if (property === 'scale') {
2254
- const scale = parseStaticScalePermille(value)
2255
- if (scale !== null) {
2256
- return [
2257
- direct('TransformScaleX', scale),
2258
- direct('TransformScaleY', scale),
2259
- ]
2250
+ if (property === 'rotate') {
2251
+ const parts = value.toLowerCase().split(/\s+/)
2252
+ if (parts.length > 1 && (/(deg|rad|turn|grad)$/.test(parts[0]) ||
2253
+ (parts.length === 2 && ['x', 'y', 'z'].includes(parts[1])))) parts.push(parts.shift())
2254
+ const none = value.toLowerCase() === 'none'
2255
+ const angle = none ? 0 : parseStaticAngleTenths(parts.at(-1))
2256
+ if (angle === null || ![1, 2, 4].includes(parts.length)) return []
2257
+ if (!none && !/(deg|rad|turn)$/i.test(parts.at(-1)) && Number(parts.at(-1)) !== 0) return []
2258
+ let axes = [0, 0, 1000000]
2259
+ if (parts.length === 2) {
2260
+ const axis = ['x', 'y', 'z'].indexOf(parts[0])
2261
+ if (axis < 0) return []
2262
+ axes = [0, 0, 0]; axes[axis] = 1000000
2263
+ } else if (parts.length === 4) {
2264
+ const vector = parts.slice(0, 3).map(Number)
2265
+ if (!vector.every(Number.isFinite)) return []
2266
+ const magnitude = Math.max(...vector.map(Math.abs))
2267
+ axes = magnitude ? vector.map(v => roundCssNumber(v / magnitude * 1000000)) : [0, 0, 0]
2260
2268
  }
2269
+ return [direct('RotatePresent', none ? 0 : 1), direct('RotateAngle', axes.some(Boolean) ? angle : 0),
2270
+ direct('RotateAxisX', axes[0]), direct('RotateAxisY', axes[1]), direct('RotateAxisZ', axes[2])]
2271
+ }
2272
+ if (property === 'scale') {
2273
+ const none = value.toLowerCase() === 'none'
2274
+ const parts = none ? ['1'] : value.split(/\s+/)
2275
+ if (parts.length < 1 || parts.length > 3) return []
2276
+ const factors = parts.map(v => v.endsWith('%') ? Number(v.slice(0, -1)) / 100 : Number(v))
2277
+ if (!factors.every(Number.isFinite)) return []
2278
+ const scales = factors.map(v => roundCssNumber(Math.max(-32768, Math.min(32767, v * 1000))))
2279
+ return [direct('ScalePresent', none ? 0 : 1), direct('ScaleX', scales[0]),
2280
+ direct('ScaleY', scales[1] ?? scales[0]), direct('ScaleZ', scales[2] ?? 1000)]
2261
2281
  }
2262
2282
 
2263
2283
  return []
@@ -2311,27 +2331,15 @@ function cssStaticFlexValue(property, rawValue) {
2311
2331
  const setup = []
2312
2332
  const words = splitCssWords(rawValue.trim())
2313
2333
  if (words.length === 0) return null
2334
+ // The compact static API has no shrink field. Preserve declarations it
2335
+ // cannot represent through the full compiler, including keyword semantics.
2336
+ if (words.some(word => /^(none|auto|content|max-content|min-content|fit-content)$/i.test(word))) return null
2314
2337
  for (const token of words) {
2315
- const lower = token.toLowerCase()
2316
- if (lower === 'none') {
2317
- grow = 0
2318
- hasBasis = false
2319
- continue
2320
- }
2321
- if (
2322
- lower === 'auto' ||
2323
- lower === 'content' ||
2324
- lower === 'max-content' ||
2325
- lower === 'min-content' ||
2326
- lower === 'fit-content' ||
2327
- lower.includes('%')
2328
- ) {
2329
- hasBasis = false
2330
- continue
2331
- }
2332
2338
  const number = Number(token)
2333
2339
  if (Number.isFinite(number) && token.match(/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/)) {
2340
+ if (number < 0 || numberIndex > 1) return null
2334
2341
  if (numberIndex === 0) grow = roundCssNumber(number)
2342
+ else if (number !== 1) return null
2335
2343
  numberIndex++
2336
2344
  continue
2337
2345
  }
@@ -2399,6 +2407,7 @@ function cssStaticBorderRadiusValue(property, rawValue) {
2399
2407
 
2400
2408
  function cssStaticBorderValue(property, rawValue) {
2401
2409
  if (property !== 'border') return null
2410
+ if (/\b(?:groove|ridge|inset|outset)\b/i.test(rawValue)) return null
2402
2411
  const parts = splitCssWords(rawValue.trim())
2403
2412
  if (parts.length === 0) return null
2404
2413
  const width = parseStaticCssLengthSpec(parts[0])
@@ -2414,6 +2423,8 @@ function cssStaticBorderValue(property, rawValue) {
2414
2423
  }
2415
2424
 
2416
2425
  function cssStaticBorderShorthandValue(property, rawValue) {
2426
+ // The compact width/color API cannot carry a shaded border style.
2427
+ if (/\b(?:groove|ridge|inset|outset)\b/i.test(rawValue)) return null
2417
2428
  const targets = {
2418
2429
  border: { width: 'BorderWidth', color: 'Border', colorProperty: 'border-color', side: false },
2419
2430
  'border-top': { width: 'BorderTopWidth', color: 'BorderTop', colorProperty: 'border-top-color', side: true },
@@ -2441,7 +2452,8 @@ function cssStaticBorderShorthandValue(property, rawValue) {
2441
2452
  }
2442
2453
  }
2443
2454
  if (!targets.side && color && !colorVar) return null
2444
- return { setup: width.setup, widthTarget: targets.width, width: width.spec, color, colorVar }
2455
+ return { setup: width.setup, widthTarget: targets.width, reliefTarget: targets.width.replace('Width', 'Relief'),
2456
+ currentColorTarget: `${targets.color}ColorCurrent`, width: width.spec, color, colorVar }
2445
2457
  }
2446
2458
 
2447
2459
  function staticColorCode(color) {
@@ -2781,6 +2793,9 @@ function cssStaticBackgroundValue(property, rawValue) {
2781
2793
  if (cssHasDynamicValue(rawValue)) return null
2782
2794
  const layers = splitTopLevelCss(rawValue, ',')
2783
2795
  if (layers.length === 0) return null
2796
+ // The gradient registration API has no base-color field. Keep combined
2797
+ // color/image shorthands on the complete native declaration parser.
2798
+ if (layers.some(layer => splitCssWords(layer).some(word => parseStaticCssColor(word) || /^(?:none|border-box|padding-box|content-box)$/i.test(word)))) return null
2784
2799
 
2785
2800
  const out = {
2786
2801
  setup: [],
@@ -2816,6 +2831,7 @@ function cssStaticBackgroundValue(property, rawValue) {
2816
2831
  }
2817
2832
 
2818
2833
  if (!out.gradient) return null
2834
+ if (layers.length !== 1 + Number(out.hasOverlayGradient) + Number(Boolean(out.radialGradient?.enabled)) + Number(Boolean(out.gridX)) + Number(Boolean(out.gridY))) return null
2819
2835
  if (out.gridX && out.gridY && !staticColorEquals(out.gridX.color, out.gridY.color) && out.gridOrder.join('') !== 'xy') return null
2820
2836
  return out
2821
2837
  }
@@ -2837,6 +2853,9 @@ function cssStaticBackgroundFullValue(property, rawValue) {
2837
2853
  return null
2838
2854
  const layers = splitTopLevelCss(rawValue, ',')
2839
2855
  if (layers.length === 0) return null
2856
+ // The gradient registration API has no base-color field. Keep combined
2857
+ // color/image shorthands on the complete native declaration parser.
2858
+ if (layers.some(layer => splitCssWords(layer).some(word => parseStaticCssColor(word) || /^(?:none|border-box|padding-box|content-box)$/i.test(word)))) return null
2840
2859
 
2841
2860
  const out = {
2842
2861
  setup: [],
@@ -2883,6 +2902,7 @@ function cssStaticBackgroundFullValue(property, rawValue) {
2883
2902
  }
2884
2903
 
2885
2904
  if (!out.gradient) return null
2905
+ if (layers.length !== 1 + Number(out.hasOverlayGradient) + Number(Boolean(out.radialGradient?.enabled)) + Number(Boolean(out.gridX)) + Number(Boolean(out.gridY))) return null
2886
2906
  if (out.gridX && out.gridY && !staticColorEquals(out.gridX.color, out.gridY.color) && out.gridOrder.join('') !== 'xy') return null
2887
2907
  return out.needsFullApi ? out : null
2888
2908
  }
@@ -2891,12 +2911,15 @@ function cssStaticBackgroundSizeValue(property, rawValue) {
2891
2911
  if (property !== 'background-size') return null
2892
2912
  if (cssHasDynamicValue(rawValue)) return null
2893
2913
  const layers = splitTopLevelCss(rawValue, ',')
2894
- if (layers.length === 0) return null
2914
+ if (layers.length !== 1) return null
2895
2915
  const parts = splitCssWords(layers[0])
2896
- if (parts.length === 0) return null
2897
- const x = parseStaticCssLengthSpec(parts[0])
2916
+ if (parts.length === 0 || parts.length > 2) return null
2917
+ const parseSize = value => value.toLowerCase() === 'auto'
2918
+ ? { setup: [], spec: { unit: 'Auto', value: '0' } }
2919
+ : parseStaticCssLengthSpec(value)
2920
+ const x = parseSize(parts[0])
2898
2921
  if (!x) return null
2899
- const y = parts.length > 1 ? parseStaticCssLengthSpec(parts[1]) : x
2922
+ const y = parseSize(parts.length > 1 ? parts[1] : 'auto')
2900
2923
  if (!y) return null
2901
2924
  return { setup: [...x.setup, ...(y === x ? [] : y.setup)], stepX: x.spec, stepY: y.spec }
2902
2925
  }
@@ -3192,6 +3215,7 @@ const staticTransformFlags = {
3192
3215
  translateZ: 1 << 5,
3193
3216
  scaleX: 1 << 8,
3194
3217
  scaleY: 1 << 9,
3218
+ scaleZ: 1 << 13,
3195
3219
  }
3196
3220
 
3197
3221
  function parseStaticAngleTenths(rawValue) {
@@ -3212,7 +3236,14 @@ function parseStaticScalePermille(rawValue) {
3212
3236
  return roundCssNumber(value * 1000)
3213
3237
  }
3214
3238
 
3239
+ function multiplyStaticScalePermille(left, right) {
3240
+ return clampInt16(roundCssNumber((left * right) / 1000))
3241
+ }
3242
+
3215
3243
  function parseStaticTransformValue(rawValue) {
3244
+ // Multiple rotation functions need ordered composition. Use the shared C++
3245
+ // CSS compiler rather than independently decomposing a matrix in JS.
3246
+ if ((rawValue.match(/rotate(?:x|y|z|3d)?\s*\(/gi) || []).length > 1) return null
3216
3247
  const value = rawValue.trim()
3217
3248
  const out = {
3218
3249
  setup: [],
@@ -3225,6 +3256,7 @@ function parseStaticTransformValue(rawValue) {
3225
3256
  translateZ: zeroStaticLengthSpec(),
3226
3257
  scaleX: 1000,
3227
3258
  scaleY: 1000,
3259
+ scaleZ: 1000,
3228
3260
  }
3229
3261
  let sawTransform = false
3230
3262
  let i = 0
@@ -3247,6 +3279,9 @@ function parseStaticTransformValue(rawValue) {
3247
3279
  if (depth !== 0) return null
3248
3280
  const arg = value.slice(argStart, i - 1)
3249
3281
  const args = splitTopLevelCss(arg, ',')
3282
+ const axes = name === 'translatex' ? 1 : name === 'translatey' ? 2 : name === 'translatez' ? 4 :
3283
+ (name === 'translate' || name === 'translate3d') ? (1 << Math.min(3, args.length)) - 1 : 0
3284
+ out.flags = (out.flags & ~(axes << 10)) | ((out.flags & 7) ? 0 : (axes << 10))
3250
3285
  sawTransform = true
3251
3286
 
3252
3287
  const setLength = (field, flag, raw) => {
@@ -3288,19 +3323,34 @@ function parseStaticTransformValue(rawValue) {
3288
3323
  const sx = parseStaticScalePermille(args[0] ?? arg)
3289
3324
  const sy = parseStaticScalePermille(args[1] ?? args[0] ?? arg)
3290
3325
  if (sx === null || sy === null) return null
3291
- out.scaleX = sx
3292
- out.scaleY = sy
3326
+ out.scaleX = multiplyStaticScalePermille(out.scaleX, sx)
3327
+ out.scaleY = multiplyStaticScalePermille(out.scaleY, sy)
3293
3328
  out.flags |= staticTransformFlags.scaleX | staticTransformFlags.scaleY
3329
+ } else if (name === 'scale3d') {
3330
+ if (args.length !== 3) return null
3331
+ const sx = parseStaticScalePermille(args[0])
3332
+ const sy = parseStaticScalePermille(args[1])
3333
+ const sz = parseStaticScalePermille(args[2])
3334
+ if (sx === null || sy === null || sz === null) return null
3335
+ out.scaleX = multiplyStaticScalePermille(out.scaleX, sx)
3336
+ out.scaleY = multiplyStaticScalePermille(out.scaleY, sy)
3337
+ out.scaleZ = multiplyStaticScalePermille(out.scaleZ, sz)
3338
+ out.flags |= staticTransformFlags.scaleX | staticTransformFlags.scaleY | staticTransformFlags.scaleZ
3294
3339
  } else if (name === 'scalex') {
3295
3340
  const scale = parseStaticScalePermille(arg)
3296
3341
  if (scale === null) return null
3297
- out.scaleX = scale
3342
+ out.scaleX = multiplyStaticScalePermille(out.scaleX, scale)
3298
3343
  out.flags |= staticTransformFlags.scaleX
3299
3344
  } else if (name === 'scaley') {
3300
3345
  const scale = parseStaticScalePermille(arg)
3301
3346
  if (scale === null) return null
3302
- out.scaleY = scale
3347
+ out.scaleY = multiplyStaticScalePermille(out.scaleY, scale)
3303
3348
  out.flags |= staticTransformFlags.scaleY
3349
+ } else if (name === 'scalez') {
3350
+ const scale = parseStaticScalePermille(arg)
3351
+ if (scale === null) return null
3352
+ out.scaleZ = multiplyStaticScalePermille(out.scaleZ, scale)
3353
+ out.flags |= staticTransformFlags.scaleZ
3304
3354
  } else {
3305
3355
  return null
3306
3356
  }
@@ -3313,17 +3363,29 @@ function cssStaticTransformValue(property, rawValue) {
3313
3363
  return parseStaticTransformValue(rawValue)
3314
3364
  }
3315
3365
 
3316
- function staticTransformArgs(transform) {
3317
- return `${transform.flags}, ${transform.rotateX}, ${transform.rotateY}, ${transform.rotateZ}, ${staticLengthSpecCode(transform.translateX)}, ${staticLengthSpecCode(transform.translateY)}, ${staticLengthSpecCode(transform.translateZ)}, ${transform.scaleX}, ${transform.scaleY}`
3366
+ function staticTransformArgs(transform, mediaArg = null) {
3367
+ const fields = `${transform.flags}, ${transform.rotateX}, ${transform.rotateY}, ${transform.rotateZ}, ${staticLengthSpecCode(transform.translateX)}, ${staticLengthSpecCode(transform.translateY)}, ${staticLengthSpecCode(transform.translateZ)}, ${transform.scaleX}, ${transform.scaleY}`
3368
+ if (mediaArg === null) return `${fields}, ${transform.scaleZ}`
3369
+ return `${fields}${mediaArg || ', nullptr'}, ${transform.scaleZ}`
3318
3370
  }
3319
3371
 
3320
3372
  const staticAnimatableLengthTargets = new Set(['Width', 'Height', 'Top', 'Left'])
3321
- const staticAnimatableColorTargets = new Set(['Color', 'Background'])
3373
+ const staticAnimatableColorTargets = new Set(['Color', 'Background', 'BackgroundColor'])
3322
3374
  const staticAnimatableDirectProperties = new Set([
3323
3375
  'gea::embedded::ui::Property::Opacity',
3376
+ 'gea::embedded::ui::Property::RotatePresent',
3377
+ 'gea::embedded::ui::Property::RotateAngle',
3378
+ 'gea::embedded::ui::Property::RotateAxisX',
3379
+ 'gea::embedded::ui::Property::RotateAxisY',
3380
+ 'gea::embedded::ui::Property::RotateAxisZ',
3381
+ 'gea::embedded::ui::Property::ScalePresent',
3382
+ 'gea::embedded::ui::Property::ScaleX',
3383
+ 'gea::embedded::ui::Property::ScaleY',
3384
+ 'gea::embedded::ui::Property::ScaleZ',
3324
3385
  'gea::embedded::ui::Property::TransformRotate',
3325
3386
  'gea::embedded::ui::Property::TransformScaleX',
3326
3387
  'gea::embedded::ui::Property::TransformScaleY',
3388
+ 'gea::embedded::ui::Property::TransformScaleZ',
3327
3389
  ])
3328
3390
 
3329
3391
  function cssStaticFilterBlurValue(property, rawValue) {
@@ -3437,12 +3499,14 @@ function emitCssRuleRegistrations(kind, selector, name, value, mediaCondition) {
3437
3499
  `__gea_stylesheet.registerStaticPropertyRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${direct.property}, ${direct.value}${mediaArg});`,
3438
3500
  ]
3439
3501
  }
3440
- const entries = directProperties
3441
- .map((direct) => `{${direct.property}, ${direct.value}}`)
3442
- .join(', ')
3443
- return [
3444
- `__gea_stylesheet.registerStaticPropertyGroupRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, {${entries}}${mediaArg});`,
3445
- ]
3502
+ // Each native compiled property group has four slots.
3503
+ const registrations = []
3504
+ for (let i = 0; i < directProperties.length; i += 4) {
3505
+ const entries = directProperties.slice(i, i + 4)
3506
+ .map(direct => `{${direct.property}, ${direct.value}}`).join(', ')
3507
+ registrations.push(`__gea_stylesheet.registerStaticPropertyGroupRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, {${entries}}${mediaArg});`)
3508
+ }
3509
+ return registrations
3446
3510
  }
3447
3511
  const flex = cssStaticFlexValue(name, value)
3448
3512
  if (flex) {
@@ -3478,6 +3542,7 @@ function emitCssRuleRegistrations(kind, selector, name, value, mediaCondition) {
3478
3542
  const calls = [
3479
3543
  ...borderShorthand.setup,
3480
3544
  `__gea_stylesheet.registerStaticLengthSpecRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleLengthProperty::${borderShorthand.widthTarget}, ${staticLengthSpecCode(borderShorthand.width)}${mediaArg});`,
3545
+ `__gea_stylesheet.registerStaticPropertyRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::Property::${borderShorthand.reliefTarget}, 0${mediaArg});`,
3481
3546
  ]
3482
3547
  if (borderShorthand.colorVar) {
3483
3548
  const colorVar = borderShorthand.colorVar
@@ -3485,6 +3550,8 @@ function emitCssRuleRegistrations(kind, selector, name, value, mediaCondition) {
3485
3550
  } else if (borderShorthand.color) {
3486
3551
  const color = borderShorthand.color
3487
3552
  calls.push(`__gea_stylesheet.registerStaticColorRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleColorProperty::${color.target}, ${color.color.r}, ${color.color.g}, ${color.color.b}, ${color.color.a}${mediaArg});`)
3553
+ } else {
3554
+ calls.push(`__gea_stylesheet.registerStaticPropertyRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::Property::${borderShorthand.currentColorTarget}, 1${mediaArg});`)
3488
3555
  }
3489
3556
  return calls
3490
3557
  }
@@ -3519,14 +3586,14 @@ function emitCssRuleRegistrations(kind, selector, name, value, mediaCondition) {
3519
3586
  if (fullBackground) {
3520
3587
  return [
3521
3588
  ...fullBackground.setup,
3522
- `__gea_stylesheet.registerStaticBackgroundFullRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLinearGradientRefCode(fullBackground.gradient)}, ${staticLinearGradientRefCode(fullBackground.overlayGradient)}, ${fullBackground.hasOverlayGradient ? 'true' : 'false'}, ${staticRadialGradientRefCode(fullBackground.radialGradient)}, ${staticGridLineCode(fullBackground.gridX)}, ${staticGridLineCode(fullBackground.gridY)}${mediaArg});`,
3589
+ `__gea_stylesheet.registerStaticBackgroundFullRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLinearGradientRefCode(fullBackground.gradient)}, ${staticLinearGradientRefCode(fullBackground.overlayGradient)}, ${fullBackground.hasOverlayGradient ? 'true' : 'false'}, ${staticRadialGradientRefCode(fullBackground.radialGradient)}, ${staticGridLineCode(fullBackground.gridX)}, ${staticGridLineCode(fullBackground.gridY)}${mediaArg || ", nullptr"}, ${name === "background-image" ? "true" : "false"});`,
3523
3590
  ]
3524
3591
  }
3525
3592
  const background = cssStaticBackgroundValue(name, value)
3526
3593
  if (background) {
3527
3594
  return [
3528
3595
  ...background.setup,
3529
- `__gea_stylesheet.registerStaticBackgroundRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLinearGradientCode(background.gradient)}, ${staticLinearGradientCode(background.overlayGradient)}, ${background.hasOverlayGradient ? 'true' : 'false'}, ${staticGridLineCode(background.gridX)}, ${staticGridLineCode(background.gridY)}${mediaArg});`,
3596
+ `__gea_stylesheet.registerStaticBackgroundRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLinearGradientCode(background.gradient)}, ${staticLinearGradientCode(background.overlayGradient)}, ${background.hasOverlayGradient ? 'true' : 'false'}, ${staticGridLineCode(background.gridX)}, ${staticGridLineCode(background.gridY)}${mediaArg || ", nullptr"}, ${name === "background-image" ? "true" : "false"});`,
3530
3597
  ]
3531
3598
  }
3532
3599
  const backgroundSize = cssStaticBackgroundSizeValue(name, value)
@@ -3591,7 +3658,7 @@ function emitCssRuleRegistrations(kind, selector, name, value, mediaCondition) {
3591
3658
  if (transform) {
3592
3659
  return [
3593
3660
  ...transform.setup,
3594
- `__gea_stylesheet.registerStaticTransformRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticTransformArgs(transform)}${mediaArg});`,
3661
+ `__gea_stylesheet.registerStaticTransformRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticTransformArgs(transform, mediaArg)});`,
3595
3662
  ]
3596
3663
  }
3597
3664
  }
@@ -3810,6 +3877,9 @@ function staticCssTapeLengthSpecParts(code) {
3810
3877
  if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) return null
3811
3878
  const parts = splitTopLevelCppArgs(trimmed.slice(1, -1))
3812
3879
  if (parts.length !== 2) return null
3880
+ // The tape is constexpr: a length expression's value is the id its
3881
+ // registration returned at run time, so that rule stays a plain call.
3882
+ if (!/^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?f?$/i.test(parts[1].trim())) return null
3813
3883
  return { unit: parts[0], value: parts[1] }
3814
3884
  }
3815
3885
 
@@ -3989,31 +4059,38 @@ auto __gea_css_tape_selector_kind = [](std::uint8_t kind) {
3989
4059
  auto __gea_css_tape_length = [](std::uint8_t unit, float value) {
3990
4060
  \treturn gea::embedded::ui::StaticStyleLengthSpec{static_cast<gea::embedded::ui::StaticStyleLengthUnit>(unit), value};
3991
4061
  };
3992
- auto __gea_register_static_css_tape = [&](
3993
- \tstd::initializer_list<__GeaStaticCssTapeKind> order,
3994
- \tstd::initializer_list<__GeaStaticCssPropertyOp> propertyOps,
3995
- \tstd::initializer_list<__GeaStaticCssLengthOp> lengthOps,
3996
- \tstd::initializer_list<__GeaStaticCssLengthOp> lengthSpecOps,
3997
- \tstd::initializer_list<__GeaStaticCssColorOp> colorOps,
3998
- \tstd::initializer_list<__GeaStaticCssColorVarOp> colorVarOps,
3999
- \tstd::initializer_list<__GeaStaticCssCustomLengthOp> customLengthOps,
4000
- \tstd::initializer_list<__GeaStaticCssCustomColorOp> customColorOps,
4001
- \tstd::initializer_list<__GeaStaticCssSelectorOnlyOp> boxShadowNoneOps,
4002
- \tstd::initializer_list<__GeaStaticCssNamedOp> fontFamilyOps,
4003
- \tstd::initializer_list<__GeaStaticCssLengthOnlyOp> filterBlurOps,
4004
- \tstd::initializer_list<__GeaStaticCssLineHeightOp> lineHeightOps) {
4005
- \tauto propertyIt = propertyOps.begin();
4006
- \tauto lengthIt = lengthOps.begin();
4007
- \tauto lengthSpecIt = lengthSpecOps.begin();
4008
- \tauto colorIt = colorOps.begin();
4009
- \tauto colorVarIt = colorVarOps.begin();
4010
- \tauto customLengthIt = customLengthOps.begin();
4011
- \tauto customColorIt = customColorOps.begin();
4012
- \tauto boxShadowNoneIt = boxShadowNoneOps.begin();
4013
- \tauto fontFamilyIt = fontFamilyOps.begin();
4014
- \tauto filterBlurIt = filterBlurOps.begin();
4015
- \tauto lineHeightIt = lineHeightOps.begin();
4016
- \tfor (const auto kind : order) {
4062
+ // Each tape's lists are static constexpr arrays the call points into: an
4063
+ // initializer_list argument is a temporary array, which the compiler builds on
4064
+ // the stack by copying it out of read-only data at every call.
4065
+ struct __GeaStaticCssTape {
4066
+ \tstd::size_t orderCount;
4067
+ \tconst __GeaStaticCssTapeKind *order;
4068
+ \tconst __GeaStaticCssPropertyOp *propertyOps;
4069
+ \tconst __GeaStaticCssLengthOp *lengthOps;
4070
+ \tconst __GeaStaticCssLengthOp *lengthSpecOps;
4071
+ \tconst __GeaStaticCssColorOp *colorOps;
4072
+ \tconst __GeaStaticCssColorVarOp *colorVarOps;
4073
+ \tconst __GeaStaticCssCustomLengthOp *customLengthOps;
4074
+ \tconst __GeaStaticCssCustomColorOp *customColorOps;
4075
+ \tconst __GeaStaticCssSelectorOnlyOp *boxShadowNoneOps;
4076
+ \tconst __GeaStaticCssNamedOp *fontFamilyOps;
4077
+ \tconst __GeaStaticCssLengthOnlyOp *filterBlurOps;
4078
+ \tconst __GeaStaticCssLineHeightOp *lineHeightOps;
4079
+ };
4080
+ auto __gea_register_static_css_tape = [&](const __GeaStaticCssTape &tape) {
4081
+ \tconst auto *propertyIt = tape.propertyOps;
4082
+ \tconst auto *lengthIt = tape.lengthOps;
4083
+ \tconst auto *lengthSpecIt = tape.lengthSpecOps;
4084
+ \tconst auto *colorIt = tape.colorOps;
4085
+ \tconst auto *colorVarIt = tape.colorVarOps;
4086
+ \tconst auto *customLengthIt = tape.customLengthOps;
4087
+ \tconst auto *customColorIt = tape.customColorOps;
4088
+ \tconst auto *boxShadowNoneIt = tape.boxShadowNoneOps;
4089
+ \tconst auto *fontFamilyIt = tape.fontFamilyOps;
4090
+ \tconst auto *filterBlurIt = tape.filterBlurOps;
4091
+ \tconst auto *lineHeightIt = tape.lineHeightOps;
4092
+ \tfor (std::size_t step = 0; step < tape.orderCount; ++step) {
4093
+ \t\tconst auto kind = tape.order[step];
4017
4094
  \t\tswitch (kind) {
4018
4095
  \t\tcase __GeaStaticCssTapeKind::Property: {
4019
4096
  \t\t\tconst auto &op = *propertyIt++;
@@ -4114,12 +4191,27 @@ function compactStaticCssTapeCalls(calls) {
4114
4191
  }
4115
4192
  usedTape = true
4116
4193
  const order = chunk.map((entry) => `__GeaStaticCssTapeKind::${entry.kind}`).join(', ')
4117
- const listFor = (kind) => {
4118
- const entries = chunk.filter((entry) => entry.kind === kind).map((entry) => entry.code)
4119
- if (entries.length === 0) return '{}'
4120
- return `{\n${entries.map((entry) => `\t\t${entry}`).join(',\n')}\n\t}`
4194
+ const opTypes = {
4195
+ Property: '__GeaStaticCssPropertyOp',
4196
+ Length: '__GeaStaticCssLengthOp',
4197
+ LengthSpec: '__GeaStaticCssLengthOp',
4198
+ Color: '__GeaStaticCssColorOp',
4199
+ ColorVar: '__GeaStaticCssColorVarOp',
4200
+ CustomLength: '__GeaStaticCssCustomLengthOp',
4201
+ CustomColor: '__GeaStaticCssCustomColorOp',
4202
+ BoxShadowNone: '__GeaStaticCssSelectorOnlyOp',
4203
+ FontFamily: '__GeaStaticCssNamedOp',
4204
+ FilterBlur: '__GeaStaticCssLengthOnlyOp',
4205
+ LineHeight: '__GeaStaticCssLineHeightOp',
4121
4206
  }
4122
- out.push(`__gea_register_static_css_tape(\n\t{${order}},\n\t${kinds.map(listFor).join(',\n\t')});`)
4207
+ const arrays = [`\tstatic constexpr __GeaStaticCssTapeKind order[] = {${order}};`]
4208
+ const pointers = kinds.map((kind) => {
4209
+ const entries = chunk.filter((entry) => entry.kind === kind).map((entry) => entry.code)
4210
+ if (entries.length === 0) return 'nullptr'
4211
+ arrays.push(`\tstatic constexpr ${opTypes[kind]} ops${kind}[] = {\n${entries.map((entry) => `\t\t${entry}`).join(',\n')}\n\t};`)
4212
+ return `ops${kind}`
4213
+ })
4214
+ out.push(`{\n${arrays.join('\n')}\n\t__gea_register_static_css_tape({${chunk.length}, order, ${pointers.join(', ')}});\n}`)
4123
4215
  chunk = []
4124
4216
  }
4125
4217
  for (const call of calls) {
@@ -4135,6 +4227,7 @@ function compactStaticCssTapeCalls(calls) {
4135
4227
  return { calls: out, usedTape, stringLiterals }
4136
4228
  }
4137
4229
 
4230
+ let staticCssRegistrationCalls = []
4138
4231
  function cssRegistrationCode(viteOutDir) {
4139
4232
  const calls = []
4140
4233
  staticCssLengthExpressionCounter = 0
@@ -4153,6 +4246,10 @@ function cssRegistrationCode(viteOutDir) {
4153
4246
  emitCssRules(stripAtBlocks(block.body), block.condition, calls)
4154
4247
  }
4155
4248
  }
4249
+ // The calls themselves, before the tape packs them: a target that resolves
4250
+ // styles at build time (the Pebble compiled UI) reads these rather than
4251
+ // re-deriving the cascade from the CSS text.
4252
+ staticCssRegistrationCalls = calls
4156
4253
  if (calls.length === 0) return ''
4157
4254
  const compacted = compactStaticCssTapeCalls(calls)
4158
4255
  const tapePrelude = compacted.usedTape ? staticCssTapePrelude(compacted.stringLiterals) : ''
@@ -4359,6 +4456,7 @@ const viteBundle = findFirstJsFile(viteOutDir)
4359
4456
  if (!viteBundle) fail(`Vite produced no JS bundle in ${viteOutDir}`)
4360
4457
  const cssPrelude = geaEmbeddedCompat ? cssRegistrationCode(viteOutDir) : ''
4361
4458
  const cssPreludePath = path.join(outDir, 'gea-style-registration.cppfrag')
4459
+ fs.writeFileSync(path.join(outDir, 'gea-style-registration.calls.json'), JSON.stringify(staticCssRegistrationCalls, null, 1) + '\n')
4362
4460
  if (cssPrelude || cppPreludeSymbol) {
4363
4461
  fs.writeFileSync(cssPreludePath, cssPrelude)
4364
4462
  } else {
@@ -0,0 +1,31 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ /** Native lengths are raw numbers; only an authored CSS unit may introduce scaling. */
5
+ export function geaNativeStylePlugin({ runtimeEntry }) {
6
+ if (!runtimeEntry) throw new Error('Native Gea styles require the typed @geajs/core source entry')
7
+ const sourceRoot = path.dirname(fs.realpathSync(runtimeEntry))
8
+ const serializer = path.join(sourceRoot, 'runtime/style-value.ts')
9
+ return {
10
+ name: 'geastack-native-style-units',
11
+ enforce: 'pre',
12
+ // Keep all compiler-generated runtime imports on the same typed source tree.
13
+ // Aliasing only the root left compiler-runtime on the browser dist path.
14
+ resolveId(id) {
15
+ const name = id === '@geajs/core' ? 'index' : id.startsWith('@geajs/core/') ? id.slice('@geajs/core/'.length) : null
16
+ if (name === null) return null
17
+ return [path.join(sourceRoot, `${name}.ts`), path.join(sourceRoot, name, 'index.ts')].find(entry => fs.existsSync(entry)) || null
18
+ },
19
+ load(id) {
20
+ if (id.split('?')[0] !== serializer) return null
21
+ // This module is the browser serialization boundary, not the JSX IR.
22
+ // Replacing it here preserves numeric values in the native IR and prevents
23
+ // browser px defaults from reaching native runtime-backed components/h().
24
+ return `export function styleProp(key: string): string {
25
+ return key.startsWith('--') ? key : key.replace(/[A-Z]/g, c => '-' + c.toLowerCase())
26
+ }
27
+ export function styleValue(_property: string, value: unknown): string { return String(value) }
28
+ `
29
+ },
30
+ }
31
+ }