@astryxdesign/core 0.1.3 → 0.1.4-canary.02643db

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 (168) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/Button/Button.d.ts.map +1 -1
  3. package/dist/Button/Button.js +29 -8
  4. package/dist/Calendar/Calendar.d.ts.map +1 -1
  5. package/dist/Calendar/Calendar.js +49 -20
  6. package/dist/Calendar/dayCellUtils.d.ts +67 -5
  7. package/dist/Calendar/dayCellUtils.d.ts.map +1 -1
  8. package/dist/Calendar/dayCellUtils.js +139 -18
  9. package/dist/CheckboxInput/CheckboxInput.d.ts +6 -1
  10. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  11. package/dist/CheckboxInput/CheckboxInput.js +7 -1
  12. package/dist/Code/Code.d.ts +15 -1
  13. package/dist/Code/Code.d.ts.map +1 -1
  14. package/dist/Code/Code.js +32 -2
  15. package/dist/Code/index.d.ts +1 -1
  16. package/dist/Code/index.d.ts.map +1 -1
  17. package/dist/CodeBlock/index.d.ts +1 -1
  18. package/dist/CodeBlock/index.d.ts.map +1 -1
  19. package/dist/CommandPalette/CommandPaletteEmpty.d.ts +1 -1
  20. package/dist/CommandPalette/CommandPaletteEmpty.d.ts.map +1 -1
  21. package/dist/CommandPalette/CommandPaletteEmpty.js +24 -4
  22. package/dist/ContextMenu/ContextMenu.d.ts +9 -1
  23. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  24. package/dist/ContextMenu/ContextMenu.js +6 -3
  25. package/dist/DateRangeInput/DateRangeInput.js +2 -2
  26. package/dist/DateTimeInput/DateTimeInput.d.ts +0 -5
  27. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  28. package/dist/Divider/Divider.d.ts +0 -22
  29. package/dist/Divider/Divider.d.ts.map +1 -1
  30. package/dist/Markdown/parser.d.ts +7 -0
  31. package/dist/Markdown/parser.d.ts.map +1 -1
  32. package/dist/Markdown/parser.js +300 -6
  33. package/dist/MultiSelector/MultiSelector.d.ts +7 -1
  34. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  35. package/dist/MultiSelector/MultiSelector.js +10 -1
  36. package/dist/RadioList/RadioList.d.ts +7 -1
  37. package/dist/RadioList/RadioList.d.ts.map +1 -1
  38. package/dist/RadioList/RadioList.js +3 -1
  39. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  40. package/dist/RadioList/RadioListItem.js +6 -1
  41. package/dist/Selector/Selector.d.ts +6 -0
  42. package/dist/Selector/Selector.d.ts.map +1 -1
  43. package/dist/Selector/Selector.js +9 -0
  44. package/dist/Slider/Slider.d.ts +6 -0
  45. package/dist/Slider/Slider.d.ts.map +1 -1
  46. package/dist/Slider/Slider.js +10 -1
  47. package/dist/Spinner/Spinner.d.ts.map +1 -1
  48. package/dist/Spinner/Spinner.js +1 -1
  49. package/dist/Switch/Switch.d.ts +6 -1
  50. package/dist/Switch/Switch.d.ts.map +1 -1
  51. package/dist/Switch/Switch.js +7 -1
  52. package/dist/Table/BaseTable.d.ts.map +1 -1
  53. package/dist/Table/BaseTable.js +8 -1
  54. package/dist/Table/TableCell.d.ts.map +1 -1
  55. package/dist/Table/TableCell.js +73 -4
  56. package/dist/Table/index.d.ts +2 -0
  57. package/dist/Table/index.d.ts.map +1 -1
  58. package/dist/Table/index.js +1 -0
  59. package/dist/Table/plugins/rowExpansion/index.d.ts +3 -0
  60. package/dist/Table/plugins/rowExpansion/index.d.ts.map +1 -0
  61. package/dist/Table/plugins/rowExpansion/index.js +3 -0
  62. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts +90 -0
  63. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -0
  64. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +398 -0
  65. package/dist/Table/tableContextMenu.d.ts +5 -1
  66. package/dist/Table/tableContextMenu.d.ts.map +1 -1
  67. package/dist/Table/tableContextMenu.js +10 -3
  68. package/dist/Text/Text.d.ts +1 -1
  69. package/dist/Text/Text.d.ts.map +1 -1
  70. package/dist/Text/Text.js +4 -3
  71. package/dist/Text/text.stylex.d.ts +35 -0
  72. package/dist/Text/text.stylex.d.ts.map +1 -1
  73. package/dist/Text/text.stylex.js +52 -1
  74. package/dist/Tokenizer/Tokenizer.d.ts +6 -1
  75. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  76. package/dist/Tokenizer/Tokenizer.js +10 -1
  77. package/dist/astryx.css +16 -0
  78. package/dist/astryx.umd.js +51 -49
  79. package/dist/astryx.umd.js.map +4 -4
  80. package/dist/docs-types.d.ts +12 -0
  81. package/dist/docs-types.d.ts.map +1 -1
  82. package/package.json +1 -1
  83. package/src/AspectRatio/AspectRatio.doc.mjs +1 -1
  84. package/src/Avatar/Avatar.doc.mjs +2 -2
  85. package/src/Button/Button.tsx +39 -6
  86. package/src/ButtonGroup/ButtonGroup.doc.mjs +1 -1
  87. package/src/Calendar/Calendar.test.tsx +106 -0
  88. package/src/Calendar/Calendar.tsx +59 -22
  89. package/src/Calendar/dayCellUtils.test.ts +306 -1
  90. package/src/Calendar/dayCellUtils.ts +206 -13
  91. package/src/Card/Card.doc.mjs +1 -1
  92. package/src/Chat/useChatNewMessages.test.tsx +4 -4
  93. package/src/CheckboxInput/CheckboxInput.doc.mjs +9 -1
  94. package/src/CheckboxInput/CheckboxInput.test.tsx +36 -0
  95. package/src/CheckboxInput/CheckboxInput.tsx +11 -0
  96. package/src/CheckboxList/CheckboxList.doc.mjs +1 -1
  97. package/src/Code/Code.test.tsx +54 -0
  98. package/src/Code/Code.tsx +48 -4
  99. package/src/Code/index.ts +1 -1
  100. package/src/CodeBlock/index.ts +1 -1
  101. package/src/CommandPalette/CommandPaletteEmpty.tsx +15 -1
  102. package/src/ContextMenu/ContextMenu.tsx +17 -1
  103. package/src/DateInput/DateInput.doc.mjs +6 -1
  104. package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
  105. package/src/DateRangeInput/DateRangeInput.tsx +2 -2
  106. package/src/DateTimeInput/DateTimeInput.doc.mjs +1 -1
  107. package/src/DateTimeInput/DateTimeInput.tsx +1 -6
  108. package/src/Divider/Divider.tsx +1 -23
  109. package/src/DropdownMenu/DropdownMenu.test.tsx +5 -3
  110. package/src/EmptyState/EmptyState.doc.mjs +1 -1
  111. package/src/Field/Field.doc.mjs +1 -1
  112. package/src/FileInput/FileInput.doc.mjs +1 -1
  113. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -3
  114. package/src/Grid/Grid.doc.mjs +3 -3
  115. package/src/Layout/__tests__/edgeCompensation.test.tsx +72 -0
  116. package/src/Markdown/Markdown.test.tsx +23 -0
  117. package/src/Markdown/incremental.test.ts +36 -0
  118. package/src/Markdown/parser.test.ts +210 -0
  119. package/src/Markdown/parser.ts +301 -6
  120. package/src/MobileNav/MobileNav.doc.mjs +20 -0
  121. package/src/MultiSelector/MultiSelector.doc.mjs +14 -1
  122. package/src/MultiSelector/MultiSelector.test.tsx +33 -0
  123. package/src/MultiSelector/MultiSelector.tsx +20 -0
  124. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  125. package/src/RadioList/RadioList.doc.mjs +7 -1
  126. package/src/RadioList/RadioList.test.tsx +46 -0
  127. package/src/RadioList/RadioList.tsx +10 -1
  128. package/src/RadioList/RadioListItem.tsx +4 -0
  129. package/src/Section/Section.test.tsx +4 -10
  130. package/src/SegmentedControl/SegmentedControl.doc.mjs +1 -1
  131. package/src/Selector/Selector.doc.mjs +12 -1
  132. package/src/Selector/Selector.test.tsx +30 -0
  133. package/src/Selector/Selector.tsx +18 -0
  134. package/src/Slider/Slider.doc.mjs +14 -2
  135. package/src/Slider/Slider.test.tsx +30 -0
  136. package/src/Slider/Slider.tsx +20 -0
  137. package/src/Spinner/Spinner.tsx +1 -0
  138. package/src/Switch/Switch.doc.mjs +14 -2
  139. package/src/Switch/Switch.test.tsx +38 -0
  140. package/src/Switch/Switch.tsx +12 -0
  141. package/src/TabList/TabList.doc.mjs +3 -3
  142. package/src/Table/BaseTable.tsx +9 -1
  143. package/src/Table/Table.test.tsx +23 -0
  144. package/src/Table/TableCell.tsx +87 -8
  145. package/src/Table/index.ts +5 -0
  146. package/src/Table/plugins/rowExpansion/index.ts +7 -0
  147. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +155 -0
  148. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +590 -0
  149. package/src/Table/tableContextMenu.test.tsx +27 -3
  150. package/src/Table/tableContextMenu.tsx +20 -2
  151. package/src/Table/useTablePagination.doc.mjs +4 -1
  152. package/src/Table/useTableRowExpansion.doc.mjs +80 -0
  153. package/src/Table/useTableSortable.doc.mjs +4 -1
  154. package/src/Text/Text.doc.mjs +2 -2
  155. package/src/Text/Text.test.tsx +11 -0
  156. package/src/Text/Text.tsx +4 -2
  157. package/src/Text/text.stylex.ts +41 -0
  158. package/src/TextArea/TextArea.doc.mjs +1 -1
  159. package/src/TextInput/TextInput.doc.mjs +1 -1
  160. package/src/TimeInput/TimeInput.doc.mjs +11 -6
  161. package/src/Tokenizer/Tokenizer.doc.mjs +14 -2
  162. package/src/Tokenizer/Tokenizer.test.tsx +33 -0
  163. package/src/Tokenizer/Tokenizer.tsx +19 -0
  164. package/src/Toolbar/Toolbar.test.tsx +1 -1
  165. package/src/Typeahead/BaseTypeahead.doc.mjs +10 -0
  166. package/src/Typeahead/Typeahead.doc.mjs +6 -1
  167. package/src/docs-types.ts +12 -0
  168. package/src/hooks/useKeyboardHint.doc.mjs +6 -6
