@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.
- package/fesm2022/kouji-ui-core-engine-CKMr0aiZ.mjs +390 -0
- package/fesm2022/kouji-ui-core-engine-CKMr0aiZ.mjs.map +1 -0
- package/fesm2022/kouji-ui-core.mjs +3183 -181
- package/fesm2022/kouji-ui-core.mjs.map +1 -1
- package/motion/motion.css +90 -0
- package/package.json +50 -2
- package/types/kouji-ui-core.d.ts +2066 -68
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
10732
|
-
*
|
|
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
|
-
* ```
|
|
10736
|
-
*
|
|
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/
|
|
11375
|
+
* @doc-category Core/AI
|
|
10739
11376
|
* @doc
|
|
10740
|
-
* @doc-name
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
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
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
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
|
-
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
|
|
11746
|
-
|
|
11747
|
-
|
|
11748
|
-
|
|
11749
|
-
|
|
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
|
-
|
|
11753
|
-
|
|
11754
|
-
|
|
11755
|
-
return this._overlayTrigger.controller;
|
|
12775
|
+
/** @internal Bind the rendered component instance for `instance`. */
|
|
12776
|
+
bindInstance(instance) {
|
|
12777
|
+
this._instance = instance;
|
|
11756
12778
|
}
|
|
11757
|
-
|
|
11758
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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: "
|
|
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
|
-
*
|
|
23532
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
26239
|
+
class KjEditor {
|
|
23545
26240
|
el = inject(ElementRef);
|
|
23546
26241
|
destroyRef = inject(DestroyRef);
|
|
23547
|
-
|
|
23548
|
-
|
|
23549
|
-
|
|
23550
|
-
|
|
23551
|
-
|
|
23552
|
-
|
|
23553
|
-
|
|
23554
|
-
|
|
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(
|
|
23557
|
-
|
|
23558
|
-
const
|
|
23559
|
-
this.
|
|
23560
|
-
this.
|
|
23561
|
-
|
|
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
|
-
|
|
23568
|
-
|
|
23569
|
-
|
|
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
|
-
|
|
23574
|
-
|
|
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:
|
|
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: '[
|
|
23580
|
-
|
|
26467
|
+
selector: '[kjEditor]',
|
|
26468
|
+
standalone: true,
|
|
26469
|
+
exportAs: 'kjEditor',
|
|
26470
|
+
host: {
|
|
26471
|
+
'[attr.aria-label]': 'kjAriaLabel()',
|
|
26472
|
+
},
|
|
23581
26473
|
}]
|
|
23582
|
-
}], ctorParameters: () => [], propDecorators: {
|
|
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
|