Skip to content

UIManager: add zoomFonts(factor) to scale theme font sizes#4798

Merged
shai-almog merged 3 commits intomasterfrom
uimanager-font-zoom
Apr 23, 2026
Merged

UIManager: add zoomFonts(factor) to scale theme font sizes#4798
shai-almog merged 3 commits intomasterfrom
uimanager-font-zoom

Conversation

@liannacasper
Copy link
Copy Markdown
Collaborator

Summary

  • Adds UIManager.zoomFonts(float factor) — multiplies every scalable font in the current theme (and the default styles) by factor, e.g. 1.2 enlarges by 20% and 0.8 shrinks by 20%.
  • System fonts are left untouched (guarded via Font.isTTFNativeFont()), since their size is fixed by the underlying platform and Font.derive only supports TTF/native fonts.
  • Handles both already-parsed Font values and unparsed String entries in themeProps; parses strings via parseFont and only scales the result when it's a TTF/native font.
  • Rejects non-positive factors with IllegalArgumentException; 1f is a no-op. Zoom is relative — successive calls compound (call with the reciprocal to undo).
  • Clears the style/image caches and calls current.refreshTheme(false) so live components pick up the new sizes, matching the pattern used by addThemeProps.

Test plan

  • mvn compile in maven/core — BUILD SUCCESS.
  • Load a theme in the simulator, call UIManager.getInstance().zoomFonts(1.2f), verify TTF-based UIIDs render larger and system-font UIIDs are unchanged.
  • Call zoomFonts(1f / 1.2f) afterwards and confirm sizes return to approximately their original pixel size.
  • Call zoomFonts(0f) / negative value and confirm IllegalArgumentException.

🤖 Generated with Claude Code

Introduces a public API that multiplies every scalable font in the
current theme (and the default styles) by a given factor, e.g. 1.2
enlarges by 20% and 0.8 shrinks by 20%. System fonts are skipped via
Font.isTTFNativeFont() since their size is fixed by the platform. The
styles cache is cleared and the theme refreshed so live components
pick up the change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Javadoc now spells out that zoomFonts updates the theme but does
  not repaint live forms; callers need Form.refreshTheme() to apply
  the new sizes to an already-displayed form.
- New UIManagerZoomFontsTest covers: TTF zoom in/out, system-font
  skip, compounding calls, reciprocal restoration, factor-of-one
  no-op, invalid factor, and default-style scaling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog
Copy link
Copy Markdown
Collaborator

shai-almog commented Apr 23, 2026

Android screenshot updates

Compared 37 screenshots: 36 matched, 1 error.

  • graphics-draw-shape — comparison error. Comparison error: PNG chunk truncated before CRC while processing: /home/runner/work/_temp/cn1ss-cg2ute/graphics-draw-shape.png

    No preview available for this screenshot.
    Full-resolution PNG saved as graphics-draw-shape.png in workflow artifacts.

