diff --git a/openspec/changes/archive/2026-10-01-carbon-ui-redesign/.openspec.yaml b/openspec/changes/archive/2026-10-01-carbon-ui-redesign/.openspec.yaml new file mode 100644 index 0000000..480ad4d --- /dev/null +++ b/openspec/changes/archive/2026-10-01-carbon-ui-redesign/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-01 diff --git a/openspec/changes/archive/2026-10-01-carbon-ui-redesign/design.md b/openspec/changes/archive/2026-10-01-carbon-ui-redesign/design.md new file mode 100644 index 0000000..5b71ffe --- /dev/null +++ b/openspec/changes/archive/2026-10-01-carbon-ui-redesign/design.md @@ -0,0 +1,60 @@ +# Design + +## Context + +Pages are Vue 2 + Bootstrap 4 via CDN with swal/toastr dialogs; behavior and +endpoints are frozen for this change (see proposal.md). Carbon web components +v2.50.0 are available as bundled per-component `.min.js` artifacts on IBM's CDN +(verified: button, text-input, dropdown, notification [inline-notification + +toast], progress-bar, tag, checkbox, textarea, tile, ui-shell, modal, loading); +`@carbon/styles` CSS (jsdelivr) provides tokens + Carbon-styled native +controls; IBM Plex Sans comes from Google Fonts. + +## Goals / Non-Goals + +**Goals:** Carbon look across landing + both tools with zero build step; +Vue 2 kept as the thin state layer; server untouched. + +**Non-Goals:** Framework rewrite; UX/flow changes; self-hosting assets; +replacing vue-croppa. + +## Decisions + +1. **CDN delivery, version-pinned (v2.50.0)** — same pattern as the existing + Vue/axios CDNs; zero tooling. Self-hosting is the fallback if CDN + availability becomes a field problem. +2. **Hybrid controls** — true web components where Vue interacts lightly + (cds-button click, cds-tag, cds-inline-notification, cds-toast, + cds-progress-bar, cds-tile); Carbon-styled native controls where v-model + matters (text inputs, selects, checkboxes, textarea use + `cds--text-input`/`cds--select-input`/`cds--checkbox` classes from + @carbon/styles CSS). Avoids value/event adapter friction with shadow-DOM + inputs. +3. **Notification models replace dialogs** — wizard: one inline-notification + slot (`notif` state, auto-dismiss on success); cropper: a fixed + `cds-toast` viewport fed by a `toasts` array with timed removal. Native + `confirm()` stays for dequeue (behavior frozen). +4. **`Vue.config.ignoredElements = [/^cds-/]`** plus a + `customElements.whenDefined('cds-button').then($forceUpdate)` so + pre-upgrade property bindings (disabled, progress value) re-apply after + upgrade. +5. **style.css rewritten on Carbon tokens** (layout helpers kept: grid, gaps, + details grid, editor grid, croppa pad, footer). + +## Risks / Trade-offs + +- [Shadow-DOM inputs would fight Vue v-model] → native controls for fields; + web components only for click/display surfaces. +- [IBM CDN availability] → pinned version; self-hosting is a drop-in swap of + the script/link URLs. +- [Lit property set before upgrade] → forceUpdate after whenDefined; disabled + states verified in the visual harness. + +## Migration Plan + +Rewrite pages/styles → visual harness screenshots (landing, wizard, cropper at +390/768/1280) → unit tests (server untouched) → docker smoke → walkthrough. + +## Open Questions + +None. diff --git a/openspec/changes/archive/2026-10-01-carbon-ui-redesign/proposal.md b/openspec/changes/archive/2026-10-01-carbon-ui-redesign/proposal.md new file mode 100644 index 0000000..071944d --- /dev/null +++ b/openspec/changes/archive/2026-10-01-carbon-ui-redesign/proposal.md @@ -0,0 +1,42 @@ +# Proposal: carbon-ui-redesign + +## Why + +The tools pages are styled with ad-hoc Bootstrap 4 overrides and ad-hoc dialogs +(swal/toastr). Adopting the Carbon Design System gives the two tools a single +professional design language (IBM Plex typography, Carbon components, Carbon +tokens) consistent with the wider YOUreka ecosystem. + +## What Changes + +- **BREAKING (visual only)**: All three surfaces — landing, `/fingerprint` + wizard, `/cropper` — are rebuilt on Carbon Design System web components + (`@carbon/web-components` v2.50.0 via the IBM CDN, version-pinned) with IBM + Plex Sans typography and Carbon tokens for layout/colors. +- Bootstrap 4, swal, toastr and jQuery are removed from the pages; errors and + confirmations surface as Carbon inline notifications and toasts. +- Form fields use Carbon-styled native controls (text input, select, checkbox, + textarea) so Vue state bindings are unchanged; buttons, tags, notifications, + toasts, progress bar and tiles are Carbon web components. +- **Behavior is frozen**: flows, endpoints, validation and copy stay exactly as + they are. The vue-croppa crop pad is kept (no Carbon equivalent), framed in a + Carbon tile/panel. + +## Capabilities + +### New Capabilities + +(none) + +### Modified Capabilities + +- `tools-app`: adds a requirement that the tools UI is built on Carbon Design + System web components with Carbon notifications. + +## Impact + +- Pages and `style.css` rewritten; `app.js` files adapted (notification/toast + models replace swal/toastr calls; `Vue.config.ignoredElements` for cds-*). +- No server-side changes; no new npm dependencies; component JS + Plex font + + Carbon CSS delivered via CDN (IBM CDN for components, jsdelivr for + @carbon/styles, Google Fonts for Plex). diff --git a/openspec/changes/archive/2026-10-01-carbon-ui-redesign/specs/tools-app/spec.md b/openspec/changes/archive/2026-10-01-carbon-ui-redesign/specs/tools-app/spec.md new file mode 100644 index 0000000..1b12c6d --- /dev/null +++ b/openspec/changes/archive/2026-10-01-carbon-ui-redesign/specs/tools-app/spec.md @@ -0,0 +1,16 @@ +# Spec Delta: tools-app + +## ADDED Requirements + +### Requirement: Carbon Design System UI +The tools pages SHALL be built on the Carbon Design System: Carbon web +components (version-pinned via CDN) for buttons, notifications, toasts, tags, +progress and tiles; Carbon-styled native form controls; IBM Plex Sans +typography and Carbon color tokens for layout. Errors, confirmations and +successes SHALL surface as Carbon notifications rather than ad-hoc alert +dialogs. + +#### Scenario: Pages render with Carbon components +- **WHEN** any tools page is opened +- **THEN** its interactive controls are Carbon components (or Carbon-styled + native controls) and its notifications render as Carbon notifications diff --git a/openspec/changes/archive/2026-10-01-carbon-ui-redesign/tasks.md b/openspec/changes/archive/2026-10-01-carbon-ui-redesign/tasks.md new file mode 100644 index 0000000..8ffff67 --- /dev/null +++ b/openspec/changes/archive/2026-10-01-carbon-ui-redesign/tasks.md @@ -0,0 +1,13 @@ +# Tasks + +- [x] 1. Retheme `style.css` on Carbon tokens (Plex, dark header, layers, + sharp corners); drop Bootstrap/swal/toastr/jQuery from all pages; load + pinned Carbon CDN artifacts + Plex + @carbon/styles CSS. +- [x] 2. Rebuild the landing page (Carbon header + cds-tile tool cards). +- [x] 3. Rebuild the fingerprint wizard with Carbon components (inputs, + dropdown, cds-progress-bar scan progress, cds-tag badges, + cds-inline-notification replacing swal); verify at 390/768/1280 via the + visual harness. +- [x] 4. Rebuild the cropper page with Carbon components (cds-toast replacing + toastr, cds-checkbox reasons, cds-tag); verify at 390/768/1280. +- [x] 5. Run unit tests + docker smoke; sync/archive the change and commit. diff --git a/openspec/specs/tools-app/spec.md b/openspec/specs/tools-app/spec.md index 17980ff..83f1d63 100644 --- a/openspec/specs/tools-app/spec.md +++ b/openspec/specs/tools-app/spec.md @@ -132,4 +132,17 @@ environment variables and SHALL NOT be stored in source. #### Scenario: Cropper credentials from the environment - **WHEN** the cropper credentials are supplied as environment variables - **THEN** the cropper routes use them without any change to the hardcoded - URL/port configuration \ No newline at end of file + URL/port configuration + +### Requirement: Carbon Design System UI +The tools pages SHALL be built on the Carbon Design System: Carbon web +components (version-pinned via CDN) for buttons, notifications, toasts, tags, +progress and tiles; Carbon-styled native form controls; IBM Plex Sans +typography and Carbon color tokens for layout. Errors, confirmations and +successes SHALL surface as Carbon notifications rather than ad-hoc alert +dialogs. + +#### Scenario: Pages render with Carbon components +- **WHEN** any tools page is opened +- **THEN** its interactive controls are Carbon components (or Carbon-styled + native controls) and its notifications render as Carbon notifications diff --git a/tools/public/assets/cropper/app.js b/tools/public/assets/cropper/app.js index 0f125b3..f33e22d 100644 --- a/tools/public/assets/cropper/app.js +++ b/tools/public/assets/cropper/app.js @@ -1,32 +1,13 @@ /* Image Cropper — review, crop, clean and save identity photos. Identity comes from the YOUreka session; the only operator credential is - the save-time OTP (required by a2loft). */ -/* global Vue, axios, toastr, Croppa */ -Vue.use(Croppa); + the save-time OTP (required by a2loft). Notifications are Carbon toasts. */ +/* global Vue, axios, Croppa, customElements */ +Vue.config.ignoredElements = [/^cds-/]; + (function () { 'use strict'; - var REVIEW_OPTIONS = [ - 'IMAGE: Not in the midline of the photo frame (aligned to the left or right)', - 'IMAGE: Taken too close-up (Only head is visible, shoulders barely visible)', - 'IMAGE: Blurry ', - 'IMAGE: Improper lighting ', - 'IMAGE: Not coloured.', - 'IMAGE: Scanned photo', - 'IMAGE: Has no adequate space to allow for proper cropping ', - 'IMAGE: Not taken on a recommended background.', - 'IMAGE: Photo is a selfie', - 'DRESSING: Unacceptable dress. ', - 'HEAD: Skewed (leaning away from the midline - vertically or horizontally)', - 'EYES: Not looking into the camera (looking downward, upward, or sideward)', - 'EYES: Not properly opened', - 'EARS: Not visible ', - 'EARS: Both ears are not equally visible (face not aligned central)', - 'FACIAL EXPRESSION: NOT neutral (subject has a facial expression)', - 'GLASSES: Subject is wearing glasses ', - 'JUST WRING: Goes against almost all acceptable passport criteria.', - 'Others not specified.', - ]; + var toastSeq = 0; var app = new Vue({ el: '#app', @@ -43,12 +24,31 @@ Vue.use(Croppa); cleanedImage: null, requiresCleaning: '', otp: '', - reviewOptions: REVIEW_OPTIONS.map(function (name, index) { - return { index: index + 1, name: name }; - }), + reviewOptions: [ + 'IMAGE: Not in the midline of the photo frame (aligned to the left or right)', + 'IMAGE: Taken too close-up (Only head is visible, shoulders barely visible)', + 'IMAGE: Blurry ', + 'IMAGE: Improper lighting ', + 'IMAGE: Not coloured.', + 'IMAGE: Scanned photo', + 'IMAGE: Has no adequate space to allow for proper cropping ', + 'IMAGE: Not taken on a recommended background.', + 'IMAGE: Photo is a selfie', + 'DRESSING: Unacceptable dress. ', + 'HEAD: Skewed (leaning away from the midline - vertically or horizontally)', + 'EYES: Not looking into the camera (looking downward, upward, or sideward)', + 'EYES: Not properly opened', + 'EARS: Not visible ', + 'EARS: Both ears are not equally visible (face not aligned central)', + 'FACIAL EXPRESSION: NOT neutral (subject has a facial expression)', + 'GLASSES: Subject is wearing glasses ', + 'JUST WRING: Goes against almost all acceptable passport criteria.', + 'Others not specified.', + ].map(function (name, index) { return { index: index + 1, name: name }; }), selectedReviewOptions: [], otherReviewOption: '', busy: { load: false, clean: false, review: false, save: false }, + toasts: [], myCroppa: {}, }, @@ -78,11 +78,31 @@ Vue.use(Croppa); return; } self.loading = false; - toastr.error('Could not load your session. Please refresh the page.', 'Oops!'); + self.toast('error', 'Session error', 'Could not load your session. Please refresh the page.'); + }); + Promise.all( + ['cds-button', 'cds-toast', 'cds-toast-item', 'cds-tag'].map(function (n) { + return customElements.whenDefined(n).catch(function () {}); + }) + ).then(function () { + self.$forceUpdate(); }); }, methods: { + toast: function (kind, title, caption) { + var self = this; + var id = ++toastSeq; + this.toasts.push({ id: id, kind: kind, title: title, caption: caption || '' }); + setTimeout(function () { + self.removeToast(id); + }, 6000); + }, + + removeToast: function (id) { + this.toasts = this.toasts.filter(function (t) { return t.id !== id; }); + }, + reset: function () { this.message = { message: { caption: null, file_url: null, domain: '' } }; this.agent = null; @@ -110,7 +130,7 @@ Vue.use(Croppa); ); if (!data.ok) { this.reset(); // nothing (more) to review — back to the load state - toastr.error(data.message, 'Oops!'); + this.toast('info', 'No picture', data.message || 'Nothing to load.'); return; } this.message = data.data; @@ -119,7 +139,7 @@ Vue.use(Croppa); ); if (!image.data.ok) { this.reset(); - toastr.error("Couldn't load the attached image. Please try again.", 'Oops!'); + this.toast('error', 'Load failed', "Couldn't load the attached image. Please try again."); return; } this.message.message.file_url = image.data.data; @@ -133,14 +153,8 @@ Vue.use(Croppa); this.agent = review.data.ok ? review.data.data : null; } } catch ({ message }) { - toastr.error(message, 'Operation Failed'); + this.toast('error', 'Operation Failed', message); } finally { - this.reviewed = false; - this.accepted = false; - this.selectedReviewOptions = []; - this.otherReviewOption = ''; - this.requiresCleaning = ''; - this.otp = ''; this.busy.load = false; } }, @@ -168,7 +182,7 @@ Vue.use(Croppa); fr.onload = function () { self.cleanedImage = fr.result; }; fr.readAsDataURL(blob); } catch ({ message }) { - toastr.error(message, 'Image Cleanup Failed'); + this.toast('error', 'Image Cleanup Failed', message); } finally { this.busy.clean = false; } @@ -178,7 +192,7 @@ Vue.use(Croppa); var self = this; if (!this.canReview) return; if (!this.accepted && this.selectedReviewOptions.length === 0 && !this.otherReviewOption) { - toastr.error('Please indicate why this image is not good enough.', 'Operation Failed'); + this.toast('error', 'Operation Failed', 'Please indicate why this image is not good enough.'); return; } this.busy.review = true; @@ -206,15 +220,15 @@ Vue.use(Croppa); ...data, }); if (!result.ok) { - toastr.error(result.message, 'Operation Failed'); + this.toast('error', 'Operation Failed', result.message || 'Review failed.'); return; } if (reviewMessage) { - toastr.success('Profile owner has been notified of the review.', 'Review Sent'); + this.toast('success', 'Review Sent', 'Profile owner has been notified of the review.'); } this.reset(); } catch ({ message }) { - toastr.error(message, 'Operation Failed'); + this.toast('error', 'Operation Failed', message); } finally { this.busy.review = false; } @@ -227,21 +241,19 @@ Vue.use(Croppa); try { var { data } = await axios.post('/cropper/dequeue', { message: this.message }); if (!data.ok) { - toastr.error(data.message, 'Not Removed'); + this.toast('error', 'Not Removed', data.message || 'Removal failed.'); return; } - toastr.success(data.message, 'Removed!'); + this.toast('success', 'Removed!', data.message || ''); this.reset(); } catch ({ message }) { - toastr.error(message, 'Not Removed'); + this.toast('error', 'Not Removed', message); } finally { this.busy.review = false; } }, saveChanges: async function () { - var self = this; - if (!this.canSave) return; this.busy.save = true; try { var { data } = await axios.post( @@ -254,13 +266,13 @@ Vue.use(Croppa); } ); if (!data.ok) { - toastr.error(data.message, 'Not Saved'); + this.toast('error', 'Not Saved', data.message || 'Save failed.'); return; } - toastr.success('Picture saved for ' + this.message.message.caption, 'Saved'); + this.toast('success', 'Saved', 'Picture saved for ' + this.message.message.caption); this.reset(); } catch ({ message }) { - toastr.error(message, 'Not Saved'); + this.toast('error', 'Not Saved', message); } finally { this.busy.save = false; } diff --git a/tools/public/assets/cropper/index.html b/tools/public/assets/cropper/index.html index aef7f52..9c30128 100644 --- a/tools/public/assets/cropper/index.html +++ b/tools/public/assets/cropper/index.html @@ -4,13 +4,19 @@
- You are not permitted to capture fingerprints for any domain. Contact your domain administrator if you believe this is a mistake. -
+