Overhaul first installation and admin editors - #170
Merged
Conversation
Introduce a local AppNote component: a single-line advisory strip (13px,
variant tint + left accent, semantic icon) with an optional Details
popover for the long text and an optional CTA slot. This replaces the
tall CoarNote block that dominated modals and settings panels.
- New src/components/AppNote.vue (uses existing --coar-* tokens, CoarPopover)
- Convert all 106 CoarNote usages across 29 views to AppNote
- Long explanations become short summary + #details popover; short
status/error/success notes use :truncate="false" (full text, may wrap)
- Add German *Short i18n keys for the new summaries; fix pre-existing
${renamedCount} interpolation to the {count} form the lib expects
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relocate per-field helper paragraphs into CoarFormField's built-in hint prop (info glyph on the label + hover popover), decluttering forms. Only fields that have a label qualify — the hint affordance lives on the label, so label-less checkbox rows keep their visible helper text. Tab/section intros and dynamic status/error text stay visible. No copy changes; text is relocated, not reworded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tabs Two defects in Realm Settings: - DCR reservedNames and both audit fields put their help text in <template #footer> / #help slots that CoarFormField never renders, so the guidance was invisible. Move it into the :hint prop. - The 11-tab bar laid out in a single non-wrapping flex row, forcing the whole settings column past the viewport and clipping tabs plus the intro text. Let the tab row wrap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app modal used MODAL_FULL (112rem / 90vh), sizing for the heaviest user-app case but leaving read-only system apps (a few fields) stranded in empty space and spanning nearly the whole viewport. A user-app settings form plus the 3-column permission catalog fit MODAL_LG (80rem / 82vh), which still gives the grid a definite height. The dense client builder keeps the full frame. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
modalTitle returned an empty string for an existing user with no first/ last name and no acronym (e.g. the seeded admin), leaving the modal header blank. Fall back to the username, then email. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The security tab rendered 2FA status with a bespoke .status-badge (hardcoded colors), while the dashboard uses CoarTag. Switch to CoarTag (success/warning/neutral) so status chips share one vocabulary, and drop the now-unused custom styles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Boolean grid columns rendered three different ways: "Ja/Nein" text (scopes, realms, clients, apis, login providers), a green check icon (users), and a tag (scheduled jobs). Apply one rule by semantics: - On/off status (Active/Enabled) → CoarTag (success/neutral) via a shared common.statusTag prefix, matching the dashboard and 2FA badge. - Classification (IsSystem) → icon (lock), like the existing HasPassword icon; never a success-green check. Adds common.statusTag.active/inactive to de.json and (finally) en.json. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per the Modal & Form Contract (R2/R3), a relationship must behave the same from both sides. UserDetails wrote direct-group add/remove immediately on every dual-listbox change, while GroupDetails stages members into Save — same relationship, two behaviors. Stage the group selection into the form (stagedGroupIds) and commit the diff as part of the single save(), guarded on groupsLoaded so a user whose Groups tab was never opened is not stripped of all memberships. Verified: moving a group fires no network call; closing without saving discards the change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per the Modal & Form Contract (R1), the app-settings "override" blocks hid every field behind their checkbox, so a setting was unfindable (Ctrl+F, scan, screen reader) and its effective value invisible until you ticked override. Load the realm defaults and keep the fields always visible: when a section does not override, the fields render disabled and greyed showing the inherited realm value (e.g. session idle/absolute days); ticking override enables editing. A fieldBind() helper centralises the override?staged:inherited + disabled wiring across all sections. Origin, email-branding and posture have no realm equivalent, so they inherit- display as empty/"(inherit from realm)". build()/populate() semantics are unchanged — an untouched section still emits null (inherit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contract rule 4 ("one modal plumbing") for the two profile modals.
ChangePasswordModal and MfaSetupModal were opened from ProfileView
through a hand-rolled Teleport plus a fixed-inset backdrop div, with
local visibility flags; MfaSetupModal committed through an in-body
<form> submit and a full-width button instead of the modal footer.
Both are now routed fragments on the /profile route (#change-password,
#mfa-setup) like every other modal in the app, so they are
URL-addressable and deep-linkable. MfaSetupModal commits through
ModalLayout :footer-button; a complete OTP still auto-submits via
@complete, which replaces the lost form-submit-on-Enter. Since a routed
modal reports back through the API rather than a return value,
ProfileView re-reads the MFA status when the fragment disappears.
Also adds useModalOverlay() for the remaining case a routed fragment
cannot cover: a modal opened from inside another modal. It opens a bare
overlay with the same preset the fragment parser uses. useDialog() is
wrong there — the CoarDialog shell draws its own header, close button
and card background, so a ModalLayout inside it renders as a visible
modal-in-modal.
The named modal sizes move to router/modal-sizes.ts so both consumers
(the route table and useModalOverlay call sites) share one definition.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Acute defects found while smoke-testing the modal plumbing. The full rework of this modal is a separate piece of work. Two of these were real i18n bugs, not cosmetics: the two options of the access-token-format select and the dual-listbox group headings were hardcoded string literals that never went through t(), and the keys accessTokenType / accessTokenTypeHint were missing from de.json while their inline fallbacks were written in German. Since en.json is empty by design and English comes from those fallbacks, an English user was shown German. All fallbacks are English now and the missing keys exist. Layout: the modal ignored the form-layout contract entirely — a flat flex column with ad-hoc <h4> headings, long help paragraphs flexed next to them, and a dead deprecated width prop. It now uses the same modal-form / form-section / modal-form-grid primitives as its siblings, with field help in CoarFormField :hint and the two listbox explanations as one-line AppNotes with a details popover. The long scroll becomes three tabs (Basics / Scopes / Apps, same in create and edit per rule 5) on a fixed panel frame, so switching tabs never resizes the modal. Both dual-listboxes take their height from a flex-section wrapper instead of from their content, so the lists are the same size on either tab and whether they are empty or full. In the owning modal, the credentials section had a "Issue credential" button that silently cut off its own label: the row let it shrink and the label is nowrap+overflow:hidden. The section also carried its own heading style and stated the same thing twice, in the paragraph and again in the empty-state box. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The create modal offered six fields and hid everything else behind a
save: the whole tab bar was suppressed in create mode, so Direct Groups
and Security were unreachable, and the active flag was edit-only. An
admin had to create a bare user and then edit it — which is exactly what
contract rule 5 forbids ("same layout in create and edit; do not switch
navigation between modes"). The migration table wrongly recorded
UserDetails as compliant.
Create now carries the same tabs as edit. Only "Effektiv" drops out:
effective membership is derived and there is nothing to derive it from
yet. The single Save writes the profile, an optional initial password,
the active flag and the email-verified flag in the POST, then applies
group memberships and the per-user 2FA policy against the id from the
response — the two things that genuinely cannot exist before the user
does. The pinned body height now applies in create too, since a tabbed
create would otherwise resize between the short General tab and the
group picker.
The initial password needed no API work: CreateUserCommand has always
accepted one, the form simply never asked for it.
IsActive on create did need API work, and the interesting part is that
setting it on the ApplicationUser document is not enough. UserView takes
IsActive exclusively from UserActivatedEvent / UserDeactivatedEvent and
defaults it to true, so a document-only write left every projection-fed
surface — the list query, the admin grid — insisting the user was active
while the document disagreed. The create path now appends the same
UserDeactivatedEvent the update path appends. Covered by a test that
asserts the read-back, not just the create response, because that is the
half that was broken.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Modelled on the storage banner in Cocoar.Atlas. The distinction to AppNote is SCOPE, not styling: a banner states something about everything below it, so it is pinned directly under a header (main or modal), is full-bleed, and there is at most one per scope. A note belongs to a single field or section, is indented with a left accent bar, and several may appear in one view. A banner a third of the way down a form would be lying about its own reach; a note under the header is too quiet for a statement about the whole surface. ModalLayout gets a #banner slot BETWEEN the header and .modal-content — inside the content region the banner would inherit the 20px padding (so it would no longer be full-bleed) and would scroll out of view. Copy shape follows the original: a two-word bold label, then the consequence, then the remedy, with an optional trailing action leading to where the state is fixed. Unlike AppNote the text wraps instead of truncating — a statement about the whole surface must stay readable, so there is no single-line + details-popover contract here. Migrates the two places that already made modal-wide statements and made them differently: the system-app warning in AppDetails (which sat BELOW the tab bar, so it read as belonging to the active tab, and duplicated the header's read-only tag) and the built-in login provider notice, whose comment already called itself a banner. In AppDetails the roles are now split: the header tag names the state, the banner gives the reason and says what the surface is still good for. Deliberately not touched: the 106 existing AppNotes. They are field- and section-scoped and correct where they are. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nner Measured against the Cocoar.Atlas banner the strip was reading far heavier than its model: the -bold border tokens are near-black (#3f2503 for warning) and were drawn top AND bottom, so a hairline box was framing the text instead of a tinted wash sitting under the header, and the text was neutral grey (#3f3f46) on a coloured patch instead of being tinted in the banner's own hue. Now: bottom rule only, in the -subtle border token, and both text and icon in the hue. Note the foreground deliberately uses the -bold BORDER token, not --coar-text-semantic-*-bold — the latter resolves to #ffffff for the info variant and would be invisible on a light background. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ntence
Migrating these to AppBanner dropped the short/long split they used to
have (systemHintShort plus a details popover) and put the LONG text on the
strip, so the system-app banner ran to three clauses over two lines.
A banner states one thing. The bold label already names WHAT this is, so
the text only has to carry the consequence — and the consequence is the
same for both, so both now say it in the same words. The dropped sentence
("changing the strings would break the RequiresPermission calls") is
rationale for developers, not something an admin can act on; it survives
in the code comment where it belongs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The system-app banner was `warning` and the built-in-login-provider banner was `info` for the same statement, with different labels — inherited from the two AppNotes they replaced, and carried over without being questioned. Unified on `info`: read-only is a fact, not a hazard. The fields are disabled and the backend rejects the write anyway, so nothing can go wrong, and warning colours only keep their force while they stay reserved for things that can actually bite. Both now read through ONE pair of strings (common.systemManagedLabel / common.systemManaged) instead of two parallel per-entity keys, so they cannot drift apart again; the four now-unused keys are removed. "System" as the label matches the vocabulary the lists already use — the login provider grid badges it "System" and the app grid has a System column. Also gives LoginProviderDetails :readonly, so it carries the same read-only tag as every other read-only modal. Its block was hand-rolled (Save hidden plus :disabled on fifteen fields), which worked but left the title bar silent about it. Tag names the state, banner gives the reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two components for one thing had already drifted once (the same statement rendered `warning` in one modal and `info` in the next), so inline and banner become one component with a `placement` prop. What differs is REACH, and reach is carried by POSITION — pinned under a header means the whole surface, in the flow means this section. Nothing else has to say it, so the old 3px near-black left bar is gone; fill, tinted ink, icon, 13px and the bold label are identical in both placements. Dropped the "App" prefix: it read as if a notice were always app-wide, which is exactly the distinction the component is trying to make. `truncate` now defaults to OFF. Of the 105 call-sites, 82 were switching truncation off — a default that three quarters of the callers opt out of is the wrong default. The 23 that relied on clamping now say so. Colour, reworked against the reference banner in Cocoar.Atlas. The `-subtle` ground (L 0.92 / C 0.04) plus the near-black `-bold` border ink (L 0.29 / C 0.06) read as grey text on a coloured patch; the reference is a pale ground carrying mid-dark SATURATED ink, and that saturation is what makes it "dark yellow" instead of "dark brown". The ground is now lightened and desaturated RELATIVELY off the token — never a literal lightness, because the tokens invert for dark mode — and the ink is a straight swap to `--coar-background-semantic-*-bold` (#8f5300, a whisker from the reference's #92400e). What remains is hue: the design system seeds warning at #cc821f (H 67, amber-orange) where the reference sits at H 95 (yellow). That is the system's warning identity, not this component's, so it stays. Dark mode lives in an unscoped block on purpose: it has to reach an ancestor, and Vue's scoped compiler silently rewrites `:global(.dark-mode) .notice--warning` down to bare `.dark-mode`, which put one variant's ink on the root element for every notice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…iants The ground derivation subtracted a fixed 0.018 from chroma. That reads fine on warning (C 0.04) but info's seed is a slate at C 0.012, so the channel went negative, clamped to zero, and every info notice rendered as flat #f8f8f8 — no colour at all. Absolute arithmetic on chroma always breaks whichever variant happens to be the quietest. Lightening alone already produces the pale ground, so chroma is now left exactly as the design system set it and each variant keeps its own identity: info #f4f9ff, warning #fff4dd, error #fff0ed, success #e8ffed. Neutral's border came straight off the neutral ramp at #5a5a5a and drew a hard box where every other variant draws a hairline; lightened to match. That one does use an absolute lightness — its source is a grey with no hue to derive from — so dark mode hands it back to the raw token, which is already the light end of the ramp there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README duplicated the dev-loop commands under "Build it yourself" and pointed only at first-time-setup for the admin bootstrap, so developing-locally.md — the page that is actually kept in sync with the code — was reachable from CONTRIBUTING and the docs nav but not from the front door. Adds it to Quick links directly under the Docker quickstart, since those two are the same question answered differently (run the image vs run from source), and marks the inline snippet as the short version so the two copies cannot silently drift apart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
real:adminexplicitlyVerification