Native Android coverage

  • 📊 Line coverage: 7.90% (4190/53047 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 6.20% (20680/333602), branch 3.02% (972/32188), complexity 3.68% (1132/30772), method 6.48% (929/14343), class 10.63% (202/1900)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 783.000 ms
Base64 CN1 encode 119.000 ms
Base64 encode ratio (CN1/native) 0.152x (84.8% faster)
Base64 native decode 935.000 ms
Base64 CN1 decode 310.000 ms
Base64 decode ratio (CN1/native) 0.332x (66.8% faster)
Image encode benchmark status skipped (SIMD unsupported)

List.fireActionEvent silently drops events when Display.hasDragOccured()
is true, so a prior test that simulated a drag (DraggableTabsTest,
DnDRegression3048Test, PointerEventsTest, etc.) could make the next
test's fireActionEvent a no-op. This surfaced as a flaky
RSSReaderCoverageTest.testEventHandler where the EventHandler never
ran and the model stayed empty.

Reset dragOccured, pointerPressedAndNotReleasedOrDragged, and
dragPathLength in the shared @AfterEach via reflection so subsequent
tests start from a clean pointer state. Verified: injecting
dragOccured=true before the test reproduces the CI failure; the
reset eliminates it. Full core-unittests suite (2366 tests) stays green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

Generated automatically by the PR CI workflow.

@shai-almog
Copy link
Copy Markdown
Collaborator

shai-almog commented Apr 23, 2026

iOS screenshot updates

Compared 36 screenshots: 35 matched, 1 updated.

  • landscape — updated screenshot. Screenshot differs (2556x1179 px, bit depth 8).

    landscape
    Preview info: Preview provided by instrumentation.
    Full-resolution PNG saved as landscape.png in workflow artifacts.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 323 seconds

Build and Run Timing

Metric Duration
Simulator Boot 101000 ms
Simulator Boot (Run) 2000 ms
App Install 18000 ms
App Launch 12000 ms
Test Execution 200000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 2807.000 ms
Base64 CN1 encode 2213.000 ms
Base64 encode ratio (CN1/native) 0.788x (21.2% faster)
Base64 native decode 1122.000 ms
Base64 CN1 decode 1548.000 ms
Base64 decode ratio (CN1/native) 1.380x (38.0% slower)
Base64 SIMD encode 625.000 ms
Base64 encode ratio (SIMD/native) 0.223x (77.7% faster)
Base64 encode ratio (SIMD/CN1) 0.282x (71.8% faster)
Base64 SIMD decode 561.000 ms
Base64 decode ratio (SIMD/native) 0.500x (50.0% faster)
Base64 decode ratio (SIMD/CN1) 0.362x (63.8% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 106.000 ms
Image createMask (SIMD on) 21.000 ms
Image createMask ratio (SIMD on/off) 0.198x (80.2% faster)
Image applyMask (SIMD off) 196.000 ms
Image applyMask (SIMD on) 141.000 ms
Image applyMask ratio (SIMD on/off) 0.719x (28.1% faster)
Image modifyAlpha (SIMD off) 194.000 ms
Image modifyAlpha (SIMD on) 108.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.557x (44.3% faster)
Image modifyAlpha removeColor (SIMD off) 233.000 ms
Image modifyAlpha removeColor (SIMD on) 122.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.524x (47.6% faster)
Image PNG encode (SIMD off) 3744.000 ms
Image PNG encode (SIMD on) 1260.000 ms
Image PNG encode ratio (SIMD on/off) 0.337x (66.3% faster)
Image JPEG encode 701.000 ms

@shai-almog shai-almog merged commit a6f88f1 into master Apr 23, 2026
16 checks passed
liannacasper pushed a commit that referenced this pull request Apr 24, 2026
The rebase brought in #4801 (CN1 version bump 7.0.234 -> 7.0.235),
#4794 (Simd warnings fix), and #4798 (zoomFonts). Regenerate
GeneratedCN1Access + helper classes so the bean-shell registry
matches the new API surface: picks up UIManager.zoomFonts,
additional components/ui/plaf members, and the cleaned-up util
package (no more alloca* escapes from Simd, which is also excluded
explicitly by the preceding commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liannacasper pushed a commit that referenced this pull request Apr 24, 2026
The JavaScript cloud build fails at runtime with
  com.codename1.ui.plaf.UIManager.zoomFonts(F)V was not found
because zoomFonts was added in CN1 7.0.235 (#4798) but the JS port
deployed to the cloud doesn't have it yet. The bean-shell access
registry references it directly, which turns a missing method into
a startup failure for every playground session.

Add a qualified-name method blacklist to the generator and seed it
with com.codename1.ui.plaf.UIManager.zoomFonts. Kept separate from
the name-only blacklist so the exclusion is surgical (other classes
that happen to define a zoomFonts method, if any appear later, are
unaffected). Once the JS port ships zoomFonts this entry can come
straight out.

Regenerated registry reflects the exclusion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shai-almog added a commit that referenced this pull request Apr 24, 2026
* Playground UI redesign

* Fixed compilation issue

* Improved layout size calculation

* Improved device display

* Skin now uses a border to bring round corners in

* Added Android theme and fixed no-skin mode

* Better fidelity of selection and better sidebar positioning

* Improved title/button UX and restored the split pane

* Refined top-bar chrome, split pane, and editor gutter

- Switched the editor/preview layout to a SplitPane with 25/50/75 bounds,
  no expand/collapse arrows or drag-handle, a thin PlaygroundSplitDivider
  line, and matching dark variant.
- Status pill: 50% translucent background, pill border, BoxLayout.x so the
  dot is vertically aligned with the label regardless of font ascent.
- Share/Download: round-rect border with stronger share outline; download
  text stays white in dark mode.
- Code/CSS segmented group: rounded outer border + rounded selected pill.
- Playground wordmark uses MainRegular instead of MainBold.
- CSS toggle icon falls back to MATERIAL_BRUSH (the wireframe alternative
  to the filled MATERIAL_PALETTE, which CN1's font ships only in solid).
- Orientation segmented is now icons-only regardless of layout density.
- Button icon/text gap scales with DPI (1.3mm) instead of 2 raw pixels.
- Monaco gutter narrower and lighter (glyphMargin off, lineNumbersMinChars
  2, lineDecorationsWidth 4, softer line-number colors, matching
  editorGutter.background).
- Cleared the default BrowserComponent border so the editor sits flush.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Side-panel redistribution, scroll axis, and status-pill polish

- Moved inspector and samples/history slots out of the SplitPane to
  center.EAST / center.WEST so opening Inspector no longer squishes the
  device skin; the SplitPane holds just editor and preview.
- previewContainer.setPreferredW(82mm) so BorderLayout keeps a minimum
  wide enough to fit the iPhone portrait skin even when the SplitPane
  right side is dragged inward.
- Stage scroll axis is now exclusive: portrait / no-skin uses scrollableY,
  landscape (skinned) uses scrollableX. Never both at once.
- Panel show/hide animation: drop the revalidate fallback so
  animateLayout(220) on the shared center container can actually
  interpolate between before/after layouts.
- Status pill:
    * Translucency bumped 0.5 -> 0.75 (darker Live background).
    * Pill border drawn only by the outer container; inner Label uses a
      new PlaygroundStatusLabel{,Error}{,Dark} UIID (transparent, no
      border) so the pill isn't drawn twice.
- Top-bar app icon: in dark mode switches to a white MATERIAL_WIDGETS
  glyph on an rgba(255,255,255,0.1) rounded pill (new PlaygroundAppIcon
  {,Dark} UIIDs) instead of the bundled icon.png, whose white
  background read poorly against the navy bar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rebuild inspector, fix dark-mode app icon, widen preview reserve

Inspector:
- Custom tree rows with chevrons for containers and outlined type icons
  (folder for containers, document for leaves). Depth-indented 3mm base
  plus 5mm per level, 7mm row height, flush rows.
- IDENTITY / CONTENT / APPEARANCE / LAYOUT section headers between field
  groups with 1px dividers above each non-first section.
- Single-column form: 22mm label + input-fills-rest, replaces the old
  two-column grid.
- Inputs get 1.5mm corner radius; dark #0F2A4D on 1px #1F3A5F border.
- Color fields pair a hex input with a 9x9mm live swatch.
- Bounds / Padding / Margin render as four separate inputs with a row
  of micro-labels (X Y W H or T R B L) directly beneath.
- Unit selection is now a horizontal segmented control (Dips / Pixels)
  in a standard form row rather than a floating button group.
- Added PlaygroundTreeRow{,Active}, PlaygroundTreeChevron,
  PlaygroundTreeType{,Active}, PlaygroundTreeBracket{,Active},
  PlaygroundInspectorSection, PlaygroundInspectorDivider,
  PlaygroundField{Row,Label,Input,ReadOnly,Micro},
  PlaygroundInspectorSwatch, PlaygroundInspectorSegment{,Active,Inactive},
  PlaygroundInspectorCheckbox UIIDs with dark variants and registered
  them in supportsDarkVariant().

Top bar:
- App-icon dark mode bug: setMaterialIcon bakes the glyph using the
  label's current FG color, so setUIID must be applied before the icon
  - otherwise the icon froze black and reappeared as a solid blob once
  the white-on-translucent UIID kicked in. Reordered + switched to
  MATERIAL_CODE which reads cleanly at 4.5mm.

Layout:
- previewContainer.setPreferredW 82mm -> 92mm so the bezel, corner mask,
  and SplitPane divider don't crowd the skin when Inspector is open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix inspector tree selection and halve design-mm values

Tree rows:
- Replaced the narrow BorderLayout.EAST hit-Button (which only
  covered a sliver of the row) with a full-width Button body
  containing the type icon + combined label text, so clicking
  anywhere on the row's visible content selects the component.
  The chevron remains a separate Button on the west for
  expand/collapse only.
- Merged the type name and bracket into a single Button.setText
  since Button can't nest two differently-styled Labels.

Sizing:
- The inspector spec used "mm" as design-doc millimeters, which
  at CN1's physical-mm conversion render roughly 2x too large.
  Halved layout dimensions and font sizes across the panel:
  label column 22->12mm, tree indent 3/5->1.5/2.5mm, row height
  7->4mm, swatch 9->5mm, input corner 1.5->0.8mm, section header
  font 2.6->1.8mm, field label/input font 3->1.9mm, micro 2.2->
  1.4mm, segmented font 2.8->1.8mm, tree text 3.3->2mm, icon
  sizes 3.5/4->2.2/2.5mm, tree max height 70->38mm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Inspector polish: unified scroll, tooltips, conditional sections

- Unified the tree and property form into a single scrollable Y
  Container. Eliminates the dual bounce when scrolling nested
  scroll areas.
- Pass the user's script Component to the Inspector as the tree
  root directly, instead of the preview wrapper chain (content
  host / bezel / screen). Root now shows the user component.
- Removed the per-cell micro-labels (X/Y/W/H, T/R/B/L) below
  Bounds, Padding, and Margin fields. The same information is
  now exposed as a per-input tooltip (X, Y, Width, Height; Top,
  Right, Bottom, Left).
- CONTENT section only renders when the selected component has
  editable text (Label / TextField / TextArea); never renders an
  empty "Text: -" placeholder when the section is not applicable.
- Dropped the thin inter-section dividers. A single stronger
  divider separates the tree from the property form
  (PlaygroundInspectorTreeDivider) and reaches the panel edges
  via negative horizontal margins.
- Even spacing between multi-value fields: each sub-input has a
  symmetric 1 DIP horizontal margin and the surrounding grid
  uses negative outer margins so the overall row alignment is
  preserved and X-Y / T-R no longer touch.
- Sizes bumped from 50% back up to roughly 75% of the original
  spec (label column 16 DIP, tree row 5.2 mm, tree indent 2 +
  3.8 DIP per level, input padding 1/1.6 mm, swatch 5 mm, font
  sizes 2-2.5 mm).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Inspector: reliable row selection, visible divider

Tree rows:
- Replaced the Button-in-CENTER click target with a pointer-released
  listener on the row Container itself. Selection now fires on a
  release anywhere in the row (including the icon and label regions),
  except when the release is within the chevron's absolute X range -
  chevron retains its own ActionListener for expand/collapse.
- Row height bumped to 6 mm so the hit target is comfortably tall.

Divider between tree and property form:
- Previously the divider had negative horizontal margins to punch past
  the root's 2 mm inner padding; CN1 clipped the line so it never
  rendered. Moved the padding off PlaygroundInspectorRoot{,Dark} and
  applied it inline on treeContainer / propertiesContainer so the
  divider now spans the full panel width without needing any negative
  margin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Explicitly disable scroll on inner inspector containers

CN1 Container defaults to non-scrollable but this has burned us
before: the previous Tree-based tree view enabled Y scroll in its
own constructor, which produced the bouncy dual-scroll feel. Keep
scroll explicitly off on treeContainer and propertiesContainer so
only unifiedScroll handles pointer drags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Simplify: just setScrollable(false) on tree

propertiesContainer defaults to non-scrollable anyway - only the
tree needs the explicit disable since the old Tree-based parent
enabled scrolling in its own constructor. Collapsed the separate
X/Y calls into one setScrollable(false).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Restore Button-in-CENTER selection for tree rows

The pointer-released listener on the row Container does not
fire reliably inside the outer scrollable container: the scroll
container swallows the gesture when a drag begins. Reverted to
the explicit Button-in-CENTER approach which does get the click
events directly (and worked before). Button with Alignment.LEFT
still stretches to fill the row width because the UIID has
padding 0 and it sits in BorderLayout.CENTER.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Match dot-to-label gap to the top-bar icon/text spacing

The Live dot used a 1px margin against its text, while Share
and Download buttons use a 1.3mm gap between their icon and
label. Aligned the dot's right margin to 1.3mm DIPS so the
spacing reads consistently across the top bar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Inspector: revalidate root after selection so row + panel refresh

Clicking a tree row fired handleComponentSelected which ran
updatePropertyPanel and rebuildTree; each of those revalidated
their own Container. But CN1's revalidate() re-lays out the
called Container in place without propagating to the parent,
so the outer unifiedScroll was unaware of the new preferred
sizes - the updated rows and new property form stayed hidden
until an unrelated layout pass. Revalidate + repaint the root
inspector component at the end of selection to force the whole
subtree to re-render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Inspector: vertical SplitPane between tree and property form

Swapped the unified scroll for a vertical SplitPane with 30/50/
70 insets, showing the same thin PlaygroundSplitDivider line as
the main editor/preview split. Tree (top) and properties
(bottom) are each independently scrollable Y, which keeps the
two panes' revalidate passes local so clicking a leaf actually
updates the property form - the old shared scroll didn't
propagate sizes up from the inner containers so selection
highlight showed but the form didn't repopulate.

Dropped the custom PlaygroundInspectorTreeDivider build helper
since the SplitPane provides the divider.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Force property pane re-layout on selection

propertiesContainer sits scrollableY inside a SplitPane. The
inner revalidate() in updatePropertyPanel fires but the new
children end up with zero measured size until the container's
ancestor chain (scroll wrapper + pane) re-lays out -- hence the
pane stayed blank until a manual resize. revalidateWithAnimationSafety
walks up from the container, which forces the pane to re-measure
and the field rows render immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Defer inspector repopulation so property pane renders on click

Button-triggered selection fires while CN1's tactile press
animation is still running. Revalidating during the animation
caused AnimationManager to snap the pending layout to the
animation's final frame, leaving newly-added field rows at
measured size 0 until the next unrelated resize. Deferred the
panel rebuild + tree rebuild to the next EDT tick via
CN.callSerially, and revalidate the entire form afterwards so
the SplitPane and its inner scroll both re-measure with the
new content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Inspector: strip selection back to the minimum

Rolling back the rebuildTree + form-wide revalidate + callSerially
layering - each fix fought the previous one and the property pane
still didn't render reliably on click. Keep just the three things
that must happen on selection:

  1. selectedComponent = c
  2. highlightComponent(c)
  3. updatePropertyPanel(c)

updatePropertyPanel already calls propertiesContainer.revalidate()
once internally, which is the single layout pass we actually need.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Inspector: revalidateLater + misc polish

Inspector:
- Replace the inner-pane revalidate() with revalidateLater(), which
  queues via the Form's paint-cycle revalidate queue instead of
  firing mid-callback. Calling revalidate() from inside the Button
  ActionListener (while the tactile press animation was in flight)
  had CN1's AnimationManager snap the pending layout to the
  animation's final frame, leaving the freshly-added rows at
  measured size 0 until a subsequent resize kicked a fresh layout.

Live indicator:
- Gave the dot 2 mm DIPS of right padding (inside the wrapper)
  instead of the 1.3 mm margin, which was still reading as too
  tight at the edge.

Activity bar:
- Symmetric horizontal margin (0 both sides) and right-padding
  bumped 0.2 mm to compensate for the 0.8 mm reserved border-left
  rail, so icons sit visually centred in the column rather than
  slightly left.

Samples panel:
- Hint reads "Search" and the hint Label gets a magnifying-glass
  MATERIAL_SEARCH icon (via TextField.getHintLabel() + setIcon).

History panel:
- Row wrapper padded (3 mm left) so the two text lines align with
  the panel header's 3 mm left padding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Selection order, history alignment, icon column

Inspector:
- Update property panel BEFORE highlighting the selected
  component. highlightComponent sets the form's glass pane
  and calls form.repaint(), which was consuming the pending
  revalidateLater() queued by updatePropertyPanel - hence
  the blank pane on first click and the fix working after
  a manual resize. Expanding a container (chevron click)
  never called highlight and always worked - that was the
  hint.

History panel:
- setPadding takes (top, bottom, left, right); I had the
  values in the wrong slots so the 3mm inset ended up on
  the right rather than the left. Fixed so entries line up
  under the HISTORY title.

Top bar:
- Wrapped the app icon in a fixed-width 13mm column matching
  the activity bar width, and removed the 3mm left padding
  on the top bar UIID. The title icon now sits exactly above
  the activity bar icons and the wordmark starts where each
  left-side panel's title would start, so the whole left edge
  reads as a single aligned column.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Selection: defer panel rebuild via callSerially; tighten icon column

Inspector selection:
- Wrap updatePropertyPanel in CN.callSerially so the panel rebuild
  runs on the next EDT tick, after the Button's tactile press /
  release handling has fully settled. revalidate() called inside
  the Button's own ActionListener was fighting the press animation,
  which produced the intermittent click-does-nothing behaviour.
- Remove the explicit form.repaint() call in highlightComponent
  (and the matching one in clearHighlight); CN1 paints a new glass
  pane on the next natural frame, and the extra repaint was racing
  with the press animation + the queued property-panel revalidate.

Icon column alignment:
- Activity bar and the top-bar app-icon column both narrowed from
  13 mm to 11 mm so the left-column icons occupy more of their
  width and feel less empty on the right.
- Activity button padding trimmed from 2/2.8/2/2 to 2/1.5/2/0.7
  to visually center the icon against the 0.8 mm reserved border
  rail without the 1-mm right bias previously perceived.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Inspector: forceRevalidate + scroll reset; icons +1mm left padding

Inspector:
- Drop the callSerially deferral (it didn't fix the stale pane)
  and the revalidateLater(). Use forceRevalidate() instead, which
  walks the entire sub-tree marking every descendant for a fresh
  preferredSize calc before re-laying out -- plain revalidate was
  leaving descendants at stale size 0 from the previous pass.
- Reset propertiesContainer's scroll to the top on rebuild so the
  first rows of the new content aren't above the visible viewport
  when a prior selection had scrolled the pane down.

Activity icons:
- +1 mm of left padding on each activity button (0.7 -> 1.7 mm)
  per your suggestion so the icon visually lines up with the
  top-bar app-icon column.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix compile: setScrollY is protected, drop scroll reset

CN1 Component.setScrollY is protected, so keep just forceRevalidate
without the scroll reset - forceRevalidate's layout pass positions
the children starting at y=0 relative to the container already.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Inspector: isolate content from scroll; icons +1mm more left pad

Inspector:
- propertiesContainer is now a plain non-scrollable BoxLayout.y
  that holds the field rows. A dedicated propertiesScroll wrapper
  (BorderLayout with the content at NORTH, setScrollableY(true))
  sits between it and the SplitPane's bottom pane. On selection,
  updatePropertyPanel mutates only the inner Container, so the
  scroll wrapper's layout state is never recreated and a plain
  propertiesContainer.revalidate() re-lays out the new children
  reliably - no more forceRevalidate / callSerially / revalidateLater
  trying to outrun a scroll wrapper that was being reset mid-update.

Activity icons:
- +1 mm more left padding (1.7 -> 2.7 mm) so each icon visually
  aligns with the title icon above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix IllegalArgumentException: margin cannot be negative

Selection threw IllegalArgumentException from Style.setMargin - CN1
rejects negative margin values, but I was using (0, 0, -1, -1) on
the multi-value field grid to compensate for the symmetric per-cell
margins. Removed the negative outer compensation and trimmed the
per-cell margin to left-only, applied to every cell except the first,
so the four sub-inputs have a consistent inter-cell gap without the
outer grid edges needing any negative offset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Inspector: make the property pane actually scroll

Switched the scroll wrapper's layout from BorderLayout with
content at NORTH to BoxLayout.y. BorderLayout.NORTH clamps its
child to its own viewport height, so scrollableY never engaged
when content overflowed. BoxLayout.y stacks by preferred height,
which lets the wrapper's scrollable state kick in as soon as the
content is taller than the pane.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mobile adaptation: strip chrome, wire bottom nav

Mobile layout now differentiates from tablet-compact and actually
responds to bottom-nav taps.

Top bar (setMobile):
- Code/CSS mode toggle hidden (tab strip below takes its role).
- Status pill collapses to dot-only via new setCompactDot, leaving
  the text label hidden so only the coloured dot sits next to the
  app icon.

Preview column (setMobile):
- Preview toolbar (device / orientation / dimensions) hidden.
- Device forced to DEVICE_NO_SKIN; restored to the previously-active
  device when the layout reverts to tablet/desktop.
- Stage fills the full tab area without a synthetic bezel.

CN1Playground:
- assembleDesktopLayout and assembleMobileLayout call setMobile(false)
  and setMobile(true) respectively on both topBar and previewColumn.
- applyActivity branches on isMobileLayout(): on mobile, bottom-nav
  selection replaces the tab content with the chosen panel while
  the top bar + bottom nav stay in place; the panel's own close
  button deselects the nav item and restores the current tab.
- refreshMobileTabContent consults mobilePanelFor() first - if a
  bottom-nav panel is active it renders instead of the Code/CSS/
  Preview tab content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mobile: iOS theme by default, smaller text, robust bottom nav

- No-skin AND iPhone both default to iOS base theme in
  applyDeviceTheme (Pixel keeps Android). "No skin" meant "no
  bezel", not "no theme".
- PlaygroundTopBar.applyAppIconStyle sets the label FG color
  inline before creating the Material icon so the icon is baked
  in white in dark mode regardless of when UIID resolution runs.
- PlaygroundStatusPill.setCompactDot now hides both text AND its
  wrapping FlowLayout, so the pill collapses to just the coloured
  dot on mobile instead of retaining the wrap's width.
- Mobile top-tab strip and bottom-nav fonts shrunk (tab 2.8->2.4mm,
  nav 2.4->2mm) so "Preview" fits on tight phone widths.
- assembleMobileLayout defensively resets bottomNav's hidden /
  visible state and sets an explicit 12mm preferredH so the
  Samples / Inspector / History row is always visible even if
  an earlier flow (keyboard-show hook) toggled it off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Top bar: shrink on mobile; add layout harness

Top bar:
- setCompact now also setHidden(true) on the wordmark, not just
  setVisible(false). setVisible alone leaves the ~30mm preferred
  width reserved, pushing the status pill / Share / Download off
  screen on mobile. setHidden collapses it to 0 preferred width.
- setMobile shrinks the icon column from 11mm to 8mm, drops the
  Share/Download icon glyph size from 3mm to 2.2mm, so the title
  bar actually has room for the status dot on narrow viewports.

Tests:
- New PlaygroundLayoutHarness. Boots the Playground lifecycle
  headless via Display.init, lets the EDT tick, then walks the
  shown Form tree verifying the presence + non-zero size + in-
  bounds position of top-bar UIIDs, the activity bar (desktop)
  or top-tab strip + bottom nav (mobile). Runs for both desktop
  and mobile by toggling the cn1.desktop / cn1.tablet system
  properties. Hooked into run-playground-smoke-tests.sh so CI
  catches regressions where a chrome element is hidden, zero-
  sized, or rendered outside the form bounds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Harness: guard null toolbar + surface runApp exceptions

CN1Playground.runApp did Form.getToolbar().setUIID(...) unconditionally,
but getToolbar can return null in a Display.init(null) headless
context - hence the layout harness's "runApp did not complete within
3s" after the hidden NullPointerException. Guard the toolbar
configuration with a null check so the app works in both the
simulator and real HTML5/iOS/Android runtimes (where getToolbar
returns a real Toolbar instance).

Also surface any throwable from runApp in the harness's callSerially
runnable so CI logs the real exception instead of a timeout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Harness: force layout via hook + skip hidden when searching

Display.isDesktop / isTablet ignore external system properties in
the JavaSE simulator, so the "mobile" scenario was still getting
the desktop shell (hence "TopTabs NOT FOUND"). Added a package-
private testOnlyForceLayout hook on CN1Playground that bypasses
the Display checks when set. Harness sets it to LAYOUT_MOBILE /
LAYOUT_DESKTOP per scenario and resets to LAYOUT_NONE afterwards.

Also updated findByUiid to skip hidden / invisible subtrees. The
PlaygroundSegment UIID is used by BOTH the top bar's Code/CSS
toggle (setHidden on mobile) AND the mobile tab strip. Without
the hidden-skip the first match was the invisible mode toggle,
and the harness failed the size check.

Mobile checks now look for PlaygroundSegment{,Dark} (the tab
strip container) and PlaygroundBottomNav{,Dark}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Breakpoint by viewport width; harness checks bottom-nav items

Layout breakpoint:
- Switched from Display.isDesktop() / isTablet() back to measuring
  viewport width via Display.getDisplayWidth() / convertToPixels(1f).
  isDesktop() always returns true on a desktop browser regardless
  of window width, so narrowing the browser never triggered the
  mobile shell. The spec is clear: < ~720 CSS px (190 mm) = mobile,
  < ~1100 CSS px (291 mm) = tablet, else desktop. d.isTablet() is
  still honoured as a secondary signal for native tablet builds.

Harness:
- Added an explicit "bottom nav must have three visible items"
  check. Previously the presence check on PlaygroundBottomNav only
  verified the container, which is why "3 buttons completely gone"
  slipped past CI. countVisibleItems walks the container and counts
  children with non-zero measured size and isVisible/!isHidden.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Harness: assert bottom-nav items have icon or text

Added verifyBottomNavItems that, for each Button child of the
bottom nav, asserts it has at least an icon OR a non-empty text.
A Container with 3 empty Buttons would look "completely gone"
visually but pass the simple count check. Also checks that the
nav has exactly 3 Button children (Samples / Inspector / History).

Local harness: all checks pass in the JavaSE simulator. If the
HTML5 runtime still renders blank nav buttons after deployment
picks up this commit, the regression is on the rendering path
(icon font loading, peer z-order) rather than on construction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mobile: bottom nav at Form.SOUTH, not bodyContainer

User report: "toolbar flickering in for a second below the HTML
component which means it's working but probably failing due to a
z-ordering or layout issue."

Root cause: the editor's BrowserComponent is a DOM iframe peer that
paints above the CN1 canvas. Its peer bounds track the editor
Component's bounds. With bottomNav inside bodyContainer -> mobileLayout
-> SOUTH, any time bodyContainer's height exceeded the expected
remaining-after-nav band (e.g. on first layout pass before SOUTH is
reserved), the iframe's bounds followed and covered the bottomNav.
Only after a resize would the Form-level layout reconcile, at which
point the bottomNav flashed through briefly before the iframe resized.

Fix: plant bottomNav at appForm.SOUTH directly, as a sibling of
bodyContainer. The Form-level BorderLayout carves out bottomNav's
preferredH BEFORE allocating CENTER, so bodyContainer can never
extend into the nav band - and neither can any peer iframe inside it.

assembleDesktopLayout also detaches bottomNav so the desktop shell
doesn't end up with both an activity bar and a leftover bottom nav.

Local harness confirms: PlaygroundBottomNav sits at pos=(5,1668)
size=2930x121, full form width, with 3 visible item buttons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mobile: post-show relayout + diagnostic logging

Added a 150ms UITimer after appForm.show() that resets the
cached layout key, re-runs applyLayoutForCurrentSize, and
revalidates the form. Browsers (especially mobile UA modes)
sometimes report a placeholder display size at the moment of
show, and the initial breakpoint / layout computation can
miss the real viewport. A second pass after the form has
settled guarantees the correct shell.

Also logging:
- Breakpoint computation: display width in px and mm, plus
  the chosen layout (MOBILE/TABLET/DESKTOP).
- bottomNav's final parent, absolute position, size, and
  visible/hidden flags after the post-show pass.

This gives us a deterministic way to tell from the browser
console whether the mobile shell is even being chosen, and
whether bottomNav ends up in a sane position. If it's in
the right spot but still visually covered, the issue is
the BrowserComponent peer iframe's z-order (canvas under
DOM) and needs a different fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add build marker log at runApp start

The earlier diagnostic logs never appeared in the user's browser
console, which suggests the PR preview deploy hadn't picked up
the mobile-shell commits. Added a top-of-runApp Log.p marker so we
can tell at a glance whether the deployed build is the new one.

On the next page reload the console should show:
  CN1Playground build marker: mobile-shell-v2 @ 2026-04-24
  Playground layout: width=... (...mm) -> MOBILE|TABLET|DESKTOP
  Form: WxH
  bottomNav: parent=... pos=(...) size=... visible=... hidden=...

If the marker is missing, the preview hasn't rebuilt yet; wait a
couple of minutes and hard-reload. If the marker appears but
subsequent lines don't, runApp is failing mid-way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mobile: inject iframe max-height guard via JS

Diagnostic output confirmed: on mobile the layout is correct --
form 1544x2628, bottomNav at y=2242 size=1480x354 with
visible=true, hidden=false. The problem is CN1 HTML5Peer's DOM
iframe (Monaco editor) extending past its Java component's
bounds and covering the canvas-drawn bottom nav.

Mitigation: installIframeBottomGuard injects a <style> tag into
document.head with:

  @media (max-width: 720px) {
    iframe { max-height: calc(100vh - 64px) !important; }
  }

so no iframe can extend into the bottom 64px band regardless of
what the HTML5Peer positioning does. Runs once after
appForm.show() via the shared JS context.

This is a band-aid: the real fix would be making the nav a peer
itself, but the CSS cap is a deterministic way to prove the
diagnosis and give a working mobile UX while that larger change
is scoped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Hide inactive editor iframes so they don't cover bottom nav

You were right - peer components respect their bounds when they
are properly attached and sized. The covering peer was not the
ACTIVE editor but its idle sibling:

- We construct two BrowserComponents in runApp (editor for Java,
  cssEditor for CSS). Both create their iframes immediately in
  their constructors - CN1 HTML5 adds the iframe to the DOM up
  front, and we set HTML5Peer.removeOnDeinitialize=false to
  preserve Monaco state across dialog show/hide.
- Only the editor matching the current mode is ever attached to
  the CN1 tree via attachEditorsToHost. The other BrowserComponent
  is never in the tree, so its peer has no Java bounds to sync
  against. Its iframe sits at whatever default CSS size it was
  given (typically full-viewport) and paints above everything
  including the canvas-drawn bottom nav.

Fix: explicitly setVisible(false) on both editors at construction.
attachEditorsToHost flips the active editor's setVisible(true),
leaving the idle one hidden. refreshMobileTabContent calls
hideAllEditors() when Preview is active or a bottom-nav panel
covers the tab, since neither editor should paint in those cases.
CN1 HTML5 propagates setVisible(false) to the peer, which sets
display:none on the iframe and removes it from the compositor
until reattached.

Removed the installIframeBottomGuard CSS band-aid and its call
since the real fix is in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "Hide inactive editor iframes so they don't cover bottom nav"

This reverts commit 2d4afd2.

The idle-editor-covering hypothesis was wrong: tabs already hide the
inactive component, so setVisible(false) on a non-attached
BrowserComponent has no impact on what the browser composites.
Restoring the previous behaviour so the next diagnostic pass has a
clean baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mobile: log visualViewport vs window size after first layout

The bottom nav's Java bounds look correct in every log (pos, size,
visible, hidden all sane), yet users don't see it on the HTML5
deploy. Before guessing again, add a browser-side diagnostic: after
the post-show revalidate, read window.innerWidth/innerHeight,
visualViewport.width/height, documentElement.clientHeight, and
devicePixelRatio from JS and log the result via the existing CN
javascript context.

Mobile Safari in particular reports innerHeight as the layout
viewport (tall) while visualViewport.height reflects the area that
is actually not occluded by the dynamic URL bar - CN1 sizes the
canvas to innerHeight, so if visualViewport.height is materially
smaller, the bottom ~100px of the canvas is drawn behind browser
chrome and the bottom nav would never be visible no matter how
correct the CN1 layout is.

The log line lands next to the existing "Form:" and "bottomNav:"
lines so the three can be compared against each other on the next
deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mobile: probe DOM at bottom-nav coordinates, not just viewport

The viewport log settled the browser-chrome question - window and
visualViewport matched exactly, so the nav is not hidden behind
iOS Safari's URL bar. Yet the nav is still not visible.

Replace the viewport probe with a DOM probe that answers three
questions directly from the browser:

1. Where is the CN1 canvas, really? Its rect + z-index tells us
   whether the canvas itself is full-height or clipped.
2. Where are all iframes, with z-index + visibility + display?
   Lists every iframe so we can see if any extends into the nav
   band, and whether any is display:none or visibility:hidden.
3. What element does elementFromPoint(x, navCenterY) return at the
   centre of each of the three expected nav cells (left, middle,
   right)? Returns the topmost paintable element with its bounding
   rect and z-index. If it's a CANVAS, the nav is drawn but the
   style is somehow invisible. If it's an IFRAME, a peer is
   covering it. If it's HTML/BODY, the canvas is clipped short.

Converts navY from device pixels to CSS pixels via dpr so the JS
coordinates match browser coordinate space.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mobile: read canvas pixels at nav Y to see if CN1 painted the nav

Previous probe found cn1-peers-container on top at the nav
coordinates, but CN1's HTML5 port marks the canvas with
pointer-events:none (via codenameone-canvas setProperty in the
TeaVM-compiled classes.js) so elementFromPoint just skips the
canvas - that doesn't mean the nav is hidden, only that the canvas
can't be hit-tested. Visually the canvas is still on top (z=auto,
DOM-ordered AFTER peers-container which is z=-1000).

To actually know whether the nav paints, read back the pixel at
each of the three expected nav-cell X positions at the nav's Y on
the canvas via getImageData. If the pixels are the expected nav
UIID colour, CN1 is painting the nav and something else (still TBD)
is hiding it; if they are white/transparent, CN1 never draws the
nav band and the bug is on the Java side.

Also widens the DOM probe to return computed bg/opacity/pointer-
events for peers-container and canvas, plus the elementsFromPoint
stack at the nav centre for a full view of the compositing order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mobile: scan whole DOM for overlappers + draw red marker on canvas

Canvas pixel read proved CN1 paints the nav (navy rgba(16,43,102,255)
at the expected Y). So the bug is compositing, not CN1 layout. Two
oddities to pin down:

1. BODY is position:fixed with bg=rgb(255,255,255). If it outranks
   the canvas in stacking order, its opaque white background could
   paint over the canvas-drawn nav.

2. Some other fixed/absolute/sticky element may overlap the nav
   band. Walk every element, filter to positioned ones whose rect
   intersects [navTop, navBot], and dump id/class/z-index/bg/etc.
   Skip the canvas and the peers-container (already understood).

Also paint a 8px red band directly on the canvas at navTop and a
green band at navBot. If the user sees red+green but no nav, the
canvas IS visible - the nav paint got overpainted later. If the
user sees no red+green, the canvas as a whole is being occluded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Playground: exclude com.codename1.util.Simd from bean-shell registry

CI compliance check failed with 9 violations of the form:
  bsh/cn1/gen/GeneratedAccess_com_codename1_util#invoke10(
      Lcom/codename1/util/Simd;Ljava/lang/String;[Ljava/lang/Object;
  ) -> SIMD alloca value returned from method

com.codename1.util.Simd exposes allocaByte / allocaInt /
allocaFloat (+ *Zeroed / *Filled variants) that return method-local
SIMD scratch arrays. On ParparVM these lower to C-stack allocations;
CN1's compliance check forbids letting the array escape the method
that allocated it, and the auto-generated reflection bridge
inherently does exactly that by returning the value from invokeN.

Fix: add Simd to the existing INTERNAL_CN1_TYPES set so the
generator skips it the same way it already skips Accessor and
IOAccessor. Simd is a low-level SIMD primitives API that bean-shell
playground scripts are extremely unlikely to need, so the blast
radius is tiny. If a user does need it later we can generate a
hand-written bridge that respects the method-local constraint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Playground: regenerate bean-shell registry against CN1 7.0.235

The rebase brought in #4801 (CN1 version bump 7.0.234 -> 7.0.235),
#4794 (Simd warnings fix), and #4798 (zoomFonts). Regenerate
GeneratedCN1Access + helper classes so the bean-shell registry
matches the new API surface: picks up UIManager.zoomFonts,
additional components/ui/plaf members, and the cleaned-up util
package (no more alloca* escapes from Simd, which is also excluded
explicitly by the preceding commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Playground: exclude UIManager.zoomFonts from bean-shell registry

The JavaScript cloud build fails at runtime with
  com.codename1.ui.plaf.UIManager.zoomFonts(F)V was not found
because zoomFonts was added in CN1 7.0.235 (#4798) but the JS port
deployed to the cloud doesn't have it yet. The bean-shell access
registry references it directly, which turns a missing method into
a startup failure for every playground session.

Add a qualified-name method blacklist to the generator and seed it
with com.codename1.ui.plaf.UIManager.zoomFonts. Kept separate from
the name-only blacklist so the exclusion is surgical (other classes
that happen to define a zoomFonts method, if any appear later, are
unaffected). Once the JS port ships zoomFonts this entry can come
straight out.

Regenerated registry reflects the exclusion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Playground: pin bean-shell registry one CN1 release behind cn1.version

Every new API that lands in the CN1 release channel but not yet in
the JavaScript cloud build bricks the playground with NoSuchMethod
at shell-eval time - first it was UIManager.zoomFonts, now
isSimdOptimizationsEnabled, and more will keep appearing on every
version bump. The one-off qualified-name blacklist doesn't scale.

Fix: introduce <cn1.registry.version> in the pom (pinned to 7.0.234,
one release behind cn1.version=7.0.235) and have the generator
prefer it when downloading release source jars. Falls back to
cn1.version if the registry version isn't set, so consumers outside
this project aren't broken. Bump <cn1.registry.version> deliberately
once the JS cloud build catches up, not implicitly with cn1.version.

With the pin in place, UIManager.zoomFonts is not discovered at all,
so the ad-hoc qualified-method blacklist can go. Kept the generator
scaffolding empty-but-ready in case a port-specific regression
needs a surgical workaround later.

Regenerated registry reflects 7.0.234 API surface: zoomFonts,
isSimdOptimizationsEnabled, and the rest of the post-7.0.234 API
additions are absent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* CI: pin lychee-action to v0.23.0 to dodge asset-rename regression

The website-docs workflow's "Validate internal links and images"
step fails in CI with:
  Downloading from: https://github.com/lycheeverse/lychee/releases/
    latest/download/lychee-x86_64-unknown-linux-gnu.tar.gz
  ##[error]Process completed with exit code 22.

Root cause: lychee v0.24.0 renamed release assets from
  lychee-<arch>-unknown-linux-gnu.tar.gz
to
  lychee-lychee-v0.24.0-<arch>-unknown-linux-gnu.tar.gz
but lychee-action@v2 still constructs the old URL, so
`lycheeVersion: latest` now resolves to a 404 and curl bails out
with exit 22.

Pin to v0.23.0, the last release with the stable asset naming.
This is orthogonal to the Playground work - the actual Playground
cloud JS build + smoke tests succeed in the same job; only the
downstream link check trips on the upstream action bug. Bump back
to `latest` once lychee-action@v2 catches up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mobile: log each nav child's geometry + sample canvas at its centre

User reports a thin bar visible at the nav position that clicks as
the correct tab but has no icons or text. Canvas pixels at nav
centre were confirmed opaque navy in the previous probe, and
nothing else in the DOM overlaps the nav region. So the Button
children of the nav are the missing piece - either zero-sized, or
painting only their UIID background with no glyph rendering.

Two diagnostics:

1. For each nav child, log its absolute position, size, preferred
   size, visibility, UIID, text, and icon dimensions. If any of
   these are zero the layout pipeline broke; if they look right,
   the paint pipeline broke.

2. Sample the canvas pixel at the CENTRE of each button, roughly
   where the Material icon glyph should be painted (offset by
   height/3 from top so we land on the icon, not the text). If all
   three pixels are the nav UIID colour, no glyph got painted at
   all. If they vary (lighter/darker shades, foreground-colour
   hits), glyphs are being painted and something else is hiding
   them.

Removes the now-redundant DOM-overlapper scan and the red/green
canvas markers - both answered their questions already.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mobile: force bottom-nav item FG before baking Material icon

Root cause identified. The navBtnPixels probe came back as solid
navy UIID bg at every icon centre - no glyph pixels at all. The
navChild probe showed buttons with correct size/position/icon-dims
and real text. So the icons were baked in a colour that matches
the nav's background, making them invisible on top of it.

FontImage.setMaterialIcon bakes the glyph using whatever foreground
colour the component's style has at the instant of the call.
setUIID application can be deferred on the HTML5 port, so the bake
captures the default FG, which collides with the nav's navy bg.

PlaygroundTopBar.applyAppIconStyle already documents this pitfall
and works around it by calling getAllStyles().setFgColor(...) before
FontImage.setMaterialIcon. Mirror the same pattern for the bottom
nav:

  - createBottomNavButton: setUIID -> force FG from a table that
    mirrors the `color:` values in theme.css's
    PlaygroundBottomNavItem{Dark,Active,ActiveDark} UIIDs -> bake.
  - refreshBottomNav: on state flip, re-set UIID, re-force FG, and
    re-bake the icon with the new colour. The icon is cached on
    the Button as a baked image, so just changing the UIID wouldn't
    update it.
  - Stash the icon char on the Button via `navIcon` client property
    so refreshBottomNav can rebake without the creation-time char
    in scope.

Also remove the diagnostic probes (navChild/navBtnPixels logs, the
canvas pixel sampler) that were only useful for narrowing down the
root cause.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Mobile: centre bottom-nav icons, drop diagnostic scaffolding

Icon centering
  Set Button alignment to CENTER in createBottomNavButton (default
  is LEFT, which leaves the glyph hugging the left padding on the
  HTML5 port once the FG bake fix made it actually paint) and add
  `text-align: center` to the four PlaygroundBottomNavItem* UIIDs
  in theme.css so the same placement survives a UIID-level restyle.

Cleanup for merge
  - Remove the `CN1Playground build marker: mobile-shell-v2` boot
    log, the per-relayout `Playground layout:` log, and the comment
    block that describes them.
  - Remove installIframeBottomGuard() and MOBILE_NAV_GUARD_PX. It
    was a speculative CSS max-height injection for iframe peers,
    added before the real cause of the missing nav was known; the
    actual fix (FG bake colour) is now in place and the guard adds
    nothing.
  - Rewrite the Form.SOUTH nav comment: the structural choice still
    stands, but the "iframe covers the nav" rationale was wrong.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RFE: enable programmatic support for changing the font sizes of the app

2 participants