@kouji-ui/core 0.1.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, booleanAttribute, Directive, inject, ElementRef, DestroyRef, PLATFORM_ID, signal, afterNextRender, forwardRef, InjectionToken, Injectable, computed, viewChild, ViewContainerRef, ViewEncapsulation, ChangeDetectionStrategy, Component, ApplicationRef, EnvironmentInjector, Injector, createComponent, model, effect, untracked, isSignal, DOCUMENT, output, contentChildren, isDevMode, LOCALE_ID, numberAttribute, contentChild, makeEnvironmentProviders, runInInjectionContext, HostListener, linkedSignal, TemplateRef, resource, EventEmitter, Output, afterEveryRender } from '@angular/core';
2
+ import { input, booleanAttribute, Directive, inject, ElementRef, DestroyRef, PLATFORM_ID, signal, afterNextRender, forwardRef, InjectionToken, Injectable, computed, viewChild, ViewContainerRef, ViewEncapsulation, ChangeDetectionStrategy, Component, ApplicationRef, EnvironmentInjector, Injector, createComponent, model, effect, untracked, isSignal, DOCUMENT, output, contentChildren, makeEnvironmentProviders, LOCALE_ID, provideEnvironmentInitializer, isDevMode, numberAttribute, contentChild, runInInjectionContext, HostListener, linkedSignal, TemplateRef, resource, EventEmitter, Output, afterEveryRender } from '@angular/core';
3
3
  import { isPlatformBrowser, DOCUMENT as DOCUMENT$1 } from '@angular/common';
4
4
  import { NG_VALUE_ACCESSOR, NG_VALIDATORS, NgForm, FormGroupDirective, FormGroup, FormArray } from '@angular/forms';
5
5
  import { Subject } from 'rxjs';
@@ -3773,6 +3773,626 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
3773
3773
  }]
3774
3774
  }] });
3775
3775
 
3776
+ /**
3777
+ * DI token for the initial locale configuration. Default factory yields an
3778
+ * empty config, so {@link KjLocale} resolves entirely from `LOCALE_ID` and
3779
+ * `<html dir>` when {@link provideKjLocale} is not called.
3780
+ */
3781
+ const KJ_LOCALE_CONFIG = new InjectionToken('kj.locale.config', { factory: () => ({}) });
3782
+ /**
3783
+ * Configures the application (or route) locale — the single source of truth
3784
+ * every locale-sensitive kouji-ui primitive falls back to for number, currency,
3785
+ * and date formatting plus logical text direction.
3786
+ *
3787
+ * Call once at the application scope (`bootstrapApplication`'s `providers`) or
3788
+ * on a route to scope a sub-tree. Runtime changes go through the
3789
+ * {@link KjLocale} service (`setLocale` / `setDirection` / `setCurrency`) — the
3790
+ * seam the upcoming RTL switch and language menu build on.
3791
+ *
3792
+ * @example
3793
+ * ```ts
3794
+ * bootstrapApplication(App, {
3795
+ * providers: [provideKjLocale({ locale: 'de-DE', currency: 'EUR' })],
3796
+ * });
3797
+ * ```
3798
+ * @doc
3799
+ * @doc-example Basic
3800
+ * Switch the active locale and watch every `Intl`-backed formatter (number,
3801
+ * currency, date) and the resolved direction update reactively.
3802
+ * @doc-file locale.basic.example.ts
3803
+ * @doc-name locale
3804
+ * @doc-order 1
3805
+ */
3806
+ function provideKjLocale(config = {}) {
3807
+ return makeEnvironmentProviders([
3808
+ { provide: KJ_LOCALE_CONFIG, useValue: config },
3809
+ ]);
3810
+ }
3811
+
3812
+ /**
3813
+ * Minimal RTL-script allow-list for engines whose `Intl.Locale` lacks
3814
+ * `getTextInfo` / `textInfo`. Keyed by the ISO 639 language subtag.
3815
+ */
3816
+ const RTL_LANGUAGES = new Set([
3817
+ 'ar', // Arabic
3818
+ 'he', // Hebrew
3819
+ 'fa', // Persian
3820
+ 'ur', // Urdu
3821
+ 'ps', // Pashto
3822
+ 'sd', // Sindhi
3823
+ 'ug', // Uyghur
3824
+ 'yi', // Yiddish
3825
+ 'dv', // Divehi
3826
+ 'ku', // Kurdish (Sorani)
3827
+ ]);
3828
+ /**
3829
+ * Derive `'ltr'` / `'rtl'` from a BCP-47 tag. Prefers the standard
3830
+ * `Intl.Locale` text-info APIs; falls back to a language-subtag allow-list.
3831
+ */
3832
+ function directionFromLocale(tag) {
3833
+ try {
3834
+ const loc = new Intl.Locale(tag);
3835
+ // `getTextInfo()` (newer) and `.textInfo` (older) both expose `.direction`.
3836
+ const info = loc.getTextInfo?.() ??
3837
+ loc.textInfo;
3838
+ if (info?.direction === 'rtl')
3839
+ return 'rtl';
3840
+ if (info?.direction === 'ltr')
3841
+ return 'ltr';
3842
+ const language = loc.language ?? tag.toLowerCase().split('-')[0];
3843
+ return RTL_LANGUAGES.has(language) ? 'rtl' : 'ltr';
3844
+ }
3845
+ catch {
3846
+ const language = tag.toLowerCase().split('-')[0];
3847
+ return RTL_LANGUAGES.has(language) ? 'rtl' : 'ltr';
3848
+ }
3849
+ }
3850
+ /**
3851
+ * Application-wide source of truth for **how locale-sensitive data renders** —
3852
+ * number / currency / date formatting and the logical text direction
3853
+ * (`ltr` / `rtl`). Every locale-aware primitive (NumberInput, DatePicker,
3854
+ * TimePicker, Calendar, currency display) falls back to this service instead of
3855
+ * re-reading `LOCALE_ID` or drilling a `kjLocale` prop.
3856
+ *
3857
+ * Configure the initial state with {@link provideKjLocale}; change it at runtime
3858
+ * with {@link setLocale} / {@link setDirection} / {@link setCurrency} — the seam
3859
+ * the RTL switch and language menu build on. Everything is `Intl`-backed and
3860
+ * SSR-safe, so anything Angular's `LOCALE_ID` already supports works with zero
3861
+ * configuration.
3862
+ *
3863
+ * @example
3864
+ * ```ts
3865
+ * private readonly locale = inject(KjLocale);
3866
+ * readonly price = computed(() => this.locale.formatCurrency(19.9)); // '€19.90'
3867
+ * readonly isRtl = this.locale.isRtl;
3868
+ * ```
3869
+ * @doc
3870
+ * @doc-name locale
3871
+ * @doc-category Core/Primitives
3872
+ * @doc-description One DI provider for locale-aware number, currency, and date formatting plus the ltr/rtl direction every primitive falls back to.
3873
+ * @doc-is-main
3874
+ */
3875
+ class KjLocale {
3876
+ defaultLocale = inject(LOCALE_ID);
3877
+ directionality = inject(KjDirectionality);
3878
+ config = inject(KJ_LOCALE_CONFIG);
3879
+ _locale = signal(this.config.locale ?? null, /* @ts-ignore */
3880
+ ...(ngDevMode ? [{ debugName: "_locale" }] : /* istanbul ignore next */ []));
3881
+ _direction = signal(this.config.direction ?? 'auto', /* @ts-ignore */
3882
+ ...(ngDevMode ? [{ debugName: "_direction" }] : /* istanbul ignore next */ []));
3883
+ _currency = signal(this.config.currency, /* @ts-ignore */
3884
+ ...(ngDevMode ? [{ debugName: "_currency" }] : /* istanbul ignore next */ []));
3885
+ /** Resolved BCP-47 locale tag. Falls back to Angular's `LOCALE_ID`. */
3886
+ locale = computed(() => this._locale() || this.defaultLocale, /* @ts-ignore */
3887
+ ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
3888
+ /**
3889
+ * Resolved logical text direction. When set to `'auto'`, derives from the
3890
+ * locale script, then falls back to the document's `<html dir>`.
3891
+ */
3892
+ direction = computed(() => {
3893
+ const explicit = this._direction();
3894
+ if (explicit === 'ltr' || explicit === 'rtl')
3895
+ return explicit;
3896
+ const fromLocale = directionFromLocale(this.locale());
3897
+ // Only honour a locale that positively resolves to RTL; otherwise defer to
3898
+ // the document direction so an app that sets `<html dir="rtl">` still wins
3899
+ // for a direction-neutral locale.
3900
+ if (fromLocale === 'rtl')
3901
+ return 'rtl';
3902
+ return this.directionality.current();
3903
+ }, /* @ts-ignore */
3904
+ ...(ngDevMode ? [{ debugName: "direction" }] : /* istanbul ignore next */ []));
3905
+ /** `true` when the resolved {@link direction} is `'rtl'`. */
3906
+ isRtl = computed(() => this.direction() === 'rtl', /* @ts-ignore */
3907
+ ...(ngDevMode ? [{ debugName: "isRtl" }] : /* istanbul ignore next */ []));
3908
+ /** Resolved default currency (ISO 4217), or `undefined`. */
3909
+ currency = this._currency.asReadonly();
3910
+ /** Override the active locale at runtime. */
3911
+ setLocale(tag) {
3912
+ this._locale.set(tag);
3913
+ }
3914
+ /** Override the active direction at runtime. `'auto'` re-enables derivation. */
3915
+ setDirection(dir) {
3916
+ this._direction.set(dir);
3917
+ }
3918
+ /** Override the default currency at runtime. */
3919
+ setCurrency(code) {
3920
+ this._currency.set(code);
3921
+ }
3922
+ /**
3923
+ * Build an `Intl.NumberFormat` bound to the resolved locale. Pass options to
3924
+ * override; `undefined` locale in options is ignored.
3925
+ */
3926
+ numberFormat(options) {
3927
+ return new Intl.NumberFormat(this.locale(), options);
3928
+ }
3929
+ /** Build an `Intl.DateTimeFormat` bound to the resolved locale. */
3930
+ dateTimeFormat(options) {
3931
+ return new Intl.DateTimeFormat(this.locale(), options);
3932
+ }
3933
+ /** Format a number with the resolved locale. */
3934
+ formatNumber(value, options) {
3935
+ return this.numberFormat(options).format(value);
3936
+ }
3937
+ /**
3938
+ * Format a currency amount. Uses `currency` when given, else the provider's
3939
+ * default {@link currency}. Returns a plain number format when neither is set.
3940
+ */
3941
+ formatCurrency(value, currency, options) {
3942
+ const code = currency ?? this._currency();
3943
+ if (!code)
3944
+ return this.formatNumber(value, options);
3945
+ return this.numberFormat({
3946
+ style: 'currency',
3947
+ currency: code,
3948
+ ...options,
3949
+ }).format(value);
3950
+ }
3951
+ /** Format a `Date` with the resolved locale. */
3952
+ formatDate(value, options) {
3953
+ return this.dateTimeFormat(options).format(value);
3954
+ }
3955
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjLocale, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3956
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjLocale, providedIn: 'root' });
3957
+ }
3958
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjLocale, decorators: [{
3959
+ type: Injectable,
3960
+ args: [{ providedIn: 'root' }]
3961
+ }] });
3962
+
3963
+ /**
3964
+ * Reflects {@link KjLocale}'s resolved logical direction onto the document's
3965
+ * `<html dir>` attribute, keeping the whole page (and every assistive
3966
+ * technology) in sync whenever the direction changes at runtime.
3967
+ *
3968
+ * This is the single writer of `<html dir>`; {@link KjDirectionality} stays the
3969
+ * *reader* that feeds `KjLocale`'s `'auto'` derivation. The write is idempotent
3970
+ * (skipped when the attribute already matches), so it never fights an app that
3971
+ * sets `dir` itself, and it is **SSR-safe** — on the server no DOM APIs are
3972
+ * touched and the attribute is left to the app's own template.
3973
+ *
3974
+ * Register once at the application scope. It is the piece the visible RTL
3975
+ * toggle (`KjDirectionToggle`) relies on to actually flip the layout: the toggle
3976
+ * calls `KjLocale.setDirection(...)`, this effect propagates it to `<html dir>`.
3977
+ *
3978
+ * @example
3979
+ * ```ts
3980
+ * bootstrapApplication(App, {
3981
+ * providers: [
3982
+ * provideKjLocale({ direction: 'auto' }),
3983
+ * provideKjDocumentDirection(),
3984
+ * ],
3985
+ * });
3986
+ * ```
3987
+ * @doc
3988
+ * @doc-name locale
3989
+ * @doc-order 2
3990
+ */
3991
+ function provideKjDocumentDirection() {
3992
+ return makeEnvironmentProviders([
3993
+ provideEnvironmentInitializer(() => {
3994
+ const platformId = inject(PLATFORM_ID);
3995
+ const doc = inject(DOCUMENT, { optional: true });
3996
+ // SSR / no-DOM: leave `<html dir>` to the app's template.
3997
+ if (!isPlatformBrowser(platformId) || !doc)
3998
+ return;
3999
+ const locale = inject(KjLocale);
4000
+ effect(() => {
4001
+ const dir = locale.direction();
4002
+ const html = doc.documentElement;
4003
+ if (html.getAttribute('dir') !== dir) {
4004
+ html.setAttribute('dir', dir);
4005
+ }
4006
+ });
4007
+ }),
4008
+ ]);
4009
+ }
4010
+
4011
+ /**
4012
+ * Media query that matches when the user has asked the OS to reduce motion.
4013
+ */
4014
+ const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';
4015
+ /**
4016
+ * Reads the user's `prefers-reduced-motion` OS setting via `matchMedia` and
4017
+ * exposes it as a signal that updates live when the setting flips. SSR-safe —
4018
+ * on the server (or where `matchMedia` is unavailable) the signal returns
4019
+ * `false` and no DOM APIs are touched.
4020
+ *
4021
+ * Pair this with the `motion.css` presets (which already no-op under reduced
4022
+ * motion in pure CSS) whenever a directive needs the value in TypeScript — e.g.
4023
+ * to shorten a JS-driven timeout, skip an imperative animation, or await
4024
+ * `animationend` only when motion is actually running.
4025
+ *
4026
+ * @example
4027
+ * ```ts
4028
+ * private readonly motion = inject(KjReducedMotion);
4029
+ * readonly animate = computed(() => !this.motion.prefersReducedMotion());
4030
+ * ```
4031
+ * @doc-category Core/Primitives
4032
+ * @doc-name reduced-motion
4033
+ * @doc-description SSR-safe signal of the user's prefers-reduced-motion setting.
4034
+ */
4035
+ class KjReducedMotion {
4036
+ platformId = inject(PLATFORM_ID);
4037
+ destroyRef = inject(DestroyRef);
4038
+ _prefersReducedMotion = signal(false, /* @ts-ignore */
4039
+ ...(ngDevMode ? [{ debugName: "_prefersReducedMotion" }] : /* istanbul ignore next */ []));
4040
+ /**
4041
+ * `true` when the user has requested reduced motion. `false` on the server
4042
+ * and as the fallback when `matchMedia` is unavailable.
4043
+ */
4044
+ prefersReducedMotion = this._prefersReducedMotion.asReadonly();
4045
+ constructor() {
4046
+ if (!isPlatformBrowser(this.platformId) || typeof window === 'undefined' || !window.matchMedia) {
4047
+ // SSR / no matchMedia: keep the default `false` and skip all DOM access.
4048
+ return;
4049
+ }
4050
+ // Read the initial value once a browser context is guaranteed, then track
4051
+ // changes. afterNextRender avoids reading during SSR.
4052
+ afterNextRender(() => {
4053
+ const mql = window.matchMedia(REDUCED_MOTION_QUERY);
4054
+ this._prefersReducedMotion.set(mql.matches);
4055
+ const onChange = (event) => {
4056
+ this._prefersReducedMotion.set(event.matches);
4057
+ };
4058
+ mql.addEventListener('change', onChange);
4059
+ this.destroyRef.onDestroy(() => mql.removeEventListener('change', onChange));
4060
+ });
4061
+ }
4062
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjReducedMotion, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4063
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjReducedMotion, providedIn: 'root' });
4064
+ }
4065
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjReducedMotion, decorators: [{
4066
+ type: Injectable,
4067
+ args: [{ providedIn: 'root' }]
4068
+ }], ctorParameters: () => [] });
4069
+
4070
+ /**
4071
+ * Applies a named motion preset from `motion.css` to its host element. The
4072
+ * animation itself lives entirely in CSS (keyed off the reflected
4073
+ * `data-kj-motion` / `data-kj-motion-state` attributes and the `--kj-motion-*`
4074
+ * custom properties); this directive is a thin, declarative opt-in.
4075
+ *
4076
+ * Presets are composable, pre-bundled names — `fade`, `slide-up`, `slide-down`,
4077
+ * `slide-left`, `slide-right`, `scale`, `slide-up-fade`, `scale-spring`. Under
4078
+ * `prefers-reduced-motion: reduce` every preset collapses to a ~1ms opacity
4079
+ * fade with no transform (WCAG 2.1 AAA 2.3.3), so consumers never have to
4080
+ * branch on the setting for the visual result. The `reduced()` signal is
4081
+ * exposed for the rare case that needs to gate JS-driven timing.
4082
+ *
4083
+ * Requires `@kouji-ui/core/motion/motion.css` to be loaded (globally or in the
4084
+ * component's styles).
4085
+ *
4086
+ * @example
4087
+ * ```html
4088
+ * <div kjMotion="slide-up-fade" [kjMotionState]="open() ? 'enter' : 'exit'">…</div>
4089
+ * ```
4090
+ *
4091
+ * @doc-aria
4092
+ * data-kj-motion — reflects the active preset name for CSS targeting
4093
+ * data-kj-motion-state — "enter" | "exit"
4094
+ * data-kj-reduced-motion — present when the user prefers reduced motion
4095
+ *
4096
+ * @doc-a11y
4097
+ * Motion is decorative and opt-in; the directive adds no interactive
4098
+ * semantics (no role, no tabindex). Every preset honours
4099
+ * prefers-reduced-motion by collapsing to a near-instant opacity fade with no
4100
+ * transform, satisfying WCAG 2.1 AAA 2.3.3 (Animation from Interactions).
4101
+ *
4102
+ * @doc
4103
+ * @doc-example Presets
4104
+ * @doc-file motion.example.ts
4105
+ * @doc-example Reduced motion
4106
+ * @doc-file motion.reduced.example.ts
4107
+ * @doc-category Core/Primitives
4108
+ * @doc-name motion
4109
+ * @doc-is-main
4110
+ * @doc-description Applies a named, reduced-motion-aware CSS motion preset to any element.
4111
+ */
4112
+ class KjMotion {
4113
+ motion = inject(KjReducedMotion);
4114
+ /** Named preset to apply, e.g. `'fade'`, `'slide-up-fade'`, `'scale-spring'`. */
4115
+ kjMotion = input.required(/* @ts-ignore */
4116
+ ...(ngDevMode ? [{ debugName: "kjMotion" }] : /* istanbul ignore next */ []));
4117
+ /** Whether to play the entrance or exit keyframe. Defaults to `'enter'`. */
4118
+ kjMotionState = input('enter', /* @ts-ignore */
4119
+ ...(ngDevMode ? [{ debugName: "kjMotionState" }] : /* istanbul ignore next */ []));
4120
+ /** `true` when the user prefers reduced motion. Mirrors `KjReducedMotion`. */
4121
+ reduced = computed(() => this.motion.prefersReducedMotion(), /* @ts-ignore */
4122
+ ...(ngDevMode ? [{ debugName: "reduced" }] : /* istanbul ignore next */ []));
4123
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjMotion, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4124
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: KjMotion, isStandalone: true, selector: "[kjMotion]", inputs: { kjMotion: { classPropertyName: "kjMotion", publicName: "kjMotion", isSignal: true, isRequired: true, transformFunction: null }, kjMotionState: { classPropertyName: "kjMotionState", publicName: "kjMotionState", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-kj-motion": "kjMotion()", "attr.data-kj-motion-state": "kjMotionState()", "attr.data-kj-reduced-motion": "reduced() ? \"\" : null" }, classAttribute: "kj-motion" }, ngImport: i0 });
4125
+ }
4126
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjMotion, decorators: [{
4127
+ type: Directive,
4128
+ args: [{
4129
+ selector: '[kjMotion]',
4130
+ standalone: true,
4131
+ host: {
4132
+ class: 'kj-motion',
4133
+ '[attr.data-kj-motion]': 'kjMotion()',
4134
+ '[attr.data-kj-motion-state]': 'kjMotionState()',
4135
+ '[attr.data-kj-reduced-motion]': 'reduced() ? "" : null',
4136
+ },
4137
+ }]
4138
+ }], propDecorators: { kjMotion: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjMotion", required: true }] }], kjMotionState: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjMotionState", required: false }] }] } });
4139
+
4140
+ /**
4141
+ * Canonical English (`en`) message catalog — the **source of truth** for
4142
+ * kouji-ui's visible / assistive-text strings. Every translation key the
4143
+ * library understands is spelled exactly once here; the {@link KjTranslationKey}
4144
+ * union and the {@link KjTranslationCatalog} shape are derived from it, so a
4145
+ * typo in any alternate catalog is a compile error and no key can be forgotten.
4146
+ *
4147
+ * Values may contain `{name}` placeholders — see {@link KjTranslationParams} —
4148
+ * which {@link KjTranslateService.translate} substitutes at lookup time.
4149
+ */
4150
+ const EN_CATALOG = {
4151
+ // -- Overlays --
4152
+ 'toast.close': 'Close notification',
4153
+ 'dialog.close': 'Close dialog',
4154
+ // -- Pagination --
4155
+ 'pagination.nav': 'Pagination',
4156
+ 'pagination.previous': 'Previous page',
4157
+ 'pagination.next': 'Next page',
4158
+ 'pagination.first': 'First page',
4159
+ 'pagination.last': 'Last page',
4160
+ 'pagination.more': 'More pages',
4161
+ 'pagination.page': 'Page {page}',
4162
+ 'pagination.pageOf': 'Page {page} of {total}',
4163
+ // -- Accessibility live-region announcements --
4164
+ 'a11y.pageChanged': 'Page {page} of {total}',
4165
+ 'a11y.selected': 'Selected',
4166
+ 'a11y.sortApplied': 'Sort applied, {rows} rows',
4167
+ };
4168
+
4169
+ /**
4170
+ * French (`fr`) message catalog — shipped as proof that alternate locales plug
4171
+ * in. Typed as `Partial<KjTranslationCatalog>`: a translator may omit keys and
4172
+ * each missing one falls through to the English source at lookup time. The key
4173
+ * union is derived from `en`, so a misspelled key here fails `tsc`.
4174
+ *
4175
+ * Import it explicitly and register with `provideKjTranslations({ fr: FR_CATALOG })`
4176
+ * — because it is a plain module, bundlers tree-shake it away when unused.
4177
+ */
4178
+ const FR_CATALOG = {
4179
+ // -- Overlays --
4180
+ 'toast.close': 'Fermer la notification',
4181
+ 'dialog.close': 'Fermer la boîte de dialogue',
4182
+ // -- Pagination --
4183
+ 'pagination.nav': 'Pagination',
4184
+ 'pagination.previous': 'Page précédente',
4185
+ 'pagination.next': 'Page suivante',
4186
+ 'pagination.first': 'Première page',
4187
+ 'pagination.last': 'Dernière page',
4188
+ 'pagination.more': 'Plus de pages',
4189
+ 'pagination.page': 'Page {page}',
4190
+ 'pagination.pageOf': 'Page {page} sur {total}',
4191
+ // -- Accessibility live-region announcements --
4192
+ 'a11y.pageChanged': 'Page {page} sur {total}',
4193
+ 'a11y.selected': 'Sélectionné',
4194
+ 'a11y.sortApplied': 'Tri appliqué, {rows} lignes',
4195
+ };
4196
+
4197
+ /**
4198
+ * Multi-provider DI token holding every registered {@link KjTranslationCatalogs}
4199
+ * group. {@link KjTranslateService} reads it once at construction and merges the
4200
+ * groups over the always-present English source.
4201
+ */
4202
+ const KJ_TRANSLATION_CATALOGS = new InjectionToken('kj.translation.catalogs');
4203
+ /**
4204
+ * Registers one or more alternate message catalogs for the enclosing injector.
4205
+ * The English (`en`) catalog is always available without registration, so this
4206
+ * only adds the languages you ship. Composes — several calls accumulate.
4207
+ *
4208
+ * Because catalogs are plain `import`-able modules, only the languages you
4209
+ * actually import are bundled (tree-shakable).
4210
+ *
4211
+ * @example
4212
+ * ```ts
4213
+ * import { FR_CATALOG, provideKjTranslations, provideKjLocale } from '@kouji-ui/core';
4214
+ *
4215
+ * bootstrapApplication(App, {
4216
+ * providers: [
4217
+ * provideKjLocale({ locale: 'fr-FR' }), // selects the catalog at runtime
4218
+ * provideKjTranslations({ fr: FR_CATALOG }),
4219
+ * ],
4220
+ * });
4221
+ * ```
4222
+ * @doc
4223
+ * @doc-name i18n
4224
+ * @doc-order 1
4225
+ */
4226
+ function provideKjTranslations(catalogs) {
4227
+ return makeEnvironmentProviders([
4228
+ { provide: KJ_TRANSLATION_CATALOGS, useValue: catalogs, multi: true },
4229
+ ]);
4230
+ }
4231
+
4232
+ /** Replace every `{name}` token in `template` with `params[name]`. */
4233
+ function interpolate(template, params) {
4234
+ if (!params)
4235
+ return template;
4236
+ return template.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match);
4237
+ }
4238
+ /**
4239
+ * Resolves kouji-ui's visible / assistive-text strings from **typed, per-locale
4240
+ * catalogs**, selecting the active catalog from the locale that already exists —
4241
+ * {@link KjLocale.locale} — and guaranteeing a fallback to the English source so
4242
+ * the UI never blanks out on a missing key.
4243
+ *
4244
+ * Selection for a key resolves in order: exact locale tag (`fr-FR`) → bare
4245
+ * language subtag (`fr`) → `en`; within the chosen catalog a missing key falls
4246
+ * through to the English value, then to the key string itself. Values may carry
4247
+ * `{name}` placeholders substituted from `params`.
4248
+ *
4249
+ * Register alternate catalogs with {@link provideKjTranslations}; switch locale
4250
+ * at runtime with `KjLocale.setLocale()` — lookups are reactive, so
4251
+ * {@link translation} signals and the {@link KjTranslate} directive re-render.
4252
+ *
4253
+ * @example
4254
+ * ```ts
4255
+ * private readonly i18n = inject(KjTranslateService);
4256
+ * readonly closeLabel = this.i18n.translation('toast.close'); // Signal<string>
4257
+ * readonly info = computed(() =>
4258
+ * this.i18n.translate('pagination.pageOf', { page: 3, total: 12 }));
4259
+ * ```
4260
+ * @doc
4261
+ * @doc-name i18n
4262
+ * @doc-is-main
4263
+ * @doc-category Core/Accessibility
4264
+ * @doc-description Resolves visible and ARIA strings from typed, tree-shakable per-locale catalogs, selected by KjLocale with an English fallback.
4265
+ */
4266
+ class KjTranslateService {
4267
+ locale = inject(KjLocale);
4268
+ catalogs = new Map();
4269
+ constructor() {
4270
+ // English is always present as the source-language fallback.
4271
+ this.catalogs.set('en', EN_CATALOG);
4272
+ const groups = inject(KJ_TRANSLATION_CATALOGS, { optional: true }) ?? [];
4273
+ for (const group of groups) {
4274
+ for (const [tag, catalog] of Object.entries(group)) {
4275
+ this.register(tag, catalog);
4276
+ }
4277
+ }
4278
+ }
4279
+ /**
4280
+ * Register (or extend) a catalog at runtime. Merges over any catalog already
4281
+ * registered for the same tag. Locale tags are matched case-insensitively.
4282
+ * @param locale - BCP-47 tag or bare language subtag (e.g. `'fr'`, `'fr-CA'`).
4283
+ * @param catalog - Partial catalog; omitted keys fall through to English.
4284
+ */
4285
+ register(locale, catalog) {
4286
+ const key = locale.toLowerCase();
4287
+ const existing = this.catalogs.get(key);
4288
+ this.catalogs.set(key, existing ? { ...existing, ...catalog } : catalog);
4289
+ }
4290
+ /**
4291
+ * Look up a key for the active locale and interpolate `params`. Reads
4292
+ * {@link KjLocale.locale}, so call it inside a `computed`/`effect` (or use
4293
+ * {@link translation}) to react to locale changes.
4294
+ */
4295
+ translate(key, params) {
4296
+ const catalog = this.selectCatalog(this.locale.locale());
4297
+ const value = catalog[key] ?? EN_CATALOG[key] ?? key;
4298
+ return interpolate(value, params);
4299
+ }
4300
+ /**
4301
+ * Reactive wrapper around {@link translate} — a `Signal` that re-emits when
4302
+ * the active locale or the resolved value changes. Ideal for host bindings
4303
+ * and template interpolation.
4304
+ */
4305
+ translation(key, params) {
4306
+ return computed(() => this.translate(key, params));
4307
+ }
4308
+ /**
4309
+ * Pick the best catalog for a locale tag: exact tag → bare language subtag →
4310
+ * English. Never returns `undefined`.
4311
+ */
4312
+ selectCatalog(tag) {
4313
+ const lower = tag.toLowerCase();
4314
+ const exact = this.catalogs.get(lower);
4315
+ if (exact)
4316
+ return exact;
4317
+ const subtag = lower.split('-')[0];
4318
+ const byLanguage = this.catalogs.get(subtag);
4319
+ if (byLanguage)
4320
+ return byLanguage;
4321
+ return EN_CATALOG;
4322
+ }
4323
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjTranslateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4324
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjTranslateService, providedIn: 'root' });
4325
+ }
4326
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjTranslateService, decorators: [{
4327
+ type: Injectable,
4328
+ args: [{ providedIn: 'root' }]
4329
+ }], ctorParameters: () => [] });
4330
+
4331
+ /**
4332
+ * Writes a localized string into its host — either the element's text content
4333
+ * or a named attribute (for `aria-label`, `title`, …). Fully reactive: the host
4334
+ * re-renders when the active locale (`KjLocale.locale()`) or the interpolation
4335
+ * params change, with no change-detection cost between changes (signals +
4336
+ * `effect`, no pipe).
4337
+ *
4338
+ * The key is compile-checked against the {@link KjTranslationKey} union, so a
4339
+ * typo fails `tsc`. Values may contain `{name}` placeholders filled from
4340
+ * `kjTranslateParams`.
4341
+ *
4342
+ * @example
4343
+ * ```html
4344
+ * <!-- visible text -->
4345
+ * <span [kjTranslate]="'pagination.pageOf'"
4346
+ * [kjTranslateParams]="{ page: page(), total: total() }"></span>
4347
+ *
4348
+ * <!-- localized aria-label on an icon-only button -->
4349
+ * <button [kjTranslate]="'toast.close'" kjTranslateAttr="aria-label">×</button>
4350
+ * ```
4351
+ * @doc
4352
+ * @doc-example Basic
4353
+ * @doc-theme default
4354
+ * @doc-file i18n.basic.example.ts
4355
+ * @doc-name i18n
4356
+ * @doc-category Core/Accessibility
4357
+ */
4358
+ class KjTranslate {
4359
+ svc = inject(KjTranslateService);
4360
+ el = inject(ElementRef);
4361
+ /** Translation key to render. Compile-checked against the catalog. */
4362
+ kjTranslate = input.required(/* @ts-ignore */
4363
+ ...(ngDevMode ? [{ debugName: "kjTranslate" }] : /* istanbul ignore next */ []));
4364
+ /** Values for `{name}` placeholders in the resolved string. */
4365
+ kjTranslateParams = input(/* @ts-ignore */
4366
+ ...(ngDevMode ? [undefined, { debugName: "kjTranslateParams" }] : /* istanbul ignore next */ []));
4367
+ /**
4368
+ * Target attribute to write (e.g. `'aria-label'`, `'title'`). When unset, the
4369
+ * translation is written to the host's text content.
4370
+ */
4371
+ kjTranslateAttr = input(/* @ts-ignore */
4372
+ ...(ngDevMode ? [undefined, { debugName: "kjTranslateAttr" }] : /* istanbul ignore next */ []));
4373
+ constructor() {
4374
+ effect(() => {
4375
+ const value = this.svc.translate(this.kjTranslate(), this.kjTranslateParams());
4376
+ const attr = this.kjTranslateAttr();
4377
+ if (attr) {
4378
+ this.el.nativeElement.setAttribute(attr, value);
4379
+ }
4380
+ else {
4381
+ this.el.nativeElement.textContent = value;
4382
+ }
4383
+ });
4384
+ }
4385
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjTranslate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4386
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: KjTranslate, isStandalone: true, selector: "[kjTranslate]", inputs: { kjTranslate: { classPropertyName: "kjTranslate", publicName: "kjTranslate", isSignal: true, isRequired: true, transformFunction: null }, kjTranslateParams: { classPropertyName: "kjTranslateParams", publicName: "kjTranslateParams", isSignal: true, isRequired: false, transformFunction: null }, kjTranslateAttr: { classPropertyName: "kjTranslateAttr", publicName: "kjTranslateAttr", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
4387
+ }
4388
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjTranslate, decorators: [{
4389
+ type: Directive,
4390
+ args: [{
4391
+ selector: '[kjTranslate]',
4392
+ standalone: true,
4393
+ }]
4394
+ }], ctorParameters: () => [], propDecorators: { kjTranslate: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjTranslate", required: true }] }], kjTranslateParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjTranslateParams", required: false }] }], kjTranslateAttr: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjTranslateAttr", required: false }] }] } });
4395
+
3776
4396
  /**
3777
4397
  * DI token holding the variant preset for the current consumer's injector
3778
4398
  * scope. Resolved by `KjVariant` at construction time.
@@ -5786,7 +6406,8 @@ class KjNumberInput {
5786
6406
  /** @internal */