@@ -5,10 +5,13 @@ import {
5
5
  computeDayCellState,
6
6
  computeRangeRounding,
7
7
  computePreviewRounding,
8
+ computeDayNeighborContinuity,
8
9
  isEndpoint,
10
+ isRangeHighlighted,
11
+ isPreviewHighlighted,
9
12
  type DayCellStateInput,
10
13
  } from './dayCellUtils';
11
- import {plainDateFromISO} from '../utils/plainDate';
14
+ import {plainDateFromISO, plainDateIsEqual} from '../utils/plainDate';
12
15
 
13
16
  function makeInput(
14
17
  overrides: Partial<DayCellStateInput> = {},
@@ -118,6 +121,49 @@ describe('computeDayCellState', () => {
118
121
  expect(stateLast.isFirstColumn).toBe(false);
119
122
  expect(stateLast.isLastColumn).toBe(true);
120
123
  });
124
+
125
+ // Regression (#2715): outside (adjacent-month) days must not receive
126
+ // selection/range/preview state. In the two-month layout the same date
127
+ // renders in both panes, so highlighting the spillover copy would duplicate
128
+ // the selection onto the wrong month's pane.
129
+ it('does not apply range state to outside days', () => {
130
+ const state = computeDayCellState(
131
+ makeInput({
132
+ date: plainDateFromISO('2024-03-17'),
133
+ isOutside: true,
134
+ mode: 'range',
135
+ rangeStart: plainDateFromISO('2024-03-15'),
136
+ rangeEnd: plainDateFromISO('2024-03-20'),
137
+ }),
138
+ );
139
+ expect(state.isInRange).toBe(false);
140
+ expect(state.isRangeStart).toBe(false);
141
+ expect(state.isRangeEnd).toBe(false);
142
+ });
143
+
144
+ it('does not apply preview state to outside days', () => {
145
+ const state = computeDayCellState(
146
+ makeInput({
147
+ date: plainDateFromISO('2024-03-17'),
148
+ isOutside: true,
149
+ previewStart: plainDateFromISO('2024-03-15'),
150
+ previewEnd: plainDateFromISO('2024-03-20'),
151
+ }),
152
+ );
153
+ expect(state.isInPreview).toBe(false);
154
+ expect(state.isPreviewStart).toBe(false);
155
+ expect(state.isPreviewEnd).toBe(false);
156
+ });
157
+
158
+ it('does not mark an outside day as selected', () => {
159
+ const state = computeDayCellState(
160
+ makeInput({
161
+ isOutside: true,
162
+ selectedDate: plainDateFromISO('2024-03-15'),
163
+ }),
164
+ );
165
+ expect(state.isSelected).toBe(false);
166
+ });
121
167
  });
