Skip to content

@memberjunction/ng-ui-components

Reusable, standalone Angular UI components for MemberJunction applications — the house replacement for Kendo UI. Every component is token-driven (--mj-* design tokens from _tokens.scss), dark-mode safe by construction, and keyboard/ARIA accessible.

The rule for consuming apps: check here before building. If a control, layout region, overlay, or pattern exists in this package, use it. Hand-rolling an app-prefixed equivalent (a bespoke button, chip, empty state, dialog…) creates divergence this package exists to eliminate. If what you need doesn’t exist, propose it — generic components belong here; app-domain components belong in the app’s shared package.

The package ships standalone components — no NgModule required (the one exception is MJAccordionModule, a convenience bundle). Import the symbols you use directly:

import { MJButtonDirective, MJEmptyStateComponent, MJDialogComponent } from '@memberjunction/ng-ui-components';
@Component({
standalone: true,
imports: [MJButtonDirective, MJEmptyStateComponent, MJDialogComponent],
...
})

Dependencies: @angular/cdk (overlays) and angular-split (splitter) come along as package dependencies; @angular/common, @angular/core, and rxjs are peers.

Components attach styles one of three ways, and one of them needs host cooperation:

  1. Inline styles in the component (most components) — nothing to do.
  2. styleUrls compiled into the component (page-header, slide-panel) — nothing to do.
  3. Global stylesheets shipped as assets — the form controls and overlay chrome (button, dialog, dropdown, combobox, input, datepicker, splitter, accordion, window, chip, switch/progress, tabs) ship their .scss to dist and expect the host application to import them globally.

If your app runs inside MJ Explorer (explorer-app), category 3 is already handled — the shell imports all of them. A standalone host must add these to its global stylesheet:

@import '@memberjunction/ng-ui-components/dist/lib/button/button';
@import '@memberjunction/ng-ui-components/dist/lib/dialog/dialog';
@import '@memberjunction/ng-ui-components/dist/lib/dropdown/dropdown';
@import '@memberjunction/ng-ui-components/dist/lib/combobox/combobox';
@import '@memberjunction/ng-ui-components/dist/lib/input/input';
@import '@memberjunction/ng-ui-components/dist/lib/input/input-switch-progress';
@import '@memberjunction/ng-ui-components/dist/lib/input/chip';
@import '@memberjunction/ng-ui-components/dist/lib/datepicker/datepicker';
@import '@memberjunction/ng-ui-components/dist/lib/splitter/splitter';
@import '@memberjunction/ng-ui-components/dist/lib/accordion/accordion';
@import '@memberjunction/ng-ui-components/dist/lib/window/window';
@import '@memberjunction/ng-ui-components/dist/lib/tabs/tabs';

tabs is the shared .mj-tabs* chrome consumed by BOTH mj-workspace-tab-strip (this package) and mj-tabstrip (@memberjunction/ng-tabstrip) — without it either strip renders as unstyled divs, so a standalone host using tab strips must include it.

All colors resolve through --mj-* semantic tokens, so light/dark theming requires zero component-level work.

mj-combobox, mj-dropdown, mj-datepicker, mj-numeric-input, and mj-switch all implement ControlValueAccessor — they work with [(ngModel)] and reactive forms, including setDisabledState.

Accessible names — one contract across every control

Section titled “Accessible names — one contract across every control”

mj-combobox, mj-dropdown, mj-datepicker, mj-numeric-input, mj-page-search and mj-switch share four optional inputs, from MJNamedControlBase. Name every one of them: without a name a control announces as its role and state alone — “combobox, collapsed”, “switch, on” — which fails WCAG 2.1 4.1.2 (Name, Role, Value).

inputfor
AriaLabelledBythe id of a visible label — the preferred wiring when one exists
AriaLabelwhen no visible label exists
AriaDescribedByhint and error text
InputIdan id on the focusable element, so other markup can reference it

Three things hold across all six:

  • Absent beats empty — an unset input renders no attribute at all. aria-label="" is worse than nothing, because an explicitly empty name overrides every other naming source.
  • The name reaches the whole control — the popup listbox, the calendar grid, the filter box, the toggle and clear buttons all take their name from the same source, so a form with six filterable dropdowns does not present six identical “Filter options” boxes. Under AriaLabelledBy this works through an aria-labelledby id list, the only mechanism that can compose a fixed word with a visible label’s own text.
  • InputId is a <label for> target wherever the focusable element is labelable — the four input-backed controls (mj-combobox, mj-datepicker, mj-numeric-input, mj-page-search) and mj-switch, whose <button> is labelable too and gains click-to-focus from it. The exception is mj-dropdown, whose trigger is a div[role=combobox]: label[for] does not associate with it, so the visible-label path there is AriaLabelledBy.