5787
6407
  formCtrl = inject(KjFormControl);
5788
6408
  el = inject(ElementRef);
5789
- localeId = inject(LOCALE_ID);
6409
+ /** Locale provider — the fallback source of truth for locale + currency. */
6410
+ locale = inject(KjLocale);
5790
6411
  group = inject(KjNumberInputGroup, { optional: true, skipSelf: true });
5791
6412
  /** Two-way bindable numeric model. `null` for empty. */
5792
6413
  kjValue = model(null, /* @ts-ignore */
@@ -5816,10 +6437,13 @@ class KjNumberInput {
5816
6437
  /** Drives `Intl.NumberFormat` style. Ignored when `kjUseNativeNumber=true`. */
5817
6438
  kjFormat = input('decimal', /* @ts-ignore */
5818
6439
  ...(ngDevMode ? [{ debugName: "kjFormat" }] : /* istanbul ignore next */ []));
5819
- /** BCP-47 tag. Falls back to the injected `LOCALE_ID`. */
6440
+ /** BCP-47 tag. Falls back to the `KjLocale` provider (`provideKjLocale`). */
5820
6441
  kjLocale = input(undefined, /* @ts-ignore */
5821
6442
  ...(ngDevMode ? [{ debugName: "kjLocale" }] : /* istanbul ignore next */ []));
5822
- /** ISO 4217 code (e.g. `'USD'`). Required when `kjFormat="currency"`. */
6443
+ /**
6444
+ * ISO 4217 code (e.g. `'USD'`). Required when `kjFormat="currency"` unless a
6445
+ * default currency is supplied via `provideKjLocale`.
6446
+ */
5823
6447
  kjCurrency = input(undefined, /* @ts-ignore */
5824
6448
  ...(ngDevMode ? [{ debugName: "kjCurrency" }] : /* istanbul ignore next */ []));
5825
6449
  /** Currency display mode. */
@@ -5889,9 +6513,9 @@ class KjNumberInput {
5889
6513
  ...(ngDevMode ? [{ debugName: "inputMode" }] : /* istanbul ignore next */ []));
5890
6514
  /** Formatter options derived from the public inputs. */
5891
6515
  formatOptions = computed(() => ({
5892
- locale: this.kjLocale() ?? this.localeId,
6516
+ locale: this.kjLocale() ?? this.locale.locale(),
5893
6517
  format: this.kjFormat(),
5894
- currency: this.kjCurrency(),
6518
+ currency: this.kjCurrency() ?? this.locale.currency(),
5895
6519
  currencyDisplay: this.kjCurrencyDisplay(),
5896
6520
  unit: this.kjUnit(),
5897
6521
  unitDisplay: this.kjUnitDisplay(),
@@ -5955,7 +6579,7 @@ class KjNumberInput {
5955
6579
  else if (editing) {
5956
6580
  // Percent: the user thinks of `50` even though we store `0.5`.
5957
6581
  const display = this.kjFormat() === 'percent' ? value * 100 : value;
5958
- next = formatForEdit(display, this.kjLocale() ?? this.localeId);
6582
+ next = formatForEdit(display, this.kjLocale() ?? this.locale.locale());
5959
6583
  }
5960
6584
  else {
5961
6585
  next = formatNumber(value, this.formatOptions());
@@ -6117,7 +6741,7 @@ class KjNumberInput {
6117
6741
  return;
6118
6742
  }
6119
6743
  el.value = this.editing()
6120
- ? formatForEdit(this.kjFormat() === 'percent' ? value * 100 : value, this.kjLocale() ?? this.localeId)
6744
+ ? formatForEdit(this.kjFormat() === 'percent' ? value * 100 : value, this.kjLocale() ?? this.locale.locale())
6121
6745
  : formatNumber(value, this.formatOptions());
6122
6746
  }
6123
6747
  fallbackForStep() {
@@ -10723,46 +11347,421 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
10723
11347
  }]
10724
11348
  }], ctorParameters: () => [], propDecorators: { kjState: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjState", required: false }] }] } });
10725
11349
 
11350
+ let _msgId = 0;
11351
+ /** Allocate a stable message id. */
11352
+ function nextChatMessageId() {
11353
+ return `kj-msg-${++_msgId}`;
11354
+ }
10726
11355
  /**
10727
- * Marks a paragraph as the lead-in paragraph for a section — slightly larger
10728
- * size with a softer tone. Reflects `data-tone="lead"` so theme CSS keys off
10729
- * it; the directive owns no styling itself.
11356
+ * Headless, **provider-agnostic** streaming chat state.
10730
11357
  *
10731
- * Lead semantics are paragraph-bound; applied to a non-`<p>` host the
10732
- * directive emits a dev-mode warning but does not enforce the tag.
11358
+ * Owns the `messages` signal, the stream `status`, and the append API. It has
11359
+ * **no** LLM SDK, `fetch`, or backend the consumer wires their own model /
11360
+ * stream and drives this store: `sendUser()`, `beginAssistant()`, then
11361
+ * `pushChunk()` per token/chunk, and finally `endAssistant()` (or `fail()` /
11362
+ * `stop()`).
11363
+ *
11364
+ * Provided **per thread** (no `providedIn`); `KjChatThread` provides one, or
11365
+ * the consumer provides their own to share state.
10733
11366
  *
10734
11367
  * @example
10735
- * ```html
10736
- * <p kjLead>Atlas helps engineering teams plan quarterly roadmaps.</p>
11368
+ * ```ts
11369
+ * const store = inject(KjChatStore);
11370
+ * store.sendUser('Summarise the spec');
11371
+ * store.beginAssistant();
11372
+ * for await (const token of myModelStream()) store.pushChunk(token);
11373
+ * store.endAssistant();
10737
11374
  * ```
10738
- * @doc-category Core/Data display
11375
+ * @doc-category Core/AI
10739
11376
  * @doc
10740
- * @doc-name typography
10741
- */
10742
- class KjLead {
10743
- el = inject(ElementRef);
10744
- constructor() {
10745
- if (isDevMode()) {
10746
- afterNextRender(() => {
10747
- const host = this.el.nativeElement;
10748
- if (host.tagName?.toLowerCase() !== 'p') {
10749
- console.warn(`[kj] kjLead applied to <${host.tagName?.toLowerCase()}>. ` +
10750
- `Lead semantics are paragraph-bound; recommended host is <p>.`);
10751
- }
10752
- });
10753
- }
11377
+ * @doc-name chat-store
11378
+ * @doc-description Headless provider-agnostic streaming chat state — messages, status, and the token-append API.
11379
+ */
11380
+ class KjChatStore {
11381
+ _messages = signal([], /* @ts-ignore */
11382
+ ...(ngDevMode ? [{ debugName: "_messages" }] : /* istanbul ignore next */ []));
11383
+ _status = signal('idle', /* @ts-ignore */
11384
+ ...(ngDevMode ? [{ debugName: "_status" }] : /* istanbul ignore next */ []));
11385
+ _streamingId = signal(null, /* @ts-ignore */
11386
+ ...(ngDevMode ? [{ debugName: "_streamingId" }] : /* istanbul ignore next */ []));
11387
+ /** The full message list. */
11388
+ messages = this._messages.asReadonly();
11389
+ /** Current stream status. */
11390
+ status = this._status.asReadonly();
11391
+ /** Id of the in-flight assistant message, or `null`. */
11392
+ streamingId = this._streamingId.asReadonly();
11393
+ /** True while an assistant message is streaming. */
11394
+ isStreaming = computed(() => this._status() === 'streaming', /* @ts-ignore */
11395
+ ...(ngDevMode ? [{ debugName: "isStreaming" }] : /* istanbul ignore next */ []));
11396
+ /** The current thread-level error, if any. */
11397
+ error = computed(() => this._messages().find((m) => m.id === this._streamingId())?.error ?? null, /* @ts-ignore */
11398
+ ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
11399
+ /** Append a user message; returns its id. */
11400
+ sendUser(content) {
11401
+ const id = nextChatMessageId();
11402
+ this.append({ id, role: 'user', content, createdAt: Date.now() });
11403
+ return id;
10754
11404
  }
10755
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjLead, deps: [], target: i0.ɵɵFactoryTarget.Directive });
10756
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.5", type: KjLead, isStandalone: true, selector: "[kjLead]", host: { properties: { "attr.data-tone": "\"lead\"" } }, exportAs: ["kjLead"], ngImport: i0 });
10757
- }
10758
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjLead, decorators: [{
10759
- type: Directive,
10760
- args: [{
10761
- selector: '[kjLead]',
10762
- standalone: true,
10763
- exportAs: 'kjLead',
10764
- host: {
10765
- '[attr.data-tone]': '"lead"',
11405
+ /** Append a system message; returns its id. */
11406
+ addSystem(content) {
11407
+ const id = nextChatMessageId();
11408
+ this.append({ id, role: 'system', content, createdAt: Date.now() });
11409
+ return id;
11410
+ }
11411
+ /**
11412
+ * Start an in-flight assistant message. Sets `status → 'streaming'` and
11413
+ * `streamingId`. Returns the new message id.
11414
+ */
11415
+ beginAssistant(seed = '') {
11416
+ const id = nextChatMessageId();
11417
+ this.append({
11418
+ id,
11419
+ role: 'assistant',
11420
+ content: seed,
11421
+ streaming: true,
11422
+ createdAt: Date.now(),
11423
+ });
11424
+ this._streamingId.set(id);
11425
+ this._status.set('streaming');
11426
+ return id;
11427
+ }
11428
+ /**
11429
+ * Append a token / chunk to the in-flight assistant message. No-op (with a
11430
+ * dev warning) if there is no in-flight message.
11431
+ */
11432
+ pushChunk(text) {
11433
+ const id = this._streamingId();
11434
+ if (id === null)
11435
+ return;
11436
+ this.patch(id, (m) => ({ ...m, content: m.content + text }));
11437
+ }
11438
+ /** Add a tool call to the in-flight assistant message. */
11439
+ addToolCall(tc) {
11440
+ const id = this._streamingId();
11441
+ if (id === null)
11442
+ return;
11443
+ this.patch(id, (m) => ({ ...m, toolCalls: [...(m.toolCalls ?? []), tc] }));
11444
+ }
11445
+ /** Patch an existing tool call by id on the in-flight message. */
11446
+ updateToolCall(toolCallId, patch) {
11447
+ const id = this._streamingId();
11448
+ if (id === null)
11449
+ return;
11450
+ this.patch(id, (m) => ({
11451
+ ...m,
11452
+ toolCalls: (m.toolCalls ?? []).map((tc) => (tc.id === toolCallId ? { ...tc, ...patch } : tc)),
11453
+ }));
11454
+ }
11455
+ /** Attach citations to the in-flight assistant message. */
11456
+ addCitations(citations) {
11457
+ const id = this._streamingId();
11458
+ if (id === null)
11459
+ return;
11460
+ this.patch(id, (m) => ({
11461
+ ...m,
11462
+ citations: [...(m.citations ?? []), ...citations],
11463
+ }));
11464
+ }
11465
+ /** Complete the in-flight message; `status → 'idle'`. */
11466
+ endAssistant() {
11467
+ const id = this._streamingId();
11468
+ if (id !== null)
11469
+ this.patch(id, (m) => ({ ...m, streaming: false }));
11470
+ this._streamingId.set(null);
11471
+ this._status.set('idle');
11472
+ }
11473
+ /**
11474
+ * Fail the in-flight turn. Sets `status → 'error'` and records the error on
11475
+ * the message. The message stops streaming but keeps any partial content.
11476
+ */
11477
+ fail(message) {
11478
+ const id = this._streamingId();
11479
+ if (id !== null) {
11480
+ this.patch(id, (m) => ({ ...m, streaming: false, error: message }));
11481
+ }
11482
+ this._status.set('error');
11483
+ }
11484
+ /**
11485
+ * Consumer-initiated stop (abort). Freezes whatever partial content exists
11486
+ * and returns to `idle`. The consumer is responsible for aborting their own
11487
+ * network stream.
11488
+ */
11489
+ stop() {
11490
+ const id = this._streamingId();
11491
+ if (id !== null)
11492
+ this.patch(id, (m) => ({ ...m, streaming: false }));
11493
+ this._streamingId.set(null);
11494
+ this._status.set('idle');
11495
+ }
11496
+ /** Replace the whole message list (e.g. load history). */
11497
+ setMessages(messages) {
11498
+ this._messages.set([...messages]);
11499
+ }
11500
+ /** Clear the thread and return to `idle`. */
11501
+ reset() {
11502
+ this._messages.set([]);
11503
+ this._streamingId.set(null);
11504
+ this._status.set('idle');
11505
+ }
11506
+ append(msg) {
11507
+ this._messages.update((list) => [...list, msg]);
11508
+ }
11509
+ patch(id, fn) {
11510
+ this._messages.update((list) => list.map((m) => (m.id === id ? fn(m) : m)));
11511
+ }
11512
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjChatStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
11513
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjChatStore });
11514
+ }
11515
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjChatStore, decorators: [{
11516
+ type: Injectable
11517
+ }] });
11518
+
11519
+ /** Sentence-ending punctuation we flush on. */
11520
+ const SENTENCE_END = /[.!?\n]/g;
11521
+ /**
11522
+ * Pure coalescing logic for the streaming live region — **the differentiator**.
11523
+ *
11524
+ * Screen readers announce every mutation of an `aria-live` region, so pushing a
11525
+ * streamed reply char-by-char produces an unusable torrent. This function holds
11526
+ * the streamed `buffer` and releases it only in **whole units**:
11527
+ *
11528
+ * 1. Flush up to and including the **last sentence boundary** (`.`, `!`, `?`,
11529
+ * newline). The trailing partial sentence stays in `remainder`.
11530
+ * 2. If no boundary exists but the buffer exceeds `maxChars`, flush up to the
11531
+ * last **word** boundary (space) so a long clause is not held silent.
11532
+ * 3. Otherwise announce nothing yet (`toAnnounce: ''`).
11533
+ *
11534
+ * The caller appends new chunks to `remainder` and calls this again; on stream
11535
+ * completion it flushes the final remainder unconditionally (see
11536
+ * {@link KjChatAnnouncer.flush}).
11537
+ *
11538
+ * @example
11539
+ * ```ts
11540
+ * coalesceAnnouncement('Hello there. How ar')
11541
+ * // → { toAnnounce: 'Hello there.', remainder: ' How ar' }
11542
+ * ```
11543
+ */
11544
+ function coalesceAnnouncement(buffer, opts = {}) {
11545
+ const maxChars = opts.maxChars ?? 160;
11546
+ // Find the last sentence boundary.
11547
+ let lastEnd = -1;
11548
+ SENTENCE_END.lastIndex = 0;
11549
+ for (let m = SENTENCE_END.exec(buffer); m; m = SENTENCE_END.exec(buffer)) {
11550
+ lastEnd = m.index;
11551
+ }
11552
+ if (lastEnd >= 0) {
11553
+ const cut = lastEnd + 1;
11554
+ return {
11555
+ toAnnounce: buffer.slice(0, cut).trim(),
11556
+ remainder: buffer.slice(cut),
11557
+ };
11558
+ }
11559
+ // No sentence boundary — flush at a word boundary only if over budget.
11560
+ if (buffer.length > maxChars) {
11561
+ const lastSpace = buffer.lastIndexOf(' ');
11562
+ if (lastSpace > 0) {
11563
+ return {
11564
+ toAnnounce: buffer.slice(0, lastSpace).trim(),
11565
+ remainder: buffer.slice(lastSpace + 1),
11566
+ };
11567
+ }
11568
+ // A single very long token — flush it whole rather than hold forever.
11569
+ return { toAnnounce: buffer.trim(), remainder: '' };
11570
+ }
11571
+ return { toAnnounce: '', remainder: buffer };
11572
+ }
11573
+ /**
11574
+ * Stateful wrapper around {@link coalesceAnnouncement} that exposes the current
11575
+ * announcement as a signal for a visually-hidden `aria-live="polite"` region.
11576
+ *
11577
+ * `push()` accumulates streamed text and emits coalesced sentences; `flush()`
11578
+ * (called on stream completion) releases the final remainder; `announce()`
11579
+ * pushes a discrete status line (e.g. an error) immediately.
11580
+ *
11581
+ * The emitted string toggles through empty between announcements so repeated
11582
+ * identical sentences are still re-announced by AT.
11583
+ *
11584
+ * @doc-category Core/AI
11585
+ * @doc
11586
+ * @doc-name chat-announcer
11587
+ * @doc-description Coalesces streamed tokens into whole-sentence polite live-region announcements.
11588
+ */
11589
+ class KjChatAnnouncer {
11590
+ buffer = '';
11591
+ _message = signal('', /* @ts-ignore */
11592
+ ...(ngDevMode ? [{ debugName: "_message" }] : /* istanbul ignore next */ []));
11593
+ toggle = false;
11594
+ /** The current announcement text for the polite live region. */
11595
+ message = this._message.asReadonly();
11596
+ /** Max chars held without a boundary before a word-boundary flush. */
11597
+ maxChars = 160;
11598
+ /** Append streamed text; emits any newly-completed sentence(s). */
11599
+ push(chunk) {
11600
+ this.buffer += chunk;
11601
+ const { toAnnounce, remainder } = coalesceAnnouncement(this.buffer, {
11602
+ maxChars: this.maxChars,
11603
+ });
11604
+ this.buffer = remainder;
11605
+ if (toAnnounce)
11606
+ this.emit(toAnnounce);
11607
+ }
11608
+ /** Force-release the buffered remainder (call on stream completion). */
11609
+ flush() {
11610
+ const text = this.buffer.trim();
11611
+ this.buffer = '';
11612
+ if (text)
11613
+ this.emit(text);
11614
+ }
11615
+ /** Announce a discrete status line immediately (bypasses coalescing). */
11616
+ announce(text) {
11617
+ if (text.trim())
11618
+ this.emit(text.trim());
11619
+ }
11620
+ /** Clear buffered text and the current announcement. */
11621
+ clear() {
11622
+ this.buffer = '';
11623
+ this._message.set('');
11624
+ }
11625
+ emit(text) {
11626
+ // Alternate a trailing zero-width space so identical consecutive
11627
+ // sentences still register as a live-region change for AT.
11628
+ this.toggle = !this.toggle;
11629
+ this._message.set(this.toggle ? text : text + '​');
11630
+ }
11631
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjChatAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
11632
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjChatAnnouncer });
11633
+ }
11634
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjChatAnnouncer, decorators: [{
11635
+ type: Injectable
11636
+ }] });
11637
+
11638
+ /**
11639
+ * Strip diacritic marks from a string (e.g. `café` → `cafe`).
11640
+ * Uses NFD normalisation followed by removal of Unicode diacritic characters.
11641
+ * Note: locale-naive for v1 (Turkish/German edge cases are documented).
11642
+ */
11643
+ function stripDiacritics(str) {
11644
+ return str.normalize('NFD').replace(/\p{Diacritic}/gu, '');
11645
+ }
11646
+ /**
11647
+ * Default filter: case- and diacritic-insensitive substring match.
11648
+ * Returns score 1 if any haystack contains the needle, 0 otherwise.
11649
+ * Returns 1 for empty queries (all items visible).
11650
+ */
11651
+ const kjSubstringFilter = (query, haystacks) => {
11652
+ if (!query)
11653
+ return 1;
11654
+ const needle = stripDiacritics(query.toLowerCase());
11655
+ return haystacks.some(h => stripDiacritics(h.toLowerCase()).includes(needle)) ? 1 : 0;
11656
+ };
11657
+ /**
11658
+ * Optional fuzzy filter: checks whether all characters of the query appear
11659
+ * in the haystack in order (abbreviation matching).
11660
+ * E.g. `gth` matches `git checkout`.
11661
+ * Returns score 1 if any haystack matches, 0 otherwise.
11662
+ * Returns 1 for empty queries.
11663
+ *
11664
+ * @example
11665
+ * ```html
11666
+ * <div kjCommandPalette [kjFilter]="kjFuzzyFilter">…</div>
11667
+ * ```
11668
+ */
11669
+ const kjFuzzyFilter = (query, haystacks) => {
11670
+ if (!query)
11671
+ return 1;
11672
+ const needle = query.toLowerCase();
11673
+ return haystacks.some(h => {
11674
+ const hay = h.toLowerCase();
11675
+ let i = 0;
11676
+ for (const c of needle) {
11677
+ const j = hay.indexOf(c, i);
11678
+ if (j < 0)
11679
+ return false;
11680
+ i = j + 1;
11681
+ }
11682
+ return true;
11683
+ }) ? 1 : 0;
11684
+ };
11685
+
11686
+ /**
11687
+ * Parse prompt text for an in-progress slash command. The menu is active only
11688
+ * when the text starts with `/` and the command token has not been completed by
11689
+ * whitespace yet — so `/sum` is active but `/summarize now` is not.
11690
+ *
11691
+ * @example
11692
+ * ```ts
11693
+ * parseSlash('/sum') // → { active: true, query: 'sum' }
11694
+ * parseSlash('/sum arg') // → { active: false, query: '' }
11695
+ * parseSlash('hi') // → { active: false, query: '' }
11696
+ * ```
11697
+ */
11698
+ function parseSlash(text) {
11699
+ if (!text.startsWith('/'))
11700
+ return { active: false, query: '' };
11701
+ const rest = text.slice(1);
11702
+ if (/\s/.test(rest))
11703
+ return { active: false, query: '' };
11704
+ return { active: true, query: rest };
11705
+ }
11706
+ /**
11707
+ * Filter slash commands against a query using the **command-palette filter**
11708
+ * (`kjSubstringFilter` by default — case- and diacritic-insensitive). Each
11709
+ * command's `[name, label, description]` form the haystack.
11710
+ *
11711
+ * @param query the text after the leading slash
11712
+ * @param commands the available commands
11713
+ * @param filter palette filter (defaults to {@link kjSubstringFilter})
11714
+ */
11715
+ function matchSlashCommands(query, commands, filter = kjSubstringFilter) {
11716
+ return commands
11717
+ .map((cmd) => ({
11718
+ cmd,
11719
+ score: filter(query, [cmd.name, cmd.label, cmd.description ?? '']),
11720
+ }))
11721
+ .filter(({ score }) => score > 0)
11722
+ .map(({ cmd }) => cmd);
11723
+ }
11724
+
11725
+ /**
11726
+ * Marks a paragraph as the lead-in paragraph for a section — slightly larger
11727
+ * size with a softer tone. Reflects `data-tone="lead"` so theme CSS keys off
11728
+ * it; the directive owns no styling itself.
11729
+ *
11730
+ * Lead semantics are paragraph-bound; applied to a non-`<p>` host the
11731
+ * directive emits a dev-mode warning but does not enforce the tag.
11732
+ *
11733
+ * @example
11734
+ * ```html
11735
+ * <p kjLead>Atlas helps engineering teams plan quarterly roadmaps.</p>
11736
+ * ```
11737
+ * @doc-category Core/Data display
11738
+ * @doc
11739
+ * @doc-name typography
11740
+ */
11741
+ class KjLead {
11742
+ el = inject(ElementRef);
11743
+ constructor() {
11744
+ if (isDevMode()) {
11745
+ afterNextRender(() => {
11746
+ const host = this.el.nativeElement;
11747
+ if (host.tagName?.toLowerCase() !== 'p') {
11748
+ console.warn(`[kj] kjLead applied to <${host.tagName?.toLowerCase()}>. ` +
11749
+ `Lead semantics are paragraph-bound; recommended host is <p>.`);
11750
+ }
11751
+ });
11752
+ }
11753
+ }
11754
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjLead, deps: [], target: i0.ɵɵFactoryTarget.Directive });
11755
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.5", type: KjLead, isStandalone: true, selector: "[kjLead]", host: { properties: { "attr.data-tone": "\"lead\"" } }, exportAs: ["kjLead"], ngImport: i0 });
11756
+ }
11757
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjLead, decorators: [{
11758
+ type: Directive,
11759
+ args: [{
11760
+ selector: '[kjLead]',
11761
+ standalone: true,
11762
+ exportAs: 'kjLead',
11763
+ host: {
11764
+ '[attr.data-tone]': '"lead"',
10766
11765
  },
10767
11766
  }]
10768
11767
  }], ctorParameters: () => [] });