122
168
 
123
169
  describe('computeRangeRounding', () => {
@@ -161,6 +207,96 @@ describe('computeRangeRounding', () => {
161
207
  const rounding = computeRangeRounding(state);
162
208
  expect(rounding.roundRight).toBe(true);
163
209
  });
210
+
211
+ // #2715 follow-up: cap the highlight where the range meets a disabled or
212
+ // adjacent-month (outside) day. A mid-range day (neither endpoint nor grid
213
+ // edge) gets an end cap on the side whose neighbour breaks the run.
214
+ it('caps the right edge when the next day breaks the range (e.g. disabled)', () => {
215
+ const state = computeDayCellState(
216
+ makeInput({
217
+ date: plainDateFromISO('2024-03-17'),
218
+ dayIndex: 3,
219
+ mode: 'range',
220
+ rangeStart: plainDateFromISO('2024-03-15'),
221
+ rangeEnd: plainDateFromISO('2024-03-20'),
222
+ }),
223
+ );
224
+ const rounding = computeRangeRounding(state, {
225
+ prevInRange: true,
226
+ nextInRange: false,
227
+ });
228
+ expect(rounding.roundLeft).toBe(false);
229
+ expect(rounding.roundRight).toBe(true);
230
+ });
231
+
232
+ it('caps the left edge when the previous day breaks the range', () => {
233
+ const state = computeDayCellState(
234
+ makeInput({
235
+ date: plainDateFromISO('2024-03-17'),
236
+ dayIndex: 3,
237
+ mode: 'range',
238
+ rangeStart: plainDateFromISO('2024-03-15'),
239
+ rangeEnd: plainDateFromISO('2024-03-20'),
240
+ }),
241
+ );
242
+ const rounding = computeRangeRounding(state, {
243
+ prevInRange: false,
244
+ nextInRange: true,
245
+ });
246
+ expect(rounding.roundLeft).toBe(true);
247
+ expect(rounding.roundRight).toBe(false);
248
+ });
249
+
250
+ it('does not cap a mid-range day when both neighbours continue the range', () => {
251
+ const state = computeDayCellState(
252
+ makeInput({
253
+ date: plainDateFromISO('2024-03-17'),
254
+ dayIndex: 3,
255
+ mode: 'range',
256
+ rangeStart: plainDateFromISO('2024-03-15'),
257
+ rangeEnd: plainDateFromISO('2024-03-20'),
258
+ }),
259
+ );
260
+ const rounding = computeRangeRounding(state, {
261
+ prevInRange: true,
262
+ nextInRange: true,
263
+ });
264
+ expect(rounding.roundLeft).toBe(false);
265
+ expect(rounding.roundRight).toBe(false);
266
+ });
267
+ });
268
+
269
+ describe('isRangeHighlighted', () => {
270
+ const base = {
271
+ date: plainDateFromISO('2024-03-17'),
272
+ mode: 'range' as const,
273
+ rangeStart: plainDateFromISO('2024-03-15'),
274
+ rangeEnd: plainDateFromISO('2024-03-20'),
275
+ isDisabled: false,
276
+ isOutside: false,
277
+ };
278
+
279
+ it('true for an enabled in-month day inside the range', () => {
280
+ expect(isRangeHighlighted(base)).toBe(true);
281
+ });
282
+
283
+ it('false for a disabled day (breaks continuity)', () => {
284
+ expect(isRangeHighlighted({...base, isDisabled: true})).toBe(false);
285
+ });
286
+
287
+ it('false for an adjacent-month (outside) day', () => {
288
+ expect(isRangeHighlighted({...base, isOutside: true})).toBe(false);
289
+ });
290
+
291
+ it('false outside the range bounds', () => {
292
+ expect(
293
+ isRangeHighlighted({...base, date: plainDateFromISO('2024-03-25')}),
294
+ ).toBe(false);
295
+ });
296
+
297
+ it('false in single mode', () => {
298
+ expect(isRangeHighlighted({...base, mode: 'single'})).toBe(false);
299
+ });
164
300
  });
