x4js 2.3.28 → 2.3.30

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.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/aicontext.md +385 -21
  3. package/package.json +8 -3
  4. package/src/components/assets/icons.ts +2 -0
  5. package/src/components/bargraph/bargraph.module.scss +44 -0
  6. package/src/components/bargraph/bargraph.ts +362 -0
  7. package/src/components/base.scss +71 -1
  8. package/src/components/calendar/calendar.module.scss +6 -5
  9. package/src/components/calendar/calendar.ts +14 -8
  10. package/src/components/canvas/canvas.ts +5 -4
  11. package/src/components/carousel/carousel.module.scss +165 -0
  12. package/src/components/carousel/carousel.ts +423 -0
  13. package/src/components/chart/chart.module.scss +8 -0
  14. package/src/components/chart/chart.ts +170 -12
  15. package/src/components/combobox/combobox.module.scss +2 -1
  16. package/src/components/components.ts +3 -0
  17. package/src/components/dialog/dialog.module.scss +2 -0
  18. package/src/components/dialog/dialog.ts +1 -0
  19. package/src/components/gantt/gantt.module.scss +462 -0
  20. package/src/components/gantt/gantt.ts +1254 -0
  21. package/src/components/knob/knob.module.scss +113 -0
  22. package/src/components/knob/knob.ts +355 -0
  23. package/src/components/mapview/mapview.module.scss +177 -0
  24. package/src/components/mapview/mapview.ts +764 -0
  25. package/src/components/menu/menu.module.scss +4 -6
  26. package/src/components/menu/menu.ts +4 -2
  27. package/src/components/planner/planner.module.scss +688 -0
  28. package/src/components/planner/planner.ts +1792 -0
  29. package/src/components/popup/popup.ts +29 -3
  30. package/src/components/radio/radio.module.scss +4 -1
  31. package/src/components/shared.scss +2 -1
  32. package/src/components/slider/slider.module.scss +22 -20
  33. package/src/components/switch/switch.module.scss +68 -44
  34. package/src/components/switch/switch.ts +6 -3
  35. package/src/components/tag/tag.module.scss +4 -4
  36. package/src/components/textedit/textedit.module.scss +5 -2
  37. package/src/components/themes.scss +27 -6
  38. package/src/components/tooltips/tooltips.scss +8 -0
  39. package/src/components/tooltips/tooltips.ts +31 -0
  40. package/src/core/core_i18n.ts +65 -0
  41. package/src/core/core_tools.ts +44 -0
  42. package/src/x4doc.ts +3 -0
  43. package/src/components/menu/caret-right-solid.svg +0 -1