@@ -11628,9 +12627,9 @@ const DRAWER_SIDE = new InjectionToken('KjDrawerSide');
11628
12627
  const DRAWER_DRAG = new InjectionToken('KjDrawerDrag');
11629
12628
 
11630
12629
  /** Downward drag fraction past which release dismisses (bottom side). */
11631
- const DEFAULT_DISMISS_THRESHOLD = 0.4;
12630
+ const DEFAULT_DISMISS_THRESHOLD$1 = 0.4;
11632
12631
  /** Downward velocity (px/s) past which release dismisses (bottom side). */
11633
- const DEFAULT_DISMISS_VELOCITY = 600;
12632
+ const DEFAULT_DISMISS_VELOCITY$1 = 600;
11634
12633
  /**
11635
12634
  * Drawer body component. Composes {@link KjOverlayPanel} so the host element
11636
12635
  * inherits `role`, `[data-state]`, and the configured side strategy from
@@ -11689,8 +12688,8 @@ class KjDrawer {
11689
12688
  const velocity = (dy / dt) * 1000;
11690
12689
  this.endDrag();
11691
12690
  const panelHeight = this.el.nativeElement.getBoundingClientRect().height || 200;
11692
- if (velocity > DEFAULT_DISMISS_VELOCITY
11693
- || dy / panelHeight >= DEFAULT_DISMISS_THRESHOLD) {
12691
+ if (velocity > DEFAULT_DISMISS_VELOCITY$1
12692
+ || dy / panelHeight >= DEFAULT_DISMISS_THRESHOLD$1) {
11694
12693
  this.ref?.close();
11695
12694
  }
11696
12695
  }
@@ -11739,55 +12738,331 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
11739
12738
  args: ['keydown.escape']
11740
12739
  }] } });
11741
12740
 
11742
- class KjTooltipTrigger {
11743
- kjOpenDelay = input(200, { ...(ngDevMode ? { debugName: "kjOpenDelay" } : /* istanbul ignore next */ {}), transform: (v) => Number(v) || 200 });
11744
- kjCloseDelay = input(0, { ...(ngDevMode ? { debugName: "kjCloseDelay" } : /* istanbul ignore next */ {}), transform: (v) => Number(v) || 0 });
11745
- kjDisabled = input(false, { ...(ngDevMode ? { debugName: "kjDisabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
11746
- constructor() {
11747
- const strat = inject(KJ_OVERLAY_TRIGGER_EVENT_STRATEGY);
11748
- if ('configure' in strat) {
11749
- strat.configure({ openDelay: this.kjOpenDelay, closeDelay: this.kjCloseDelay });
11750
- }
12741
+ /**
12742
+ * Reference returned by `KjSheetService.open()`. Mirrors
12743
+ * {@link import('../drawer/drawer.ref').KjDrawerRef}.
12744
+ *
12745
+ * Use `close(result?)` to dismiss the bottom sheet programmatically. Subscribe
12746
+ * to `afterClosed$` for the close result, or await the `result` promise.
12747
+ *
12748
+ * @doc-category Core/Overlay
12749
+ */
12750
+ class KjSheetRef {
12751
+ controller;
12752
+ _instance = null;
12753
+ _result;
12754
+ resolveResult;
12755
+ /** Promise resolving with the close result. */
12756
+ result;
12757
+ _afterOpened = new Subject();
12758
+ _afterClosed = new Subject();
12759
+ /** Emits once after the sheet has finished opening. */
12760
+ afterOpened$ = this._afterOpened.asObservable();
12761
+ /** Emits the close result once the sheet has finished closing. */
12762
+ afterClosed$ = this._afterClosed.asObservable();
12763
+ /** Reactive lifecycle state mirrored from the underlying controller. */
12764
+ state;
12765
+ /** Convenience for `state() === 'open' || 'opening'`. */
12766
+ isOpen;
12767
+ constructor(controller) {
12768
+ this.controller = controller;
12769
+ this.state = controller.state;
12770
+ this.isOpen = controller.isOpen;
12771
+ this.result = new Promise((res) => {
12772
+ this.resolveResult = res;
12773
+ });
11751
12774
  }
11752
- _overlayTrigger = inject(KjOverlayTrigger, { self: true });
11753
- /** The controller of the composed `KjOverlayTrigger`, exposed for sibling `[kjFor]` panels. */
11754
- get controller() {
11755
- return this._overlayTrigger.controller;
12775
+ /** @internal Bind the rendered component instance for `instance`. */
12776
+ bindInstance(instance) {
12777
+ this._instance = instance;
11756
12778
  }
11757
- attachPanel(panel) {
11758
- this._overlayTrigger.attachPanel(panel);
12779
+ /** The rendered sheet body component instance. */
12780
+ get instance() {
12781
+ if (!this._instance) {
12782
+ throw new Error('KjSheetRef: instance not bound');
12783
+ }
12784
+ return this._instance;
12785
+ }
12786
+ /** Close the sheet with an optional result payload. */
12787
+ close(result) {
12788
+ this._result = result;
12789
+ this.controller.close('programmatic');
12790
+ queueMicrotask(() => {
12791
+ this._afterClosed.next(this._result);
12792
+ this._afterClosed.complete();
12793
+ this.resolveResult(this._result);
12794
+ });
11759
12795
  }
11760
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjTooltipTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
11761
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: KjTooltipTrigger, isStandalone: true, selector: "[kjTooltipTrigger]", inputs: { kjOpenDelay: { classPropertyName: "kjOpenDelay", publicName: "kjOpenDelay", isSignal: true, isRequired: false, transformFunction: null }, kjCloseDelay: { classPropertyName: "kjCloseDelay", publicName: "kjCloseDelay", isSignal: true, isRequired: false, transformFunction: null }, kjDisabled: { classPropertyName: "kjDisabled", publicName: "kjDisabled", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
11762
- KjOverlayController,
11763
- {
11764
- provide: KJ_OVERLAY_TRIGGER_EVENT_STRATEGY,
11765
- useFactory: () => onHover({ openDelay: 200, closeDelay: 0 }),
11766
- },
11767
- { provide: KJ_OVERLAY_PANEL_ROLE, useValue: 'tooltip' },
11768
- ], exportAs: ["kjTooltipTrigger"], hostDirectives: [{ directive: KjOverlayTrigger, inputs: ["kjOpen", "kjOpen"] }], ngImport: i0 });
11769
12796
  }
11770
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjTooltipTrigger, decorators: [{
11771
- type: Directive,
11772
- args: [{
11773
- selector: '[kjTooltipTrigger]',
11774
- exportAs: 'kjTooltipTrigger',
11775
- standalone: true,
11776
- hostDirectives: [
11777
- { directive: KjOverlayTrigger, inputs: ['kjOpen'] },
11778
- ],
11779
- providers: [
11780
- KjOverlayController,
11781
- {
11782
- provide: KJ_OVERLAY_TRIGGER_EVENT_STRATEGY,
11783
- useFactory: () => onHover({ openDelay: 200, closeDelay: 0 }),
11784
- },
11785
- { provide: KJ_OVERLAY_PANEL_ROLE, useValue: 'tooltip' },
11786
- ],
11787
- }]
11788
- }], ctorParameters: () => [], propDecorators: { kjOpenDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjOpenDelay", required: false }] }], kjCloseDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjCloseDelay", required: false }] }], kjDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjDisabled", required: false }] }] } });
11789
12797
 
11790
- // tooltip-content.ts
12798
+ /**
12799
+ * Programmatic service for opening bottom sheets — a mobile-first,
12800
+ * bottom-anchored modal surface with a grab handle and drag-to-dismiss.
12801
+ *
12802
+ * Composes the same overlay primitive stack as `KjDrawer` and `KjDialog`
12803
+ * (`edgeSheet` position, `solidBackdrop`, `tabCycle` focus trap,
12804
+ * `htmlOverflow` scroll lock) through {@link KjOverlayBuilder} — the overlay
12805
+ * engine is reused, not reinvented.
12806
+ *
12807
+ * @doc-category Core/Overlay
12808
+ */
12809
+ class KjSheetService {
12810
+ builder = inject(KjOverlayBuilder);
12811
+ env = inject(EnvironmentInjector);
12812
+ /**
12813
+ * Open a component as a modal bottom sheet.
12814
+ *
12815
+ * @param component - The body component rendered inside the sheet.
12816
+ * @param opts - Data, detent, dismissible, and close-on-outside controls.
12817
+ * @returns A {@link KjSheetRef} for closing and observing the sheet.
12818
+ */
12819
+ open(component, opts = {}) {
12820
+ const dismissible = opts.dismissible ?? true;
12821
+ const handle = this.builder.create({
12822
+ mount: inPlace(),
12823
+ position: edgeSheet({ side: 'bottom' }),
12824
+ backdrop: solidBackdrop({
12825
+ inert: true,
12826
+ closeOnClick: opts.closeOnOutside ?? true,
12827
+ }),
12828
+ focusTrap: tabCycle({ returnFocus: true }),
12829
+ scrollLock: htmlOverflow(),
12830
+ liveAnnouncer: silent(),
12831
+ trigger: programmatic(),
12832
+ panelRole: 'dialog',
12833
+ });
12834
+ const ref = new KjSheetRef(handle.controller);
12835
+ const cmpRef = this.builder.attachComponent(handle, component, {
12836
+ providers: [
12837
+ { provide: KjSheetRef, useValue: ref },
12838
+ { provide: SHEET_DATA, useValue: opts.data },
12839
+ { provide: SHEET_DETENT, useValue: opts.detent ?? 'auto' },
12840
+ { provide: SHEET_DISMISSIBLE, useValue: dismissible },
12841
+ { provide: SHEET_ARIA_LABEL, useValue: opts.ariaLabel ?? null },
12842
+ ],
12843
+ });
12844
+ ref.bindInstance(cmpRef.instance);
12845
+ runInInjectionContext(this.env, () => {
12846
+ let wasOpen = false;
12847
+ const eff = effect(() => {
12848
+ const s = handle.controller.state();
12849
+ if (s === 'open' || s === 'opening')
12850
+ wasOpen = true;
12851
+ if (s === 'closed' && wasOpen) {
12852
+ eff.destroy();
12853
+ queueMicrotask(() => handle.destroy());
12854
+ }
12855
+ }, /* @ts-ignore */
12856
+ ...(ngDevMode ? [{ debugName: "eff" }] : /* istanbul ignore next */ []));
12857
+ });
12858
+ handle.controller.open();
12859
+ return ref;
12860
+ }
12861
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjSheetService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
12862
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjSheetService, providedIn: 'root' });
12863
+ }
12864
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjSheetService, decorators: [{
12865
+ type: Injectable,
12866
+ args: [{ providedIn: 'root' }]
12867
+ }] });
12868
+ /** Token for passing data to a programmatically opened sheet body. */
12869
+ const SHEET_DATA = new InjectionToken('KjSheetData');
12870
+ /** Token exposing the resolved initial detent to the rendered body. */
12871
+ const SHEET_DETENT = new InjectionToken('KjSheetDetent');
12872
+ /** Token exposing whether drag-to-dismiss is enabled to the rendered body. */
12873
+ const SHEET_DISMISSIBLE = new InjectionToken('KjSheetDismissible');
12874
+ /** Token exposing the fallback accessible name to the rendered body. */
12875
+ const SHEET_ARIA_LABEL = new InjectionToken('KjSheetAriaLabel');
12876
+
12877
+ /** Downward drag fraction past which release dismisses. */
12878
+ const DEFAULT_DISMISS_THRESHOLD = 0.4;
12879
+ /** Downward velocity (px/s) past which release dismisses. */
12880
+ const DEFAULT_DISMISS_VELOCITY = 600;
12881
+ /**
12882
+ * Bottom-sheet body component. Composes {@link KjOverlayPanel} so the host
12883
+ * inherits `role="dialog"`, `aria-modal`, `[data-state]`, and the bottom
12884
+ * edge-sheet position from `KjSheetService.open()`.
12885
+ *
12886
+ * Renders a grab handle (a real `<button>` for keyboard/click dismissal) and
12887
+ * hosts drag-to-dismiss: a downward pointer drag past 40% of the panel height
12888
+ * or 600 px/s velocity calls `ref.close()`. The drag mechanics mirror the
12889
+ * proven drawer bottom-drag path — no new gesture surface.
12890
+ *
12891
+ * @doc-category Core/Overlay
12892
+ */
12893
+ class KjSheet {
12894
+ /** Resolved initial detent (provided by `KjSheetService.open`). */
12895
+ detent = inject(SHEET_DETENT, { optional: true }) ?? 'auto';
12896
+ /** Whether grab-handle + drag-to-dismiss is active. */
12897
+ dismissible = inject(SHEET_DISMISSIBLE, { optional: true }) ?? true;
12898
+ /** Fallback accessible name applied to the host when no heading is projected. */
12899
+ ariaLabel = inject(SHEET_ARIA_LABEL, { optional: true }) ?? null;
12900
+ ref = inject(KjSheetRef, { optional: true });
12901
+ el = inject(ElementRef);
12902
+ startY = 0;
12903
+ startTime = 0;
12904
+ pointerId = null;
12905
+ _dragging = false;
12906
+ /** Reactive flag for the `data-kj-dragging` host binding. */
12907
+ dragging = computed(() => this._dragging, /* @ts-ignore */
12908
+ ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
12909
+ /** @internal */
12910
+ onPointerDown(event) {
12911
+ if (!this.dismissible)
12912
+ return;
12913
+ if (event.button !== undefined && event.button !== 0)
12914
+ return;
12915
+ this.pointerId = event.pointerId;
12916
+ this.startY = event.clientY;
12917
+ this.startTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
12918
+ this._dragging = true;
12919
+ event.target?.setPointerCapture?.(event.pointerId);
12920
+ }
12921
+ /** @internal */
12922
+ onPointerMove(event) {
12923
+ if (!this._dragging)
12924
+ return;
12925
+ if (this.pointerId !== null && event.pointerId !== this.pointerId)
12926
+ return;
12927
+ const offset = Math.max(0, event.clientY - this.startY);
12928
+ this.el.nativeElement.style.setProperty('--kj-sheet-drag-offset', `${offset}px`);
12929
+ }
12930
+ /** @internal */
12931
+ onPointerUp(event) {
12932
+ if (!this._dragging)
12933
+ return;
12934
+ if (this.pointerId !== null && event.pointerId !== this.pointerId)
12935
+ return;
12936
+ const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
12937
+ const dy = Math.max(0, event.clientY - this.startY);
12938
+ const dt = Math.max(1, now - this.startTime);
12939
+ const velocity = (dy / dt) * 1000;
12940
+ this.endDrag();
12941
+ const panelHeight = this.el.nativeElement.getBoundingClientRect().height || 200;
12942
+ if (velocity > DEFAULT_DISMISS_VELOCITY
12943
+ || dy / panelHeight >= DEFAULT_DISMISS_THRESHOLD) {
12944
+ this.ref?.close();
12945
+ }
12946
+ }
12947
+ /** @internal */
12948
+ onPointerCancel(_event) {
12949
+ this.endDrag();
12950
+ }
12951
+ endDrag() {
12952
+ this._dragging = false;
12953
+ this.pointerId = null;
12954
+ this.el.nativeElement.style.removeProperty('--kj-sheet-drag-offset');
12955
+ }
12956
+ /** Close the sheet with an optional payload. */
12957
+ close(result) {
12958
+ this.ref?.close(result);
12959
+ }
12960
+ /** Esc closes via the overlay-stack coordinator on the controller. */
12961
+ onEscape() {
12962
+ this.ref?.close();
12963
+ }
12964
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjSheet, deps: [], target: i0.ɵɵFactoryTarget.Component });
12965
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: KjSheet, isStandalone: true, selector: "kj-sheet", host: { listeners: { "pointerdown": "onPointerDown($event)", "pointermove": "onPointerMove($event)", "pointerup": "onPointerUp($event)", "pointercancel": "onPointerCancel($event)", "keydown.escape": "onEscape()" }, properties: { "attr.data-kj-detent": "detent", "attr.aria-label": "ariaLabel", "attr.data-kj-dragging": "dragging() ? \"\" : null", "style.touch-action": "dragging() ? \"none\" : null" }, classAttribute: "kj-sheet" }, hostDirectives: [{ directive: KjOverlayPanel }], ngImport: i0, template: `
12966
+ @if (dismissible) {
12967
+ <button
12968
+ type="button"
12969
+ class="kj-sheet__handle"
12970
+ aria-label="Close sheet"
12971
+ (click)="close()"
12972
+ >
12973
+ <span class="kj-sheet__grip" aria-hidden="true"></span>
12974
+ </button>
12975
+ }
12976
+ <div class="kj-sheet__content"><ng-content /></div>
12977
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
12978
+ }
12979
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjSheet, decorators: [{
12980
+ type: Component,
12981
+ args: [{
12982
+ selector: 'kj-sheet',
12983
+ standalone: true,
12984
+ hostDirectives: [{ directive: KjOverlayPanel }],
12985
+ host: {
12986
+ 'class': 'kj-sheet',
12987
+ '[attr.data-kj-detent]': 'detent',
12988
+ '[attr.aria-label]': 'ariaLabel',
12989
+ '[attr.data-kj-dragging]': 'dragging() ? "" : null',
12990
+ '[style.touch-action]': 'dragging() ? "none" : null',
12991
+ '(pointerdown)': 'onPointerDown($event)',
12992
+ '(pointermove)': 'onPointerMove($event)',
12993
+ '(pointerup)': 'onPointerUp($event)',
12994
+ '(pointercancel)': 'onPointerCancel($event)',
12995
+ },
12996
+ changeDetection: ChangeDetectionStrategy.OnPush,
12997
+ encapsulation: ViewEncapsulation.None,
12998
+ template: `
12999
+ @if (dismissible) {
13000
+ <button
13001
+ type="button"
13002
+ class="kj-sheet__handle"
13003
+ aria-label="Close sheet"
13004
+ (click)="close()"
13005
+ >
13006
+ <span class="kj-sheet__grip" aria-hidden="true"></span>
13007
+ </button>
13008
+ }
13009
+ <div class="kj-sheet__content"><ng-content /></div>
13010
+ `,
13011
+ }]
13012
+ }], propDecorators: { onEscape: [{
13013
+ type: HostListener,
13014
+ args: ['keydown.escape']
13015
+ }] } });
13016
+
13017
+ class KjTooltipTrigger {
13018
+ kjOpenDelay = input(200, { ...(ngDevMode ? { debugName: "kjOpenDelay" } : /* istanbul ignore next */ {}), transform: (v) => Number(v) || 200 });
13019
+ kjCloseDelay = input(0, { ...(ngDevMode ? { debugName: "kjCloseDelay" } : /* istanbul ignore next */ {}), transform: (v) => Number(v) || 0 });
13020
+ kjDisabled = input(false, { ...(ngDevMode ? { debugName: "kjDisabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
13021
+ constructor() {
13022
+ const strat = inject(KJ_OVERLAY_TRIGGER_EVENT_STRATEGY);
13023
+ if ('configure' in strat) {
13024
+ strat.configure({ openDelay: this.kjOpenDelay, closeDelay: this.kjCloseDelay });
13025
+ }
13026
+ }
13027
+ _overlayTrigger = inject(KjOverlayTrigger, { self: true });
13028
+ /** The controller of the composed `KjOverlayTrigger`, exposed for sibling `[kjFor]` panels. */
13029
+ get controller() {
13030
+ return this._overlayTrigger.controller;
13031
+ }
13032
+ attachPanel(panel) {
13033
+ this._overlayTrigger.attachPanel(panel);
13034
+ }
13035
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjTooltipTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
13036
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: KjTooltipTrigger, isStandalone: true, selector: "[kjTooltipTrigger]", inputs: { kjOpenDelay: { classPropertyName: "kjOpenDelay", publicName: "kjOpenDelay", isSignal: true, isRequired: false, transformFunction: null }, kjCloseDelay: { classPropertyName: "kjCloseDelay", publicName: "kjCloseDelay", isSignal: true, isRequired: false, transformFunction: null }, kjDisabled: { classPropertyName: "kjDisabled", publicName: "kjDisabled", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
13037
+ KjOverlayController,
13038
+ {
13039
+ provide: KJ_OVERLAY_TRIGGER_EVENT_STRATEGY,
13040
+ useFactory: () => onHover({ openDelay: 200, closeDelay: 0 }),
13041
+ },
13042
+ { provide: KJ_OVERLAY_PANEL_ROLE, useValue: 'tooltip' },
13043
+ ], exportAs: ["kjTooltipTrigger"], hostDirectives: [{ directive: KjOverlayTrigger, inputs: ["kjOpen", "kjOpen"] }], ngImport: i0 });
13044
+ }
13045
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjTooltipTrigger, decorators: [{
13046
+ type: Directive,
13047
+ args: [{
13048
+ selector: '[kjTooltipTrigger]',
13049
+ exportAs: 'kjTooltipTrigger',
13050
+ standalone: true,
13051
+ hostDirectives: [
13052
+ { directive: KjOverlayTrigger, inputs: ['kjOpen'] },
13053
+ ],
13054
+ providers: [
13055
+ KjOverlayController,
13056
+ {
13057
+ provide: KJ_OVERLAY_TRIGGER_EVENT_STRATEGY,
13058
+ useFactory: () => onHover({ openDelay: 200, closeDelay: 0 }),
13059
+ },
13060
+ { provide: KJ_OVERLAY_PANEL_ROLE, useValue: 'tooltip' },
13061
+ ],
13062
+ }]
13063
+ }], ctorParameters: () => [], propDecorators: { kjOpenDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjOpenDelay", required: false }] }], kjCloseDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjCloseDelay", required: false }] }], kjDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjDisabled", required: false }] }] } });
13064
+
13065
+ // tooltip-content.ts
11791
13066
  class KjTooltipContent {
11792
13067
  kjSide = input('top', /* @ts-ignore */
11793
13068
  ...(ngDevMode ? [{ debugName: "kjSide" }] : /* istanbul ignore next */ []));
@@ -14000,22 +15275,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
14000
15275
  * for new code — this directive is for cases where the dismiss target id is
14001
15276
  * known outside the template context.
14002
15277
  *
15278
+ * Carries a **localized default `aria-label`** (from the `'toast.close'`
15279
+ * translation key), so an icon-only close button is named for assistive tech in
15280
+ * the active locale with no extra markup. Override per-instance with
15281
+ * `[kjToastCloseLabel]`.
15282
+ *
14003
15283
  * @example
14004
15284
  * ```html
