Skip to content

Add website-targeted JavaScript Skin Designer integration#4758

Merged
liannacasper merged 56 commits intomasterfrom
codex/build-javascript-version-of-skindesigner
May 4, 2026
Merged

Add website-targeted JavaScript Skin Designer integration#4758
liannacasper merged 56 commits intomasterfrom
codex/build-javascript-version-of-skindesigner

Conversation

@liannacasper
Copy link
Copy Markdown
Collaborator

Motivation

  • Provide a web-friendly JavaScript build of the existing Skin Designer so it can be embedded into the website similarly to the Playground and Initializr, and enable save/export flows in browser mode.

Description

  • Add a website-native ShouldExecute implementation for the JavaScript port that returns true and posts a cn1-skindesigner-ui-ready message to the parent page to support embedded hosting (file: scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_ShouldExecute.js).
  • Add a new site page and Hugo layout that embeds the JavaScript bundle at /skindesigner/ with loading UI and dark-mode sync (files: docs/website/content/skindesigner.md and docs/website/layouts/_default/skindesigner.html).
  • Add a Developers menu entry for "Skin Designer" in docs/website/hugo.toml.
  • Extend scripts/website/build.sh to optionally build and extract the Skin Designer JavaScript bundle into docs/website/static/skindesigner-app using WEBSITE_INCLUDE_SKINDESIGNER, following the same pattern used for Playground/Initializr.

Testing

  • Ran bash -n scripts/website/build.sh which reported no syntax errors in the modified build script.
  • Attempted to build the JavaScript module with sh ./mvnw -q -pl javascript -am -DskipTests -Dcodename1.platform=javascript package in scripts/skindesigner, but the build failed in this environment due to missing Java 8 (JAVA_HOME path not present) and network/maven wrapper download errors (java.net.SocketException: Network is unreachable).
  • Changes were committed and a branch/PR message was prepared; no further automated tests were executed due to the environment limitations described above.

Codex Task

@github-actions
Copy link
Copy Markdown
Contributor

Cloudflare Preview

Comment thread docs/website/layouts/_default/skindesigner.html Fixed
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog
Copy link
Copy Markdown
Collaborator

shai-almog commented Apr 17, 2026

Compared 7 screenshots: 7 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

liannacasper and others added 14 commits May 2, 2026 15:35
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Signed-off-by: liannacasper <67953602+liannacasper@users.noreply.github.com>
…lmer styling

- Hide the Form's default Toolbar so the custom topbar is the only header,
  and replace the green "CN1" pill with a Material phone icon in CN1 blue.
- Replace cn1-pill-border stepper badges with a CircleBadge custom Component
  that paints a true filled disc with the digit (or check) centered, so the
  numbers stop getting cropped on every platform.
- Switch the step separator back to a thin colored Label with explicit
  preferred dimensions; the custom-paint Component was getting stretched
  vertically by BoxLayout into a vertical bar.
- Replace setLeadComponent(label)+addPointerReleasedListener with a
  LayeredLayout overlay carrying a transparent Button (SkinDesignerCardOverlay)
  on top of the card content. Clicks now fire reliably inside scrollable
  parents (device cards, source cards, preset tiles, cutout select area).
- Drop cn1-pill-border from buttons in favor of cn1-round-border 1.6mm with
  explicit .pressed/.disabled selectors, so the toolkit's default pressed
  styling never appears. Filter chips keep cn1-pill-border (small enough to
  render as a true pill) and gain matching pressed variants.
- Defer device-grid rebuild via CN.callSerially when filter chips are
  tapped, lower DEVICE_GRID_LIMIT 200 -> 60, debounce search 180 ms, and
  show a "Showing X of Y - type to narrow" hint, so filter switches feel
  snappy on a 5000-device DB.
- Bump padding/margin throughout: topbar, step head/body, device cards,
  source cards, footer, buttons.

Device DB scraper:
- Add --mode latest that walks GSMArena's latest-mobiles.php3 page; combined
  with --limit, this is the trickle scrape CI now uses.
- Workflow runs every 6 hours on master only (no more pull_request trigger
  that fired on every PR), with an if: github.repository/ref guard so it
  also skips forks and feature branches. workflow_dispatch supports a
  trickle/full mode choice, and the create-pull-request step is gated on
  an actual diff so empty runs don't open noise PRs.

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

