@yewdiel21/motion-kit 0.10.0 → 0.14.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/README.md +155 -93
- package/dist/auto/index.d.ts +53 -3
- package/dist/auto/index.js +101 -3
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-IKQ5FODN.js → chunk-6P4P7NU6.js} +2 -218
- package/dist/chunk-6P4P7NU6.js.map +1 -0
- package/dist/chunk-CU6SXW3E.js +20334 -0
- package/dist/chunk-CU6SXW3E.js.map +1 -0
- package/dist/{chunk-XAKC2QY7.js → chunk-EY4BJKC4.js} +3 -28
- package/dist/chunk-EY4BJKC4.js.map +1 -0
- package/dist/chunk-QP4QJOEN.js +72 -0
- package/dist/chunk-QP4QJOEN.js.map +1 -0
- package/dist/css/components/activity-feed.css +240 -0
- package/dist/css/components/attachment.css +266 -0
- package/dist/css/components/breadcrumb.css +110 -0
- package/dist/css/components/chart-tooltip.css +168 -0
- package/dist/css/components/chart.css +296 -0
- package/dist/css/components/code-block.css +264 -0
- package/dist/css/components/color-area.css +82 -0
- package/dist/css/components/color-field.css +67 -0
- package/dist/css/components/color-picker.css +131 -0
- package/dist/css/components/color-slider.css +113 -0
- package/dist/css/components/command-search.css +252 -0
- package/dist/css/components/context-menu.css +23 -0
- package/dist/css/components/credit-card.css +288 -0
- package/dist/css/components/data-table.css +591 -0
- package/dist/css/components/date-range-picker.css +168 -0
- package/dist/css/components/date-time-picker.css +178 -0
- package/dist/css/components/drawer.css +58 -0
- package/dist/css/components/editable-table.css +201 -0
- package/dist/css/components/empty-state.css +61 -0
- package/dist/css/components/file-explorer.css +633 -0
- package/dist/css/components/filter-builder.css +232 -0
- package/dist/css/components/hold-confirm.css +87 -0
- package/dist/css/components/hover-card.css +13 -7
- package/dist/css/components/image-preview.css +195 -0
- package/dist/css/components/json-viewer.css +272 -0
- package/dist/css/components/like-button.css +78 -0
- package/dist/css/components/link-preview.css +275 -0
- package/dist/css/components/loading-button.css +77 -0
- package/dist/css/components/multi-select.css +267 -0
- package/dist/css/components/notification-bell.css +127 -0
- package/dist/css/components/password-strength.css +183 -0
- package/dist/css/components/popover.css +25 -0
- package/dist/css/components/progress.css +140 -0
- package/dist/css/components/questionnaire.css +217 -0
- package/dist/css/components/range-calendar.css +186 -0
- package/dist/css/components/range-slider.css +192 -0
- package/dist/css/components/resizable-panel.css +243 -0
- package/dist/css/components/rich-text-editor.css +284 -0
- package/dist/css/components/scratch-card.css +75 -0
- package/dist/css/components/skeleton.css +108 -0
- package/dist/css/components/split-view.css +19 -0
- package/dist/css/components/star-rating.css +80 -0
- package/dist/css/components/stat-card.css +164 -0
- package/dist/css/components/swipe-actions.css +105 -0
- package/dist/css/components/tag-input.css +244 -0
- package/dist/css/components/time-field.css +88 -0
- package/dist/css/components/timeline.css +219 -0
- package/dist/css/components/tooltip.css +79 -0
- package/dist/css/components/tree-view.css +210 -0
- package/dist/css/components/virtual-list.css +195 -0
- package/dist/css/tokens.css +185 -181
- package/dist/elements/index.d.ts +674 -5
- package/dist/elements/index.js +1361 -8
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-GDGwQnJE.d.ts → engine-Cn6IhE3z.d.ts} +1 -1
- package/dist/engine-QECQnGz1.d.ts +33 -0
- package/dist/index.css +9104 -189
- package/dist/index.d.ts +89 -5
- package/dist/index.js +243 -9
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +579 -23
- package/dist/react/index.js +2042 -7
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-BmBzKH94.d.ts → reduced-BBUusld6.d.ts} +9 -2
- package/dist/reveal-DEvp6U6T.d.ts +2381 -0
- package/dist/{store-DYb3tTsZ.d.ts → store-DjkTBX7f.d.ts} +2 -2
- package/package.json +1 -1
- package/registry.json +406 -1
- package/src/auto/auto-init.ts +98 -0
- package/src/components/activity-feed/README.md +56 -0
- package/src/components/activity-feed/activity-feed.css +239 -0
- package/src/components/activity-feed/element.ts +36 -0
- package/src/components/activity-feed/engine.ts +351 -0
- package/src/components/activity-feed/react.tsx +32 -0
- package/src/components/attachment/README.md +27 -0
- package/src/components/attachment/attachment.css +265 -0
- package/src/components/attachment/element.ts +50 -0
- package/src/components/attachment/engine.ts +469 -0
- package/src/components/attachment/react.tsx +65 -0
- package/src/components/breadcrumb/README.md +38 -0
- package/src/components/breadcrumb/breadcrumb.css +109 -0
- package/src/components/breadcrumb/element.ts +27 -0
- package/src/components/breadcrumb/engine.ts +249 -0
- package/src/components/breadcrumb/react.tsx +30 -0
- package/src/components/chart/README.md +24 -0
- package/src/components/chart/chart.css +295 -0
- package/src/components/chart/element.ts +41 -0
- package/src/components/chart/engine.ts +811 -0
- package/src/components/chart/react.tsx +50 -0
- package/src/components/chart-tooltip/README.md +53 -0
- package/src/components/chart-tooltip/chart-tooltip.css +167 -0
- package/src/components/chart-tooltip/element.ts +37 -0
- package/src/components/chart-tooltip/engine.ts +442 -0
- package/src/components/chart-tooltip/react.tsx +47 -0
- package/src/components/code-block/README.md +19 -0
- package/src/components/code-block/code-block.css +263 -0
- package/src/components/code-block/element.ts +34 -0
- package/src/components/code-block/engine.ts +254 -0
- package/src/components/code-block/highlight.ts +244 -0
- package/src/components/code-block/react.tsx +94 -0
- package/src/components/color-area/README.md +50 -0
- package/src/components/color-area/color-area.css +81 -0
- package/src/components/color-area/element.ts +26 -0
- package/src/components/color-area/engine.ts +246 -0
- package/src/components/color-area/react.tsx +42 -0
- package/src/components/color-field/README.md +43 -0
- package/src/components/color-field/color-field.css +66 -0
- package/src/components/color-field/element.ts +26 -0
- package/src/components/color-field/engine.ts +174 -0
- package/src/components/color-field/react.tsx +42 -0
- package/src/components/color-picker/README.md +45 -0
- package/src/components/color-picker/color-picker.css +130 -0
- package/src/components/color-picker/element.ts +45 -0
- package/src/components/color-picker/engine.ts +314 -0
- package/src/components/color-picker/react.tsx +47 -0
- package/src/components/color-slider/README.md +55 -0
- package/src/components/color-slider/color-slider.css +112 -0
- package/src/components/color-slider/element.ts +26 -0
- package/src/components/color-slider/engine.ts +234 -0
- package/src/components/color-slider/react.tsx +45 -0
- package/src/components/command-search/README.md +15 -0
- package/src/components/command-search/command-search.css +251 -0
- package/src/components/command-search/element.ts +36 -0
- package/src/components/command-search/engine.ts +356 -0
- package/src/components/command-search/match.ts +53 -0
- package/src/components/command-search/react.tsx +66 -0
- package/src/components/context-menu/README.md +31 -0
- package/src/components/context-menu/context-menu.css +22 -0
- package/src/components/context-menu/element.ts +40 -0
- package/src/components/context-menu/engine.ts +153 -0
- package/src/components/context-menu/react.tsx +57 -0
- package/src/components/credit-card/README.md +20 -0
- package/src/components/credit-card/card.ts +77 -0
- package/src/components/credit-card/credit-card.css +287 -0
- package/src/components/credit-card/element.ts +33 -0
- package/src/components/credit-card/engine.ts +410 -0
- package/src/components/credit-card/react.tsx +36 -0
- package/src/components/data-table/README.md +83 -0
- package/src/components/data-table/data-table.css +590 -0
- package/src/components/data-table/element.ts +54 -0
- package/src/components/data-table/engine.ts +1059 -0
- package/src/components/data-table/react.tsx +94 -0
- package/src/components/date-range-picker/README.md +51 -0
- package/src/components/date-range-picker/date-range-picker.css +167 -0
- package/src/components/date-range-picker/element.ts +52 -0
- package/src/components/date-range-picker/engine.ts +352 -0
- package/src/components/date-range-picker/react.tsx +57 -0
- package/src/components/date-time-picker/README.md +48 -0
- package/src/components/date-time-picker/date-time-picker.css +177 -0
- package/src/components/date-time-picker/element.ts +47 -0
- package/src/components/date-time-picker/engine.ts +350 -0
- package/src/components/date-time-picker/react.tsx +54 -0
- package/src/components/dialog/README.md +103 -102
- package/src/components/dialog/element.ts +123 -118
- package/src/components/dialog/engine.ts +449 -432
- package/src/components/drawer/README.md +28 -0
- package/src/components/drawer/drawer.css +57 -0
- package/src/components/drawer/element.ts +21 -0
- package/src/components/drawer/engine.ts +56 -0
- package/src/components/drawer/react.tsx +85 -0
- package/src/components/editable-table/README.md +67 -0
- package/src/components/editable-table/editable-table.css +200 -0
- package/src/components/editable-table/element.ts +45 -0
- package/src/components/editable-table/engine.ts +775 -0
- package/src/components/editable-table/react.tsx +79 -0
- package/src/components/empty-state/README.md +41 -0
- package/src/components/empty-state/element.ts +23 -0
- package/src/components/empty-state/empty-state.css +60 -0
- package/src/components/empty-state/engine.ts +232 -0
- package/src/components/empty-state/react.tsx +34 -0
- package/src/components/file-explorer/README.md +67 -0
- package/src/components/file-explorer/element.ts +39 -0
- package/src/components/file-explorer/engine.ts +1307 -0
- package/src/components/file-explorer/file-explorer.css +632 -0
- package/src/components/file-explorer/react.tsx +77 -0
- package/src/components/filter-builder/README.md +47 -0
- package/src/components/filter-builder/element.ts +34 -0
- package/src/components/filter-builder/engine.ts +365 -0
- package/src/components/filter-builder/filter-builder.css +231 -0
- package/src/components/filter-builder/react.tsx +36 -0
- package/src/components/hold-confirm/README.md +13 -0
- package/src/components/hold-confirm/element.ts +24 -0
- package/src/components/hold-confirm/engine.ts +216 -0
- package/src/components/hold-confirm/hold-confirm.css +86 -0
- package/src/components/hold-confirm/react.tsx +28 -0
- package/src/components/hover-card/README.md +6 -2
- package/src/components/hover-card/engine.ts +68 -6
- package/src/components/hover-card/hover-card.css +13 -7
- package/src/components/image-preview/README.md +16 -0
- package/src/components/image-preview/element.ts +26 -0
- package/src/components/image-preview/engine.ts +325 -0
- package/src/components/image-preview/image-preview.css +194 -0
- package/src/components/image-preview/react.tsx +43 -0
- package/src/components/json-viewer/README.md +56 -0
- package/src/components/json-viewer/element.ts +37 -0
- package/src/components/json-viewer/engine.ts +440 -0
- package/src/components/json-viewer/json-viewer.css +271 -0
- package/src/components/json-viewer/react.tsx +56 -0
- package/src/components/like-button/README.md +13 -0
- package/src/components/like-button/element.ts +47 -0
- package/src/components/like-button/engine.ts +145 -0
- package/src/components/like-button/like-button.css +77 -0
- package/src/components/like-button/react.tsx +39 -0
- package/src/components/link-preview/README.md +19 -0
- package/src/components/link-preview/element.ts +29 -0
- package/src/components/link-preview/engine.ts +192 -0
- package/src/components/link-preview/link-preview.css +274 -0
- package/src/components/link-preview/react.tsx +101 -0
- package/src/components/loading-button/README.md +45 -0
- package/src/components/loading-button/element.ts +34 -0
- package/src/components/loading-button/engine.ts +201 -0
- package/src/components/loading-button/loading-button.css +76 -0
- package/src/components/loading-button/react.tsx +43 -0
- package/src/components/multi-select/README.md +48 -0
- package/src/components/multi-select/element.ts +28 -0
- package/src/components/multi-select/engine.ts +294 -0
- package/src/components/multi-select/multi-select.css +266 -0
- package/src/components/multi-select/react.tsx +39 -0
- package/src/components/notification-bell/README.md +14 -0
- package/src/components/notification-bell/element.ts +48 -0
- package/src/components/notification-bell/engine.ts +202 -0
- package/src/components/notification-bell/notification-bell.css +126 -0
- package/src/components/notification-bell/react.tsx +57 -0
- package/src/components/password-strength/README.md +17 -0
- package/src/components/password-strength/element.ts +22 -0
- package/src/components/password-strength/engine.ts +212 -0
- package/src/components/password-strength/password-strength.css +182 -0
- package/src/components/password-strength/react.tsx +36 -0
- package/src/components/password-strength/score.ts +62 -0
- package/src/components/popover/README.md +44 -0
- package/src/components/popover/element.ts +31 -0
- package/src/components/popover/engine.ts +114 -0
- package/src/components/popover/popover.css +24 -0
- package/src/components/popover/react.tsx +50 -0
- package/src/components/progress/README.md +17 -0
- package/src/components/progress/element.ts +30 -0
- package/src/components/progress/engine.ts +173 -0
- package/src/components/progress/progress.css +139 -0
- package/src/components/progress/react.tsx +35 -0
- package/src/components/questionnaire/README.md +32 -0
- package/src/components/questionnaire/element.ts +118 -0
- package/src/components/questionnaire/engine.ts +255 -0
- package/src/components/questionnaire/questionnaire.css +216 -0
- package/src/components/questionnaire/react.tsx +140 -0
- package/src/components/range-calendar/README.md +58 -0
- package/src/components/range-calendar/element.ts +42 -0
- package/src/components/range-calendar/engine.ts +390 -0
- package/src/components/range-calendar/range-calendar.css +185 -0
- package/src/components/range-calendar/react.tsx +56 -0
- package/src/components/range-slider/README.md +23 -0
- package/src/components/range-slider/element.ts +53 -0
- package/src/components/range-slider/engine.ts +316 -0
- package/src/components/range-slider/range-slider.css +191 -0
- package/src/components/range-slider/react.tsx +66 -0
- package/src/components/resizable-panel/README.md +61 -0
- package/src/components/resizable-panel/element.ts +36 -0
- package/src/components/resizable-panel/engine.ts +551 -0
- package/src/components/resizable-panel/react.tsx +74 -0
- package/src/components/resizable-panel/resizable-panel.css +242 -0
- package/src/components/rich-text-editor/README.md +57 -0
- package/src/components/rich-text-editor/element.ts +34 -0
- package/src/components/rich-text-editor/engine.ts +673 -0
- package/src/components/rich-text-editor/react.tsx +60 -0
- package/src/components/rich-text-editor/rich-text-editor.css +283 -0
- package/src/components/scratch-card/README.md +15 -0
- package/src/components/scratch-card/element.ts +30 -0
- package/src/components/scratch-card/engine.ts +257 -0
- package/src/components/scratch-card/react.tsx +40 -0
- package/src/components/scratch-card/scratch-card.css +74 -0
- package/src/components/skeleton/README.md +15 -0
- package/src/components/skeleton/element.ts +32 -0
- package/src/components/skeleton/engine.ts +104 -0
- package/src/components/skeleton/react.tsx +41 -0
- package/src/components/skeleton/skeleton.css +107 -0
- package/src/components/split-view/README.md +50 -0
- package/src/components/split-view/element.ts +34 -0
- package/src/components/split-view/engine.ts +114 -0
- package/src/components/split-view/react.tsx +63 -0
- package/src/components/split-view/split-view.css +18 -0
- package/src/components/star-rating/README.md +14 -0
- package/src/components/star-rating/element.ts +26 -0
- package/src/components/star-rating/engine.ts +217 -0
- package/src/components/star-rating/react.tsx +33 -0
- package/src/components/star-rating/star-rating.css +79 -0
- package/src/components/stat-card/README.md +42 -0
- package/src/components/stat-card/element.ts +40 -0
- package/src/components/stat-card/engine.ts +283 -0
- package/src/components/stat-card/react.tsx +36 -0
- package/src/components/stat-card/stat-card.css +163 -0
- package/src/components/swipe-actions/README.md +19 -0
- package/src/components/swipe-actions/element.ts +26 -0
- package/src/components/swipe-actions/engine.ts +260 -0
- package/src/components/swipe-actions/react.tsx +80 -0
- package/src/components/swipe-actions/swipe-actions.css +104 -0
- package/src/components/tag-input/README.md +42 -0
- package/src/components/tag-input/element.ts +39 -0
- package/src/components/tag-input/engine.ts +313 -0
- package/src/components/tag-input/react.tsx +54 -0
- package/src/components/tag-input/tag-input.css +243 -0
- package/src/components/time-field/README.md +54 -0
- package/src/components/time-field/element.ts +38 -0
- package/src/components/time-field/engine.ts +110 -0
- package/src/components/time-field/react.tsx +45 -0
- package/src/components/time-field/time-field.css +87 -0
- package/src/components/timeline/README.md +57 -0
- package/src/components/timeline/element.ts +31 -0
- package/src/components/timeline/engine.ts +300 -0
- package/src/components/timeline/react.tsx +59 -0
- package/src/components/timeline/timeline.css +218 -0
- package/src/components/tooltip/README.md +46 -0
- package/src/components/tooltip/element.ts +22 -0
- package/src/components/tooltip/engine.ts +288 -0
- package/src/components/tooltip/react.tsx +30 -0
- package/src/components/tooltip/tooltip.css +78 -0
- package/src/components/tree-view/README.md +61 -0
- package/src/components/tree-view/element.ts +32 -0
- package/src/components/tree-view/engine.ts +569 -0
- package/src/components/tree-view/react.tsx +78 -0
- package/src/components/tree-view/tree-view.css +209 -0
- package/src/components/virtual-list/README.md +81 -0
- package/src/components/virtual-list/element.ts +42 -0
- package/src/components/virtual-list/engine.ts +678 -0
- package/src/components/virtual-list/react.tsx +101 -0
- package/src/components/virtual-list/virtual-list.css +194 -0
- package/src/core/chips.ts +196 -0
- package/src/core/color.ts +304 -0
- package/src/core/date-range.ts +67 -0
- package/src/core/dates.ts +11 -1
- package/src/core/files.ts +107 -0
- package/src/core/filter.ts +229 -0
- package/src/core/gsap.ts +80 -65
- package/src/core/listbox.ts +332 -0
- package/src/core/mark.ts +29 -0
- package/src/core/position.ts +89 -0
- package/src/core/relative-time.ts +77 -0
- package/src/core/segment-field.ts +373 -0
- package/src/core/segments.ts +153 -0
- package/src/core/tree.ts +116 -0
- package/src/core/virtual.ts +79 -0
- package/src/css/tokens.css +186 -182
- package/src/elements/index.ts +147 -0
- package/src/index.ts +72 -0
- package/src/motion/micro.ts +248 -0
- package/src/motion/sheet.ts +88 -65
- package/src/motion/surface.ts +9 -0
- package/src/react/index.ts +57 -0
- package/dist/chunk-IKQ5FODN.js.map +0 -1
- package/dist/chunk-J6OCFNGU.js +0 -177
- package/dist/chunk-J6OCFNGU.js.map +0 -1
- package/dist/chunk-TFNRIZHU.js +0 -3977
- package/dist/chunk-TFNRIZHU.js.map +0 -1
- package/dist/chunk-XAKC2QY7.js.map +0 -1
- package/dist/reveal-jsX-uwUz.d.ts +0 -625
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { gsap, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { reduced } from '../../core/reduced';
|
|
3
|
+
import { createScope } from '../../core/scope';
|
|
4
|
+
import { inert, type Instance } from '../../core/types';
|
|
5
|
+
import { dayKey, toDate, type DateInput } from '../../core/dates';
|
|
6
|
+
import { daysIn } from '../../core/segments';
|
|
7
|
+
import { createSegmentField, type SegmentValues } from '../../core/segment-field';
|
|
8
|
+
import { isPickable, rangeIsPickable, rangeKeys, rangeLength, toRange, type DateRange, type DayRules } from '../../core/date-range';
|
|
9
|
+
import { press, shake } from '../../motion/micro';
|
|
10
|
+
import { flowInMenu } from '../../motion/entrances';
|
|
11
|
+
import { createDialog } from '../dialog/engine';
|
|
12
|
+
import { bandDuration, createRangeCalendar } from '../range-calendar/engine';
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* Date range picker: a field group with two segmented dates (start – end, the
|
|
16
|
+
* locale's order: dd/mm/aaaa in Spanish) and a calendar button. The button
|
|
17
|
+
* opens a range-calendar in an anchored dialog that grows out of it (two months
|
|
18
|
+
* from 680 px, one below), with optional preset chips above; like a menu's
|
|
19
|
+
* options, its content flows in while the surface grows. Typing two
|
|
20
|
+
* complete dates validates them: an end before the start, a day out of
|
|
21
|
+
* min/max or a range crossing an unavailable day marks the field invalid,
|
|
22
|
+
* explains why under it and shakes once when focus leaves. Picking in the
|
|
23
|
+
* calendar fills both fields (the digits roll), keeps the popover while the
|
|
24
|
+
* band draws and the end day pops, plus a moment to see them (SETTLE), then
|
|
25
|
+
* folds it back into the button, which gets the focus back; touching the
|
|
26
|
+
* calendar meanwhile keeps it open. onChange only reports valid ranges.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export interface DateRangePreset {
|
|
30
|
+
label: string;
|
|
31
|
+
start: DateInput;
|
|
32
|
+
end: DateInput;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DateRangePickerOptions {
|
|
36
|
+
value?: { start: DateInput; end: DateInput } | null;
|
|
37
|
+
min?: DateInput | null;
|
|
38
|
+
max?: DateInput | null;
|
|
39
|
+
unavailable?: string[] | ((day: Date) => boolean);
|
|
40
|
+
firstDay?: number;
|
|
41
|
+
locale?: string;
|
|
42
|
+
today?: DateInput;
|
|
43
|
+
/** Accessible name of the field (default 'Fechas'). */
|
|
44
|
+
label?: string;
|
|
45
|
+
/** Hidden inputs name[start] and name[end] (YYYY-MM-DD). */
|
|
46
|
+
name?: string;
|
|
47
|
+
presets?: DateRangePreset[];
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
onChange?: (range: DateRange | null) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface DateRangePickerInstance extends Instance {
|
|
53
|
+
open(): void;
|
|
54
|
+
close(): void;
|
|
55
|
+
setValue(range: { start: DateInput; end: DateInput } | null): void;
|
|
56
|
+
readonly value: DateRange | null;
|
|
57
|
+
readonly isOpen: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const CALENDAR_ICON =
|
|
61
|
+
'<svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="5" width="17" height="15.5" rx="3"/><path d="M3.5 10h17M8 3v4M16 3v4"/></svg>';
|
|
62
|
+
|
|
63
|
+
const ERRORS = {
|
|
64
|
+
order: 'La fecha de fin es anterior a la de inicio',
|
|
65
|
+
bounds: 'Fecha fuera del rango permitido',
|
|
66
|
+
blocked: 'El rango incluye días no disponibles',
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
let sequence = 0;
|
|
70
|
+
/** Seconds the picked range stays in view after its band has drawn, before the popover folds back. */
|
|
71
|
+
const SETTLE = 0.35;
|
|
72
|
+
|
|
73
|
+
/** A complete date from the segments, or null; years 0–99 stay years 0–99 (setFullYear). */
|
|
74
|
+
const dateOf = (values: SegmentValues): Date | null => {
|
|
75
|
+
const { day, month, year } = values;
|
|
76
|
+
if (day == null || month == null || year == null || day > daysIn(year, month)) return null;
|
|
77
|
+
const date = new Date(2000, 0, 1);
|
|
78
|
+
date.setFullYear(year, month - 1, day);
|
|
79
|
+
return date;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const segmentsOf = (date: Date | null) => (date ? { day: date.getDate(), month: date.getMonth() + 1, year: date.getFullYear() } : { day: null, month: null, year: null });
|
|
83
|
+
|
|
84
|
+
export function createDateRangePicker(root: HTMLElement | null, options: DateRangePickerOptions = {}): DateRangePickerInstance {
|
|
85
|
+
if (!root) return inert({ open() {}, close() {}, setValue() {}, value: null, isOpen: false });
|
|
86
|
+
registerMotion();
|
|
87
|
+
const scope = createScope();
|
|
88
|
+
const effects = new Set<gsap.core.Animation>();
|
|
89
|
+
const keep = (animation: gsap.core.Animation | null) => {
|
|
90
|
+
if (!animation) return;
|
|
91
|
+
effects.add(animation);
|
|
92
|
+
// Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
|
|
93
|
+
for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
|
|
94
|
+
};
|
|
95
|
+
const uid = `mk-range-${++sequence}`;
|
|
96
|
+
const locale = options.locale ?? root.closest('[lang]')?.getAttribute('lang') ?? 'es';
|
|
97
|
+
const unavailable = options.unavailable;
|
|
98
|
+
const rules: DayRules = {
|
|
99
|
+
min: options.min ? toDate(options.min) : null,
|
|
100
|
+
max: options.max ? toDate(options.max) : null,
|
|
101
|
+
isUnavailable: typeof unavailable === 'function' ? unavailable : Array.isArray(unavailable) ? (day) => unavailable.includes(dayKey(day)) : undefined,
|
|
102
|
+
};
|
|
103
|
+
const presets = options.presets ?? [];
|
|
104
|
+
let value: DateRange | null = toRange(options.value);
|
|
105
|
+
|
|
106
|
+
const made: Element[] = [];
|
|
107
|
+
const field = document.createElement('div');
|
|
108
|
+
field.dataset.mkRangeField = '';
|
|
109
|
+
field.setAttribute('role', 'group');
|
|
110
|
+
field.setAttribute('aria-label', options.label ?? 'Fechas');
|
|
111
|
+
field.setAttribute('aria-describedby', `${uid}-error`);
|
|
112
|
+
root.append(field);
|
|
113
|
+
made.push(field);
|
|
114
|
+
|
|
115
|
+
const which = (suffix: string) => ({ day: `día ${suffix}`, month: `mes ${suffix}`, year: `año ${suffix}` });
|
|
116
|
+
const startField = createSegmentField(field, { kind: 'date', locale, labels: which('de inicio'), disabled: options.disabled, onChange: () => typed() });
|
|
117
|
+
startField.element.setAttribute('data-mk-range-start', '');
|
|
118
|
+
startField.element.setAttribute('aria-label', 'Fecha de inicio');
|
|
119
|
+
const dash = document.createElement('span');
|
|
120
|
+
dash.dataset.mkRangeDash = '';
|
|
121
|
+
dash.setAttribute('aria-hidden', 'true');
|
|
122
|
+
dash.textContent = '–';
|
|
123
|
+
field.append(dash);
|
|
124
|
+
const endField = createSegmentField(field, { kind: 'date', locale, labels: which('de fin'), disabled: options.disabled, onChange: () => typed() });
|
|
125
|
+
endField.element.setAttribute('data-mk-range-end', '');
|
|
126
|
+
endField.element.setAttribute('aria-label', 'Fecha de fin');
|
|
127
|
+
scope.add(() => {
|
|
128
|
+
startField.destroy();
|
|
129
|
+
endField.destroy();
|
|
130
|
+
});
|
|
131
|
+
const button = document.createElement('button');
|
|
132
|
+
button.type = 'button';
|
|
133
|
+
button.dataset.mkRangeButton = '';
|
|
134
|
+
button.setAttribute('aria-label', 'Abrir calendario');
|
|
135
|
+
button.setAttribute('aria-haspopup', 'dialog');
|
|
136
|
+
button.setAttribute('aria-expanded', 'false');
|
|
137
|
+
button.disabled = Boolean(options.disabled);
|
|
138
|
+
button.innerHTML = CALENDAR_ICON;
|
|
139
|
+
field.append(button);
|
|
140
|
+
|
|
141
|
+
const error = document.createElement('p');
|
|
142
|
+
error.dataset.mkRangeError = '';
|
|
143
|
+
error.id = `${uid}-error`;
|
|
144
|
+
error.setAttribute('aria-live', 'polite');
|
|
145
|
+
error.hidden = true;
|
|
146
|
+
root.append(error);
|
|
147
|
+
made.push(error);
|
|
148
|
+
|
|
149
|
+
const dialogElement = document.createElement('dialog');
|
|
150
|
+
// A class, not only an attribute: the copy that folds back into the button on close keeps classes only.
|
|
151
|
+
dialogElement.className = 'mk-dialog mk-range-popover';
|
|
152
|
+
dialogElement.dataset.mkRangePopover = '';
|
|
153
|
+
dialogElement.setAttribute('aria-label', 'Elegir fechas');
|
|
154
|
+
const presetsHost = document.createElement('div');
|
|
155
|
+
presetsHost.dataset.mkRangePresets = '';
|
|
156
|
+
presetsHost.setAttribute('role', 'group');
|
|
157
|
+
presetsHost.setAttribute('aria-label', 'Atajos');
|
|
158
|
+
presets.forEach((preset, index) => {
|
|
159
|
+
const chip = document.createElement('button');
|
|
160
|
+
chip.type = 'button';
|
|
161
|
+
chip.dataset.mkRangePreset = '';
|
|
162
|
+
chip.dataset.mkIndex = String(index);
|
|
163
|
+
chip.textContent = preset.label;
|
|
164
|
+
presetsHost.append(chip);
|
|
165
|
+
});
|
|
166
|
+
const calendarHost = document.createElement('div');
|
|
167
|
+
calendarHost.className = 'mk-range-calendar';
|
|
168
|
+
if (presets.length) dialogElement.append(presetsHost);
|
|
169
|
+
dialogElement.append(calendarHost);
|
|
170
|
+
root.append(dialogElement);
|
|
171
|
+
made.push(dialogElement);
|
|
172
|
+
|
|
173
|
+
let hiddenStart: HTMLInputElement | null = null;
|
|
174
|
+
let hiddenEnd: HTMLInputElement | null = null;
|
|
175
|
+
if (options.name) {
|
|
176
|
+
const hiddenInput = (key: 'start' | 'end') => {
|
|
177
|
+
const input = document.createElement('input');
|
|
178
|
+
input.type = 'hidden';
|
|
179
|
+
input.name = `${options.name}[${key}]`;
|
|
180
|
+
root.append(input);
|
|
181
|
+
made.push(input);
|
|
182
|
+
return input;
|
|
183
|
+
};
|
|
184
|
+
hiddenStart = hiddenInput('start');
|
|
185
|
+
hiddenEnd = hiddenInput('end');
|
|
186
|
+
}
|
|
187
|
+
scope.add(() => made.forEach((node) => node.remove()));
|
|
188
|
+
// Listeners on the picker hear the picker, not the calendar inside its popover.
|
|
189
|
+
scope.on(dialogElement, 'mk:change', (event) => event.stopPropagation());
|
|
190
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
191
|
+
|
|
192
|
+
const showError = (code: keyof typeof ERRORS | null) => {
|
|
193
|
+
error.textContent = code ? ERRORS[code] : '';
|
|
194
|
+
error.hidden = !code;
|
|
195
|
+
if (code) field.setAttribute('aria-invalid', 'true');
|
|
196
|
+
else field.removeAttribute('aria-invalid');
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const check = (range: DateRange): keyof typeof ERRORS | null => {
|
|
200
|
+
if (range.end < range.start) return 'order';
|
|
201
|
+
const bounds = { min: rules.min, max: rules.max };
|
|
202
|
+
if (!isPickable(range.start, bounds) || !isPickable(range.end, bounds)) return 'bounds';
|
|
203
|
+
if (!rangeIsPickable(range, rules)) return 'blocked';
|
|
204
|
+
return null;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const writeHidden = () => {
|
|
208
|
+
if (hiddenStart) hiddenStart.value = value ? dayKey(value.start) : '';
|
|
209
|
+
if (hiddenEnd) hiddenEnd.value = value ? dayKey(value.end) : '';
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
let calendarReady = false;
|
|
213
|
+
const commit = (range: DateRange | null, fill: boolean) => {
|
|
214
|
+
const same = range && value ? dayKey(range.start) === dayKey(value.start) && dayKey(range.end) === dayKey(value.end) : range === value;
|
|
215
|
+
value = range;
|
|
216
|
+
if (fill) {
|
|
217
|
+
startField.setValues(segmentsOf(range?.start ?? null), true);
|
|
218
|
+
endField.setValues(segmentsOf(range?.end ?? null), true);
|
|
219
|
+
showError(null);
|
|
220
|
+
}
|
|
221
|
+
writeHidden();
|
|
222
|
+
if (same) return;
|
|
223
|
+
if (calendarReady) calendar.setValue(range ? rangeKeys(range) : null, false);
|
|
224
|
+
options.onChange?.(range ? { ...range } : null);
|
|
225
|
+
root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: range ? { ...rangeKeys(range), length: rangeLength(range) } : null }));
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Typing: validate as soon as both dates are complete; report only a new valid range.
|
|
229
|
+
const typed = () => {
|
|
230
|
+
const start = dateOf(startField.values());
|
|
231
|
+
const end = dateOf(endField.values());
|
|
232
|
+
if (!start || !end) return showError(null);
|
|
233
|
+
const range = { start, end };
|
|
234
|
+
const code = check(range);
|
|
235
|
+
showError(code);
|
|
236
|
+
if (code) return;
|
|
237
|
+
commit(range, false);
|
|
238
|
+
};
|
|
239
|
+
// The shake waits until focus leaves the field, so it never fires mid-typing.
|
|
240
|
+
scope.on(field, 'focusout', (event) => {
|
|
241
|
+
if (!field.contains((event as FocusEvent).relatedTarget as Node | null) && field.hasAttribute('aria-invalid')) keep(shake(field));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const wide = typeof matchMedia === 'function' && matchMedia('(min-width: 680px)').matches;
|
|
245
|
+
const calendar = createRangeCalendar(calendarHost, {
|
|
246
|
+
value: value ? rangeKeys(value) : null,
|
|
247
|
+
visibleMonths: wide ? 2 : 1,
|
|
248
|
+
firstDay: options.firstDay,
|
|
249
|
+
min: options.min,
|
|
250
|
+
max: options.max,
|
|
251
|
+
unavailable: options.unavailable,
|
|
252
|
+
locale,
|
|
253
|
+
today: options.today,
|
|
254
|
+
onChange: (range) => {
|
|
255
|
+
commit(range, true);
|
|
256
|
+
cancelClose();
|
|
257
|
+
if (reduced()) return popover.close();
|
|
258
|
+
// The band draws and the end day pops; the range stays in view a moment, then the panel folds back.
|
|
259
|
+
closing = setTimeout(() => {
|
|
260
|
+
closing = undefined;
|
|
261
|
+
popover.close();
|
|
262
|
+
}, (bandDuration(rangeLength(range)) + SETTLE) * 1000);
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
calendarReady = true;
|
|
266
|
+
scope.add(() => calendar.destroy());
|
|
267
|
+
|
|
268
|
+
let closing: ReturnType<typeof setTimeout> | undefined;
|
|
269
|
+
const cancelClose = () => {
|
|
270
|
+
clearTimeout(closing);
|
|
271
|
+
closing = undefined;
|
|
272
|
+
};
|
|
273
|
+
scope.add(cancelClose);
|
|
274
|
+
// Touching the calendar again while a picked range settles keeps the popover open (capture: before a pick).
|
|
275
|
+
scope.on(dialogElement, 'pointerdown', cancelClose, true);
|
|
276
|
+
scope.on(dialogElement, 'keydown', cancelClose, true);
|
|
277
|
+
const startDay = () => value?.start ?? options.today ?? new Date();
|
|
278
|
+
|
|
279
|
+
const popover = createDialog(dialogElement, {
|
|
280
|
+
placement: 'anchor',
|
|
281
|
+
backdrop: false,
|
|
282
|
+
drag: false,
|
|
283
|
+
autoResize: false,
|
|
284
|
+
triggers: false,
|
|
285
|
+
animateContent: false,
|
|
286
|
+
onOpen: () => {
|
|
287
|
+
calendar.focusDay(startDay(), false);
|
|
288
|
+
// Like a menu's options: the presets, the header and the months flow in while the surface grows.
|
|
289
|
+
keep(flowInMenu([...dialogElement.querySelectorAll<HTMLElement>('[data-mk-range-presets], [data-mk-range-header], [data-mk-range-month]')]));
|
|
290
|
+
},
|
|
291
|
+
onClose: () => {
|
|
292
|
+
cancelClose();
|
|
293
|
+
button.setAttribute('aria-expanded', 'false');
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
scope.add(() => popover.destroy());
|
|
297
|
+
|
|
298
|
+
const open = () => {
|
|
299
|
+
if (options.disabled || popover.isOpen) return;
|
|
300
|
+
// The calendar shows the range (or today) before the panel is measured and placed; a half-picked range is dropped.
|
|
301
|
+
calendar.setValue(value ? rangeKeys(value) : null, false);
|
|
302
|
+
calendar.focusDay(startDay(), false);
|
|
303
|
+
popover.open(button);
|
|
304
|
+
button.setAttribute('aria-expanded', 'true');
|
|
305
|
+
};
|
|
306
|
+
scope.on(button, 'click', () => {
|
|
307
|
+
keep(press(button));
|
|
308
|
+
if (popover.isOpen) popover.close();
|
|
309
|
+
else open();
|
|
310
|
+
});
|
|
311
|
+
// Presets: commit, show it in the calendar, close.
|
|
312
|
+
scope.on(presetsHost, 'click', (event) => {
|
|
313
|
+
const chip = (event.target as Element | null)?.closest<HTMLButtonElement>('[data-mk-range-preset]');
|
|
314
|
+
if (!chip) return;
|
|
315
|
+
const chosen = toRange(presets[Number(chip.dataset.mkIndex)]);
|
|
316
|
+
if (!chosen) return;
|
|
317
|
+
keep(press(chip));
|
|
318
|
+
commit(chosen, true);
|
|
319
|
+
popover.close();
|
|
320
|
+
});
|
|
321
|
+
scope.add(() => {
|
|
322
|
+
for (const effect of effects) effect.kill();
|
|
323
|
+
effects.clear();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
startField.setValues(segmentsOf(value?.start ?? null));
|
|
327
|
+
endField.setValues(segmentsOf(value?.end ?? null));
|
|
328
|
+
writeHidden();
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
open,
|
|
332
|
+
close: () => popover.close(),
|
|
333
|
+
setValue(next) {
|
|
334
|
+
if (scope.disposed) return;
|
|
335
|
+
const range = toRange(next);
|
|
336
|
+
const same = range && value ? dayKey(range.start) === dayKey(value.start) && dayKey(range.end) === dayKey(value.end) : range === value;
|
|
337
|
+
value = range;
|
|
338
|
+
startField.setValues(segmentsOf(range?.start ?? null), true);
|
|
339
|
+
endField.setValues(segmentsOf(range?.end ?? null), true);
|
|
340
|
+
showError(null);
|
|
341
|
+
writeHidden();
|
|
342
|
+
if (!same) calendar.setValue(range ? rangeKeys(range) : null, false);
|
|
343
|
+
},
|
|
344
|
+
get value() {
|
|
345
|
+
return value ? { ...value } : null;
|
|
346
|
+
},
|
|
347
|
+
get isOpen() {
|
|
348
|
+
return popover.isOpen;
|
|
349
|
+
},
|
|
350
|
+
destroy: () => scope.dispose(),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { rangeKeys, toRange } from '../../core/date-range';
|
|
5
|
+
import type { DateInput } from '../../core/dates';
|
|
6
|
+
import { createDateRangePicker, type DateRangePickerInstance, type DateRangePickerOptions } from './engine';
|
|
7
|
+
|
|
8
|
+
type RangeValue = { start: DateInput; end: DateInput } | null;
|
|
9
|
+
|
|
10
|
+
export interface DateRangePickerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'>, Omit<DateRangePickerOptions, 'value'> {
|
|
11
|
+
/** Controlled range; without it the picker keeps its own (defaultValue). */
|
|
12
|
+
value?: RangeValue;
|
|
13
|
+
defaultValue?: RangeValue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const keyOf = (value: RangeValue | undefined) => {
|
|
17
|
+
const range = toRange(value);
|
|
18
|
+
return range ? JSON.stringify(rangeKeys(range)) : 'null';
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** <DateRangePicker value={range} onChange={setRange} presets={[…]} />. */
|
|
22
|
+
export function DateRangePicker({ value, defaultValue, min, max, unavailable, firstDay, locale, today, label, name, presets, disabled, onChange, className, ...props }: DateRangePickerProps) {
|
|
23
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
24
|
+
const instance = useRef<DateRangePickerInstance | null>(null);
|
|
25
|
+
const change = useLatest(onChange);
|
|
26
|
+
const test = useLatest(unavailable);
|
|
27
|
+
const start = useRef<RangeValue>(value !== undefined ? value : (defaultValue ?? null));
|
|
28
|
+
const rules = typeof unavailable === 'function' ? 'function' : JSON.stringify(unavailable ?? null);
|
|
29
|
+
const presetKey = JSON.stringify(presets ?? null);
|
|
30
|
+
useIsomorphicLayoutEffect(() => {
|
|
31
|
+
const created = createDateRangePicker(ref.current, {
|
|
32
|
+
value: value !== undefined ? value : start.current,
|
|
33
|
+
min,
|
|
34
|
+
max,
|
|
35
|
+
unavailable: typeof unavailable === 'function' ? (day) => (typeof test.current === 'function' ? test.current(day) : false) : unavailable,
|
|
36
|
+
firstDay,
|
|
37
|
+
locale,
|
|
38
|
+
today,
|
|
39
|
+
label,
|
|
40
|
+
name,
|
|
41
|
+
presets,
|
|
42
|
+
disabled,
|
|
43
|
+
onChange: (range) => change.current?.(range),
|
|
44
|
+
});
|
|
45
|
+
instance.current = created;
|
|
46
|
+
return () => {
|
|
47
|
+
start.current = created.value;
|
|
48
|
+
created.destroy();
|
|
49
|
+
instance.current = null;
|
|
50
|
+
};
|
|
51
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
52
|
+
}, [String(min ?? ''), String(max ?? ''), rules, firstDay, locale, String(today ?? ''), label, name, presetKey, disabled]);
|
|
53
|
+
useIsomorphicLayoutEffect(() => {
|
|
54
|
+
if (value !== undefined && instance.current && keyOf(value) !== keyOf(instance.current.value)) instance.current.setValue(value);
|
|
55
|
+
}, [keyOf(value)]);
|
|
56
|
+
return <div {...props} ref={ref} className={['mk-date-range-picker', className].filter(Boolean).join(' ')} />;
|
|
57
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# date-time-picker
|
|
2
|
+
|
|
3
|
+
Fecha y hora en un solo control: un campo por segmentos en el orden y con la unión del idioma («23/11/2026, 13:05» en español; 12 h con AM/PM donde el idioma lo usa). El botón del calendario hace crecer un panel desde sí mismo (un diálogo anclado, como `menu`), y su contenido entra en cascada como las opciones de un menú:
|
|
4
|
+
- un calendario de un solo día (`range-calendar` con `selection: 'single'`);
|
|
5
|
+
- un campo de hora (`time-field`);
|
|
6
|
+
- los botones «Ahora» y «Listo».
|
|
7
|
+
|
|
8
|
+
Elegir un día conserva la hora; cambiar la hora conserva el día; cada cambio hace rodar los dígitos del campo. Al escribir una fecha y hora completas se comprueban `min`, `max` y los días no disponibles: fuera de rango marca el campo, dice por qué debajo y lo sacude una vez al salir. `onChange` informa de los valores válidos (y de `null` si se vacía el campo).
|
|
9
|
+
|
|
10
|
+
Importa también `css/components/dialog.css`, `range-calendar.css` y `time-field.css` (o el CSS completo). La CLI los copia: la entrada `requires` dialog, range-calendar y time-field.
|
|
11
|
+
|
|
12
|
+
## HTML / Astro
|
|
13
|
+
|
|
14
|
+
```html
|
|
15
|
+
<mk-date-time-picker class="mk-date-time-picker" value="2026-11-23T10:30" minute-step="15"
|
|
16
|
+
min="2026-11-10T08:00" name="cita"></mk-date-time-picker>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## React
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<DateTimePicker value={when} onChange={setWhen} minuteStep={15} unavailable={(day) => day.getDay() === 0} />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Opciones
|
|
26
|
+
|
|
27
|
+
| Opción | Atributo | Por defecto | Qué hace |
|
|
28
|
+
|---|---|---|---|
|
|
29
|
+
| `value` | `value` | — | Date o `'AAAA-MM-DDTHH:MM'` (hora local). |
|
|
30
|
+
| `min`, `max` | `min`, `max` | — | Límites (con hora). |
|
|
31
|
+
| `locale` | `locale` | `lang` de la página, o `es` | Orden de los segmentos y nombres. |
|
|
32
|
+
| `hourCycle` | `hour-cycle` | el del idioma | `12` o `24`. |
|
|
33
|
+
| `minuteStep` | `minute-step` | `5` | Paso de los minutos con las flechas. |
|
|
34
|
+
| `firstDay` | `first-day` | `1` (lunes) | Primer día de la semana. |
|
|
35
|
+
| `unavailable` | `unavailable` (lista) | — | Días `'AAAA-MM-DD'` o una función. |
|
|
36
|
+
| `today` | `today` | hoy | Para demos y pruebas. |
|
|
37
|
+
| `label` | `label` | `'Fecha y hora'` | Nombre accesible del campo. |
|
|
38
|
+
| `name` | `name` | — | Campo oculto con `'AAAA-MM-DDTHH:MM'`. |
|
|
39
|
+
| `disabled` | `disabled` | `false` | Desactivado. |
|
|
40
|
+
| `onChange(value)` | — | — | Date válido o `null`. |
|
|
41
|
+
|
|
42
|
+
## API y eventos
|
|
43
|
+
|
|
44
|
+
`open()`, `close()`, `setValue(valor | null)`, `value`, `isOpen`, `destroy()`. Evento `mk:change` con `{ value: 'AAAA-MM-DDTHH:MM' | null }`. Utilidades `toDateTimeValue(input)` y `dateTimeKey(date)`.
|
|
45
|
+
|
|
46
|
+
## Teclado
|
|
47
|
+
|
|
48
|
+
En el campo, el de `time-field`: las flechas cambian el segmento, los dígitos lo rellenan y pasan al siguiente, Retroceso borra y vuelve. En el panel, el de `range-calendar` y `time-field`; Escape o «Listo» lo cierran y el foco vuelve al botón. Con movimiento reducido el panel aparece sin crecer y no hay rodillo, sacudida ni píldora.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/* date-time-picker — one 12 px field on surface-3 holding the date and time segments and an 8 px calendar button;
|
|
2
|
+
the focused segment sits on a 6 px accent-soft pill; invalid draws an error ring and a message under it. The popover
|
|
3
|
+
(an anchored dialog: needs dialog.css, range-calendar.css and time-field.css) has 20 px corners. */
|
|
4
|
+
.mk-date-time-picker,
|
|
5
|
+
mk-date-time-picker {
|
|
6
|
+
display: inline-block;
|
|
7
|
+
max-width: 100%;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
[data-mk-dt-field] {
|
|
11
|
+
display: inline-flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
gap: 0.35rem;
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
max-width: 100%;
|
|
16
|
+
min-height: 2.75rem;
|
|
17
|
+
padding: 0.25rem 0.25rem 0.25rem 0.75rem;
|
|
18
|
+
border-radius: 12px;
|
|
19
|
+
background: var(--mk-surface-3);
|
|
20
|
+
color: var(--mk-text);
|
|
21
|
+
font-variant-numeric: tabular-nums;
|
|
22
|
+
transition: box-shadow 0.2s ease;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[data-mk-dt-field]:focus-within {
|
|
26
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--mk-accent) 35%, transparent);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[data-mk-dt-field]:has([aria-invalid='true']) {
|
|
30
|
+
box-shadow: 0 0 0 2px var(--mk-error, #c62828);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
[data-mk-dt-field] [data-mk-segments] {
|
|
34
|
+
position: relative;
|
|
35
|
+
display: inline-flex;
|
|
36
|
+
flex-wrap: wrap;
|
|
37
|
+
align-items: center;
|
|
38
|
+
min-width: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
[data-mk-dt-field] [data-mk-segment] {
|
|
42
|
+
position: relative;
|
|
43
|
+
z-index: 1;
|
|
44
|
+
padding: 0.2rem 0.15rem;
|
|
45
|
+
border-radius: 6px;
|
|
46
|
+
caret-color: transparent;
|
|
47
|
+
outline: none;
|
|
48
|
+
cursor: default;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
[data-mk-dt-field] [data-mk-segment][data-mk-empty] {
|
|
52
|
+
color: var(--mk-muted);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
[data-mk-dt-field] [data-mk-segment]:focus {
|
|
56
|
+
color: var(--mk-on-accent-soft, var(--mk-text));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
[data-mk-dt-field] [data-mk-segment]::selection {
|
|
60
|
+
background: transparent;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
[data-mk-dt-field] [data-mk-segment-literal] {
|
|
64
|
+
padding: 0 0.05rem;
|
|
65
|
+
color: var(--mk-muted);
|
|
66
|
+
white-space: pre;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
[data-mk-dt-field] [data-mk-segment-focus] {
|
|
70
|
+
position: absolute;
|
|
71
|
+
top: 50%;
|
|
72
|
+
left: 0;
|
|
73
|
+
height: 1.75rem;
|
|
74
|
+
margin-top: -0.875rem;
|
|
75
|
+
border-radius: 6px;
|
|
76
|
+
background: var(--mk-accent-soft);
|
|
77
|
+
opacity: 0;
|
|
78
|
+
pointer-events: none;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[data-mk-dt-button] {
|
|
82
|
+
display: grid;
|
|
83
|
+
flex: none;
|
|
84
|
+
place-items: center;
|
|
85
|
+
width: 2.25rem;
|
|
86
|
+
height: 2.25rem;
|
|
87
|
+
margin-left: auto;
|
|
88
|
+
padding: 0;
|
|
89
|
+
border: 0;
|
|
90
|
+
border-radius: 8px;
|
|
91
|
+
background: var(--mk-surface-1);
|
|
92
|
+
color: inherit;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
[data-mk-dt-button]:focus-visible {
|
|
97
|
+
outline: 3px solid color-mix(in srgb, var(--mk-accent) 45%, transparent);
|
|
98
|
+
outline-offset: 2px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
[data-mk-dt-error] {
|
|
102
|
+
margin: 0.4rem 0 0;
|
|
103
|
+
color: var(--mk-error, #c62828);
|
|
104
|
+
font-size: 0.8125rem;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
[data-mk-dt-error][hidden] {
|
|
108
|
+
display: none;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Classes, not data attributes: the copy that folds back into the button on close keeps only the classes. */
|
|
112
|
+
.mk-dialog.mk-dt-popover {
|
|
113
|
+
--mk-dialog-radius: 20px;
|
|
114
|
+
--mk-dialog-bg: var(--mk-surface-1);
|
|
115
|
+
width: max-content;
|
|
116
|
+
max-width: calc(100vw - 16px);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.mk-dialog.mk-dt-popover > .mk-range-calendar {
|
|
120
|
+
display: grid;
|
|
121
|
+
box-sizing: border-box;
|
|
122
|
+
max-width: 100%;
|
|
123
|
+
padding-bottom: 0.25rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
[data-mk-dt-time-row] {
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
justify-content: space-between;
|
|
130
|
+
gap: 0.75rem;
|
|
131
|
+
padding: 0.25rem 0.75rem;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
[data-mk-dt-time-label] {
|
|
135
|
+
color: var(--mk-muted);
|
|
136
|
+
font-size: 0.875rem;
|
|
137
|
+
font-weight: 500;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
[data-mk-dt-footer] {
|
|
141
|
+
display: flex;
|
|
142
|
+
justify-content: flex-end;
|
|
143
|
+
gap: 0.5rem;
|
|
144
|
+
padding: 0.5rem 0.75rem 0.75rem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
[data-mk-dt-footer] button {
|
|
148
|
+
min-height: 2.25rem;
|
|
149
|
+
padding: 0 0.9rem;
|
|
150
|
+
border: 0;
|
|
151
|
+
border-radius: 8px;
|
|
152
|
+
background: var(--mk-surface-3);
|
|
153
|
+
color: var(--mk-text);
|
|
154
|
+
font: inherit;
|
|
155
|
+
font-size: 0.875rem;
|
|
156
|
+
font-weight: 500;
|
|
157
|
+
cursor: pointer;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
[data-mk-dt-footer] [data-mk-dt-done] {
|
|
161
|
+
background: var(--mk-accent);
|
|
162
|
+
color: var(--mk-on-accent);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@media (prefers-reduced-motion: reduce) {
|
|
166
|
+
[data-mk-dt-field] [data-mk-segment]:focus {
|
|
167
|
+
background: var(--mk-accent-soft);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
[data-mk-dt-field] [data-mk-segment-focus] {
|
|
171
|
+
display: none;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
[data-mk-dt-field] {
|
|
175
|
+
transition: none;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createDateTimePicker, type DateTimePickerInstance } from './engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* <mk-date-time-picker class="mk-date-time-picker" value="2026-11-23T10:30" minute-step="15" name="cita"></mk-date-time-picker>
|
|
6
|
+
* Emits mk:change ({value: 'YYYY-MM-DDTHH:MM' | null}).
|
|
7
|
+
*/
|
|
8
|
+
export class MkDateTimePickerElement extends MkElement<DateTimePickerInstance> {
|
|
9
|
+
static options: Record<string, OptionType> = {
|
|
10
|
+
value: 'string',
|
|
11
|
+
min: 'string',
|
|
12
|
+
max: 'string',
|
|
13
|
+
locale: 'string',
|
|
14
|
+
hourCycle: 'number',
|
|
15
|
+
minuteStep: 'number',
|
|
16
|
+
firstDay: 'number',
|
|
17
|
+
unavailable: 'list',
|
|
18
|
+
today: 'string',
|
|
19
|
+
label: 'string',
|
|
20
|
+
name: 'string',
|
|
21
|
+
disabled: 'boolean',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
protected create(options: Record<string, unknown>) {
|
|
25
|
+
return createDateTimePicker(this, options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
29
|
+
if (previous === next || !this.instance) return;
|
|
30
|
+
if (name === 'value') this.instance.setValue(next);
|
|
31
|
+
else super.attributeChangedCallback(name, previous, next);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
open() {
|
|
35
|
+
this.instance?.open();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
close() {
|
|
39
|
+
this.instance?.close();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get value() {
|
|
43
|
+
return this.instance?.value ?? null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const defineDateTimePicker = () => defineElement('mk-date-time-picker', MkDateTimePickerElement);
|