14005
- * <button [kjToastClose]="toast.id" aria-label="Dismiss">×</button>
15285
+ * <button [kjToastClose]="toast.id">×</button>
14006
15286
  * ```
14007
15287
  * @doc-category Core/Overlay
14008
15288
  */
14009
15289
  class KjToastClose {
14010
15290
  svc = inject(KjToastService);
15291
+ i18n = inject(KjTranslateService);
14011
15292
  /** The id of the toast to dismiss on click. */
14012
15293
  kjToastClose = input.required(/* @ts-ignore */
14013
15294
  ...(ngDevMode ? [{ debugName: "kjToastClose" }] : /* istanbul ignore next */ []));
15295
+ /** Overrides the localized default `aria-label`. */
15296
+ kjToastCloseLabel = input(/* @ts-ignore */
15297
+ ...(ngDevMode ? [undefined, { debugName: "kjToastCloseLabel" }] : /* istanbul ignore next */ []));
15298
+ /** Resolved `aria-label`: explicit override, else the localized default. */
15299
+ closeLabel = computed(() => this.kjToastCloseLabel() ?? this.i18n.translate('toast.close'), /* @ts-ignore */
15300
+ ...(ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []));
14014
15301
  dismiss() {
14015
15302
  this.svc.dismiss(this.kjToastClose());
14016
15303
  }
14017
15304
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjToastClose, deps: [], target: i0.ɵɵFactoryTarget.Directive });
14018
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: KjToastClose, isStandalone: true, selector: "[kjToastClose]", inputs: { kjToastClose: { classPropertyName: "kjToastClose", publicName: "kjToastClose", isSignal: true, isRequired: true, transformFunction: null } }, host: { listeners: { "click": "dismiss()" }, classAttribute: "kj-toast-close" }, ngImport: i0 });
15305
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: KjToastClose, isStandalone: true, selector: "[kjToastClose]", inputs: { kjToastClose: { classPropertyName: "kjToastClose", publicName: "kjToastClose", isSignal: true, isRequired: true, transformFunction: null }, kjToastCloseLabel: { classPropertyName: "kjToastCloseLabel", publicName: "kjToastCloseLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "dismiss()" }, properties: { "attr.aria-label": "closeLabel()" }, classAttribute: "kj-toast-close" }, ngImport: i0 });
14019
15306
  }
14020
15307
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjToastClose, decorators: [{
14021
15308
  type: Directive,
@@ -14025,9 +15312,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
14025
15312
  host: {
14026
15313
  'class': 'kj-toast-close',
14027
15314
  '(click)': 'dismiss()',
15315
+ '[attr.aria-label]': 'closeLabel()',
14028
15316
  },
14029
15317
  }]
14030
- }], propDecorators: { kjToastClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjToastClose", required: true }] }] } });
15318
+ }], propDecorators: { kjToastClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjToastClose", required: true }] }], kjToastCloseLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjToastCloseLabel", required: false }] }] } });
14031
15319
  /**
14032
15320
  * Service-launched toast panel. Composes `KjOverlayPanel` as a host directive
14033
15321
  * so the overlay primitives wire role/state/aria management. The role is
@@ -14256,6 +15544,19 @@ class KjCommandInput {
14256
15544
  palette = inject(KjCommandPalette);
14257
15545
  nav = inject(KjListNavigator);
14258
15546
  el = inject(ElementRef);
15547
+ constructor() {
15548
+ // Reflect the query signal back onto the DOM input. The `(input)` binding
15549
+ // is one-way (DOM → signal), so an external reset of `kjQuery` (e.g. the
15550
+ // wrapper clearing it when the palette closes) would otherwise leave the
15551
+ // previous text visible on reopen. During typing the values already match,
15552
+ // so this is a no-op and never disturbs the caret position.
15553
+ effect(() => {
15554
+ const q = this.palette.query();
15555
+ const el = this.el.nativeElement;
15556
+ if (el.value !== q)
15557
+ el.value = q;
15558
+ });
15559
+ }
14259
15560
  ngOnInit() {
14260
15561
  this.palette._setNavigator(this.nav);
14261
15562
  }
@@ -14296,7 +15597,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
14296
15597
  '(input)': 'palette.setQuery($any($event.target).value)',
14297
15598
  },
14298
15599
  }]
14299
- }], propDecorators: { onKeydown: [{
15600
+ }], ctorParameters: () => [], propDecorators: { onKeydown: [{
14300
15601
  type: HostListener,
14301
15602
  args: ['keydown', ['$event']]
14302
15603
  }] } });
@@ -14608,54 +15909,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
14608
15909
  }]
14609
15910
  }], propDecorators: { kjHotkey: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjHotkey", required: false }] }] } });
14610
15911
 
14611
- /**
14612
- * Strip diacritic marks from a string (e.g. `café` → `cafe`).
14613
- * Uses NFD normalisation followed by removal of Unicode diacritic characters.
14614
- * Note: locale-naive for v1 (Turkish/German edge cases are documented).
14615
- */
14616
- function stripDiacritics(str) {
14617
- return str.normalize('NFD').replace(/\p{Diacritic}/gu, '');
14618
- }
14619
- /**
14620
- * Default filter: case- and diacritic-insensitive substring match.
14621
- * Returns score 1 if any haystack contains the needle, 0 otherwise.
14622
- * Returns 1 for empty queries (all items visible).
14623
- */
14624
- const kjSubstringFilter = (query, haystacks) => {
14625
- if (!query)
14626
- return 1;
14627
- const needle = stripDiacritics(query.toLowerCase());
14628
- return haystacks.some(h => stripDiacritics(h.toLowerCase()).includes(needle)) ? 1 : 0;
14629
- };
14630
- /**
14631
- * Optional fuzzy filter: checks whether all characters of the query appear
14632
- * in the haystack in order (abbreviation matching).
14633
- * E.g. `gth` matches `git checkout`.
14634
- * Returns score 1 if any haystack matches, 0 otherwise.
14635
- * Returns 1 for empty queries.
14636
- *
14637
- * @example
14638
- * ```html
14639
- * <div kjCommandPalette [kjFilter]="kjFuzzyFilter">…</div>
14640
- * ```
14641
- */
14642
- const kjFuzzyFilter = (query, haystacks) => {
14643
- if (!query)
14644
- return 1;
14645
- const needle = query.toLowerCase();
14646
- return haystacks.some(h => {
14647
- const hay = h.toLowerCase();
14648
- let i = 0;
14649
- for (const c of needle) {
14650
- const j = hay.indexOf(c, i);
14651
- if (j < 0)
14652
- return false;
14653
- i = j + 1;
14654
- }
14655
- return true;
14656
- }) ? 1 : 0;
14657
- };
14658
-
14659
15912
  /**
14660
15913
  * Structured filter models — a near-direct adaptation of AG-Grid's
14661
15914
  * `FilterModel` shape. Each built-in filter writes one of these models
@@ -23162,7 +24415,7 @@ function nextPanelId() {
23162
24415
  */
23163
24416
  class KjDatePicker {
23164
24417
  disabledHost = inject(KjDisabled);
23165
- defaultLocale = inject(LOCALE_ID);
24418
+ localeProvider = inject(KjLocale);
23166
24419
  /** Current selected value. Two-way bindable — `[(kjValue)]`. */
23167
24420
  kjValue = model(null, /* @ts-ignore */
23168
24421
  ...(ngDevMode ? [{ debugName: "kjValue" }] : /* istanbul ignore next */ []));
@@ -23175,7 +24428,7 @@ class KjDatePicker {
23175
24428
  /** Per-date predicate. */
23176
24429
  kjDisabledDates = input(null, /* @ts-ignore */
23177
24430
  ...(ngDevMode ? [{ debugName: "kjDisabledDates" }] : /* istanbul ignore next */ []));
23178
- /** BCP-47 locale tag. Defaults to Angular's `LOCALE_ID`. */
24431
+ /** BCP-47 locale tag. Falls back to the `KjLocale` provider (`provideKjLocale`). */
23179
24432
  kjLocale = input('', /* @ts-ignore */
23180
24433
  ...(ngDevMode ? [{ debugName: "kjLocale" }] : /* istanbul ignore next */ []));
23181
24434
  /** First day of the week override (0=Sun … 6=Sat). */
@@ -23205,7 +24458,7 @@ class KjDatePicker {
23205
24458
  ...(ngDevMode ? [{ debugName: "maxDate" }] : /* istanbul ignore next */ []));
23206
24459
  disabledDates = computed(() => this.kjDisabledDates(), /* @ts-ignore */
23207
24460
  ...(ngDevMode ? [{ debugName: "disabledDates" }] : /* istanbul ignore next */ []));
23208
- locale = computed(() => this.kjLocale() || this.defaultLocale, /* @ts-ignore */
24461
+ locale = computed(() => this.kjLocale() || this.localeProvider.locale(), /* @ts-ignore */
23209
24462
  ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
23210
24463
  disabled = this.disabledHost.disabled;
23211
24464
  readonly = computed(() => this.kjReadonly(), /* @ts-ignore */
@@ -23319,9 +24572,20 @@ class KjDatePickerTrigger {
23319
24572
  el = inject(ElementRef);
23320
24573
  /** Cached typed text — avoids overwriting mid-edit. */
23321
24574
  editing = false;
24575
+ /**
24576
+ * Optional custom display formatter, e.g. for datetime pickers that show
24577
+ * the time next to the date. Free-text parsing (`commitTyped`) still uses
24578
+ * the locale date parser — text that doesn't parse as a plain date is
24579
+ * reverted to the formatted value on the next value change.
24580
+ */
24581
+ kjDisplayFormat = input(null, /* @ts-ignore */
24582
+ ...(ngDevMode ? [{ debugName: "kjDisplayFormat" }] : /* istanbul ignore next */ []));
23322
24583
  displayValue = computed(() => {
23323
24584
  const v = this.ctx.value();
23324
- return v ? formatDateShort(v, this.ctx.locale()) : '';
24585
+ if (!v)
24586
+ return '';
24587
+ const fmt = this.kjDisplayFormat();
24588
+ return fmt ? fmt(v, this.ctx.locale()) : formatDateShort(v, this.ctx.locale());
23325
24589
  }, /* @ts-ignore */
23326
24590
  ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
23327
24591
  constructor() {
@@ -23425,7 +24689,7 @@ class KjDatePickerTrigger {
23425
24689
  return false;
23426
24690
  }
23427
24691
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjDatePickerTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
23428
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.5", type: KjDatePickerTrigger, isStandalone: true, selector: "input[kjDatePickerTrigger]", host: { attributes: { "role": "combobox", "autocomplete": "off", "spellcheck": "false" }, listeners: { "input": "onInput($event)", "blur": "onBlur()", "keydown": "onKeydown($event)" }, properties: { "attr.aria-disabled": "ctx.disabled() ? \"true\" : null", "attr.aria-readonly": "ctx.readonly() ? \"true\" : null", "attr.disabled": "ctx.disabled() ? \"\" : null", "attr.readonly": "ctx.readonly() ? \"\" : null" } }, providers: [
24692
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: KjDatePickerTrigger, isStandalone: true, selector: "input[kjDatePickerTrigger]", inputs: { kjDisplayFormat: { classPropertyName: "kjDisplayFormat", publicName: "kjDisplayFormat", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "combobox", "autocomplete": "off", "spellcheck": "false" }, listeners: { "input": "onInput($event)", "blur": "onBlur()", "keydown": "onKeydown($event)" }, properties: { "attr.aria-disabled": "ctx.disabled() ? \"true\" : null", "attr.aria-readonly": "ctx.readonly() ? \"true\" : null", "attr.disabled": "ctx.disabled() ? \"\" : null", "attr.readonly": "ctx.readonly() ? \"\" : null" } }, providers: [
23429
24693
  KjOverlayController,
23430
24694
  { provide: KJ_OVERLAY_TRIGGER_EVENT_STRATEGY, useFactory: () => clickOrFocus() },
23431
24695
  { provide: KJ_OVERLAY_PANEL_ROLE, useValue: 'dialog' },
@@ -23460,7 +24724,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
23460
24724
  '(keydown)': 'onKeydown($event)',
23461
24725
  },
23462
24726
  }]
23463
- }], ctorParameters: () => [] });
24727
+ }], ctorParameters: () => [], propDecorators: { kjDisplayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjDisplayFormat", required: false }] }] } });
23464
24728
 
23465
24729
  /**
23466
24730
  * Marker directive for the calendar slot of a Date Picker. Composes the
@@ -23528,58 +24792,1711 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
23528
24792
  }], ctorParameters: () => [], propDecorators: { kjSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjSide", required: false }] }], kjAlign: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjAlign", required: false }] }], kjOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjOffset", required: false }] }] } });
23529
24793
 
23530
24794
  /**
23531
- * Wraps Apache ECharts. Initializes after first render, updates reactively, disposes on destroy.
23532
- * Always provide `kjChartLabel` for WCAG AAA compliance.
24795
+ * Resolves a preset against `now`, normalizing both bounds to `startOfDay`.
24796
+ * `null` when the preset produces an inverted range (`start > end`).
24797
+ */
24798
+ function resolveDateRangePreset(preset, now) {
24799
+ const raw = preset.getRange(now);
24800
+ const start = startOfDay(raw.start);
24801
+ const end = startOfDay(raw.end);
24802
+ if (start.getTime() > end.getTime())
24803
+ return null;
24804
+ return { start, end };
24805
+ }
24806
+ const KJ_DATE_RANGE_PRESETS = new InjectionToken('KjDateRangePresets');
24807
+
24808
+ /** First day of the week containing `date`, given a week start (0=Sun…6=Sat). */
24809
+ function startOfWeek(date, weekStartsOn) {
24810
+ const day = startOfDay(date);
24811
+ const diff = (day.getDay() - weekStartsOn + 7) % 7;
24812
+ return addDays(day, -diff);
24813
+ }
24814
+ /** First day of the calendar quarter containing `date`. */
24815
+ function startOfQuarter(date) {
24816
+ const q = Math.floor(date.getMonth() / 3) * 3;
24817
+ return new Date(date.getFullYear(), q, 1);
24818
+ }
24819
+ /**
24820
+ * The built-in date range presets — Today, Yesterday, Last 7 / 30 days, This
24821
+ * week / month, Last month, This quarter, Year to date, Last year.
23533
24822
  *
23534
- * @example
24823
+ * All ranges are inclusive of both bounds. `Last 7 days` spans 7 calendar days
24824
+ * *including* today (today − 6 … today), matching how analytics tools count.
24825
+ *
24826
+ * @param weekStartsOn - First day of the week (0=Sun … 6=Sat) used by the
24827
+ * `This week` preset. Defaults to Sunday; pass the locale's week start to
24828
+ * align with the calendar.
24829
+ *
24830
+ * @doc-category Core/Data input
24831
+ * @doc
24832
+ * @doc-name date-range-presets
24833
+ */
24834
+ function defaultDateRangePresets(weekStartsOn = 0) {
24835
+ return [
24836
+ {
24837
+ id: 'today',
24838
+ label: 'Today',
24839
+ getRange: (now) => ({ start: now, end: now }),
24840
+ },
24841
+ {
24842
+ id: 'yesterday',
24843
+ label: 'Yesterday',
24844
+ getRange: (now) => {
24845
+ const d = addDays(now, -1);
24846
+ return { start: d, end: d };
24847
+ },
24848
+ },
24849
+ {
24850
+ id: 'last-7-days',
24851
+ label: 'Last 7 days',
24852
+ getRange: (now) => ({ start: addDays(now, -6), end: now }),
24853
+ },
24854
+ {
24855
+ id: 'last-30-days',
24856
+ label: 'Last 30 days',
24857
+ getRange: (now) => ({ start: addDays(now, -29), end: now }),
24858
+ },
24859
+ {
24860
+ id: 'this-week',
24861
+ label: 'This week',
24862
+ getRange: (now) => ({ start: startOfWeek(now, weekStartsOn), end: now }),
24863
+ },
24864
+ {
24865
+ id: 'this-month',
24866
+ label: 'This month',
24867
+ getRange: (now) => ({ start: startOfMonth(now), end: now }),
24868
+ },
24869
+ {
24870
+ id: 'last-month',
24871
+ label: 'Last month',
24872
+ getRange: (now) => {
24873
+ const prev = new Date(now.getFullYear(), now.getMonth() - 1, 1);
24874
+ return { start: startOfMonth(prev), end: endOfMonth(prev) };
24875
+ },
24876
+ },
24877
+ {
24878
+ id: 'this-quarter',
24879
+ label: 'This quarter',
24880
+ getRange: (now) => ({ start: startOfQuarter(now), end: now }),
24881
+ },
24882
+ {
24883
+ id: 'year-to-date',
24884
+ label: 'Year to date',
24885
+ getRange: (now) => ({ start: new Date(now.getFullYear(), 0, 1), end: now }),
24886
+ },
24887
+ {
24888
+ id: 'last-year',
24889
+ label: 'Last year',
24890
+ getRange: (now) => ({
24891
+ start: new Date(now.getFullYear() - 1, 0, 1),
24892
+ end: new Date(now.getFullYear() - 1, 11, 31),
24893
+ }),
24894
+ },
24895
+ ];
24896
+ }
24897
+
24898
+ /**
24899
+ * Headless Date Range Presets listbox. Renders a set of named quick-selects
24900
+ * ("Last 7 days", "This quarter", …) as `role="option"` children; picking one
24901
+ * resolves its `{ start, end }` range and commits the two-way `kjValue`.
24902
+ *
24903
+ * Designed to slot beside a range calendar, but usable standalone against any
24904
+ * `signal<KjDateRange | null>`.
24905
+ *
24906
+ * **Compound shape:**
24907
+ *
24908
+ * ```html
24909
+ * <div kjDateRangePresets [(kjValue)]="range">
24910
+ * @for (p of presets.presets(); track p.id) {
24911
+ * <button kjDateRangePresetOption [kjPreset]="p">{{ p.label }}</button>
24912
+ * }
24913
+ * </div>
24914
+ * ```
24915
+ *
24916
+ * Composes {@link KjRovingTabindex} (vertical) so the whole list is a single
24917
+ * tab stop with Arrow / Home / End navigation.
24918
+ *
24919
+ * @doc-category Core/Data input
24920
+ * @doc
24921
+ * @doc-name date-range-presets
24922
+ * @doc-description Unstyled listbox of named date-range quick-selects that resolve to an inclusive `{ start, end }` range.
24923
+ * @doc-is-main
24924
+ */
24925
+ class KjDateRangePresets {
24926
+ disabledHost = inject(KjDisabled);
24927
+ /** Selected range. Two-way bindable — `[(kjValue)]`. `null` when empty. */
24928
+ kjValue = model(null, /* @ts-ignore */
24929
+ ...(ngDevMode ? [{ debugName: "kjValue" }] : /* istanbul ignore next */ []));
24930
+ /** Presets to render as options. Defaults to {@link defaultDateRangePresets}. */
24931
+ kjPresets = input(defaultDateRangePresets(), /* @ts-ignore */
24932
+ ...(ngDevMode ? [{ debugName: "kjPresets" }] : /* istanbul ignore next */ []));
24933
+ /** Accessible name for the listbox. */
24934
+ kjLabel = input('Date range presets', /* @ts-ignore */
24935
+ ...(ngDevMode ? [{ debugName: "kjLabel" }] : /* istanbul ignore next */ []));
24936
+ /**
24937
+ * Injectable "now" for the preset math — defaults to the current instant.
24938
+ * Pass a fixed `Date` to freeze "today" (tests, storybook, replay).
24939
+ */
24940
+ kjNow = input(null, /* @ts-ignore */
24941
+ ...(ngDevMode ? [{ debugName: "kjNow" }] : /* istanbul ignore next */ []));
24942
+ /** Read-only — value displays but cannot be edited. */
24943
+ kjReadonly = input(false, { ...(ngDevMode ? { debugName: "kjReadonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
24944
+ // ── KjDateRangePresetsContext implementation ───────────────────────
24945
+ presets = computed(() => this.kjPresets(), /* @ts-ignore */
24946
+ ...(ngDevMode ? [{ debugName: "presets" }] : /* istanbul ignore next */ []));
24947
+ disabled = this.disabledHost.disabled;
24948
+ /**
24949
+ * Id of the preset whose resolved range matches `kjValue`, or `null`. Derived
24950
+ * from the value so an externally-set range still highlights its preset.
24951
+ */
24952
+ selectedId = computed(() => {
24953
+ const value = this.kjValue();
24954
+ if (!value)
24955
+ return null;
24956
+ const now = this.now();
24957
+ for (const preset of this.presets()) {
24958
+ const range = resolveDateRangePreset(preset, now);
24959
+ if (range
24960
+ && range.start.getTime() === value.start.getTime()
24961
+ && range.end.getTime() === value.end.getTime()) {
24962
+ return preset.id;
24963
+ }
24964
+ }
24965
+ return null;
24966
+ }, /* @ts-ignore */
24967
+ ...(ngDevMode ? [{ debugName: "selectedId" }] : /* istanbul ignore next */ []));
24968
+ now() {
24969
+ return this.kjNow() ?? new Date();
24970
+ }
24971
+ select(preset) {
24972
+ if (this.disabled() || this.kjReadonly())
24973
+ return;
24974
+ const range = resolveDateRangePreset(preset, this.now());
24975
+ if (range)
24976
+ this.kjValue.set(range);
24977
+ }
24978
+ isSelected(id) {
24979
+ return this.selectedId() === id;
24980
+ }
24981
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjDateRangePresets, deps: [], target: i0.ɵɵFactoryTarget.Directive });
24982
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: KjDateRangePresets, isStandalone: true, selector: "[kjDateRangePresets]", inputs: { kjValue: { classPropertyName: "kjValue", publicName: "kjValue", isSignal: true, isRequired: false, transformFunction: null }, kjPresets: { classPropertyName: "kjPresets", publicName: "kjPresets", isSignal: true, isRequired: false, transformFunction: null }, kjLabel: { classPropertyName: "kjLabel", publicName: "kjLabel", isSignal: true, isRequired: false, transformFunction: null }, kjNow: { classPropertyName: "kjNow", publicName: "kjNow", isSignal: true, isRequired: false, transformFunction: null }, kjReadonly: { classPropertyName: "kjReadonly", publicName: "kjReadonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { kjValue: "kjValueChange" }, host: { attributes: { "role": "listbox", "aria-orientation": "vertical" }, properties: { "attr.aria-label": "kjLabel()", "attr.aria-disabled": "disabled() ? \"true\" : null" } }, providers: [
24983
+ { provide: KJ_DATE_RANGE_PRESETS, useExisting: KjDateRangePresets },
24984
+ ], exportAs: ["kjDateRangePresets"], hostDirectives: [{ directive: KjDisabled, inputs: ["kjDisabled", "kjDisabled"] }, { directive: KjRovingTabindex }], ngImport: i0 });
24985
+ }
24986
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjDateRangePresets, decorators: [{
24987
+ type: Directive,
24988
+ args: [{
24989
+ selector: '[kjDateRangePresets]',
24990
+ standalone: true,
24991
+ exportAs: 'kjDateRangePresets',
24992
+ hostDirectives: [
24993
+ { directive: KjDisabled, inputs: ['kjDisabled'] },
24994
+ KjRovingTabindex,
24995
+ ],
24996
+ providers: [
24997
+ { provide: KJ_DATE_RANGE_PRESETS, useExisting: KjDateRangePresets },
24998
+ ],
24999
+ host: {
25000
+ 'role': 'listbox',
25001
+ 'aria-orientation': 'vertical',
25002
+ '[attr.aria-label]': 'kjLabel()',
25003
+ '[attr.aria-disabled]': 'disabled() ? "true" : null',
25004
+ },
25005
+ }]
25006
+ }], propDecorators: { kjValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjValue", required: false }] }, { type: i0.Output, args: ["kjValueChange"] }], kjPresets: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjPresets", required: false }] }], kjLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjLabel", required: false }] }], kjNow: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjNow", required: false }] }], kjReadonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjReadonly", required: false }] }] } });
25007
+
25008
+ /**
25009
+ * One option inside a `[kjDateRangePresets]` listbox. Apply to a native
25010
+ * `<button>` so Enter / Space activation comes for free; the composed
25011
+ * {@link KjRovingTabindexItemDirective} manages its `tabindex` so the list is
25012
+ * a single tab stop.
25013
+ *
25014
+ * ```html
25015
+ * <button kjDateRangePresetOption [kjPreset]="preset">{{ preset.label }}</button>
25016
+ * ```
25017
+ *
25018
+ * @doc-category Core/Data input
25019
+ * @doc
25020
+ * @doc-name date-range-presets
25021
+ */
25022
+ class KjDateRangePresetOption {
25023
+ /** @internal */
25024
+ ctx = inject(KJ_DATE_RANGE_PRESETS);
25025
+ /** The preset this option represents. */
25026
+ kjPreset = input.required(/* @ts-ignore */
25027
+ ...(ngDevMode ? [{ debugName: "kjPreset" }] : /* istanbul ignore next */ []));
25028
+ /** Whether this option is the selected one. */
25029
+ selected = computed(() => this.ctx.isSelected(this.kjPreset().id), /* @ts-ignore */
25030
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
25031
+ /** @internal */
25032
+ onClick() {
25033
+ this.ctx.select(this.kjPreset());
25034
+ }
25035
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjDateRangePresetOption, deps: [], target: i0.ɵɵFactoryTarget.Directive });
25036
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: KjDateRangePresetOption, isStandalone: true, selector: "button[kjDateRangePresetOption]", inputs: { kjPreset: { classPropertyName: "kjPreset", publicName: "kjPreset", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "type": "button", "role": "option" }, listeners: { "click": "onClick()" }, properties: { "attr.aria-selected": "selected() ? \"true\" : \"false\"", "attr.disabled": "ctx.disabled() ? \"\" : null" } }, exportAs: ["kjDateRangePresetOption"], hostDirectives: [{ directive: KjRovingTabindexItemDirective }], ngImport: i0 });
25037
+ }
25038
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjDateRangePresetOption, decorators: [{
25039
+ type: Directive,
25040
+ args: [{
25041
+ selector: 'button[kjDateRangePresetOption]',
25042
+ standalone: true,
25043
+ exportAs: 'kjDateRangePresetOption',
25044
+ hostDirectives: [KjRovingTabindexItemDirective],
25045
+ host: {
25046
+ 'type': 'button',
25047
+ 'role': 'option',
25048
+ '[attr.aria-selected]': 'selected() ? "true" : "false"',
25049
+ '[attr.disabled]': 'ctx.disabled() ? "" : null',
25050
+ '(click)': 'onClick()',
25051
+ },
25052
+ }]
25053
+ }], propDecorators: { kjPreset: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjPreset", required: true }] }] } });
25054
+
25055
+ /**
25056
+ * Resolves the chart color palette from kj theme tokens on the given host element.
25057
+ * Reads `--kj-chart-1..6` first; for any empty slot, falls back to the matching
25058
+ * intent token (`--kj-bg-primary`, `--kj-bg-accent`, `--kj-bg-success`,
25059
+ * `--kj-bg-warning`, `--kj-bg-danger`) in that order. Slots that remain empty
25060
+ * after fallback are dropped.
25061
+ */
25062
+ function resolveChartPalette(host) {
25063
+ const cs = getComputedStyle(host);
25064
+ const fallbacks = [
25065
+ '--kj-bg-primary',
25066
+ '--kj-bg-accent',
25067
+ '--kj-bg-success',
25068
+ '--kj-bg-warning',
25069
+ '--kj-bg-danger',
25070
+ ];
25071
+ const out = [];
25072
+ for (let i = 0; i < 6; i++) {
25073
+ const chart = cs.getPropertyValue(`--kj-chart-${i + 1}`).trim();
25074
+ if (chart) {
25075
+ out.push(chart);
25076
+ continue;
25077
+ }
25078
+ const fallback = fallbacks[i];
25079
+ if (fallback) {
25080
+ const v = cs.getPropertyValue(fallback).trim();
25081
+ if (v)
25082
+ out.push(v);
25083
+ }
25084
+ }
25085
+ return out;
25086
+ }
25087
+
25088
+ /**
25089
+ * Projects a screen-reader-only table fallback for a `KjChart`. When present
25090
+ * inside a `[kjChart]` host, the host directive renders the template as a table
25091
+ * *sibling* of the chart element — outside the `role="img"` subtree — so
25092
+ * assistive technology reads structured data instead of the canvas.
25093
+ *
25094
+ * This directive only exposes its `TemplateRef`; `KjChart` performs the
25095
+ * rendering (see its `_fallback` content query). Rendering it standalone,
25096
+ * without a `[kjChart]` host, produces no output.
25097
+ *
25098
+ * @example
25099
+ * ```html
25100
+ * <div kjChart [kjChartOption]="opt()" kjChartLabel="Sales">
25101
+ * <ng-container *kjChartTableFallback>
25102
+ * <table>...</table>
25103
+ * </ng-container>
25104
+ * </div>
25105
+ * ```
25106
+ */
25107
+ class KjChartTableFallback {
25108
+ tpl = inject((TemplateRef));
25109
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjChartTableFallback, deps: [], target: i0.ɵɵFactoryTarget.Directive });
25110
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.5", type: KjChartTableFallback, isStandalone: true, selector: "[kjChartTableFallback]", ngImport: i0 });
25111
+ }
25112
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjChartTableFallback, decorators: [{
25113
+ type: Directive,
25114
+ args: [{
25115
+ selector: '[kjChartTableFallback]',
25116
+ standalone: true,
25117
+ }]
25118
+ }] });
25119
+
25120
+ /**
25121
+ * DI token holding the optional {@link KjEChartsLoader}. When unset (default),
25122
+ * {@link KjChart} falls back to a dynamic `import('echarts')` of the full
25123
+ * build — zero-config convenience at the cost of bundle size.
25124
+ *
25125
+ * Prefer {@link provideECharts} over binding this token directly.
25126
+ * @doc
25127
+ * @doc-name chart
25128
+ * @doc-order 2
25129
+ */
25130
+ const KJ_ECHARTS = new InjectionToken('KJ_ECHARTS', { providedIn: 'root', factory: () => null });
25131
+ /**
25132
+ * Registers a tree-shaken ECharts build for {@link KjChart}. Call at app
25133
+ * bootstrap (or a route's `providers`) so every `[kjChart]` uses the minimal
25134
+ * engine instead of the full `import('echarts')` fallback.
25135
+ *
25136
+ * @example
25137
+ * ```ts
25138
+ * // main.ts
25139
+ * import { provideECharts } from '@kouji-ui/core';
25140
+ * import * as echarts from 'echarts/core';
25141
+ * import { LineChart, BarChart } from 'echarts/charts';
25142
+ * import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components';
25143
+ * import { CanvasRenderer } from 'echarts/renderers';
25144
+ *
25145
+ * echarts.use([LineChart, BarChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer]);
25146
+ *
25147
+ * bootstrapApplication(App, {
25148
+ * providers: [provideECharts(() => echarts)],
25149
+ * });
25150
+ * ```
25151
+ * @doc
25152
+ * @doc-name chart
25153
+ * @doc-order 1
25154
+ */
25155
+ function provideECharts(loader) {
25156
+ return makeEnvironmentProviders([
25157
+ { provide: KJ_ECHARTS, useValue: loader },
25158
+ ]);
25159
+ }
25160
+
25161
+ let nextDescId = 0;
25162
+ /**
25163
+ * Wraps Apache ECharts. Initializes after first render, updates reactively
25164
+ * (resize, reduced-motion, kj theme palette), disposes on destroy.
25165
+ * Always provide `kjChartLabel` for WCAG AAA compliance.
25166
+ *
25167
+ * @example
25168
+ * ```html
25169
+ * <div kjChart [kjChartOption]="chartOption()" kjChartLabel="Monthly revenue" style="height:300px"></div>
25170
+ * ```
25171
+ * @doc-category Core/Data
25172
+ * @doc
25173
+ * @doc-name chart
25174
+ * @doc-description Renders a reactive ECharts chart on any sized element with an accessible label.
25175
+ * @doc-is-main
25176
+ * @doc-example Line
25177
+ * @doc-file chart.example.ts
25178
+ * @doc-example Bar
25179
+ * @doc-file chart.bar.example.ts
25180
+ * @doc-example Donut
25181
+ * @doc-file chart.donut.example.ts
25182
+ * @doc-example Area
25183
+ * @doc-file chart.area.example.ts
25184
+ * @doc-example Sparkline
25185
+ * @doc-file chart.sparkline.example.ts
25186
+ * @doc-example Events
25187
+ * @doc-file chart.events.example.ts
25188
+ * @doc-example Loading
25189
+ * @doc-file chart.loading.example.ts
25190
+ * @doc-example Table fallback
25191
+ * @doc-file chart.fallback.example.ts
25192
+ * @doc-example Pluggable engine + general events
25193
+ * @doc-file chart.pluggable.example.ts
25194
+ */
25195
+ class KjChart {
25196
+ el = inject(ElementRef);
25197
+ destroyRef = inject(DestroyRef);
25198
+ vcr = inject(ViewContainerRef);
25199
+ /** Optional consumer-supplied ECharts loader (via `provideECharts`); null → full-import fallback. */
25200
+ echartsLoader = inject(KJ_ECHARTS, { optional: true });
25201
+ /** ECharts option object defining the chart. */
25202
+ kjChartOption = input.required(/* @ts-ignore */
25203
+ ...(ngDevMode ? [{ debugName: "kjChartOption" }] : /* istanbul ignore next */ []));
25204
+ /** Accessible short label for the chart. Required for WCAG AAA compliance. */
25205
+ kjChartLabel = input.required(/* @ts-ignore */
25206
+ ...(ngDevMode ? [{ debugName: "kjChartLabel" }] : /* istanbul ignore next */ []));
25207
+ /** Longer description; rendered visually-hidden and wired via aria-describedby. */
25208
+ kjChartDescription = input('', /* @ts-ignore */
25209
+ ...(ngDevMode ? [{ debugName: "kjChartDescription" }] : /* istanbul ignore next */ []));
25210
+ /** Toggles ECharts showLoading/hideLoading. */
25211
+ kjChartLoading = input(false, /* @ts-ignore */
25212
+ ...(ngDevMode ? [{ debugName: "kjChartLoading" }] : /* istanbul ignore next */ []));
25213
+ /** Explicit color array; falls back to kj theme palette (resolveChartPalette) when undefined. */
25214
+ kjChartPalette = input(undefined, /* @ts-ignore */
25215
+ ...(ngDevMode ? [{ debugName: "kjChartPalette" }] : /* istanbul ignore next */ []));
25216
+ /** Honored unless prefers-reduced-motion: reduce is set. */
25217
+ kjChartAnimate = input(true, /* @ts-ignore */
25218
+ ...(ngDevMode ? [{ debugName: "kjChartAnimate" }] : /* istanbul ignore next */ []));
25219
+ /**
25220
+ * ECharts event names to forward through `(kjChartEvent)`. Bound via
25221
+ * `chart.on(name, …)` and re-bound reactively when this list changes.
25222
+ * e.g. `['click', 'datazoom', 'legendselectchanged']`.
25223
+ */
25224
+ kjChartOn = input([], /* @ts-ignore */
25225
+ ...(ngDevMode ? [{ debugName: "kjChartOn" }] : /* istanbul ignore next */ []));
25226
+ /** Emits the ECharts instance after its first `setOption` (ready with data). Re-emits on re-init. */
25227
+ kjChartReady = output();
25228
+ /**
25229
+ * Emits `{ type, params }` for every ECharts event named in `kjChartOn`.
25230
+ * Use this for arbitrary events; `kjChartReady` still exposes the raw
25231
+ * instance for full manual `.on(...)` wiring.
25232
+ */
25233
+ kjChartEvent = output();
25234
+ /** Emits ECharts 'click' events. Convenience — also available via `kjChartOn`. */
25235
+ kjChartClick = output();
25236
+ /** Emits ECharts 'legendselectchanged' events. Convenience — also available via `kjChartOn`. */
25237
+ kjChartLegendSelect = output();
25238
+ /** Unique id for the description div; used by host's aria-describedby binding. */
25239
+ descriptionId = computed(() => this.kjChartDescription() ? `kj-chart-desc-${this._descSeq}` : '', /* @ts-ignore */
25240
+ ...(ngDevMode ? [{ debugName: "descriptionId" }] : /* istanbul ignore next */ []));
25241
+ _descSeq = ++nextDescId;
25242
+ /** Projected `*kjChartTableFallback`, if any. Rendered as an SR table sibling. */
25243
+ _fallback = contentChild(KjChartTableFallback, /* @ts-ignore */
25244
+ ...(ngDevMode ? [{ debugName: "_fallback" }] : /* istanbul ignore next */ []));
25245
+ /** The live ECharts instance. A signal so event-binding + loading effects react to init/dispose. */
25246
+ chart = signal(null, /* @ts-ignore */
25247
+ ...(ngDevMode ? [{ debugName: "chart" }] : /* istanbul ignore next */ []));
25248
+ prefersReducedMotion = signal(false, /* @ts-ignore */
25249
+ ...(ngDevMode ? [{ debugName: "prefersReducedMotion" }] : /* istanbul ignore next */ []));
25250
+ /** Currently-bound `kjChartOn` forwarders, tracked so they can be unbound on re-bind/destroy. */
25251
+ forwarded = [];
25252
+ constructor() {
25253
+ afterNextRender(async () => {
25254
+ try {
25255
+ // Resolve ECharts from DI: a consumer-provided (tree-shaken) build via
25256
+ // provideECharts, else fall back to a dynamic import of the full module.
25257
+ const echarts = this.echartsLoader
25258
+ ? await this.echartsLoader()
25259
+ : await import('echarts');
25260
+ const chart = echarts.init(this.el.nativeElement);
25261
+ // prefers-reduced-motion — subscribe and re-apply on change. Guarded:
25262
+ // matchMedia is absent in some non-browser/test environments.
25263
+ const mql = typeof window !== 'undefined' && typeof window.matchMedia === 'function'
25264
+ ? window.matchMedia('(prefers-reduced-motion: reduce)')
25265
+ : null;
25266
+ if (mql) {
25267
+ this.prefersReducedMotion.set(mql.matches);
25268
+ const onMqlChange = () => {
25269
+ this.prefersReducedMotion.set(mql.matches);
25270
+ chart.setOption(this.resolveOption());
25271
+ };
25272
+ mql.addEventListener('change', onMqlChange);
25273
+ this.destroyRef.onDestroy(() => mql.removeEventListener('change', onMqlChange));
25274
+ }
25275
+ // First setOption populates the chart, THEN we publish it — so both the
25276
+ // signal-driven effects (events, loading) and kjChartReady observers get
25277
+ // an instance that is already showing data.
25278
+ chart.setOption(this.resolveOption());
25279
+ this.chart.set(chart);
25280
+ this.kjChartReady.emit(chart);
25281
+ // Convenience events — always emitted regardless of kjChartOn.
25282
+ chart.on('click', (e) => this.kjChartClick.emit(e));
25283
+ chart.on('legendselectchanged', (e) => this.kjChartLegendSelect.emit(e));
25284
+ // Initial general-event binding (the reactive effect below re-binds on
25285
+ // any later kjChartOn change; this guarantees the first bind even before
25286
+ // the next change-detection pass).
25287
+ this.bindForwardedEvents(chart, this.kjChartOn());
25288
+ // ResizeObserver — coalesce via rAF so a burst of entries collapses to one resize.
25289
+ // Guarded: ResizeObserver is absent in some non-browser environments.
25290
+ if (typeof ResizeObserver !== 'undefined') {
25291
+ let pendingRaf = 0;
25292
+ const ro = new ResizeObserver(() => {
25293
+ if (pendingRaf)
25294
+ return;
25295
+ pendingRaf = requestAnimationFrame(() => {
25296
+ pendingRaf = 0;
25297
+ chart.resize();
25298
+ });
25299
+ });
25300
+ ro.observe(this.el.nativeElement);
25301
+ this.destroyRef.onDestroy(() => {
25302
+ if (pendingRaf)
25303
+ cancelAnimationFrame(pendingRaf);
25304
+ ro.disconnect();
25305
+ });
25306
+ }
25307
+ // Theme changes on <html> re-resolve the kj palette and re-apply the
25308
+ // option. This never disposes the instance, so kjChartReady fires once.
25309
+ if (typeof MutationObserver !== 'undefined') {
25310
+ const themeMo = new MutationObserver(() => chart.setOption(this.resolveOption()));
25311
+ themeMo.observe(document.documentElement, {
25312
+ attributes: true,
25313
+ attributeFilter: ['class', 'data-theme'],
25314
+ });
25315
+ this.destroyRef.onDestroy(() => themeMo.disconnect());
25316
+ }
25317
+ this.destroyRef.onDestroy(() => {
25318
+ chart.dispose();
25319
+ this.chart.set(null);
25320
+ });
25321
+ }
25322
+ catch {
25323
+ // ECharts cannot initialize in non-browser environments (jsdom, SSR)
25324
+ }
25325
+ });
25326
+ afterEveryRender(() => {
25327
+ this.chart()?.setOption(this.resolveOption());
25328
+ });
25329
+ // General event API — re-forward kjChartOn through (kjChartEvent) whenever
25330
+ // the list changes (the initial bind happens imperatively at init).
25331
+ // bindForwardedEvents is idempotent, so a redundant first run is harmless.
25332
+ effect(() => {
25333
+ const names = this.kjChartOn();
25334
+ const chart = this.chart();
25335
+ if (chart)
25336
+ this.bindForwardedEvents(chart, names);
25337
+ });
25338
+ // Loading overlay driven reactively by [kjChartLoading].
25339
+ effect(() => {
25340
+ const loading = this.kjChartLoading();
25341
+ const chart = this.chart();
25342
+ if (!chart)
25343
+ return;
25344
+ if (loading)
25345
+ chart.showLoading();
25346
+ else
25347
+ chart.hideLoading();
25348
+ });
25349
+ // Visually-hidden description element, referenced by the host's aria-describedby.
25350
+ let descDiv = null;
25351
+ effect(() => {
25352
+ const text = this.kjChartDescription();
25353
+ const id = this.descriptionId();
25354
+ const host = this.el.nativeElement;
25355
+ if (!text) {
25356
+ descDiv?.remove();
25357
+ descDiv = null;
25358
+ return;
25359
+ }
25360
+ if (!descDiv) {
25361
+ descDiv = document.createElement('div');
25362
+ Object.assign(descDiv.style, {
25363
+ position: 'absolute',
25364
+ width: '1px',
25365
+ height: '1px',
25366
+ padding: '0',
25367
+ margin: '-1px',
25368
+ overflow: 'hidden',
25369
+ clip: 'rect(0 0 0 0)',
25370
+ whiteSpace: 'nowrap',
25371
+ border: '0',
25372
+ });
25373
+ host.appendChild(descDiv);
25374
+ }
25375
+ descDiv.id = id;
25376
+ descDiv.textContent = text;
25377
+ });
25378
+ // Project a *kjChartTableFallback as an SR table *outside* the role="img"
25379
+ // host (as a sibling), so assistive tech reads structured data while the
25380
+ // canvas subtree stays presentational.
25381
+ effect(() => {
25382
+ const fb = this._fallback();
25383
+ this.vcr.clear();
25384
+ if (fb)
25385
+ this.vcr.createEmbeddedView(fb.tpl);
25386
+ });
25387
+ }
25388
+ /**
25389
+ * (Re)binds the `kjChartOn` event forwarders: unbinds the previous set, then
25390
+ * binds `chart.on(name, …)` for each name, emitting `(kjChartEvent)`.
25391
+ * Idempotent — safe to call from both init and the reactive effect.
25392
+ */
25393
+ bindForwardedEvents(chart, names) {
25394
+ for (const { name, handler } of this.forwarded)
25395
+ chart.off(name, handler);
25396
+ this.forwarded = names.map((name) => {
25397
+ const handler = (params) => this.kjChartEvent.emit({ type: name, params });
25398
+ chart.on(name, handler);
25399
+ return { name, handler };
25400
+ });
25401
+ }
25402
+ /** Merges reactive concerns (palette, reduced-motion) into the user option. */
25403
+ resolveOption() {
25404
+ const base = this.kjChartOption();
25405
+ const animate = this.kjChartAnimate() && !this.prefersReducedMotion();
25406
+ const explicit = this.kjChartPalette();
25407
+ const color = explicit ?? resolveChartPalette(this.el.nativeElement);
25408
+ return {
25409
+ ...base,
25410
+ color: color.length ? color : base.color,
25411
+ animation: animate,
25412
+ animationDuration: animate
25413
+ ? base.animationDuration ?? 1000
25414
+ : 0,
25415
+ };
25416
+ }
25417
+ /** Imperative resize — wraps chart.resize(). */
25418
+ resize() {
25419
+ this.chart()?.resize();
25420
+ }
25421
+ /** Imperative dispatch — passes through to ECharts. */
25422
+ dispatchAction(payload) {
25423
+ this.chart()?.dispatchAction(payload);
25424
+ }
25425
+ /** Reads current option — passes through to ECharts. */
25426
+ getOption() {
25427
+ return this.chart()?.getOption();
25428
+ }
25429
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjChart, deps: [], target: i0.ɵɵFactoryTarget.Directive });
25430
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.0.5", type: KjChart, isStandalone: true, selector: "[kjChart]", inputs: { kjChartOption: { classPropertyName: "kjChartOption", publicName: "kjChartOption", isSignal: true, isRequired: true, transformFunction: null }, kjChartLabel: { classPropertyName: "kjChartLabel", publicName: "kjChartLabel", isSignal: true, isRequired: true, transformFunction: null }, kjChartDescription: { classPropertyName: "kjChartDescription", publicName: "kjChartDescription", isSignal: true, isRequired: false, transformFunction: null }, kjChartLoading: { classPropertyName: "kjChartLoading", publicName: "kjChartLoading", isSignal: true, isRequired: false, transformFunction: null }, kjChartPalette: { classPropertyName: "kjChartPalette", publicName: "kjChartPalette", isSignal: true, isRequired: false, transformFunction: null }, kjChartAnimate: { classPropertyName: "kjChartAnimate", publicName: "kjChartAnimate", isSignal: true, isRequired: false, transformFunction: null }, kjChartOn: { classPropertyName: "kjChartOn", publicName: "kjChartOn", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { kjChartReady: "kjChartReady", kjChartEvent: "kjChartEvent", kjChartClick: "kjChartClick", kjChartLegendSelect: "kjChartLegendSelect" }, host: { attributes: { "role": "img" }, properties: { "attr.aria-label": "kjChartLabel() || null", "attr.aria-describedby": "descriptionId() || null" } }, queries: [{ propertyName: "_fallback", first: true, predicate: KjChartTableFallback, descendants: true, isSignal: true }], exportAs: ["kjChart"], ngImport: i0 });
25431
+ }
25432
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjChart, decorators: [{
25433
+ type: Directive,
25434
+ args: [{
25435
+ selector: '[kjChart]',
25436
+ standalone: true,
25437
+ exportAs: 'kjChart',
25438
+ host: {
25439
+ role: 'img',
25440
+ '[attr.aria-label]': 'kjChartLabel() || null',
25441
+ '[attr.aria-describedby]': 'descriptionId() || null',
25442
+ },
25443
+ }]
25444
+ }], ctorParameters: () => [], propDecorators: { kjChartOption: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjChartOption", required: true }] }], kjChartLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjChartLabel", required: true }] }], kjChartDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjChartDescription", required: false }] }], kjChartLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjChartLoading", required: false }] }], kjChartPalette: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjChartPalette", required: false }] }], kjChartAnimate: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjChartAnimate", required: false }] }], kjChartOn: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjChartOn", required: false }] }], kjChartReady: [{ type: i0.Output, args: ["kjChartReady"] }], kjChartEvent: [{ type: i0.Output, args: ["kjChartEvent"] }], kjChartClick: [{ type: i0.Output, args: ["kjChartClick"] }], kjChartLegendSelect: [{ type: i0.Output, args: ["kjChartLegendSelect"] }], _fallback: [{ type: i0.ContentChild, args: [i0.forwardRef(() => KjChartTableFallback), { isSignal: true }] }] } });
25445
+
25446
+ /**
25447
+ * Context token for the rich-text editor. A {@link KjRichTextEditor} provides it
25448
+ * pointing to itself; descendants (toolbars, feature directives) inject it.
25449
+ */
25450
+ const KJ_RICH_TEXT = new InjectionToken('KJ_RICH_TEXT');
25451
+ /**
25452
+ * Multi-provider token for app- or scope-wide rich-text features. Contribute to
25453
+ * it with {@link provideKjRichText}; every {@link KjRichTextEditor} in that
25454
+ * injector scope activates them.
25455
+ */
25456
+ const KJ_RICH_TEXT_FEATURES = new InjectionToken('KJ_RICH_TEXT_FEATURES');
25457
+ /** @deprecated Renamed to {@link KJ_RICH_TEXT_FEATURES}. Same token instance. */
25458
+ const KJ_RICH_TEXT_EXTENSIONS = KJ_RICH_TEXT_FEATURES;
25459
+ /**
25460
+ * Register one or more rich-text features for every editor in this injector
25461
+ * scope (app config, a route, or a component's `providers`). Only the chosen
25462
+ * features load their packages and contribute toolbar/overlay UI.
25463
+ *
25464
+ * @example
25465
+ * ```ts
25466
+ * providers: [provideKjRichText(bold(), italic(), link())]
25467
+ * ```
25468
+ */
25469
+ function provideKjRichText(...features) {
25470
+ return features.map((feature) => ({
25471
+ provide: KJ_RICH_TEXT_FEATURES,
25472
+ useValue: feature,
25473
+ multi: true,
25474
+ }));
25475
+ }
25476
+ /**
25477
+ * Injection token holding the Lexical node instance being decorated. An Angular
25478
+ * component mounted for a decorator node injects it (via {@link injectRichTextNode})
25479
+ * to read the node's data.
25480
+ */
25481
+ const KJ_RICH_TEXT_NODE = new InjectionToken('KJ_RICH_TEXT_NODE');
25482
+ /** Inject the Lexical node instance a decorator-node component is rendering. */
25483
+ function injectRichTextNode() {
25484
+ return inject(KJ_RICH_TEXT_NODE);
25485
+ }
25486
+ /** Injection token holding the data a feature passed to `context.openOverlay(id, data)`. */
25487
+ const KJ_RTE_OVERLAY_DATA = new InjectionToken('KJ_RTE_OVERLAY_DATA');
25488
+ /** Inject the data supplied to the currently rendered rich-text overlay component. */
25489
+ function injectRteOverlayData() {
25490
+ return inject(KJ_RTE_OVERLAY_DATA);
25491
+ }
25492
+
25493
+ const EMPTY_STATE = {
25494
+ activeFormats: new Set(),
25495
+ blockType: 'paragraph',
25496
+ canUndo: false,
25497
+ canRedo: false,
25498
+ isLink: false,
25499
+ empty: true,
25500
+ };
25501
+ /** Stable ordering for toolbar groups; unknown groups sort last, alphabetically. */
25502
+ const GROUP_ORDER = ['format', 'block', 'list', 'insert', 'history'];
25503
+ /**
25504
+ * Headless, client-driven rich-text editor wrapping [Lexical](https://lexical.dev).
25505
+ *
25506
+ * Apply to a block element to turn it into an editable, accessible surface
25507
+ * (`role="textbox"`, `aria-multiline`). The editor is composed from **features**
25508
+ * (see {@link KjRichTextFeature}) supplied via {@link provideKjRichText}, the
25509
+ * `kjFeatures` input, or `[kjRichTextExtension]` child directives. Each feature
25510
+ * lazily loads its own `@lexical/*` package(s) in the browser, so disabling a
25511
+ * feature keeps its code out of the bundle. SSR-safe: the engine loads via
25512
+ * dynamic `import()` inside `afterNextRender`.
25513
+ *
25514
+ * Exposes the aggregated {@link toolbarItems}, reactive `state`, and imperative
25515
+ * helpers (`runItem`, `undo`, …) for a dynamic toolbar to bind to, and
25516
+ * implements {@link ControlValueAccessor} (HTML string model) for Angular forms.
25517
+ *
25518
+ * @doc-category Core/Forms
25519
+ * @doc
25520
+ * @doc-name rich-text-editor
25521
+ * @doc-description Headless, feature-composed Lexical rich-text editor directive with a dynamic toolbar contract and form support.
25522
+ * @doc-is-main
25523
+ */
25524
+ class KjRichTextEditor {
25525
+ el = inject(ElementRef);
25526
+ destroyRef = inject(DestroyRef);
25527
+ platformId = inject(PLATFORM_ID);
25528
+ envInjector = inject(EnvironmentInjector);
25529
+ appRef = inject(ApplicationRef);
25530
+ /** App-/scope-wide features contributed via {@link provideKjRichText}. */
25531
+ providedFeatures = inject(KJ_RICH_TEXT_FEATURES, { optional: true }) ?? [];
25532
+ /** Features registered by child directives via {@link registerFeature}. */
25533
+ childFeatures = signal([], /* @ts-ignore */
25534
+ ...(ngDevMode ? [{ debugName: "childFeatures" }] : /* istanbul ignore next */ []));
25535
+ /** Initial content as an HTML string. Ongoing edits are reported via outputs / forms. */
25536
+ kjValue = input('', /* @ts-ignore */
25537
+ ...(ngDevMode ? [{ debugName: "kjValue" }] : /* istanbul ignore next */ []));
25538
+ /** Per-instance features, merged with provided + child-registered features. */
25539
+ kjFeatures = input([], /* @ts-ignore */
25540
+ ...(ngDevMode ? [{ debugName: "kjFeatures" }] : /* istanbul ignore next */ []));
25541
+ /** @deprecated Renamed to {@link kjFeatures}. Still honored (merged). */
25542
+ kjExtensions = input([], /* @ts-ignore */
25543
+ ...(ngDevMode ? [{ debugName: "kjExtensions" }] : /* istanbul ignore next */ []));
25544
+ /** @deprecated Renamed to {@link kjFeatures}. Still honored (merged). */
25545
+ kjPlugins = input([], /* @ts-ignore */
25546
+ ...(ngDevMode ? [{ debugName: "kjPlugins" }] : /* istanbul ignore next */ []));
25547
+ /** Makes the editor non-editable while still selectable. */
25548
+ kjReadonly = input(false, /* @ts-ignore */
25549
+ ...(ngDevMode ? [{ debugName: "kjReadonly" }] : /* istanbul ignore next */ []));
25550
+ /** Native spellcheck toggle. */
25551
+ kjSpellcheck = input(true, /* @ts-ignore */
25552
+ ...(ngDevMode ? [{ debugName: "kjSpellcheck" }] : /* istanbul ignore next */ []));
25553
+ /** Lexical namespace (diagnostics only). */
25554
+ kjNamespace = input('kj-rich-text', /* @ts-ignore */
25555
+ ...(ngDevMode ? [{ debugName: "kjNamespace" }] : /* istanbul ignore next */ []));
25556
+ /** Emits the serialized HTML whenever the document changes. */
25557
+ valueChange = output();
25558
+ /** Emits the plain-text content whenever the document changes. */
25559
+ textChange = output();
25560
+ /** Emits the Lexical `SerializedEditorState` whenever the document changes. */
25561
+ jsonChange = output();
25562
+ /** Emits messages a feature asked to announce to assistive technology. */
25563
+ announce = output();
25564
+ editorSig = signal(null, /* @ts-ignore */
25565
+ ...(ngDevMode ? [{ debugName: "editorSig" }] : /* istanbul ignore next */ []));
25566
+ /** The live Lexical editor instance, or `null` before initialization. */
25567
+ editor = this.editorSig.asReadonly();
25568
+ /** Current formatting state derived from the selection. */
25569
+ state = signal(EMPTY_STATE, /* @ts-ignore */
25570
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
25571
+ isBold = computed(() => this.state().activeFormats.has('bold'), /* @ts-ignore */
25572
+ ...(ngDevMode ? [{ debugName: "isBold" }] : /* istanbul ignore next */ []));
25573
+ isItalic = computed(() => this.state().activeFormats.has('italic'), /* @ts-ignore */
25574
+ ...(ngDevMode ? [{ debugName: "isItalic" }] : /* istanbul ignore next */ []));
25575
+ isUnderline = computed(() => this.state().activeFormats.has('underline'), /* @ts-ignore */
25576
+ ...(ngDevMode ? [{ debugName: "isUnderline" }] : /* istanbul ignore next */ []));
25577
+ isStrikethrough = computed(() => this.state().activeFormats.has('strikethrough'), /* @ts-ignore */
25578
+ ...(ngDevMode ? [{ debugName: "isStrikethrough" }] : /* istanbul ignore next */ []));
25579
+ isCode = computed(() => this.state().activeFormats.has('code'), /* @ts-ignore */
25580
+ ...(ngDevMode ? [{ debugName: "isCode" }] : /* istanbul ignore next */ []));
25581
+ blockType = computed(() => this.state().blockType, /* @ts-ignore */
25582
+ ...(ngDevMode ? [{ debugName: "blockType" }] : /* istanbul ignore next */ []));
25583
+ canUndo = computed(() => this.state().canUndo, /* @ts-ignore */
25584
+ ...(ngDevMode ? [{ debugName: "canUndo" }] : /* istanbul ignore next */ []));
25585
+ canRedo = computed(() => this.state().canRedo, /* @ts-ignore */
25586
+ ...(ngDevMode ? [{ debugName: "canRedo" }] : /* istanbul ignore next */ []));
25587
+ isLink = computed(() => this.state().isLink, /* @ts-ignore */
25588
+ ...(ngDevMode ? [{ debugName: "isLink" }] : /* istanbul ignore next */ []));
25589
+ empty = computed(() => this.state().empty, /* @ts-ignore */
25590
+ ...(ngDevMode ? [{ debugName: "empty" }] : /* istanbul ignore next */ []));
25591
+ /** All active features (provided + inputs + child-registered). */
25592
+ features = computed(() => [
25593
+ ...this.providedFeatures,
25594
+ ...this.kjFeatures(),
25595
+ ...this.kjExtensions(),
25596
+ ...this.kjPlugins(),
25597
+ ...this.childFeatures(),
25598
+ ], /* @ts-ignore */
25599
+ ...(ngDevMode ? [{ debugName: "features" }] : /* istanbul ignore next */ []));
25600
+ /** Toolbar items contributed by active features, sorted by group then order. */
25601
+ toolbarItems = computed(() => this.features()
25602
+ .flatMap((feature) => feature.toolbar ?? [])
25603
+ .slice()
25604
+ .sort((a, b) => {
25605
+ const ga = GROUP_ORDER.indexOf(a.group);
25606
+ const gb = GROUP_ORDER.indexOf(b.group);
25607
+ const oa = ga === -1 ? GROUP_ORDER.length : ga;
25608
+ const ob = gb === -1 ? GROUP_ORDER.length : gb;
25609
+ if (oa !== ob)
25610
+ return oa - ob;
25611
+ if (a.group !== b.group)
25612
+ return a.group.localeCompare(b.group);
25613
+ return a.order - b.order;
25614
+ }), /* @ts-ignore */
25615
+ ...(ngDevMode ? [{ debugName: "toolbarItems" }] : /* istanbul ignore next */ []));
25616
+ /** Toolbar items grouped into contiguous runs (for rendering separators). */
25617
+ toolbarGroups = computed(() => {
25618
+ const groups = [];
25619
+ for (const item of this.toolbarItems()) {
25620
+ const last = groups[groups.length - 1];
25621
+ if (!last || last.group !== item.group)
25622
+ groups.push({ group: item.group, items: [item] });
25623
+ else
25624
+ last.items.push(item);
25625
+ }
25626
+ return groups;
25627
+ }, /* @ts-ignore */
25628
+ ...(ngDevMode ? [{ debugName: "toolbarGroups" }] : /* istanbul ignore next */ []));
25629
+ /** Overlay descriptors contributed by active features. */
25630
+ overlays = computed(() => this.features().flatMap((feature) => feature.overlay ?? []), /* @ts-ignore */
25631
+ ...(ngDevMode ? [{ debugName: "overlays" }] : /* istanbul ignore next */ []));
25632
+ /** The overlay currently open (opened by a feature), or `null`. */
25633
+ activeOverlay = signal(null, /* @ts-ignore */
25634
+ ...(ngDevMode ? [{ debugName: "activeOverlay" }] : /* istanbul ignore next */ []));
25635
+ /** @internal CVA disabled flag. */
25636
+ disabledState = signal(false, /* @ts-ignore */
25637
+ ...(ngDevMode ? [{ debugName: "disabledState" }] : /* istanbul ignore next */ []));
25638
+ engine = null;
25639
+ pendingValue = null;
25640
+ lastHtml = '';
25641
+ applyingExternal = false;
25642
+ destroyed = false;
25643
+ onChange = () => { };
25644
+ /** @internal blur handler wired via host bindings. */
25645
+ onTouched = () => { };
25646
+ constructor() {
25647
+ this.destroyRef.onDestroy(() => {
25648
+ this.destroyed = true;
25649
+ this.engine?.destroy();
25650
+ this.engine = null;
25651
+ });
25652
+ effect(() => {
25653
+ const editable = !this.kjReadonly() && !this.disabledState();
25654
+ this.engine?.setEditable(editable);
25655
+ });
25656
+ afterNextRender(async () => {
25657
+ if (!isPlatformBrowser(this.platformId))
25658
+ return;
25659
+ const { createRichTextEngine } = await import('./kouji-ui-core-engine-CKMr0aiZ.mjs');
25660
+ if (this.destroyed)
25661
+ return;
25662
+ const initial = this.pendingValue ?? this.kjValue();
25663
+ this.applyingExternal = true;
25664
+ const engine = await createRichTextEngine(this.el.nativeElement, {
25665
+ initialHtml: initial,
25666
+ features: this.features(),
25667
+ namespace: this.kjNamespace(),
25668
+ mount: this.createMountAdapter(),
25669
+ onOverlayOpen: (id, data) => this.openOverlayById(id, data),
25670
+ onOverlayClose: () => this.activeOverlay.set(null),
25671
+ onAnnounce: (message) => this.announce.emit(message),
25672
+ }, {
25673
+ onState: (s) => this.state.set(s),
25674
+ onValue: (v) => this.emitValue(v.html, v.text, v.json),
25675
+ });
25676
+ this.applyingExternal = false;
25677
+ if (this.destroyed) {
25678
+ engine.destroy();
25679
+ return;
25680
+ }
25681
+ this.engine = engine;
25682
+ this.editorSig.set(engine.editor);
25683
+ this.pendingValue = null;
25684
+ this.lastHtml = engine.getHtml();
25685
+ engine.setEditable(!this.kjReadonly() && !this.disabledState());
25686
+ });
25687
+ }
25688
+ emitValue(html, text, json) {
25689
+ this.lastHtml = html;
25690
+ if (this.applyingExternal)
25691
+ return;
25692
+ this.onChange(html);
25693
+ this.valueChange.emit(html);
25694
+ this.textChange.emit(text);
25695
+ this.jsonChange.emit(json);
25696
+ }
25697
+ openOverlayById(id, data) {
25698
+ const overlay = this.overlays().find((o) => o.id === id);
25699
+ if (overlay)
25700
+ this.activeOverlay.set({ overlay, data });
25701
+ }
25702
+ // -- feature registration + toolbar API ----------------------------------
25703
+ /** {@inheritDoc KjRichTextHost.registerFeature} */
25704
+ registerFeature(feature) {
25705
+ this.childFeatures.update((list) => [...list, feature]);
25706
+ }
25707
+ /** @deprecated Renamed to {@link registerFeature}. */
25708
+ registerExtension(feature) {
25709
+ this.registerFeature(feature);
25710
+ }
25711
+ /** Run a toolbar item's action against the live editor (no-op until ready). */
25712
+ runItem(item) {
25713
+ if (this.engine)
25714
+ item.run(this.engine.context);
25715
+ }
25716
+ /** Whether a toggle toolbar item is currently active. */
25717
+ itemActive(item) {
25718
+ return item.kind === 'toggle' && !!item.isActive?.(this.state());
25719
+ }
25720
+ /** Whether a toolbar item is currently disabled. */
25721
+ itemDisabled(item) {
25722
+ return !!item.isDisabled?.(this.state());
25723
+ }
25724
+ /** Close any open feature overlay. */
25725
+ closeOverlay() {
25726
+ this.activeOverlay.set(null);
25727
+ }
25728
+ // -- imperative editor helpers -------------------------------------------
25729
+ /** Undo the last edit. */
25730
+ undo() {
25731
+ this.engine?.undo();
25732
+ }
25733
+ /** Redo the last undone edit. */
25734
+ redo() {
25735
+ this.engine?.redo();
25736
+ }
25737
+ /** Move focus into the editor. */
25738
+ focus() {
25739
+ this.engine?.focus();
25740
+ }
25741
+ /** Remove all content, leaving a single empty paragraph. */
25742
+ clear() {
25743
+ this.engine?.clear();
25744
+ }
25745
+ /** Serialize the current content to HTML. */
25746
+ getHtml() {
25747
+ return this.engine?.getHtml() ?? this.lastHtml;
25748
+ }
25749
+ /** Replace the content from an HTML string. */
25750
+ setHtml(html) {
25751
+ this.writeValue(html);
25752
+ }
25753
+ /** Serialize the current content to a Lexical `SerializedEditorState`. */
25754
+ getJson() {
25755
+ return this.engine?.getJson() ?? null;
25756
+ }
25757
+ /** Replace the content from a Lexical `SerializedEditorState`. */
25758
+ setJson(json) {
25759
+ if (!this.engine)
25760
+ return;
25761
+ this.applyingExternal = true;
25762
+ this.engine.setJson(json);
25763
+ this.applyingExternal = false;
25764
+ }
25765
+ /** Build the Angular mount adapter the engine uses for decorator-node components. */
25766
+ createMountAdapter() {
25767
+ return {
25768
+ mount: (component, node) => {
25769
+ const elementInjector = Injector.create({
25770
+ parent: this.envInjector,
25771
+ providers: [{ provide: KJ_RICH_TEXT_NODE, useValue: node }],
25772
+ });
25773
+ const ref = createComponent(component, {
25774
+ environmentInjector: this.envInjector,
25775
+ elementInjector,
25776
+ });
25777
+ this.appRef.attachView(ref.hostView);
25778
+ return {
25779
+ element: ref.location.nativeElement,
25780
+ destroy: () => {
25781
+ this.appRef.detachView(ref.hostView);
25782
+ ref.destroy();
25783
+ },
25784
+ };
25785
+ },
25786
+ };
25787
+ }
25788
+ // -- ControlValueAccessor ------------------------------------------------
25789
+ writeValue(value) {
25790
+ const html = value ?? '';
25791
+ if (html === this.lastHtml)
25792
+ return;
25793
+ if (this.engine) {
25794
+ this.applyingExternal = true;
25795
+ this.engine.setHtml(html);
25796
+ this.applyingExternal = false;
25797
+ this.lastHtml = html;
25798
+ }
25799
+ else {
25800
+ this.pendingValue = html;
25801
+ }
25802
+ }
25803
+ registerOnChange(fn) {
25804
+ this.onChange = fn;
25805
+ }
25806
+ registerOnTouched(fn) {
25807
+ this.onTouched = fn;
25808
+ }
25809
+ setDisabledState(isDisabled) {
25810
+ this.disabledState.set(isDisabled);
25811
+ }
25812
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjRichTextEditor, deps: [], target: i0.ɵɵFactoryTarget.Directive });
25813
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: KjRichTextEditor, isStandalone: true, selector: "[kjRichTextEditor]", inputs: { kjValue: { classPropertyName: "kjValue", publicName: "kjValue", isSignal: true, isRequired: false, transformFunction: null }, kjFeatures: { classPropertyName: "kjFeatures", publicName: "kjFeatures", isSignal: true, isRequired: false, transformFunction: null }, kjExtensions: { classPropertyName: "kjExtensions", publicName: "kjExtensions", isSignal: true, isRequired: false, transformFunction: null }, kjPlugins: { classPropertyName: "kjPlugins", publicName: "kjPlugins", isSignal: true, isRequired: false, transformFunction: null }, kjReadonly: { classPropertyName: "kjReadonly", publicName: "kjReadonly", isSignal: true, isRequired: false, transformFunction: null }, kjSpellcheck: { classPropertyName: "kjSpellcheck", publicName: "kjSpellcheck", isSignal: true, isRequired: false, transformFunction: null }, kjNamespace: { classPropertyName: "kjNamespace", publicName: "kjNamespace", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", textChange: "textChange", jsonChange: "jsonChange", announce: "announce" }, host: { attributes: { "role": "textbox", "aria-multiline": "true" }, listeners: { "blur": "onTouched()" }, properties: { "attr.contenteditable": "kjReadonly() || disabledState() ? \"false\" : \"true\"", "attr.spellcheck": "kjSpellcheck()", "attr.aria-readonly": "kjReadonly() ? \"true\" : null", "attr.aria-disabled": "disabledState() ? \"true\" : null", "attr.data-empty": "empty() ? \"true\" : null" } }, providers: [
25814
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => KjRichTextEditor), multi: true },
25815
+ { provide: KJ_RICH_TEXT, useExisting: forwardRef(() => KjRichTextEditor) },
25816
+ ], exportAs: ["kjRichTextEditor"], ngImport: i0 });
25817
+ }
25818
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjRichTextEditor, decorators: [{
25819
+ type: Directive,
25820
+ args: [{
25821
+ selector: '[kjRichTextEditor]',
25822
+ standalone: true,
25823
+ exportAs: 'kjRichTextEditor',
25824
+ host: {
25825
+ role: 'textbox',
25826
+ 'aria-multiline': 'true',
25827
+ // Lexical listens on this element but does NOT set `contenteditable` itself —
25828
+ // the host must. Without this the editor attaches but the surface can't be
25829
+ // clicked into or typed in. Readonly/disabled → non-editable (still selectable).
25830
+ '[attr.contenteditable]': 'kjReadonly() || disabledState() ? "false" : "true"',
25831
+ '[attr.spellcheck]': 'kjSpellcheck()',
25832
+ '[attr.aria-readonly]': 'kjReadonly() ? "true" : null',
25833
+ '[attr.aria-disabled]': 'disabledState() ? "true" : null',
25834
+ '[attr.data-empty]': 'empty() ? "true" : null',
25835
+ '(blur)': 'onTouched()',
25836
+ },
25837
+ providers: [
25838
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => KjRichTextEditor), multi: true },
25839
+ { provide: KJ_RICH_TEXT, useExisting: forwardRef(() => KjRichTextEditor) },
25840
+ ],
25841
+ }]
25842
+ }], ctorParameters: () => [], propDecorators: { kjValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjValue", required: false }] }], kjFeatures: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjFeatures", required: false }] }], kjExtensions: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjExtensions", required: false }] }], kjPlugins: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjPlugins", required: false }] }], kjReadonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjReadonly", required: false }] }], kjSpellcheck: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjSpellcheck", required: false }] }], kjNamespace: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjNamespace", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], textChange: [{ type: i0.Output, args: ["textChange"] }], jsonChange: [{ type: i0.Output, args: ["jsonChange"] }], announce: [{ type: i0.Output, args: ["announce"] }] } });
25843
+
25844
+ /**
25845
+ * Registers one or more {@link KjRichTextFeature}s with the nearest
25846
+ * {@link KjRichTextEditor} — the signal-context pattern (like `Option`
25847
+ * registering with `Select`).
25848
+ *
25849
+ * Place it on the same element as `[kjRichTextEditor]`, or on a descendant that
25850
+ * can inject {@link KJ_RICH_TEXT} (e.g. an `<ng-container>`). Registration
25851
+ * happens in `ngOnInit`, before the editor initializes, so node-contributing
25852
+ * features are picked up.
25853
+ *
25854
+ * @example
23535
25855
  * ```html
23536
- * <div kjChart [kjChartOption]="chartOption()" kjChartLabel="Monthly revenue" style="height:300px"></div>
25856
+ * <div kjRichTextEditor [kjFeatures]="[mentionFeature]"></div>
25857
+ * <!-- or as a child directive -->
25858
+ * <div kjRichTextEditor [kjRichTextFeature]="mentionFeature"></div>
25859
+ * ```
25860
+ * @doc-category Core/Forms
25861
+ * @doc
25862
+ * @doc-name rich-text-editor
25863
+ */
25864
+ class KjRichTextExtensionDirective {
25865
+ host = inject(KJ_RICH_TEXT);
25866
+ /** The feature (or features) to register with the host editor. */
25867
+ kjRichTextFeature = input(/* @ts-ignore */
25868
+ ...(ngDevMode ? [undefined, { debugName: "kjRichTextFeature" }] : /* istanbul ignore next */ []));
25869
+ /** @deprecated Renamed to {@link kjRichTextFeature}. */
25870
+ kjRichTextExtension = input(/* @ts-ignore */
25871
+ ...(ngDevMode ? [undefined, { debugName: "kjRichTextExtension" }] : /* istanbul ignore next */ []));
25872
+ ngOnInit() {
25873
+ const value = this.kjRichTextFeature() ?? this.kjRichTextExtension();
25874
+ if (!value)
25875
+ return;
25876
+ const features = Array.isArray(value) ? value : [value];
25877
+ for (const feature of features) {
25878
+ this.host.registerFeature(feature);
25879
+ }
25880
+ }
25881
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjRichTextExtensionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
25882
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: KjRichTextExtensionDirective, isStandalone: true, selector: "[kjRichTextFeature], [kjRichTextExtension]", inputs: { kjRichTextFeature: { classPropertyName: "kjRichTextFeature", publicName: "kjRichTextFeature", isSignal: true, isRequired: false, transformFunction: null }, kjRichTextExtension: { classPropertyName: "kjRichTextExtension", publicName: "kjRichTextExtension", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
25883
+ }
25884
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjRichTextExtensionDirective, decorators: [{
25885
+ type: Directive,
25886
+ args: [{
25887
+ selector: '[kjRichTextFeature], [kjRichTextExtension]',
25888
+ standalone: true,
25889
+ }]
25890
+ }], propDecorators: { kjRichTextFeature: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjRichTextFeature", required: false }] }], kjRichTextExtension: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjRichTextExtension", required: false }] }] } });
25891
+
25892
+ /**
25893
+ * Build a self-contained Lexical `DecoratorNode` subclass whose instances render
25894
+ * an Angular component (mounted by the editor's decorator bridge). This is the
25895
+ * reusable "render an Angular component as an editor node" framework — define a
25896
+ * custom node from outside the engine in a handful of lines.
25897
+ *
25898
+ * The node stores an arbitrary JSON-serializable `data` object; the mounted
25899
+ * component reads it via {@link injectRichTextNode}. `lexical` is passed in (not
25900
+ * imported here) so this stays SSR-safe and out of the base bundle.
25901
+ *
25902
+ * @example
25903
+ * ```ts
25904
+ * const badge = createKjDecoratorNode(lexical, { type: 'badge', component: BadgeChip, inline: true });
25905
+ * // badge.Node -> register via extension.nodes; badge.$create({ label }) -> insert
25906
+ * ```
25907
+ */
25908
+ function createKjDecoratorNode(lexical, config) {
25909
+ const { DecoratorNode, $applyNodeReplacement } = lexical;
25910
+ class KjBridgedDecoratorNode extends DecoratorNode {
25911
+ __data;
25912
+ static getType() {
25913
+ return config.type;
25914
+ }
25915
+ static clone(node) {
25916
+ return new KjBridgedDecoratorNode(node.__data, node.__key);
25917
+ }
25918
+ static importJSON(json) {
25919
+ return new KjBridgedDecoratorNode((json.data ?? {}));
25920
+ }
25921
+ constructor(data = {}, key) {
25922
+ super(key);
25923
+ this.__data = data;
25924
+ }
25925
+ exportJSON() {
25926
+ return { ...super.exportJSON(), type: config.type, version: 1, data: this.__data };
25927
+ }
25928
+ /** The node's payload; read by the mounted component. */
25929
+ getData() {
25930
+ return this.__data;
25931
+ }
25932
+ createDOM() {
25933
+ const el = document.createElement(config.inline ? 'span' : 'div');
25934
+ el.setAttribute('data-lexical-decorator', config.type);
25935
+ el.className = 'kj-rte-decorator';
25936
+ // Decorator content is not directly editable; selection steps over it.
25937
+ el.contentEditable = 'false';
25938
+ if (config.inline)
25939
+ el.style.display = 'inline-block';
25940
+ if (config.ariaLabel)
25941
+ el.setAttribute('aria-label', config.ariaLabel);
25942
+ return el;
25943
+ }
25944
+ updateDOM() {
25945
+ return false;
25946
+ }
25947
+ isInline() {
25948
+ return !!config.inline;
25949
+ }
25950
+ /** Returned to the decorator bridge, which maps the node → its component. */
25951
+ decorate() {
25952
+ return this;
25953
+ }
25954
+ }
25955
+ return {
25956
+ Node: KjBridgedDecoratorNode,
25957
+ $create: (data) => $applyNodeReplacement(new KjBridgedDecoratorNode(data ?? {})),
25958
+ $is: (node) => node instanceof KjBridgedDecoratorNode,
25959
+ };
25960
+ }
25961
+
25962
+ /**
25963
+ * Build a self-rendering block image `DecoratorNode` subclass. It paints its own
25964
+ * `<figure><img></figure>` in `createDOM` (no framework decorator infra needed)
25965
+ * and round-trips through HTML via `importDOM`/`exportDOM`.
25966
+ *
25967
+ * `lexical` is passed in (not imported here) so this module carries no eager
25968
+ * Lexical import and stays SSR-safe — the image feature calls it inside `load()`.
25969
+ */
25970
+ function createKjImageNode(lexical) {
25971
+ const { DecoratorNode, $applyNodeReplacement } = lexical;
25972
+ class KjImageNode extends DecoratorNode {
25973
+ __src;
25974
+ __alt;
25975
+ __width;
25976
+ __height;
25977
+ static getType() {
25978
+ return 'kj-image';
25979
+ }
25980
+ static clone(node) {
25981
+ return new KjImageNode({ src: node.__src, alt: node.__alt, width: node.__width, height: node.__height }, node.__key);
25982
+ }
25983
+ static importJSON(json) {
25984
+ return new KjImageNode({
25985
+ src: json.src,
25986
+ alt: json.alt,
25987
+ width: json.width,
25988
+ height: json.height,
25989
+ });
25990
+ }
25991
+ constructor(props, key) {
25992
+ super(key);
25993
+ this.__src = props.src;
25994
+ this.__alt = props.alt ?? '';
25995
+ this.__width = props.width;
25996
+ this.__height = props.height;
25997
+ }
25998
+ exportJSON() {
25999
+ return {
26000
+ ...super.exportJSON(),
26001
+ type: 'kj-image',
26002
+ version: 1,
26003
+ src: this.__src,
26004
+ alt: this.__alt,
26005
+ width: this.__width,
26006
+ height: this.__height,
26007
+ };
26008
+ }
26009
+ createDOM() {
26010
+ const figure = document.createElement('figure');
26011
+ figure.className = 'kj-rte-image';
26012
+ figure.contentEditable = 'false';
26013
+ const img = document.createElement('img');
26014
+ img.src = this.__src;
26015
+ img.alt = this.__alt;
26016
+ img.setAttribute('data-lexical-image', 'true');
26017
+ if (this.__width)
26018
+ img.width = this.__width;
26019
+ if (this.__height)
26020
+ img.height = this.__height;
26021
+ figure.appendChild(img);
26022
+ return figure;
26023
+ }
26024
+ updateDOM(prev) {
26025
+ return (prev.__src !== this.__src ||
26026
+ prev.__alt !== this.__alt ||
26027
+ prev.__width !== this.__width ||
26028
+ prev.__height !== this.__height);
26029
+ }
26030
+ decorate() {
26031
+ return null;
26032
+ }
26033
+ exportDOM() {
26034
+ const element = document.createElement('img');
26035
+ element.setAttribute('data-lexical-image', 'true');
26036
+ element.src = this.__src;
26037
+ element.alt = this.__alt;
26038
+ if (this.__width)
26039
+ element.width = this.__width;
26040
+ if (this.__height)
26041
+ element.height = this.__height;
26042
+ return { element };
26043
+ }
26044
+ static importDOM() {
26045
+ return {
26046
+ img: () => ({
26047
+ conversion: (domNode) => {
26048
+ const img = domNode;
26049
+ const src = img.getAttribute('src');
26050
+ if (!src)
26051
+ return null;
26052
+ return {
26053
+ node: $create({
26054
+ src,
26055
+ alt: img.alt || '',
26056
+ width: img.width || undefined,
26057
+ height: img.height || undefined,
26058
+ }),
26059
+ };
26060
+ },
26061
+ priority: 0,
26062
+ }),
26063
+ };
26064
+ }
26065
+ isInline() {
26066
+ return false;
26067
+ }
26068
+ getSrc() {
26069
+ return this.__src;
26070
+ }
26071
+ getAlt() {
26072
+ return this.__alt;
26073
+ }
26074
+ }
26075
+ const $create = (image) => $applyNodeReplacement(new KjImageNode(image));
26076
+ return {
26077
+ Node: KjImageNode,
26078
+ $create,
26079
+ $is: (node) => node instanceof KjImageNode,
26080
+ };
26081
+ }
26082
+
26083
+ // Only SSR-safe (Lexical-free at runtime) symbols are re-exported here so that
26084
+ // importing `@kouji-ui/core` never eagerly loads Lexical or any feature package.
26085
+ // The node factories (`createKjDecoratorNode`, `createKjImageNode`) receive the
26086
+ // `lexical` namespace as an argument, so they carry no eager import.
26087
+
26088
+ /** DI token holding the resolved {@link KjMonacoConfig}. Defaults to `{}`. */
26089
+ const KJ_MONACO_CONFIG = new InjectionToken('KJ_MONACO_CONFIG', {
26090
+ providedIn: 'root',
26091
+ factory: () => ({}),
26092
+ });
26093
+
26094
+ /**
26095
+ * Registered per-language lazy loaders, keyed by (normalised) language id.
26096
+ * `multi` so several `provideMonacoLanguages` calls compose; later
26097
+ * registrations win on key collision. Consumed by `KjEditorLoader.ensureLanguage`.
26098
+ */
26099
+ const KJ_MONACO_LANGUAGE_LOADERS = new InjectionToken('KJ_MONACO_LANGUAGE_LOADERS', { providedIn: 'root', factory: () => [] });
26100
+ /**
26101
+ * Register lazy loaders for individual Monaco languages so only the languages an
26102
+ * editor actually uses are downloaded, and only when first used. This keeps the
26103
+ * base editor lean when you bundle a **minimal** Monaco (the `provideMonaco({ loader })`
26104
+ * path); with the default CDN loader every language is already bundled, so
26105
+ * registering loaders is optional (a missing id simply falls back to the
26106
+ * built-in language).
26107
+ *
26108
+ * @example
26109
+ * provideMonacoLanguages({
26110
+ * python: () => import('monaco-editor/esm/vs/basic-languages/python/python.contribution'),
26111
+ * rust: () => import('monaco-editor/esm/vs/basic-languages/rust/rust.contribution'),
26112
+ * })
26113
+ *
26114
+ * @doc
26115
+ * @doc-name editor
26116
+ * @doc-order 2
26117
+ */
26118
+ function provideMonacoLanguages(loaders) {
26119
+ return makeEnvironmentProviders([
26120
+ { provide: KJ_MONACO_LANGUAGE_LOADERS, useValue: loaders, multi: true },
26121
+ ]);
26122
+ }
26123
+ /** Short aliases → canonical Monaco language ids. */
26124
+ const LANGUAGE_ALIASES = {
26125
+ ts: 'typescript',
26126
+ js: 'javascript',
26127
+ jsx: 'javascript',
26128
+ tsx: 'typescript',
26129
+ md: 'markdown',
26130
+ yml: 'yaml',
26131
+ sh: 'shell',
26132
+ bash: 'shell',
26133
+ py: 'python',
26134
+ rb: 'ruby',
26135
+ 'c++': 'cpp',
26136
+ 'c#': 'csharp',
26137
+ cs: 'csharp',
26138
+ htm: 'html',
26139
+ text: 'plaintext',
26140
+ '': 'plaintext',
26141
+ };
26142
+ /** Map a friendly/alias language name to the canonical Monaco language id. */
26143
+ function normalizeLanguage(lang) {
26144
+ if (!lang)
26145
+ return 'plaintext';
26146
+ const lower = lang.toLowerCase();
26147
+ return LANGUAGE_ALIASES[lower] ?? lower;
26148
+ }
26149
+
26150
+ /**
26151
+ * Resolves the Monaco namespace **once** and memoises the promise, so every
26152
+ * `KjEditor` on the page shares a single Monaco instance.
26153
+ *
26154
+ * Resolution strategy (see {@link KjMonacoConfig}):
26155
+ * 1. A consumer-supplied `loader` wins — self-hosted / bundled Monaco.
26156
+ * 2. Otherwise dynamically `import('@monaco-editor/loader')` and `init()` it,
26157
+ * applying `vsPath` when provided. The dynamic import keeps both Monaco and
26158
+ * the loader out of the base bundle (their own lazy chunk).
26159
+ *
26160
+ * Browser-only: callers must gate `load()` behind `afterNextRender` /
26161
+ * `isPlatformBrowser`. Naming keeps the `Loader` suffix because `KjEditor`
26162
+ * already names the directive.
26163
+ *
26164
+ * @doc
26165
+ * @doc-name editor
26166
+ * @doc-description Loads and memoises Monaco for the code editor; source is configurable via provideMonaco.
26167
+ */
26168
+ class KjEditorLoader {
26169
+ config = inject(KJ_MONACO_CONFIG);
26170
+ languageLoaders = inject(KJ_MONACO_LANGUAGE_LOADERS);
26171
+ promise = null;
26172
+ loadedLanguages = new Map();
26173
+ /** Resolve Monaco (cached after the first call). */
26174
+ load() {
26175
+ if (!this.promise) {
26176
+ this.promise = this.config.loader ? this.config.loader() : this.loadFromCdn();
26177
+ }
26178
+ return this.promise;
26179
+ }
26180
+ /**
26181
+ * Ensure a language's contribution is loaded before it's used. Runs the loader
26182
+ * registered via {@link provideMonacoLanguages} for this id (once, memoised).
26183
+ * No-ops when no loader is registered — the default CDN Monaco already ships
26184
+ * every language, so this only does work for lean/self-hosted setups.
26185
+ */
26186
+ ensureLanguage(language) {
26187
+ const id = normalizeLanguage(language);
26188
+ const existing = this.loadedLanguages.get(id);
26189
+ if (existing)
26190
+ return existing;
26191
+ let loader;
26192
+ for (const map of this.languageLoaders) {
26193
+ if (map[id])
26194
+ loader = map[id];
26195
+ }
26196
+ const done = loader ? loader().then(() => undefined) : Promise.resolve();
26197
+ this.loadedLanguages.set(id, done);
26198
+ return done;
26199
+ }
26200
+ async loadFromCdn() {
26201
+ // Dynamic import → separate lazy chunk. `@monaco-editor/loader` injects
26202
+ // Monaco (and its language workers) from a CDN at runtime, sidestepping the
26203
+ // esbuild worker-URL setup entirely.
26204
+ const mod = await import('@monaco-editor/loader');
26205
+ const loader = mod.default;
26206
+ if (this.config.vsPath) {
26207
+ loader.config({ paths: { vs: this.config.vsPath } });
26208
+ }
26209
+ return loader.init();
26210
+ }
26211
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjEditorLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
26212
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjEditorLoader, providedIn: 'root' });
26213
+ }
26214
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjEditorLoader, decorators: [{
26215
+ type: Injectable,
26216
+ args: [{ providedIn: 'root' }]
26217
+ }] });
26218
+
26219
+ /**
26220
+ * Headless code editor — wraps [Monaco](https://microsoft.github.io/monaco-editor/)
26221
+ * (VS Code's editor) on its host element. Loads Monaco lazily after first
26222
+ * render (SSR-safe), binds `kjValue` two-way, and disposes on destroy.
26223
+ *
26224
+ * Monaco is browser-only and heavy: it is resolved through {@link KjEditorLoader}
26225
+ * whose source is configurable via `provideMonaco()` (defaults to a CDN loader
26226
+ * so nothing bloats the base bundle). The styled `<kj-editor>` wrapper in
26227
+ * `@kouji-ui/components` adds theming, a toolbar and a status bar on top.
26228
+ *
26229
+ * @example
26230
+ * ```html
26231
+ * <div kjEditor [(kjValue)]="code" kjLanguage="typescript" style="height:320px"></div>
23537
26232
  * ```
23538
26233
  * @doc-category Core/Data
23539
26234
  * @doc
23540
- * @doc-name chart
23541
- * @doc-description Renders a reactive ECharts chart on any sized element with an accessible label.
26235
+ * @doc-name editor
23542
26236
  * @doc-is-main
26237
+ * @doc-description Headless Monaco-wrapped code editor directive — two-way value, language, options, SSR-safe lazy load.
23543
26238
  */