A control that renders with no accessible name warns in dev mode (WarnIfUnnamed, alongside the existing mjButton and mjClickable guards) and is silent in production. It fires when the name would be genuinely empty — including an aria-labelledby whose ids resolve to nothing, which names the control no better than no attribute at all. A placeholder does not count as a name for a form control, since it disappears the moment the user types; mj-page-search is the one control that opts into counting it, because its placeholder is the caller’s statement of what the box searches.


button[mjButton] / a[mjButton] — MJButtonDirective

Section titled “button[mjButton] / a[mjButton] — MJButtonDirective”

Attribute directive that styles a native button or anchor. Variants: primary | secondary (default) | outline | flat | danger | icon | success | warning. Sizes: sm | md (default) | lg. Toggle mode via [toggleable] + [(selected)] (sets aria-pressed). Dev-mode warns when an icon button lacks an accessible name — pass ariaLabel.

<button mjButton variant="primary" (click)="save()">Save</button>
<button mjButton variant="icon" ariaLabel="Remove" (click)="remove()"><i class="fa-solid fa-xmark"></i></button>
<button mjButton variant="flat" [toggleable]="true" [(selected)]="isActive">Toggle</button>

Editable text-input combobox (CDK overlay): type-to-filter, keyboard nav, optional custom values ([AllowCustom]). Inputs: Data, TextField, ValueField, ValuePrimitive, Filterable (default true), Placeholder, Disabled, plus the four naming inputs above. Outputs: ValueChange, FilterChange. Custom item template via <ng-template #mjComboboxItem>.

role="combobox" and its state attributes (aria-expanded, aria-controls, aria-activedescendant) sit on the input — the element that takes focus — not on the wrapper. The toggle and clear buttons take their names from the combobox’s own name.

<mj-combobox AriaLabel="Category" [Data]="categories" TextField="text" ValueField="value"
[(ngModel)]="selected" [ValuePrimitive]="true" [AllowCustom]="true" />

Non-editable select (CDK overlay) with optional in-panel filter ([Filterable], default false) and DefaultItem clear option. Same Data/TextField/ValueField/ValuePrimitive contract as combobox. Custom item template via <ng-template #mjDropdownItem>. Tab in an open panel closes it, puts focus back on the field, and leaves the key to the browser, so Tab moves on from the field.

Give every dropdown an accessible name — see Accessible names. The trigger is a div[role=combobox], so <label for> neither names nor focuses it: the visible-label path here is AriaLabelledBy, not InputId. The name is applied to the popup listbox as well as the trigger, and a filterable panel’s filter box takes its name from the same source.

<!-- A visible label already on screen (preferred) -->
<span id="persona-label">Interview persona</span>
<mj-dropdown AriaLabelledBy="persona-label" [Data]="items" TextField="name" ValueField="id"
[(ngModel)]="selectedId" [ValuePrimitive]="true" />
<!-- No visible label -->
<mj-dropdown AriaLabel="Interview persona" [Data]="items" TextField="name" ValueField="id"
[(ngModel)]="selectedId" [ValuePrimitive]="true" />

Text input + calendar popup with Min/Max range disabling and typed-input parsing. Accepts Date | string | null through forms; emits ValueChange: Date | null. (Display format is currently fixed at MM/dd/yyyy.) The toggle button and the calendar grid take their names from the field’s own name, so two pickers on one form do not present two grids both called “Calendar”. Tab in an open calendar closes it, puts focus back on the field, and leaves the key to the browser, so Tab moves on from the field.

<mj-datepicker AriaLabel="Due date" [(ngModel)]="dueDate" [Min]="minDate" [Max]="maxDate" Placeholder="Select a date" />

mj-numeric-input — MJNumericInputComponent

Section titled “mj-numeric-input — MJNumericInputComponent”

Native number input that clamps to Min/Max, rounds to Decimals, and formats on blur. Value flows through forms only (no separate output).

<mj-numeric-input AriaLabel="Quantity" [(ngModel)]="quantity" [Min]="0" [Max]="100" [Decimals]="2" />

