@frame-kit/ui-ng-patterns 0.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FrameKit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @frame-kit/ui-ng-patterns
2
+
3
+ Opinionated, **dashboard-ready Angular components** composed from
4
+ [`@frame-kit/ui-ng`](https://www.npmjs.com/package/@frame-kit/ui-ng) primitives. Where `ui-ng` is
5
+ deliberately unstyled (structure, accessibility, behavior), this layer adds the **composition, layout,
6
+ and sensible defaults** you'd otherwise rebuild on every dashboard — while staying fully themeable
7
+ through the same `--fk-*` token contract.
8
+
9
+ ```
10
+ @frame-kit/tokens design variables (--fk-*)
11
+
12
+ @frame-kit/ui-ng unstyled primitives (button, card, icon, …)
13
+
14
+ @frame-kit/ui-ng-patterns ← you are here: composed, styled patterns
15
+
16
+ your app domain-specific glue only
17
+ ```
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm i @frame-kit/ui-ng-patterns @frame-kit/ui-ng @frame-kit/tokens
23
+ ```
24
+
25
+ `@frame-kit/ui-ng` and the Angular packages are peer dependencies. Load the `@frame-kit/tokens`
26
+ stylesheets (and any brand overrides) as described in the
27
+ [theming guide](https://github.com/frame-kit/packages/blob/main/packages/ui-ng/THEMING.md).
28
+
29
+ ## Usage
30
+
31
+ Patterns use the **`fk-`** selector prefix, the same as `ui-ng` primitives. Each layer ships as its
32
+ own entry point for tree-shaking:
33
+
34
+ ```ts
35
+ import { PageHeaderComponent } from '@frame-kit/ui-ng-patterns/dashboard';
36
+ import { EndpointLinkComponent } from '@frame-kit/ui-ng-patterns/docs';
37
+ ```
38
+
39
+ ## What belongs here
40
+
41
+ A component is promoted into this package when it:
42
+
43
+ - Composes **two or more** `ui-ng` primitives/layouts,
44
+ - Would be reused across **two or more** apps/dashboards,
45
+ - Carries **no domain data** (no app APIs, enums, or business concepts),
46
+ - Is **token-driven** (`--fk-*` plus component-scoped `--fk-<component>-*` with two-tier fallbacks).
47
+
48
+ Domain-specific UI (auth banners, business cards, feature-flagged screens) stays in the consuming app.
49
+
50
+ ## Building components
51
+
52
+ See the [Component Development Guide](./DEVELOPMENT_GUIDE.md) for this package's
53
+ conventions. It inherits the [ui-ng guide](../ui-ng/DEVELOPMENT_GUIDE.md) and
54
+ documents the package-specific differences (layers, one entry point per layer,
55
+ selector prefix, story/token rules) plus the ui-ng rules that do **not** apply here.
56
+
57
+ ## Status
58
+
59
+ Early — components are being added one at a time. See the
60
+ [patterns catalog](./COMPONENTS.md) and the
61
+ [ui-ng primitive catalog](https://github.com/frame-kit/packages/blob/main/packages/ui-ng/COMPONENTS.md).
62
+
63
+ ## License
64
+
65
+ MIT
@@ -0,0 +1,279 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, ChangeDetectionStrategy, ViewEncapsulation, Component, computed, HostBinding } from '@angular/core';
3
+ import { HeadlineComponent } from '@frame-kit/ui-ng/core/headline';
4
+ import { TextComponent } from '@frame-kit/ui-ng/core/text';
5
+ import { IconComponent } from '@frame-kit/ui-ng/core/icon';
6
+
7
+ /**
8
+ * Page-level dashboard header: title + optional description on the left, up to
9
+ * ~3 actions on the right.
10
+ *
11
+ * When the header's own width gets tight, the actions drop below the text and
12
+ * go full-width (mobile-style). The break is driven by a CSS container query on
13
+ * the host, so it responds to the content region's width — not the viewport,
14
+ * which means it behaves correctly inside a narrow panel.
15
+ *
16
+ * Project actions by tagging each button with `pageHeaderActions`:
17
+ *
18
+ * <fk-page-header title="Users" description="...">
19
+ * <fk-button pageHeaderActions variant="outline" size="sm">Import</fk-button>
20
+ * <fk-button pageHeaderActions variant="primary" size="sm">Create user</fk-button>
21
+ * </fk-page-header>
22
+ *
23
+ * Tagging each button (rather than wrapping them in a div) makes every button a
24
+ * direct flex child, so full-width stacking falls out of `align-items: stretch`
25
+ * with no `::ng-deep` reach-in.
26
+ *
27
+ * The title row has two projected slots — `[pageHeaderLeading]` before the
28
+ * title and `[pageHeaderTrailing]` after it. Project a plain `fk-icon` for a
29
+ * decorative mark, or a `button` for a clickable one (the consumer owns the
30
+ * interaction, focus ring, and aria-label):
31
+ *
32
+ * <fk-page-header title="Permissions">
33
+ * <fk-icon pageHeaderLeading name="key-outline" />
34
+ * <button pageHeaderTrailing type="button" aria-label="Docs" (click)="openDocs()">
35
+ * <fk-icon name="view-api" />
36
+ * </button>
37
+ * </fk-page-header>
38
+ */
39
+ class PageHeaderComponent {
40
+ title = input.required(...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
41
+ description = input(null, ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
42
+ headingLevel = input(1, ...(ngDevMode ? [{ debugName: "headingLevel" }] : /* istanbul ignore next */ []));
43
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PageHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
44
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: PageHeaderComponent, isStandalone: true, selector: "fk-page-header", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<header class=\"fk-page-header\">\n <div class=\"fk-page-header__text\">\n <div class=\"fk-page-header__title-row\">\n <ng-content select=\"[pageHeaderLeading]\" />\n\n <fk-headline [level]=\"headingLevel()\">{{ title() }}</fk-headline>\n\n <ng-content select=\"[pageHeaderTrailing]\" />\n </div>\n\n @if (description()) {\n <fk-text class=\"fk-page-header__description\" tone=\"muted\">{{\n description()\n }}</fk-text>\n }\n </div>\n\n <div class=\"fk-page-header__actions\">\n <ng-content select=\"[pageHeaderActions]\" />\n </div>\n</header>\n", styles: [":host{display:block;container-type:inline-size}.fk-page-header{display:flex;flex-direction:column;gap:var(--fk-rhythm-4, 1rem)}.fk-page-header__text{min-width:0}.fk-page-header__title-row{display:flex;align-items:center;gap:var(--fk-rhythm-2, .5rem)}.fk-page-header__title-row fk-headline{margin-block:0}.fk-page-header__description{display:block;margin-top:var(--fk-rhythm-2, .5rem);max-width:60ch;line-height:1.5}.fk-page-header__actions{display:flex;flex-direction:column;gap:var(--fk-rhythm-3, .75rem)}@container (min-width: 40rem){.fk-page-header{flex-direction:row;align-items:flex-start;justify-content:space-between}.fk-page-header__actions{flex-direction:row;flex-shrink:0;align-items:center}}\n"], dependencies: [{ kind: "component", type: HeadlineComponent, selector: "fk-headline", inputs: ["ariaLabel", "className", "fontSize", "id", "level", "variant", "visuallyHidden"] }, { kind: "component", type: TextComponent, selector: "fk-text", inputs: ["as", "variant", "tone", "align", "weight", "truncate", "maxLines", "gutterBottom", "italic", "visuallyHidden", "className", "id", "ariaLabel", "ariaDescribedBy", "role", "htmlFor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
45
+ }
46
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PageHeaderComponent, decorators: [{
47
+ type: Component,
48
+ args: [{ selector: 'fk-page-header', standalone: true, imports: [HeadlineComponent, TextComponent], encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "<header class=\"fk-page-header\">\n <div class=\"fk-page-header__text\">\n <div class=\"fk-page-header__title-row\">\n <ng-content select=\"[pageHeaderLeading]\" />\n\n <fk-headline [level]=\"headingLevel()\">{{ title() }}</fk-headline>\n\n <ng-content select=\"[pageHeaderTrailing]\" />\n </div>\n\n @if (description()) {\n <fk-text class=\"fk-page-header__description\" tone=\"muted\">{{\n description()\n }}</fk-text>\n }\n </div>\n\n <div class=\"fk-page-header__actions\">\n <ng-content select=\"[pageHeaderActions]\" />\n </div>\n</header>\n", styles: [":host{display:block;container-type:inline-size}.fk-page-header{display:flex;flex-direction:column;gap:var(--fk-rhythm-4, 1rem)}.fk-page-header__text{min-width:0}.fk-page-header__title-row{display:flex;align-items:center;gap:var(--fk-rhythm-2, .5rem)}.fk-page-header__title-row fk-headline{margin-block:0}.fk-page-header__description{display:block;margin-top:var(--fk-rhythm-2, .5rem);max-width:60ch;line-height:1.5}.fk-page-header__actions{display:flex;flex-direction:column;gap:var(--fk-rhythm-3, .75rem)}@container (min-width: 40rem){.fk-page-header{flex-direction:row;align-items:flex-start;justify-content:space-between}.fk-page-header__actions{flex-direction:row;flex-shrink:0;align-items:center}}\n"] }]
49
+ }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }] } });
50
+
51
+ /**
52
+ * Empty-state panel for dashboard tables and lists: a centered icon, a title,
53
+ * an optional description, and a projected actions slot (typically one primary
54
+ * fk-button). Bordered-card styling, meant to stand in for a table body when
55
+ * there are no rows.
56
+ *
57
+ * The action row flips from a horizontal row to a centered vertical stack on
58
+ * narrow widths, driven by a container query on the host — so it reacts to the
59
+ * empty-state's own width (e.g. inside a split column) rather than the viewport.
60
+ *
61
+ * <fk-table-empty-state
62
+ * icon="globe"
63
+ * title="No webhooks configured"
64
+ * description="Add a webhook to receive events."
65
+ * >
66
+ * <fk-button variant="primary" size="sm">Add webhook</fk-button>
67
+ * </fk-table-empty-state>
68
+ *
69
+ * The actions slot is an open `ng-content`, so project as many actions as the
70
+ * design needs (one primary is the norm; two reads cleanly).
71
+ */
72
+ class TableEmptyStateComponent {
73
+ // ===== INPUTS =====
74
+ icon = input(null, ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
75
+ title = input.required(...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
76
+ description = input(null, ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
77
+ // ===== BASE PROPS =====
78
+ className = input('', ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
79
+ id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
80
+ ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
81
+ // ===== COMPUTED =====
82
+ classes = computed(() => ['fk-table-empty-state', this.className()].filter(Boolean).join(' '), ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
83
+ get hostClass() {
84
+ return this.classes();
85
+ }
86
+ get hostId() {
87
+ return this.id();
88
+ }
89
+ get hostAriaLabel() {
90
+ return this.ariaLabel();
91
+ }
92
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: TableEmptyStateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
93
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: TableEmptyStateComponent, isStandalone: true, selector: "fk-table-empty-state", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClass", "attr.id": "this.hostId", "attr.aria-label": "this.hostAriaLabel" } }, ngImport: i0, template: "@if (icon()) {\n <div class=\"fk-table-empty-state__icon\">\n <fk-icon [name]=\"icon()!\" size=\"lg\" />\n </div>\n}\n\n<h3 class=\"fk-table-empty-state__title\">{{ title() }}</h3>\n\n@if (description()) {\n <p class=\"fk-table-empty-state__description\">{{ description() }}</p>\n}\n\n<div class=\"fk-table-empty-state__actions\">\n <ng-content />\n</div>\n", styles: [":host{container-type:inline-size;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:var(--fk-table-empty-state-gap, var(--fk-rhythm-3, .75rem));padding:var(--fk-table-empty-state-padding-block, var(--fk-rhythm-8, 2rem)) var(--fk-table-empty-state-padding-inline, var(--fk-rhythm-4, 1rem));border:1px solid var(--fk-table-empty-state-border-color, var(--fk-color-border, #d9e2ee));border-radius:var(--fk-table-empty-state-radius, var(--fk-radius-lg, .75rem));background:var(--fk-table-empty-state-bg, var(--fk-color-surface, #ffffff))}.fk-table-empty-state__icon{color:var(--fk-table-empty-state-icon-color, var(--fk-color-muted, #8a98a8))}.fk-table-empty-state__title{margin:0;font-size:var(--fk-table-empty-state-title-font-size, var(--fk-typography-body-font-size, .9375rem));font-weight:var(--fk-table-empty-state-title-font-weight, var(--fk-font-weight-semibold, 600));color:var(--fk-table-empty-state-title-color, var(--fk-color-text-strong, #0b1420))}.fk-table-empty-state__description{margin:0;font-size:var(--fk-table-empty-state-description-font-size, var(--fk-typography-small-font-size, .8125rem));color:var(--fk-table-empty-state-description-color, var(--fk-color-muted, #8a98a8));max-width:var(--fk-table-empty-state-description-max-width, 24rem)}.fk-table-empty-state__actions{display:flex;gap:var(--fk-table-empty-state-actions-gap, var(--fk-rhythm-2, .5rem));margin-top:var(--fk-table-empty-state-actions-margin-top, var(--fk-rhythm-1, .25rem))}.fk-table-empty-state__actions:empty{display:none}@container (max-width: 28rem){.fk-table-empty-state__actions{flex-direction:column;align-self:stretch;align-items:stretch}}\n"], dependencies: [{ kind: "component", type: IconComponent, selector: "fk-icon", inputs: ["name", "size", "color", "className", "id", "ariaLabel", "ariaHidden"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
94
+ }
95
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: TableEmptyStateComponent, decorators: [{
96
+ type: Component,
97
+ args: [{ selector: 'fk-table-empty-state', standalone: true, imports: [IconComponent], encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (icon()) {\n <div class=\"fk-table-empty-state__icon\">\n <fk-icon [name]=\"icon()!\" size=\"lg\" />\n </div>\n}\n\n<h3 class=\"fk-table-empty-state__title\">{{ title() }}</h3>\n\n@if (description()) {\n <p class=\"fk-table-empty-state__description\">{{ description() }}</p>\n}\n\n<div class=\"fk-table-empty-state__actions\">\n <ng-content />\n</div>\n", styles: [":host{container-type:inline-size;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:var(--fk-table-empty-state-gap, var(--fk-rhythm-3, .75rem));padding:var(--fk-table-empty-state-padding-block, var(--fk-rhythm-8, 2rem)) var(--fk-table-empty-state-padding-inline, var(--fk-rhythm-4, 1rem));border:1px solid var(--fk-table-empty-state-border-color, var(--fk-color-border, #d9e2ee));border-radius:var(--fk-table-empty-state-radius, var(--fk-radius-lg, .75rem));background:var(--fk-table-empty-state-bg, var(--fk-color-surface, #ffffff))}.fk-table-empty-state__icon{color:var(--fk-table-empty-state-icon-color, var(--fk-color-muted, #8a98a8))}.fk-table-empty-state__title{margin:0;font-size:var(--fk-table-empty-state-title-font-size, var(--fk-typography-body-font-size, .9375rem));font-weight:var(--fk-table-empty-state-title-font-weight, var(--fk-font-weight-semibold, 600));color:var(--fk-table-empty-state-title-color, var(--fk-color-text-strong, #0b1420))}.fk-table-empty-state__description{margin:0;font-size:var(--fk-table-empty-state-description-font-size, var(--fk-typography-small-font-size, .8125rem));color:var(--fk-table-empty-state-description-color, var(--fk-color-muted, #8a98a8));max-width:var(--fk-table-empty-state-description-max-width, 24rem)}.fk-table-empty-state__actions{display:flex;gap:var(--fk-table-empty-state-actions-gap, var(--fk-rhythm-2, .5rem));margin-top:var(--fk-table-empty-state-actions-margin-top, var(--fk-rhythm-1, .25rem))}.fk-table-empty-state__actions:empty{display:none}@container (max-width: 28rem){.fk-table-empty-state__actions{flex-direction:column;align-self:stretch;align-items:stretch}}\n"] }]
98
+ }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hostClass: [{
99
+ type: HostBinding,
100
+ args: ['class']
101
+ }], hostId: [{
102
+ type: HostBinding,
103
+ args: ['attr.id']
104
+ }], hostAriaLabel: [{
105
+ type: HostBinding,
106
+ args: ['attr.aria-label']
107
+ }] } });
108
+
109
+ /**
110
+ * Two-column split layout with container-query responsive stacking.
111
+ *
112
+ * Projects content into `[start]` (grows to fill) and `[end]` (content-sized or
113
+ * fixed width). Below 700px container width, columns stack vertically.
114
+ *
115
+ * A standalone layout primitive for custom dashboard rows.
116
+ */
117
+ class ContentSplitLayoutComponent {
118
+ endWidth = input(null, ...(ngDevMode ? [{ debugName: "endWidth" }] : /* istanbul ignore next */ []));
119
+ gap = input('var(--fk-rhythm-4, 1rem)', ...(ngDevMode ? [{ debugName: "gap" }] : /* istanbul ignore next */ []));
120
+ hostClass = 'fk-content-split-layout';
121
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ContentSplitLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
122
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: ContentSplitLayoutComponent, isStandalone: true, selector: "fk-content-split-layout", inputs: { endWidth: { classPropertyName: "endWidth", publicName: "endWidth", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClass" } }, ngImport: i0, template: "<div\n class=\"fk-content-split-layout__row\"\n [style.--fk-content-split-gap]=\"gap()\"\n>\n <div class=\"fk-content-split-layout__start\">\n <ng-content select=\"[start]\" />\n </div>\n\n <div\n class=\"fk-content-split-layout__end\"\n [class.fk-content-split-layout__end--fixed]=\"endWidth()\"\n [style.--fk-content-split-end-width]=\"endWidth()\"\n >\n <ng-content select=\"[end]\" />\n </div>\n</div>\n", styles: [":host{display:block;container-type:inline-size}.fk-content-split-layout__row{display:flex;flex-direction:column;gap:var(--fk-content-split-gap, var(--fk-rhythm-4, 1rem))}@container (min-width: 700px){.fk-content-split-layout__row{flex-direction:row;align-items:flex-start}}.fk-content-split-layout__start{display:flex;flex-direction:column;padding-top:var(--fk-rhythm-4, 1rem);width:100%}.fk-content-split-layout__start>*+*{margin-top:var(--fk-content-split-gap, var(--fk-rhythm-4, 1rem))}@container (min-width: 700px){.fk-content-split-layout__start{flex:1;min-width:0}}.fk-content-split-layout__end{width:100%}@container (min-width: 700px){.fk-content-split-layout__end{flex:0 0 auto}}@container (min-width: 700px){.fk-content-split-layout__end--fixed{flex:0 0 var(--fk-content-split-end-width)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
123
+ }
124
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ContentSplitLayoutComponent, decorators: [{
125
+ type: Component,
126
+ args: [{ selector: 'fk-content-split-layout', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"fk-content-split-layout__row\"\n [style.--fk-content-split-gap]=\"gap()\"\n>\n <div class=\"fk-content-split-layout__start\">\n <ng-content select=\"[start]\" />\n </div>\n\n <div\n class=\"fk-content-split-layout__end\"\n [class.fk-content-split-layout__end--fixed]=\"endWidth()\"\n [style.--fk-content-split-end-width]=\"endWidth()\"\n >\n <ng-content select=\"[end]\" />\n </div>\n</div>\n", styles: [":host{display:block;container-type:inline-size}.fk-content-split-layout__row{display:flex;flex-direction:column;gap:var(--fk-content-split-gap, var(--fk-rhythm-4, 1rem))}@container (min-width: 700px){.fk-content-split-layout__row{flex-direction:row;align-items:flex-start}}.fk-content-split-layout__start{display:flex;flex-direction:column;padding-top:var(--fk-rhythm-4, 1rem);width:100%}.fk-content-split-layout__start>*+*{margin-top:var(--fk-content-split-gap, var(--fk-rhythm-4, 1rem))}@container (min-width: 700px){.fk-content-split-layout__start{flex:1;min-width:0}}.fk-content-split-layout__end{width:100%}@container (min-width: 700px){.fk-content-split-layout__end{flex:0 0 auto}}@container (min-width: 700px){.fk-content-split-layout__end--fixed{flex:0 0 var(--fk-content-split-end-width)}}\n"] }]
127
+ }], propDecorators: { endWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "endWidth", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }], hostClass: [{
128
+ type: HostBinding,
129
+ args: ['class']
130
+ }] } });
131
+
132
+ /**
133
+ * Responsive column layout for dashboard content. Lays projected children out
134
+ * in equal-width columns that wrap — and ultimately collapse to a single
135
+ * stacked column — based on the layout's *own* available width, never the
136
+ * viewport. So it reacts correctly when the dashboard content region is
137
+ * squeezed (sidenav open, a drawer pushed in, a split pane) while the viewport
138
+ * still reports "desktop".
139
+ *
140
+ * Implemented with an intrinsic CSS grid — `repeat(auto-fit, minmax(min, 1fr))`
141
+ * — so there is no container query, no JS, and no viewport breakpoint to keep
142
+ * in sync. `minColumnWidth` is the smallest a column may shrink to before the
143
+ * grid drops a column; empty tracks collapse, so N children never produce more
144
+ * than N columns. Spacing on both axes is a single `gutter` gap, so wrapped
145
+ * rows and side-by-side columns separate consistently with no edge artifacts.
146
+ *
147
+ * <fk-columns minColumnWidth="18rem">
148
+ * <fk-field-group>…</fk-field-group>
149
+ * <fk-field-group>…</fk-field-group>
150
+ * </fk-columns>
151
+ */
152
+ class ColumnsComponent {
153
+ // ===== INPUTS =====
154
+ /**
155
+ * The smallest width a column may shrink to before the grid drops to fewer
156
+ * columns. This is the single knob that controls when columns wrap/stack.
157
+ */
158
+ minColumnWidth = input('16rem', ...(ngDevMode ? [{ debugName: "minColumnWidth" }] : /* istanbul ignore next */ []));
159
+ /** Gap between side-by-side columns and between wrapped rows. */
160
+ gutter = input('var(--fk-rhythm-4, 1rem)', ...(ngDevMode ? [{ debugName: "gutter" }] : /* istanbul ignore next */ []));
161
+ // ===== BASE PROPS =====
162
+ className = input('', ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
163
+ id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
164
+ ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
165
+ // ===== COMPUTED =====
166
+ classes = computed(() => ['fk-columns', this.className()].filter(Boolean).join(' '), ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
167
+ get hostClass() {
168
+ return this.classes();
169
+ }
170
+ get hostId() {
171
+ return this.id();
172
+ }
173
+ get hostAriaLabel() {
174
+ return this.ariaLabel();
175
+ }
176
+ get hostMin() {
177
+ return this.minColumnWidth();
178
+ }
179
+ get hostGutter() {
180
+ return this.gutter();
181
+ }
182
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ColumnsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
183
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: ColumnsComponent, isStandalone: true, selector: "fk-columns", inputs: { minColumnWidth: { classPropertyName: "minColumnWidth", publicName: "minColumnWidth", isSignal: true, isRequired: false, transformFunction: null }, gutter: { classPropertyName: "gutter", publicName: "gutter", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClass", "attr.id": "this.hostId", "attr.aria-label": "this.hostAriaLabel", "style.--fk-columns-min": "this.hostMin", "style.--fk-columns-gutter": "this.hostGutter" } }, ngImport: i0, template: "<ng-content />\n", styles: [":host{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(var(--fk-columns-min, 16rem),100%),1fr));gap:var(--fk-columns-gutter, var(--fk-rhythm-4, 1rem));align-items:start}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
184
+ }
185
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ColumnsComponent, decorators: [{
186
+ type: Component,
187
+ args: [{ selector: 'fk-columns', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content />\n", styles: [":host{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(var(--fk-columns-min, 16rem),100%),1fr));gap:var(--fk-columns-gutter, var(--fk-rhythm-4, 1rem));align-items:start}\n"] }]
188
+ }], propDecorators: { minColumnWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minColumnWidth", required: false }] }], gutter: [{ type: i0.Input, args: [{ isSignal: true, alias: "gutter", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hostClass: [{
189
+ type: HostBinding,
190
+ args: ['class']
191
+ }], hostId: [{
192
+ type: HostBinding,
193
+ args: ['attr.id']
194
+ }], hostAriaLabel: [{
195
+ type: HostBinding,
196
+ args: ['attr.aria-label']
197
+ }], hostMin: [{
198
+ type: HostBinding,
199
+ args: ['style.--fk-columns-min']
200
+ }], hostGutter: [{
201
+ type: HostBinding,
202
+ args: ['style.--fk-columns-gutter']
203
+ }] } });
204
+
205
+ /**
206
+ * Padded container for an expanded dashboard detail region — typically the body
207
+ * of a data-table row expansion, holding an `fk-columns` of form controls.
208
+ *
209
+ * The `disabled` input greys the panel and makes its entire subtree `inert`
210
+ * (non-interactive, unfocusable, hidden from assistive technology) — for
211
+ * controls gated behind a feature toggle that must not be editable until the
212
+ * feature is enabled.
213
+ *
214
+ * <fk-detail-panel [disabled]="!feature.enabled">
215
+ * <fk-columns>…</fk-columns>
216
+ * </fk-detail-panel>
217
+ */
218
+ class DetailPanelComponent {
219
+ // ===== INPUTS =====
220
+ /**
221
+ * Greys the panel and makes its content inert (locked) — for feature-gated
222
+ * controls that should not be editable until the feature is enabled.
223
+ */
224
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
225
+ // ===== BASE PROPS =====
226
+ className = input('', ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
227
+ id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
228
+ ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
229
+ // ===== COMPUTED =====
230
+ classes = computed(() => [
231
+ 'fk-detail-panel',
232
+ this.disabled() ? 'fk-detail-panel--disabled' : '',
233
+ this.className(),
234
+ ]
235
+ .filter(Boolean)
236
+ .join(' '), ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
237
+ get hostClass() {
238
+ return this.classes();
239
+ }
240
+ get hostId() {
241
+ return this.id();
242
+ }
243
+ get hostAriaLabel() {
244
+ return this.ariaLabel();
245
+ }
246
+ /**
247
+ * Lock the whole subtree when disabled. `inert` blocks pointer interaction,
248
+ * focus, and assistive-tech access in one attribute — stronger and more
249
+ * correct than `pointer-events: none`, which only blocks the mouse.
250
+ */
251
+ get hostInert() {
252
+ return this.disabled() ? '' : null;
253
+ }
254
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DetailPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
255
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: DetailPanelComponent, isStandalone: true, selector: "fk-detail-panel", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClass", "attr.id": "this.hostId", "attr.aria-label": "this.hostAriaLabel", "attr.inert": "this.hostInert" } }, ngImport: i0, template: "<ng-content />\n", styles: [":host{display:block;padding:var(--fk-detail-panel-padding-block, var(--fk-rhythm-2, .5rem)) var(--fk-detail-panel-padding-inline, var(--fk-rhythm-1, .25rem))}:host.fk-detail-panel--disabled{opacity:var(--fk-detail-panel-disabled-opacity, .55)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
256
+ }
257
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DetailPanelComponent, decorators: [{
258
+ type: Component,
259
+ args: [{ selector: 'fk-detail-panel', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content />\n", styles: [":host{display:block;padding:var(--fk-detail-panel-padding-block, var(--fk-rhythm-2, .5rem)) var(--fk-detail-panel-padding-inline, var(--fk-rhythm-1, .25rem))}:host.fk-detail-panel--disabled{opacity:var(--fk-detail-panel-disabled-opacity, .55)}\n"] }]
260
+ }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hostClass: [{
261
+ type: HostBinding,
262
+ args: ['class']
263
+ }], hostId: [{
264
+ type: HostBinding,
265
+ args: ['attr.id']
266
+ }], hostAriaLabel: [{
267
+ type: HostBinding,
268
+ args: ['attr.aria-label']
269
+ }], hostInert: [{
270
+ type: HostBinding,
271
+ args: ['attr.inert']
272
+ }] } });
273
+
274
+ /**
275
+ * Generated bundle index. Do not edit.
276
+ */
277
+
278
+ export { ColumnsComponent, ContentSplitLayoutComponent, DetailPanelComponent, PageHeaderComponent, TableEmptyStateComponent };
279
+ //# sourceMappingURL=frame-kit-ui-ng-patterns-dashboard.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-kit-ui-ng-patterns-dashboard.mjs","sources":["../../../../packages/ui-ng-patterns/dashboard/page-header/page-header.component.ts","../../../../packages/ui-ng-patterns/dashboard/page-header/page-header.component.html","../../../../packages/ui-ng-patterns/dashboard/table-empty-state/table-empty-state.component.ts","../../../../packages/ui-ng-patterns/dashboard/table-empty-state/table-empty-state.component.html","../../../../packages/ui-ng-patterns/dashboard/content-split-layout/content-split-layout.component.ts","../../../../packages/ui-ng-patterns/dashboard/content-split-layout/content-split-layout.component.html","../../../../packages/ui-ng-patterns/dashboard/columns/columns.component.ts","../../../../packages/ui-ng-patterns/dashboard/columns/columns.component.html","../../../../packages/ui-ng-patterns/dashboard/detail-panel/detail-panel.component.ts","../../../../packages/ui-ng-patterns/dashboard/detail-panel/detail-panel.component.html","../../../../packages/ui-ng-patterns/dashboard/frame-kit-ui-ng-patterns-dashboard.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n input,\n ViewEncapsulation,\n} from '@angular/core';\n\nimport type { HeadlineLevel } from '@frame-kit/ui-ng/core/headline';\nimport { HeadlineComponent } from '@frame-kit/ui-ng/core/headline';\nimport { TextComponent } from '@frame-kit/ui-ng/core/text';\n\n/**\n * Page-level dashboard header: title + optional description on the left, up to\n * ~3 actions on the right.\n *\n * When the header's own width gets tight, the actions drop below the text and\n * go full-width (mobile-style). The break is driven by a CSS container query on\n * the host, so it responds to the content region's width — not the viewport,\n * which means it behaves correctly inside a narrow panel.\n *\n * Project actions by tagging each button with `pageHeaderActions`:\n *\n * <fk-page-header title=\"Users\" description=\"...\">\n * <fk-button pageHeaderActions variant=\"outline\" size=\"sm\">Import</fk-button>\n * <fk-button pageHeaderActions variant=\"primary\" size=\"sm\">Create user</fk-button>\n * </fk-page-header>\n *\n * Tagging each button (rather than wrapping them in a div) makes every button a\n * direct flex child, so full-width stacking falls out of `align-items: stretch`\n * with no `::ng-deep` reach-in.\n *\n * The title row has two projected slots — `[pageHeaderLeading]` before the\n * title and `[pageHeaderTrailing]` after it. Project a plain `fk-icon` for a\n * decorative mark, or a `button` for a clickable one (the consumer owns the\n * interaction, focus ring, and aria-label):\n *\n * <fk-page-header title=\"Permissions\">\n * <fk-icon pageHeaderLeading name=\"key-outline\" />\n * <button pageHeaderTrailing type=\"button\" aria-label=\"Docs\" (click)=\"openDocs()\">\n * <fk-icon name=\"view-api\" />\n * </button>\n * </fk-page-header>\n */\n@Component({\n selector: 'fk-page-header',\n standalone: true,\n imports: [HeadlineComponent, TextComponent],\n // Emulated (not None) so `:host` works — `:host { display: block }` is what\n // keeps the custom element from defaulting to `display: inline` and\n // collapsing to min-content under the container query below.\n encapsulation: ViewEncapsulation.Emulated,\n changeDetection: ChangeDetectionStrategy.OnPush,\n templateUrl: './page-header.component.html',\n styleUrl: './page-header.component.scss',\n})\nexport class PageHeaderComponent {\n readonly title = input.required<string>();\n readonly description = input<string | null>(null);\n readonly headingLevel = input<HeadlineLevel>(1);\n}\n","<header class=\"fk-page-header\">\n <div class=\"fk-page-header__text\">\n <div class=\"fk-page-header__title-row\">\n <ng-content select=\"[pageHeaderLeading]\" />\n\n <fk-headline [level]=\"headingLevel()\">{{ title() }}</fk-headline>\n\n <ng-content select=\"[pageHeaderTrailing]\" />\n </div>\n\n @if (description()) {\n <fk-text class=\"fk-page-header__description\" tone=\"muted\">{{\n description()\n }}</fk-text>\n }\n </div>\n\n <div class=\"fk-page-header__actions\">\n <ng-content select=\"[pageHeaderActions]\" />\n </div>\n</header>\n","import {\n ChangeDetectionStrategy,\n Component,\n computed,\n HostBinding,\n input,\n ViewEncapsulation,\n} from '@angular/core';\n\nimport { IconComponent } from '@frame-kit/ui-ng/core/icon';\n\n/**\n * Empty-state panel for dashboard tables and lists: a centered icon, a title,\n * an optional description, and a projected actions slot (typically one primary\n * fk-button). Bordered-card styling, meant to stand in for a table body when\n * there are no rows.\n *\n * The action row flips from a horizontal row to a centered vertical stack on\n * narrow widths, driven by a container query on the host — so it reacts to the\n * empty-state's own width (e.g. inside a split column) rather than the viewport.\n *\n * <fk-table-empty-state\n * icon=\"globe\"\n * title=\"No webhooks configured\"\n * description=\"Add a webhook to receive events.\"\n * >\n * <fk-button variant=\"primary\" size=\"sm\">Add webhook</fk-button>\n * </fk-table-empty-state>\n *\n * The actions slot is an open `ng-content`, so project as many actions as the\n * design needs (one primary is the norm; two reads cleanly).\n */\n@Component({\n selector: 'fk-table-empty-state',\n standalone: true,\n imports: [IconComponent],\n // Emulated (default) so `:host` works — the host is the bordered flex card\n // and establishes the container-query context the actions break against.\n encapsulation: ViewEncapsulation.Emulated,\n changeDetection: ChangeDetectionStrategy.OnPush,\n templateUrl: './table-empty-state.component.html',\n styleUrl: './table-empty-state.component.scss',\n})\nexport class TableEmptyStateComponent {\n // ===== INPUTS =====\n readonly icon = input<string | null>(null);\n readonly title = input.required<string>();\n readonly description = input<string | null>(null);\n\n // ===== BASE PROPS =====\n readonly className = input<string>('');\n readonly id = input<string | null>(null);\n readonly ariaLabel = input<string | null>(null);\n\n // ===== COMPUTED =====\n readonly classes = computed(() =>\n ['fk-table-empty-state', this.className()].filter(Boolean).join(' '),\n );\n\n @HostBinding('class')\n get hostClass(): string {\n return this.classes();\n }\n\n @HostBinding('attr.id')\n get hostId(): string | null {\n return this.id();\n }\n\n @HostBinding('attr.aria-label')\n get hostAriaLabel(): string | null {\n return this.ariaLabel();\n }\n}\n","@if (icon()) {\n <div class=\"fk-table-empty-state__icon\">\n <fk-icon [name]=\"icon()!\" size=\"lg\" />\n </div>\n}\n\n<h3 class=\"fk-table-empty-state__title\">{{ title() }}</h3>\n\n@if (description()) {\n <p class=\"fk-table-empty-state__description\">{{ description() }}</p>\n}\n\n<div class=\"fk-table-empty-state__actions\">\n <ng-content />\n</div>\n","import {\n ChangeDetectionStrategy,\n Component,\n HostBinding,\n input,\n} from '@angular/core';\n\n/**\n * Two-column split layout with container-query responsive stacking.\n *\n * Projects content into `[start]` (grows to fill) and `[end]` (content-sized or\n * fixed width). Below 700px container width, columns stack vertically.\n *\n * A standalone layout primitive for custom dashboard rows.\n */\n@Component({\n selector: 'fk-content-split-layout',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n templateUrl: './content-split-layout.component.html',\n styleUrl: './content-split-layout.component.scss',\n})\nexport class ContentSplitLayoutComponent {\n readonly endWidth = input<string | null>(null);\n readonly gap = input<string>('var(--fk-rhythm-4, 1rem)');\n\n @HostBinding('class')\n readonly hostClass = 'fk-content-split-layout';\n}\n","<div\n class=\"fk-content-split-layout__row\"\n [style.--fk-content-split-gap]=\"gap()\"\n>\n <div class=\"fk-content-split-layout__start\">\n <ng-content select=\"[start]\" />\n </div>\n\n <div\n class=\"fk-content-split-layout__end\"\n [class.fk-content-split-layout__end--fixed]=\"endWidth()\"\n [style.--fk-content-split-end-width]=\"endWidth()\"\n >\n <ng-content select=\"[end]\" />\n </div>\n</div>\n","import {\n ChangeDetectionStrategy,\n Component,\n computed,\n HostBinding,\n input,\n} from '@angular/core';\n\n/**\n * Responsive column layout for dashboard content. Lays projected children out\n * in equal-width columns that wrap — and ultimately collapse to a single\n * stacked column — based on the layout's *own* available width, never the\n * viewport. So it reacts correctly when the dashboard content region is\n * squeezed (sidenav open, a drawer pushed in, a split pane) while the viewport\n * still reports \"desktop\".\n *\n * Implemented with an intrinsic CSS grid — `repeat(auto-fit, minmax(min, 1fr))`\n * — so there is no container query, no JS, and no viewport breakpoint to keep\n * in sync. `minColumnWidth` is the smallest a column may shrink to before the\n * grid drops a column; empty tracks collapse, so N children never produce more\n * than N columns. Spacing on both axes is a single `gutter` gap, so wrapped\n * rows and side-by-side columns separate consistently with no edge artifacts.\n *\n * <fk-columns minColumnWidth=\"18rem\">\n * <fk-field-group>…</fk-field-group>\n * <fk-field-group>…</fk-field-group>\n * </fk-columns>\n */\n@Component({\n selector: 'fk-columns',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n templateUrl: './columns.component.html',\n styleUrl: './columns.component.scss',\n})\nexport class ColumnsComponent {\n // ===== INPUTS =====\n /**\n * The smallest width a column may shrink to before the grid drops to fewer\n * columns. This is the single knob that controls when columns wrap/stack.\n */\n readonly minColumnWidth = input<string>('16rem');\n /** Gap between side-by-side columns and between wrapped rows. */\n readonly gutter = input<string>('var(--fk-rhythm-4, 1rem)');\n\n // ===== BASE PROPS =====\n readonly className = input<string>('');\n readonly id = input<string | null>(null);\n readonly ariaLabel = input<string | null>(null);\n\n // ===== COMPUTED =====\n readonly classes = computed(() =>\n ['fk-columns', this.className()].filter(Boolean).join(' '),\n );\n\n @HostBinding('class')\n get hostClass(): string {\n return this.classes();\n }\n\n @HostBinding('attr.id')\n get hostId(): string | null {\n return this.id();\n }\n\n @HostBinding('attr.aria-label')\n get hostAriaLabel(): string | null {\n return this.ariaLabel();\n }\n\n @HostBinding('style.--fk-columns-min')\n get hostMin(): string {\n return this.minColumnWidth();\n }\n\n @HostBinding('style.--fk-columns-gutter')\n get hostGutter(): string {\n return this.gutter();\n }\n}\n","<ng-content />\n","import {\n ChangeDetectionStrategy,\n Component,\n computed,\n HostBinding,\n input,\n} from '@angular/core';\n\n/**\n * Padded container for an expanded dashboard detail region — typically the body\n * of a data-table row expansion, holding an `fk-columns` of form controls.\n *\n * The `disabled` input greys the panel and makes its entire subtree `inert`\n * (non-interactive, unfocusable, hidden from assistive technology) — for\n * controls gated behind a feature toggle that must not be editable until the\n * feature is enabled.\n *\n * <fk-detail-panel [disabled]=\"!feature.enabled\">\n * <fk-columns>…</fk-columns>\n * </fk-detail-panel>\n */\n@Component({\n selector: 'fk-detail-panel',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n templateUrl: './detail-panel.component.html',\n styleUrl: './detail-panel.component.scss',\n})\nexport class DetailPanelComponent {\n // ===== INPUTS =====\n /**\n * Greys the panel and makes its content inert (locked) — for feature-gated\n * controls that should not be editable until the feature is enabled.\n */\n readonly disabled = input<boolean>(false);\n\n // ===== BASE PROPS =====\n readonly className = input<string>('');\n readonly id = input<string | null>(null);\n readonly ariaLabel = input<string | null>(null);\n\n // ===== COMPUTED =====\n readonly classes = computed(() =>\n [\n 'fk-detail-panel',\n this.disabled() ? 'fk-detail-panel--disabled' : '',\n this.className(),\n ]\n .filter(Boolean)\n .join(' '),\n );\n\n @HostBinding('class')\n get hostClass(): string {\n return this.classes();\n }\n\n @HostBinding('attr.id')\n get hostId(): string | null {\n return this.id();\n }\n\n @HostBinding('attr.aria-label')\n get hostAriaLabel(): string | null {\n return this.ariaLabel();\n }\n\n /**\n * Lock the whole subtree when disabled. `inert` blocks pointer interaction,\n * focus, and assistive-tech access in one attribute — stronger and more\n * correct than `pointer-events: none`, which only blocks the mouse.\n */\n @HostBinding('attr.inert')\n get hostInert(): '' | null {\n return this.disabled() ? '' : null;\n }\n}\n","<ng-content />\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, computed, HostBinding, ChangeDetectionStrategy, Component, inject } from '@angular/core';
3
+ import { RouterLink, RouterLinkActive } from '@angular/router';
4
+ import { AppShellComponent } from '@frame-kit/ui-ng/layouts/app-shell';
5
+
6
+ /**
7
+ * Small rectangular badge showing an HTTP method label (e.g. `GET`, `POST`).
8
+ *
9
+ * Each method has its own hue so consumers can identify the verb in a dense
10
+ * list. Background colors are exposed as CSS variables
11
+ * (`--fk-method-badge-bg-get`, etc.) with defaults baked in so the component
12
+ * renders correctly without a theme.
13
+ */
14
+ class MethodBadgeComponent {
15
+ // ===== INPUTS =====
16
+ method = input.required(...(ngDevMode ? [{ debugName: "method" }] : /* istanbul ignore next */ []));
17
+ // ===== BASE PROPS =====
18
+ className = input('', ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
19
+ id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
20
+ ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
21
+ // ===== COMPUTED =====
22
+ classes = computed(() => ['fk-method-badge', `fk-method-badge--${this.method()}`, this.className()]
23
+ .filter(Boolean)
24
+ .join(' '), ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
25
+ label = computed(() => this.method().toUpperCase(), ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
26
+ get hostClass() {
27
+ return this.classes();
28
+ }
29
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: MethodBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
30
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: MethodBadgeComponent, isStandalone: true, selector: "fk-method-badge", inputs: { method: { classPropertyName: "method", publicName: "method", isSignal: true, isRequired: true, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClass" } }, ngImport: i0, template: "<span\n class=\"fk-method-badge__label\"\n [id]=\"id() ?? undefined\"\n [attr.aria-label]=\"ariaLabel()\"\n >{{ label() }}</span\n>\n", styles: [":host{display:inline-flex;align-items:center;justify-content:center;min-width:var(--fk-method-badge-min-width, 3rem);padding:var(--fk-method-badge-padding-block, 2px) var(--fk-method-badge-padding-inline, var(--fk-rhythm-2, .5rem));border-radius:var(--fk-method-badge-radius, var(--fk-radius-sm, .25rem));background-color:var(--fk-method-badge-bg, var(--fk-method-badge-bg-neutral, #d6d6d6));color:var(--fk-method-badge-color, var(--fk-color-text, #1f2d3d));font-family:var(--fk-method-badge-font-family, var(--fk-font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace));font-size:var(--fk-method-badge-font-size, .6875rem);font-weight:var(--fk-method-badge-font-weight, var(--fk-font-weight-bold, 700));letter-spacing:.05em;line-height:1.4;text-transform:uppercase}:host.fk-method-badge--get{background-color:var(--fk-method-badge-bg-get, #89ecac);color:var(--fk-method-badge-color-get, var(--fk-color-text, #1f2d3d))}:host.fk-method-badge--post{background-color:var(--fk-method-badge-bg-post, #94c4fd);color:var(--fk-method-badge-color-post, var(--fk-color-text, #1f2d3d))}:host.fk-method-badge--put{background-color:var(--fk-method-badge-bg-put, #fbb972);color:var(--fk-method-badge-color-put, var(--fk-color-text, #1f2d3d))}:host.fk-method-badge--patch{background-color:var(--fk-method-badge-bg-patch, #c8a7fb);color:var(--fk-method-badge-color-patch, var(--fk-color-text, #1f2d3d))}:host.fk-method-badge--delete{background-color:var(--fk-method-badge-bg-delete, #fb8d8d);color:var(--fk-method-badge-color-delete, var(--fk-color-text, #1f2d3d))}:host.fk-method-badge--head{background-color:var(--fk-method-badge-bg-head, var(--fk-method-badge-bg-neutral, #d6d6d6));color:var(--fk-method-badge-color-head, var(--fk-color-text, #1f2d3d))}:host.fk-method-badge--options{background-color:var(--fk-method-badge-bg-options, var(--fk-method-badge-bg-neutral, #d6d6d6));color:var(--fk-method-badge-color-options, var(--fk-color-text, #1f2d3d))}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
31
+ }
32
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: MethodBadgeComponent, decorators: [{
33
+ type: Component,
34
+ args: [{ selector: 'fk-method-badge', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<span\n class=\"fk-method-badge__label\"\n [id]=\"id() ?? undefined\"\n [attr.aria-label]=\"ariaLabel()\"\n >{{ label() }}</span\n>\n", styles: [":host{display:inline-flex;align-items:center;justify-content:center;min-width:var(--fk-method-badge-min-width, 3rem);padding:var(--fk-method-badge-padding-block, 2px) var(--fk-method-badge-padding-inline, var(--fk-rhythm-2, .5rem));border-radius:var(--fk-method-badge-radius, var(--fk-radius-sm, .25rem));background-color:var(--fk-method-badge-bg, var(--fk-method-badge-bg-neutral, #d6d6d6));color:var(--fk-method-badge-color, var(--fk-color-text, #1f2d3d));font-family:var(--fk-method-badge-font-family, var(--fk-font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace));font-size:var(--fk-method-badge-font-size, .6875rem);font-weight:var(--fk-method-badge-font-weight, var(--fk-font-weight-bold, 700));letter-spacing:.05em;line-height:1.4;text-transform:uppercase}:host.fk-method-badge--get{background-color:var(--fk-method-badge-bg-get, #89ecac);color:var(--fk-method-badge-color-get, var(--fk-color-text, #1f2d3d))}:host.fk-method-badge--post{background-color:var(--fk-method-badge-bg-post, #94c4fd);color:var(--fk-method-badge-color-post, var(--fk-color-text, #1f2d3d))}:host.fk-method-badge--put{background-color:var(--fk-method-badge-bg-put, #fbb972);color:var(--fk-method-badge-color-put, var(--fk-color-text, #1f2d3d))}:host.fk-method-badge--patch{background-color:var(--fk-method-badge-bg-patch, #c8a7fb);color:var(--fk-method-badge-color-patch, var(--fk-color-text, #1f2d3d))}:host.fk-method-badge--delete{background-color:var(--fk-method-badge-bg-delete, #fb8d8d);color:var(--fk-method-badge-color-delete, var(--fk-color-text, #1f2d3d))}:host.fk-method-badge--head{background-color:var(--fk-method-badge-bg-head, var(--fk-method-badge-bg-neutral, #d6d6d6));color:var(--fk-method-badge-color-head, var(--fk-color-text, #1f2d3d))}:host.fk-method-badge--options{background-color:var(--fk-method-badge-bg-options, var(--fk-method-badge-bg-neutral, #d6d6d6));color:var(--fk-method-badge-color-options, var(--fk-color-text, #1f2d3d))}\n"] }]
35
+ }], propDecorators: { method: [{ type: i0.Input, args: [{ isSignal: true, alias: "method", required: true }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hostClass: [{
36
+ type: HostBinding,
37
+ args: ['class']
38
+ }] } });
39
+
40
+ /**
41
+ * Sidebar/list item for an API endpoint in developer documentation.
42
+ *
43
+ * Renders a leading `fk-method-badge` followed by the endpoint label as a
44
+ * single clickable router link. Used to list operations (e.g. "GET List
45
+ * permissions", "POST Create identity") in a documentation sidebar or any
46
+ * denser endpoint index.
47
+ *
48
+ * Active state is driven by `routerLinkActive`, matching the behavior of
49
+ * `fk-sidenav-link` so every router-aware nav item in the library resolves
50
+ * active state the same way.
51
+ *
52
+ * When rendered inside an `fk-app-shell`, clicks also call `dismissSidenav`
53
+ * on the ancestor shell — this closes the mobile overlay so the user lands
54
+ * directly on the selected endpoint. On desktop it's a no-op.
55
+ */
56
+ class EndpointLinkComponent {
57
+ shell = inject(AppShellComponent, { optional: true });
58
+ // ===== INPUTS =====
59
+ method = input.required(...(ngDevMode ? [{ debugName: "method" }] : /* istanbul ignore next */ []));
60
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+ routerLink = input.required(...(ngDevMode ? [{ debugName: "routerLink" }] : /* istanbul ignore next */ []));
63
+ /**
64
+ * Whether to require an exact URL match for the active state. Defaults to
65
+ * false — each endpoint has a unique final path segment so prefix matching
66
+ * resolves to the same thing in practice.
67
+ */
68
+ exact = input(false, ...(ngDevMode ? [{ debugName: "exact" }] : /* istanbul ignore next */ []));
69
+ // ===== BASE PROPS =====
70
+ className = input('', ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
71
+ // ===== COMPUTED =====
72
+ classes = computed(() => ['fk-endpoint-link', this.className()].filter(Boolean).join(' '), ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
73
+ activeOptions = computed(() => ({ exact: this.exact() }), ...(ngDevMode ? [{ debugName: "activeOptions" }] : /* istanbul ignore next */ []));
74
+ get hostClass() {
75
+ return this.classes();
76
+ }
77
+ dismiss() {
78
+ this.shell?.dismissSidenav();
79
+ }
80
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EndpointLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
81
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: EndpointLinkComponent, isStandalone: true, selector: "fk-endpoint-link", inputs: { method: { classPropertyName: "method", publicName: "method", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: true, transformFunction: null }, exact: { classPropertyName: "exact", publicName: "exact", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClass" } }, ngImport: i0, template: "<a\n class=\"fk-endpoint-link__anchor\"\n [routerLink]=\"routerLink()\"\n routerLinkActive=\"fk-endpoint-link__anchor--active\"\n [routerLinkActiveOptions]=\"activeOptions()\"\n (click)=\"$event.stopPropagation(); dismiss()\"\n>\n <fk-method-badge [method]=\"method()\" class=\"fk-endpoint-link__badge\" />\n <span class=\"fk-endpoint-link__label\">{{ label() }}</span>\n</a>\n", styles: [".fk-endpoint-link{display:block}.fk-endpoint-link__anchor{display:flex;align-items:center;gap:var(--fk-endpoint-link-gap, var(--fk-rhythm-2, .5rem));padding:var(--fk-endpoint-link-padding-block, var(--fk-rhythm-1, .25rem)) var(--fk-endpoint-link-padding-inline, var(--fk-rhythm-3, .75rem));border-radius:var(--fk-endpoint-link-radius, var(--fk-radius-md, .5rem));color:var(--fk-endpoint-link-color, var(--fk-color-text, #1f2d3d));font-size:var(--fk-endpoint-link-font-size, .875rem);line-height:1.4;text-decoration:none;transition:background-color .15s ease}.fk-endpoint-link__anchor:hover{background-color:var(--fk-endpoint-link-bg-hover, var(--fk-color-surface-muted, #f7f9fb))}.fk-endpoint-link__anchor:focus-visible{outline:none;box-shadow:var(--fk-endpoint-link-focus-ring, var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18)))}.fk-endpoint-link__anchor--active,.fk-endpoint-link__anchor--active:hover{background-color:var(--fk-endpoint-link-bg-active, var(--fk-color-primary-light, #dbeafe));color:var(--fk-endpoint-link-color-active, var(--fk-color-primary, #1e3a8a));font-weight:var(--fk-endpoint-link-font-weight-active, var(--fk-font-weight-semibold, 600))}.fk-endpoint-link__badge{flex-shrink:0}.fk-endpoint-link__label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: MethodBadgeComponent, selector: "fk-method-badge", inputs: ["method", "className", "id", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
82
+ }
83
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: EndpointLinkComponent, decorators: [{
84
+ type: Component,
85
+ args: [{ selector: 'fk-endpoint-link', standalone: true, imports: [RouterLink, RouterLinkActive, MethodBadgeComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<a\n class=\"fk-endpoint-link__anchor\"\n [routerLink]=\"routerLink()\"\n routerLinkActive=\"fk-endpoint-link__anchor--active\"\n [routerLinkActiveOptions]=\"activeOptions()\"\n (click)=\"$event.stopPropagation(); dismiss()\"\n>\n <fk-method-badge [method]=\"method()\" class=\"fk-endpoint-link__badge\" />\n <span class=\"fk-endpoint-link__label\">{{ label() }}</span>\n</a>\n", styles: [".fk-endpoint-link{display:block}.fk-endpoint-link__anchor{display:flex;align-items:center;gap:var(--fk-endpoint-link-gap, var(--fk-rhythm-2, .5rem));padding:var(--fk-endpoint-link-padding-block, var(--fk-rhythm-1, .25rem)) var(--fk-endpoint-link-padding-inline, var(--fk-rhythm-3, .75rem));border-radius:var(--fk-endpoint-link-radius, var(--fk-radius-md, .5rem));color:var(--fk-endpoint-link-color, var(--fk-color-text, #1f2d3d));font-size:var(--fk-endpoint-link-font-size, .875rem);line-height:1.4;text-decoration:none;transition:background-color .15s ease}.fk-endpoint-link__anchor:hover{background-color:var(--fk-endpoint-link-bg-hover, var(--fk-color-surface-muted, #f7f9fb))}.fk-endpoint-link__anchor:focus-visible{outline:none;box-shadow:var(--fk-endpoint-link-focus-ring, var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18)))}.fk-endpoint-link__anchor--active,.fk-endpoint-link__anchor--active:hover{background-color:var(--fk-endpoint-link-bg-active, var(--fk-color-primary-light, #dbeafe));color:var(--fk-endpoint-link-color-active, var(--fk-color-primary, #1e3a8a));font-weight:var(--fk-endpoint-link-font-weight-active, var(--fk-font-weight-semibold, 600))}.fk-endpoint-link__badge{flex-shrink:0}.fk-endpoint-link__label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
86
+ }], propDecorators: { method: [{ type: i0.Input, args: [{ isSignal: true, alias: "method", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: true }] }], exact: [{ type: i0.Input, args: [{ isSignal: true, alias: "exact", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], hostClass: [{
87
+ type: HostBinding,
88
+ args: ['class']
89
+ }] } });
90
+
91
+ /**
92
+ * Generated bundle index. Do not edit.
93
+ */
94
+
95
+ export { EndpointLinkComponent, MethodBadgeComponent };
96
+ //# sourceMappingURL=frame-kit-ui-ng-patterns-docs.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-kit-ui-ng-patterns-docs.mjs","sources":["../../../../packages/ui-ng-patterns/docs/method-badge/method-badge.component.ts","../../../../packages/ui-ng-patterns/docs/method-badge/method-badge.component.html","../../../../packages/ui-ng-patterns/docs/endpoint-link/endpoint-link.component.ts","../../../../packages/ui-ng-patterns/docs/endpoint-link/endpoint-link.component.html","../../../../packages/ui-ng-patterns/docs/frame-kit-ui-ng-patterns-docs.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n computed,\n HostBinding,\n input,\n} from '@angular/core';\n\nimport type { MethodBadgeMethod } from './method-badge.types';\n\n/**\n * Small rectangular badge showing an HTTP method label (e.g. `GET`, `POST`).\n *\n * Each method has its own hue so consumers can identify the verb in a dense\n * list. Background colors are exposed as CSS variables\n * (`--fk-method-badge-bg-get`, etc.) with defaults baked in so the component\n * renders correctly without a theme.\n */\n@Component({\n selector: 'fk-method-badge',\n standalone: true,\n imports: [],\n changeDetection: ChangeDetectionStrategy.OnPush,\n templateUrl: './method-badge.component.html',\n styleUrl: './method-badge.component.scss',\n})\nexport class MethodBadgeComponent {\n // ===== INPUTS =====\n readonly method = input.required<MethodBadgeMethod>();\n\n // ===== BASE PROPS =====\n readonly className = input<string>('');\n readonly id = input<string | null>(null);\n readonly ariaLabel = input<string | null>(null);\n\n // ===== COMPUTED =====\n readonly classes = computed(() =>\n ['fk-method-badge', `fk-method-badge--${this.method()}`, this.className()]\n .filter(Boolean)\n .join(' '),\n );\n\n readonly label = computed(() => this.method().toUpperCase());\n\n @HostBinding('class')\n get hostClass(): string {\n return this.classes();\n }\n}\n","<span\n class=\"fk-method-badge__label\"\n [id]=\"id() ?? undefined\"\n [attr.aria-label]=\"ariaLabel()\"\n >{{ label() }}</span\n>\n","import {\n ChangeDetectionStrategy,\n Component,\n computed,\n HostBinding,\n inject,\n input,\n} from '@angular/core';\nimport { RouterLink, RouterLinkActive } from '@angular/router';\n\nimport { AppShellComponent } from '@frame-kit/ui-ng/layouts/app-shell';\nimport { MethodBadgeComponent } from '../method-badge/method-badge.component';\nimport type { MethodBadgeMethod } from '../method-badge/method-badge.types';\n\n/**\n * Sidebar/list item for an API endpoint in developer documentation.\n *\n * Renders a leading `fk-method-badge` followed by the endpoint label as a\n * single clickable router link. Used to list operations (e.g. \"GET List\n * permissions\", \"POST Create identity\") in a documentation sidebar or any\n * denser endpoint index.\n *\n * Active state is driven by `routerLinkActive`, matching the behavior of\n * `fk-sidenav-link` so every router-aware nav item in the library resolves\n * active state the same way.\n *\n * When rendered inside an `fk-app-shell`, clicks also call `dismissSidenav`\n * on the ancestor shell — this closes the mobile overlay so the user lands\n * directly on the selected endpoint. On desktop it's a no-op.\n */\n@Component({\n selector: 'fk-endpoint-link',\n standalone: true,\n imports: [RouterLink, RouterLinkActive, MethodBadgeComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n templateUrl: './endpoint-link.component.html',\n styleUrl: './endpoint-link.component.scss',\n})\nexport class EndpointLinkComponent {\n private readonly shell = inject(AppShellComponent, { optional: true });\n\n // ===== INPUTS =====\n readonly method = input.required<MethodBadgeMethod>();\n readonly label = input.required<string>();\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly routerLink = input.required<string | any[]>();\n\n /**\n * Whether to require an exact URL match for the active state. Defaults to\n * false — each endpoint has a unique final path segment so prefix matching\n * resolves to the same thing in practice.\n */\n readonly exact = input<boolean>(false);\n\n // ===== BASE PROPS =====\n readonly className = input<string>('');\n\n // ===== COMPUTED =====\n readonly classes = computed(() =>\n ['fk-endpoint-link', this.className()].filter(Boolean).join(' '),\n );\n\n readonly activeOptions = computed(() => ({ exact: this.exact() }));\n\n @HostBinding('class')\n get hostClass(): string {\n return this.classes();\n }\n\n protected dismiss(): void {\n this.shell?.dismissSidenav();\n }\n}\n","<a\n class=\"fk-endpoint-link__anchor\"\n [routerLink]=\"routerLink()\"\n routerLinkActive=\"fk-endpoint-link__anchor--active\"\n [routerLinkActiveOptions]=\"activeOptions()\"\n (click)=\"$event.stopPropagation(); dismiss()\"\n>\n <fk-method-badge [method]=\"method()\" class=\"fk-endpoint-link__badge\" />\n <span class=\"fk-endpoint-link__label\">{{ label() }}</span>\n</a>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export * from '@frame-kit/ui-ng-patterns/dashboard';
2
+ export * from '@frame-kit/ui-ng-patterns/docs';
3
+
4
+ // @frame-kit/ui-ng-patterns
5
+ //
6
+ // Opinionated, dashboard-ready components composed from @frame-kit/ui-ng primitives.
7
+ // Most components get their own secondary entry point (packages/ui-ng-patterns/<area>/<name>/).
8
+ // The docs/ and dashboard/ layers are single entry points, like forms/ in ui-ng.
9
+
10
+ /**
11
+ * Generated bundle index. Do not edit.
12
+ */
13
+ //# sourceMappingURL=frame-kit-ui-ng-patterns.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame-kit-ui-ng-patterns.mjs","sources":["../../../../packages/ui-ng-patterns/index.ts","../../../../packages/ui-ng-patterns/frame-kit-ui-ng-patterns.ts"],"sourcesContent":["// @frame-kit/ui-ng-patterns\n//\n// Opinionated, dashboard-ready components composed from @frame-kit/ui-ng primitives.\n// Most components get their own secondary entry point (packages/ui-ng-patterns/<area>/<name>/).\n// The docs/ and dashboard/ layers are single entry points, like forms/ in ui-ng.\n\nexport * from '@frame-kit/ui-ng-patterns/dashboard';\nexport * from '@frame-kit/ui-ng-patterns/docs';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAAA;AACA;AACA;AACA;AACA;;ACJA;;AAEG"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@frame-kit/ui-ng-patterns",
3
+ "version": "0.0.2",
4
+ "description": "Opinionated, dashboard-ready Angular component patterns composed from @frame-kit/ui-ng.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/frame-kit/packages.git",
9
+ "directory": "packages/ui-ng-patterns"
10
+ },
11
+ "homepage": "https://github.com/frame-kit/packages/tree/main/packages/ui-ng-patterns#readme",
12
+ "bugs": "https://github.com/frame-kit/packages/issues",
13
+ "peerDependencies": {
14
+ "@angular/cdk": "^21.2.0",
15
+ "@angular/common": "^21.2.0",
16
+ "@angular/core": "^21.2.0",
17
+ "@angular/forms": "^21.2.0",
18
+ "@angular/platform-browser": "^21.2.0",
19
+ "@angular/router": "^21.2.0",
20
+ "@frame-kit/ui-ng": "^0.0.2",
21
+ "rxjs": "^7.0.0"
22
+ },
23
+ "sideEffects": false,
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "module": "fesm2022/frame-kit-ui-ng-patterns.mjs",
28
+ "typings": "types/frame-kit-ui-ng-patterns.d.ts",
29
+ "exports": {
30
+ "./package.json": {
31
+ "default": "./package.json"
32
+ },
33
+ ".": {
34
+ "types": "./types/frame-kit-ui-ng-patterns.d.ts",
35
+ "default": "./fesm2022/frame-kit-ui-ng-patterns.mjs"
36
+ },
37
+ "./dashboard": {
38
+ "types": "./types/frame-kit-ui-ng-patterns-dashboard.d.ts",
39
+ "default": "./fesm2022/frame-kit-ui-ng-patterns-dashboard.mjs"
40
+ },
41
+ "./docs": {
42
+ "types": "./types/frame-kit-ui-ng-patterns-docs.d.ts",
43
+ "default": "./fesm2022/frame-kit-ui-ng-patterns-docs.mjs"
44
+ }
45
+ },
46
+ "type": "module",
47
+ "dependencies": {
48
+ "tslib": "^2.3.0"
49
+ }
50
+ }
@@ -0,0 +1,173 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { HeadlineLevel } from '@frame-kit/ui-ng/core/headline';
3
+
4
+ /**
5
+ * Page-level dashboard header: title + optional description on the left, up to
6
+ * ~3 actions on the right.
7
+ *
8
+ * When the header's own width gets tight, the actions drop below the text and
9
+ * go full-width (mobile-style). The break is driven by a CSS container query on
10
+ * the host, so it responds to the content region's width — not the viewport,
11
+ * which means it behaves correctly inside a narrow panel.
12
+ *
13
+ * Project actions by tagging each button with `pageHeaderActions`:
14
+ *
15
+ * <fk-page-header title="Users" description="...">
16
+ * <fk-button pageHeaderActions variant="outline" size="sm">Import</fk-button>
17
+ * <fk-button pageHeaderActions variant="primary" size="sm">Create user</fk-button>
18
+ * </fk-page-header>
19
+ *
20
+ * Tagging each button (rather than wrapping them in a div) makes every button a
21
+ * direct flex child, so full-width stacking falls out of `align-items: stretch`
22
+ * with no `::ng-deep` reach-in.
23
+ *
24
+ * The title row has two projected slots — `[pageHeaderLeading]` before the
25
+ * title and `[pageHeaderTrailing]` after it. Project a plain `fk-icon` for a
26
+ * decorative mark, or a `button` for a clickable one (the consumer owns the
27
+ * interaction, focus ring, and aria-label):
28
+ *
29
+ * <fk-page-header title="Permissions">
30
+ * <fk-icon pageHeaderLeading name="key-outline" />
31
+ * <button pageHeaderTrailing type="button" aria-label="Docs" (click)="openDocs()">
32
+ * <fk-icon name="view-api" />
33
+ * </button>
34
+ * </fk-page-header>
35
+ */
36
+ declare class PageHeaderComponent {
37
+ readonly title: _angular_core.InputSignal<string>;
38
+ readonly description: _angular_core.InputSignal<string | null>;
39
+ readonly headingLevel: _angular_core.InputSignal<HeadlineLevel>;
40
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<PageHeaderComponent, never>;
41
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<PageHeaderComponent, "fk-page-header", never, { "title": { "alias": "title"; "required": true; "isSignal": true; }; "description": { "alias": "description"; "required": false; "isSignal": true; }; "headingLevel": { "alias": "headingLevel"; "required": false; "isSignal": true; }; }, {}, never, ["[pageHeaderLeading]", "[pageHeaderTrailing]", "[pageHeaderActions]"], true, never>;
42
+ }
43
+
44
+ /**
45
+ * Empty-state panel for dashboard tables and lists: a centered icon, a title,
46
+ * an optional description, and a projected actions slot (typically one primary
47
+ * fk-button). Bordered-card styling, meant to stand in for a table body when
48
+ * there are no rows.
49
+ *
50
+ * The action row flips from a horizontal row to a centered vertical stack on
51
+ * narrow widths, driven by a container query on the host — so it reacts to the
52
+ * empty-state's own width (e.g. inside a split column) rather than the viewport.
53
+ *
54
+ * <fk-table-empty-state
55
+ * icon="globe"
56
+ * title="No webhooks configured"
57
+ * description="Add a webhook to receive events."
58
+ * >
59
+ * <fk-button variant="primary" size="sm">Add webhook</fk-button>
60
+ * </fk-table-empty-state>
61
+ *
62
+ * The actions slot is an open `ng-content`, so project as many actions as the
63
+ * design needs (one primary is the norm; two reads cleanly).
64
+ */
65
+ declare class TableEmptyStateComponent {
66
+ readonly icon: _angular_core.InputSignal<string | null>;
67
+ readonly title: _angular_core.InputSignal<string>;
68
+ readonly description: _angular_core.InputSignal<string | null>;
69
+ readonly className: _angular_core.InputSignal<string>;
70
+ readonly id: _angular_core.InputSignal<string | null>;
71
+ readonly ariaLabel: _angular_core.InputSignal<string | null>;
72
+ readonly classes: _angular_core.Signal<string>;
73
+ get hostClass(): string;
74
+ get hostId(): string | null;
75
+ get hostAriaLabel(): string | null;
76
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TableEmptyStateComponent, never>;
77
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TableEmptyStateComponent, "fk-table-empty-state", never, { "icon": { "alias": "icon"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": true; "isSignal": true; }; "description": { "alias": "description"; "required": false; "isSignal": true; }; "className": { "alias": "className"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
78
+ }
79
+
80
+ /**
81
+ * Two-column split layout with container-query responsive stacking.
82
+ *
83
+ * Projects content into `[start]` (grows to fill) and `[end]` (content-sized or
84
+ * fixed width). Below 700px container width, columns stack vertically.
85
+ *
86
+ * A standalone layout primitive for custom dashboard rows.
87
+ */
88
+ declare class ContentSplitLayoutComponent {
89
+ readonly endWidth: _angular_core.InputSignal<string | null>;
90
+ readonly gap: _angular_core.InputSignal<string>;
91
+ readonly hostClass = "fk-content-split-layout";
92
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<ContentSplitLayoutComponent, never>;
93
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<ContentSplitLayoutComponent, "fk-content-split-layout", never, { "endWidth": { "alias": "endWidth"; "required": false; "isSignal": true; }; "gap": { "alias": "gap"; "required": false; "isSignal": true; }; }, {}, never, ["[start]", "[end]"], true, never>;
94
+ }
95
+
96
+ /**
97
+ * Responsive column layout for dashboard content. Lays projected children out
98
+ * in equal-width columns that wrap — and ultimately collapse to a single
99
+ * stacked column — based on the layout's *own* available width, never the
100
+ * viewport. So it reacts correctly when the dashboard content region is
101
+ * squeezed (sidenav open, a drawer pushed in, a split pane) while the viewport
102
+ * still reports "desktop".
103
+ *
104
+ * Implemented with an intrinsic CSS grid — `repeat(auto-fit, minmax(min, 1fr))`
105
+ * — so there is no container query, no JS, and no viewport breakpoint to keep
106
+ * in sync. `minColumnWidth` is the smallest a column may shrink to before the
107
+ * grid drops a column; empty tracks collapse, so N children never produce more
108
+ * than N columns. Spacing on both axes is a single `gutter` gap, so wrapped
109
+ * rows and side-by-side columns separate consistently with no edge artifacts.
110
+ *
111
+ * <fk-columns minColumnWidth="18rem">
112
+ * <fk-field-group>…</fk-field-group>
113
+ * <fk-field-group>…</fk-field-group>
114
+ * </fk-columns>
115
+ */
116
+ declare class ColumnsComponent {
117
+ /**
118
+ * The smallest width a column may shrink to before the grid drops to fewer
119
+ * columns. This is the single knob that controls when columns wrap/stack.
120
+ */
121
+ readonly minColumnWidth: _angular_core.InputSignal<string>;
122
+ /** Gap between side-by-side columns and between wrapped rows. */
123
+ readonly gutter: _angular_core.InputSignal<string>;
124
+ readonly className: _angular_core.InputSignal<string>;
125
+ readonly id: _angular_core.InputSignal<string | null>;
126
+ readonly ariaLabel: _angular_core.InputSignal<string | null>;
127
+ readonly classes: _angular_core.Signal<string>;
128
+ get hostClass(): string;
129
+ get hostId(): string | null;
130
+ get hostAriaLabel(): string | null;
131
+ get hostMin(): string;
132
+ get hostGutter(): string;
133
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<ColumnsComponent, never>;
134
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<ColumnsComponent, "fk-columns", never, { "minColumnWidth": { "alias": "minColumnWidth"; "required": false; "isSignal": true; }; "gutter": { "alias": "gutter"; "required": false; "isSignal": true; }; "className": { "alias": "className"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
135
+ }
136
+
137
+ /**
138
+ * Padded container for an expanded dashboard detail region — typically the body
139
+ * of a data-table row expansion, holding an `fk-columns` of form controls.
140
+ *
141
+ * The `disabled` input greys the panel and makes its entire subtree `inert`
142
+ * (non-interactive, unfocusable, hidden from assistive technology) — for
143
+ * controls gated behind a feature toggle that must not be editable until the
144
+ * feature is enabled.
145
+ *
146
+ * <fk-detail-panel [disabled]="!feature.enabled">
147
+ * <fk-columns>…</fk-columns>
148
+ * </fk-detail-panel>
149
+ */
150
+ declare class DetailPanelComponent {
151
+ /**
152
+ * Greys the panel and makes its content inert (locked) — for feature-gated
153
+ * controls that should not be editable until the feature is enabled.
154
+ */
155
+ readonly disabled: _angular_core.InputSignal<boolean>;
156
+ readonly className: _angular_core.InputSignal<string>;
157
+ readonly id: _angular_core.InputSignal<string | null>;
158
+ readonly ariaLabel: _angular_core.InputSignal<string | null>;
159
+ readonly classes: _angular_core.Signal<string>;
160
+ get hostClass(): string;
161
+ get hostId(): string | null;
162
+ get hostAriaLabel(): string | null;
163
+ /**
164
+ * Lock the whole subtree when disabled. `inert` blocks pointer interaction,
165
+ * focus, and assistive-tech access in one attribute — stronger and more
166
+ * correct than `pointer-events: none`, which only blocks the mouse.
167
+ */
168
+ get hostInert(): '' | null;
169
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<DetailPanelComponent, never>;
170
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<DetailPanelComponent, "fk-detail-panel", never, { "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "className": { "alias": "className"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
171
+ }
172
+
173
+ export { ColumnsComponent, ContentSplitLayoutComponent, DetailPanelComponent, PageHeaderComponent, TableEmptyStateComponent };
@@ -0,0 +1,68 @@
1
+ import * as _angular_core from '@angular/core';
2
+
3
+ /**
4
+ * Supported HTTP methods for `fk-method-badge`. Lowercased to match OpenAPI
5
+ * conventions so consumers can pass spec values directly without translation.
6
+ */
7
+ type MethodBadgeMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options';
8
+
9
+ /**
10
+ * Sidebar/list item for an API endpoint in developer documentation.
11
+ *
12
+ * Renders a leading `fk-method-badge` followed by the endpoint label as a
13
+ * single clickable router link. Used to list operations (e.g. "GET List
14
+ * permissions", "POST Create identity") in a documentation sidebar or any
15
+ * denser endpoint index.
16
+ *
17
+ * Active state is driven by `routerLinkActive`, matching the behavior of
18
+ * `fk-sidenav-link` so every router-aware nav item in the library resolves
19
+ * active state the same way.
20
+ *
21
+ * When rendered inside an `fk-app-shell`, clicks also call `dismissSidenav`
22
+ * on the ancestor shell — this closes the mobile overlay so the user lands
23
+ * directly on the selected endpoint. On desktop it's a no-op.
24
+ */
25
+ declare class EndpointLinkComponent {
26
+ private readonly shell;
27
+ readonly method: _angular_core.InputSignal<MethodBadgeMethod>;
28
+ readonly label: _angular_core.InputSignal<string>;
29
+ readonly routerLink: _angular_core.InputSignal<string | any[]>;
30
+ /**
31
+ * Whether to require an exact URL match for the active state. Defaults to
32
+ * false — each endpoint has a unique final path segment so prefix matching
33
+ * resolves to the same thing in practice.
34
+ */
35
+ readonly exact: _angular_core.InputSignal<boolean>;
36
+ readonly className: _angular_core.InputSignal<string>;
37
+ readonly classes: _angular_core.Signal<string>;
38
+ readonly activeOptions: _angular_core.Signal<{
39
+ exact: boolean;
40
+ }>;
41
+ get hostClass(): string;
42
+ protected dismiss(): void;
43
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<EndpointLinkComponent, never>;
44
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<EndpointLinkComponent, "fk-endpoint-link", never, { "method": { "alias": "method"; "required": true; "isSignal": true; }; "label": { "alias": "label"; "required": true; "isSignal": true; }; "routerLink": { "alias": "routerLink"; "required": true; "isSignal": true; }; "exact": { "alias": "exact"; "required": false; "isSignal": true; }; "className": { "alias": "className"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
45
+ }
46
+
47
+ /**
48
+ * Small rectangular badge showing an HTTP method label (e.g. `GET`, `POST`).
49
+ *
50
+ * Each method has its own hue so consumers can identify the verb in a dense
51
+ * list. Background colors are exposed as CSS variables
52
+ * (`--fk-method-badge-bg-get`, etc.) with defaults baked in so the component
53
+ * renders correctly without a theme.
54
+ */
55
+ declare class MethodBadgeComponent {
56
+ readonly method: _angular_core.InputSignal<MethodBadgeMethod>;
57
+ readonly className: _angular_core.InputSignal<string>;
58
+ readonly id: _angular_core.InputSignal<string | null>;
59
+ readonly ariaLabel: _angular_core.InputSignal<string | null>;
60
+ readonly classes: _angular_core.Signal<string>;
61
+ readonly label: _angular_core.Signal<string>;
62
+ get hostClass(): string;
63
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<MethodBadgeComponent, never>;
64
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<MethodBadgeComponent, "fk-method-badge", never, { "method": { "alias": "method"; "required": true; "isSignal": true; }; "className": { "alias": "className"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
65
+ }
66
+
67
+ export { EndpointLinkComponent, MethodBadgeComponent };
68
+ export type { MethodBadgeMethod as EndpointLinkMethod, MethodBadgeMethod };
@@ -0,0 +1,2 @@
1
+ export * from '@frame-kit/ui-ng-patterns/dashboard';
2
+ export * from '@frame-kit/ui-ng-patterns/docs';