23544
- class KjChart {
26239
+ class KjEditor {
23545
26240
  el = inject(ElementRef);
23546
26241
  destroyRef = inject(DestroyRef);
23547
- /** ECharts option object defining the chart. */
23548
- kjChartOption = input.required(/* @ts-ignore */
23549
- ...(ngDevMode ? [{ debugName: "kjChartOption" }] : /* istanbul ignore next */ []));
23550
- /** Accessible label for the chart. Required for WCAG AAA compliance. */
23551
- kjChartLabel = input('', /* @ts-ignore */
23552
- ...(ngDevMode ? [{ debugName: "kjChartLabel" }] : /* istanbul ignore next */ []));
23553
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
23554
- chart;
26242
+ loader = inject(KjEditorLoader);
26243
+ /** Two-way editor text. */
26244
+ kjValue = model('', /* @ts-ignore */
26245
+ ...(ngDevMode ? [{ debugName: "kjValue" }] : /* istanbul ignore next */ []));
26246
+ /** Code language friendly name or Monaco id; short aliases (`ts`, `md`) normalised. */
26247
+ kjLanguage = input('plaintext', /* @ts-ignore */
26248
+ ...(ngDevMode ? [{ debugName: "kjLanguage" }] : /* istanbul ignore next */ []));
26249
+ /** Read-only mode. */
26250
+ kjReadonly = input(false, /* @ts-ignore */
26251
+ ...(ngDevMode ? [{ debugName: "kjReadonly" }] : /* istanbul ignore next */ []));
26252
+ /** Show the minimap. */
26253
+ kjMinimap = input(false, /* @ts-ignore */
26254
+ ...(ngDevMode ? [{ debugName: "kjMinimap" }] : /* istanbul ignore next */ []));
26255
+ /** Gutter line-number mode. */
26256
+ kjLineNumbers = input('on', /* @ts-ignore */
26257
+ ...(ngDevMode ? [{ debugName: "kjLineNumbers" }] : /* istanbul ignore next */ []));
26258
+ /** Soft wrap. */
26259
+ kjWordWrap = input('off', /* @ts-ignore */
26260
+ ...(ngDevMode ? [{ debugName: "kjWordWrap" }] : /* istanbul ignore next */ []));
26261
+ /** Font size in px. */
26262
+ kjFontSize = input(13, /* @ts-ignore */
26263
+ ...(ngDevMode ? [{ debugName: "kjFontSize" }] : /* istanbul ignore next */ []));
26264
+ /** Grow the host to fit content instead of filling its container. */
26265
+ kjAutoHeight = input(false, /* @ts-ignore */
26266
+ ...(ngDevMode ? [{ debugName: "kjAutoHeight" }] : /* istanbul ignore next */ []));
26267
+ /** Cap for `kjAutoHeight` in px (content scrolls past it). Uncapped when unset. */
26268
+ kjMaxHeight = input(undefined, /* @ts-ignore */
26269
+ ...(ngDevMode ? [{ debugName: "kjMaxHeight" }] : /* istanbul ignore next */ []));
26270
+ /** Explicit Monaco theme id; overrides the wrapper's auto light/dark. */
26271
+ kjTheme = input(undefined, /* @ts-ignore */
26272
+ ...(ngDevMode ? [{ debugName: "kjTheme" }] : /* istanbul ignore next */ []));
26273
+ /** Accessible name — set as Monaco `ariaLabel` and the host `aria-label`. */
26274
+ kjAriaLabel = input('Code editor', /* @ts-ignore */
26275
+ ...(ngDevMode ? [{ debugName: "kjAriaLabel" }] : /* istanbul ignore next */ []));
26276
+ /**
26277
+ * Start with Tab moving focus out instead of inserting a tab. Consumers who
26278
+ * embed the editor in a form flow may prefer this so keyboard users are never
26279
+ * trapped; the `Ctrl+M` toggle remains available either way.
26280
+ */
26281
+ kjTabFocusMode = input(false, /* @ts-ignore */
26282
+ ...(ngDevMode ? [{ debugName: "kjTabFocusMode" }] : /* istanbul ignore next */ []));
26283
+ /** Escape hatch — merged last into Monaco's construction options. */
26284
+ kjOptions = input({}, /* @ts-ignore */
26285
+ ...(ngDevMode ? [{ debugName: "kjOptions" }] : /* istanbul ignore next */ []));
26286
+ /** Emits the live Monaco editor once created, for imperative use. */
26287
+ kjReady = output();
26288
+ editor = null;
26289
+ monaco = null;
26290
+ applyingExternal = false;
26291
+ /** Our tracked copy of Monaco's tabFocusMode (no public getter exists). */
26292
+ tabFocusOn = false;
26293
+ /** Recompute-height callback, wired once auto-height is set up. */
26294
+ autoHeightUpdate = null;
26295
+ reducedMotion = signal(false, /* @ts-ignore */
26296
+ ...(ngDevMode ? [{ debugName: "reducedMotion" }] : /* istanbul ignore next */ []));
23555
26297
  constructor() {
23556
- afterNextRender(async () => {
23557
- try {
23558
- const echarts = await import('echarts');
23559
- this.chart = echarts.init(this.el.nativeElement);
23560
- this.chart.setOption(this.kjChartOption());
23561
- this.destroyRef.onDestroy(() => this.chart?.dispose());
23562
- }
23563
- catch {
23564
- // ECharts cannot initialize in non-browser environments (jsdom, SSR)
26298
+ afterNextRender(() => {
26299
+ if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
26300
+ const mql = window.matchMedia('(prefers-reduced-motion: reduce)');
26301
+ this.reducedMotion.set(mql.matches);
26302
+ const onChange = (e) => this.reducedMotion.set(e.matches);
26303
+ mql.addEventListener('change', onChange);
26304
+ this.destroyRef.onDestroy(() => mql.removeEventListener('change', onChange));
23565
26305
  }
26306
+ void this.init();
23566
26307
  });
23567
- afterEveryRender(() => {
23568
- if (this.chart) {
23569
- this.chart.setOption(this.kjChartOption());
26308
+ // Push external value changes into the model (guard against typing echo).
26309
+ effect(() => {
26310
+ const next = this.kjValue();
26311
+ if (this.editor && !this.applyingExternal) {
26312
+ const model = this.editor.getModel();
26313
+ if (model && model.getValue() !== next) {
26314
+ model.setValue(next);
26315
+ }
23570
26316
  }
23571
26317
  });
26318
+ // Language switch — lazy-load the language contribution first, then apply.
26319
+ effect(() => {
26320
+ const lang = normalizeLanguage(this.kjLanguage());
26321
+ const editor = this.editor;
26322
+ const monaco = this.monaco;
26323
+ if (!editor || !monaco)
26324
+ return;
26325
+ const model = editor.getModel();
26326
+ if (!model || model.getLanguageId() === lang)
26327
+ return;
26328
+ void this.loader.ensureLanguage(lang).then(() => {
26329
+ if (this.editor === editor && this.monaco) {
26330
+ this.monaco.editor.setModelLanguage(model, lang);
26331
+ }
26332
+ });
26333
+ });
26334
+ // Live option updates (readonly / minimap / line-numbers / wrap / font / motion / overrides).
26335
+ effect(() => {
26336
+ const opts = this.resolveOptions();
26337
+ if (this.editor)
26338
+ this.editor.updateOptions(opts);
26339
+ });
26340
+ // Keep the auto-height fit in sync when the cap or toggle changes.
26341
+ effect(() => {
26342
+ this.kjAutoHeight();
26343
+ this.kjMaxHeight();
26344
+ this.autoHeightUpdate?.();
26345
+ });
26346
+ // Tab-focus mode (keyboard-trap escape) — keep Monaco in sync with the input.
26347
+ effect(() => {
26348
+ const tabMoves = this.kjTabFocusMode();
26349
+ if (this.editor)
26350
+ this.syncTabFocus(tabMoves);
26351
+ });
26352
+ this.destroyRef.onDestroy(() => this.dispose());
23572
26353
  }
23573
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjChart, deps: [], target: i0.ɵɵFactoryTarget.Directive });
23574
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: KjChart, isStandalone: true, selector: "[kjChart]", inputs: { kjChartOption: { classPropertyName: "kjChartOption", publicName: "kjChartOption", isSignal: true, isRequired: true, transformFunction: null }, kjChartLabel: { classPropertyName: "kjChartLabel", publicName: "kjChartLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "img" }, properties: { "attr.aria-label": "kjChartLabel() || null" } }, ngImport: i0 });
26354
+ /** Focus the editor. */
26355
+ focus() {
26356
+ this.editor?.focus();
26357
+ }
26358
+ /** Relayout the editor to its host size. */
26359
+ layout() {
26360
+ this.editor?.layout();
26361
+ }
26362
+ /** The live Monaco editor instance, or `null` before mount / after destroy. */
26363
+ getEditor() {
26364
+ return this.editor;
26365
+ }
26366
+ async init() {
26367
+ const host = this.el.nativeElement;
26368
+ if (!host)
26369
+ return;
26370
+ try {
26371
+ this.monaco = await this.loader.load();
26372
+ }
26373
+ catch {
26374
+ // Monaco cannot load in non-browser / offline environments — leave the
26375
+ // host empty; the styled wrapper keeps showing its loading region.
26376
+ return;
26377
+ }
26378
+ if (!this.monaco)
26379
+ return;
26380
+ const language = normalizeLanguage(this.kjLanguage());
26381
+ await this.loader.ensureLanguage(language);
26382
+ if (!this.monaco)
26383
+ return;
26384
+ this.editor = this.monaco.editor.create(host, {
26385
+ value: this.kjValue(),
26386
+ language,
26387
+ ...this.resolveOptions(),
26388
+ });
26389
+ // Typing → model (mark as external-safe so the value effect doesn't echo).
26390
+ const instance = this.editor;
26391
+ instance.onDidChangeModelContent(() => {
26392
+ this.applyingExternal = true;
26393
+ this.kjValue.set(instance.getValue());
26394
+ this.applyingExternal = false;
26395
+ });
26396
+ if (this.kjAutoHeight())
26397
+ this.setupAutoHeight(instance, host);
26398
+ this.syncTabFocus(this.kjTabFocusMode());
26399
+ this.kjReady.emit(instance);
26400
+ }
26401
+ /**
26402
+ * Size the host to the editor's content height (capped by `kjMaxHeight`),
26403
+ * updating whenever the content grows/shrinks. Mirrors the docs code-viewer
26404
+ * behaviour so a snippet fits its lines instead of needing a fixed height.
26405
+ */
26406
+ setupAutoHeight(editor, host) {
26407
+ const update = () => {
26408
+ if (!this.kjAutoHeight())
26409
+ return;
26410
+ const cap = this.kjMaxHeight() ?? Number.POSITIVE_INFINITY;
26411
+ const height = Math.min(cap, editor.getContentHeight());
26412
+ host.style.height = `${height}px`;
26413
+ editor.layout({ width: host.clientWidth, height });
26414
+ };
26415
+ this.autoHeightUpdate = update;
26416
+ const sub = editor.onDidContentSizeChange(update);
26417
+ this.destroyRef.onDestroy(() => sub.dispose());
26418
+ update();
26419
+ }
26420
+ resolveOptions() {
26421
+ const reduced = this.reducedMotion();
26422
+ const base = {
26423
+ readOnly: this.kjReadonly(),
26424
+ minimap: { enabled: this.kjMinimap() },
26425
+ lineNumbers: this.kjLineNumbers(),
26426
+ wordWrap: this.kjWordWrap(),
26427
+ fontSize: this.kjFontSize(),
26428
+ ariaLabel: this.kjAriaLabel(),
26429
+ // AAA: let Monaco detect a screen reader and switch to accessible rendering.
26430
+ accessibilitySupport: 'auto',
26431
+ automaticLayout: true,
26432
+ scrollBeyondLastLine: false,
26433
+ // Reduced-motion (2.3.3): kill caret/scroll animation.
26434
+ cursorBlinking: reduced ? 'solid' : 'blink',
26435
+ cursorSmoothCaretAnimation: reduced ? 'off' : 'on',
26436
+ smoothScrolling: !reduced,
26437
+ };
26438
+ const theme = this.kjTheme();
26439
+ if (theme)
26440
+ base.theme = theme;
26441
+ return { ...base, ...this.kjOptions() };
26442
+ }
26443
+ /**
26444
+ * Force Monaco's tabFocusMode to a specific state (idempotent). `tabFocusMode`
26445
+ * is not a construction option — it's a context key flipped by the
26446
+ * `toggleTabFocusMode` command (bound to `Ctrl+M`). We track our own copy
26447
+ * since Monaco exposes no public getter, and only trigger the toggle when the
26448
+ * desired state differs from what we last applied.
26449
+ */
26450
+ syncTabFocus(tabMoves) {
26451
+ if (!this.editor || this.tabFocusOn === tabMoves)
26452
+ return;
26453
+ this.editor.trigger('kjEditor', 'editor.action.toggleTabFocusMode', undefined);
26454
+ this.tabFocusOn = tabMoves;
26455
+ }
26456
+ dispose() {
26457
+ this.editor?.getModel()?.dispose();
26458
+ this.editor?.dispose();
26459
+ this.editor = null;
26460
+ }
26461
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjEditor, deps: [], target: i0.ɵɵFactoryTarget.Directive });
26462
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: KjEditor, isStandalone: true, selector: "[kjEditor]", inputs: { kjValue: { classPropertyName: "kjValue", publicName: "kjValue", isSignal: true, isRequired: false, transformFunction: null }, kjLanguage: { classPropertyName: "kjLanguage", publicName: "kjLanguage", isSignal: true, isRequired: false, transformFunction: null }, kjReadonly: { classPropertyName: "kjReadonly", publicName: "kjReadonly", isSignal: true, isRequired: false, transformFunction: null }, kjMinimap: { classPropertyName: "kjMinimap", publicName: "kjMinimap", isSignal: true, isRequired: false, transformFunction: null }, kjLineNumbers: { classPropertyName: "kjLineNumbers", publicName: "kjLineNumbers", isSignal: true, isRequired: false, transformFunction: null }, kjWordWrap: { classPropertyName: "kjWordWrap", publicName: "kjWordWrap", isSignal: true, isRequired: false, transformFunction: null }, kjFontSize: { classPropertyName: "kjFontSize", publicName: "kjFontSize", isSignal: true, isRequired: false, transformFunction: null }, kjAutoHeight: { classPropertyName: "kjAutoHeight", publicName: "kjAutoHeight", isSignal: true, isRequired: false, transformFunction: null }, kjMaxHeight: { classPropertyName: "kjMaxHeight", publicName: "kjMaxHeight", isSignal: true, isRequired: false, transformFunction: null }, kjTheme: { classPropertyName: "kjTheme", publicName: "kjTheme", isSignal: true, isRequired: false, transformFunction: null }, kjAriaLabel: { classPropertyName: "kjAriaLabel", publicName: "kjAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, kjTabFocusMode: { classPropertyName: "kjTabFocusMode", publicName: "kjTabFocusMode", isSignal: true, isRequired: false, transformFunction: null }, kjOptions: { classPropertyName: "kjOptions", publicName: "kjOptions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { kjValue: "kjValueChange", kjReady: "kjReady" }, host: { properties: { "attr.aria-label": "kjAriaLabel()" } }, exportAs: ["kjEditor"], ngImport: i0 });
23575
26463
  }
23576
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjChart, decorators: [{
26464
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjEditor, decorators: [{
23577
26465
  type: Directive,
23578
26466
  args: [{
23579
- selector: '[kjChart]', standalone: true,
23580
- host: { role: 'img', '[attr.aria-label]': 'kjChartLabel() || null' },
26467
+ selector: '[kjEditor]',
26468
+ standalone: true,
26469
+ exportAs: 'kjEditor',
26470
+ host: {
26471
+ '[attr.aria-label]': 'kjAriaLabel()',
26472
+ },
23581
26473
  }]
23582
- }], ctorParameters: () => [], propDecorators: { kjChartOption: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjChartOption", required: true }] }], kjChartLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjChartLabel", required: false }] }] } });
26474
+ }], ctorParameters: () => [], propDecorators: { kjValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjValue", required: false }] }, { type: i0.Output, args: ["kjValueChange"] }], kjLanguage: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjLanguage", required: false }] }], kjReadonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjReadonly", required: false }] }], kjMinimap: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjMinimap", required: false }] }], kjLineNumbers: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjLineNumbers", required: false }] }], kjWordWrap: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjWordWrap", required: false }] }], kjFontSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjFontSize", required: false }] }], kjAutoHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjAutoHeight", required: false }] }], kjMaxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjMaxHeight", required: false }] }], kjTheme: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjTheme", required: false }] }], kjAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjAriaLabel", required: false }] }], kjTabFocusMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjTabFocusMode", required: false }] }], kjOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjOptions", required: false }] }], kjReady: [{ type: i0.Output, args: ["kjReady"] }] } });
26475
+
26476
+ /**
26477
+ * Configure the Monaco source for `KjEditor` / `<kj-editor>`. Call once at the
26478
+ * app (or route) level. With no arguments the editor loads Monaco from the
26479
+ * default CDN via `@monaco-editor/loader`.
26480
+ *
26481
+ * @example
26482
+ * // Default CDN loader (nothing to install beyond the peer deps):
26483
+ * provideMonaco()
26484
+ *
26485
+ * @example
26486
+ * // Self-hosted Monaco assets:
26487
+ * provideMonaco({ vsPath: '/assets/monaco/vs' })
26488
+ *
26489
+ * @example
26490
+ * // Fully custom / bundled Monaco (you own the worker setup):
26491
+ * provideMonaco({ loader: () => import('monaco-editor') })
26492
+ *
26493
+ * @doc
26494
+ * @doc-name editor
26495
+ * @doc-order 1
26496
+ */
26497
+ function provideMonaco(config = {}) {
26498
+ return makeEnvironmentProviders([{ provide: KJ_MONACO_CONFIG, useValue: config }]);
26499
+ }
23583
26500
 