Boolean toggle with role="switch", optional OnLabel/OffLabel. Name it: OnLabel/OffLabel render inside the button, so an unnamed switch is named after its own state — “Off, switch, off” — and AriaLabel/AriaLabelledBy is what replaces that with its purpose.

<mj-switch AriaLabel="Email notifications" [(ngModel)]="isEnabled" OnLabel="On" OffLabel="Off" />

Retrofits a non-semantic element (card, tile, row) into an accessible control: sets role, tabindex, aria-label, and makes Enter/Space fire (click). Pass the accessible name as the attribute value; role may be button (default) or link; testId emits data-testid.

<div class="app-card" [mjClickable]="app.Name" (click)="open(app)">…</div>

calendar/calendar-utils exports pure helpers shared with the datepicker: BuildCalendarWeeks, FormatDate, FormatDateTime, GetMonthYearLabel, WEEK_DAYS, MONTH_NAMES.

The page-* family composes into the standard page skeleton:

<mj-page-layout>
<mj-page-header Title="Agents" Icon="fa-solid fa-robot" Subtitle="Manage your agents">
<span meta><mj-stat-badge [Count]="filtered" [Total]="total" Label="agents" /></span>
<div actions><button mjButton variant="primary">New</button></div>
<div toolbar><mj-page-search [Value]="q" (ValueChange)="q = $event" /></div>
</mj-page-header>
<mj-page-body>
<!-- content; scrolling and the 24px gutter are handled for you -->
</mj-page-body>
</mj-page-layout>

And the left-nav shell variant for multi-section pages:

<mj-page-layout>
<mj-page-header Title="Admin" Icon="fa-solid fa-gear" />
<mj-page-body [Flex]="true" [Padding]="false" Direction="row">
<mj-left-nav [Sections]="sections" [ActiveId]="activeId" (ItemClicked)="go($event)" />
<mj-left-nav-content [Loading]="loading" [Error]="err">
<mj-page-header-interior Subtitle="Runtime state">
<div actions><button mjButton size="sm">+ Add</button></div>
</mj-page-header-interior>
<mj-page-body-interior><!-- sub-page content --></mj-page-body-interior>
</mj-left-nav-content>
</mj-page-body>
</mj-page-layout>

Flex-column, full-height, overflow:hidden, page background. No inputs.

Inputs: Title, Icon, Subtitle. Slots: [meta] (badges under the title), [actions] (right-aligned), [toolbar] (secondary row).

Inputs: Padding (default true; false removes the gutter), Flex (default false), Direction: 'row' | 'column' (row stacks to column at ≤700px).

mj-page-header-interior / mj-page-body-interior

Section titled “mj-page-header-interior / mj-page-body-interior”

The same pair for sub-pages inside a left-nav shell — card-like chrome, responsive padding. Header-interior adds Role/AriaLabel inputs and the same three slots.

mj-left-nav / mj-left-nav-content — MJLeftNavComponent

Section titled “mj-left-nav / mj-left-nav-content — MJLeftNavComponent”

Canonical left rail: sections, flat + tree items (badges, descriptions, disabled), off-canvas drawer at ≤700px. Inputs: Sections: MJLeftNavSection[], ActiveId, ExpandedIds, Width (240), MobileTitle, IconOnly (per-item tooltips + accessible names for an externally-narrowed rail). Outputs: ItemClicked, ItemToggled. Slots: [header], [footer]. Pair with mj-left-nav-content, which provides Loading/Error states and hides (not destroys) content while busy.

Desktop collapse (opt-in): set [Collapsible]="true" and bind [(Collapsed)] — the rail renders a locked-position double-angle toggle chip and, when collapsed, becomes a CollapsedWidth (60px) icons-only strip: labels visually hidden but kept in the a11y tree, section labels folded to divider lines, badges docked on the icon corner, item tooltips auto-enabled. The component owns presentation only — the consumer owns + persists the state (same split as ExpandedIds). Desktop-only; the ≤700px drawer is unaffected. There is deliberately no hover-to-peek.

<mj-left-nav [Sections]="sections" [ActiveId]="activeId" [Collapsible]="true"
[(Collapsed)]="railCollapsed" (CollapsedChange)="persist($event)" />

Collapsing a rail whose content is richer than a flat icon list:

