@energy8platform/game-engine 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -410,7 +410,7 @@ If no custom view is provided, components fall back to Graphics-based rendering
410
410
 
411
411
  ### FlexContainer
412
412
 
413
- Lightweight flexbox-like layout container. Children added via `addChild()` automatically participate in flex layout.
413
+ Lightweight flexbox-like layout container. Children added via `addChild()` automatically participate in flex layout. Supports auto-sizing (no explicit `width`/`height` required), percentage dimensions, absolute positioning for excluded children, and multi-line alignment.
414
414
 
415
415
  ```typescript
416
416
  const toolbar = new FlexContainer({
@@ -426,6 +426,32 @@ toolbar.addFlexChild(spacer, { flexGrow: 1 }); // with flex config
426
426
  toolbar.resize(800, 60);
427
427
  ```
428
428
 
429
+ **FlexContainerConfig:**
430
+
431
+ | Property | Type | Default | Description |
432
+ | --- | --- | --- | --- |
433
+ | `direction` | `'row' \| 'column'` | `'row'` | Layout direction |
434
+ | `justifyContent` | `'start' \| 'center' \| 'end' \| 'space-between' \| 'space-around'` | `'start'` | Main-axis distribution |
435
+ | `alignItems` | `'start' \| 'center' \| 'end' \| 'stretch'` | `'start'` | Cross-axis alignment |
436
+ | `alignContent` | `'start' \| 'center' \| 'end' \| 'space-between' \| 'stretch'` | `'start'` | Multi-line cross-axis distribution (with `flexWrap`) |
437
+ | `gap` | `number` | `0` | Gap between children |
438
+ | `padding` | `number \| [top, right, bottom, left]` | `0` | Padding (shorthand) |
439
+ | `paddingTop/Right/Bottom/Left` | `number` | — | Individual padding overrides (take priority over `padding`) |
440
+ | `flexWrap` | `boolean` | `false` | Enable wrapping to next line |
441
+ | `width` | `number \| string` | — | Container width — pixels or `"50%"` relative to parent |
442
+ | `height` | `number \| string` | — | Container height — pixels or `"50%"` relative to parent |
443
+
444
+ **Auto-sizing:** When no explicit `width`/`height` is set, the container computes its size from content (`_computedWidth`/`_computedHeight`). Cross-axis `alignItems` works automatically (centers relative to tallest/widest child). Parent FlexContainers correctly measure auto-sized children.
445
+
446
+ **Percentage dimensions:** String values like `"100%"` or `"50%"` resolve against the parent FlexContainer's content area. Works for both container `width`/`height` and child `layoutWidth`/`layoutHeight`.
447
+
448
+ ```typescript
449
+ // Parent with explicit size, child fills 100% width and 50% height
450
+ const parent = new FlexContainer({ direction: 'column', width: 800, height: 600 });
451
+ const child = new FlexContainer({ width: '100%', height: '50%', direction: 'row' });
452
+ parent.addFlexChild(child); // child resolves to 800×300
453
+ ```
454
+
429
455
  **FlexItemConfig** — per-child options passed via `addFlexChild(child, config)` or JSX props:
430
456
 
431
457
  | Property | Type | Default | Description |