23584
26501
  /**
23585
26502
  * Marks an element as a kouji divider / separator. Owns the small a11y opinion
@@ -23992,6 +26909,91 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
23992
26909
  }]
23993
26910
  }], ctorParameters: () => [], propDecorators: { kjDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjDisabled", required: false }] }], kjUnderline: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjUnderline", required: false }] }], kjExternal: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjExternal", required: false }] }] } });
23994
26911
 
26912
+ /**
26913
+ * Headless "skip to content" link. Turns a native `<a>` into a
26914
+ * [WCAG 2.4.1 Bypass Blocks](https://www.w3.org/TR/WCAG21/#bypass-blocks)
26915
+ * mechanism: a fragment link that, when activated, moves **keyboard focus** to
26916
+ * the page's main-content landmark — not merely the scroll position.
26917
+ *
26918
+ * Owns the two behaviours a CSS-only skip link cannot deliver:
26919
+ *
26920
+ * 1. **Fragment `href`.** `[attr.href]` reflects `#<target-id>`, so the element
26921
+ * is a real anchor (role=link, Enter activates) and carries the id in the
26922
+ * SSR-prerendered HTML.
26923
+ * 2. **Deterministic focus move.** On `click` (which Enter also fires on an
26924
+ * anchor) the directive `preventDefault()`s the navigation, looks the target
26925
+ * up by id, makes it programmatically focusable via `tabindex="-1"` when it
26926
+ * has no `tabindex`, and calls `focus()` (which also scrolls it into view).
26927
+ *
26928
+ * `preventDefault()` is required, not optional: under a `<base href="/">`
26929
+ * (the norm for Angular SPAs) a fragment-only reference like `#main-content`
26930
+ * resolves against the **base URL**, not the current document — so the native
26931
+ * click would navigate to `/#main-content` (the root route), swapping the
26932
+ * page out and discarding focus. Moving focus programmatically is both the
26933
+ * correct behaviour and immune to that gotcha.
26934
+ *
26935
+ * Styling (visually-hidden-until-focused) is a component-layer concern; see
26936
+ * `KjSkipLinkComponent` in `@kouji-ui/components`.
26937
+ *
26938
+ * @example
26939
+ * ```html
26940
+ * <a kjSkipLink>Skip to main content</a>
26941
+ * <main id="main-content" tabindex="-1">…</main>
26942
+ * ```
26943
+ * @example
26944
+ * ```html
26945
+ * <a kjSkipLink="page-body">Skip to content</a>
26946
+ * <section id="page-body" tabindex="-1">…</section>
26947
+ * ```
26948
+ *
26949
+ * @doc-category Core/Navigation
26950
+ * @doc
26951
+ * @doc-name skip-link
26952
+ * @doc-description Turns a native anchor into a focus-moving "skip to content" bypass link.
26953
+ * @doc-is-main
26954
+ */
26955
+ class KjSkipLink {
26956
+ document = inject(DOCUMENT$1);
26957
+ /**
26958
+ * `id` of the element to move focus to. Aliased to the selector attribute so
26959
+ * `<a kjSkipLink="page-body">` sets it directly. Defaults to `'main-content'`.
26960
+ *
26961
+ * The `transform` maps an empty value to the default: a bare `<a kjSkipLink>`
26962
+ * binds the attribute as `''` (the selector attribute is present but valueless),
26963
+ * which would otherwise shadow the initial value.
26964
+ */
26965
+ kjSkipLink = input('main-content', { ...(ngDevMode ? { debugName: "kjSkipLink" } : /* istanbul ignore next */ {}), transform: (value) => value || 'main-content' });
26966
+ /**
26967
+ * Moves keyboard focus to the target landmark. Suppresses the anchor's native
26968
+ * navigation (see class docs — it is base-relative and would leave the page),
26969
+ * then adds `tabindex="-1"` when the target is not already focusable so
26970
+ * `focus()` succeeds while keeping it out of the sequential tab order.
26971
+ */
26972
+ onActivate(event) {
26973
+ event.preventDefault();
26974
+ const target = this.document.getElementById(this.kjSkipLink());
26975
+ if (!target)
26976
+ return;
26977
+ if (!target.hasAttribute('tabindex')) {
26978
+ target.setAttribute('tabindex', '-1');
26979
+ }
26980
+ target.focus();
26981
+ }
26982
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjSkipLink, deps: [], target: i0.ɵɵFactoryTarget.Directive });
26983
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.5", type: KjSkipLink, isStandalone: true, selector: "a[kjSkipLink]", inputs: { kjSkipLink: { classPropertyName: "kjSkipLink", publicName: "kjSkipLink", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "onActivate($event)" }, properties: { "attr.href": "\"#\" + kjSkipLink()" } }, ngImport: i0 });
26984
+ }
26985
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KjSkipLink, decorators: [{
26986
+ type: Directive,
26987
+ args: [{
26988
+ selector: 'a[kjSkipLink]',
26989
+ standalone: true,
26990
+ host: {
26991
+ '[attr.href]': '"#" + kjSkipLink()',
26992
+ '(click)': 'onActivate($event)',
26993
+ },
26994
+ }]
26995
+ }], propDecorators: { kjSkipLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "kjSkipLink", required: false }] }] } });
26996
+
23995
26997
  /**
23996
26998
  * Default Breadcrumb presets shipped by kouji-ui. Exported so consumers can
23997
26999
  * spread when extending: `[...KJ_BREADCRUMB_DEFAULTS.sizes, 'xl']`.
@@ -26785,5 +29787,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
26785
29787
  * Generated bundle index. Do not edit.
26786
29788
  */