ContentCollapsed behavior
Tree sections (item.children)Fold to top-level items only — no chevrons, no indentation, children hidden. A top-level item paints active (aria-current="true") when the active item is one of its descendants, so the rail still shows where you are. ExpandedIds is untouched, so the tree returns exactly as the user left it on expand.
Items with no iconRender an uppercase monogram from the label, so an item never collapses to a blank-but-clickable button.
[header] / [footer] slotsYours to handle. The rail can’t restyle projected content, so a wide header overflows a 60px strip — bind the same collapsed flag in your consumer and project a compact variant.

Data-driven tab strip. Tabs: TabConfig[] (key, label, icon?, badge?, badgeVariant?: 'default'|'error'|'warning'|'success'), ActiveKey, (TabChange) emits the key.

The ARIA tabs keyboard contract, shared by every MJ tab strip. Apply to the element holding the tabs; mark each tab role="tab" and keep aria-selected truthful. Owns role="tablist", the roving tabindex (one stop per strip), Arrow/Home/End navigation with focus-follows-selection, Enter/Space activation, and Delete/Backspace close. Emits (TabActivateRequested) / (TabCloseRequested) with the tab’s index in the full list plus the element, so index-addressed and id-addressed hosts both map it onto their own model. Skips display:none tabs; leaves keys alone inside editable content.

mj-workspace-card / mj-workspace-tab-strip — MJWorkspaceCardComponent, MJWorkspaceTabStripComponent

Section titled “mj-workspace-card / mj-workspace-tab-strip — MJWorkspaceCardComponent, MJWorkspaceTabStripComponent”

The workspace pattern: a screen where the user builds several drafts in parallel, each in a browser-style tab (open, switch, drag-reorder via CDK, close, dirty-dot, rejected/complete states). MJWorkspaceTabStore<TState> is the pure state machine (open-activates-existing, neighbour activation on close, reorder, lifecycle + dirty tracking — exhaustively unit-tested); mj-workspace-tab-strip is dumb presentation over it (renders tabs, emits intent); mj-workspace-card is the slotted frame — card surface, tab-strip row, [workspaceHeader] identity band, a single scrolling body (default slot), and an opt-in standardized footer (ShowFooter: primary confirm + save-as-draft + discard, only the labels vary; [workspaceFooterNote] caption slot). Tabs carry an opaque State the host owns — no app types in the framework. v1 is session-scoped (no DB persistence).

The card is a query container (container-type: size): size content inside it with cqh/cqw, never vh/vw (viewport units break the moment the card sits in a split or a smaller pane).

<mj-workspace-card [Tabs]="store.Tabs" [ActiveId]="store.ActiveId" NewTabLabel="New entry"
[ShowFooter]="true" ConfirmLabel="Create entry"
(TabSelected)="store.Activate($event)" (TabClosed)="onClose($event)"
(NewTabRequested)="openBlank()" (TabReordered)="store.Reorder($event.previousIndex, $event.currentIndex)"
(Confirm)="create()" (SaveDraft)="keep()" (Discard)="discard()">
<div workspaceHeader><!-- per-workshop identity badges --></div>
<!-- the workshop's form/body -->
</mj-workspace-card>

Delayed (~450ms still-pointer), non-interactive, token-themed hover tooltip that by default shows only when the host is truncated (scrollWidth > clientWidth); mjTipAlways forces it for hosts whose truncation can’t be measured (a native <select>). Used by the workspace tab strip for full labels on capped tabs.

Compact toolbar search input. Placeholder, Value, Icon, plus the four naming inputs above; (ValueChange) on input. The placeholder is the name of last resort — it is what the name computation falls back to, and it disappears the moment the user types — so pass AriaLabel for a name that survives having text in the box.

<mj-page-search AriaLabel="Search templates" Placeholder="Search templates..."
[Value]="q" (ValueChange)="q = $event" />

Chrome-only slide-in panel from the right (resizable) or centered dialog (Mode: 'slide' | 'dialog'). Inputs include Title, Visible, Resizable, MinWidthPx, MaxWidthRatio, WidthPx, and a CanClose guard callback. Outputs: Closed, WidthChanged. Dialog-mode body projects into [dialog-content].

Re-export of angular-split — use <as-split> / <as-split-area> directly:

<as-split direction="horizontal">
<as-split-area [size]="30">Left</as-split-area>
<as-split-area [size]="70">Right</as-split-area>
</as-split>

mj-accordion-panel — MJAccordionPanelComponent (+ directives)

Section titled “mj-accordion-panel — MJAccordionPanelComponent (+ directives)”