165
301
 
166
302
  describe('computePreviewRounding', () => {
@@ -175,6 +311,175 @@ describe('computePreviewRounding', () => {
175
311
  expect(rounding.roundLeft).toBe(true);
176
312
  expect(rounding.roundRight).toBe(false);
177
313
  });
314
+
315
+ // #2715: the hover preview needs the same disabled/outside caps as the
316
+ // committed range, so the transient highlight terminates cleanly at the gap.
317
+ it('caps the right edge when the next day breaks the preview run', () => {
318
+ const state = computeDayCellState(
319
+ makeInput({
320
+ date: plainDateFromISO('2024-03-17'),
321
+ dayIndex: 3,
322
+ previewStart: plainDateFromISO('2024-03-15'),
323
+ previewEnd: plainDateFromISO('2024-03-20'),
324
+ }),
325
+ );
326
+ const rounding = computePreviewRounding(state, {
327
+ prevInPreview: true,
328
+ nextInPreview: false,
329
+ });
330
+ expect(rounding.roundLeft).toBe(false);
331
+ expect(rounding.roundRight).toBe(true);
332
+ });
333
+
334
+ it('caps the left edge when the previous day breaks the preview run', () => {
335
+ const state = computeDayCellState(
336
+ makeInput({
337
+ date: plainDateFromISO('2024-03-17'),
338
+ dayIndex: 3,
339
+ previewStart: plainDateFromISO('2024-03-15'),
340
+ previewEnd: plainDateFromISO('2024-03-20'),
341
+ }),
342
+ );
343
+ const rounding = computePreviewRounding(state, {
344
+ prevInPreview: false,
345
+ nextInPreview: true,
346
+ });
347
+ expect(rounding.roundLeft).toBe(true);
348
+ expect(rounding.roundRight).toBe(false);
349
+ });
350
+
351
+ it('does not cap a mid-preview day when both neighbours continue', () => {
352
+ const state = computeDayCellState(
353
+ makeInput({
354
+ date: plainDateFromISO('2024-03-17'),
355
+ dayIndex: 3,
356
+ previewStart: plainDateFromISO('2024-03-15'),
357
+ previewEnd: plainDateFromISO('2024-03-20'),
358
+ }),
359
+ );
360
+ const rounding = computePreviewRounding(state, {
361
+ prevInPreview: true,
362
+ nextInPreview: true,
363
+ });
364
+ expect(rounding.roundLeft).toBe(false);
365
+ expect(rounding.roundRight).toBe(false);
366
+ });
367
+ });
368
+
369
+ describe('isPreviewHighlighted', () => {
370
+ const base = {
371
+ date: plainDateFromISO('2024-03-17'),
372
+ previewStart: plainDateFromISO('2024-03-15'),
373
+ previewEnd: plainDateFromISO('2024-03-20'),
374
+ isDisabled: false,
375
+ isOutside: false,
376
+ };
377
+
378
+ it('true for an enabled in-month day inside the preview span', () => {
379
+ expect(isPreviewHighlighted(base)).toBe(true);
380
+ });
381
+
382
+ it('false for a disabled day', () => {
383
+ expect(isPreviewHighlighted({...base, isDisabled: true})).toBe(false);
384
+ });
385
+
386
+ it('false for an adjacent-month (outside) day', () => {
387
+ expect(isPreviewHighlighted({...base, isOutside: true})).toBe(false);
388
+ });
389
+
390
+ it('false outside the preview bounds', () => {
391
+ expect(
392
+ isPreviewHighlighted({...base, date: plainDateFromISO('2024-03-25')}),
393
+ ).toBe(false);
394
+ });
395
+
396
+ it('false with no preview span', () => {
397
+ expect(
398
+ isPreviewHighlighted({...base, previewStart: null, previewEnd: null}),
399
+ ).toBe(false);
400
+ });
401
+ });
402
+
403
+ describe('computeDayNeighborContinuity', () => {
404
+ // A week row of 5 consecutive in-month days, Mar 15–19.
405
+ const week = (
406
+ [
407
+ '2024-03-15',
408
+ '2024-03-16',
409
+ '2024-03-17',
410
+ '2024-03-18',
411
+ '2024-03-19',
412
+ ] as const
413
+ ).map(iso => ({
414
+ date: plainDateFromISO(iso),
415
+ isOutside: false,
416
+ }));
417
+
418
+ const baseInput = {
419
+ week,
420
+ mode: 'range' as const,
421
+ rangeStart: plainDateFromISO('2024-03-15'),
422
+ rangeEnd: plainDateFromISO('2024-03-19'),
423
+ previewStart: null,
424
+ previewEnd: null,
425
+ isDisabled: () => false,
426
+ };
427
+
428
+ it('reports both range neighbours continuing for a mid-range day', () => {
429
+ const c = computeDayNeighborContinuity({...baseInput, dayIndex: 2});
430
+ expect(c.prevInRange).toBe(true);
431
+ expect(c.nextInRange).toBe(true);
432
+ });
433
+
434
+ it('breaks continuity where a neighbour is disabled', () => {
435
+ // Disable Mar 18 (index 3): its neighbour Mar 17 (index 2) sees a break to
436
+ // the right.
437
+ const disabledDay = plainDateFromISO('2024-03-18');
438
+ const c = computeDayNeighborContinuity({
439
+ ...baseInput,
440
+ dayIndex: 2,
441
+ isDisabled: date => plainDateIsEqual(date, disabledDay),
442
+ });
443
+ expect(c.prevInRange).toBe(true);
444
+ expect(c.nextInRange).toBe(false);
445
+ });
446
+
447
+ it('breaks continuity where a neighbour is an outside day', () => {
448
+ const weekWithOutside = week.map((d, i) =>
449
+ i === 3 ? {...d, isOutside: true} : d,
450
+ );
451
+ const c = computeDayNeighborContinuity({
452
+ ...baseInput,
453
+ week: weekWithOutside,
454
+ dayIndex: 2,
455
+ });
456
+ expect(c.nextInRange).toBe(false);
457
+ });
458
+
459
+ it('treats a missing neighbour (row edge) as a break', () => {
460
+ const first = computeDayNeighborContinuity({...baseInput, dayIndex: 0});
461
+ expect(first.prevInRange).toBe(false);
462
+ const last = computeDayNeighborContinuity({
463
+ ...baseInput,
464
+ dayIndex: week.length - 1,
465
+ });
466
+ expect(last.nextInRange).toBe(false);
467
+ });
468
+
469
+ it('derives preview continuity independently of range', () => {
470
+ const c = computeDayNeighborContinuity({
471
+ ...baseInput,
472
+ rangeStart: null,
473
+ rangeEnd: null,
474
+ previewStart: plainDateFromISO('2024-03-15'),
475
+ previewEnd: plainDateFromISO('2024-03-19'),
476
+ dayIndex: 2,
477
+ });
478
+ expect(c.prevInRange).toBe(false);
479
+ expect(c.nextInRange).toBe(false);
480
+ expect(c.prevInPreview).toBe(true);
481
+ expect(c.nextInPreview).toBe(true);
482
+ });
178
483
  });