Stepper:
- Active step gets a translucent blue pill behind it again (cn1-pill-border
  + e8f0ff bg) so the user can tell which step they're on without reading.
- Number badges go back to a Label sized to a fixed square in code with
  cn1-pill-border, which renders as a true circle and centers the digit
  via the label's intrinsic text alignment. Removed the custom CircleBadge
  painter that was producing badly-aligned numbers.
- Done check uses a smaller, lighter ring (e8f3c8 / 6e8a1a) and a finer
  check icon at 2.6mm rather than the heavy lime disc.

Header text:
- H1 dropped from MainBold to MainMedium and 5.6mm -> 5.0mm; sub goes from
  text-muted (#7f8aa3) to a darker #4a5775 so it's actually readable; H3
  also drops to MainMedium.
- "Which device is this skin for?" sits closer to the topbar now (StepHead
  padding-top 4mm -> 2mm).
- Sub uses a non-editable TextArea so the full second sentence ("...so you
  can focus on the skin shape") wraps properly instead of being clipped.

Search field:
- setHintIcon with the magnifier glyph (MATERIAL_SEARCH).
- cn1-round-border 1.4mm so the field matches the design.

Filter chips (All / Phones / Tablets / Foldables):
- Padding tightened (1.2mm -> 0.8mm vertical, 3.2mm -> 2.4mm horizontal).

Device cards:
- cn1-round-border 2mm; 2.4mm padding; 1mm margin (was 4mm/1.6mm).
- Use MATERIAL_APPLE / MATERIAL_ANDROID brand glyphs in the OS mark instead
  of the silhouette phone icons that read as identical at this size.
- Selecting a device used to call renderStep() which rebuilt the body and
  jumped the scroll. selectDevice() now toggles only the affected cards'
  UIIDs in place via stored card / check / deviceId client properties.
- The selected check icon is always part of the layout (visibility-only
  toggle) so the row width doesn't shift when selection changes.
- OS mark gets a rounded background and the brand glyph at 3.2mm.

Source step:
- "Build a skin for <device>." now renders the device name in bold via a
  three-Label FlowLayout row (prefix + bold + suffix) so the full sentence
  is visible.
- Card descriptions go through a non-editable TextArea so the full two-
  sentence text wraps cleanly. Updated to the exact strings the user
  asked for ("...from there.", etc.).
- Cards: cn1-round-border 3mm + .pressed state with a translucent blue
  fill (closest CN1 CSS-subset analog to :hover, which doesn't exist in
  the compiler today). Inner illustration also gets cn1-round-border 2mm.

Done step:
- Added a "Save again" Button so the user can re-trigger the .skin file
  download if the browser blocked the auto-download or they just want
  another copy. Button is disabled when no skin bytes are cached.
- Renamed "New skin" -> "Make another skin" and gave it a refresh icon.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hover:
- Subclass Form to override pointerHover(int[], int[]) so we can intercept
  hover events at the form level. CN1's CSS subset doesn't expose a :hover
  state and overriding pointerHover on a child Container is unreliable
  when an overlay Button sits on top (the overlay swallows the dispatch).
- New registerHover(Container) registers a card; updateHoverState(x, y)
  walks the registry on each hover event, finds the card under the cursor
  via Component.contains(), and toggles UIID -> UIID + "Hover" exactly
  once when crossing card boundaries. The base UIID is stored on the
  component as a clientProperty so selection-driven UIID swaps still pick
  the right hover variant.
- Wired into source cards (3 boxes on stage 2) and device cards (stage 1).
- renderStep() now clearHoverState()s before rebuilding, so we never hold
  references to detached containers.
- Added *Hover CSS variants for SkinDesignerSourceCard,
  SkinDesignerDeviceCard, and SkinDesignerDeviceCardSelected (light + dark).

Skin download:
- The auto-Display.execute() in buildDoneStep() ran one event-loop tick
  after the Finish click; on browsers that meant the user-gesture context
  was already gone and the download was silently blocked.
- Moved skin generation + write + Display.execute() into a new
  saveSkinFromUserGesture() that the Finish button calls SYNCHRONOUSLY
  inside its action listener, keeping us in the gesture window.
- Done step now shows a big primary "Download skin" Button as a manual
  fallback. Clicking it re-fires Display.execute() (or generates if Finish
  somehow didn't). It's also now the only primary action; the "Make
  another skin" path moved to a secondary button.
- restart() clears lastSkinBytes / lastSkinFile so a fresh wizard run
  doesn't show stale "saved" state.

Bug fix in selection visuals:
- applySelectionToGrid was toggling the inner card's UIID, but
  makeClickable moves the visible UIID to the outer wrapper. Selecting a
  device wasn't actually changing appearance. Now toggles wrapper's UIID
  (and updates baseUIID so hover swaps onto the selected variant).

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

CSS borders were using the wrong syntax. Per docs/developer-guide/css.asciidoc,
cn1-round-border / cn1-pill-border are VALUES used in the background or
border shorthand, not standalone properties. cn1-round-border:1.6mm; was
silently ignored, leaving every "rounded" surface square. Fixes:
- Buttons (Primary / Secondary / Ghost), search field, device cards,
  source cards, source illustration, OS mark — switched to border-radius:
  Nmm so CN1 maps them onto RoundRectBorder.
- Stepper item active pill, stepper number badges, filter chips, done
  check — switched to "background: cn1-pill-border; background-color: ...;"
  which is the documented filled-pill pattern. Stepper number badges
  remain sized to a square in code so the pill renders as a true circle.

Cutout list bug:
- buildCutoutRow was discarding the LayeredLayout wrapper makeClickable
  returns, then re-parenting selectArea into the row. The orphaned
  wrapper kept references and clicking the row threw on the next
  rebuild. Now adds the wrapper directly.

Skin file save:
- The auto Display.execute() inside saveSkinFromUserGesture was firing
  AFTER the slow createSkinBytes() call, by which time the browser's
  user-gesture window had expired and the popup was blocked. Removed
  the auto-trigger; saveSkinFromUserGesture now only generates and
  writes the file. The done step's primary "Download skin" Button
  fires Display.execute() on the cached file path — that runs in a
  fresh, fast user-gesture chain and reliably triggers the download.

Filename redundancy:
- SkinModel.resetForDevice generated "<device.name> skin", which the
  saver then suffixed with .skin → "Apple-iPad-Air-13-2024-skin.skin".
  Drop the trailing " skin" so we save as "Apple-iPad-Air-13-2024.skin".

Safe-area updates with cutouts:
- buildProperties now computes effectiveSafeTop = max(skin.safeTop,
  deepest cutout extent) so a user-added notch / island / hole always
  pushes the safe area below it, regardless of the device's default
  safeTop. Doesn't mutate skin.safeTop, so the user can still see /
  edit the configured value.

Title divider:
- The "really thick separator line" came from TextArea's default
  underline border bleeding through the SkinDesignerSub UIID. Switched
  the device-step sub, source-card description, and done-step message
  from non-editable TextArea to SpanLabel, with fresh
  SkinDesignerSubBlock / SkinDesignerSourcePBlock container UIIDs that
  carry no border styling.

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

Stepper separator:
- The colored Label between step items was getting stretched by BoxLayout
  to fill the row height, producing a really thick vertical bar. Replaced
  with a single em-dash (—) Label - one character can't grow beyond
  its glyph, regardless of how the layout tries to size it.

Brand title:
- "Skin Designer" header dropped from MainBold to MainMedium and a
  notch smaller. Reads less heavy in the topbar.

Material icon backgrounds:
- FontImage.setMaterialIcon copies the host's unselected style into the
  baked image, including bg color & transparency. That meant the icon
  inside SkinDesignerSecondaryButton's white pill carried a white square
  forever - even after applyDarkRecursive flipped the button to dark
  blue (the user reported "the arrow in the Back button has white
  background"). Same bug behind "the green-circle check has a white
  background tinted green".

  Added a small applyMaterialIcon helper that:
   1. Stamps the icon char onto the component as a clientProperty.
   2. Builds a Style copy with bgTransparency=0 so the FontImage's
      backing image only contains the glyph - no solid bg block.
  Replaced all 16 FontImage.setMaterialIcon callers.

  applyDarkRecursive now also re-bakes the icon when it changes a UIID,
  so a runtime theme switch picks up the new fg color.

Hover staleness in dark mode:
- registerHover used to capture baseUIID at build time, but
  applyDarkRecursive runs *after* build and rewrites the UIID, so we'd
  cache "SkinDesignerSourceCard" while the live UIID was
  "SkinDesignerSourceCardDark". On hover-out we restored to the cached
  light variant - that's the "light bg stays light after moving to the
  next item" the user saw.

  Now resolves baseUIID lazily on first hover (whatever the card's UIID
  is at that moment is correct, dark or light), and a small
  hoverVariantOf() helper appends "Hover" or "HoverDark" so we always
  pick the correct themed hover style. applyDarkRecursive also rebases
  cached baseUIIDs on a runtime theme switch, and applySelectionToGrid
  composes the correct dark/light variant when toggling device
  selection.

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

Search hint magnifier in dark mode:
- search.setHintIcon was passing search.getStyle() straight to
  FontImage.createMaterial, so the baked icon image carried the search
  field's solid white background. After dark mode flipped the field to
  a dark blue, the icon still displayed as a white square. Build a
  Style copy with bgTransparency=0 before createMaterial so the icon
  is glyph-only.

Filter toggle losing dark mode on rebuild:
- rebuildDeviceGrid runs outside of renderStep (it's wired to filter
  chip clicks and the debounced search), so the freshly-built device
  cards never went through applyDarkRecursive — they stayed in
  light-mode UIIDs while the rest of the form was already dark. Selecting
  one then swapped just the wrapper to the dark variant via
  applySelectionToGrid, leaving the inner Labels/specs in light mode.
  Now applyDarkRecursive(grid) is invoked at the end of rebuildDeviceGrid
  so all new cards inherit the current theme.

Idle freeze from theme polling:
- UITimer was hitting readThemeFromUrl every 900 ms forever. Each tick
  crosses the CN1 JS-port bridge to read window.location.href, and
  enough idle ticks accumulate enough JS-side work that the browser
  tab eventually locks up. Bumped the interval to 5 s — still
  near-instant response when the user toggles ?theme= on the website
  shell, but ~5x less work over an idle session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tiny fonts (text 0.6mm tall on a 460ppi device):
- DeviceDatabase stores iOS-style point font sizes (12/15/22) which we
  were writing into skin.properties as smallFontSize/mediumFontSize/
  largeFontSize. The simulator (JavaSEPort.java around line 2870) reads
  those as physical pixels and uses them verbatim — on a high-ppi screen
  that renders sub-millimeter text and the UI is unreadable.

  When those properties are absent, the simulator auto-derives them from
  pixelMilliRatio: med = round(2.6 * ppmm), sm = 2 * ppmm, la = 3.3 * ppmm
  — which is the right size for the device. Stop writing the font-size
  properties so the simulator's auto-compute kicks in.

DPI propagation:
- Also write `ppi` directly (the simulator prefers `ppi` over `pixelRatio`
  when both are present — JavaSEPort uses ppi/25.4 to derive the same
  pixelMilliRatio). Keep `pixelRatio` (= ppi/25.4) as a fallback for
  any older skin loaders.

Bezel coordinate consistency:
- skinBezelInPx (used by buildProperties to write safe area / display
  origin) and generatePortraitImage (used to draw the skin and overlay
  PNGs) were computing different bezel pixel values. The skin image
  placed the screen rect at (bezelPx_a, bezelPx_a) but safePortraitX/Y
  was written against bezelPx_b — leaving safe area off by ~10px on
  high-res devices. Both now use:
      bezelPx = round(skin.bezel * (resolutionW / VB_W))
  so the safe-area rectangle aligns with the screen rect in the
  generated skin.png.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Filter chip dark mode regression:
- Click handler always set the light-mode UIID (FilterTag /
  FilterTagActive). In dark mode the chips momentarily flashed to
  light variants — title/text remained visibly light even after
  applyDarkRecursive ran. Click handler now composes the dark suffix
  via themedUiid() so the chip stays themed across clicks.

Safe area coordinate space:
- Looking at Container.snapToSafeAreaInternal in CodenameOne core:
      int safeTopMargin = rect.getY();
      int safeLeftMargin = rect.getX();
  The framework treats the rect from getDisplaySafeArea() as
  *display-relative* margins (origin at the screen's top-left),
  not skin-image absolute coords. We were writing
  safePortraitY = bezelPx + safeTopPx (skin-image coords), so the
  framework saw a top inset of bezelPx + 238 = 327 — pushing UI way
  too far down — but, more relevant for the user, the bezelPx + offset
  meant the iOS theme's title bar was anchored relative to the
  display origin and never reached the inset, leaving it stuck at
  display y = 0 visually behind the dynamic island.

  Now writes safe area in display coords:
      safePortraitX  = 0
      safePortraitY  = safeTopPx
      safePortraitWidth  = device.resolutionW
      safePortraitHeight = device.resolutionH - safeTopPx - safeBottomPx
  Landscape is the 90° rotation: portrait top inset becomes landscape
  left inset, portrait bottom inset becomes landscape right inset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the builtin iPhone X / 11 / 12 / 13 skin behavior the user
expected: a gap above the screen that the notch / island fits into,
and rounded screen corners.

Cutout placement:
- Old behavior: cutouts were drawn as opaque-black shapes inside the
  screen rect (closer to iPhone 14 Pro reality, where the island is
  software-reserved within a rectangular display). The simulator
  renders UI behind the cutout and the iOS 7-era theme has no idea to
  avoid it.
- New behavior: the skin image now reserves a top frame extension
  (height = max cutout extent above screen) and renders cutouts in
  that extension hanging down toward the screen top. The screen rect
  starts BELOW the cutouts. computeTopCutoutPx() / applyTopFrameCutouts()
  encode this; generateOverlay positions the skin_map's screen rect
  to match.
- For NOTCH (c.y=0), the cutout's bottom touches the screen top.
- For ISLAND / HOLE, c.y is the gap (in vb px) between cutout-bottom
  and screen-top.

Rounded screen corners:
- Replaced carveScreenRect with carveRoundedScreenRect: only carves
  the inside of a rounded rect, leaving frame material at the four
  corners. The visible screen now has a rounded outline that matches
  the bezel's outer corner radius minus 8 vb px (consistent with the
  editor preview's screenR formula).
- Screen corner radius = max(0, cornerR - 8) * scale.

Safe-area follow-up:
- Now that cutouts no longer intrude into the screen, dropped the
  cutout-extent contribution to safeTop. The user's configured
  skin.safeTop is the only inset. Default values from
  DeviceDatabase (e.g. 59 for iPhone with island) still cover the
  iOS status bar above the screen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit moved every cutout type into the top frame extension
above the screen, which is correct for iPhone X / 11 / 12 / 13 hardware
notches but wrong for Dynamic Island and Android punch-holes — those are
software-reserved space *inside* a rectangular display, not physical
cutouts. The user wanted the island to "float" on top of the iOS status
bar rather than sit in the device frame.

Now the cutout types are split:

- NOTCH (physical hardware cutout): rendered in the top frame extension
  above the screen, with the bottom edge touching the screen top.
  computeTopCutoutPx() / applyTopFrameCutouts() handle these. Matches
  the builtin iPhone X skin behaviour — a gap above the screen the
  notch fits into.

- ISLAND, HOLE (software / in-display): drawn as opaque pills/circles
  inside the screen rect via applyInScreenCutouts(). The iOS status bar
  paints in the safe area top and the island appears floating on top of
  it, like real iPhone 14 Pro+. computeTopCutoutPx no longer counts
  these so the screen rect doesn't shift down for them.

Safe-area knock-on:
- Restored the cutout-extent contribution to safeTop, but ONLY for
  in-screen cutouts (islands, holes). effectiveSafeTopVB =
  max(skin.safeTop, max(c.y + c.h) for non-notch cutouts) so app
  content lands below the floating island even when the user-set
  safeTop is smaller than the cutout extent.

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

Looking at JavaSEPort.java line ~1622, when roundedSkin is true the
simulator paints the skin image *on top of* the rendered UI buffer
after the UI is drawn:

    if(roundedSkin) {
        Graphics2D bg = buffer.createGraphics();
        BufferedImage skin = getSkin();
        bg.drawImage(skin, ...);
    }

That overlay-on-top render is what makes opaque shapes inside the
screen rect (Dynamic Island, punch-hole cameras) appear floating over
the status bar / app content. Without roundScreen=true, the simulator
takes the skin_map analysis path which clips the UI render to the
black-pixel screen rect — the island circle in skin.png never lands
on top of UI, it just becomes part of the frame visual.

Now the generated .skin sets:
  roundScreen=true
  displayX = bezelPx
  displayY = bezelPx + topCutoutPx           (notch extension, if any)
  displayWidth = device.resolutionW
  displayHeight = device.resolutionH

The display rect points at where the screen actually starts in the
generated skin.png (matches the carveRoundedScreenRect call), and
the simulator overlays the skin — including the floating island /
hole shapes — on top of the rendered UI. The status bar from the
iOS theme paints at the safe-area top and the island appears on top
of it, like real iPhone 14 Pro+.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tutorial:
- New chapter at docs/developer-guide/Skin-Designer.asciidoc walks
  through each wizard stage (device picker, source picker, three
  editor tabs, finish/download), the file layout of a generated
  .skin, and how to refresh the bundled device catalog.
- Wired into developer-guide.asciidoc just before the Maven
  appendices.
- Six embedded screenshots under
  docs/developer-guide/img/skin-designer/ generated by CI.

Demo-mode hooks in SkinDesigner.java:
- applyDemoOverrides() reads cn1.skindesigner.demo* system properties
  and forces the wizard to a specific (step, device, source, preset,
  sidebar tab) combination during loadState. Lets the screenshot
  harness drive the UI to a deterministic state without persisting
  Preferences across runs.

Screenshot harness:
- scripts/skindesigner/screenshots/lib/SkinDesignerScreenshotter.java
  spawns the CN1 simulator JVM with the demo overrides forwarded as
  -Dcn1.skindesigner.* properties, waits for the UI to settle, then
  captures the desktop with java.awt.Robot. Modeled on the existing
  scripts/javase/lib/SimulatorWindowModeVerifier.java.
- scripts/skindesigner/screenshots/take-screenshots.sh builds the
  Skin Designer Maven project, resolves the simulator runtime
  classpath via dependency:build-classpath, compiles the harness, and
  runs each scenario inside xvfb-run on Linux. Six scenarios mirror
  the developer-guide chapter (device picker, source picker, editor
  shape/cutouts/info tabs, done page).
- scripts/skindesigner/screenshots/README.md documents the demo
  override properties and the local-run command.

CI:
- .github/workflows/skin-designer-screenshots.yml runs the script on
  workflow_dispatch and on PRs that touch the harness or wizard code,
  uploads the PNGs as an artifact, and on manual dispatch opens an
  automated PR if the committed images drifted. Excluded from the
  monthly device-DB refresh because the wizard UI changes much less
  often than the catalog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaced the desktop-Robot harness with a Codename One Lifecycle that
uses Display.captureScreen() and saves PNGs through Storage. On the
JavaSE port Storage maps to ~/.cn1/, so the shell script just copies
files out of there into docs/developer-guide/img/skin-designer/.

ScreenshotApp:
- Lifecycle subclass at
  scripts/skindesigner/common/src/main/java/.../screenshots/ScreenshotApp.java
- Walks each wizard stage in turn: clears persistent wizard state
  (Preferences keys + SkinModel.clearPersisted), sets the
  cn1.skindesigner.demo* system properties for the scenario, calls
  new SkinDesigner().runApp(), waits 1.5 s for layout, captures via
  Display.captureScreen(), saves the PNG through Storage.
- After the last scenario calls Display.exitApplication() so the JVM
  exits cleanly.

take-screenshots.sh:
- Drops the Robot / classpath-resolution / harness-compile dance.
- Builds the Skin Designer with mvn install, then launches the
  simulator with -Dcodename1.mainClass pointing at ScreenshotApp.
  xvfb-run is only needed because the JavaSE simulator still creates
  AWT windows, but no Robot capture happens.
- After the simulator exits, copies the PNGs from ~/.cn1/ into
  docs/developer-guide/img/skin-designer/. A missing PNG fails the
  script.

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

github-actions Bot commented May 3, 2026

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: Vale failed (exit code 2) (report)
  • Image references: 1 unused image(s) found (report)

Unused image preview:

  • img/skin-designer/.gitkeep

shai-almog and others added 10 commits May 3, 2026 17:56
…checkout

The CN1 ZipSupport cn1lib's main.zip is created by the
install-cn1lib plugin goal on local first-run and isn't tracked in git
(only the ZipSupport.cn1lib bundle and the cn1libs/ZipSupport/pom.xml
are). On a fresh CI checkout mvn install fails at the build-helper
attach-artifact step because cn1libs/ZipSupport/jars/main.zip doesn't
exist:

    [ERROR] Failed to install artifact ... skindesigner-ZipSupport:jar:common ...
    /home/runner/work/.../cn1libs/ZipSupport/jars/main.zip

Added an explicit extract step at the top of take-screenshots.sh that
unzips main.zip out of the .cn1lib bundle if it isn't already present.
The .cn1lib is just a zip containing main.zip + stubs.zip + the
codenameone_library_*.properties metadata, so unzip -p does the job
without needing the CN1 plugin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The css goal of the CN1 plugin (cn1-process-classes execution)
instantiates JavaSEPort to compile theme.css. JavaSEPort's static
initializer calls calcRetinaScale -> getDefaultScreenDevice(), which
throws HeadlessException on a fresh GitHub-actions runner. Need a
display for mvn install just like for the simulator launch itself.

Wrapped both phases in xvfb-run when available so the build runs
under a virtual X server. Local non-Linux invocations still use the
plain mvn invocation since macOS has a real display.
The screenshots CI run failed inside the simulator because
Display.isDarkMode() returns a Boolean that is null on the headless
Linux JavaSE port. readThemeFromUrl() unboxed it directly and crashed
SkinDesigner.runApp before the first form could render.

Treat the null return as "not dark" and let the URL ?theme= override
keep working.

Also drop two CSS values the bundled designer's CSS compiler
(7.0.228) does not understand and was logging as parse errors:

- cn1rgba(...) translucent overlays -> opaque hex equivalents
- font-family "native:MainMedium" -> "native:MainLight"

These were warnings, not fatal, but they polluted the CI log and the
hover/pressed colors silently fell back to the default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Skin Designer chapter references six screenshots generated by the
skin-designer-screenshots workflow. Until that workflow lands its
first successful run, lychee link-checking on the Hugo build fails
with "Cannot find file" on each missing PNG, blocking the website
build for every PR on this branch.

Commit 8x8 grayscale placeholders under the same names so the build
passes. The screenshots workflow overwrites these with the real
captures on its first successful run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The website builds the Hugo site from the same checkout that hosts
the developer guide, so the Skin Designer chapter's PNGs need to
exist by the time scripts/website/build.sh runs. Previously the only
producer was the standalone skin-designer-screenshots workflow,
whose output never made it onto the website branch — every Hugo build
failed lychee link-checking on six missing PNGs, and the placeholder
PNGs added in the prior commit would have shipped to production.

Wire the screenshots into the website pipeline directly:

- Trigger website-docs.yml on changes under docs/developer-guide/ and
  scripts/skindesigner/ so wizard or guide edits rebuild the site.
- Insert Java 17 + xvfb + CodenameOne tooling steps and run
  take-screenshots.sh before the existing website build, so the PNGs
  land in docs/developer-guide/img/skin-designer/ in time for
  build.sh's rsync into static/developer-guide/.
- Drop the placeholder PNGs committed in the previous fix; the build
  now produces real ones.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier the screenshots step booted the full JavaSE simulator via
mvn -Psimulator verify with -Dcodename1.mainClass=ScreenshotApp. On
CI the simulator launched, started the source-change watcher, then
sat idle until the workflow timed out at 25 minutes — the Lifecycle
override never reliably wired up inside the simulator launch path.

Replace the harness with a plain main():

- ScreenshotApp.main() calls Display.init(new java.awt.Container())
  in CN1's quiet test mode, walks each scenario on the EDT, lays the
  form out at iPhone-class dimensions, and writes the PNG via
  Form.toImage() + FileOutputStream + cn1 ImageIO.
- take-screenshots.sh runs `mvn install -pl common -am` to compile
  and CSS-bake, then `mvn exec:java -Dexec.mainClass=...` for the
  capture. No cn1:simulator, no verify lifecycle.
- Output lands directly in OUT_DIR (no Storage round-trip via ~/.cn1
  needed any more).
- Drop the standalone skin-designer-screenshots.yml; the website-docs
  workflow now produces the PNGs on demand for every Hugo build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ScreenshotApp's Display.init needs ImplementationFactory, which
codenameone-javase provides under test scope in skindesigner-common's
pom. Running exec:java with classpathScope=compile threw
ClassNotFoundException at startup. Switch the screenshot exec to
classpathScope=test so the JavaSE port is on the classpath.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ScreenshotApp bypasses Lifecycle.start, so the bundled theme.res
never gets registered with UIManager. Without it Form.getToolbar()
returns null and SkinDesigner.runApp NPEs on the first tweak.

Mirror Lifecycle.start by calling UIManager.initFirstTheme("/theme")
right after Display.init so each scenario renders against the same
themed UIIDs the wizard expects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UIManager.initFirstTheme alone does not flip Toolbar.globalToolbar —
the flag is only ever set by an explicit Toolbar.setGlobalToolbar
call (CN1's bundled simulator does this; Lifecycle.start does not).
Without it, Form.getToolbar() stays null and SkinDesigner.runApp
NPEs on its first form.getToolbar().setHidden(true).

Set the flag explicitly after Display.init in the screenshot
harness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reframe the chapter to match how users actually meet the tool — at
codenameone.com/skindesigner, in a browser tab, no install. The
prior chapter pitched it as a standalone CN1 app with three flavours
and a "Launching the tool" section walking through run.sh / .bat;
remove that, point at the URL, and treat the screenshots as captures
from the hosted tool.

Tighten the prose:

- Cut implementation details that don't concern users (Preferences
  keys reset, in-memory state mgmt, the catalog refresh script and
  scrape-source mention).
- Refer to the "device catalog" rather than devices.json.
- Drop the "Refreshing the bundled device catalog" appendix entirely
  — that's a CI/maintainer concern.

Widen the screenshot harness from a phone-class 390×844 to a
desktop-class 1280×800 viewport so the editor's two-column layout
renders at the same width a website visitor sees.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Skins menu now leads with the two actions a user actually
reaches for:

- "Add Skin" — file picker for a .skin (renamed from "Add New...")
- "Skin Designer" — opens https://www.codenameone.com/skindesigner/
  in the user's default browser, replacing the bundled gallery
  flow as the way to build a new skin

Everything else from the previous Skins menu — the radio-button
list of bundled skins, Desktop.skin, UWP Desktop.skin, the
"More..." Cloudflare gallery, and "Reset Skins" — moves under a
"Legacy Skins" submenu. The gallery downloader's refresh callback
now repopulates that submenu instead of rebuilding the whole menu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@liannacasper liannacasper merged commit fd1d0b9 into master May 4, 2026
15 of 16 checks passed
shai-almog added a commit that referenced this pull request May 5, 2026
The scheduled trickle scrape has been hitting a 404 since #4758 landed
(latest-mobiles.php3 doesn't exist on GSMArena), so every 6h run pulled
zero phones and only opened PRs because the JSON envelope churned
between runs (e.g. #4862). Replace the trickle path with a weekly run
of the verified per-brand walk, and make the script skip writing the
file when no device records actually change so envelope drift can't
spam PRs again.

- Workflow: weekly Mon 03:00 UTC, single brands scrape, 75m timeout
  (cold cache). Drop the mode dispatch input and trickle/full split.
- Script: drop dead --mode latest path (walk_latest, RE_LATEST_LINK,
  --mode flag). Add _devices_changed() so the file is only rewritten
  when actual records added/removed/modified, ignoring envelope diffs.
- README: document the new weekly cadence.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RFE: clean up the old skins (and maybe add some more of the recents ones like iOS Pro Max, 13" iPad, ...)

3 participants