Collapsible panel replacing Kendo panelbar/expansion panel. Inputs: Title, Expanded (+ExpandedChange), Disabled, Variant: 'default'|'primary'|'secondary', Size: 'sm'|'md', Bare (no border, header background, body fill or row hover fill, for a panel inside a card or a section directly on a page; on a device that can hover, pointing at the title or chevron gives both the brand color), HeadingLevel: 1–6 | null (wraps the toggle in a role="heading" element with that aria-level; null, the default, adds no wrapper), FlushBody, Fill (consume leftover height). Rich content via structural directives — *mjAccordionTitle, *mjAccordionActions (rendered outside the toggle button), and *mjAccordionBody (lazy — instantiated on first expand, then kept alive). Import MJAccordionModule to get the panel plus all three directives in one symbol.

<mj-accordion-panel [Expanded]="open" (ExpandedChange)="open = $event">
<ng-template mjAccordionTitle><i class="fa-solid fa-code"></i> Template Editor</ng-template>
<ng-template mjAccordionBody><mj-code-editor /></ng-template>
</mj-accordion-panel>

Modal with backdrop, Esc/backdrop close (Closeable), body scroll lock. Inputs: Visible, Title, Size: 'sm'|'md'|'lg'|'xl'|'auto' (400/600/800/1000px), Width/Height/MinWidth, Role: 'dialog'|'alertdialog'. Output: Close. Visible is not two-way — set it false in your (Close) handler. Slots: default body, mj-dialog-titlebar, mj-dialog-actions.

Focus inputs, each on by default:

  • AutoFocus (default true) moves focus into the dialog on open: [data-autofocus], else the first field, else the first body or actions button (never the close button), else the container.
  • TrapFocus (default true) wraps only. The browser moves Tab between stops. Tab on the last stop wraps to the first, and Shift+Tab on the first stop or the container wraps to the last. Only the topmost visible dialog brings a Tab in, and only when it traps focus. Focus in another modal, or in an overlay panel, keeps its Tab. When a dropdown or calendar on the first or last stop hands focus back to its field, the dialog wraps. Stops skip display: none, [hidden], visibility: hidden, disabled controls, hidden inputs, and tabindex="-1". A contenteditable region is a stop unless contenteditable="false". A nested dialog handles its own Tab first.
  • RestoreFocus (default true) returns focus to the element that opened the dialog.
  • AriaLabel names the dialog when Title is empty. A title uses aria-labelledby and does not also set aria-label.
<mj-dialog [Visible]="show" Title="Confirm" Size="md" (Close)="show = false">
<p>Are you sure?</p>
<mj-dialog-actions>
<button mjButton variant="primary" (click)="confirm()">Yes</button>
<button mjButton (click)="show = false">No</button>
</mj-dialog-actions>
</mj-dialog>

Open(settings): MJDialogRef with { title, content: string | Type, actions: [{ text, primary?, themeColor? }], width, … }. ref.Result (Observable) emits the clicked action or undefined on dismiss; ref.Content.instance exposes a mounted content component; ref.Close(result?).

mj-confirm-dialog — MJConfirmDialogComponent

Section titled “mj-confirm-dialog — MJConfirmDialogComponent”

Purpose-built confirmation on top of mj-dialog. Inputs: [(Visible)] (two-way), Type: 'default'|'danger'|'warning'|'info', Title, Message, DetailMessage, ConfirmText, CancelText, Icon, Processing (spinner + blocks dismissal for async work). Outputs: Confirmed (stays open — you close it when the work finishes), Cancelled (auto-closes). Confirm button is leftmost, per MJ dialog convention.

MJConfirmService — promise-based confirms

Section titled “MJConfirmService — promise-based confirms”

await Confirm({ message, title?, detail?, type?, … }) → boolean, and ConfirmDelete(...) which forces danger styling and “Delete”. Mounts above mj-window (z-index 20000).

private confirm = inject(MJConfirmService);
if (await this.confirm.ConfirmDelete({ message: 'Delete this view?', detail: 'Cannot be undone.' })) { … }

Floating, non-modal panel (no backdrop): Draggable, Resizable (8 handles), State: 'default' | 'maximized' with geometry save/restore, Top/Left/Width/Height/MinWidth/MinHeight. Outputs: Close, StateChange, Resize. Methods: SetPosition, SetSize, GetWindowElement. Slots: default body, mj-window-titlebar, mj-window-actions. Visible is not two-way.

mj-filter-popover — MJFilterPopoverComponent

Section titled “mj-filter-popover — MJFilterPopoverComponent”