179
484
 
180
485
  describe('isEndpoint', () => {
@@ -31,7 +31,14 @@ export interface DayCellStateInput {
31
31
  isOutside: boolean;
32
32
  }
33
33
 
34
- /** Derives all visual/interaction states for a single day cell. */
34
+ /**
35
+ * Derives all visual/interaction states for a single day cell.
36
+ *
37
+ * Outside (adjacent-month) days never receive selection, range, or preview
38
+ * state: in the two-month layout the same date renders in both panes, and
39
+ * highlighting the spillover copy would duplicate the selection onto the wrong
40
+ * month's pane (#2715).
41
+ */
35
42
  export function computeDayCellState(input: DayCellStateInput): DayCellState {
36
43
  const {
37
44
  date,
@@ -51,55 +58,241 @@ export function computeDayCellState(input: DayCellStateInput): DayCellState {
51
58
  effectivelyDisabled: isDisabled || isOutside,
52
59
  isToday: plainDateIsEqual(date, today),
53
60
  isSelected: !!(
61
+ !isOutside &&
54
62
  mode === 'single' &&
55
63
  selectedDate &&
56
64
  plainDateIsEqual(date, selectedDate)
57
65
  ),
58
66
  isInRange: !!(
67
+ !isOutside &&
59
68
  mode === 'range' &&
60
69
  rangeStart &&
61
70
  rangeEnd &&
62
71
  plainDateIsInRange(date, [rangeStart, rangeEnd])
63
72
  ),
64
73
  isRangeStart: !!(
74
+ !isOutside &&
65
75
  mode === 'range' &&
66
76
  rangeStart &&
67
77
  plainDateIsEqual(date, rangeStart)
68
78
  ),
69
79
  isRangeEnd: !!(
80
+ !isOutside &&
70
81
  mode === 'range' &&
71
82
  rangeEnd &&
72
83
  plainDateIsEqual(date, rangeEnd)
73
84
  ),
74
85
  isInPreview: !!(
86
+ !isOutside &&
75
87
  previewStart &&
76
88
  previewEnd &&
77
89
  plainDateIsInRange(date, [previewStart, previewEnd])
78
90
  ),
79
- isPreviewStart: !!(previewStart && plainDateIsEqual(date, previewStart)),
80
- isPreviewEnd: !!(previewEnd && plainDateIsEqual(date, previewEnd)),
91
+ isPreviewStart: !!(
92
+ !isOutside &&
93
+ previewStart &&
94
+ plainDateIsEqual(date, previewStart)
95
+ ),
96
+ isPreviewEnd: !!(
97
+ !isOutside &&
98
+ previewEnd &&
99
+ plainDateIsEqual(date, previewEnd)
100
+ ),
81
101
  isFirstColumn: dayIndex === 0,
82
102
  isLastColumn: dayIndex === 6,
83
103
  };
84
104
  }
85
105
 
86
- /** Edge rounding for range background (rounds at grid edges and range endpoints). */
87
- export function computeRangeRounding(state: DayCellState) {
106
+ /**
107
+ * Generic rounding for a highlight run (range or preview).
108
+ *
109
+ * Rounds at the run's endpoints and the grid row edges, and also caps the
110
+ * highlight where the run meets a break in continuity — a neighbouring cell
111
+ * that is disabled or an adjacent-month (outside) day (#2715).
112
+ */
113
+ function computeHighlightRounding(input: {
114
+ isStart: boolean;
115
+ isEnd: boolean;
116
+ isFirstColumn: boolean;
117
+ isLastColumn: boolean;
118
+ prevContinues?: boolean;
119
+ nextContinues?: boolean;
120
+ }) {
121
+ const prevBreaks =
122
+ input.prevContinues !== undefined ? !input.prevContinues : false;
123
+ const nextBreaks =
124
+ input.nextContinues !== undefined ? !input.nextContinues : false;
88
125
  return {
89
- roundLeft: state.isRangeStart || state.isFirstColumn,
90
- roundRight: state.isRangeEnd || state.isLastColumn,
126
+ roundLeft: input.isStart || input.isFirstColumn || prevBreaks,
127
+ roundRight: input.isEnd || input.isLastColumn || nextBreaks,
91
128
  };
92
129
  }
93
130
 
94
- /** Edge rounding for preview background. */
95
- export function computePreviewRounding(state: DayCellState) {
96
- return {
97
- roundLeft: state.isPreviewStart || state.isFirstColumn,
98
- roundRight: state.isPreviewEnd || state.isLastColumn,
99
- };
131
+ /** Rounding for the committed range background. */
132
+ export function computeRangeRounding(
133
+ state: DayCellState,
134
+ neighbors?: {prevInRange?: boolean; nextInRange?: boolean},
135
+ ) {
136
+ return computeHighlightRounding({
137
+ isStart: state.isRangeStart,
138
+ isEnd: state.isRangeEnd,
139
+ isFirstColumn: state.isFirstColumn,
140
+ isLastColumn: state.isLastColumn,
141
+ prevContinues: neighbors?.prevInRange,
142
+ nextContinues: neighbors?.nextInRange,
143
+ });
144
+ }
145
+
146
+ /** Rounding for the hover-preview background. */
147
+ export function computePreviewRounding(
148
+ state: DayCellState,
149
+ neighbors?: {prevInPreview?: boolean; nextInPreview?: boolean},
150
+ ) {
151
+ return computeHighlightRounding({
152
+ isStart: state.isPreviewStart,
153
+ isEnd: state.isPreviewEnd,
154
+ isFirstColumn: state.isFirstColumn,
155
+ isLastColumn: state.isLastColumn,
156
+ prevContinues: neighbors?.prevInPreview,
157
+ nextContinues: neighbors?.nextInPreview,
158
+ });
100
159
  }
101
160
 
102
161
  /** Whether a day is a selection endpoint (selected, range start, or range end). */
103
162
  export function isEndpoint(state: DayCellState): boolean {
104
163
  return state.isSelected || state.isRangeStart || state.isRangeEnd;
105
164
  }
165
+
166
+ /**
167
+ * Whether a day is part of an unbroken highlight run (enabled, in-month, inside
168
+ * the given span). Outside and disabled days break the run, producing end caps.
169
+ */
170
+ function isSpanHighlighted(input: {
171
+ date: PlainDate;
172
+ spanStart: PlainDate | null;
173
+ spanEnd: PlainDate | null;
174
+ isDisabled: boolean;
175
+ isOutside: boolean;
176
+ }): boolean {
177
+ const {date, spanStart, spanEnd, isDisabled, isOutside} = input;
178
+ return !!(
179
+ !isOutside &&
180
+ !isDisabled &&
181
+ spanStart &&
182
+ spanEnd &&
183
+ plainDateIsInRange(date, [spanStart, spanEnd])
184
+ );
185
+ }
186
+
187
+ /** Whether a day participates in the continuous committed-range highlight. */
188
+ export function isRangeHighlighted(input: {
189
+ date: PlainDate;
190
+ mode: 'single' | 'range';
191
+ rangeStart: PlainDate | null;
192
+ rangeEnd: PlainDate | null;
193
+ isDisabled: boolean;
194
+ isOutside: boolean;
195
+ }): boolean {
196
+ if (input.mode !== 'range') {return false;}
197
+ return isSpanHighlighted({
198
+ date: input.date,
199
+ spanStart: input.rangeStart,
200
+ spanEnd: input.rangeEnd,
201
+ isDisabled: input.isDisabled,
202
+ isOutside: input.isOutside,
203
+ });
204
+ }
205
+
206
+ /** Whether a day participates in the continuous hover-preview highlight. */
207
+ export function isPreviewHighlighted(input: {
208
+ date: PlainDate;
209
+ previewStart: PlainDate | null;
210
+ previewEnd: PlainDate | null;
211
+ isDisabled: boolean;
212
+ isOutside: boolean;
213
+ }): boolean {
214
+ return isSpanHighlighted({
215
+ date: input.date,
216
+ spanStart: input.previewStart,
217
+ spanEnd: input.previewEnd,
218
+ isDisabled: input.isDisabled,
219
+ isOutside: input.isOutside,
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Continuity of the highlighted run through a day's immediate neighbours in the
225
+ * same week row, for both the committed range and the hover preview. A day gets
226
+ * an end cap on whichever side its neighbour does not continue the run (see
227
+ * {@link computeRangeRounding} / {@link computePreviewRounding}).
228
+ */
229
+ export interface DayNeighborContinuity {
230
+ prevInRange: boolean;
231
+ nextInRange: boolean;
232
+ prevInPreview: boolean;
233
+ nextInPreview: boolean;
234
+ }
235
+
236
+ /** Minimal shape needed from a week's day cells to derive neighbour continuity. */
237
+ export interface NeighborDay {
238
+ date: PlainDate;
239
+ isOutside: boolean;
240
+ }
241
+
242
+ /**
243
+ * Derives whether the previous/next day in the same week row continues the
244
+ * highlighted run, for both range and preview. Broken out from the render path
245
+ * so the neighbour logic is unit-testable in isolation.
246
+ */
247
+ export function computeDayNeighborContinuity(input: {
248
+ week: ReadonlyArray<NeighborDay>;
249
+ dayIndex: number;
250
+ mode: 'single' | 'range';
251
+ rangeStart: PlainDate | null;
252
+ rangeEnd: PlainDate | null;
253
+ previewStart: PlainDate | null;
254
+ previewEnd: PlainDate | null;
255
+ isDisabled: (date: PlainDate) => boolean;
256
+ }): DayNeighborContinuity {
257
+ const {
258
+ week,
259
+ dayIndex,
260
+ mode,
261
+ rangeStart,
262
+ rangeEnd,
263
+ previewStart,
264
+ previewEnd,
265
+ isDisabled,
266
+ } = input;
267
+
268
+ const prev = week[dayIndex - 1];
269
+ const next = week[dayIndex + 1];
270
+
271
+ const rangeContinues = (day: NeighborDay | undefined): boolean =>
272
+ day != null &&
273
+ isRangeHighlighted({
274
+ date: day.date,
275
+ mode,
276
+ rangeStart,
277
+ rangeEnd,
278
+ isDisabled: isDisabled(day.date),
279
+ isOutside: day.isOutside,
280
+ });
281
+
282
+ const previewContinues = (day: NeighborDay | undefined): boolean =>
283
+ day != null &&
284
+ isPreviewHighlighted({
285
+ date: day.date,
286
+ previewStart,
287
+ previewEnd,
288
+ isDisabled: isDisabled(day.date),
289
+ isOutside: day.isOutside,
290
+ });
291
+
292
+ return {
293
+ prevInRange: rangeContinues(prev),
294
+ nextInRange: rangeContinues(next),
295
+ prevInPreview: previewContinues(prev),
296
+ nextInPreview: previewContinues(next),
297
+ };
298
+ }
@@ -84,7 +84,7 @@ export const docs = {
84
84
  theming: {
85
85
  container: true,
86
86
  targets: [
87
- {className: 'astryx-card'},
87
+ {className: 'astryx-card', visualProps: ['variant']},
88
88
  ],
89
89
  vars: [
90
90
  {name: '--_card-radius', description: 'Border radius of the card', default: 'var(--radius-container)', private: true},
@@ -64,7 +64,7 @@ describe('useChatNewMessages — callback ref (issue #2282)', () => {
64
64
  const {contentRef} = useChatNewMessages({isLocked: true, onResize});
65
65
  return (
66
66
  <div ref={contentRef} data-testid="content">
67
- <div className="xds-chat-message">msg</div>
67
+ <div className="astryx-chat-message">msg</div>
68
68
  </div>
69
69
  );
70
70
  }
@@ -91,7 +91,7 @@ describe('useChatNewMessages — callback ref (issue #2282)', () => {
91
91
  </button>
92
92
  {mounted && (
93
93
  <div ref={contentRef} data-testid="content">
94
- <div className="xds-chat-message">msg</div>
94
+ <div className="astryx-chat-message">msg</div>
95
95
  </div>
96
96
  )}
97
97
  </>
@@ -129,7 +129,7 @@ describe('useChatNewMessages — callback ref (issue #2282)', () => {
129
129
  </button>
130
130
  {mounted && (
131
131
  <div ref={contentRef} data-testid="content">
132
- <div className="xds-chat-message">msg</div>
132
+ <div className="astryx-chat-message">msg</div>
133
133
  </div>
134
134
  )}
135
135
  </>
@@ -198,7 +198,7 @@ describe('useChatNewMessages — callback ref (issue #2282)', () => {
198
198
  if (!el.querySelector?.('.astryx-chat-message')) {
199
199
  return false;
200
200
  }
201
- if (el.className?.includes('xds-chat-layout')) {
201
+ if (el.className?.includes('astryx-chat-layout')) {
202
202
  return false;
203
203
  }
204
204
  return true;
@@ -63,11 +63,17 @@ export const docs = {
63
63
  description: 'Whether the checkbox is disabled.',
64
64
  default: 'false',
65
65
  },
66
+ {
67
+ name: 'htmlName',
68
+ type: 'string',
69
+ description:
70
+ 'The HTML name attribute for the underlying checkbox input, useful for form submissions (submits "on" when checked).',
71
+ },
66
72
  {
67
73
  name: 'disabledMessage',
68
74
  type: 'string',
69
75
  description:
70
- 'Explains why the checkbox is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the checkbox focusable via aria-disabled (toggling stays blocked). Use this instead of wrapping a disabled CheckboxInput in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
76
+ 'Explains why the checkbox is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the checkbox focusable via aria-disabled (toggling stays blocked). Use this instead of wrapping a disabled CheckboxInput in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
71
77
  },
72
78
  {
73
79
  name: 'isReadOnly',
@@ -171,6 +177,7 @@ export const docsZh = {
171
177
  },
172
178
  {name: 'isLoading', type: 'boolean', description: '复选框是否处于加载状态。显示旋转器并阻止交互。', default: 'false'},
173
179
  {name: 'isDisabled', type: 'boolean', description: '复选框是否禁用。', default: 'false'},
180
+ {name: 'htmlName', type: 'string', description: '底层复选框输入的 HTML name 属性,用于表单提交(勾选时提交 "on")。'},
174
181
  {name: 'disabledMessage', type: 'string', description: 'Explains why the checkbox is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the checkbox focusable via aria-disabled (toggling stays blocked). Use this instead of wrapping a disabled CheckboxInput in Tooltip — disabled controls swallow the hover events an external Tooltip needs.'},
175
182
  {name: 'isReadOnly', type: 'boolean', description: '复选框是否为只读。以完整不透明度显示当前状态但阻止交互。与 isDisabled 不同,只读复选框不会变暗。', default: 'false'},
176
183
  {name: 'isOptional', type: 'boolean', description: '字段是否可选。与 isRequired 互斥。', default: 'false'},
@@ -222,6 +229,7 @@ export const docsDense = {
222
229
  changeAction: 'async action; fires after onChange, shows spinner while pending',
223
230
  isLoading: 'shows spinner + prevents interaction',
224
231
  isDisabled: 'disable checkbox',
232
+ htmlName: 'HTML name attr for the checkbox; submits "on" when checked.',
225
233
  isOptional: 'mark field as optional (mutually exclusive w/ isRequired)',
226
234
  isRequired: 'mark field as required (mutually exclusive w/ isOptional)',
227
235
  size: 'sm (compact) or md (default)',