26787
29789
 
26788
- export { DRAWER_DATA, DRAWER_DRAG, DRAWER_SIDE, KJ_ACCORDION, KJ_ACCORDION_ITEM, KJ_ALERT, KJ_ALERT_CONFIG, KJ_ALERT_DEFAULTS, KJ_AVATAR, KJ_AVATAR_GROUP, KJ_BREADCRUMB, KJ_BREADCRUMB_CONFIG, KJ_BREADCRUMB_DEFAULTS, KJ_BUTTON_CONFIG, KJ_BUTTON_DEFAULTS, KJ_BUTTON_GROUP, KJ_CALENDAR, KJ_CAROUSEL, KJ_CAROUSEL_INDICATORS, KJ_CAROUSEL_SLIDE, KJ_CASCADE_SELECT, KJ_CHAT, KJ_CHAT_BUBBLE_CONFIG, KJ_CHAT_BUBBLE_DEFAULTS, KJ_CHAT_LOG, KJ_COLOR_PICKER, KJ_CONFIRM_POPUP, KJ_DATE_PICKER, KJ_DROPDOWN_MENU, KJ_FIELD, KJ_FILE_UPLOAD, KJ_FILE_UPLOAD_DEFAULT_MESSAGES, KJ_FILE_UPLOAD_ITEM, KJ_FORM, KJ_FORM_FIELD, KJ_ICON_CSS_PATH, KJ_ICON_ENTRIES, KJ_ICON_LOADER, KJ_ICON_REGISTRY, KJ_ICON_RESOLVER, KJ_INPUT_GROUP, KJ_INPUT_MASK_PRESETS, KJ_INPUT_MASK_TOKENS, KJ_INPUT_OTP, KJ_KBD_SIZE_PRESET, KJ_LINK_CONFIG, KJ_LINK_DEFAULTS, KJ_LIST, KJ_LIST_FOCUS_MODE, KJ_LIST_NAVIGATOR_CONFIG, KJ_LIST_ROW, KJ_MENUBAR, KJ_NUMBER_INPUT, KJ_OVERLAY_BACKDROP_STRATEGY, KJ_OVERLAY_BADGE, KJ_OVERLAY_FOCUS_TRAP_STRATEGY, KJ_OVERLAY_LIVE_ANNOUNCER_STRATEGY, KJ_OVERLAY_MOUNT_STRATEGY, KJ_OVERLAY_PANEL_ROLE, KJ_OVERLAY_POSITION_STRATEGY, KJ_OVERLAY_SCROLL_LOCK_STRATEGY, KJ_OVERLAY_TRIGGER_EVENT_STRATEGY, KJ_PAGINATION, KJ_PAGINATION_CONFIG, KJ_PAGINATION_DEFAULTS, KJ_PASSWORD_INPUT, KJ_PROGRESS_BAR, KJ_PROGRESS_BAR_CONFIG, KJ_PROGRESS_BAR_DEFAULTS, KJ_PROSE_CSS_PATH, KJ_RADIO_GROUP, KJ_ROVING_TABINDEX, KJ_SELECT, KJ_SIZE_PRESET, KJ_SLIDER, KJ_SPEED_DIAL, KJ_SPINNER_CONFIG, KJ_SPINNER_DEFAULTS, KJ_STEP, KJ_STEPPER, KJ_TABLE, KJ_TABLE_STORAGE, KJ_TABS, KJ_TAG, KJ_TAG_CONFIG, KJ_TAG_DEFAULTS, KJ_TAG_LIST, KJ_TEXTAREA_CONFIG, KJ_TEXTAREA_DEFAULTS, KJ_TIME_PICKER, KJ_TOAST_LIST_STRATEGY, KJ_TOAST_SONNER_STRATEGY, KJ_TOAST_STRATEGY, KJ_TREE_SELECT, KJ_VARIANT_PRESET, KjAccordion, KjAccordionContent, KjAccordionItem, KjAccordionTrigger, KjAlert, KjAlertActions, KjAlertDescription, KjAlertDismiss, KjAlertIcon, KjAlertTitle, KjAriaDescribedBy, KjAriaLabelledBy, KjAvatar, KjAvatarFallback, KjAvatarGroup, KjAvatarImage, KjBackdrop, KjBadge, KjBlockquote, KjBreadcrumb, KjBreadcrumbCurrent, KjBreadcrumbEllipsis, KjBreadcrumbItem, KjBreadcrumbLink, KjBreadcrumbList, KjBreadcrumbSeparator, KjButton, KjButtonGroup, KjCalendar, KjCalendarDay, KjCalendarGrid, KjCalendarHeader, KjCarousel, KjCarouselAutoplay, KjCarouselIndicator, KjCarouselIndicators, KjCarouselNext, KjCarouselPauseToggle, KjCarouselPrevious, KjCarouselSlide, KjCarouselViewport, KjCascadeSelect, KjCascadeSelectOption, KjCascadeSelectPanel, KjCascadeSelectSubPanel, KjCascadeSelectTrigger, KjChart, KjChat, KjChatAvatar, KjChatBubble, KjChatFooter, KjChatHeader, KjChatLog, KjCheckbox, KjCode, KjColorPicker, KjColorPickerAlphaSlider, KjColorPickerArea, KjColorPickerHueSlider, KjColorPickerInput, KjColorPickerPanel, KjColorPickerTrigger, KjCombobox, KjComboboxInput, KjComboboxListbox, KjComboboxOption, KjCommandEmpty, KjCommandGroup, KjCommandInput, KjCommandItem, KjCommandList, KjCommandPalette, KjCommandPaletteDialog, KjCommandPaletteTrigger, KjCommandSeparator, KjConfirmPopup, KjConfirmPopupAction, KjConfirmPopupCancel, KjConfirmPopupContent, KjConfirmPopupMessage, KjConfirmPopupTrigger, KjDatePicker, KjDatePickerCalendar, KjDatePickerTrigger, KjDialog$1 as KjDialog, KjDialogRef, KjDialog as KjDialogService, KjDirectionality, KjDisabled, KjDivider, KjDrawer, KjDrawerRef, KjDrawerService, KjDropdownMenu, KjDropdownMenuContent, KjDropdownMenuGroup, KjDropdownMenuItem, KjDropdownMenuLabel, KjDropdownMenuSeparator, KjDropdownMenuTrigger, KjField, KjFieldError, KjFieldGroup, KjFieldHelp, KjFieldLabel, KjFileUpload, KjFileUploadDropzone, KjFileUploadItem, KjFileUploadList, KjFileUploadTrigger, KjFilterableList, KjFocusRing, KjFocusTrap, KjForm, KjFormControl, KjFormError, KjFormErrorSummary, KjFormField, KjFormLabel, KjIconDirective, KjId, KjInput, KjInputGroup, KjInputGroupAddon, KjInputMask, KjInputOtp, KjInputOtpCell, KjKbd, KjLead, KjLink, KjList, KjListGroup, KjListGroupLabel, KjListItem, KjListNavigator, KjListRow, KjListSeparator, KjLiveRegion, KjMenubar, KjMenubarItem, KjMuted, KjNumberInput, KjNumberInputGroup, KjNumberStepper, KjOption, KjOverlayBadge, KjOverlayBadgeContent, KjOverlayBuilder, KjOverlayController, KjOverlayHandle, KjOverlayPanel, KjOverlayStack, KjOverlayTrigger, KjOverlayWrapper, KjPagination, KjPaginationEllipsis, KjPaginationFirst, KjPaginationInfo, KjPaginationItem, KjPaginationLast, KjPaginationNext, KjPaginationPrevious, KjPasswordCapsLockWarning, KjPasswordInput, KjPasswordInputScope, KjPasswordStrength, KjPasswordToggle, KjPopoverArrow, KjPopoverClose, KjPopoverContent, KjPopoverTitle, KjPopoverTrigger, KjProgressBar, KjProgressBarFill, KjRadio, KjRadioGroup, KjRovingTabindex, KjRovingTabindexItemDirective, KjSelect, KjSelectContent, KjSelectTrigger, KjSelectionModel, KjSize, KjSkeleton, KjSlider, KjSliderRange, KjSliderThumb, KjSliderTrack, KjSpeedDial, KjSpeedDialAction, KjSpeedDialActions, KjSpeedDialTrigger, KjSpinner, KjStep, KjStepContent, KjStepLabel, KjStepper, KjStepperNext, KjStepperPrevious, KjStepperReset, KjTab, KjTabList, KjTabPanel, KjTable, KjTableCell, KjTableFilterOutlet, KjTableHeader, KjTableKeyboardNav, KjTableRow, KjTabs, KjTag, KjTagList, KjTagRemove, KjTextarea, KjTimePicker, KjTimePickerHours, KjTimePickerMeridiem, KjTimePickerMinutes, KjTimePickerSeconds, KjToast, KjToastClose, KjToastPanel, KjToastRef, KjToastService, KjToastViewport, KjToggle, KjTooltipArrow, KjTooltipContent, KjTooltipGroup, KjTooltipTrigger, KjTreeSelect, KjTreeSelectContent, KjTreeSelectNode, KjTreeSelectToggle, KjTreeSelectTrigger, KjTruncate, KjTypeAhead, KjVariant, KjVisuallyHidden, MaskEngine, addDays, addMonths, addYears, anchoredTo, assertive, bindPresets, blurredBackdrop, bodyPortal, buildMonthMatrix, compareDay, compileMask, corner, cssClip, defaultMaskTokens, defaultPasswordScorer, edgeSheet, endOfMonth, firstDayOfWeek, formatDateLong, formatDateShort, formatMonthYear, getIconMode, htmlOverflow, inContainer, inMemoryAdapter, inPlace, inPlaceSibling, inertBased, injectFilterableList, injectKjFilterParams, injectKjIconResolver, injectListItem, injectSelectionModel, isInRange, isKjFilterModel, isSameDay, isSameMonth, kjColumn, kjColumnGroup, kjContainsFilter, kjDateFilterFn, kjFileMatchesAccept, kjFuzzyFilter, kjHsvToHsl, kjHsvToRgb, kjMultiFilterFn, kjNumberFilterFn, kjParseHex, kjRgbToHex, kjRgbToHsv, kjSetFilterFn, kjStartsWithFilter, kjSubstringFilter, kjTableResource, kjTextFilterFn, localStorageAdapter, nextCascadeId, nextDropdownMenuLabelId, nextPopoverTitleId, noBackdrop, noScrollLock, noTrap, onClick, onContextMenu, onFocus, onFocusOrInput, onHotkey, onHover, parseDate, pointAt, polite, programmatic, provideIconLoader, provideIconResolver, provideIcons, provideKjAlert, provideKjBreadcrumb, provideKjButton, provideKjChatBubble, provideKjFilterParams, provideKjInputMaskTokens, provideKjLink, provideKjPagination, provideKjProgressBar, provideKjSpinner, provideKjTableStorage, provideKjTag, provideKjTextarea, provideKjToastListStrategy, provideKjToastSonnerStrategy, provideKjToastStrategy, sessionStorageAdapter, silent, solidBackdrop, startOfDay, startOfMonth, stripDiacritics, tabCycle, toDeepSignal, viewportCentered, weekdayLongNames, weekdayShortNames };
29790
+ export { DRAWER_DATA, DRAWER_DRAG, DRAWER_SIDE, EN_CATALOG, FR_CATALOG, KJ_ACCORDION, KJ_ACCORDION_ITEM, KJ_ALERT, KJ_ALERT_CONFIG, KJ_ALERT_DEFAULTS, KJ_AVATAR, KJ_AVATAR_GROUP, KJ_BREADCRUMB, KJ_BREADCRUMB_CONFIG, KJ_BREADCRUMB_DEFAULTS, KJ_BUTTON_CONFIG, KJ_BUTTON_DEFAULTS, KJ_BUTTON_GROUP, KJ_CALENDAR, KJ_CAROUSEL, KJ_CAROUSEL_INDICATORS, KJ_CAROUSEL_SLIDE, KJ_CASCADE_SELECT, KJ_CHAT, KJ_CHAT_BUBBLE_CONFIG, KJ_CHAT_BUBBLE_DEFAULTS, KJ_CHAT_LOG, KJ_COLOR_PICKER, KJ_CONFIRM_POPUP, KJ_DATE_PICKER, KJ_DATE_RANGE_PRESETS, KJ_DROPDOWN_MENU, KJ_ECHARTS, KJ_FIELD, KJ_FILE_UPLOAD, KJ_FILE_UPLOAD_DEFAULT_MESSAGES, KJ_FILE_UPLOAD_ITEM, KJ_FORM, KJ_FORM_FIELD, KJ_ICON_CSS_PATH, KJ_ICON_ENTRIES, KJ_ICON_LOADER, KJ_ICON_REGISTRY, KJ_ICON_RESOLVER, KJ_INPUT_GROUP, KJ_INPUT_MASK_PRESETS, KJ_INPUT_MASK_TOKENS, KJ_INPUT_OTP, KJ_KBD_SIZE_PRESET, KJ_LINK_CONFIG, KJ_LINK_DEFAULTS, KJ_LIST, KJ_LIST_FOCUS_MODE, KJ_LIST_NAVIGATOR_CONFIG, KJ_LIST_ROW, KJ_LOCALE_CONFIG, KJ_MENUBAR, KJ_MONACO_CONFIG, KJ_MONACO_LANGUAGE_LOADERS, KJ_NUMBER_INPUT, KJ_OVERLAY_BACKDROP_STRATEGY, KJ_OVERLAY_BADGE, KJ_OVERLAY_FOCUS_TRAP_STRATEGY, KJ_OVERLAY_LIVE_ANNOUNCER_STRATEGY, KJ_OVERLAY_MOUNT_STRATEGY, KJ_OVERLAY_PANEL_ROLE, KJ_OVERLAY_POSITION_STRATEGY, KJ_OVERLAY_SCROLL_LOCK_STRATEGY, KJ_OVERLAY_TRIGGER_EVENT_STRATEGY, KJ_PAGINATION, KJ_PAGINATION_CONFIG, KJ_PAGINATION_DEFAULTS, KJ_PASSWORD_INPUT, KJ_PROGRESS_BAR, KJ_PROGRESS_BAR_CONFIG, KJ_PROGRESS_BAR_DEFAULTS, KJ_PROSE_CSS_PATH, KJ_RADIO_GROUP, KJ_RICH_TEXT, KJ_RICH_TEXT_EXTENSIONS, KJ_RICH_TEXT_FEATURES, KJ_RICH_TEXT_NODE, KJ_ROVING_TABINDEX, KJ_RTE_OVERLAY_DATA, KJ_SELECT, KJ_SIZE_PRESET, KJ_SLIDER, KJ_SPEED_DIAL, KJ_SPINNER_CONFIG, KJ_SPINNER_DEFAULTS, KJ_STEP, KJ_STEPPER, KJ_TABLE, KJ_TABLE_STORAGE, KJ_TABS, KJ_TAG, KJ_TAG_CONFIG, KJ_TAG_DEFAULTS, KJ_TAG_LIST, KJ_TEXTAREA_CONFIG, KJ_TEXTAREA_DEFAULTS, KJ_TIME_PICKER, KJ_TOAST_LIST_STRATEGY, KJ_TOAST_SONNER_STRATEGY, KJ_TOAST_STRATEGY, KJ_TRANSLATION_CATALOGS, KJ_TREE_SELECT, KJ_VARIANT_PRESET, KjAccordion, KjAccordionContent, KjAccordionItem, KjAccordionTrigger, KjAlert, KjAlertActions, KjAlertDescription, KjAlertDismiss, KjAlertIcon, KjAlertTitle, KjAriaDescribedBy, KjAriaLabelledBy, KjAvatar, KjAvatarFallback, KjAvatarGroup, KjAvatarImage, KjBackdrop, KjBadge, KjBlockquote, KjBreadcrumb, KjBreadcrumbCurrent, KjBreadcrumbEllipsis, KjBreadcrumbItem, KjBreadcrumbLink, KjBreadcrumbList, KjBreadcrumbSeparator, KjButton, KjButtonGroup, KjCalendar, KjCalendarDay, KjCalendarGrid, KjCalendarHeader, KjCarousel, KjCarouselAutoplay, KjCarouselIndicator, KjCarouselIndicators, KjCarouselNext, KjCarouselPauseToggle, KjCarouselPrevious, KjCarouselSlide, KjCarouselViewport, KjCascadeSelect, KjCascadeSelectOption, KjCascadeSelectPanel, KjCascadeSelectSubPanel, KjCascadeSelectTrigger, KjChart, KjChartTableFallback, KjChat, KjChatAnnouncer, KjChatAvatar, KjChatBubble, KjChatFooter, KjChatHeader, KjChatLog, KjChatStore, KjCheckbox, KjCode, KjColorPicker, KjColorPickerAlphaSlider, KjColorPickerArea, KjColorPickerHueSlider, KjColorPickerInput, KjColorPickerPanel, KjColorPickerTrigger, KjCombobox, KjComboboxInput, KjComboboxListbox, KjComboboxOption, KjCommandEmpty, KjCommandGroup, KjCommandInput, KjCommandItem, KjCommandList, KjCommandPalette, KjCommandPaletteDialog, KjCommandPaletteTrigger, KjCommandSeparator, KjConfirmPopup, KjConfirmPopupAction, KjConfirmPopupCancel, KjConfirmPopupContent, KjConfirmPopupMessage, KjConfirmPopupTrigger, KjDatePicker, KjDatePickerCalendar, KjDatePickerTrigger, KjDateRangePresetOption, KjDateRangePresets, KjDialog$1 as KjDialog, KjDialogRef, KjDialog as KjDialogService, KjDirectionality, KjDisabled, KjDivider, KjDrawer, KjDrawerRef, KjDrawerService, KjDropdownMenu, KjDropdownMenuContent, KjDropdownMenuGroup, KjDropdownMenuItem, KjDropdownMenuLabel, KjDropdownMenuSeparator, KjDropdownMenuTrigger, KjEditor, KjEditorLoader, KjField, KjFieldError, KjFieldGroup, KjFieldHelp, KjFieldLabel, KjFileUpload, KjFileUploadDropzone, KjFileUploadItem, KjFileUploadList, KjFileUploadTrigger, KjFilterableList, KjFocusRing, KjFocusTrap, KjForm, KjFormControl, KjFormError, KjFormErrorSummary, KjFormField, KjFormLabel, KjIconDirective, KjId, KjInput, KjInputGroup, KjInputGroupAddon, KjInputMask, KjInputOtp, KjInputOtpCell, KjKbd, KjLead, KjLink, KjList, KjListGroup, KjListGroupLabel, KjListItem, KjListNavigator, KjListRow, KjListSeparator, KjLiveRegion, KjLocale, KjMenubar, KjMenubarItem, KjMotion, KjMuted, KjNumberInput, KjNumberInputGroup, KjNumberStepper, KjOption, KjOverlayBadge, KjOverlayBadgeContent, KjOverlayBuilder, KjOverlayController, KjOverlayHandle, KjOverlayPanel, KjOverlayStack, KjOverlayTrigger, KjOverlayWrapper, KjPagination, KjPaginationEllipsis, KjPaginationFirst, KjPaginationInfo, KjPaginationItem, KjPaginationLast, KjPaginationNext, KjPaginationPrevious, KjPasswordCapsLockWarning, KjPasswordInput, KjPasswordInputScope, KjPasswordStrength, KjPasswordToggle, KjPopoverArrow, KjPopoverClose, KjPopoverContent, KjPopoverTitle, KjPopoverTrigger, KjProgressBar, KjProgressBarFill, KjRadio, KjRadioGroup, KjReducedMotion, KjRichTextEditor, KjRichTextExtensionDirective, KjRovingTabindex, KjRovingTabindexItemDirective, KjSelect, KjSelectContent, KjSelectTrigger, KjSelectionModel, KjSheet, KjSheetRef, KjSheetService, KjSize, KjSkeleton, KjSkipLink, KjSlider, KjSliderRange, KjSliderThumb, KjSliderTrack, KjSpeedDial, KjSpeedDialAction, KjSpeedDialActions, KjSpeedDialTrigger, KjSpinner, KjStep, KjStepContent, KjStepLabel, KjStepper, KjStepperNext, KjStepperPrevious, KjStepperReset, KjTab, KjTabList, KjTabPanel, KjTable, KjTableCell, KjTableFilterOutlet, KjTableHeader, KjTableKeyboardNav, KjTableRow, KjTabs, KjTag, KjTagList, KjTagRemove, KjTextarea, KjTimePicker, KjTimePickerHours, KjTimePickerMeridiem, KjTimePickerMinutes, KjTimePickerSeconds, KjToast, KjToastClose, KjToastPanel, KjToastRef, KjToastService, KjToastViewport, KjToggle, KjTooltipArrow, KjTooltipContent, KjTooltipGroup, KjTooltipTrigger, KjTranslate, KjTranslateService, KjTreeSelect, KjTreeSelectContent, KjTreeSelectNode, KjTreeSelectToggle, KjTreeSelectTrigger, KjTruncate, KjTypeAhead, KjVariant, KjVisuallyHidden, MaskEngine, SHEET_ARIA_LABEL, SHEET_DATA, SHEET_DETENT, SHEET_DISMISSIBLE, addDays, addMonths, addYears, anchoredTo, assertive, bindPresets, blurredBackdrop, bodyPortal, buildMonthMatrix, coalesceAnnouncement, compareDay, compileMask, corner, createKjDecoratorNode, createKjImageNode, cssClip, defaultDateRangePresets, defaultMaskTokens, defaultPasswordScorer, edgeSheet, endOfMonth, firstDayOfWeek, formatDateLong, formatDateShort, formatMonthYear, getIconMode, htmlOverflow, inContainer, inMemoryAdapter, inPlace, inPlaceSibling, inertBased, injectFilterableList, injectKjFilterParams, injectKjIconResolver, injectListItem, injectRichTextNode, injectRteOverlayData, injectSelectionModel, isInRange, isKjFilterModel, isSameDay, isSameMonth, kjColumn, kjColumnGroup, kjContainsFilter, kjDateFilterFn, kjFileMatchesAccept, kjFuzzyFilter, kjHsvToHsl, kjHsvToRgb, kjMultiFilterFn, kjNumberFilterFn, kjParseHex, kjRgbToHex, kjRgbToHsv, kjSetFilterFn, kjStartsWithFilter, kjSubstringFilter, kjTableResource, kjTextFilterFn, localStorageAdapter, matchSlashCommands, nextCascadeId, nextChatMessageId, nextDropdownMenuLabelId, nextPopoverTitleId, noBackdrop, noScrollLock, noTrap, normalizeLanguage, onClick, onContextMenu, onFocus, onFocusOrInput, onHotkey, onHover, parseDate, parseSlash, pointAt, polite, programmatic, provideECharts, provideIconLoader, provideIconResolver, provideIcons, provideKjAlert, provideKjBreadcrumb, provideKjButton, provideKjChatBubble, provideKjDocumentDirection, provideKjFilterParams, provideKjInputMaskTokens, provideKjLink, provideKjLocale, provideKjPagination, provideKjProgressBar, provideKjRichText, provideKjSpinner, provideKjTableStorage, provideKjTag, provideKjTextarea, provideKjToastListStrategy, provideKjToastSonnerStrategy, provideKjToastStrategy, provideKjTranslations, provideMonaco, provideMonacoLanguages, resolveChartPalette, resolveDateRangePreset, sessionStorageAdapter, silent, solidBackdrop, startOfDay, startOfMonth, stripDiacritics, tabCycle, toDeepSignal, viewportCentered, weekdayLongNames, weekdayShortNames };
26789
29791
  //# sourceMappingURL=kouji-ui-core.mjs.map