From 62960e0b7225687a329490724d884362f2ddc384 Mon Sep 17 00:00:00 2001 From: Marko Stijak Date: Tue, 19 May 2026 14:59:46 +0200 Subject: [PATCH 1/3] Port DateTimePicker endless wheel scrolling to master MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings the DateTimePicker infinite-scroll improvement (PR #1280, merged to feat/master/litmus-ts) onto master so it ships in cx@26.5.1. The date and time wheels render the option list three times and recentre on the nearest copy, so wraps are seamless with no hard list edges. Only the cycling feature is ported: DateTimePicker.tsx is master plus #1280, and Wheel.tsx is master plus the `cycle` prop. master's existing wheel-width fix (Wheel.scss) is kept untouched — the litmus-ts branch predates it, so its Wheel.tsx/Wheel.scss were not used. --- .../cx/src/widgets/form/DateTimePicker.tsx | 845 ++++++++++-------- packages/cx/src/widgets/form/Wheel.tsx | 43 +- 2 files changed, 489 insertions(+), 399 deletions(-) diff --git a/packages/cx/src/widgets/form/DateTimePicker.tsx b/packages/cx/src/widgets/form/DateTimePicker.tsx index c0eacc8ab..71ebee537 100644 --- a/packages/cx/src/widgets/form/DateTimePicker.tsx +++ b/packages/cx/src/widgets/form/DateTimePicker.tsx @@ -12,32 +12,38 @@ import { WheelComponent } from "./Wheel"; enableCultureSensitiveFormatting(); export class DateTimePicker extends Widget { - declare public size: number; - declare public segment: string; - declare public autoFocus?: boolean; - declare public showSeconds?: boolean; - declare public encoding?: (date: Date) => string; - declare public onFocusOut?: string | ((instance: Instance) => void); - declare public onSelect?: string | ((e: React.KeyboardEvent, instance: Instance, date: Date) => void); - declare baseClass: string; - - declareData(...args: Record[]): void { - return super.declareData(...args, { - value: undefined, - }); - } - - render(context: RenderingContext, instance: Instance, key: string): React.ReactNode { - return ( - - ); - } + declare public size: number; + declare public segment: string; + declare public autoFocus?: boolean; + declare public showSeconds?: boolean; + declare public encoding?: (date: Date) => string; + declare public onFocusOut?: string | ((instance: Instance) => void); + declare public onSelect?: + | string + | ((e: React.KeyboardEvent, instance: Instance, date: Date) => void); + declare baseClass: string; + + declareData(...args: Record[]): void { + return super.declareData(...args, { + value: undefined, + }); + } + + render( + context: RenderingContext, + instance: Instance, + key: string, + ): React.ReactNode { + return ( + + ); + } } DateTimePicker.prototype.baseClass = "datetimepicker"; @@ -47,383 +53,438 @@ DateTimePicker.prototype.autoFocus = false; DateTimePicker.prototype.segment = "datetime"; DateTimePicker.prototype.showSeconds = false; +// Builds the option spans for a numeric wheel — one zero-padded span per value. +// Pass the result to a WheelComponent with `cycle` set to make it scroll +// endlessly; centre the current value by passing `index = value + range`. +function buildNumberWheel(range: number, startAt: number): React.ReactNode[] { + return Array.from({ length: range }, (_, j) => ( + {String(j + startAt).padStart(2, "0")} + )); +} + interface DateTimePickerComponentProps { - instance: Instance; - data: Record; - size: number; - segment: string; + instance: Instance; + data: Record; + size: number; + segment: string; } interface DateTimePickerComponentState { - date: Date; - activeWheel: string | null; + date: Date; + activeWheel: string | null; } -class DateTimePickerComponent extends VDOM.Component { - el!: HTMLDivElement; - declare wheels: Record; - keyDownPipes: Record void>; - - constructor(props: DateTimePickerComponentProps) { - super(props); - let date = props.data.value ? parseDateInvariant(props.data.value as string | number | Date) : new Date(); - if (isNaN(date.getTime())) date = new Date(); - this.state = { - date: date, - activeWheel: null, - }; - - let { widget } = props.instance; - let pickerWidget = widget as DateTimePicker; - - this.handleChange = this.handleChange.bind(this); - this.onFocus = this.onFocus.bind(this); - this.onBlur = this.onBlur.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); - - let showDate = props.segment.indexOf("date") !== -1; - let showTime = props.segment.indexOf("time") !== -1; - - this.wheels = { - year: showDate, - month: showDate, - date: showDate, - hours: showTime, - minutes: showTime, - seconds: showTime && !!pickerWidget.showSeconds, - }; - - this.keyDownPipes = {}; - } - - UNSAFE_componentWillReceiveProps(props: DateTimePickerComponentProps): void { - let date = props.data.value ? parseDateInvariant(props.data.value as string | number | Date) : new Date(); - if (isNaN(date.getTime())) date = new Date(); - this.setState({ date }); - } - - setDateComponent(date: Date, component: string, value: number): Date { - let v = new Date(date); - switch (component) { - case "year": - v.setFullYear(value); - break; - - case "month": - v.setMonth(value); - break; - - case "date": - v.setDate(value); - break; - - case "hours": - v.setHours(value); - break; - - case "minutes": - v.setMinutes(value); - break; - - case "seconds": - v.setSeconds(value); - break; - } - return v; - } - - handleChange(): void { - let { widget } = this.props.instance; - let pickerWidget = widget as DateTimePicker; - let encode = pickerWidget.encoding || Culture.getDefaultDateEncoding(); - this.props.instance.set("value", encode!(this.state.date)); - } - - render(): React.ReactNode { - let { instance, data, size } = this.props; - let { widget } = instance; - let { CSS, baseClass } = widget; - let pickerWidget = widget as DateTimePicker; - let date = this.state.date; - - let culture = Culture.getDateTimeCulture(); - let monthNames = culture.getMonthNames("short"); - - let years = []; - for (let y = 1970; y <= 2050; y++) years.push({y}); - - let days = []; - let start = new Date(date.getFullYear(), date.getMonth(), 1); - while (start.getMonth() === date.getMonth()) { - let day = start.getDate(); - days.push({day < 10 ? "0" + day : day}); - start.setDate(start.getDate() + 1); - } - - let hours = []; - for (let h = 0; h < 24; h++) { - hours.push({h < 10 ? "0" + h : h}); - } - - let minutes = []; - for (let m = 0; m < 60; m++) { - minutes.push({m < 10 ? "0" + m : m}); - } - - return ( -
{ - this.el = el!; +class DateTimePickerComponent extends VDOM.Component< + DateTimePickerComponentProps, + DateTimePickerComponentState +> { + el!: HTMLDivElement; + declare wheels: Record; + keyDownPipes: Record void>; + declare years: any[]; + declare days: any[]; + declare hours: any[]; + declare minutes: any[]; + declare century: number; + declare firstYear: number; + declare numberOfDaysInMonth: number; + + constructor(props: DateTimePickerComponentProps) { + super(props); + let date = props.data.value + ? parseDateInvariant(props.data.value as string | number | Date) + : new Date(); + if (isNaN(date.getTime())) date = new Date(); + this.state = { + date: date, + activeWheel: null, + }; + this.century = (date.getFullYear() / 100) | 0; + + let { widget } = props.instance; + let pickerWidget = widget as DateTimePicker; + + this.handleChange = this.handleChange.bind(this); + this.onFocus = this.onFocus.bind(this); + this.onBlur = this.onBlur.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + + let showDate = props.segment.indexOf("date") !== -1; + let showTime = props.segment.indexOf("time") !== -1; + + this.wheels = { + year: showDate, + month: showDate, + date: showDate, + hours: showTime, + minutes: showTime, + seconds: showTime && !!pickerWidget.showSeconds, + }; + + this.keyDownPipes = {}; + } + + UNSAFE_componentWillReceiveProps(props: DateTimePickerComponentProps): void { + let date = props.data.value + ? parseDateInvariant(props.data.value as string | number | Date) + : new Date(); + if (isNaN(date.getTime())) date = new Date(); + this.setState({ date }); + } + + setDateComponent(date: Date, component: string, value: number): Date { + let v = new Date(date); + switch (component) { + case "year": + v.setFullYear(value); + break; + + case "month": + v.setMonth(value); + break; + + case "date": + v.setDate(value); + break; + + case "hours": + v.setHours(value); + break; + + case "minutes": + v.setMinutes(value); + break; + + case "seconds": + v.setSeconds(value); + break; + } + return v; + } + + handleChange(): void { + let { widget } = this.props.instance; + let pickerWidget = widget as DateTimePicker; + let encode = pickerWidget.encoding || Culture.getDefaultDateEncoding(); + this.props.instance.set("value", encode!(this.state.date)); + } + + render(): React.ReactNode { + let { instance, data, size } = this.props; + let { widget } = instance; + let { CSS, baseClass } = widget; + let pickerWidget = widget as DateTimePicker; + let date = this.state.date; + + let culture = Culture.getDateTimeCulture(); + let monthNames = culture.getMonthNames("short"); + + // Years: a window spanning the current century, rebuilt when it changes. + let currentCentury = (date.getFullYear() / 100) | 0; + if (!this.years || this.century !== currentCentury) { + this.century = currentCentury; + this.firstYear = currentCentury * 100 - 3; + let lastYear = (currentCentury + 1) * 100 + 5; + this.years = []; + for (let y = this.firstYear; y <= lastYear; y++) + this.years.push({y}); + } + let years = this.years; + + // Day/hour/minute wheels use a 3x buffer (see buildNumberWheel). The day + // buffer depends on the month length, so it is rebuilt when that changes. + const numberOfDaysInMonth = new Date( + date.getFullYear(), + date.getMonth() + 1, + 0, + ).getDate(); + if (!this.days || this.numberOfDaysInMonth !== numberOfDaysInMonth) { + this.numberOfDaysInMonth = numberOfDaysInMonth; + this.days = buildNumberWheel(numberOfDaysInMonth, 1); + } + let days = this.days; + + if (!this.hours) this.hours = buildNumberWheel(24, 0); + let hours = this.hours; + + if (!this.minutes) this.minutes = buildNumberWheel(60, 0); + let minutes = this.minutes; + + return ( +
{ + this.el = el!; + }} + className={data.classNames as string} + onFocus={this.onFocus} + onBlur={this.onBlur} + onKeyDown={this.onKeyDown} + > + {this.wheels.year && ( + { + this.setState( + (state) => ({ + date: this.setDateComponent( + state.date, + "year", + newIndex + this.firstYear, + ), + }), + this.handleChange, + ); }} - className={data.classNames as string} - onFocus={this.onFocus} - onBlur={this.onBlur} - onKeyDown={this.onKeyDown} - > - {this.wheels.year && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "year", newIndex + 1970), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["year"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "year" }); - }} - > - {years} - - )} - {this.wheels.year && this.wheels.month && -} - {this.wheels.month && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "month", newIndex), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["month"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "month" }); - }} - > - {monthNames.map((m: string, i: number) => ( - {m} - ))} - - )} - {this.wheels.month && this.wheels.date && -} - {this.wheels.date && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "date", newIndex + 1), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["date"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "date" }); - }} - > - {days} - - )} - {this.wheels.hours && this.wheels.year && } - {this.wheels.hours && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "hours", newIndex), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["hours"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "hours" }); - }} - > - {hours} - - )} - {this.wheels.hours && this.wheels.minutes && :} - {this.wheels.minutes && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "minutes", newIndex), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["minutes"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "minutes" }); - }} - > - {minutes} - - )} - {this.wheels.minutes && this.wheels.seconds && :} - {this.wheels.seconds && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "seconds", newIndex), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["seconds"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "seconds" }); - }} - > - {minutes} - - )} -
- ); - } - - componentDidMount(): void { - let { widget } = this.props.instance; - let pickerWidget = widget as DateTimePicker; - if (pickerWidget.autoFocus) this.el.focus(); - } - - componentWillUnmount(): void { - offFocusOut(this); - } - - onFocus(): void { - oneFocusOut(this, this.el, this.onFocusOut.bind(this)); - - if (!this.state.activeWheel) { - let firstWheel: string | null = null; - for (let wheel in this.wheels) { - if (this.wheels[wheel]) { - firstWheel = wheel; - break; - } - } - - this.setState({ - activeWheel: firstWheel, - }); + onPipeKeyDown={(kd) => { + this.keyDownPipes["year"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "year" }); + }} + > + {years} + + )} + {this.wheels.year && this.wheels.month && -} + {this.wheels.month && ( + { + this.setState( + (state) => ({ + date: this.setDateComponent(state.date, "month", newIndex), + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["month"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "month" }); + }} + > + {monthNames.map((m: string, i: number) => ( + {m} + ))} + + )} + {this.wheels.month && this.wheels.date && -} + {this.wheels.date && ( + { + let day = rawIndex % this.numberOfDaysInMonth; + this.setState( + (state) => ({ + date: this.setDateComponent(state.date, "date", day + 1), + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["date"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "date" }); + }} + > + {days} + + )} + {this.wheels.hours && this.wheels.year && ( + + )} + {this.wheels.hours && ( + { + let hour = rawIndex % 24; + this.setState( + (state) => ({ + date: this.setDateComponent(state.date, "hours", hour), + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["hours"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "hours" }); + }} + > + {hours} + + )} + {this.wheels.hours && this.wheels.minutes && :} + {this.wheels.minutes && ( + { + let minute = rawIndex % 60; + this.setState( + (state) => ({ + date: this.setDateComponent(state.date, "minutes", minute), + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["minutes"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "minutes" }); + }} + > + {minutes} + + )} + {this.wheels.minutes && this.wheels.seconds && :} + {this.wheels.seconds && ( + { + let second = rawIndex % 60; + this.setState( + (state) => ({ + date: this.setDateComponent(state.date, "seconds", second), + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["seconds"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "seconds" }); + }} + > + {minutes} + + )} +
+ ); + } + + componentDidMount(): void { + let { widget } = this.props.instance; + let pickerWidget = widget as DateTimePicker; + if (pickerWidget.autoFocus) this.el.focus(); + } + + componentWillUnmount(): void { + offFocusOut(this); + } + + onFocus(): void { + oneFocusOut(this, this.el, this.onFocusOut.bind(this)); + + if (!this.state.activeWheel) { + let firstWheel: string | null = null; + for (let wheel in this.wheels) { + if (this.wheels[wheel]) { + firstWheel = wheel; + break; + } } - } - onFocusOut(): void { - let { instance } = this.props; - let { widget } = instance; - let pickerWidget = widget as DateTimePicker; - if (pickerWidget.onFocusOut) instance.invoke("onFocusOut", null, instance); - } - - onBlur(): void { this.setState({ - activeWheel: null, + activeWheel: firstWheel, }); - } - - onKeyDown(e: React.KeyboardEvent): void { - let tmp: string | null = null; - let { instance } = this.props; - let { widget } = instance; - let pickerWidget = widget as DateTimePicker; - - switch (e.keyCode) { - case KeyCode.right: - e.preventDefault(); - for (let wheel in this.wheels) { - if (this.wheels[wheel]) { - if (tmp === this.state.activeWheel) { - this.setState({ activeWheel: wheel }); - break; - } - tmp = wheel; - } + } + } + + onFocusOut(): void { + let { instance } = this.props; + let { widget } = instance; + let pickerWidget = widget as DateTimePicker; + if (pickerWidget.onFocusOut) instance.invoke("onFocusOut", null, instance); + } + + onBlur(): void { + this.setState({ + activeWheel: null, + }); + } + + onKeyDown(e: React.KeyboardEvent): void { + let tmp: string | null = null; + let { instance } = this.props; + let { widget } = instance; + let pickerWidget = widget as DateTimePicker; + + switch (e.keyCode) { + case KeyCode.right: + e.preventDefault(); + for (let wheel in this.wheels) { + if (this.wheels[wheel]) { + if (tmp === this.state.activeWheel) { + this.setState({ activeWheel: wheel }); + break; } - break; - - case KeyCode.left: - e.preventDefault(); - for (let wheel in this.wheels) { - if (this.wheels[wheel]) { - if (wheel === this.state.activeWheel && tmp) { - this.setState({ activeWheel: tmp }); - break; - } - tmp = wheel; - } + tmp = wheel; + } + } + break; + + case KeyCode.left: + e.preventDefault(); + for (let wheel in this.wheels) { + if (this.wheels[wheel]) { + if (wheel === this.state.activeWheel && tmp) { + this.setState({ activeWheel: tmp }); + break; } - break; - - case KeyCode.enter: - e.preventDefault(); - if (pickerWidget.onSelect) instance.invoke("onSelect", e, instance, this.state.date); - break; - - default: let kdp = this.keyDownPipes[this.state.activeWheel!]; - if (kdp) kdp(e); - break; - } - } + tmp = wheel; + } + } + break; + + case KeyCode.enter: + e.preventDefault(); + if (pickerWidget.onSelect) + instance.invoke("onSelect", e, instance, this.state.date); + break; + + default: + let kdp = this.keyDownPipes[this.state.activeWheel!]; + if (kdp) kdp(e); + break; + } + } } diff --git a/packages/cx/src/widgets/form/Wheel.tsx b/packages/cx/src/widgets/form/Wheel.tsx index 789535b5f..0141aaaa8 100644 --- a/packages/cx/src/widgets/form/Wheel.tsx +++ b/packages/cx/src/widgets/form/Wheel.tsx @@ -65,6 +65,10 @@ Wheel.prototype.baseClass = "wheel"; Wheel.prototype.size = 3; Wheel.prototype.styled = true; +/** A cyclic wheel renders its option list this many times, leaving scroll + * headroom on both sides of the centre copy. */ +export const WHEEL_BUFFER_COPIES = 3; + export interface WheelComponentProps { size: number; children: React.ReactNode[]; @@ -74,6 +78,11 @@ export interface WheelComponentProps { className?: string; style?: React.CSSProperties; index?: number; + /** Set to render the option list as an endlessly scrolling wheel. The list + * is rendered `WHEEL_BUFFER_COPIES` times and the wheel snaps to whichever + * copy of the selected value is nearest its current position, so a wrap + * scrolls seamlessly into the adjacent identical copy. */ + cycle?: boolean; onChange: (newIndex: number) => void; onPipeKeyDown?: (fn: (e: React.KeyboardEvent) => void) => void; onMouseDown?: () => void; @@ -109,23 +118,30 @@ export class WheelComponent extends VDOM.Component void; render(): React.ReactNode { - let { size, children, CSS, baseClass, active, className, style, onMouseDown } = this.props; + let { size, children, CSS, baseClass, active, className, style, onMouseDown, cycle } = this.props; let optionClass = CSS.element(baseClass, "option"); let dummyClass = CSS.element(baseClass, "option", { dummy: true }); + // A cyclic wheel repeats the option list so it can scroll past either end. + let options = children; + if (cycle) { + options = []; + for (let i = 0; i < WHEEL_BUFFER_COPIES; i++) options.push(...children); + } + let tpad = [], bpad = [], padSize = 0; for (let i = 0; i < (size - 1) / 2; i++) { - tpad.push({ key: -1 - i, child: children[0], cls: dummyClass }); - bpad.push({ key: -100 - i, child: children[0], cls: dummyClass }); + tpad.push({ key: -1 - i, child: options[0], cls: dummyClass }); + bpad.push({ key: -100 - i, child: options[0], cls: dummyClass }); padSize++; } let displayedOptions = [ ...tpad, - ...children.map((c, i) => ({ + ...options.map((c, i) => ({ key: i, child: c, cls: optionClass, @@ -228,7 +244,19 @@ export class WheelComponent extends VDOM.Component Date: Tue, 19 May 2026 14:59:46 +0200 Subject: [PATCH 2/3] Add cx@26.5.1 changelog --- homedocs/src/pages/changelog.mdx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/homedocs/src/pages/changelog.mdx b/homedocs/src/pages/changelog.mdx index a3f98d097..ee6da0477 100644 --- a/homedocs/src/pages/changelog.mdx +++ b/homedocs/src/pages/changelog.mdx @@ -4,6 +4,24 @@ title: Changelog description: Version history and release notes --- +## cx\@26.5.1 + +**Features** + +- Added a `daybefore` date format that renders a date shifted back by one calendar day, using the same pattern argument as `datetime` — handy for displaying the exclusive end of a date range as an inclusive value. Also added the `dayBefore` date utility ([#1290](https://github.com/codaxy/cxjs/issues/1290)) +- Added a `quarter` date format that renders the calendar quarter of a date from a string-template pattern (`{q}`, `{yyyy}`, `{yy}`), with an `exclusive` flag for displaying the end of a half-open range. Also added the `dateQuarter` date utility +- Added a `hideClippedLabels` option to chart axes that drops a first/last label, and its tick, when it would be clipped at the chart edge — enabled by default on `TimeAxis` ([#1291](https://github.com/codaxy/cxjs/issues/1291)) +- `DateTimePicker` date and time wheels now scroll endlessly, so picking values across large ranges no longer hits hard list boundaries + +**Fixes** + +- `Grid`'s `onCreateFilter` callback may now return `null` to apply no filter +- Fixed the `ValidationGroup` `visited` flag not propagating to nested fields when a field's own bound data was unchanged ([#1276](https://github.com/codaxy/cxjs/issues/1276)) +- Added the missing SCSS import for the `RangeMarker` chart component — its styles were not being generated +- Added missing `@use` declarations in `RangeMarker.scss` for `besm`, `include`, and `variables` + +--- + ## cx\@26.4.4 **Fixes** From ed490ca8924fc5f486f18b275f4a7cac077b6d38 Mon Sep 17 00:00:00 2001 From: Marko Stijak Date: Tue, 19 May 2026 15:01:02 +0200 Subject: [PATCH 3/3] Bump cx version to 26.5.1 --- packages/cx/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cx/package.json b/packages/cx/package.json index ba3fa9f8a..dcb7068ab 100644 --- a/packages/cx/package.json +++ b/packages/cx/package.json @@ -1,6 +1,6 @@ { "name": "cx", - "version": "26.4.4", + "version": "26.5.1", "description": "Advanced JavaScript UI framework for admin and dashboard applications with ready to use grid, form and chart components.", "exports": { "./data": {