Filter trigger button + CDK popover on desktop, docked bottom-sheet on mobile. Inputs: Label, Icon, ActiveCount (badge), ShowClearAll. Output: ClearAllRequested. Project your filter UI (dropdowns, chips) as content.

mj-bottom-sheet — MJBottomSheetComponent

Section titled “mj-bottom-sheet — MJBottomSheetComponent”

Generic mobile bottom sheet: scrim + panel sliding up from the bottom edge with a grab handle. Pure chrome, no breakpoint logic — the caller decides when a sheet is the right presentation. Fully modal: role="dialog" + aria-modal, focus moves in on open and is trapped (Tab/Shift+Tab cycle within), body scroll locks while open, focus returns to the opener on close. Owns enter/exit transitions (class-driven, dismissal animates), Escape, scrim-click close, prefers-reduced-motion. Inputs: [(Visible)] (two-way), Title (optional header), AriaLabel. Output: Closed (after the exit transition). Sits in the mobile-chrome z band (9998/9999, below mj-window); settled open state is transform: none so position: fixed descendants (dropdowns) aren’t displaced.

<mj-bottom-sheet [(Visible)]="sheetOpen" Title="Open Records">
<div class="rows">…</div>
</mj-bottom-sheet>

Centered placeholder for empty/error/no-results states. Inputs: Variant: 'empty'|'no-results'|'success'|'warning'|'error' (drives default icon + live-region role), Size: 'compact'|'default'|'large', Icon, Title, Message, built-in CTA via ActionText/ActionIcon/ActionVariant with (Action). Extra CTAs project into [actions]; richer body projects as default content.

<mj-empty-state Variant="error" Title="Couldn't load permissions" [Message]="error"
ActionText="Try Again" ActionIcon="fa-solid fa-rotate-right" (Action)="loadData()" />

Read-only stat pill, designed for the page-header [meta] slot. Count, Total (renders “X of Y label”), Label, Icon, Variant: 'default'|'success'|'error'|'warning'|'running'|'info'.

<mj-stat-badge [Count]="filteredCount" [Total]="totalCount" Label="agents" />

Persistent in-flow banner (not a toast). Variant: 'info'|'success'|'warning'|'error', Size: 'sm'|'md', Title, Message, Dismissible + (Dismissed), action buttons via [actions] slot. Correct ARIA live role per variant automatically.

mj-progress-bar — MJProgressBarComponent

Section titled “mj-progress-bar — MJProgressBarComponent”

Value (0–100, clamped) or Type="infinite" for indeterminate. Styles ship in the global input-switch-progress sheet (see Styling model).

Segmented control for view modes. Options: { key, icon?, label?, title? }[], ActiveKey, (KeyChange). Provide title for icon-only options.

mj-refresh-button — MJRefreshButtonComponent

Section titled “mj-refresh-button — MJRefreshButtonComponent”

Canonical refresh: Loading spins + disables, label auto-hides on mobile. (Clicked) suppressed while loading/disabled.

Clickable filter pill: Label, Icon, Count, Active (+aria-pressed), (Clicked).

mj-filter-field — MJFilterFieldComponent

Section titled “mj-filter-field — MJFilterFieldComponent”

Labeled row (icon + uppercase label) wrapping any projected widget — the escape hatch inside filter UIs.

mj-filter-panel — MJFilterPanelComponent

Section titled “mj-filter-panel — MJFilterPanelComponent”

Config-driven filter stack: Fields: FilterFieldConfig[] with type: 'text' | 'dropdown' | 'chips' (chips support multi), [(Values)] as a plain record, Reset handling, plus projected custom mj-filter-fields.

mj-applied-filters — MJAppliedFiltersComponent

Section titled “mj-applied-filters — MJAppliedFiltersComponent”

“Filtered by …” removable chip row. Renders nothing when Filters is empty, so it’s safe to place unconditionally. (Remove), (ClearAll).


  • Generic components belong here; app-domain components don’t. If a component would make sense in any MJ app, add it to this package. If it encodes one app’s domain (e.g. scoring bands), keep it in that app’s shared package.
  • Components must be standalone: true, use --mj-* tokens for every color (no hardcoded hexes), support both themes, and be keyboard/ARIA accessible.
  • If a component ships a global .scss, register it in the copy-assets script in package.json and add it to the import list in this README and in explorer-app’s global styles.
  • Update the catalog above when adding or changing a component’s public API.