@memberjunction/ng-ui-components 5.47.0 → 5.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,276 @@
1
+ # @memberjunction/ng-ui-components
2
+
3
+ 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.
4
+
5
+ **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.
6
+
7
+ ## Installation & usage
8
+
9
+ The package ships standalone components — no NgModule required (the one exception is `MJAccordionModule`, a convenience bundle). Import the symbols you use directly:
10
+
11
+ ```typescript
12
+ import { MJButtonDirective, MJEmptyStateComponent, MJDialogComponent } from '@memberjunction/ng-ui-components';
13
+
14
+ @Component({
15
+ standalone: true,
16
+ imports: [MJButtonDirective, MJEmptyStateComponent, MJDialogComponent],
17
+ ...
18
+ })
19
+ ```
20
+
21
+ Dependencies: `@angular/cdk` (overlays) and `angular-split` (splitter) come along as package dependencies; `@angular/common`, `@angular/core`, and `rxjs` are peers.
22
+
23
+ ## Styling model — read this once
24
+
25
+ Components attach styles one of three ways, and one of them needs host cooperation:
26
+
27
+ 1. **Inline `styles` in the component** (most components) — nothing to do.
28
+ 2. **`styleUrls` compiled into the component** (`page-header`, `slide-panel`) — nothing to do.
29
+ 3. **Global stylesheets shipped as assets** — the form controls and overlay chrome (`button`, `dialog`, `dropdown`, `combobox`, `input`, `datepicker`, `splitter`, `accordion`, `window`, `chip`, `switch`/`progress`) ship their `.scss` to `dist` and expect the **host application** to import them globally.
30
+
31
+ **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:
32
+
33
+ ```scss
34
+ @import '@memberjunction/ng-ui-components/dist/lib/button/button';
35
+ @import '@memberjunction/ng-ui-components/dist/lib/dialog/dialog';
36
+ @import '@memberjunction/ng-ui-components/dist/lib/dropdown/dropdown';
37
+ @import '@memberjunction/ng-ui-components/dist/lib/combobox/combobox';
38
+ @import '@memberjunction/ng-ui-components/dist/lib/input/input';
39
+ @import '@memberjunction/ng-ui-components/dist/lib/input/input-switch-progress';
40
+ @import '@memberjunction/ng-ui-components/dist/lib/input/chip';
41
+ @import '@memberjunction/ng-ui-components/dist/lib/datepicker/datepicker';
42
+ @import '@memberjunction/ng-ui-components/dist/lib/splitter/splitter';
43
+ @import '@memberjunction/ng-ui-components/dist/lib/accordion/accordion';
44
+ @import '@memberjunction/ng-ui-components/dist/lib/window/window';
45
+ ```
46
+
47
+ All colors resolve through `--mj-*` semantic tokens, so light/dark theming requires zero component-level work.
48
+
49
+ ## Forms integration
50
+
51
+ `mj-combobox`, `mj-dropdown`, `mj-datepicker`, `mj-numeric-input`, and `mj-switch` all implement `ControlValueAccessor` — they work with `[(ngModel)]` and reactive forms, including `setDisabledState`.
52
+
53
+ ---
54
+
55
+ # Component catalog
56
+
57
+ ## Controls
58
+
59
+ ### `button[mjButton]` / `a[mjButton]` — MJButtonDirective
60
+ 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`.
61
+
62
+ ```html
63
+ <button mjButton variant="primary" (click)="save()">Save</button>
64
+ <button mjButton variant="icon" ariaLabel="Remove" (click)="remove()"><i class="fa-solid fa-xmark"></i></button>
65
+ <button mjButton variant="flat" [toggleable]="true" [(selected)]="isActive">Toggle</button>
66
+ ```
67
+
68
+ ### `mj-combobox` — MJComboboxComponent
69
+ 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`. Outputs: `ValueChange`, `FilterChange`. Custom item template via `<ng-template #mjComboboxItem>`.
70
+
71
+ ```html
72
+ <mj-combobox [Data]="categories" TextField="text" ValueField="value"
73
+ [(ngModel)]="selected" [ValuePrimitive]="true" [AllowCustom]="true" />
74
+ ```
75
+
76
+ ### `mj-dropdown` — MJDropdownComponent
77
+ 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>`.
78
+
79
+ ```html
80
+ <mj-dropdown [Data]="items" TextField="name" ValueField="id"
81
+ [(ngModel)]="selectedId" [ValuePrimitive]="true" />
82
+ ```
83
+
84
+ ### `mj-datepicker` — MJDatepickerComponent
85
+ 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`.)
86
+
87
+ ```html
88
+ <mj-datepicker [(ngModel)]="dueDate" [Min]="minDate" [Max]="maxDate" Placeholder="Select a date" />
89
+ ```
90
+
91
+ ### `mj-numeric-input` — MJNumericInputComponent
92
+ Native number input that clamps to `Min`/`Max`, rounds to `Decimals`, and formats on blur. Value flows through forms only (no separate output).
93
+
94
+ ```html
95
+ <mj-numeric-input [(ngModel)]="quantity" [Min]="0" [Max]="100" [Decimals]="2" />
96
+ ```
97
+
98
+ ### `mj-switch` — MJSwitchComponent
99
+ Boolean toggle with `role="switch"`, optional `OnLabel`/`OffLabel`.
100
+
101
+ ```html
102
+ <mj-switch [(ngModel)]="isEnabled" OnLabel="On" OffLabel="Off" />
103
+ ```
104
+
105
+ ### `[mjClickable]` — MJClickableDirective
106
+ 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`.
107
+
108
+ ```html
109
+ <div class="app-card" [mjClickable]="app.Name" (click)="open(app)">…</div>
110
+ ```
111
+
112
+ ### Calendar utilities
113
+ `calendar/calendar-utils` exports pure helpers shared with the datepicker: `BuildCalendarWeeks`, `FormatDate`, `FormatDateTime`, `GetMonthYearLabel`, `WEEK_DAYS`, `MONTH_NAMES`.
114
+
115
+ ## Layout & page chrome
116
+
117
+ The `page-*` family composes into the standard page skeleton:
118
+
119
+ ```html
120
+ <mj-page-layout>
121
+ <mj-page-header Title="Agents" Icon="fa-solid fa-robot" Subtitle="Manage your agents">
122
+ <span meta><mj-stat-badge [Count]="filtered" [Total]="total" Label="agents" /></span>
123
+ <div actions><button mjButton variant="primary">New</button></div>
124
+ <div toolbar><mj-page-search [Value]="q" (ValueChange)="q = $event" /></div>
125
+ </mj-page-header>
126
+ <mj-page-body>
127
+ <!-- content; scrolling and the 24px gutter are handled for you -->
128
+ </mj-page-body>
129
+ </mj-page-layout>
130
+ ```
131
+
132
+ And the left-nav shell variant for multi-section pages:
133
+
134
+ ```html
135
+ <mj-page-layout>
136
+ <mj-page-header Title="Admin" Icon="fa-solid fa-gear" />
137
+ <mj-page-body [Flex]="true" [Padding]="false" Direction="row">
138
+ <mj-left-nav [Sections]="sections" [ActiveId]="activeId" (ItemClicked)="go($event)" />
139
+ <mj-left-nav-content [Loading]="loading" [Error]="err">
140
+ <mj-page-header-interior Subtitle="Runtime state">
141
+ <div actions><button mjButton size="sm">+ Add</button></div>
142
+ </mj-page-header-interior>
143
+ <mj-page-body-interior><!-- sub-page content --></mj-page-body-interior>
144
+ </mj-left-nav-content>
145
+ </mj-page-body>
146
+ </mj-page-layout>
147
+ ```
148
+
149
+ ### `mj-page-layout` — outer shell
150
+ Flex-column, full-height, `overflow:hidden`, page background. No inputs.
151
+
152
+ ### `mj-page-header` — page chrome band
153
+ Inputs: `Title`, `Icon`, `Subtitle`. Slots: `[meta]` (badges under the title), `[actions]` (right-aligned), `[toolbar]` (secondary row).
154
+
155
+ ### `mj-page-body` — scrolling body region
156
+ Inputs: `Padding` (default true; false removes the gutter), `Flex` (default false), `Direction: 'row' | 'column'` (row stacks to column at ≤700px).
157
+
158
+ ### `mj-page-header-interior` / `mj-page-body-interior`
159
+ 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.
160
+
161
+ ### `mj-left-nav` / `mj-left-nav-content` — MJLeftNavComponent
162
+ Canonical left rail: sections, flat + tree items (badges, descriptions, disabled), off-canvas drawer at ≤700px. Inputs: `Sections: MJLeftNavSection[]`, `ActiveId`, `ExpandedIds`, `Width` (240), `MobileTitle`. Outputs: `ItemClicked`, `ItemToggled`. Slots: `[header]`, `[footer]`. Pair with `mj-left-nav-content`, which provides `Loading`/`Error` states and hides (not destroys) content while busy.
163
+
164
+ ### `mj-tab-nav` — MJTabNavComponent
165
+ Data-driven tab strip. `Tabs: TabConfig[]` (`key`, `label`, `icon?`, `badge?`, `badgeVariant?: 'default'|'error'|'warning'|'success'`), `ActiveKey`, `(TabChange)` emits the key.
166
+
167
+ ### `mj-page-search` — MJPageSearchComponent
168
+ Compact toolbar search input. `Placeholder`, `Value`, `Icon`; `(ValueChange)` on input.
169
+
170
+ ### `mj-slide-panel` — MjSlidePanelComponent
171
+ 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]`.
172
+
173
+ ### Splitter
174
+ Re-export of `angular-split` — use `<as-split>` / `<as-split-area>` directly:
175
+
176
+ ```html
177
+ <as-split direction="horizontal">
178
+ <as-split-area [size]="30">Left</as-split-area>
179
+ <as-split-area [size]="70">Right</as-split-area>
180
+ </as-split>
181
+ ```
182
+
183
+ ### `mj-accordion-panel` — MJAccordionPanelComponent (+ directives)
184
+ Collapsible panel replacing Kendo panelbar/expansion panel. Inputs: `Title`, `Expanded` (+`ExpandedChange`), `Disabled`, `Variant: 'default'|'primary'|'secondary'`, `Size: 'sm'|'md'`, `Bare`, `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.
185
+
186
+ ```html
187
+ <mj-accordion-panel [Expanded]="open" (ExpandedChange)="open = $event">
188
+ <ng-template mjAccordionTitle><i class="fa-solid fa-code"></i> Template Editor</ng-template>
189
+ <ng-template mjAccordionBody><mj-code-editor /></ng-template>
190
+ </mj-accordion-panel>
191
+ ```
192
+
193
+ ## Overlays
194
+
195
+ ### `mj-dialog` — MJDialogComponent
196
+ 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`.
197
+
198
+ ```html
199
+ <mj-dialog [Visible]="show" Title="Confirm" Size="md" (Close)="show = false">
200
+ <p>Are you sure?</p>
201
+ <mj-dialog-actions>
202
+ <button mjButton variant="primary" (click)="confirm()">Yes</button>
203
+ <button mjButton (click)="show = false">No</button>
204
+ </mj-dialog-actions>
205
+ </mj-dialog>
206
+ ```
207
+
208
+ ### `MJDialogService` — programmatic dialogs
209
+ `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?)`.
210
+
211
+ ### `mj-confirm-dialog` — MJConfirmDialogComponent
212
+ 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.
213
+
214
+ ### `MJConfirmService` — promise-based confirms
215
+ `await Confirm({ message, title?, detail?, type?, … })` → `boolean`, and `ConfirmDelete(...)` which forces danger styling and "Delete". Mounts above `mj-window` (z-index 20000).
216
+
217
+ ```typescript
218
+ private confirm = inject(MJConfirmService);
219
+ if (await this.confirm.ConfirmDelete({ message: 'Delete this view?', detail: 'Cannot be undone.' })) { … }
220
+ ```
221
+
222
+ ### `mj-window` — MJWindowComponent
223
+ 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.
224
+
225
+ ### `mj-filter-popover` — MJFilterPopoverComponent
226
+ 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.
227
+
228
+ ## Patterns
229
+
230
+ ### `mj-empty-state` — MJEmptyStateComponent
231
+ 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.
232
+
233
+ ```html
234
+ <mj-empty-state Variant="error" Title="Couldn't load permissions" [Message]="error"
235
+ ActionText="Try Again" ActionIcon="fa-solid fa-rotate-right" (Action)="loadData()" />
236
+ ```
237
+
238
+ ### `mj-stat-badge` — MJStatBadgeComponent
239
+ 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'`.
240
+
241
+ ```html
242
+ <mj-stat-badge [Count]="filteredCount" [Total]="totalCount" Label="agents" />
243
+ ```
244
+
245
+ ### `mj-alert` — MJAlertComponent
246
+ 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.
247
+
248
+ ### `mj-progress-bar` — MJProgressBarComponent
249
+ `Value` (0–100, clamped) or `Type="infinite"` for indeterminate. Styles ship in the global `input-switch-progress` sheet (see Styling model).
250
+
251
+ ### `mj-view-toggle` — MJViewToggleComponent
252
+ Segmented control for view modes. `Options: { key, icon?, label?, title? }[]`, `ActiveKey`, `(KeyChange)`. Provide `title` for icon-only options.
253
+
254
+ ### `mj-refresh-button` — MJRefreshButtonComponent
255
+ Canonical refresh: `Loading` spins + disables, label auto-hides on mobile. `(Clicked)` suppressed while loading/disabled.
256
+
257
+ ### `mj-filter-chip` — MJFilterChipComponent
258
+ Clickable filter pill: `Label`, `Icon`, `Count`, `Active` (+`aria-pressed`), `(Clicked)`.
259
+
260
+ ### `mj-filter-field` — MJFilterFieldComponent
261
+ Labeled row (icon + uppercase label) wrapping any projected widget — the escape hatch inside filter UIs.
262
+
263
+ ### `mj-filter-panel` — MJFilterPanelComponent
264
+ 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-field`s.
265
+
266
+ ### `mj-applied-filters` — MJAppliedFiltersComponent
267
+ "Filtered by …" removable chip row. Renders nothing when `Filters` is empty, so it's safe to place unconditionally. `(Remove)`, `(ClearAll)`.
268
+
269
+ ---
270
+
271
+ ## Contributing
272
+
273
+ - **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.
274
+ - Components must be `standalone: true`, use `--mj-*` tokens for every color (no hardcoded hexes), support both themes, and be keyboard/ARIA accessible.
275
+ - 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.
276
+ - Update the catalog above when adding or changing a component's public API.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/ng-ui-components",
3
- "version": "5.47.0",
3
+ "version": "5.49.0",
4
4
  "description": "MemberJunction: Angular UI Components - reusable standalone components replacing Kendo UI dependencies.",
5
5
  "main": "./dist/public-api.js",
6
6
  "typings": "./dist/public-api.d.ts",
@@ -10,7 +10,7 @@
10
10
  "scripts": {
11
11
  "test": "vitest run",
12
12
  "build": "npm run clean && ngc && npm run copy-assets",
13
- "copy-assets": "cpy src/lib/button/button.scss dist/lib/button --flat && cpy src/lib/dialog/dialog.scss dist/lib/dialog --flat && cpy src/lib/dropdown/dropdown.scss dist/lib/dropdown --flat && cpy src/lib/combobox/combobox.scss dist/lib/combobox --flat && cpy src/lib/input/input.scss dist/lib/input --flat && cpy src/lib/input/input-switch-progress.scss dist/lib/input --flat && cpy src/lib/datepicker/datepicker.scss dist/lib/datepicker --flat && cpy src/lib/splitter/splitter.scss dist/lib/splitter --flat && cpy src/lib/accordion/accordion.scss dist/lib/accordion --flat && cpy src/lib/window/window.scss dist/lib/window --flat && cpy src/lib/input/chip.scss dist/lib/input --flat && cpy src/lib/page-header/page-header.scss dist/lib/page-header --flat",
13
+ "copy-assets": "cpy src/lib/button/button.scss dist/lib/button --flat && cpy src/lib/dialog/dialog.scss dist/lib/dialog --flat && cpy src/lib/dropdown/dropdown.scss dist/lib/dropdown --flat && cpy src/lib/combobox/combobox.scss dist/lib/combobox --flat && cpy src/lib/input/input.scss dist/lib/input --flat && cpy src/lib/input/input-switch-progress.scss dist/lib/input --flat && cpy src/lib/datepicker/datepicker.scss dist/lib/datepicker --flat && cpy src/lib/splitter/splitter.scss dist/lib/splitter --flat && cpy src/lib/accordion/accordion.scss dist/lib/accordion --flat && cpy src/lib/window/window.scss dist/lib/window --flat && cpy src/lib/input/chip.scss dist/lib/input --flat",
14
14
  "clean": "rimraf dist",
15
15
  "test:watch": "vitest"
16
16
  },
@@ -23,7 +23,7 @@
23
23
  "devDependencies": {
24
24
  "@angular/compiler": "21.1.3",
25
25
  "@angular/compiler-cli": "21.1.3",
26
- "@memberjunction/ng-test-utils": "5.47.0",
26
+ "@memberjunction/ng-test-utils": "5.49.0",
27
27
  "vitest": "^4.0.18"
28
28
  },
29
29
  "peerDependencies": {
@@ -1,207 +0,0 @@
1
- :host {
2
- display: block;
3
- /* External consumers using <mj-page-header> outside <mj-page-layout> get a
4
- 24px bottom margin so the header has breathing room from whatever follows.
5
- This preserves backward compatibility with the published v5.x contract. */
6
- margin-bottom: var(--mj-space-6);
7
- }
8
-
9
- /* When nested inside <mj-page-layout>, the spacing between header and body is
10
- owned by <mj-page-body>'s padding-top — so the header sits flush against the
11
- body. This lets pages using <mj-page-body [Padding]="false"> (e.g. AI
12
- Analytics flush sidebar) render truly flush against the header. */
13
- :host-context(mj-page-layout) {
14
- margin-bottom: 0;
15
- }
16
-
17
- /* Slot-wrapper passthrough — any element a consumer projects with the `meta`,
18
- `actions`, or `toolbar` attribute (regardless of class name) becomes a
19
- transparent container so its children become direct flex children of the
20
- page-header row and inherit the row's gap. Eliminates the per-page
21
- `.{section}-header-meta { display: contents }` rules that pages had to
22
- declare in their own CSS files. */
23
- :host ::ng-deep [meta],
24
- :host ::ng-deep [actions],
25
- :host ::ng-deep [toolbar] {
26
- display: contents;
27
- }
28
-
29
- .mj-page-header {
30
- display: flex;
31
- flex-direction: column;
32
- background: var(--mj-bg-surface);
33
- /* Drop shadow for visual separation from the body below.
34
- `position: relative; z-index: 1` lifts the header into its own stacking
35
- context above <mj-page-body> (which has `position: relative` and would
36
- otherwise paint over the downward shadow as a later flex sibling).
37
- --mj-shadow-md is used instead of --mj-shadow-sm because the latter is
38
- rgba(0,0,0,0.05) in light mode — too faint to read as separation. */
39
- box-shadow: var(--mj-shadow-md);
40
- position: relative;
41
- z-index: 1;
42
- }
43
-
44
- .mj-page-header-row {
45
- display: flex;
46
- align-items: center;
47
- gap: var(--mj-space-3);
48
- padding: 0 var(--mj-space-6);
49
- }
50
-
51
- /* Primary row centers the actions cluster vertically against the identity
52
- column. The identity column stacks title → subtitle → (optional meta);
53
- centering keeps the actions visually balanced against the whole block
54
- instead of pinning them to the title baseline. When meta badges are
55
- present the actions drift down a touch — acceptable trade for the more
56
- common no-meta case where flex-start looks top-heavy. */
57
- .mj-page-header-row--primary {
58
- justify-content: space-between;
59
- align-items: center;
60
- gap: var(--mj-space-4);
61
- padding-top: var(--mj-space-4);
62
- padding-bottom: var(--mj-space-4);
63
- }
64
-
65
- .mj-page-header-row--toolbar {
66
- flex-wrap: wrap;
67
- /* Control bar (search · Filter · view) is right-aligned on desktop; the
68
- mobile block below resets it to flex-start. */
69
- justify-content: flex-end;
70
- padding-top: 0;
71
- padding-bottom: var(--mj-space-4);
72
- border-top: 1px solid var(--mj-border-subtle);
73
- margin-top: 0;
74
- padding-top: var(--mj-space-3);
75
- }
76
-
77
- .mj-page-header-row--toolbar:empty {
78
- display: none;
79
- border-top: 0;
80
- padding: 0;
81
- }
82
-
83
- .mj-page-header-titlebar {
84
- display: flex;
85
- align-items: center;
86
- gap: var(--mj-space-3);
87
- flex: 1 1 auto;
88
- min-width: 0;
89
- }
90
-
91
- .mj-page-header-icon {
92
- flex-shrink: 0;
93
- width: 40px;
94
- height: 40px;
95
- display: flex;
96
- align-items: center;
97
- justify-content: center;
98
- background: color-mix(in srgb, var(--mj-brand-primary) 12%, transparent);
99
- color: var(--mj-brand-primary);
100
- border-radius: var(--mj-radius-md);
101
- font-size: var(--mj-text-lg);
102
- }
103
-
104
- /* Identity column — title, subtitle, meta stacked vertically. */
105
- .mj-page-header-text {
106
- min-width: 0;
107
- flex: 1 1 auto;
108
- display: flex;
109
- flex-direction: column;
110
- }
111
-
112
- .mj-page-header-title {
113
- margin: 0;
114
- font-size: var(--mj-text-xl);
115
- font-weight: var(--mj-font-semibold);
116
- color: var(--mj-text-primary);
117
- line-height: 1.25;
118
- white-space: nowrap;
119
- overflow: hidden;
120
- text-overflow: ellipsis;
121
- }
122
-
123
- .mj-page-header-subtitle {
124
- margin: var(--mj-space-1) 0 0;
125
- font-size: var(--mj-text-sm);
126
- color: var(--mj-text-secondary);
127
- line-height: 1.4;
128
- }
129
-
130
- /* Meta lives in the identity column UNDER the subtitle — supporting info
131
- for the identity, not a peer to the title. Wraps when there are many
132
- badges. Matches the mj-page-header-interior pattern so both chrome
133
- surfaces have one mental model. */
134
- .mj-page-header-meta {
135
- display: flex;
136
- flex-wrap: wrap;
137
- align-items: center;
138
- gap: var(--mj-space-2);
139
- margin-top: var(--mj-space-2);
140
- }
141
-
142
- .mj-page-header-actions {
143
- display: flex;
144
- align-items: center;
145
- gap: var(--mj-space-3);
146
- flex-shrink: 0;
147
- }
148
-
149
- .mj-page-header-meta:empty,
150
- .mj-page-header-actions:empty {
151
- display: none;
152
- }
153
-
154
- /* ── Mobile compaction (≤768px) ───────────────────────────────────────────
155
- On a phone the header must not eat the viewport. We tighten padding, drop
156
- the title a size, hide the decorative icon + subtitle (identity is carried
157
- by the title alone), and let the actions cluster wrap tightly under the
158
- title. The meta badge and toolbar stay — they're functional. */
159
- @media (max-width: 768px) {
160
- .mj-page-header-row {
161
- padding-left: var(--mj-space-4);
162
- padding-right: var(--mj-space-4);
163
- gap: var(--mj-space-2);
164
- }
165
- .mj-page-header-row--primary {
166
- /* Stays a single row on mobile: title (truncates) + the primary CTA in
167
- [actions]. Filters/search/view now live in the [toolbar] control bar, so
168
- the primary row no longer needs to stack. */
169
- gap: var(--mj-space-2);
170
- padding-top: var(--mj-space-3);
171
- padding-bottom: var(--mj-space-3);
172
- }
173
- /* Icon + subtitle are desktop niceties; reclaim their footprint on mobile. */
174
- .mj-page-header-icon,
175
- .mj-page-header-subtitle {
176
- display: none;
177
- }
178
- .mj-page-header-title {
179
- font-size: var(--mj-text-lg);
180
- line-height: 1.2;
181
- }
182
- .mj-page-header-meta {
183
- margin-top: var(--mj-space-1);
184
- }
185
- .mj-page-header-actions {
186
- flex-wrap: wrap;
187
- justify-content: flex-start;
188
- gap: var(--mj-space-2);
189
- }
190
- /* Action buttons go icon-only on mobile — consumers wrap the text in
191
- <span class="action-btn-label"> so it can hide without touching mjButton. */
192
- .mj-page-header-actions ::ng-deep .action-btn-label {
193
- display: none;
194
- }
195
- .mj-page-header-row--toolbar {
196
- /* Left-aligned on mobile (search grows to fill the row). */
197
- justify-content: flex-start;
198
- padding-top: var(--mj-space-2);
199
- padding-bottom: var(--mj-space-3);
200
- }
201
- /* In the control bar (search · Filter · view), let search grow so the Filter
202
- button + view-toggle sit at the trailing edge on one line. */
203
- .mj-page-header-row--toolbar ::ng-deep mj-page-search {
204
- flex: 1;
205
- min-width: 0;
206
- }
207
- }