@frame-kit/ui-ng 0.1.5 → 0.1.7

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.
@@ -36,6 +36,7 @@ FrameKit UI-ng is a style-agnostic, token-driven Angular component library. Comp
36
36
  2. **Components only consume tokens.** Token definitions belong in theme files, never in component SCSS.
37
37
  3. **Components are self-sufficient.** Every component renders correctly with zero external dependencies — no theme file required.
38
38
  4. **Stable public API.** Consumers use `<fk-*>` components with stable inputs. Internal implementations can change without breaking consumers.
39
+ 5. **Icons are not a dependency.** No component or directive **outside the icon family** (`core/icon`, `ui/icon-badge`, `ui/icon-list`) may import `IconComponent` or render `<fk-icon>`. This keeps ui-ng usable without the icon registry. Take consumer-supplied icons three ways (see [Icons](#icons)); a CI guardrail (`icon-dependency.guardrail.spec.ts`) enforces this.
39
40
 
40
41
  ### Library structure
41
42
 
@@ -637,6 +638,20 @@ readonly iconStartRef = contentChild(FK_ALERT_ICON_START);
637
638
 
638
639
  ---
639
640
 
641
+ ## Icons
642
+
643
+ **ui-ng is icon-agnostic.** Only the icon family — `core/icon`, `ui/icon-badge`, `ui/icon-list` — may import `IconComponent` or render `<fk-icon>`. Every other component takes icons from the consumer so the library works without the icon registry and without pinning a specific icon set. A CI guardrail (`packages/ui-ng/icon-dependency.guardrail.spec.ts`) fails the build on any violation.
644
+
645
+ Pick the mechanism by how the icon is supplied:
646
+
647
+ 1. **Structural glyph** (an arrow, caret, grip, spinner that is part of the component's own chrome) → **inline SVG** with `currentColor` and tokenized dimensions. No input, no dependency. Example: `fk-pagination` prev/next arrows.
648
+ 2. **A single consumer-supplied icon** → a **projection slot** via a marker directive (see [Content-slot directives](#content-slot-directives)). The consumer projects their own `<fk-icon>` (or anything). Provide an inline-SVG default inside the `<ng-content>` when there's a sensible fallback. Examples: `fk-menu-item` (`[fkMenuItemIcon]`), `fk-file-dropzone` (`[fkFileDropzoneIcon]`).
649
+ 3. **A data-driven icon** (a list/tree/timeline where each item carries an icon name) → an **icon `TemplateRef` input** rendered per item with the item as `$implicit`. The consumer's template renders the icon; the library renders the template. Keep the data field (e.g. `node.icon`) but don't render it directly. Examples: `fk-node-tree` / `fk-timeline` (`iconTemplate`), `fk-form-renderer` (`prefixIconTemplate`).
650
+
651
+ Never add an `icon` **string input** that the component feeds to `<fk-icon>` — that couples ui-ng to the icon component and a specific icon set. Use mechanism 2 or 3 instead.
652
+
653
+ ---
654
+
640
655
  ## Services
641
656
 
642
657
  Runtime services with no UI that **depend on Angular** (e.g. `OverlayOrchestrator`, `ToastService`, `SpotlightService`). Each lives in its own folder under `packages/ui-ng/services/<name>/` and ships a README alongside the source. Framework-agnostic helpers (color, date math) do **not** belong here — they go in `@frame-kit/utils`.
@@ -2,11 +2,10 @@ import * as i0 from '@angular/core';
2
2
  import { input, computed, HostBinding, ChangeDetectionStrategy, Component, InjectionToken, signal, output, Directive, contentChild, inject, ChangeDetectorRef, effect, Injectable, TemplateRef, viewChild, forwardRef, DestroyRef, ViewContainerRef, ElementRef } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { Validators, FormControl, FormGroup, FormArray, NgControl, ReactiveFormsModule } from '@angular/forms';
5
+ import { NgTemplateOutlet } from '@angular/common';
5
6
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
7
  import { map, distinctUntilChanged } from 'rxjs';
7
- import { IconComponent } from '@frame-kit/ui-ng/core/icon';
8
8
  import { LinkComponent } from '@frame-kit/ui-ng/core/link';
9
- import { NgTemplateOutlet } from '@angular/common';
10
9
  import { TextComponent } from '@frame-kit/ui-ng/core/text';
11
10
 
12
11
  let nextErrorId = 0;
@@ -2887,6 +2886,10 @@ class FieldOutletComponent {
2887
2886
  form = input.required(...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
2888
2887
  /** Whether the parent form has been submitted; propagated to fk-form-field. */
2889
2888
  submitted = input(false, ...(ngDevMode ? [{ debugName: "submitted" }] : /* istanbul ignore next */ []));
2889
+ /** Optional template rendering a field's prefix icon (the app owns the icon
2890
+ * system). Receives the field as `$implicit`; render it from
2891
+ * `field.ui.prefixIcon`. */
2892
+ prefixIconTemplate = input(undefined, ...(ngDevMode ? [{ debugName: "prefixIconTemplate" }] : /* istanbul ignore next */ []));
2890
2893
  /** Whether this field is visible (for conditional visibility). */
2891
2894
  visible = signal(true, ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
2892
2895
  classes = computed(() => ['fk-field-outlet', this.className()].filter(Boolean).join(' '), ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
@@ -3022,19 +3025,19 @@ class FieldOutletComponent {
3022
3025
  }
3023
3026
  }
3024
3027
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FieldOutletComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3025
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: FieldOutletComponent, isStandalone: true, selector: "fk-field-outlet", inputs: { className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, submitted: { classPropertyName: "submitted", publicName: "submitted", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClass", "style.display": "this.hostDisplay" } }, viewQueries: [{ propertyName: "controlHostRef", first: true, predicate: ["controlHost"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "@if (field().ui?.labelEndLink) {\n <div class=\"fk-field-outlet__label-row\">\n <span class=\"fk-field-outlet__label-text\">\n {{ field().label }}\n @if (field().required) {\n <span class=\"fk-field-outlet__required\" aria-hidden=\"true\">*</span>\n }\n </span>\n <fk-link\n variant=\"primary\"\n size=\"small\"\n [routerLink]=\"field().ui!.labelEndLink!.route\"\n >\n {{ field().ui!.labelEndLink!.text }}\n </fk-link>\n </div>\n}\n\n<fk-form-field\n [label]=\"\n field().ui?.labelEndLink\n ? null\n : field().type === 'checkbox'\n ? null\n : (field().label ?? null)\n \"\n [showErrorsOn]=\"'touched'\"\n [submitted]=\"submitted()\"\n [errorMessages]=\"field().errorMessages ?? null\"\n [fallbackControl]=\"form().get(field().key)\"\n [style.display]=\"visible() ? '' : 'none'\"\n>\n @if (field().ui?.prefixIcon) {\n <fk-icon\n fkPrefix\n [name]=\"field().ui!.prefixIcon!\"\n size=\"sm\"\n color=\"muted\"\n />\n }\n <ng-container #controlHost />\n @if (field().ui?.hint) {\n <fk-hint>{{ field().ui?.hint }}</fk-hint>\n }\n</fk-form-field>\n", styles: [".fk-field-outlet__label-row{display:flex;align-items:baseline;justify-content:space-between;font-family:var(--fk-font-family-base);font-size:var(--fk-form-label-font-size);font-weight:var(--fk-form-label-font-weight);line-height:var(--fk-form-label-line-height);color:var(--fk-form-label-color)}.fk-field-outlet__label-text{display:flex;align-items:baseline;gap:.25em}.fk-field-outlet__required{color:var(--fk-form-error-color)}.fk-field-outlet__label-row fk-link{--fk-link-font-size-small: .75rem;--fk-link-font-size-medium: .75rem}.fk-field-outlet__label-row+fk-form-field{margin-top:0}\n"], dependencies: [{ kind: "component", type: FormFieldComponent, selector: "fk-form-field", inputs: ["className", "label", "showErrorsOn", "submitted", "externalDescribedBy", "fallbackControl", "errorMessages"] }, { kind: "directive", type: FkPrefixDirective, selector: "[fkPrefix]" }, { kind: "component", type: HintComponent, selector: "fk-hint", inputs: ["className", "id"] }, { kind: "component", type: IconComponent, selector: "fk-icon", inputs: ["name", "size", "color", "className", "id", "ariaLabel", "ariaHidden"] }, { kind: "component", type: LinkComponent, selector: "fk-link", inputs: ["href", "routerLink", "queryParams", "variant", "size", "underline", "target", "rel", "external", "disabled", "loading", "download", "className", "id", "ariaLabel", "ariaDescribedBy", "ariaCurrent", "role", "tabIndex", "trackingId", "trackingCategory", "trackingLabel", "dataAttributes"], outputs: ["clicked"] }, { kind: "ngmodule", type: ReactiveFormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3028
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: FieldOutletComponent, isStandalone: true, selector: "fk-field-outlet", inputs: { className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, submitted: { classPropertyName: "submitted", publicName: "submitted", isSignal: true, isRequired: false, transformFunction: null }, prefixIconTemplate: { classPropertyName: "prefixIconTemplate", publicName: "prefixIconTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClass", "style.display": "this.hostDisplay" } }, viewQueries: [{ propertyName: "controlHostRef", first: true, predicate: ["controlHost"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "@if (field().ui?.labelEndLink) {\n <div class=\"fk-field-outlet__label-row\">\n <span class=\"fk-field-outlet__label-text\">\n {{ field().label }}\n @if (field().required) {\n <span class=\"fk-field-outlet__required\" aria-hidden=\"true\">*</span>\n }\n </span>\n <fk-link\n variant=\"primary\"\n size=\"small\"\n [routerLink]=\"field().ui!.labelEndLink!.route\"\n >\n {{ field().ui!.labelEndLink!.text }}\n </fk-link>\n </div>\n}\n\n<fk-form-field\n [label]=\"\n field().ui?.labelEndLink\n ? null\n : field().type === 'checkbox'\n ? null\n : (field().label ?? null)\n \"\n [showErrorsOn]=\"'touched'\"\n [submitted]=\"submitted()\"\n [errorMessages]=\"field().errorMessages ?? null\"\n [fallbackControl]=\"form().get(field().key)\"\n [style.display]=\"visible() ? '' : 'none'\"\n>\n @if (field().ui?.prefixIcon && prefixIconTemplate(); as tpl) {\n <span fkPrefix>\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{ $implicit: field() }\"\n />\n </span>\n }\n <ng-container #controlHost />\n @if (field().ui?.hint) {\n <fk-hint>{{ field().ui?.hint }}</fk-hint>\n }\n</fk-form-field>\n", styles: [".fk-field-outlet__label-row{display:flex;align-items:baseline;justify-content:space-between;font-family:var(--fk-font-family-base);font-size:var(--fk-form-label-font-size);font-weight:var(--fk-form-label-font-weight);line-height:var(--fk-form-label-line-height);color:var(--fk-form-label-color)}.fk-field-outlet__label-text{display:flex;align-items:baseline;gap:.25em}.fk-field-outlet__required{color:var(--fk-form-error-color)}.fk-field-outlet__label-row fk-link{--fk-link-font-size-small: .75rem;--fk-link-font-size-medium: .75rem}.fk-field-outlet__label-row+fk-form-field{margin-top:0}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: FormFieldComponent, selector: "fk-form-field", inputs: ["className", "label", "showErrorsOn", "submitted", "externalDescribedBy", "fallbackControl", "errorMessages"] }, { kind: "directive", type: FkPrefixDirective, selector: "[fkPrefix]" }, { kind: "component", type: HintComponent, selector: "fk-hint", inputs: ["className", "id"] }, { kind: "component", type: LinkComponent, selector: "fk-link", inputs: ["href", "routerLink", "queryParams", "variant", "size", "underline", "target", "rel", "external", "disabled", "loading", "download", "className", "id", "ariaLabel", "ariaDescribedBy", "ariaCurrent", "role", "tabIndex", "trackingId", "trackingCategory", "trackingLabel", "dataAttributes"], outputs: ["clicked"] }, { kind: "ngmodule", type: ReactiveFormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3026
3029
  }
3027
3030
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FieldOutletComponent, decorators: [{
3028
3031
  type: Component,
3029
3032
  args: [{ selector: 'fk-field-outlet', standalone: true, imports: [
3033
+ NgTemplateOutlet,
3030
3034
  FormFieldComponent,
3031
3035
  FkPrefixDirective,
3032
3036
  HintComponent,
3033
- IconComponent,
3034
3037
  LinkComponent,
3035
3038
  ReactiveFormsModule,
3036
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (field().ui?.labelEndLink) {\n <div class=\"fk-field-outlet__label-row\">\n <span class=\"fk-field-outlet__label-text\">\n {{ field().label }}\n @if (field().required) {\n <span class=\"fk-field-outlet__required\" aria-hidden=\"true\">*</span>\n }\n </span>\n <fk-link\n variant=\"primary\"\n size=\"small\"\n [routerLink]=\"field().ui!.labelEndLink!.route\"\n >\n {{ field().ui!.labelEndLink!.text }}\n </fk-link>\n </div>\n}\n\n<fk-form-field\n [label]=\"\n field().ui?.labelEndLink\n ? null\n : field().type === 'checkbox'\n ? null\n : (field().label ?? null)\n \"\n [showErrorsOn]=\"'touched'\"\n [submitted]=\"submitted()\"\n [errorMessages]=\"field().errorMessages ?? null\"\n [fallbackControl]=\"form().get(field().key)\"\n [style.display]=\"visible() ? '' : 'none'\"\n>\n @if (field().ui?.prefixIcon) {\n <fk-icon\n fkPrefix\n [name]=\"field().ui!.prefixIcon!\"\n size=\"sm\"\n color=\"muted\"\n />\n }\n <ng-container #controlHost />\n @if (field().ui?.hint) {\n <fk-hint>{{ field().ui?.hint }}</fk-hint>\n }\n</fk-form-field>\n", styles: [".fk-field-outlet__label-row{display:flex;align-items:baseline;justify-content:space-between;font-family:var(--fk-font-family-base);font-size:var(--fk-form-label-font-size);font-weight:var(--fk-form-label-font-weight);line-height:var(--fk-form-label-line-height);color:var(--fk-form-label-color)}.fk-field-outlet__label-text{display:flex;align-items:baseline;gap:.25em}.fk-field-outlet__required{color:var(--fk-form-error-color)}.fk-field-outlet__label-row fk-link{--fk-link-font-size-small: .75rem;--fk-link-font-size-medium: .75rem}.fk-field-outlet__label-row+fk-form-field{margin-top:0}\n"] }]
3037
- }], ctorParameters: () => [], propDecorators: { className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: true }] }], submitted: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitted", required: false }] }], hostClass: [{
3039
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (field().ui?.labelEndLink) {\n <div class=\"fk-field-outlet__label-row\">\n <span class=\"fk-field-outlet__label-text\">\n {{ field().label }}\n @if (field().required) {\n <span class=\"fk-field-outlet__required\" aria-hidden=\"true\">*</span>\n }\n </span>\n <fk-link\n variant=\"primary\"\n size=\"small\"\n [routerLink]=\"field().ui!.labelEndLink!.route\"\n >\n {{ field().ui!.labelEndLink!.text }}\n </fk-link>\n </div>\n}\n\n<fk-form-field\n [label]=\"\n field().ui?.labelEndLink\n ? null\n : field().type === 'checkbox'\n ? null\n : (field().label ?? null)\n \"\n [showErrorsOn]=\"'touched'\"\n [submitted]=\"submitted()\"\n [errorMessages]=\"field().errorMessages ?? null\"\n [fallbackControl]=\"form().get(field().key)\"\n [style.display]=\"visible() ? '' : 'none'\"\n>\n @if (field().ui?.prefixIcon && prefixIconTemplate(); as tpl) {\n <span fkPrefix>\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{ $implicit: field() }\"\n />\n </span>\n }\n <ng-container #controlHost />\n @if (field().ui?.hint) {\n <fk-hint>{{ field().ui?.hint }}</fk-hint>\n }\n</fk-form-field>\n", styles: [".fk-field-outlet__label-row{display:flex;align-items:baseline;justify-content:space-between;font-family:var(--fk-font-family-base);font-size:var(--fk-form-label-font-size);font-weight:var(--fk-form-label-font-weight);line-height:var(--fk-form-label-line-height);color:var(--fk-form-label-color)}.fk-field-outlet__label-text{display:flex;align-items:baseline;gap:.25em}.fk-field-outlet__required{color:var(--fk-form-error-color)}.fk-field-outlet__label-row fk-link{--fk-link-font-size-small: .75rem;--fk-link-font-size-medium: .75rem}.fk-field-outlet__label-row+fk-form-field{margin-top:0}\n"] }]
3040
+ }], ctorParameters: () => [], propDecorators: { className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: true }] }], submitted: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitted", required: false }] }], prefixIconTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIconTemplate", required: false }] }], hostClass: [{
3038
3041
  type: HostBinding,
3039
3042
  args: ['class']
3040
3043
  }], hostDisplay: [{
@@ -3065,6 +3068,8 @@ class FieldArrayOutletComponent {
3065
3068
  field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
3066
3069
  /** The FormArray backing the repeatable rows. */
3067
3070
  formArray = input.required(...(ngDevMode ? [{ debugName: "formArray" }] : /* istanbul ignore next */ []));
3071
+ /** Prefix-icon template threaded to each child field (the app owns the icon). */
3072
+ prefixIconTemplate = input(undefined, ...(ngDevMode ? [{ debugName: "prefixIconTemplate" }] : /* istanbul ignore next */ []));
3068
3073
  /** Whether the parent form has been submitted; propagated to each fk-field-outlet. */
3069
3074
  submitted = input(false, ...(ngDevMode ? [{ debugName: "submitted" }] : /* istanbul ignore next */ []));
3070
3075
  /** Fires with the index of the newly added row. */
@@ -3112,12 +3117,12 @@ class FieldArrayOutletComponent {
3112
3117
  this.rowRemoved.emit(index);
3113
3118
  }
3114
3119
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FieldArrayOutletComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3115
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: FieldArrayOutletComponent, isStandalone: true, selector: "fk-field-array-outlet", inputs: { className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, formArray: { classPropertyName: "formArray", publicName: "formArray", isSignal: true, isRequired: true, transformFunction: null }, submitted: { classPropertyName: "submitted", publicName: "submitted", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowAdded: "rowAdded", rowRemoved: "rowRemoved" }, host: { properties: { "class": "this.hostClass" } }, ngImport: i0, template: "@if (field().label) {\n <legend class=\"fk-field-array__legend\">{{ field().label }}</legend>\n}\n@for (row of rows(); track $index) {\n <div class=\"fk-field-array__row\" [formGroup]=\"row\">\n @for (childField of field().arrayFields ?? []; track childField.key) {\n <fk-field-outlet\n [field]=\"childField\"\n [form]=\"row\"\n [submitted]=\"submitted()\"\n />\n }\n @if (canRemove()) {\n <button\n type=\"button\"\n class=\"fk-field-array__remove\"\n aria-label=\"Remove row\"\n (click)=\"removeRow($index)\"\n >\n &times;\n </button>\n }\n </div>\n}\n@if (canAdd()) {\n <button type=\"button\" class=\"fk-field-array__add\" (click)=\"addRow()\">\n + Add\n </button>\n}\n", styles: [":host{display:block}.fk-field-array__legend{font-size:var( --fk-field-array-label-font-size, var(--fk-form-label-font-size, .875rem) );font-weight:var( --fk-field-array-label-font-weight, var(--fk-form-label-font-weight, 500) );color:var( --fk-field-array-label-color, var(--fk-form-label-color, #1f2d3d) )}.fk-field-array__row{display:flex;column-gap:var(--fk-field-array-row-gap, var(--fk-rhythm-3, .75rem));align-items:flex-start;margin-top:var(--fk-field-array-row-gap, var(--fk-rhythm-3, .75rem))}.fk-field-array__legend+.fk-field-array__row{margin-top:var( --fk-field-array-label-gap, var(--fk-form-label-gap, .375rem) )}.fk-field-array__row>fk-field-outlet{flex:1;--fk-form-label-gap: 0}.fk-field-array__remove{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:var(--fk-field-array-remove-size, var(--fk-rhythm-8, 2rem));height:var(--fk-field-array-remove-size, var(--fk-rhythm-8, 2rem));margin-top:var(--fk-field-array-remove-offset, .25rem);padding:0;font-size:1.125rem;line-height:1;color:var(--fk-field-array-remove-color, var(--fk-color-muted, #8a98a8));background:transparent;border:1px solid var( --fk-field-array-remove-border-color, var(--fk-color-border, #d9e2ee) );border-radius:var( --fk-field-array-remove-radius, var(--fk-radius-md, .5rem) );cursor:pointer;transition:color .15s,border-color .15s}.fk-field-array__remove:hover{color:var( --fk-field-array-remove-hover-color, var(--fk-form-error-color, #dc2626) );border-color:var( --fk-field-array-remove-hover-color, var(--fk-form-error-color, #dc2626) )}.fk-field-array__remove:focus-visible{outline:none;box-shadow:var( --fk-field-array-remove-focus-ring, var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18)) )}.fk-field-array__add{margin-top:var(--fk-field-array-add-gap, var(--fk-rhythm-3, .75rem));padding:var(--fk-field-array-add-padding-y, var(--fk-rhythm-2, .5rem)) var(--fk-field-array-add-padding-x, var(--fk-rhythm-4, 1rem));font-size:var( --fk-field-array-add-font-size, var(--fk-typography-small-font-size, .8125rem) );color:var(--fk-field-array-add-color, var(--fk-color-primary, #0a84ff));background:transparent;border:1px dashed var(--fk-field-array-add-border-color, var(--fk-color-border, #d9e2ee));border-radius:var( --fk-field-array-add-radius, var(--fk-radius-md, .5rem) );cursor:pointer;transition:color .15s,border-color .15s}.fk-field-array__add:hover{border-color:var( --fk-field-array-add-hover-border-color, var(--fk-color-primary, #0a84ff) )}.fk-field-array__add:focus-visible{outline:none;box-shadow:var( --fk-field-array-add-focus-ring, var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18)) )}\n"], dependencies: [{ kind: "component", type: FieldOutletComponent, selector: "fk-field-outlet", inputs: ["className", "field", "form", "submitted"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3120
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: FieldArrayOutletComponent, isStandalone: true, selector: "fk-field-array-outlet", inputs: { className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, formArray: { classPropertyName: "formArray", publicName: "formArray", isSignal: true, isRequired: true, transformFunction: null }, prefixIconTemplate: { classPropertyName: "prefixIconTemplate", publicName: "prefixIconTemplate", isSignal: true, isRequired: false, transformFunction: null }, submitted: { classPropertyName: "submitted", publicName: "submitted", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowAdded: "rowAdded", rowRemoved: "rowRemoved" }, host: { properties: { "class": "this.hostClass" } }, ngImport: i0, template: "@if (field().label) {\n <legend class=\"fk-field-array__legend\">{{ field().label }}</legend>\n}\n@for (row of rows(); track $index) {\n <div class=\"fk-field-array__row\" [formGroup]=\"row\">\n @for (childField of field().arrayFields ?? []; track childField.key) {\n <fk-field-outlet\n [field]=\"childField\"\n [form]=\"row\"\n [submitted]=\"submitted()\"\n [prefixIconTemplate]=\"prefixIconTemplate()\"\n />\n }\n @if (canRemove()) {\n <button\n type=\"button\"\n class=\"fk-field-array__remove\"\n aria-label=\"Remove row\"\n (click)=\"removeRow($index)\"\n >\n &times;\n </button>\n }\n </div>\n}\n@if (canAdd()) {\n <button type=\"button\" class=\"fk-field-array__add\" (click)=\"addRow()\">\n + Add\n </button>\n}\n", styles: [":host{display:block}.fk-field-array__legend{font-size:var( --fk-field-array-label-font-size, var(--fk-form-label-font-size, .875rem) );font-weight:var( --fk-field-array-label-font-weight, var(--fk-form-label-font-weight, 500) );color:var( --fk-field-array-label-color, var(--fk-form-label-color, #1f2d3d) )}.fk-field-array__row{display:flex;column-gap:var(--fk-field-array-row-gap, var(--fk-rhythm-3, .75rem));align-items:flex-start;margin-top:var(--fk-field-array-row-gap, var(--fk-rhythm-3, .75rem))}.fk-field-array__legend+.fk-field-array__row{margin-top:var( --fk-field-array-label-gap, var(--fk-form-label-gap, .375rem) )}.fk-field-array__row>fk-field-outlet{flex:1;--fk-form-label-gap: 0}.fk-field-array__remove{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:var(--fk-field-array-remove-size, var(--fk-rhythm-8, 2rem));height:var(--fk-field-array-remove-size, var(--fk-rhythm-8, 2rem));margin-top:var(--fk-field-array-remove-offset, .25rem);padding:0;font-size:1.125rem;line-height:1;color:var(--fk-field-array-remove-color, var(--fk-color-muted, #8a98a8));background:transparent;border:1px solid var( --fk-field-array-remove-border-color, var(--fk-color-border, #d9e2ee) );border-radius:var( --fk-field-array-remove-radius, var(--fk-radius-md, .5rem) );cursor:pointer;transition:color .15s,border-color .15s}.fk-field-array__remove:hover{color:var( --fk-field-array-remove-hover-color, var(--fk-form-error-color, #dc2626) );border-color:var( --fk-field-array-remove-hover-color, var(--fk-form-error-color, #dc2626) )}.fk-field-array__remove:focus-visible{outline:none;box-shadow:var( --fk-field-array-remove-focus-ring, var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18)) )}.fk-field-array__add{margin-top:var(--fk-field-array-add-gap, var(--fk-rhythm-3, .75rem));padding:var(--fk-field-array-add-padding-y, var(--fk-rhythm-2, .5rem)) var(--fk-field-array-add-padding-x, var(--fk-rhythm-4, 1rem));font-size:var( --fk-field-array-add-font-size, var(--fk-typography-small-font-size, .8125rem) );color:var(--fk-field-array-add-color, var(--fk-color-primary, #0a84ff));background:transparent;border:1px dashed var(--fk-field-array-add-border-color, var(--fk-color-border, #d9e2ee));border-radius:var( --fk-field-array-add-radius, var(--fk-radius-md, .5rem) );cursor:pointer;transition:color .15s,border-color .15s}.fk-field-array__add:hover{border-color:var( --fk-field-array-add-hover-border-color, var(--fk-color-primary, #0a84ff) )}.fk-field-array__add:focus-visible{outline:none;box-shadow:var( --fk-field-array-add-focus-ring, var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18)) )}\n"], dependencies: [{ kind: "component", type: FieldOutletComponent, selector: "fk-field-outlet", inputs: ["className", "field", "form", "submitted", "prefixIconTemplate"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3116
3121
  }
3117
3122
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FieldArrayOutletComponent, decorators: [{
3118
3123
  type: Component,
3119
- args: [{ selector: 'fk-field-array-outlet', standalone: true, imports: [FieldOutletComponent, ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (field().label) {\n <legend class=\"fk-field-array__legend\">{{ field().label }}</legend>\n}\n@for (row of rows(); track $index) {\n <div class=\"fk-field-array__row\" [formGroup]=\"row\">\n @for (childField of field().arrayFields ?? []; track childField.key) {\n <fk-field-outlet\n [field]=\"childField\"\n [form]=\"row\"\n [submitted]=\"submitted()\"\n />\n }\n @if (canRemove()) {\n <button\n type=\"button\"\n class=\"fk-field-array__remove\"\n aria-label=\"Remove row\"\n (click)=\"removeRow($index)\"\n >\n &times;\n </button>\n }\n </div>\n}\n@if (canAdd()) {\n <button type=\"button\" class=\"fk-field-array__add\" (click)=\"addRow()\">\n + Add\n </button>\n}\n", styles: [":host{display:block}.fk-field-array__legend{font-size:var( --fk-field-array-label-font-size, var(--fk-form-label-font-size, .875rem) );font-weight:var( --fk-field-array-label-font-weight, var(--fk-form-label-font-weight, 500) );color:var( --fk-field-array-label-color, var(--fk-form-label-color, #1f2d3d) )}.fk-field-array__row{display:flex;column-gap:var(--fk-field-array-row-gap, var(--fk-rhythm-3, .75rem));align-items:flex-start;margin-top:var(--fk-field-array-row-gap, var(--fk-rhythm-3, .75rem))}.fk-field-array__legend+.fk-field-array__row{margin-top:var( --fk-field-array-label-gap, var(--fk-form-label-gap, .375rem) )}.fk-field-array__row>fk-field-outlet{flex:1;--fk-form-label-gap: 0}.fk-field-array__remove{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:var(--fk-field-array-remove-size, var(--fk-rhythm-8, 2rem));height:var(--fk-field-array-remove-size, var(--fk-rhythm-8, 2rem));margin-top:var(--fk-field-array-remove-offset, .25rem);padding:0;font-size:1.125rem;line-height:1;color:var(--fk-field-array-remove-color, var(--fk-color-muted, #8a98a8));background:transparent;border:1px solid var( --fk-field-array-remove-border-color, var(--fk-color-border, #d9e2ee) );border-radius:var( --fk-field-array-remove-radius, var(--fk-radius-md, .5rem) );cursor:pointer;transition:color .15s,border-color .15s}.fk-field-array__remove:hover{color:var( --fk-field-array-remove-hover-color, var(--fk-form-error-color, #dc2626) );border-color:var( --fk-field-array-remove-hover-color, var(--fk-form-error-color, #dc2626) )}.fk-field-array__remove:focus-visible{outline:none;box-shadow:var( --fk-field-array-remove-focus-ring, var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18)) )}.fk-field-array__add{margin-top:var(--fk-field-array-add-gap, var(--fk-rhythm-3, .75rem));padding:var(--fk-field-array-add-padding-y, var(--fk-rhythm-2, .5rem)) var(--fk-field-array-add-padding-x, var(--fk-rhythm-4, 1rem));font-size:var( --fk-field-array-add-font-size, var(--fk-typography-small-font-size, .8125rem) );color:var(--fk-field-array-add-color, var(--fk-color-primary, #0a84ff));background:transparent;border:1px dashed var(--fk-field-array-add-border-color, var(--fk-color-border, #d9e2ee));border-radius:var( --fk-field-array-add-radius, var(--fk-radius-md, .5rem) );cursor:pointer;transition:color .15s,border-color .15s}.fk-field-array__add:hover{border-color:var( --fk-field-array-add-hover-border-color, var(--fk-color-primary, #0a84ff) )}.fk-field-array__add:focus-visible{outline:none;box-shadow:var( --fk-field-array-add-focus-ring, var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18)) )}\n"] }]
3120
- }], propDecorators: { className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], formArray: [{ type: i0.Input, args: [{ isSignal: true, alias: "formArray", required: true }] }], submitted: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitted", required: false }] }], rowAdded: [{ type: i0.Output, args: ["rowAdded"] }], rowRemoved: [{ type: i0.Output, args: ["rowRemoved"] }], hostClass: [{
3124
+ args: [{ selector: 'fk-field-array-outlet', standalone: true, imports: [FieldOutletComponent, ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (field().label) {\n <legend class=\"fk-field-array__legend\">{{ field().label }}</legend>\n}\n@for (row of rows(); track $index) {\n <div class=\"fk-field-array__row\" [formGroup]=\"row\">\n @for (childField of field().arrayFields ?? []; track childField.key) {\n <fk-field-outlet\n [field]=\"childField\"\n [form]=\"row\"\n [submitted]=\"submitted()\"\n [prefixIconTemplate]=\"prefixIconTemplate()\"\n />\n }\n @if (canRemove()) {\n <button\n type=\"button\"\n class=\"fk-field-array__remove\"\n aria-label=\"Remove row\"\n (click)=\"removeRow($index)\"\n >\n &times;\n </button>\n }\n </div>\n}\n@if (canAdd()) {\n <button type=\"button\" class=\"fk-field-array__add\" (click)=\"addRow()\">\n + Add\n </button>\n}\n", styles: [":host{display:block}.fk-field-array__legend{font-size:var( --fk-field-array-label-font-size, var(--fk-form-label-font-size, .875rem) );font-weight:var( --fk-field-array-label-font-weight, var(--fk-form-label-font-weight, 500) );color:var( --fk-field-array-label-color, var(--fk-form-label-color, #1f2d3d) )}.fk-field-array__row{display:flex;column-gap:var(--fk-field-array-row-gap, var(--fk-rhythm-3, .75rem));align-items:flex-start;margin-top:var(--fk-field-array-row-gap, var(--fk-rhythm-3, .75rem))}.fk-field-array__legend+.fk-field-array__row{margin-top:var( --fk-field-array-label-gap, var(--fk-form-label-gap, .375rem) )}.fk-field-array__row>fk-field-outlet{flex:1;--fk-form-label-gap: 0}.fk-field-array__remove{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:var(--fk-field-array-remove-size, var(--fk-rhythm-8, 2rem));height:var(--fk-field-array-remove-size, var(--fk-rhythm-8, 2rem));margin-top:var(--fk-field-array-remove-offset, .25rem);padding:0;font-size:1.125rem;line-height:1;color:var(--fk-field-array-remove-color, var(--fk-color-muted, #8a98a8));background:transparent;border:1px solid var( --fk-field-array-remove-border-color, var(--fk-color-border, #d9e2ee) );border-radius:var( --fk-field-array-remove-radius, var(--fk-radius-md, .5rem) );cursor:pointer;transition:color .15s,border-color .15s}.fk-field-array__remove:hover{color:var( --fk-field-array-remove-hover-color, var(--fk-form-error-color, #dc2626) );border-color:var( --fk-field-array-remove-hover-color, var(--fk-form-error-color, #dc2626) )}.fk-field-array__remove:focus-visible{outline:none;box-shadow:var( --fk-field-array-remove-focus-ring, var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18)) )}.fk-field-array__add{margin-top:var(--fk-field-array-add-gap, var(--fk-rhythm-3, .75rem));padding:var(--fk-field-array-add-padding-y, var(--fk-rhythm-2, .5rem)) var(--fk-field-array-add-padding-x, var(--fk-rhythm-4, 1rem));font-size:var( --fk-field-array-add-font-size, var(--fk-typography-small-font-size, .8125rem) );color:var(--fk-field-array-add-color, var(--fk-color-primary, #0a84ff));background:transparent;border:1px dashed var(--fk-field-array-add-border-color, var(--fk-color-border, #d9e2ee));border-radius:var( --fk-field-array-add-radius, var(--fk-radius-md, .5rem) );cursor:pointer;transition:color .15s,border-color .15s}.fk-field-array__add:hover{border-color:var( --fk-field-array-add-hover-border-color, var(--fk-color-primary, #0a84ff) )}.fk-field-array__add:focus-visible{outline:none;box-shadow:var( --fk-field-array-add-focus-ring, var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18)) )}\n"] }]
3125
+ }], propDecorators: { className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], formArray: [{ type: i0.Input, args: [{ isSignal: true, alias: "formArray", required: true }] }], prefixIconTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIconTemplate", required: false }] }], submitted: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitted", required: false }] }], rowAdded: [{ type: i0.Output, args: ["rowAdded"] }], rowRemoved: [{ type: i0.Output, args: ["rowRemoved"] }], hostClass: [{
3121
3126
  type: HostBinding,
3122
3127
  args: ['class']
3123
3128
  }] } });
@@ -3143,6 +3148,8 @@ class SectionOutletComponent {
3143
3148
  form = input.required(...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
3144
3149
  /** Whether the parent form has been submitted; propagated to each fk-field-outlet. */
3145
3150
  submitted = input(false, ...(ngDevMode ? [{ debugName: "submitted" }] : /* istanbul ignore next */ []));
3151
+ /** Prefix-icon template threaded to each field (the app owns the icon). */
3152
+ prefixIconTemplate = input(undefined, ...(ngDevMode ? [{ debugName: "prefixIconTemplate" }] : /* istanbul ignore next */ []));
3146
3153
  classes = computed(() => ['fk-section-outlet', this.className()].filter(Boolean).join(' '), ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
3147
3154
  get hostClass() {
3148
3155
  return this.classes();
@@ -3152,12 +3159,12 @@ class SectionOutletComponent {
3152
3159
  return this.section().fields.find((f) => f.key === key);
3153
3160
  }
3154
3161
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SectionOutletComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3155
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: SectionOutletComponent, isStandalone: true, selector: "fk-section-outlet", inputs: { className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, section: { classPropertyName: "section", publicName: "section", isSignal: true, isRequired: true, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, submitted: { classPropertyName: "submitted", publicName: "submitted", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClass" } }, ngImport: i0, template: "@if (section().title) {\n <h3 class=\"fk-section__title\">{{ section().title }}</h3>\n}\n@if (section().description) {\n <p class=\"fk-section__description\">{{ section().description }}</p>\n}\n<div class=\"fk-section__fields\">\n @if (section().layout?.length) {\n @for (row of section().layout!; track $index) {\n <div class=\"fk-section__row\">\n @for (col of row.columns; track col.fieldKey) {\n <div class=\"fk-section__col\" [style.--fk-col-span]=\"col.span ?? 12\">\n @if (getField(col.fieldKey); as field) {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"form()\"\n [submitted]=\"submitted()\"\n />\n }\n </div>\n }\n </div>\n }\n } @else {\n @for (field of section().fields; track field.key) {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"form()\"\n [submitted]=\"submitted()\"\n />\n }\n }\n</div>\n", styles: [":host{display:block;margin-bottom:var(--fk-rhythm-6, 1.5rem)}.fk-section__title{font-size:1.125rem;font-weight:600;color:var(--fk-form-label-color);margin:0 0 var(--fk-rhythm-2, .5rem)}.fk-section__description{font-size:var(--fk-form-hint-font-size);color:var(--fk-form-hint-color);margin:0 0 var(--fk-rhythm-4, 1rem)}.fk-section__fields{display:flex;flex-direction:column}.fk-section__row{display:grid;grid-template-columns:repeat(12,1fr);column-gap:var(--fk-form-field-gap)}.fk-section__col{grid-column:span var(--fk-col-span, 12)}\n"], dependencies: [{ kind: "component", type: FieldOutletComponent, selector: "fk-field-outlet", inputs: ["className", "field", "form", "submitted"] }, { kind: "ngmodule", type: ReactiveFormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3162
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: SectionOutletComponent, isStandalone: true, selector: "fk-section-outlet", inputs: { className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, section: { classPropertyName: "section", publicName: "section", isSignal: true, isRequired: true, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, submitted: { classPropertyName: "submitted", publicName: "submitted", isSignal: true, isRequired: false, transformFunction: null }, prefixIconTemplate: { classPropertyName: "prefixIconTemplate", publicName: "prefixIconTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClass" } }, ngImport: i0, template: "@if (section().title) {\n <h3 class=\"fk-section__title\">{{ section().title }}</h3>\n}\n@if (section().description) {\n <p class=\"fk-section__description\">{{ section().description }}</p>\n}\n<div class=\"fk-section__fields\">\n @if (section().layout?.length) {\n @for (row of section().layout!; track $index) {\n <div class=\"fk-section__row\">\n @for (col of row.columns; track col.fieldKey) {\n <div class=\"fk-section__col\" [style.--fk-col-span]=\"col.span ?? 12\">\n @if (getField(col.fieldKey); as field) {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"form()\"\n [submitted]=\"submitted()\"\n [prefixIconTemplate]=\"prefixIconTemplate()\"\n />\n }\n </div>\n }\n </div>\n }\n } @else {\n @for (field of section().fields; track field.key) {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"form()\"\n [submitted]=\"submitted()\"\n [prefixIconTemplate]=\"prefixIconTemplate()\"\n />\n }\n }\n</div>\n", styles: [":host{display:block;margin-bottom:var(--fk-rhythm-6, 1.5rem)}.fk-section__title{font-size:1.125rem;font-weight:600;color:var(--fk-form-label-color);margin:0 0 var(--fk-rhythm-2, .5rem)}.fk-section__description{font-size:var(--fk-form-hint-font-size);color:var(--fk-form-hint-color);margin:0 0 var(--fk-rhythm-4, 1rem)}.fk-section__fields{display:flex;flex-direction:column}.fk-section__row{display:grid;grid-template-columns:repeat(12,1fr);column-gap:var(--fk-form-field-gap)}.fk-section__col{grid-column:span var(--fk-col-span, 12)}\n"], dependencies: [{ kind: "component", type: FieldOutletComponent, selector: "fk-field-outlet", inputs: ["className", "field", "form", "submitted", "prefixIconTemplate"] }, { kind: "ngmodule", type: ReactiveFormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3156
3163
  }
3157
3164
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SectionOutletComponent, decorators: [{
3158
3165
  type: Component,
3159
- args: [{ selector: 'fk-section-outlet', standalone: true, imports: [FieldOutletComponent, ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (section().title) {\n <h3 class=\"fk-section__title\">{{ section().title }}</h3>\n}\n@if (section().description) {\n <p class=\"fk-section__description\">{{ section().description }}</p>\n}\n<div class=\"fk-section__fields\">\n @if (section().layout?.length) {\n @for (row of section().layout!; track $index) {\n <div class=\"fk-section__row\">\n @for (col of row.columns; track col.fieldKey) {\n <div class=\"fk-section__col\" [style.--fk-col-span]=\"col.span ?? 12\">\n @if (getField(col.fieldKey); as field) {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"form()\"\n [submitted]=\"submitted()\"\n />\n }\n </div>\n }\n </div>\n }\n } @else {\n @for (field of section().fields; track field.key) {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"form()\"\n [submitted]=\"submitted()\"\n />\n }\n }\n</div>\n", styles: [":host{display:block;margin-bottom:var(--fk-rhythm-6, 1.5rem)}.fk-section__title{font-size:1.125rem;font-weight:600;color:var(--fk-form-label-color);margin:0 0 var(--fk-rhythm-2, .5rem)}.fk-section__description{font-size:var(--fk-form-hint-font-size);color:var(--fk-form-hint-color);margin:0 0 var(--fk-rhythm-4, 1rem)}.fk-section__fields{display:flex;flex-direction:column}.fk-section__row{display:grid;grid-template-columns:repeat(12,1fr);column-gap:var(--fk-form-field-gap)}.fk-section__col{grid-column:span var(--fk-col-span, 12)}\n"] }]
3160
- }], propDecorators: { className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], section: [{ type: i0.Input, args: [{ isSignal: true, alias: "section", required: true }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: true }] }], submitted: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitted", required: false }] }], hostClass: [{
3166
+ args: [{ selector: 'fk-section-outlet', standalone: true, imports: [FieldOutletComponent, ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (section().title) {\n <h3 class=\"fk-section__title\">{{ section().title }}</h3>\n}\n@if (section().description) {\n <p class=\"fk-section__description\">{{ section().description }}</p>\n}\n<div class=\"fk-section__fields\">\n @if (section().layout?.length) {\n @for (row of section().layout!; track $index) {\n <div class=\"fk-section__row\">\n @for (col of row.columns; track col.fieldKey) {\n <div class=\"fk-section__col\" [style.--fk-col-span]=\"col.span ?? 12\">\n @if (getField(col.fieldKey); as field) {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"form()\"\n [submitted]=\"submitted()\"\n [prefixIconTemplate]=\"prefixIconTemplate()\"\n />\n }\n </div>\n }\n </div>\n }\n } @else {\n @for (field of section().fields; track field.key) {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"form()\"\n [submitted]=\"submitted()\"\n [prefixIconTemplate]=\"prefixIconTemplate()\"\n />\n }\n }\n</div>\n", styles: [":host{display:block;margin-bottom:var(--fk-rhythm-6, 1.5rem)}.fk-section__title{font-size:1.125rem;font-weight:600;color:var(--fk-form-label-color);margin:0 0 var(--fk-rhythm-2, .5rem)}.fk-section__description{font-size:var(--fk-form-hint-font-size);color:var(--fk-form-hint-color);margin:0 0 var(--fk-rhythm-4, 1rem)}.fk-section__fields{display:flex;flex-direction:column}.fk-section__row{display:grid;grid-template-columns:repeat(12,1fr);column-gap:var(--fk-form-field-gap)}.fk-section__col{grid-column:span var(--fk-col-span, 12)}\n"] }]
3167
+ }], propDecorators: { className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], section: [{ type: i0.Input, args: [{ isSignal: true, alias: "section", required: true }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: true }] }], submitted: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitted", required: false }] }], prefixIconTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIconTemplate", required: false }] }], hostClass: [{
3161
3168
  type: HostBinding,
3162
3169
  args: ['class']
3163
3170
  }] } });
@@ -3195,6 +3202,12 @@ class FormRendererComponent {
3195
3202
  * If not provided, one is auto-generated from the schema.
3196
3203
  */
3197
3204
  form = input(null, ...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
3205
+ /**
3206
+ * Optional template rendering a field's prefix icon (the app owns the icon
3207
+ * system). Receives the field as `$implicit`; render it from
3208
+ * `field.ui.prefixIcon`. Fields without a `prefixIcon` render no icon.
3209
+ */
3210
+ prefixIconTemplate = input(undefined, ...(ngDevMode ? [{ debugName: "prefixIconTemplate" }] : /* istanbul ignore next */ []));
3198
3211
  /** Emits the FormGroup value when the form is submitted. */
3199
3212
  formSubmit = output();
3200
3213
  /** The resolved FormGroup (either provided or auto-generated). */
@@ -3260,7 +3273,7 @@ class FormRendererComponent {
3260
3273
  });
3261
3274
  }
3262
3275
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FormRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3263
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: FormRendererComponent, isStandalone: true, selector: "fk-form-renderer", inputs: { className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { formSubmit: "formSubmit" }, host: { properties: { "class": "this.hostClass" } }, ngImport: i0, template: "@if (resolvedForm()) {\n <fk-form (formSubmit)=\"onFormSubmit()\">\n <div class=\"fk-form-renderer__fields\" [formGroup]=\"resolvedForm()!\">\n @if (schema().sections?.length) {\n @for (section of schema().sections!; track $index) {\n <fk-section-outlet\n [section]=\"section\"\n [form]=\"resolvedForm()!\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n />\n }\n }\n @if (schema().layout?.length) {\n @for (row of schema().layout!; track $index) {\n <div class=\"fk-form-renderer__row\">\n @for (col of row.columns; track col.fieldKey) {\n <div\n class=\"fk-form-renderer__col\"\n [style.--fk-col-span]=\"col.span ?? 12\"\n >\n @if (getField(col.fieldKey); as field) {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"resolvedForm()!\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n />\n }\n </div>\n }\n </div>\n }\n } @else {\n @for (field of schema().fields; track field.key) {\n @if (field.arrayFields?.length) {\n <fk-field-array-outlet\n [field]=\"field\"\n [formArray]=\"getFormArray(field.key)\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n />\n } @else {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"resolvedForm()!\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n />\n }\n }\n }\n </div>\n <ng-content />\n </fk-form>\n}\n", styles: [":host{display:block}.fk-form-renderer__fields{display:flex;flex-direction:column;row-gap:var(--fk-form-field-gap, var(--fk-rhythm-4, 1rem))}.fk-form-renderer__row{display:grid;grid-template-columns:repeat(12,1fr);column-gap:var(--fk-form-field-gap, var(--fk-rhythm-4, 1rem))}.fk-form-renderer__col{grid-column:span var(--fk-col-span, 12)}\n"], dependencies: [{ kind: "component", type: FieldArrayOutletComponent, selector: "fk-field-array-outlet", inputs: ["className", "field", "formArray", "submitted"], outputs: ["rowAdded", "rowRemoved"] }, { kind: "component", type: FieldOutletComponent, selector: "fk-field-outlet", inputs: ["className", "field", "form", "submitted"] }, { kind: "component", type: FormComponent, selector: "fk-form", inputs: ["className"], outputs: ["formSubmit"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormArrayDirective, selector: "[formArray]", inputs: ["formArray"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: SectionOutletComponent, selector: "fk-section-outlet", inputs: ["className", "section", "form", "submitted"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3276
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: FormRendererComponent, isStandalone: true, selector: "fk-form-renderer", inputs: { className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: false, transformFunction: null }, prefixIconTemplate: { classPropertyName: "prefixIconTemplate", publicName: "prefixIconTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { formSubmit: "formSubmit" }, host: { properties: { "class": "this.hostClass" } }, ngImport: i0, template: "@if (resolvedForm()) {\n <fk-form (formSubmit)=\"onFormSubmit()\">\n <div class=\"fk-form-renderer__fields\" [formGroup]=\"resolvedForm()!\">\n @if (schema().sections?.length) {\n @for (section of schema().sections!; track $index) {\n <fk-section-outlet\n [section]=\"section\"\n [form]=\"resolvedForm()!\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n [prefixIconTemplate]=\"prefixIconTemplate()\"\n />\n }\n }\n @if (schema().layout?.length) {\n @for (row of schema().layout!; track $index) {\n <div class=\"fk-form-renderer__row\">\n @for (col of row.columns; track col.fieldKey) {\n <div\n class=\"fk-form-renderer__col\"\n [style.--fk-col-span]=\"col.span ?? 12\"\n >\n @if (getField(col.fieldKey); as field) {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"resolvedForm()!\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n [prefixIconTemplate]=\"prefixIconTemplate()\"\n />\n }\n </div>\n }\n </div>\n }\n } @else {\n @for (field of schema().fields; track field.key) {\n @if (field.arrayFields?.length) {\n <fk-field-array-outlet\n [field]=\"field\"\n [formArray]=\"getFormArray(field.key)\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n [prefixIconTemplate]=\"prefixIconTemplate()\"\n />\n } @else {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"resolvedForm()!\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n [prefixIconTemplate]=\"prefixIconTemplate()\"\n />\n }\n }\n }\n </div>\n <ng-content />\n </fk-form>\n}\n", styles: [":host{display:block}.fk-form-renderer__fields{display:flex;flex-direction:column;row-gap:var(--fk-form-field-gap, var(--fk-rhythm-4, 1rem))}.fk-form-renderer__row{display:grid;grid-template-columns:repeat(12,1fr);column-gap:var(--fk-form-field-gap, var(--fk-rhythm-4, 1rem))}.fk-form-renderer__col{grid-column:span var(--fk-col-span, 12)}\n"], dependencies: [{ kind: "component", type: FieldArrayOutletComponent, selector: "fk-field-array-outlet", inputs: ["className", "field", "formArray", "prefixIconTemplate", "submitted"], outputs: ["rowAdded", "rowRemoved"] }, { kind: "component", type: FieldOutletComponent, selector: "fk-field-outlet", inputs: ["className", "field", "form", "submitted", "prefixIconTemplate"] }, { kind: "component", type: FormComponent, selector: "fk-form", inputs: ["className"], outputs: ["formSubmit"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormArrayDirective, selector: "[formArray]", inputs: ["formArray"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: SectionOutletComponent, selector: "fk-section-outlet", inputs: ["className", "section", "form", "submitted", "prefixIconTemplate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3264
3277
  }
3265
3278
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FormRendererComponent, decorators: [{
3266
3279
  type: Component,
@@ -3270,8 +3283,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
3270
3283
  FormComponent,
3271
3284
  ReactiveFormsModule,
3272
3285
  SectionOutletComponent,
3273
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (resolvedForm()) {\n <fk-form (formSubmit)=\"onFormSubmit()\">\n <div class=\"fk-form-renderer__fields\" [formGroup]=\"resolvedForm()!\">\n @if (schema().sections?.length) {\n @for (section of schema().sections!; track $index) {\n <fk-section-outlet\n [section]=\"section\"\n [form]=\"resolvedForm()!\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n />\n }\n }\n @if (schema().layout?.length) {\n @for (row of schema().layout!; track $index) {\n <div class=\"fk-form-renderer__row\">\n @for (col of row.columns; track col.fieldKey) {\n <div\n class=\"fk-form-renderer__col\"\n [style.--fk-col-span]=\"col.span ?? 12\"\n >\n @if (getField(col.fieldKey); as field) {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"resolvedForm()!\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n />\n }\n </div>\n }\n </div>\n }\n } @else {\n @for (field of schema().fields; track field.key) {\n @if (field.arrayFields?.length) {\n <fk-field-array-outlet\n [field]=\"field\"\n [formArray]=\"getFormArray(field.key)\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n />\n } @else {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"resolvedForm()!\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n />\n }\n }\n }\n </div>\n <ng-content />\n </fk-form>\n}\n", styles: [":host{display:block}.fk-form-renderer__fields{display:flex;flex-direction:column;row-gap:var(--fk-form-field-gap, var(--fk-rhythm-4, 1rem))}.fk-form-renderer__row{display:grid;grid-template-columns:repeat(12,1fr);column-gap:var(--fk-form-field-gap, var(--fk-rhythm-4, 1rem))}.fk-form-renderer__col{grid-column:span var(--fk-col-span, 12)}\n"] }]
3274
- }], propDecorators: { className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: true }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: false }] }], formSubmit: [{ type: i0.Output, args: ["formSubmit"] }], hostClass: [{
3286
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (resolvedForm()) {\n <fk-form (formSubmit)=\"onFormSubmit()\">\n <div class=\"fk-form-renderer__fields\" [formGroup]=\"resolvedForm()!\">\n @if (schema().sections?.length) {\n @for (section of schema().sections!; track $index) {\n <fk-section-outlet\n [section]=\"section\"\n [form]=\"resolvedForm()!\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n [prefixIconTemplate]=\"prefixIconTemplate()\"\n />\n }\n }\n @if (schema().layout?.length) {\n @for (row of schema().layout!; track $index) {\n <div class=\"fk-form-renderer__row\">\n @for (col of row.columns; track col.fieldKey) {\n <div\n class=\"fk-form-renderer__col\"\n [style.--fk-col-span]=\"col.span ?? 12\"\n >\n @if (getField(col.fieldKey); as field) {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"resolvedForm()!\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n [prefixIconTemplate]=\"prefixIconTemplate()\"\n />\n }\n </div>\n }\n </div>\n }\n } @else {\n @for (field of schema().fields; track field.key) {\n @if (field.arrayFields?.length) {\n <fk-field-array-outlet\n [field]=\"field\"\n [formArray]=\"getFormArray(field.key)\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n [prefixIconTemplate]=\"prefixIconTemplate()\"\n />\n } @else {\n <fk-field-outlet\n [field]=\"field\"\n [form]=\"resolvedForm()!\"\n [submitted]=\"formRef()?.submitted() ?? false\"\n [prefixIconTemplate]=\"prefixIconTemplate()\"\n />\n }\n }\n }\n </div>\n <ng-content />\n </fk-form>\n}\n", styles: [":host{display:block}.fk-form-renderer__fields{display:flex;flex-direction:column;row-gap:var(--fk-form-field-gap, var(--fk-rhythm-4, 1rem))}.fk-form-renderer__row{display:grid;grid-template-columns:repeat(12,1fr);column-gap:var(--fk-form-field-gap, var(--fk-rhythm-4, 1rem))}.fk-form-renderer__col{grid-column:span var(--fk-col-span, 12)}\n"] }]
3287
+ }], propDecorators: { className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: true }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: false }] }], prefixIconTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIconTemplate", required: false }] }], formSubmit: [{ type: i0.Output, args: ["formSubmit"] }], hostClass: [{
3275
3288
  type: HostBinding,
3276
3289
  args: ['class']
3277
3290
  }] } });