@@ -0,0 +1,165 @@
1
+ /**
2
+ * ___ ___ __
3
+ * \ \/ / / _
4
+ * \ / /_| |_
5
+ * / \____ _|
6
+ * /__/\__\ |_|
7
+ *
8
+ * @file carousel.module.scss
9
+ * @author Etienne Cochard
10
+ *
11
+ * @copyright (c) 2026 R-libre ingenierie
12
+ *
13
+ * Use of this source code is governed by an MIT-style license
14
+ * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
+ **/
16
+
17
+ :root {
18
+ --carousel-gap: 16px;
19
+ --carousel-duration: 0.4s;
20
+ --carousel-arrow-background: color-mix( in srgb, var( --background-primary ) 80%, transparent );
21
+ --carousel-arrow-color: var( --text-primary );
22
+ --carousel-dot: var( --border-hover );
23
+ --carousel-dot-active: var( --accent-background );
24
+ --carousel-radius: 8px;
25
+ --carousel-caption-color: #ffffff;
26
+ --carousel-caption-background: rgba( 0, 0, 0, 0.6 );
27
+ --carousel-disabled-filter: grayscale( 1 );
28
+ }
29
+
30
+ // the code sets --carousel-index (the first slide seen), --carousel-per-view, --carousel-drag (the swipe, px)
31
+ .x4carousel {
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: 8px;
35
+ outline: none;
36
+
37
+ // the width of a slide: the view less the gaps between the slides seen, shared
38
+ --carousel-slide: calc( ( 100% - ( var( --carousel-per-view, 1 ) - 1 ) * var( --carousel-gap ) ) / var( --carousel-per-view, 1 ) );
39
+
40
+ // disabled: inert (enable), faded as a Box, its images grey
41
+ &[disabled] {
42
+ opacity: var( --box-disabled-opacity );
43
+ filter: var( --carousel-disabled-filter );
44
+
45
+ // a disabled Box in it: faded once, not twice
46
+ .x4box[disabled] {
47
+ opacity: 1;
48
+ }
49
+ }
50
+
51
+ &:focus-visible .viewport {
52
+ outline: 1px solid var( --border-focus );
53
+ outline-offset: 2px;
54
+ }
55
+
56
+ .viewport {
57
+ position: relative;
58
+ overflow: hidden;
59
+ flex: 1;
60
+ min-height: 0;
61
+ touch-action: pan-y; // across: the page scrolls; along: the swipe
62
+ }
63
+
64
+ // percents of a translate are the width of the track: the one of the view
65
+ .track {
66
+ display: flex;
67
+ gap: var( --carousel-gap );
68
+ height: 100%;
69
+ transform: translateX( calc( var( --carousel-index, 0 ) * -1 * ( var( --carousel-slide ) + var( --carousel-gap ) ) + var( --carousel-drag, 0px ) ) );
70
+ transition: transform var( --carousel-duration ) ease;
71
+ }
72
+
73
+ // the slide follows the finger
74
+ &.dragging {
75
+ cursor: grabbing;
76
+
77
+ .track {
78
+ transition: none;
79
+ }
80
+ }
81
+
82
+ .slide {
83
+ flex: 0 0 var( --carousel-slide );
84
+ min-width: 0;
85
+ overflow: hidden;
86
+ }
87
+
88
+ // an image: all the slide, its caption over its bottom
89
+ .slide.image {
90
+ position: relative;
91
+ border-radius: var( --carousel-radius );
92
+
93
+ .x4image {
94
+ width: 100%;
95
+ height: 100%;
96
+ background-color: var( --background-ternary ); // while it loads
97
+ }
98
+
99
+ .caption {
100
+ position: absolute;
101
+ left: 0;
102
+ right: 0;
103
+ bottom: 0;
104
+ padding: 24px 16px 12px;
105
+ color: var( --carousel-caption-color );
106
+ background: linear-gradient( transparent, var( --carousel-caption-background ) );
107
+ }
108
+ }
109
+
110
+ .arrow {
111
+ position: absolute;
112
+ top: 50%;
113
+ transform: translateY( -50% );
114
+ z-index: 1;
115
+ width: 36px;
116
+ min-width: 36px;
117
+ height: 36px;
118
+ padding: 0;
119
+ border-radius: 50%;
120
+ justify-content: center;
121
+
122
+ --button-background: var( --carousel-arrow-background );
123
+ --button-background-hover: var( --carousel-arrow-background );
124
+ --button-background-active: var( --carousel-arrow-background );
125
+ --button-background-focus: var( --carousel-arrow-background );
126
+ --button-icon-color: var( --carousel-arrow-color );
127
+ --button-background-disabled: transparent;
128
+
129
+ box-shadow: 0 1px 4px rgba( 0, 0, 0, 0.25 );
130
+
131
+ &.prev {
132
+ left: 8px;
133
+ }
134
+
135
+ &.next {
136
+ right: 8px;
137
+ }
138
+
139
+ // at an end: not shown, its place kept
140
+ &[disabled] {
141
+ opacity: 0;
142
+ }
143
+ }
144
+
145
+ .dots {
146
+ justify-content: center;
147
+ gap: 8px;
148
+ }
149
+
150
+ .dot {
151
+ width: 8px;
152
+ height: 8px;
153
+ padding: 0;
154
+ border: none;
155
+ border-radius: 4px;
156
+ background-color: var( --carousel-dot );
157
+ cursor: pointer;
158
+ transition: width var( --carousel-duration ), background-color var( --carousel-duration );
159
+
160
+ &.selected {
161
+ width: 20px;
162
+ background-color: var( --carousel-dot-active );
163
+ }
164
+ }
165
+ }
@@ -0,0 +1,423 @@
1
+ /**
2
+ * ___ ___ __
3
+ * \ \/ / / _
4
+ * \ / /_| |_
5
+ * / \____ _|
6
+ * /__/\__\ |_|
7
+ *
8
+ * @file carousel.ts
9
+ * @author Etienne Cochard
10
+ *
11
+ * @copyright (c) 2026 R-libre ingenierie
12
+ *
13
+ * Use of this source code is governed by an MIT-style license
14
+ * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
+ **/
16
+
17
+ import { EventCallback } from '../../core/core_events';
18
+ import { Component, ComponentEvents, ComponentProps, EvChange } from '../../core/component';
19
+ import { _tr } from '../../core/core_i18n';
20
+ import { clamp, sprintf, UnsafeHtml } from '../../core/core_tools';
21
+
22
+ import { HBox } from '../boxes/boxes';
23
+ import { Button } from '../button/button';
24
+ import { Image, ImageProps } from '../image/image';
25
+
26
+ import './carousel.module.scss';
27
+
28
+ export interface CarouselEvents extends ComponentEvents {
29
+ change: EvChange;
30
+ }
31
+
32
+ /** a slide that is an image: loaded when it comes near the view */
33
+ export interface CarouselImage {
34
+ src: string;
35
+ alt?: string;
36
+ /** over the bottom of the image */
37
+ caption?: string | UnsafeHtml;
38
+ /** cover by default: the image fills the slide, cut; contain: all of it, with margins */
39
+ fit?: "cover" | "contain";
40
+ }
41
+
42
+ export type CarouselItem = Component | CarouselImage;
43
+
44
+ export interface CarouselProps extends Omit<ComponentProps,"content"> {
45
+ /** the slides: components, or images */
46
+ items?: CarouselItem[];
47
+ /** the slides seen side by side, 1 by default; a move goes one slide further */
48
+ perView?: number;
49
+ /** the slide at the start (its index), 0 by default */
50
+ index?: number;
51
+ /** after the last slide comes the first one, and before the first the last */
52
+ loop?: boolean;
53
+ /** ms between two slides: it goes by itself (and loops), paused while the pointer is over it or the focus in it */
54
+ autoplay?: number;
55
+ /** the buttons previous and next on its sides; true by default */
56
+ arrows?: boolean;
57
+ /** a dot for each place, under the slides; true by default */
58
+ dots?: boolean;
59
+ /** the user (arrows, dots, swipe, keys) or the autoplay went to another slide: value is its index; not by the code */
60
+ change?: EventCallback<EvChange>;
61
+ }
62
+
63
+ const icon_prev = `<svg viewBox="0 0 24 24"><path d="M15.4 5.4L14 4l-8 8 8 8 1.4-1.4L8.8 12z"/></svg>`;
64
+ const icon_next = `<svg viewBox="0 0 24 24"><path d="M8.6 5.4L10 4l8 8-8 8-1.4-1.4 6.6-6.6z"/></svg>`;
65
+
66
+ /**
67
+ * slides side by side, one place at a time (or perView): arrows, dots, a swipe with the finger or
68
+ * the mouse, the arrow keys, and an autoplay. A slide is a component, or an image.
69
+ *
70
+ * @example
71
+ * new Carousel( {
72
+ * height: 300, autoplay: 5000,
73
+ * items: photos.map( p => ( { src: p.url, alt: p.title, caption: p.title } ) ),
74
+ * } )
75
+ *
76
+ * @cssvar
77
+ * ```
78
+ * --carousel-gap
79
+ * --carousel-duration
80
+ * --carousel-arrow-background
81
+ * --carousel-arrow-color
82
+ * --carousel-dot
83
+ * --carousel-dot-active
84
+ * --carousel-radius
85
+ * --carousel-caption-color
86
+ * --carousel-caption-background
87
+ * --carousel-disabled-filter
88
+ * ```
89
+ */
90
+
91
+ export class Carousel extends Component<CarouselProps,CarouselEvents> {
92
+ static classPrefix = "x4";
93
+
94
+ private _index = 0;
95
+ private _slides: Component[] = [];
96
+ private _images = new Map<number, { image: Image, src: string }>( ); // slides that are images, not loaded yet
97
+
98
+ private _viewport: Component;
99
+ private _track: Component;
100
+ private _prev: Button;
101
+ private _next: Button;
102
+ private _dots: HBox;
103
+
104
+ // swipe: where it started; dragging once it moved more than a click
105
+ private _swipe: { id: number, x: number, y: number, dragging: boolean } = null;
106
+
107
+ // autoplay: paused while the pointer is over it, the focus in it, or a swipe
108
+ private _hover = false;
109
+ private _focus = false;
110
+
111
+ constructor( props: CarouselProps ) {
112
+ super( props );
113
+
114
+ this.mapPropEvents( props, "change" );
115
+
116
+ this.setContent( [
117
+ this._viewport = new Component( { cls: "viewport", content: [
118
+ this._track = new Component( { cls: "track" } ),
119
+ this._prev = new Button( { cls: "arrow prev", icon: icon_prev, tabindex: false, attrs: { "aria-label": _tr.global.carousel.prev }, click: ( ) => this._go( this._index-1 ) } ),
120
+ this._next = new Button( { cls: "arrow next", icon: icon_next, tabindex: false, attrs: { "aria-label": _tr.global.carousel.next }, click: ( ) => this._go( this._index+1 ) } ),
121
+ ] } ),
122
+ this._dots = new HBox( { cls: "dots" } ),
123
+ ] );
124
+
125
+ this.setAttribute( "tabindex", 0 );
126
+ this.setAttribute( "role", "region" );
127
+ this.setAria( "aria-roledescription", _tr.global.carousel.carousel );
128
+
129
+ this._viewport.setDOMEvents( {
130
+ pointerdown: ( ev ) => this._on_pointerdown( ev ),
131
+ pointermove: ( ev ) => this._on_pointermove( ev ),
132
+ pointerup: ( ev ) => this._on_pointerup( ev ),
133
+ pointercancel: ( ev ) => this._on_pointerup( ev ),
134
+ // an image dragged by the browser would take the swipe
135
+ dragstart: ( ev ) => ev.preventDefault( ),
136
+ } );
137
+
138
+ this.setDOMEvents( {
139
+ keydown: ( ev ) => this._on_key( ev ),
140
+ pointerenter: ( ) => { this._hover = true; },
141
+ pointerleave: ( ) => { this._hover = false; },
142
+ focusin: ( ) => { this._focus = true; },
143
+ focusout: ( ) => { this._focus = false; },
144
+ // the timers of a component are not stopped when it leaves the page
145
+ created: ( ) => this._autoplay( ),
146
+ removed: ( ) => this.clearTimeouts( ),
147
+ } );
148
+
149
+ this.setItems( props.items ?? [], props.index ?? 0 );
150
+ }
151
+
152
+ private _perView( ) { return Math.max( 1, Math.round( this.props.perView ?? 1 ) ); }
153
+
154
+ // the last place: the last slides fill the view
155
+ private _maxIndex( ) { return Math.max( 0, this._slides.length - this._perView( ) ); }
156
+
157
+ private _loops( ) { return !!this.props.loop || !!this.props.autoplay; }
158
+
159
+ // :: places ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
160
+
161
+ // beyond the ends, the other end when it loops; notify: the user (arrows, dots, swipe, keys), not the code
162
+ private _go( index: number, notify = true ) {
163
+ const max = this._maxIndex( );
164
+
165
+ if( this._loops( ) ) {
166
+ index = index>max ? 0 : ( index<0 ? max : index );
167
+ }
168
+
169
+ // a move of the user starts the time of the autoplay again
170
+ this._autoplay( );
171
+
172
+ if( this._show( index ) && notify ) {
173
+ this.fire( "change", { value: this._index } );
174
+ }
175
+ }
176
+
177
+ // true when the place changed
178
+ private _show( index: number ) {
179
+ index = clamp( index, 0, this._maxIndex( ) );
180
+ const changed = index!=this._index;
181
+
182
+ this._index = index;
183
+ this.setStyleVariable( "--carousel-index", index+"" );
184
+
185
+ // the images of the slides seen, and of their neighbours: ready for the next move
186
+ const pv = this._perView( );
187
+ this._load( index-1, index+pv );
188
+
189
+ // the slides out of the view: no focus, no reader
190
+ this._slides.forEach( ( s, i ) => {
191
+ const seen = i>=index && i<index+pv;
192
+ s.setAttribute( "inert", !seen );
193
+ s.setAria( "aria-hidden", !seen );
194
+ } );
195
+
196
+ this._dots.enumChildComponents( false ).forEach( ( d, i ) => {
197
+ d.setClass( "selected", i==index );
198
+ d.setAria( "aria-current", i==index );
199
+ } );
200
+
201
+ const loops = this._loops( );
202
+ this._prev.enable( loops || index>0 );
203
+ this._next.enable( loops || index<this._maxIndex( ) );
204
+
205
+ return changed;
206
+ }
207
+
208
+ private _load( from: number, to: number ) {
209
+ const n = this._slides.length;
210
+ if( !n ) {
211
+ return;
212
+ }
213
+
214
+ for( let i=from; i<=to; i++ ) {
215
+ // the neighbours of the ends, when it loops
216
+ const k = ( ( i % n ) + n ) % n;
217
+ const img = this._images.get( k );
218
+ if( img ) {
219
+ img.image.setImage( img.src );
220
+ this._images.delete( k );
221
+ }
222
+ }
223
+ }
224
+
225
+ private _buildSlide( item: CarouselItem ) {
226
+ if( item instanceof Component ) {
227
+ return item;
228
+ }
229
+
230
+ // src given when it comes near the view (_load)
231
+ const image = new Image<ImageProps>( { src: "", alt: item.alt, fit: item.fit ?? "cover" } );
232
+ const content: Component[] = [ image ];
233
+
234
+ if( item.caption ) {
235
+ content.push( new Component( { cls: "caption", content: item.caption } ) );
236
+ }
237
+
238
+ return { image, src: item.src, content };
239
+ }
240
+
241
+ // :: autoplay ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
242
+
243
+ private _autoplay( ) {
244
+ const ms = this.props.autoplay;
245
+ if( !ms ) {
246
+ return;
247
+ }
248
+
249
+ // a named timer: starting it again replaces it
250
+ this.setInterval( "autoplay", ms, ( ) => {
251
+ if( this._hover || this._focus || this._swipe || document.hidden ) {
252
+ return;
253
+ }
254
+
255
+ const index = this._index>=this._maxIndex( ) ? 0 : this._index+1;
256
+ if( this._show( index ) ) {
257
+ this.fire( "change", { value: this._index } );
258
+ }
259
+ } );
260
+ }
261
+
262
+ // :: swipe :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
263
+
264
+ private _on_pointerdown( ev: PointerEvent ) {
265
+ if( ev.button!=0 || Component.closest( ev.target as Element, ".arrow" ) ) {
266
+ return;
267
+ }
268
+
269
+ this._swipe = { id: ev.pointerId, x: ev.clientX, y: ev.clientY, dragging: false };
270
+ }
271
+
272
+ private _on_pointermove( ev: PointerEvent ) {
273
+ const sw = this._swipe;
274
+ if( !sw || sw.id!=ev.pointerId ) {
275
+ return;
276
+ }
277
+
278
+ let dx = ev.clientX - sw.x;
279
+
280
+ if( !sw.dragging ) {
281
+ // more than a click; along the slides, not across (the page scrolls)
282
+ if( Math.abs( dx )<6 || Math.abs( dx )<Math.abs( ev.clientY - sw.y ) ) {
283
+ return;
284
+ }
285
+
286
+ // captured: the click at the end goes to the viewport, not to what is in the slide
287
+ sw.dragging = true;
288
+ this._viewport.setCapture( ev.pointerId );
289
+ this.addClass( "dragging" );
290
+ }
291
+
292
+ // at an end, it resists
293
+ const max = this._maxIndex( );
294
+ if( !this._loops( ) && ( ( this._index==0 && dx>0 ) || ( this._index==max && dx<0 ) ) ) {
295
+ dx /= 3;
296
+ }
297
+
298
+ this.setStyleVariable( "--carousel-drag", dx+"px" );
299
+ }
300
+
301
+ // far enough (a fifth of a slide): to the next places, as many as the slides it went over
302
+ private _on_pointerup( ev: PointerEvent ) {
303
+ const sw = this._swipe;
304
+ if( !sw || sw.id!=ev.pointerId ) {
305
+ return;
306
+ }
307
+
308
+ this._swipe = null;
309
+ if( !sw.dragging ) {
310
+ return;
311
+ }
312
+
313
+ this._viewport.releaseCapture( ev.pointerId );
314
+ this.removeClass( "dragging" );
315
+ this.setStyleVariable( "--carousel-drag", "0px" );
316
+
317
+ // taken by the browser (the page scrolls): back to its place
318
+ if( ev.type=="pointercancel" ) {
319
+ return;
320
+ }
321
+
322
+ const dx = ev.clientX - sw.x;
323
+ const gap = parseFloat( this._track.getComputedStyle( ).columnGap ) || 0;
324
+ const step = ( this._viewport.getBoundingRect( ).width + gap ) / this._perView( );
325
+
326
+ let n = Math.round( -dx / step );
327
+ if( n==0 && Math.abs( dx )>step/5 ) {
328
+ n = dx<0 ? 1 : -1;
329
+ }
330
+
331
+ if( n ) {
332
+ this._go( this._index + n );
333
+ }
334
+ }
335
+
336
+ private _on_key( ev: KeyboardEvent ) {
337
+ switch( ev.key ) {
338
+ case "ArrowLeft": this._go( this._index-1 ); break;
339
+ case "ArrowRight": this._go( this._index+1 ); break;
340
+ case "Home": this._go( 0 ); break;
341
+ case "End": this._go( this._maxIndex( ) ); break;
342
+ default: return;
343
+ }
344
+
345
+ ev.preventDefault( );
346
+ }
347
+
348
+ // :: api :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
349
+
350
+ /**
351
+ * the slides; index: the place to show (the current one by default)
352
+ */
353
+
354
+ setItems( items: CarouselItem[], index = this._index ) {
355
+ const n = items.length;
356
+ this._images.clear( );
357
+
358
+ this._slides = items.map( ( item, i ) => {
359
+ const built = this._buildSlide( item );
360
+ let s: Component;
361
+
362
+ if( built instanceof Component ) {
363
+ s = new Component( { cls: "slide", content: built } );
364
+ }
365
+ else {
366
+ s = new Component( { cls: "slide image", content: built.content } );
367
+ this._images.set( i, { image: built.image, src: built.src } );
368
+ }
369
+
370
+ s.setAttribute( "role", "group" );
371
+ s.setAria( "aria-roledescription", _tr.global.carousel.slide );
372
+ s.setAria( "aria-label", sprintf( _tr.global.carousel.position, i+1, n ) );
373
+ return s;
374
+ } );
375
+
376
+ this._track.setContent( this._slides );
377
+ this.setStyleVariable( "--carousel-per-view", this._perView( )+"" );
378
+
379
+ // a dot for each place
380
+ const places = this._maxIndex( ) + 1;
381
+ this._dots.setContent( Array.from( { length: places }, ( _, i ) => new Component( {
382
+ tag: "button", cls: "dot",
383
+ attrs: { type: "button", tabindex: -1, "aria-label": sprintf( _tr.global.carousel.position, i+1, places ) },
384
+ dom_events: { click: ( ) => this._go( i ) },
385
+ } ) ) );
386
+
387
+ // nothing to move: no commands
388
+ const moves = places>1;
389
+ this._prev.show( moves && this.props.arrows!==false );
390
+ this._next.show( moves && this.props.arrows!==false );
391
+ this._dots.show( moves && this.props.dots!==false );
392
+
393
+ this._index = -1;
394
+ this._show( index );
395
+ }
396
+
397
+ /**
398
+ * goes to a slide, without a change event; beyond the ends: the end
399
+ */
400
+
401
+ select( index: number ) {
402
+ this._show( index );
403
+ this._autoplay( );
404
+ }
405
+
406
+ /** the next place, the first after the last when it loops; without a change event */
407
+ next( ) {
408
+ this._go( this._index+1, false );
409
+ }
410
+
411
+ /** the place before, the last before the first when it loops; without a change event */
412
+ prev( ) {
413
+ this._go( this._index-1, false );
414
+ }
415
+
416
+ getIndex( ) {
417
+ return this._index;
418
+ }
419
+
420
+ getCount( ) {
421
+ return this._slides.length;
422
+ }
423
+ }
@@ -60,6 +60,14 @@
60
60
  stroke: var( --chart-axis );
61
61
  }
62
62
 
63
+ // radar: the rings and the axes are oblique, crispEdges would make them jagged
64
+ .radar {
65
+ .grid,
66
+ .base {
67
+ shape-rendering: auto;
68
+ }
69
+ }
70
+
63
71
  // --chart-c is the color of the element (palette slot, or serie.color)
64
72
  @for $i from 1 through 8 {
65
73
  .c#{$i} {