@@ -434,16 +460,21 @@ toolbar.resize(800, 60);
434
460
  | `flexShrink` | `number` | `1` | Flex shrink factor (`0` = don't shrink when content overflows) |
435
461
  | `alignSelf` | `'auto' \| 'start' \| 'center' \| 'end' \| 'stretch'` | `'auto'` | Override parent's `alignItems` for this child |
436
462
  | `flexExclude` | `boolean` | `false` | Exclude from flex layout (like `position: absolute`) |
437
- | `layoutWidth` | `number` | — | Explicit width override for layout calculations |
438
- | `layoutHeight` | `number` | — | Explicit height override for layout calculations |
463
+ | `layoutWidth` | `number \| string` | — | Width override pixels or `"50%"` of parent content area |
464
+ | `layoutHeight` | `number \| string` | — | Height override pixels or `"50%"` of parent content area |
465
+ | `top` | `number` | — | Absolute positioning (only with `flexExclude`) — distance from top edge |
466
+ | `right` | `number` | — | Absolute positioning — distance from right edge |
467
+ | `bottom` | `number` | — | Absolute positioning — distance from bottom edge |
468
+ | `left` | `number` | — | Absolute positioning — distance from left edge |
439
469
 
440
470
  ```typescript
441
471
  // Fixed item that won't shrink + centered override
442
472
  toolbar.addFlexChild(logo, { flexShrink: 0 });
443
473
  toolbar.addFlexChild(badge, { alignSelf: 'center' });
444
474
 
445
- // Background excluded from layout flow
446
- toolbar.addFlexChild(background, { flexExclude: true });
475
+ // Absolute positioning (like CSS position: absolute)
476
+ toolbar.addFlexChild(closeBtn, { flexExclude: true, top: 8, right: 8 });
477
+ toolbar.addFlexChild(background, { flexExclude: true, top: 0, left: 0 });
447
478
  ```
448
479
 
449
480
  ### Layout
@@ -906,10 +937,30 @@ All engine UI components are config-based: the reconciler passes JSX props as a
906
937
 
907
938
  {/* Flex item props — work on any element inside <flexContainer> */}
908
939
  <flexContainer direction="row" width={800} height={60}>
909
- <graphics draw={drawBg} flexExclude /> {/* excluded from flow */}
910
- <label text="Logo" flexShrink={0} /> {/* won't shrink */}
911
- <container flexGrow={1} /> {/* fills remaining space */}
912
- <button text="Menu" alignSelf="center" /> {/* centered on cross-axis */}
940
+ <graphics draw={drawBg} flexExclude top={0} left={0} /> {/* absolute positioning */}
941
+ <label text="Logo" flexShrink={0} /> {/* won't shrink */}
942
+ <container flexGrow={1} /> {/* fills remaining space */}
943
+ <button text="Menu" alignSelf="center" /> {/* centered on cross-axis */}
944
+ <sprite texture="close" flexExclude top={4} right={4} /> {/* top-right corner */}
945
+ </flexContainer>
946
+
947
+ {/* Individual padding props */}
948
+ <flexContainer direction="column" paddingTop={20} paddingLeft={16} paddingRight={16}>
949
+ <label text="Content" />
950
+ </flexContainer>
951
+
952
+ {/* Percentage dimensions */}
953
+ <flexContainer direction="column" width={screen.width} height={screen.height}>
954
+ <flexContainer width="100%" height="50%" direction="row" alignItems="center">
955
+ <label text="Top half" />
956
+ </flexContainer>
957
+ </flexContainer>
958
+
959
+ {/* alignContent for wrapped lines */}
960
+ <flexContainer direction="row" flexWrap alignContent="center" width={400} height={400} gap={8}>
961
+ <button width={180} height={40} text="A" />
962
+ <button width={180} height={40} text="B" />
963
+ <button width={180} height={40} text="C" />
913
964
  </flexContainer>
914
965
  ```
915
966
 
package/dist/index.cjs.js CHANGED
@@ -2912,26 +2912,90 @@ class SpriteAnimation {
2912
2912
  function normalizePadding(p) {
2913
2913
  return typeof p === 'number' ? [p, p, p, p] : p;
2914
2914
  }
2915
+ /** Resolve padding from config: individual props override the base `padding` value */
2916
+ function resolvePadding(config) {
2917
+ const base = normalizePadding(config.padding ?? 0);
2918
+ return [
2919
+ config.paddingTop ?? base[0],
2920
+ config.paddingRight ?? base[1],
2921
+ config.paddingBottom ?? base[2],
2922
+ config.paddingLeft ?? base[3],
2923
+ ];
2924
+ }
2925
+ /** Resolve a dimension value — number passes through, "50%" resolves against reference */
2926
+ function resolveDimension(value, reference) {
2927
+ if (value === undefined)
2928
+ return undefined;
2929
+ if (typeof value === 'number')
2930
+ return value;
2931
+ if (typeof value === 'string' && value.endsWith('%')) {
2932
+ const pct = parseFloat(value);
2933
+ if (!isNaN(pct) && reference > 0 && isFinite(reference))
2934
+ return (pct / 100) * reference;
2935
+ }
2936
+ return undefined;
2937
+ }
2915
2938
  /** Measure a child's size and bounds offset for layout purposes */
2916
- function measureChild(child) {
2939
+ function measureChild(child, parentContentW = 0, parentContentH = 0) {
2917
2940
  const cfg = child._flexConfig;
2918
- if (cfg?.layoutWidth !== undefined && cfg?.layoutHeight !== undefined) {
2919
- return { w: cfg.layoutWidth, h: cfg.layoutHeight, ox: 0, oy: 0 };
2941
+ const resolvedLW = resolveDimension(cfg?.layoutWidth, parentContentW);
2942
+ const resolvedLH = resolveDimension(cfg?.layoutHeight, parentContentH);
2943
+ if (resolvedLW !== undefined && resolvedLH !== undefined) {
2944
+ return { w: resolvedLW, h: resolvedLH, ox: 0, oy: 0 };
2920
2945
  }
2921
- // For FlexContainers, use their explicit size if set
2946
+ // For FlexContainers, use their explicit or computed size
2922
2947
  if (child instanceof FlexContainer) {
2923
2948
  const fc = child;
2924
- if (fc._explicitWidth > 0 && fc._explicitHeight > 0) {
2925
- return { w: fc._explicitWidth, h: fc._explicitHeight, ox: 0, oy: 0 };
2949
+ const w = fc._explicitWidth > 0 ? fc._explicitWidth : (fc._computedWidth > 0 ? fc._computedWidth : undefined);
2950
+ const h = fc._explicitHeight > 0 ? fc._explicitHeight : (fc._computedHeight > 0 ? fc._computedHeight : undefined);
2951
+ if (w !== undefined && h !== undefined) {
2952
+ return { w, h, ox: 0, oy: 0 };
2926
2953
  }
2927
2954
  }
2928
2955
  // Use localBounds to get the true visual extent and origin offset.
2929
2956
  // This handles children with non-zero anchors (e.g. Button, Label with centered text).
2930
2957
  const bounds = child.getLocalBounds();
2931
- const w = cfg?.layoutWidth ?? bounds.width;
2932
- const h = cfg?.layoutHeight ?? bounds.height;
2958
+ const w = resolvedLW ?? bounds.width;
2959
+ const h = resolvedLH ?? bounds.height;
2933
2960
  return { w, h, ox: bounds.x, oy: bounds.y };
2934
2961
  }
2962
+ /**
2963
+ * Set a child's main or cross dimension.
2964
+ * For FlexContainer children, calls resize() to trigger internal relayout
2965
+ * instead of the PixiJS scale setter.
2966
+ */
2967
+ function setChildMainSize(child, isRow, mainSize, item) {
2968
+ if (child instanceof FlexContainer) {
2969
+ const fc = child;
2970
+ fc.resize(isRow ? mainSize : fc._explicitWidth || fc._computedWidth, isRow ? fc._explicitHeight || fc._computedHeight : mainSize);
2971
+ }
2972
+ else {
2973
+ if (isRow) {
2974
+ child.width = mainSize;
2975
+ }
2976
+ else {
2977
+ child.height = mainSize;
2978
+ }
2979
+ }
2980
+ if (isRow)
2981
+ item.w = mainSize;
2982
+ else
2983
+ item.h = mainSize;
2984
+ }
2985
+ function setChildCrossSize(child, isRow, crossSize) {
2986
+ if (child instanceof FlexContainer) {
2987
+ const fc = child;
2988
+ fc.resize(isRow ? fc._explicitWidth || fc._computedWidth : crossSize, isRow ? crossSize : fc._explicitHeight || fc._computedHeight);
2989
+ }
2990
+ else {
2991
+ if (isRow) {
2992
+ child.height = crossSize;
2993
+ }
2994
+ else {
2995
+ child.width = crossSize;
2996
+ }
2997
+ }
2998
+ }
2935
2999
  function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, crossSize) {
2936
3000
  if (items.length === 0)
2937
3001
  return;
@@ -2955,14 +3019,7 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
2955
3019
  const grow = item.child._flexConfig?.flexGrow ?? 0;
2956
3020
  if (grow > 0) {
2957
3021
  const flexSize = (grow / totalGrow) * availableForFlex;
2958
- if (isRow) {
2959
- item.w = flexSize;
2960
- item.child.width = flexSize;
2961
- }
2962
- else {
2963
- item.h = flexSize;
2964
- item.child.height = flexSize;
2965
- }
3022
+ setChildMainSize(item.child, isRow, flexSize, item);
2966
3023
  }
2967
3024
  }
2968
3025
  }
@@ -2984,14 +3041,7 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
2984
3041
  const itemMain = isRow ? item.w : item.h;
2985
3042
  const reduction = overflow * (itemMain / totalShrinkable);
2986
3043
  const newSize = Math.max(0, itemMain - reduction);
2987
- if (isRow) {
2988
- item.w = newSize;
2989
- item.child.width = newSize;
2990
- }
2991
- else {
2992
- item.h = newSize;
2993
- item.child.height = newSize;
2994
- }
3044
+ setChildMainSize(item.child, isRow, newSize, item);
2995
3045
  }
2996
3046
  }
2997
3047
  }
@@ -3048,12 +3098,7 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
3048
3098
  crossPos += crossSize - crossDim;
3049
3099
  break;
3050
3100
  case 'stretch':
3051
- if (isRow) {
3052
- item.child.height = crossSize;
3053
- }
3054
- else {
3055
- item.child.width = crossSize;
3056
- }
3101
+ setChildCrossSize(item.child, isRow, crossSize);
3057
3102
  break;
3058
3103
  }
3059
3104
  // Compensate for local bounds offset (e.g. centered anchors)
@@ -3098,6 +3143,12 @@ class FlexContainer extends pixi_js.Container {
3098
3143
  _maxHeight;
3099
3144
  /** @internal */ _explicitWidth;
3100
3145
  /** @internal */ _explicitHeight;
3146
+ /** @internal */ _computedWidth = 0;
3147
+ /** @internal */ _computedHeight = 0;
3148
+ /** @internal */ _availableWidth = 0;
3149
+ /** @internal */ _availableHeight = 0;
3150
+ /** @internal */ _rawWidth;
3151
+ /** @internal */ _rawHeight;
3101
3152
  _layoutChildren = [];
3102
3153
  _layoutDirty = true;
3103
3154
  constructor(config = {}) {
@@ -3108,12 +3159,15 @@ class FlexContainer extends pixi_js.Container {
3108
3159
  alignItems: config.alignItems ?? 'start',
3109
3160
  gap: config.gap ?? 0,
3110
3161
  flexWrap: config.flexWrap ?? false,
3162
+ alignContent: config.alignContent ?? 'start',
3111
3163
  };
3112
- this._padding = normalizePadding(config.padding ?? 0);
3164
+ this._padding = resolvePadding(config);
3113
3165
  this._maxWidth = config.maxWidth ?? Infinity;
3114
3166
  this._maxHeight = config.maxHeight ?? Infinity;
3115
- this._explicitWidth = config.width ?? 0;
3116
- this._explicitHeight = config.height ?? 0;
3167
+ this._rawWidth = config.width ?? 0;
3168
+ this._rawHeight = config.height ?? 0;
3169
+ this._explicitWidth = typeof this._rawWidth === 'number' ? this._rawWidth : 0;
3170
+ this._explicitHeight = typeof this._rawHeight === 'number' ? this._rawHeight : 0;
3117
3171
  }
3118
3172
  // ─── Public API ──────────────────────────────────────
3119
3173
  /** Add a child with optional flex config. Also registers in flex layout. */
@@ -3215,19 +3269,41 @@ class FlexContainer extends pixi_js.Container {
3215
3269
  */
3216
3270
  updateLayout() {
3217
3271
  this._layoutDirty = false;
3218
- const { direction, justifyContent, alignItems, gap, flexWrap } = this._config;
3272
+ const { direction, justifyContent, alignItems, gap, flexWrap, alignContent } = this._config;
3219
3273
  const [pt, pr, pb, pl] = this._padding;
3220
3274
  const isRow = direction === 'row';
3275
+ // Resolve percentage width/height against parent's available space
3276
+ if (typeof this._rawWidth === 'string') {
3277
+ this._explicitWidth = resolveDimension(this._rawWidth, this._availableWidth) ?? 0;
3278
+ }
3279
+ if (typeof this._rawHeight === 'string') {
3280
+ this._explicitHeight = resolveDimension(this._rawHeight, this._availableHeight) ?? 0;
3281
+ }
3221
3282
  const contentW = this._explicitWidth > 0 ? this._explicitWidth - pl - pr : Infinity;
3222
3283
  const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
3223
3284
  const mainLimit = isRow ? contentW : contentH;
3224
3285
  const crossLimit = isRow ? contentH : contentW;
3286
+ // Pass content area to measureChild for percentage resolution
3287
+ const pctRefW = contentW < Infinity ? contentW : 0;
3288
+ const pctRefH = contentH < Infinity ? contentH : 0;
3289
+ // Propagate available size to child FlexContainers and resolve their percentages
3290
+ for (const child of this._layoutChildren) {
3291
+ if (child instanceof FlexContainer) {
3292
+ const fc = child;
3293
+ fc._availableWidth = pctRefW;
3294
+ fc._availableHeight = pctRefH;
3295
+ // If child has percentage dimensions, trigger its layout to resolve them
3296
+ if (typeof fc._rawWidth === 'string' || typeof fc._rawHeight === 'string') {
3297
+ fc.updateLayout();
3298
+ }
3299
+ }
3300
+ }
3225
3301
  // Measure children (skip flexExclude — they position themselves)
3226
3302
  const measured = [];
3227
3303
  for (const child of this._layoutChildren) {
3228
3304
  if (child._flexConfig?.flexExclude)
3229
3305
  continue;
3230
- const { w, h, ox, oy } = measureChild(child);
3306
+ const { w, h, ox, oy } = measureChild(child, pctRefW, pctRefH);
3231
3307
  measured.push({ child, w, h, ox, oy });
3232
3308
  }
3233
3309
  // Split into lines (if wrapping)
@@ -3264,42 +3340,106 @@ class FlexContainer extends pixi_js.Container {
3264
3340
  }
3265
3341
  return maxCross;
3266
3342
  });
3343
+ // Compute natural main size (for auto-sizing when no explicit size given)
3344
+ let naturalMainSize = 0;
3345
+ if (mainLimit === Infinity) {
3346
+ for (const line of lines) {
3347
+ let lineMain = 0;
3348
+ for (const item of line) {
3349
+ lineMain += isRow ? item.w : item.h;
3350
+ }
3351
+ lineMain += gap * Math.max(0, line.length - 1);
3352
+ naturalMainSize = Math.max(naturalMainSize, lineMain);
3353
+ }
3354
+ }
3355
+ // Effective main size: explicit if set, otherwise natural content size
3356
+ const effectiveMainSize = mainLimit < Infinity ? mainLimit : naturalMainSize;
3357
+ // Compute alignContent offsets for multi-line layouts
3358
+ const totalLinesCross = lineCrossSizes.reduce((s, v) => s + v, 0) + gap * Math.max(0, lines.length - 1);
3359
+ let acOffset = 0;
3360
+ let acExtraGap = 0;
3361
+ if (lines.length > 1 && crossLimit < Infinity) {
3362
+ const freeSpace = Math.max(0, crossLimit - totalLinesCross);
3363
+ switch (alignContent) {
3364
+ case 'center':
3365
+ acOffset = freeSpace / 2;
3366
+ break;
3367
+ case 'end':
3368
+ acOffset = freeSpace;
3369
+ break;
3370
+ case 'space-between':
3371
+ if (lines.length > 1) {
3372
+ acExtraGap = freeSpace / (lines.length - 1);
3373
+ }
3374
+ break;
3375
+ case 'stretch':
3376
+ if (lines.length > 0) {
3377
+ const extra = freeSpace / lines.length;
3378
+ for (let i = 0; i < lineCrossSizes.length; i++) {
3379
+ lineCrossSizes[i] += extra;
3380
+ }
3381
+ }
3382
+ break;
3383
+ // 'start' — no adjustment
3384
+ }
3385
+ }
3267
3386
  // Layout each line
3268
- let crossOffset = isRow ? pt : pl;
3387
+ let crossOffset = (isRow ? pt : pl) + acOffset;
3269
3388
  for (let i = 0; i < lines.length; i++) {
3270
3389
  const line = lines[i];
3271
3390
  const lineCross = lineCrossSizes[i];
3272
3391
  const mainStart = isRow ? pl : pt;
3273
3392
  // Offset items by padding
3274
3393
  const tempItems = line.map((item) => ({ ...item }));
3275
- // For single-line layouts, use the full available cross space for alignment;
3276
- // for multi-line (wrapping), each line gets its own measured cross size.
3277
- const effectiveCross = lines.length === 1 && crossLimit < Infinity
3278
- ? crossLimit
3279
- : (crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
3280
- layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
3394
+ // Cross size for alignment: use container cross size for single-line, line cross for multi-line
3395
+ const effectiveCross = lines.length === 1 && crossLimit < Infinity ? crossLimit : lineCross;
3396
+ layoutLine(tempItems, isRow, effectiveMainSize, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
3281
3397
  // Apply main-axis padding offset
3282
3398
  for (const item of tempItems) {
3283
3399
  const origChild = line.find((l) => l.child === item.child);
3284
3400
  origChild.child.x = item.child.x + (isRow ? mainStart : 0);
3285
3401
  origChild.child.y = item.child.y + (isRow ? 0 : mainStart);
3286
3402
  }
3287
- crossOffset += lineCross + gap;
3403
+ crossOffset += lineCross + gap + acExtraGap;
3404
+ }
3405
+ // Compute and store actual dimensions for measureChild() and getContentSize()
3406
+ let totalCrossNatural = 0;
3407
+ for (let i = 0; i < lineCrossSizes.length; i++) {
3408
+ totalCrossNatural += lineCrossSizes[i];
3409
+ if (i < lineCrossSizes.length - 1)
3410
+ totalCrossNatural += gap;
3411
+ }
3412
+ if (isRow) {
3413
+ this._computedWidth = this._explicitWidth > 0 ? this._explicitWidth : (pl + naturalMainSize + pr);
3414
+ this._computedHeight = this._explicitHeight > 0 ? this._explicitHeight : (pt + totalCrossNatural + pb);
3415
+ }
3416
+ else {
3417
+ this._computedWidth = this._explicitWidth > 0 ? this._explicitWidth : (pl + totalCrossNatural + pr);
3418
+ this._computedHeight = this._explicitHeight > 0 ? this._explicitHeight : (pt + naturalMainSize + pb);
3419
+ }
3420
+ // Position flexExclude children (absolute positioning)
3421
+ for (const child of this._layoutChildren) {
3422
+ if (!child._flexConfig?.flexExclude)
3423
+ continue;
3424
+ const cfg = child._flexConfig;
3425
+ const { w, h } = measureChild(child, pctRefW, pctRefH);
3426
+ const cw = this._computedWidth;
3427
+ const ch = this._computedHeight;
3428
+ if (cfg.left !== undefined)
3429
+ child.x = cfg.left;
3430
+ else if (cfg.right !== undefined)
3431
+ child.x = cw - w - cfg.right;
3432
+ if (cfg.top !== undefined)
3433
+ child.y = cfg.top;
3434
+ else if (cfg.bottom !== undefined)
3435
+ child.y = ch - h - cfg.bottom;
3288
3436
  }
3289
3437
  }
3290
3438
  /** Computed content size (after layout) */
3291
3439
  getContentSize() {
3292
3440
  if (this._layoutDirty)
3293
3441
  this.updateLayout();
3294
- let maxX = 0;
3295
- let maxY = 0;
3296
- for (const child of this._layoutChildren) {
3297
- const { w, h } = measureChild(child);
3298
- maxX = Math.max(maxX, child.x + w);
3299
- maxY = Math.max(maxY, child.y + h);
3300
- }
3301
- const [, pr, pb] = this._padding;
3302
- return { width: maxX + pr, height: maxY + pb };
3442
+ return { width: this._computedWidth, height: this._computedHeight };
3303
3443
  }
3304
3444
  /** React reconciler update hook — applies changed config props */
3305
3445
  updateConfig(changed) {
@@ -3311,15 +3451,34 @@ class FlexContainer extends pixi_js.Container {
3311
3451
  this.setAlignItems(changed.alignItems);
3312
3452
  if ('gap' in changed)
3313
3453
  this.setGap(changed.gap);
3314
- if ('padding' in changed)
3315
- this.setPadding(changed.padding);
3454
+ if ('padding' in changed || 'paddingTop' in changed || 'paddingRight' in changed || 'paddingBottom' in changed || 'paddingLeft' in changed) {
3455
+ this._padding = resolvePadding(changed);
3456
+ this._layoutDirty = true;
3457
+ }
3316
3458
  if ('flexWrap' in changed) {
3317
3459
  this._config.flexWrap = changed.flexWrap;
3318
3460
  this._layoutDirty = true;
3319
3461
  }
3462
+ if ('alignContent' in changed) {
3463
+ this._config.alignContent = changed.alignContent;
3464
+ this._layoutDirty = true;
3465
+ }
3320
3466
  if ('width' in changed || 'height' in changed) {
3321
- this.resize(changed.width ?? this._explicitWidth, changed.height ?? this._explicitHeight);
3322
- return; // resize calls updateLayout
3467
+ const w = changed.width ?? this._rawWidth;
3468
+ const h = changed.height ?? this._rawHeight;
3469
+ this._rawWidth = w;
3470
+ this._rawHeight = h;
3471
+ if (typeof w === 'number' && typeof h === 'number') {
3472
+ this.resize(w, h);
3473
+ }
3474
+ else {
3475
+ // Percentage — will resolve in updateLayout
3476
+ this._explicitWidth = typeof w === 'number' ? w : 0;
3477
+ this._explicitHeight = typeof h === 'number' ? h : 0;
3478
+ this._layoutDirty = true;
3479
+ this.updateLayout();
3480
+ }
3481
+ return;
3323
3482
  }
3324
3483
  if (this._layoutDirty)
3325
3484
  this.updateLayout();