@@ -3422,8 +3435,6 @@ class FileDropzoneComponent {
3422
3435
  multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
3423
3436
  /** When true, prevents drag-and-drop and click-to-browse interaction. */
3424
3437
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
3425
- /** Icon name rendered inside the dropzone surface. */
3426
- icon = input('upload', ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
3427
3438
  /** Primary prompt text displayed inside the dropzone. */
3428
3439
  prompt = input.required(...(ngDevMode ? [{ debugName: "prompt" }] : /* istanbul ignore next */ []));
3429
3440
  /** Secondary hint text shown below the prompt. */
@@ -3493,16 +3504,55 @@ class FileDropzoneComponent {
3493
3504
  this.filesSelected.emit(this.multiple() ? files : [files[0]]);
3494
3505
  }
3495
3506
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FileDropzoneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3496
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: FileDropzoneComponent, isStandalone: true, selector: "fk-file-dropzone", inputs: { accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, prompt: { classPropertyName: "prompt", publicName: "prompt", isSignal: true, isRequired: true, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", 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 } }, outputs: { filesSelected: "filesSelected" }, host: { properties: { "class": "this.hostClass" } }, ngImport: i0, template: "<label\n class=\"fk-file-dropzone__container\"\n [attr.id]=\"id() ?? undefined\"\n [attr.aria-label]=\"ariaLabel()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n <fk-icon [name]=\"icon()\" size=\"lg\" class=\"fk-file-dropzone__icon\" />\n\n <fk-text tone=\"muted\" class=\"fk-file-dropzone__prompt\">\n {{ prompt() }}\n </fk-text>\n\n @if (hint(); as h) {\n <fk-text tone=\"muted\" class=\"fk-file-dropzone__hint\">\n {{ h }}\n </fk-text>\n }\n\n <input\n #nativeInput\n type=\"file\"\n class=\"fk-file-dropzone__native\"\n [attr.accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n (change)=\"onChange($event)\"\n />\n</label>\n", styles: [":host{display:block}.fk-file-dropzone__container{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--fk-file-dropzone-gap, var(--fk-rhythm-2, .5rem));min-height:var(--fk-file-dropzone-min-height, 10rem);padding:var(--fk-file-dropzone-padding, var(--fk-rhythm-10, 2.5rem));background:var(--fk-file-dropzone-bg, var(--fk-color-surface-raised, #f8fafc));border:var(--fk-file-dropzone-border-width, 2px) var(--fk-file-dropzone-border-style, dashed) var(--fk-file-dropzone-border-color, var(--fk-color-border, #d9e2ee));border-radius:var(--fk-file-dropzone-border-radius, var(--fk-radius-lg, .75rem));cursor:pointer;transition:border-color .15s ease,background-color .15s ease}.fk-file-dropzone__container:hover{border-color:var(--fk-file-dropzone-border-color-hover, var(--fk-color-primary, #0a84ff))}.fk-file-dropzone__container:focus-within{outline:none;box-shadow:var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18))}.fk-file-dropzone__prompt{font-weight:var(--fk-font-weight-semibold, 600);color:var(--fk-file-dropzone-prompt-color, var(--fk-color-text, #1f2d3d))}.fk-file-dropzone__hint{font-size:var(--fk-file-dropzone-hint-font-size, .8125rem)}.fk-file-dropzone__native{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}:host.fk-file-dropzone--dragging .fk-file-dropzone__container{border-color:var(--fk-file-dropzone-border-color-dragging, var(--fk-color-primary, #0a84ff));background:var(--fk-file-dropzone-bg-dragging, rgba(10, 132, 255, .04))}:host.fk-file-dropzone--disabled .fk-file-dropzone__container{cursor:not-allowed;opacity:var(--fk-file-dropzone-opacity-disabled, .6)}:host.fk-file-dropzone--disabled .fk-file-dropzone__container:hover{border-color:var(--fk-file-dropzone-border-color, var(--fk-color-border, #d9e2ee))}:host.fk-file-dropzone--disabled .fk-file-dropzone__icon{color:var(--fk-file-dropzone-icon-color-disabled, var(--fk-color-muted, #8a98a8))}\n"], dependencies: [{ kind: "component", type: IconComponent, selector: "fk-icon", inputs: ["name", "size", "color", "className", "id", "ariaLabel", "ariaHidden"] }, { 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 });
3507
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: FileDropzoneComponent, isStandalone: true, selector: "fk-file-dropzone", inputs: { accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, prompt: { classPropertyName: "prompt", publicName: "prompt", isSignal: true, isRequired: true, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", 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 } }, outputs: { filesSelected: "filesSelected" }, host: { properties: { "class": "this.hostClass" } }, ngImport: i0, template: "<label\n class=\"fk-file-dropzone__container\"\n [attr.id]=\"id() ?? undefined\"\n [attr.aria-label]=\"ariaLabel()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n <ng-content select=\"[fkFileDropzoneIcon]\">\n <svg\n class=\"fk-file-dropzone__icon\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 3v12M7 8l5-5 5 5M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </ng-content>\n\n <fk-text tone=\"muted\" class=\"fk-file-dropzone__prompt\">\n {{ prompt() }}\n </fk-text>\n\n @if (hint(); as h) {\n <fk-text tone=\"muted\" class=\"fk-file-dropzone__hint\">\n {{ h }}\n </fk-text>\n }\n\n <input\n #nativeInput\n type=\"file\"\n class=\"fk-file-dropzone__native\"\n [attr.accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n (change)=\"onChange($event)\"\n />\n</label>\n", styles: [":host{display:block}.fk-file-dropzone__container{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--fk-file-dropzone-gap, var(--fk-rhythm-2, .5rem));min-height:var(--fk-file-dropzone-min-height, 10rem);padding:var(--fk-file-dropzone-padding, var(--fk-rhythm-10, 2.5rem));background:var(--fk-file-dropzone-bg, var(--fk-color-surface-raised, #f8fafc));border:var(--fk-file-dropzone-border-width, 2px) var(--fk-file-dropzone-border-style, dashed) var(--fk-file-dropzone-border-color, var(--fk-color-border, #d9e2ee));border-radius:var(--fk-file-dropzone-border-radius, var(--fk-radius-lg, .75rem));cursor:pointer;transition:border-color .15s ease,background-color .15s ease}.fk-file-dropzone__container:hover{border-color:var(--fk-file-dropzone-border-color-hover, var(--fk-color-primary, #0a84ff))}.fk-file-dropzone__container:focus-within{outline:none;box-shadow:var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18))}.fk-file-dropzone__icon{width:var(--fk-file-dropzone-icon-size, 2rem);height:var(--fk-file-dropzone-icon-size, 2rem);color:var(--fk-file-dropzone-icon-color, var(--fk-color-muted, #8a98a8))}.fk-file-dropzone__prompt{font-weight:var(--fk-font-weight-semibold, 600);color:var(--fk-file-dropzone-prompt-color, var(--fk-color-text, #1f2d3d))}.fk-file-dropzone__hint{font-size:var(--fk-file-dropzone-hint-font-size, .8125rem)}.fk-file-dropzone__native{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}:host.fk-file-dropzone--dragging .fk-file-dropzone__container{border-color:var(--fk-file-dropzone-border-color-dragging, var(--fk-color-primary, #0a84ff));background:var(--fk-file-dropzone-bg-dragging, rgba(10, 132, 255, .04))}:host.fk-file-dropzone--disabled .fk-file-dropzone__container{cursor:not-allowed;opacity:var(--fk-file-dropzone-opacity-disabled, .6)}:host.fk-file-dropzone--disabled .fk-file-dropzone__container:hover{border-color:var(--fk-file-dropzone-border-color, var(--fk-color-border, #d9e2ee))}:host.fk-file-dropzone--disabled .fk-file-dropzone__icon{color:var(--fk-file-dropzone-icon-color-disabled, var(--fk-color-muted, #8a98a8))}\n"], dependencies: [{ 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 });
3497
3508
  }
3498
3509
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FileDropzoneComponent, decorators: [{
3499
3510
  type: Component,
3500
- args: [{ selector: 'fk-file-dropzone', standalone: true, imports: [IconComponent, TextComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<label\n class=\"fk-file-dropzone__container\"\n [attr.id]=\"id() ?? undefined\"\n [attr.aria-label]=\"ariaLabel()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n <fk-icon [name]=\"icon()\" size=\"lg\" class=\"fk-file-dropzone__icon\" />\n\n <fk-text tone=\"muted\" class=\"fk-file-dropzone__prompt\">\n {{ prompt() }}\n </fk-text>\n\n @if (hint(); as h) {\n <fk-text tone=\"muted\" class=\"fk-file-dropzone__hint\">\n {{ h }}\n </fk-text>\n }\n\n <input\n #nativeInput\n type=\"file\"\n class=\"fk-file-dropzone__native\"\n [attr.accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n (change)=\"onChange($event)\"\n />\n</label>\n", styles: [":host{display:block}.fk-file-dropzone__container{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--fk-file-dropzone-gap, var(--fk-rhythm-2, .5rem));min-height:var(--fk-file-dropzone-min-height, 10rem);padding:var(--fk-file-dropzone-padding, var(--fk-rhythm-10, 2.5rem));background:var(--fk-file-dropzone-bg, var(--fk-color-surface-raised, #f8fafc));border:var(--fk-file-dropzone-border-width, 2px) var(--fk-file-dropzone-border-style, dashed) var(--fk-file-dropzone-border-color, var(--fk-color-border, #d9e2ee));border-radius:var(--fk-file-dropzone-border-radius, var(--fk-radius-lg, .75rem));cursor:pointer;transition:border-color .15s ease,background-color .15s ease}.fk-file-dropzone__container:hover{border-color:var(--fk-file-dropzone-border-color-hover, var(--fk-color-primary, #0a84ff))}.fk-file-dropzone__container:focus-within{outline:none;box-shadow:var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18))}.fk-file-dropzone__prompt{font-weight:var(--fk-font-weight-semibold, 600);color:var(--fk-file-dropzone-prompt-color, var(--fk-color-text, #1f2d3d))}.fk-file-dropzone__hint{font-size:var(--fk-file-dropzone-hint-font-size, .8125rem)}.fk-file-dropzone__native{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}:host.fk-file-dropzone--dragging .fk-file-dropzone__container{border-color:var(--fk-file-dropzone-border-color-dragging, var(--fk-color-primary, #0a84ff));background:var(--fk-file-dropzone-bg-dragging, rgba(10, 132, 255, .04))}:host.fk-file-dropzone--disabled .fk-file-dropzone__container{cursor:not-allowed;opacity:var(--fk-file-dropzone-opacity-disabled, .6)}:host.fk-file-dropzone--disabled .fk-file-dropzone__container:hover{border-color:var(--fk-file-dropzone-border-color, var(--fk-color-border, #d9e2ee))}:host.fk-file-dropzone--disabled .fk-file-dropzone__icon{color:var(--fk-file-dropzone-icon-color-disabled, var(--fk-color-muted, #8a98a8))}\n"] }]
3501
- }], propDecorators: { accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], prompt: [{ type: i0.Input, args: [{ isSignal: true, alias: "prompt", required: true }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", 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 }] }], filesSelected: [{ type: i0.Output, args: ["filesSelected"] }], hostClass: [{
3511
+ args: [{ selector: 'fk-file-dropzone', standalone: true, imports: [TextComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<label\n class=\"fk-file-dropzone__container\"\n [attr.id]=\"id() ?? undefined\"\n [attr.aria-label]=\"ariaLabel()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n <ng-content select=\"[fkFileDropzoneIcon]\">\n <svg\n class=\"fk-file-dropzone__icon\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 3v12M7 8l5-5 5 5M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </ng-content>\n\n <fk-text tone=\"muted\" class=\"fk-file-dropzone__prompt\">\n {{ prompt() }}\n </fk-text>\n\n @if (hint(); as h) {\n <fk-text tone=\"muted\" class=\"fk-file-dropzone__hint\">\n {{ h }}\n </fk-text>\n }\n\n <input\n #nativeInput\n type=\"file\"\n class=\"fk-file-dropzone__native\"\n [attr.accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n (change)=\"onChange($event)\"\n />\n</label>\n", styles: [":host{display:block}.fk-file-dropzone__container{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--fk-file-dropzone-gap, var(--fk-rhythm-2, .5rem));min-height:var(--fk-file-dropzone-min-height, 10rem);padding:var(--fk-file-dropzone-padding, var(--fk-rhythm-10, 2.5rem));background:var(--fk-file-dropzone-bg, var(--fk-color-surface-raised, #f8fafc));border:var(--fk-file-dropzone-border-width, 2px) var(--fk-file-dropzone-border-style, dashed) var(--fk-file-dropzone-border-color, var(--fk-color-border, #d9e2ee));border-radius:var(--fk-file-dropzone-border-radius, var(--fk-radius-lg, .75rem));cursor:pointer;transition:border-color .15s ease,background-color .15s ease}.fk-file-dropzone__container:hover{border-color:var(--fk-file-dropzone-border-color-hover, var(--fk-color-primary, #0a84ff))}.fk-file-dropzone__container:focus-within{outline:none;box-shadow:var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18))}.fk-file-dropzone__icon{width:var(--fk-file-dropzone-icon-size, 2rem);height:var(--fk-file-dropzone-icon-size, 2rem);color:var(--fk-file-dropzone-icon-color, var(--fk-color-muted, #8a98a8))}.fk-file-dropzone__prompt{font-weight:var(--fk-font-weight-semibold, 600);color:var(--fk-file-dropzone-prompt-color, var(--fk-color-text, #1f2d3d))}.fk-file-dropzone__hint{font-size:var(--fk-file-dropzone-hint-font-size, .8125rem)}.fk-file-dropzone__native{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}:host.fk-file-dropzone--dragging .fk-file-dropzone__container{border-color:var(--fk-file-dropzone-border-color-dragging, var(--fk-color-primary, #0a84ff));background:var(--fk-file-dropzone-bg-dragging, rgba(10, 132, 255, .04))}:host.fk-file-dropzone--disabled .fk-file-dropzone__container{cursor:not-allowed;opacity:var(--fk-file-dropzone-opacity-disabled, .6)}:host.fk-file-dropzone--disabled .fk-file-dropzone__container:hover{border-color:var(--fk-file-dropzone-border-color, var(--fk-color-border, #d9e2ee))}:host.fk-file-dropzone--disabled .fk-file-dropzone__icon{color:var(--fk-file-dropzone-icon-color-disabled, var(--fk-color-muted, #8a98a8))}\n"] }]
3512
+ }], propDecorators: { accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], prompt: [{ type: i0.Input, args: [{ isSignal: true, alias: "prompt", required: true }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", 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 }] }], filesSelected: [{ type: i0.Output, args: ["filesSelected"] }], hostClass: [{
3502
3513
  type: HostBinding,
3503
3514
  args: ['class']
3504
3515
  }] } });
3505
3516
 
3517
+ /** Injection token for the icon slot of `fk-file-dropzone`. */
3518
+ const FK_FILE_DROPZONE_ICON = new InjectionToken('FK_FILE_DROPZONE_ICON');
3519
+ /**
3520
+ * Marks projected content as the icon of an `fk-file-dropzone`, overriding the
3521
+ * built-in upload glyph. Project any icon element (the app owns the icon
3522
+ * system):
3523
+ *
3524
+ * ```html
3525
+ * <fk-file-dropzone prompt="Drop a file">
3526
+ * <fk-icon fkFileDropzoneIcon name="file-content" size="lg" />
3527
+ * </fk-file-dropzone>
3528
+ * ```
3529
+ */
3530
+ class FkFileDropzoneIconDirective {
3531
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FkFileDropzoneIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3532
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: FkFileDropzoneIconDirective, isStandalone: true, selector: "[fkFileDropzoneIcon]", host: { attributes: { "aria-hidden": "true" } }, providers: [
3533
+ {
3534
+ provide: FK_FILE_DROPZONE_ICON,
3535
+ useExisting: FkFileDropzoneIconDirective,
3536
+ },
3537
+ ], ngImport: i0 });
3538
+ }
3539
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FkFileDropzoneIconDirective, decorators: [{
3540
+ type: Directive,
3541
+ args: [{
3542
+ selector: '[fkFileDropzoneIcon]',
3543
+ standalone: true,
3544
+ providers: [
3545
+ {
3546
+ provide: FK_FILE_DROPZONE_ICON,
3547
+ useExisting: FkFileDropzoneIconDirective,
3548
+ },
3549
+ ],
3550
+ host: {
3551
+ 'aria-hidden': 'true',
3552
+ },
3553
+ }]
3554
+ }] });
3555
+
3506
3556
  class SegmentedBarComponent {
3507
3557
  // ===== INPUTS =====
3508
3558
  /** Segments to render, each with a value, optional label, and color. */
@@ -3628,5 +3678,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
3628
3678
  * Generated bundle index. Do not edit.
3629
3679
  */
3630
3680
 
3631
- export { AutocompleteComponent, CheckboxComponent, DEFAULT_ERROR_MESSAGES, DateInputComponent, DatetimeInputComponent, ERROR_MESSAGES, ErrorComponent, FIELD_CONTROL, FK_AUTOCOMPLETE_OPTION_TPL, FK_PREFIX, FK_SUFFIX, FORM, FieldArrayOutletComponent, FieldGroupComponent, FieldOutletComponent, FieldRegistry, FileDropzoneComponent, FileUploadComponent, FkAutocompleteOptionTplDirective, FkPrefixDirective, FkSuffixDirective, FormComponent, FormFieldComponent, FormRendererComponent, FormSectionComponent, HintComponent, InputComponent, LabelComponent, NumberInputComponent, PasswordInputComponent, RadioComponent, SchemaParser, SectionOutletComponent, SegmentedBarComponent, SelectComponent, SliderComponent, SwitchComponent, TagInputComponent, TextareaComponent, TimeInputComponent, ValidationRegistry, getError, getFieldState, getInvalidFields, hasError, interpolateMessage, isFormValid, markAllTouched, resolveErrorMessage };
3681
+ export { AutocompleteComponent, CheckboxComponent, DEFAULT_ERROR_MESSAGES, DateInputComponent, DatetimeInputComponent, ERROR_MESSAGES, ErrorComponent, FIELD_CONTROL, FK_AUTOCOMPLETE_OPTION_TPL, FK_FILE_DROPZONE_ICON, FK_PREFIX, FK_SUFFIX, FORM, FieldArrayOutletComponent, FieldGroupComponent, FieldOutletComponent, FieldRegistry, FileDropzoneComponent, FileUploadComponent, FkAutocompleteOptionTplDirective, FkFileDropzoneIconDirective, FkPrefixDirective, FkSuffixDirective, FormComponent, FormFieldComponent, FormRendererComponent, FormSectionComponent, HintComponent, InputComponent, LabelComponent, NumberInputComponent, PasswordInputComponent, RadioComponent, SchemaParser, SectionOutletComponent, SegmentedBarComponent, SelectComponent, SliderComponent, SwitchComponent, TagInputComponent, TextareaComponent, TimeInputComponent, ValidationRegistry, getError, getFieldState, getInvalidFields, hasError, interpolateMessage, isFormValid, markAllTouched, resolveErrorMessage };
3632
3682
  //# sourceMappingURL=frame-kit-ui-ng-forms.mjs.map