@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,79 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { forwardRef, useImperativeHandle, useRef, type HTMLAttributes, type ReactElement, type Ref } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import type { DataRow } from '../data-table/engine';
|
|
5
|
+
import { createEditableTable, type EditableTableInstance, type EditableTableOptions } from './engine';
|
|
6
|
+
|
|
7
|
+
export interface EditableTableProps<T extends DataRow> extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'children'>, Omit<EditableTableOptions<T>, 'rows'> {
|
|
8
|
+
rows: T[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type EditableTableHandle<T extends DataRow = DataRow> = Pick<EditableTableInstance<T>, 'edit' | 'setValue' | 'addRow' | 'removeRows' | 'save' | 'discard'>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* <EditableTable columns={columns} rows={rows} onSave={async (changes, rows) => { await api.save(changes); setRows(rows); }} />.
|
|
15
|
+
* New `rows` from the app replace the table's (pending edits go with them): hand them back after a save.
|
|
16
|
+
*/
|
|
17
|
+
function EditableTableInner<T extends DataRow>(
|
|
18
|
+
{ columns, rows, rowKey, caption, pageSize, pageSizes, selectable, search, filters, columnsMenu, actions, bulkActions, sort, emptyTitle, emptyDescription, locale, onSave, onChange, newRow, onAction, onBulkAction, onSelectionChange, className, ...props }: EditableTableProps<T>,
|
|
19
|
+
handle: Ref<EditableTableHandle<T>>,
|
|
20
|
+
) {
|
|
21
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
22
|
+
const instance = useRef<EditableTableInstance<T> | null>(null);
|
|
23
|
+
const shown = useRef(rows);
|
|
24
|
+
const callbacks = useLatest({ onSave, onChange, newRow, onAction, onBulkAction, onSelectionChange });
|
|
25
|
+
useImperativeHandle(
|
|
26
|
+
handle,
|
|
27
|
+
() => ({
|
|
28
|
+
edit: (id, key) => instance.current?.edit(id, key),
|
|
29
|
+
setValue: (id, key, value) => instance.current?.setValue(id, key, value) ?? false,
|
|
30
|
+
addRow: (values) => instance.current?.addRow(values) ?? '',
|
|
31
|
+
removeRows: (ids) => instance.current?.removeRows(ids),
|
|
32
|
+
save: () => instance.current?.save() ?? Promise.resolve(false),
|
|
33
|
+
discard: () => instance.current?.discard(),
|
|
34
|
+
}),
|
|
35
|
+
[],
|
|
36
|
+
);
|
|
37
|
+
const columnsKey = JSON.stringify((columns ?? []).map(({ key, label, type, editable, editor, required, min, max, hidden, options: choices }) => [key, label, type, editable, editor, required, min, max, hidden, choices]));
|
|
38
|
+
const setupKey = JSON.stringify([columnsKey, rowKey, caption, pageSize, pageSizes, selectable, search, filters, columnsMenu, actions, bulkActions, emptyTitle, emptyDescription, locale, Boolean(newRow)]);
|
|
39
|
+
useIsomorphicLayoutEffect(() => {
|
|
40
|
+
const created = createEditableTable<T>(ref.current, {
|
|
41
|
+
columns,
|
|
42
|
+
rows: shown.current,
|
|
43
|
+
rowKey,
|
|
44
|
+
caption,
|
|
45
|
+
pageSize,
|
|
46
|
+
pageSizes,
|
|
47
|
+
selectable,
|
|
48
|
+
search,
|
|
49
|
+
filters,
|
|
50
|
+
columnsMenu,
|
|
51
|
+
actions,
|
|
52
|
+
bulkActions,
|
|
53
|
+
sort,
|
|
54
|
+
emptyTitle,
|
|
55
|
+
emptyDescription,
|
|
56
|
+
locale,
|
|
57
|
+
onSave: (changes, next) => callbacks.current.onSave?.(changes, next),
|
|
58
|
+
onChange: (changes) => callbacks.current.onChange?.(changes),
|
|
59
|
+
newRow: newRow ? () => callbacks.current.newRow?.() ?? {} : undefined,
|
|
60
|
+
onAction: (action, row) => callbacks.current.onAction?.(action, row),
|
|
61
|
+
onBulkAction: (action, chosen) => callbacks.current.onBulkAction?.(action, chosen),
|
|
62
|
+
onSelectionChange: (ids, chosen) => callbacks.current.onSelectionChange?.(ids, chosen),
|
|
63
|
+
});
|
|
64
|
+
instance.current = created;
|
|
65
|
+
return () => {
|
|
66
|
+
created.destroy();
|
|
67
|
+
instance.current = null;
|
|
68
|
+
};
|
|
69
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
70
|
+
}, [setupKey]);
|
|
71
|
+
useIsomorphicLayoutEffect(() => {
|
|
72
|
+
if (shown.current === rows) return;
|
|
73
|
+
shown.current = rows;
|
|
74
|
+
instance.current?.setRows(rows);
|
|
75
|
+
}, [rows]);
|
|
76
|
+
return <div {...props} ref={ref} className={['mk-data-table', 'mk-editable-table', className].filter(Boolean).join(' ')} />;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const EditableTable = forwardRef(EditableTableInner) as <T extends DataRow>(props: EditableTableProps<T> & { ref?: Ref<EditableTableHandle<T>> }) => ReactElement;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# empty-state
|
|
2
|
+
|
|
3
|
+
Estado vacío: ilustración, título, una línea de ayuda y acciones, para un sitio que aún no tiene nada (sin resultados, bandeja vacía, error, tarea terminada). Las ilustraciones son SVG en línea con los colores del tema.
|
|
4
|
+
|
|
5
|
+
Cuando entra en pantalla, la ilustración aparece con un pop (escala .85, blur 12 → 0, .6 s) y el texto y las acciones entran después (12 px y blur 8, 60 ms entre piezas). Luego queda un movimiento suave en reposo: el sobre flota, la lupa da vueltas, la hoja asoma de la carpeta, el aviso respira y los destellos parpadean. Se pausa cuando sale de pantalla. `update()` cambia el contenido con un fundido con blur.
|
|
6
|
+
|
|
7
|
+
## HTML / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<mk-empty-state class="mk-empty-state" illustration="search" title="Sin resultados"
|
|
11
|
+
description="Prueba con otras palabras o quita algún filtro."
|
|
12
|
+
actions='[{"id":"clear","label":"Quitar filtros"},{"id":"new","label":"Crear tarea"}]'></mk-empty-state>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Sin `title` ni `description`, conserva el contenido que escribas dentro y solo añade la ilustración delante.
|
|
16
|
+
|
|
17
|
+
## React
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<EmptyState illustration="inbox" title="Bandeja vacía" description="Aquí verás los mensajes nuevos."
|
|
21
|
+
actions={[{ id: 'compose', label: 'Escribir mensaje' }]} onAction={(id) => id === 'compose' && openComposer()} />
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Opciones
|
|
25
|
+
|
|
26
|
+
| Opción | Atributo | Por defecto | Qué hace |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| `illustration` | `illustration` | `'inbox'` | `inbox`, `search`, `folder`, `error`, `success` o `none`. |
|
|
29
|
+
| `title` | `title` | — | Título (h3). |
|
|
30
|
+
| `description` | `description` | — | Texto de ayuda. |
|
|
31
|
+
| `actions` | `actions` (JSON) | — | `{ id, label, variant?, href? }[]`; la primera es primaria y las demás tonales. |
|
|
32
|
+
| `loop` | `loop` | `true` | Movimiento suave en reposo. |
|
|
33
|
+
| `onAction(id, event)` | — | — | Al pulsar una acción. |
|
|
34
|
+
|
|
35
|
+
## API y eventos
|
|
36
|
+
|
|
37
|
+
`update({ illustration, title, description, actions })`, `destroy()`. Evento `mk:action` con `{ id }`.
|
|
38
|
+
|
|
39
|
+
## Estilo
|
|
40
|
+
|
|
41
|
+
Columna centrada de 28rem como máximo. Variables para recolorear la ilustración: `--mk-empty-ink`, `--mk-empty-fill`, `--mk-empty-card`, `--mk-empty-accent`, `--mk-empty-soft`, `--mk-empty-alert`. Con movimiento reducido no hay entrada ni movimiento en reposo.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createEmptyState, type EmptyStateInstance } from './engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* <mk-empty-state class="mk-empty-state" illustration="search" title="Sin resultados"
|
|
6
|
+
* description="Prueba con otras palabras." actions='[{"id":"clear","label":"Borrar filtros"}]'></mk-empty-state>
|
|
7
|
+
* Emits mk:action ({id}).
|
|
8
|
+
*/
|
|
9
|
+
export class MkEmptyStateElement extends MkElement<EmptyStateInstance> {
|
|
10
|
+
static options: Record<string, OptionType> = { illustration: 'string', title: 'string', description: 'string', actions: 'json', loop: 'boolean' };
|
|
11
|
+
|
|
12
|
+
protected create(options: Record<string, unknown>) {
|
|
13
|
+
return createEmptyState(this, options);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
17
|
+
if (previous === next || !this.instance) return;
|
|
18
|
+
if (name === 'loop') return super.attributeChangedCallback(name, previous, next);
|
|
19
|
+
this.instance.update(this.options);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const defineEmptyState = () => defineElement('mk-empty-state', MkEmptyStateElement);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* empty-state — centred column: a 160 × 120 illustration on the theme's colours, a 1.125rem title, a muted line of
|
|
2
|
+
help (36ch) and the actions (kit buttons). The illustration's colours are variables, so a theme or a card can
|
|
3
|
+
recolour it. */
|
|
4
|
+
.mk-empty-state,
|
|
5
|
+
mk-empty-state {
|
|
6
|
+
--mk-empty-ink: var(--mk-muted);
|
|
7
|
+
--mk-empty-fill: var(--mk-surface-3);
|
|
8
|
+
--mk-empty-card: var(--mk-surface-1);
|
|
9
|
+
--mk-empty-accent: var(--mk-accent);
|
|
10
|
+
--mk-empty-soft: var(--mk-accent-soft);
|
|
11
|
+
--mk-empty-shadow: color-mix(in srgb, var(--mk-text) 9%, transparent);
|
|
12
|
+
--mk-empty-fill-ink: color-mix(in srgb, var(--mk-muted) 35%, transparent);
|
|
13
|
+
--mk-empty-alert: var(--mk-error);
|
|
14
|
+
--mk-empty-alert-soft: var(--mk-error-soft);
|
|
15
|
+
display: grid;
|
|
16
|
+
justify-items: center;
|
|
17
|
+
gap: 0.75rem;
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
max-width: 28rem;
|
|
20
|
+
margin-inline: auto;
|
|
21
|
+
padding: 2rem 1.25rem;
|
|
22
|
+
text-align: center;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[data-mk-empty-illustration] svg {
|
|
26
|
+
display: block;
|
|
27
|
+
width: min(160px, 50vw);
|
|
28
|
+
height: auto;
|
|
29
|
+
overflow: visible;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[data-mk-empty-title] {
|
|
33
|
+
margin: 0.25rem 0 0;
|
|
34
|
+
color: var(--mk-text);
|
|
35
|
+
font-size: 1.125rem;
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
line-height: 1.3;
|
|
38
|
+
text-wrap: balance;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
[data-mk-empty-description] {
|
|
42
|
+
max-width: 36ch;
|
|
43
|
+
margin: 0;
|
|
44
|
+
color: var(--mk-muted);
|
|
45
|
+
font-size: 0.9375rem;
|
|
46
|
+
line-height: 1.5;
|
|
47
|
+
text-wrap: pretty;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[data-mk-empty-actions] {
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-wrap: wrap;
|
|
53
|
+
justify-content: center;
|
|
54
|
+
gap: 0.5rem;
|
|
55
|
+
margin-top: 0.25rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
:is(.mk-empty-state, mk-empty-state) [hidden] {
|
|
59
|
+
display: none;
|
|
60
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { gsap, EASE, 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 { press } from '../../motion/micro';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Empty state: an illustration, a title, a line of help and actions for a
|
|
9
|
+
* place with nothing in it yet (no results, an empty inbox, an error). The
|
|
10
|
+
* illustrations are inline SVG on the theme's colours. When it scrolls into
|
|
11
|
+
* view the illustration pops in (scale .85, blur 12 → 0, .6 s, pop curve) and
|
|
12
|
+
* the text and actions flow in after it (12 px, blur 8, 60 ms apart); then a
|
|
13
|
+
* gentle idle motion runs (the envelope floats, the magnifier circles, the page
|
|
14
|
+
* peeks out of the folder, the warning breathes, the sparkles twinkle), paused
|
|
15
|
+
* while it is off screen. update() swaps the content with a blur cross-fade.
|
|
16
|
+
* Authored content is kept; the illustration is added before it.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type EmptyIllustration = 'inbox' | 'search' | 'folder' | 'error' | 'success' | 'none';
|
|
20
|
+
|
|
21
|
+
export interface EmptyAction {
|
|
22
|
+
id: string;
|
|
23
|
+
label: string;
|
|
24
|
+
/** primary (default for the first) or secondary. */
|
|
25
|
+
variant?: 'primary' | 'secondary';
|
|
26
|
+
/** A link instead of a button. */
|
|
27
|
+
href?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EmptyStateOptions {
|
|
31
|
+
illustration?: EmptyIllustration;
|
|
32
|
+
title?: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
actions?: EmptyAction[];
|
|
35
|
+
/** The illustration's idle motion (default true). */
|
|
36
|
+
loop?: boolean;
|
|
37
|
+
onAction?: (id: string, event: MouseEvent) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface EmptyStateInstance extends Instance {
|
|
41
|
+
update(next: Pick<EmptyStateOptions, 'illustration' | 'title' | 'description' | 'actions'>): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const INK = 'stroke="var(--mk-empty-ink)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"';
|
|
45
|
+
|
|
46
|
+
const ART: Record<Exclude<EmptyIllustration, 'none'>, string> = {
|
|
47
|
+
inbox: `<ellipse data-mk-part="shadow" cx="80" cy="108" rx="40" ry="5" fill="var(--mk-empty-shadow)"/>
|
|
48
|
+
<path d="M36 66h22l6 10h32l6-10h22v24a8 8 0 0 1-8 8H44a8 8 0 0 1-8-8z" fill="var(--mk-empty-fill)" ${INK}/>
|
|
49
|
+
<g data-mk-part="float"><rect x="55" y="22" width="50" height="36" rx="7" fill="var(--mk-empty-card)" ${INK}/><path d="M58 27l22 16 22-16" fill="none" stroke="var(--mk-empty-accent)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></g>
|
|
50
|
+
<g data-mk-part="twinkle" fill="var(--mk-empty-accent)"><path d="M122 20l2 5 5 2-5 2-2 5-2-5-5-2 5-2z"/><circle cx="38" cy="40" r="3"/><circle cx="128" cy="52" r="2"/></g>`,
|
|
51
|
+
search: `<ellipse data-mk-part="shadow" cx="80" cy="108" rx="40" ry="5" fill="var(--mk-empty-shadow)"/>
|
|
52
|
+
<rect x="44" y="18" width="62" height="80" rx="9" fill="var(--mk-empty-card)" ${INK}/>
|
|
53
|
+
<path d="M56 36h38M56 48h30M56 60h34M56 72h22" fill="none" stroke="var(--mk-empty-fill-ink)" stroke-width="4" stroke-linecap="round"/>
|
|
54
|
+
<g data-mk-part="orbit"><circle cx="100" cy="66" r="15" fill="var(--mk-empty-soft)" ${INK}/><path d="M111 77l13 13" fill="none" ${INK} stroke-width="5"/></g>
|
|
55
|
+
<g data-mk-part="twinkle" fill="var(--mk-empty-accent)"><path d="M30 30l2 5 5 2-5 2-2 5-2-5-5-2 5-2z"/><circle cx="130" cy="34" r="2.5"/></g>`,
|
|
56
|
+
folder: `<ellipse data-mk-part="shadow" cx="80" cy="108" rx="44" ry="5" fill="var(--mk-empty-shadow)"/>
|
|
57
|
+
<path d="M34 40a8 8 0 0 1 8-8h22l8 8h46a8 8 0 0 1 8 8v42a8 8 0 0 1-8 8H42a8 8 0 0 1-8-8z" fill="var(--mk-empty-fill)" ${INK}/>
|
|
58
|
+
<g data-mk-part="peek"><rect x="52" y="30" width="56" height="44" rx="5" fill="var(--mk-empty-card)" ${INK}/><path d="M62 44h30M62 54h20" fill="none" stroke="var(--mk-empty-accent)" stroke-width="3" stroke-linecap="round"/></g>
|
|
59
|
+
<path d="M34 58h92v32a8 8 0 0 1-8 8H42a8 8 0 0 1-8-8z" fill="var(--mk-empty-soft)" ${INK}/>
|
|
60
|
+
<g data-mk-part="twinkle" fill="var(--mk-empty-accent)"><path d="M130 24l2 5 5 2-5 2-2 5-2-5-5-2 5-2z"/><circle cx="28" cy="30" r="2.5"/></g>`,
|
|
61
|
+
error: `<ellipse data-mk-part="shadow" cx="80" cy="108" rx="40" ry="5" fill="var(--mk-empty-shadow)"/>
|
|
62
|
+
<path d="M50 30h40l18 18v42a8 8 0 0 1-8 8H50a8 8 0 0 1-8-8V38a8 8 0 0 1 8-8z" fill="var(--mk-empty-card)" ${INK}/>
|
|
63
|
+
<path d="M90 30v12a6 6 0 0 0 6 6h12" fill="none" ${INK}/>
|
|
64
|
+
<g data-mk-part="pulse"><circle cx="76" cy="70" r="16" fill="var(--mk-empty-alert-soft)" stroke="var(--mk-empty-alert)" stroke-width="2.5"/><path d="M76 62v9M76 78v.5" fill="none" stroke="var(--mk-empty-alert)" stroke-width="3.5" stroke-linecap="round"/></g>`,
|
|
65
|
+
success: `<ellipse data-mk-part="shadow" cx="80" cy="108" rx="36" ry="5" fill="var(--mk-empty-shadow)"/>
|
|
66
|
+
<circle cx="80" cy="60" r="34" fill="var(--mk-empty-soft)" ${INK}/>
|
|
67
|
+
<path data-mk-part="draw" d="M64 61l11 11 22-24" fill="none" stroke="var(--mk-empty-accent)" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
68
|
+
<g data-mk-part="twinkle" fill="var(--mk-empty-accent)"><path d="M126 26l2 5 5 2-5 2-2 5-2-5-5-2 5-2z"/><circle cx="34" cy="34" r="3"/><circle cx="130" cy="84" r="2.5"/><path d="M30 80l1.5 3.5 3.5 1.5-3.5 1.5-1.5 3.5-1.5-3.5-3.5-1.5 3.5-1.5z"/></g>`,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** The idle motion of each moving part; repeat forever, back and forth. */
|
|
72
|
+
function idle(svg: SVGSVGElement) {
|
|
73
|
+
const timeline = gsap.timeline({ repeat: -1, yoyo: true, defaults: { ease: 'sine.inOut' } });
|
|
74
|
+
const part = (name: string) => svg.querySelector<SVGGElement>(`[data-mk-part="${name}"]`);
|
|
75
|
+
const float = part('float');
|
|
76
|
+
if (float) timeline.to(float, { y: -6, duration: 1.8 }, 0).to(part('shadow'), { scaleX: 0.86, transformOrigin: '50% 50%', duration: 1.8 }, 0);
|
|
77
|
+
const orbit = part('orbit');
|
|
78
|
+
if (orbit) timeline.to(orbit, { keyframes: { x: [0, -8, -4, 0], y: [0, -4, -10, 0] }, duration: 3.2, ease: 'sine.inOut' }, 0);
|
|
79
|
+
const peek = part('peek');
|
|
80
|
+
if (peek) timeline.to(peek, { y: -7, duration: 1.6 }, 0);
|
|
81
|
+
const pulse = part('pulse');
|
|
82
|
+
if (pulse) timeline.to(pulse, { scale: 1.07, transformOrigin: '50% 50%', duration: 1.1 }, 0);
|
|
83
|
+
const twinkle = part('twinkle');
|
|
84
|
+
if (twinkle) timeline.to(twinkle.children, { opacity: 0.25, scale: 0.7, transformOrigin: '50% 50%', duration: 0.9, stagger: 0.3 }, 0);
|
|
85
|
+
return timeline;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createEmptyState(root: HTMLElement | null, options: EmptyStateOptions = {}): EmptyStateInstance {
|
|
89
|
+
if (!root) return inert({ update() {} });
|
|
90
|
+
registerMotion();
|
|
91
|
+
const scope = createScope();
|
|
92
|
+
const effects = new Set<gsap.core.Animation>();
|
|
93
|
+
const keep = (animation: gsap.core.Animation | null) => {
|
|
94
|
+
if (!animation) return;
|
|
95
|
+
effects.add(animation);
|
|
96
|
+
// Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
|
|
97
|
+
for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
|
|
98
|
+
};
|
|
99
|
+
let loop: gsap.core.Timeline | null = null;
|
|
100
|
+
scope.add(() => {
|
|
101
|
+
loop?.kill();
|
|
102
|
+
for (const effect of effects) effect.kill();
|
|
103
|
+
effects.clear();
|
|
104
|
+
});
|
|
105
|
+
scope.attr(root, 'role', root.getAttribute('role') ?? 'status');
|
|
106
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
107
|
+
|
|
108
|
+
const authored = root.childNodes.length > 0 && !options.title && !options.description;
|
|
109
|
+
// The entrance writes inline styles: authored children get theirs back on destroy.
|
|
110
|
+
for (const child of root.children) scope.attr(child, 'style', child.getAttribute('style'));
|
|
111
|
+
const made: Element[] = [];
|
|
112
|
+
scope.add(() => made.forEach((node) => node.remove()));
|
|
113
|
+
|
|
114
|
+
const art = document.createElement('div');
|
|
115
|
+
art.dataset.mkEmptyIllustration = '';
|
|
116
|
+
art.setAttribute('aria-hidden', 'true');
|
|
117
|
+
root.prepend(art);
|
|
118
|
+
made.push(art);
|
|
119
|
+
const drawArt = (name: EmptyIllustration | undefined) => {
|
|
120
|
+
loop?.kill();
|
|
121
|
+
loop = null;
|
|
122
|
+
const kind = name ?? 'inbox';
|
|
123
|
+
art.hidden = kind === 'none';
|
|
124
|
+
if (kind === 'none') return art.replaceChildren();
|
|
125
|
+
art.innerHTML = `<svg viewBox="0 0 160 120" width="160" height="120">${ART[kind]}</svg>`;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Built content (or the authored one, kept as it is).
|
|
129
|
+
const title = document.createElement('h3');
|
|
130
|
+
title.dataset.mkEmptyTitle = '';
|
|
131
|
+
const description = document.createElement('p');
|
|
132
|
+
description.dataset.mkEmptyDescription = '';
|
|
133
|
+
const actions = document.createElement('div');
|
|
134
|
+
actions.dataset.mkEmptyActions = '';
|
|
135
|
+
const fill = (next: Pick<EmptyStateOptions, 'title' | 'description' | 'actions'>) => {
|
|
136
|
+
title.textContent = next.title ?? '';
|
|
137
|
+
title.hidden = !next.title;
|
|
138
|
+
description.textContent = next.description ?? '';
|
|
139
|
+
description.hidden = !next.description;
|
|
140
|
+
actions.replaceChildren(
|
|
141
|
+
...(next.actions ?? []).map((action, i) => {
|
|
142
|
+
const element = document.createElement(action.href ? 'a' : 'button');
|
|
143
|
+
if (element instanceof HTMLAnchorElement) element.href = action.href!;
|
|
144
|
+
else element.type = 'button';
|
|
145
|
+
const variant = action.variant ?? (i === 0 ? 'primary' : 'secondary');
|
|
146
|
+
element.className = `mk-button ${variant === 'primary' ? 'mk-button--primary' : 'mk-button--tonal'}`;
|
|
147
|
+
element.dataset.mkAction = action.id;
|
|
148
|
+
element.textContent = action.label;
|
|
149
|
+
return element;
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
actions.hidden = !next.actions?.length;
|
|
153
|
+
};
|
|
154
|
+
if (!authored) {
|
|
155
|
+
root.append(title, description, actions);
|
|
156
|
+
made.push(title, description, actions);
|
|
157
|
+
fill(options);
|
|
158
|
+
}
|
|
159
|
+
drawArt(options.illustration);
|
|
160
|
+
|
|
161
|
+
const parts = () => [...root.children].filter((child) => child !== art && !(child as HTMLElement).hidden) as HTMLElement[];
|
|
162
|
+
|
|
163
|
+
const startLoop = () => {
|
|
164
|
+
const svg = art.querySelector('svg');
|
|
165
|
+
if (!svg || options.loop === false || reduced() || loop) return;
|
|
166
|
+
loop = idle(svg);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** The entrance: illustration first, then the text and the actions. */
|
|
170
|
+
const enter = () => {
|
|
171
|
+
const svg = art.querySelector('svg');
|
|
172
|
+
if (reduced()) return;
|
|
173
|
+
if (svg) {
|
|
174
|
+
keep(gsap.fromTo(art, { opacity: 0, scale: 0.85, filter: 'blur(12px)' }, { opacity: 1, scale: 1, filter: 'blur(0px)', duration: 0.6, ease: EASE.pop, clearProps: 'opacity,transform,filter' }));
|
|
175
|
+
const draw = svg.querySelector<SVGGeometryElement>('[data-mk-part="draw"]');
|
|
176
|
+
if (draw) keep(gsap.fromTo(draw, { strokeDasharray: 60, strokeDashoffset: 60 }, { strokeDashoffset: 0, duration: 0.5, delay: 0.3, ease: EASE.wipe }));
|
|
177
|
+
}
|
|
178
|
+
keep(gsap.fromTo(parts(), { opacity: 0, y: 12, filter: 'blur(8px)' }, { opacity: 1, y: 0, filter: 'blur(0px)', duration: 0.45, delay: svg ? 0.15 : 0, stagger: 0.06, ease: 'power2.out', clearProps: 'opacity,transform,filter' }));
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
let entered = false;
|
|
182
|
+
let visible = true;
|
|
183
|
+
if (typeof IntersectionObserver === 'function') {
|
|
184
|
+
// Before it is seen, everything waits hidden; afterwards the loop only runs on screen.
|
|
185
|
+
if (!reduced()) gsap.set([art, ...parts()], { opacity: 0 });
|
|
186
|
+
scope.observe(
|
|
187
|
+
new IntersectionObserver((entries) => {
|
|
188
|
+
visible = entries.some((entry) => entry.isIntersecting);
|
|
189
|
+
if (visible && !entered) {
|
|
190
|
+
entered = true;
|
|
191
|
+
enter();
|
|
192
|
+
startLoop();
|
|
193
|
+
}
|
|
194
|
+
if (visible) loop?.play();
|
|
195
|
+
else loop?.pause();
|
|
196
|
+
}),
|
|
197
|
+
).observe(root);
|
|
198
|
+
} else {
|
|
199
|
+
entered = true;
|
|
200
|
+
enter();
|
|
201
|
+
startLoop();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
scope.on(root, 'click', (event) => {
|
|
205
|
+
const action = (event.target as Element | null)?.closest<HTMLElement>('[data-mk-action]');
|
|
206
|
+
if (!action || !root.contains(action)) return;
|
|
207
|
+
keep(press(action));
|
|
208
|
+
options.onAction?.(action.dataset.mkAction!, event as MouseEvent);
|
|
209
|
+
root.dispatchEvent(new CustomEvent('mk:action', { bubbles: true, detail: { id: action.dataset.mkAction } }));
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
update(next) {
|
|
214
|
+
if (scope.disposed) return;
|
|
215
|
+
const swap = () => {
|
|
216
|
+
if (next.illustration !== undefined) {
|
|
217
|
+
options = { ...options, illustration: next.illustration };
|
|
218
|
+
drawArt(next.illustration);
|
|
219
|
+
}
|
|
220
|
+
if (!authored) fill({ title: next.title ?? options.title, description: next.description ?? options.description, actions: next.actions ?? options.actions });
|
|
221
|
+
options = { ...options, ...next };
|
|
222
|
+
if (entered) {
|
|
223
|
+
enter();
|
|
224
|
+
if (visible) startLoop();
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
if (reduced() || !entered) return swap();
|
|
228
|
+
keep(gsap.to([art, ...parts()], { opacity: 0, filter: 'blur(8px)', duration: 0.15, ease: 'power1.in', onComplete: swap }));
|
|
229
|
+
},
|
|
230
|
+
destroy: () => scope.dispose(),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { createEmptyState, type EmptyStateInstance, type EmptyStateOptions } from './engine';
|
|
5
|
+
|
|
6
|
+
export interface EmptyStateProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'children'>, EmptyStateOptions {}
|
|
7
|
+
|
|
8
|
+
/** <EmptyState illustration="search" title="Sin resultados" actions={[{ id: 'clear', label: 'Borrar filtros' }]} onAction={…} />. */
|
|
9
|
+
export function EmptyState({ illustration, title, description, actions, loop, onAction, className, ...props }: EmptyStateProps) {
|
|
10
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
11
|
+
const instance = useRef<EmptyStateInstance | null>(null);
|
|
12
|
+
const action = useLatest(onAction);
|
|
13
|
+
const content = { illustration, title, description, actions };
|
|
14
|
+
const key = JSON.stringify(content);
|
|
15
|
+
useIsomorphicLayoutEffect(() => {
|
|
16
|
+
const created = createEmptyState(ref.current, { ...content, loop, onAction: (id, event) => action.current?.(id, event) });
|
|
17
|
+
instance.current = created;
|
|
18
|
+
return () => {
|
|
19
|
+
created.destroy();
|
|
20
|
+
instance.current = null;
|
|
21
|
+
};
|
|
22
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
23
|
+
}, [loop]);
|
|
24
|
+
const first = useRef(true);
|
|
25
|
+
useIsomorphicLayoutEffect(() => {
|
|
26
|
+
if (first.current) {
|
|
27
|
+
first.current = false;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
instance.current?.update(content);
|
|
31
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
32
|
+
}, [key]);
|
|
33
|
+
return <div {...props} ref={ref} className={['mk-empty-state', className].filter(Boolean).join(' ')} />;
|
|
34
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# file-explorer
|
|
2
|
+
|
|
3
|
+
Explorador de carpetas y archivos: el árbol de carpetas a un lado, la ruta como *breadcrumb* y el contenido de la carpeta como lista (nombre, tamaño y fecha, ordenable) o como cuadrícula de iconos grandes. Los iconos son dibujos: las carpetas, en el color de acento; los archivos, una hoja teñida según su familia (imagen, PDF, documento, hoja de cálculo, presentación, código, comprimido, audio o video) con la extensión en una etiqueta.
|
|
4
|
+
|
|
5
|
+
Al abrir una carpeta, el contenido se empuja: el anterior sale hacia un lado con blur y el nuevo entra por el otro (.34 s, curva signature); al volver, al revés. El árbol y la ruta lo siguen. Al cambiar entre lista y cuadrícula, cada icono vuela a su nuevo lugar (Flip, .5 s). Ordenar desliza los elementos a su nuevo orden (el resorte de layout del kit). Una carpeta nueva crece desde un blur y pide su nombre ahí mismo; un nombre repetido sacude el campo. Lo eliminado se encoge y lo demás cierra el hueco.
|
|
6
|
+
|
|
7
|
+
Para mover, arrastra: se levanta una tarjeta con el número de elementos que sigue al puntero y se inclina con su velocidad. Una carpeta bajo ella (en el contenido, en el árbol o en la ruta) abre la tapa, y las ramas del árbol se abren tras un momento. Al soltar, la tarjeta vuela hacia la carpeta. En pantallas táctiles, el arrastre empieza con una pulsación larga.
|
|
8
|
+
|
|
9
|
+
## HTML / Astro
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<mk-file-explorer class="mk-file-explorer" root-label="Mi unidad"
|
|
13
|
+
files='[{"id":"docs","name":"Documentos","children":[
|
|
14
|
+
{"id":"cv","name":"Currículum.pdf","size":184320,"modified":"2026-10-02"},
|
|
15
|
+
{"id":"facturas","name":"Facturas","children":[]}]},
|
|
16
|
+
{"id":"notas","name":"notas.txt","size":900}]'></mk-file-explorer>
|
|
17
|
+
|
|
18
|
+
<script>
|
|
19
|
+
const explorer = document.querySelector('mk-file-explorer');
|
|
20
|
+
explorer.addEventListener('mk:open', (event) => abrir(event.detail.entry));
|
|
21
|
+
explorer.addEventListener('mk:change', (event) => guardar(event.detail.files, event.detail.change));
|
|
22
|
+
</script>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## React
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
const [files, setFiles] = useState(inicial);
|
|
29
|
+
|
|
30
|
+
<FileExplorer files={files} onChange={setFiles} onOpen={(file) => preview(file)} rootLabel="Mi unidad" />;
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Lo que devuelve `onChange` puede volver como `files` sin redibujar nada; unos `files` distintos que lleguen de la app reemplazan lo que se muestra.
|
|
34
|
+
|
|
35
|
+
## Opciones
|
|
36
|
+
|
|
37
|
+
| Opción | Atributo | Por defecto | Qué hace |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| `files` | `files` (JSON) | `[]` | Lo que contiene la carpeta superior: `{ id, name, kind?, children?, size?, modified? }[]`. Una entrada con `children` (o `kind: 'folder'`) es una carpeta. |
|
|
40
|
+
| `rootLabel` | `root-label` | `'Inicio'` | Nombre de la carpeta superior. |
|
|
41
|
+
| `folder` | `folder` | `'/'` | Carpeta abierta al empezar. |
|
|
42
|
+
| `view` | `view` | `'list'` | `'list'` o `'grid'`. |
|
|
43
|
+
| `tree` | `tree` | `true` | El árbol de carpetas (se oculta por debajo de 560 px). |
|
|
44
|
+
| `readOnly` | `read-only` | `false` | Sin crear, renombrar, eliminar ni mover. |
|
|
45
|
+
| `locale` | `locale` | el `lang` más cercano | Idioma de fechas, tamaños y orden. |
|
|
46
|
+
| `label` | `label` | `'Explorador de archivos'` | Nombre accesible. |
|
|
47
|
+
| `onOpen(file)` | — | — | Se abrió un archivo (doble clic o Intro). |
|
|
48
|
+
| `onNavigate(folder)` | — | — | Se abrió una carpeta. |
|
|
49
|
+
| `onSelectionChange(ids)` | — | — | Cambió la selección. |
|
|
50
|
+
| `onViewChange(view)` | — | — | Cambió la vista (también desde su interruptor). |
|
|
51
|
+
| `onChange(files, change)` | — | — | Tras crear, renombrar, eliminar o mover; `change` dice qué pasó (`{ type: 'create' \| 'rename' \| 'delete' \| 'move', … }`). |
|
|
52
|
+
|
|
53
|
+
## API y eventos
|
|
54
|
+
|
|
55
|
+
`open(id)` (una carpeta, o un archivo: abre su carpeta y llama a `onOpen`), `up()`, `setView(view)`, `select(ids)`, `createFolder(nombre?)`, `rename(id, nombre?)` (sin nombre, lo pide en su sitio), `remove(ids?)`, `move(ids, carpeta)`, `setFiles(files)`, `files`, `folder`, `selection`, `view`, `destroy()`. Eventos `mk:open` con `{ id, entry }`, `mk:navigate` con `{ id, folder }`, `mk:select` con `{ ids }` y `mk:change` con `{ files, change }`.
|
|
56
|
+
|
|
57
|
+
## Teclado
|
|
58
|
+
|
|
59
|
+
Flechas para moverse (en la cuadrícula, también ← →), Mayús para extender la selección, Ctrl/⌘ para mover el foco sin seleccionar y Ctrl/⌘ + Espacio para sumar, Ctrl/⌘ + A selecciona todo, Intro abre, Retroceso o Alt + ↑ suben, F2 renombra, Supr elimina, Ctrl/⌘ + X y Ctrl/⌘ + V mueven a otra carpeta, Escape limpia la selección y escribir salta a un nombre. Durante un arrastre, Escape lo cancela.
|
|
60
|
+
|
|
61
|
+
## Variables CSS
|
|
62
|
+
|
|
63
|
+
`--mk-fx-height` (alto, `26rem`), `--mk-fx-side` (ancho del árbol, `13rem`).
|
|
64
|
+
|
|
65
|
+
## Accesibilidad
|
|
66
|
+
|
|
67
|
+
Una región con nombre; el contenido es un `listbox` de selección múltiple con un solo elemento tabulable, y cada opción dice su nombre, su tipo, su tamaño y su fecha. El árbol sigue el patrón de árbol de WAI-ARIA y la ruta es un `nav`. Un estado discreto anuncia cada carpeta abierta, cada cambio y cada movimiento; el error de un nombre se lee desde el campo. Mover también se puede hacer sin arrastrar (Ctrl + X, Ctrl + V). Con movimiento reducido no hay deslizamientos, vuelos ni inclinación.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createFileExplorer, type FileEntry, type FileExplorerInstance } from './engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* <mk-file-explorer class="mk-file-explorer" root-label="Mi unidad" view="grid"
|
|
6
|
+
* files='[{"id":"docs","name":"Documentos","children":[{"id":"cv","name":"Currículum.pdf","size":184320}]}]'>
|
|
7
|
+
* </mk-file-explorer>. Emits mk:open ({id, entry}), mk:navigate ({id, folder}), mk:select ({ids}), mk:change ({files, change}).
|
|
8
|
+
*/
|
|
9
|
+
export class MkFileExplorerElement extends MkElement<FileExplorerInstance> {
|
|
10
|
+
static options: Record<string, OptionType> = {
|
|
11
|
+
files: 'json',
|
|
12
|
+
rootLabel: 'string',
|
|
13
|
+
folder: 'string',
|
|
14
|
+
view: 'string',
|
|
15
|
+
tree: 'boolean',
|
|
16
|
+
readOnly: 'boolean',
|
|
17
|
+
locale: 'string',
|
|
18
|
+
label: 'string',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
protected create(options: Record<string, unknown>) {
|
|
22
|
+
return createFileExplorer(this, options);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
26
|
+
if (previous === next || !this.instance) return;
|
|
27
|
+
if (name === 'files') {
|
|
28
|
+
try {
|
|
29
|
+
this.instance.setFiles(JSON.parse(next ?? '[]') as FileEntry[]);
|
|
30
|
+
} catch {
|
|
31
|
+
// Malformed JSON: keep the current files.
|
|
32
|
+
}
|
|
33
|
+
} else if (name === 'view') this.instance.setView(next === 'grid' ? 'grid' : 'list');
|
|
34
|
+
else if (name === 'folder') this.instance.open(next || '/');
|
|
35
|
+
else super.attributeChangedCallback(name, previous, next);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const defineFileExplorer = () => defineElement('mk-file-explorer', MkFileExplorerElement);
|