Image assets (3/3): widget config UI + settings image-cache card (replicates mxtommy/Kip #1082) - #62
Conversation
resolveKipPluginBaseUrl() extracts the <server>/plugins/kip/ resolution (mirroring kip-series-api-client) and snapImageWidth() snaps a container width (x DPR) to the server's variant allow-list so the client requests stable, cache-friendly URLs. ImageAssetService wraps the plugin endpoints: upload (multipart FormData with progress), list, delete, cacheStats, purgeCache, and urlFor(id, cssWidth, dpr) for the widget. Requests are auto-authenticated by the JWT interceptor. 9 vitest cases green.
New widget-image renders a selected image (by id) via ImageAssetService, scaled to fit the widget with object-fit (contain/cover) over a configurable background that defaults to transparent so the dashboard shows through. It tracks its container width with the existing kipResizeObserver directive and requests a server variant matched to that width (x DPR) so a small widget never downloads a full-resolution image. Shows an empty state when nothing is selected. Registered in WidgetService (component map + Component-category definition); adds the `image` config block to IWidgetSvcConfig. 3 vitest cases (empty state, render+object-fit, background default/override).
Lets users upload an image (client-side 10 MB + type guard, progress), pick or delete from the shared library, and set the scaling, alt text, and a background color or transparent background for the Image widget. Wired into the widget config Display tab for widgets that expose an image config group.
Shows the on-disk size of generated image variants and a Purge button (with confirmation). Purging keeps originals; variants regenerate on demand. Refresh re-reads the size after viewing images.
Splits the thumbnail into a select button and a sibling delete button so both are keyboard-focusable and avoid an invalid nested-button structure.
Make the gallery selection a signal so the highlight updates under zoneless change detection even when set from async upload/delete callbacks, instead of relying on an incidental signal write. Add a sequence guard so a slow image list() response can't overwrite a newer one.
Add a small horizontal gap between the Refresh and Purge buttons on the settings image-cache card (the footer is text-align:end, not flex).
The image cache card was placed on the Configurations tab, which is for KIP configuration management (backup, restore, import, export). Move it to a new Media tab, placed before Configurations, so the image cache lives with media concerns rather than config management. Extracts the image-cache logic into a SettingsMediaComponent (with its own spec) and removes it from SettingsConfigComponent. Updates the Image widget help doc to point at Settings → Media → Image Cache.
The Image widget had no icon in the widget picker (icon was empty). Add an "imageWidget" landscape/image glyph to the shared SVG icon set and reference it from the widget definition so the widget shows a recognizable icon.
Acts on a UX audit of the Image widget. Safety / state - Deleting an image now asks for confirmation, naming the shared, boat-wide scope — a single tap previously removed it from every display with no undo. The delete control moved to a 44px button in a footer row, out of the select tap zone. - The gallery distinguishes a failed library load from a truly empty one (loading spinner, error + Retry), so an unreachable server no longer reads as "no images". The live widget shows "Image unavailable" instead of a broken glyph when its image disappears. - Upload and delete share one status-aware error mapper. Accessibility - Thumbnails expose selection (aria-pressed) and group semantics, visible keyboard focus, a named delete label, and a live region announces upload/delete status. "Alt text" is now "Description" with a hint; thumbnails are captioned with the image name and sorted newest-first. Clarity / theming - Plainer copy throughout (errors, the Fill option, the cache card's "Clear cache"). Touch-safe color control. The opaque background seeds from the active theme instead of hardcoded black, transparent diagrams sit on a neutral checkerboard, and a live preview shows the chosen fit/background. Updates the component specs (delete confirmation, 401 mapping, gallery error state, newest-first sort, theme-seeded background).
Relabels the help text to match the new UI ("Alt text" -> "Description",
"Purge" -> "Clear cache") and refreshes the screenshots: the redesigned
config dialog (shared "Boat image library" with thumbnails, captions and
footer delete) and the Media -> Image Cache card showing a real cache size.
Also points the help menu at the renamed image-widget.md and gitignores the
Playwright MCP working dir.
Spins up an open (no-login) Signal K server with the integrated KIP image plugin + webapp, seeded with realistic marine diagrams, so the Image widget help screenshots (gallery, thumbnails, real cache size) can be reliably re-captured as the UI evolves. The image feature spans three branches (server/client/config-ui); the README documents merging the server branch and `npm pack` to produce the kip.tgz the Dockerfile installs (which resolves native deps like sharp for the container).
main replaced static widget imports with a lazy _componentTypeMap resolved by getComponentType(). Re-expressed the Image widget registration as a lazy import() entry instead of the eager static import + reference; the _widgetDefinition entry (componentClassName WidgetImageComponent, selector widget-image) is unchanged. Verified: lint clean, dev build succeeds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ETfeXXs8R5ciw6nGxGYj2C
…etup Merging the lazy-widget-registry main exposed a latent circular import: RootModalWidgetConfigComponent (imports ImageSourceSetupComponent) → ImageSourceSetupComponent (injected DialogService) → DialogService (imports RootModalWidgetConfigComponent to open it). In the full test bundle this left ImageSourceSetupComponent undefined when root-modal's standalone imports compiled (TypeError reading 'ɵcmp'); it passed in isolation, only surfacing with the whole suite's load order. image-source-setup used DialogService solely for a delete confirmation. Open DialogConfirmationComponent through MatDialog directly instead, dropping the DialogService import and cutting the cycle; sibling config components already avoid DialogService for the same reason. Spec updated to mock MatDialog. Verified: lint clean, dev build succeeds, full vitest suite 576/576. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ETfeXXs8R5ciw6nGxGYj2C
Content review: accept-with-fixes — clean, idiomatic client; one real reactivity bugRead the widget, config sub-component, Genuinely solid (verified):
Findings:
Plus low: an initial 0-width container snaps to the 2560 px variant → a full-res fetch then an immediate re-fetch at the real size. Bottom line: well-built, idiomatic zoneless-Angular with correct subscription and race handling; the reactivity bug is the real one (widget blank after boot until interaction). Adopt together with #54 — this client is inert without the server plugin. (Resolution applied earlier: Image widget → main's lazy registry, and broke a |
… review) - ImageAssetService.pluginBaseUrl is now a signal, so the Image widget's imageUrl computed re-runs once the SK endpoint resolves after first paint, instead of staying blank until the widget is resized or reopened. - The widget distinguishes "no image configured" from "configured but not loadable yet": the latter shows "Loading image…" rather than the misleading "No image selected" on a safety display. - snapImageWidth snaps an unknown/zero container width to the SMALLEST variant, not the largest, so the initial 0-width render no longer fetches the full-resolution image and immediately re-fetches at the real size. Tests: reactive recompute + loading state (widget), reactive base URL (service), smallest-for-zero-width (util). Full vitest suite 579/579. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ETfeXXs8R5ciw6nGxGYj2C
Fixes pushedAddresses the correctness + empty-state findings:
Tests: reactive recompute + loading state (widget), reactive base URL (service), smallest-for-zero-width (util). Full vitest suite 579/579; CI green. |
The Image widget help doc used parent-relative (../../assets/...) image paths, which the help-docs path guard (#1056, now on main) correctly rejects — they don't resolve under the app's base href. Make them base-relative like the other help docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ETfeXXs8R5ciw6nGxGYj2C
Resolves the additive conflicts now that #62 (image) is on main: both features register in widget.service.ts (Image + Video lazy entries), add their config component to root-modal-widget-config, extend widgets-interface, and add help-docs menu + gitignore lines — kept both sides. Deps union (hls.js/piexifjs + sharp/heic/multer/dompurify); lockfile regenerated. Fixed the video help-doc images to base-relative paths (the merged #1056 guard rejects ../). Verified: lint clean, dev build succeeds, full vitest suite 748/748. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ETfeXXs8R5ciw6nGxGYj2C
Replicates upstream PR mxtommy/Kip#1082 ("Image assets (3/3): widget config UI + settings image-cache card") by dillan.
This is the third part of the image-assets series: the Image widget, its
image-source-setupconfig UI (upload, gallery, fit/background), theImageAssetService+ shared plugin-URL helper, and an image-cache card surfaced under a new Media settings tab.Method: commit-by-commit cherry-pick of the upstream PR range (linear history, no merge commits). One commit (
d3b07875, "Use takeUntilDestroyed on image-cache subscriptions") became empty after conflict resolution and was skipped, because its only change targeted the image-cache code that SKip keeps on its own side ofconfig.component(see below).The upstream PR first adds an image-cache card directly into
config.component, then a later commit (adf95687) moves that card out into a brand-newmedia.componentMedia tab. SKip'sconfig.componentis independently diverged (rewritten around profiles), so the intermediate "add card to config" states conflicted. Because the PR's net effect onconfig.componentis zero (the card lives in the new Media tab), every conflict there was resolved by keeping SKip's version:src/app/core/components/options/configuration/config.component.ts— kept SKip's profiles-based version; the cache-card logic lands in the newmedia.componentinstead.src/app/core/components/options/configuration/config.component.html— kept SKip's version (no cache card in config).src/app/core/components/options/configuration/config.component.scss— kept SKip's version (verified byte-identical to base).src/app/core/components/options/configuration/config.component.spec.ts— kept SKip's version.CHANGELOG.md— kept SKip's changelog; the upstream entry was not merged.The new
media.component, the Media tab wiring intabs.component, theImageAssetService, the Image widget, and theimage-source-setupconfig UI were all applied unchanged from upstream. The resulting net diff is identical to the upstream PR except for the intentionally-preserved SKipCHANGELOG.md.Needs a build + review before merge.
Note
This is an experimental replica carried in the fork for evaluation. No build/install/lint was run during replication;
package-lock.jsonwas not affected (the PR adds no dependencies).