@synopackageland/ui 0.20.2 → 0.20.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSES/ux-foundation.md +83 -0
  3. package/LICENSES/ux-grid-baserow.md +35 -0
  4. package/LICENSES/ux-grid-teable.md +48 -0
  5. package/README.md +3 -0
  6. package/dist/cli.js +0 -0
  7. package/dist/components/app/app-layout/AppLayout.vue.d.ts +31 -4
  8. package/dist/components/app/app-root/AppRoot.vue.d.ts +9 -0
  9. package/dist/components/app/aside-panel/AsidePanel.vue.d.ts +69 -0
  10. package/dist/components/app/aside-panel/asidePreference.d.ts +28 -0
  11. package/dist/components/composite/adaptive-panel/AdaptivePanel.vue.d.ts +61 -0
  12. package/dist/components/composite/connection-topology/ConnectionTopology.vue.d.ts +1 -1
  13. package/dist/components/composite/data-grid/DataGrid.vue.d.ts +3 -2
  14. package/dist/components/composite/data-grid/viewState.d.ts +2 -2
  15. package/dist/components/composite/file-picker/FilePicker.vue.d.ts +1 -1
  16. package/dist/components/composite/mention-list/MentionList.vue.d.ts +2 -2
  17. package/dist/components/composite/overflow-toolbar/OverflowToolbar.vue.d.ts +2 -2
  18. package/dist/components/composite/record-panel/RecordPanel.vue.d.ts +77 -0
  19. package/dist/components/composite/record-panel/recordPanelWidth.d.ts +54 -0
  20. package/dist/components/composite/summary-card/SummaryCard.vue.d.ts +1 -1
  21. package/dist/components/composite/tag-editor/TagEditor.vue.d.ts +2 -2
  22. package/dist/components/composite/tree-view/TreeView.vue.d.ts +1 -1
  23. package/dist/components/composite/wizard/Wizard.vue.d.ts +2 -2
  24. package/dist/components/composite/wizard/WizardDialog.vue.d.ts +2 -2
  25. package/dist/components/containers/card/Card.vue.d.ts +1 -1
  26. package/dist/components/containers/card/CardField.vue.d.ts +26 -0
  27. package/dist/components/containers/card-grid/CardCover.vue.d.ts +16 -0
  28. package/dist/components/containers/card-grid/CardGrid.vue.d.ts +1 -1
  29. package/dist/components/containers/card-grid/VirtualCardGrid.vue.d.ts +71 -0
  30. package/dist/components/containers/expandable-item/ExpandableItem.vue.d.ts +1 -1
  31. package/dist/components/containers/fieldset/Fieldset.vue.d.ts +1 -1
  32. package/dist/components/containers/list-detail/ListDetail.vue.d.ts +1 -1
  33. package/dist/components/containers/split-pane/SplitPane.vue.d.ts +1 -1
  34. package/dist/components/feedback/notification/Notification.vue.d.ts +1 -1
  35. package/dist/components/feedback/notification/NotificationProvider.vue.d.ts +1 -1
  36. package/dist/components/primitives/breadcrumb/Breadcrumb.vue.d.ts +2 -2
  37. package/dist/components/primitives/context-menu/ContextMenu.vue.d.ts +3 -0
  38. package/dist/components/primitives/context-menu/ContextMenuItems.vue.d.ts +4 -5
  39. package/dist/components/primitives/dialog/Dialog.vue.d.ts +2 -2
  40. package/dist/components/primitives/dropdown-menu/DropdownMenu.vue.d.ts +4 -0
  41. package/dist/components/primitives/dropdown-menu/DropdownMenuItems.vue.d.ts +6 -3
  42. package/dist/components/primitives/popover/Popover.vue.d.ts +10 -2
  43. package/dist/components/primitives/popover/floating.d.ts +27 -0
  44. package/dist/components/primitives/popover/overlay.d.ts +36 -0
  45. package/dist/components/primitives/segment-control/SegmentControl.vue.d.ts +4 -1
  46. package/dist/components/primitives/segment-control/context.d.ts +3 -0
  47. package/dist/components/primitives/select/Select.vue.d.ts +4 -0
  48. package/dist/components/primitives/select/internal/ComboboxField.vue.d.ts +1 -0
  49. package/dist/components/primitives/select/internal/types.d.ts +2 -0
  50. package/dist/components/primitives/skeleton/SectionSkeleton.vue.d.ts +25 -4
  51. package/dist/components/primitives/skeleton/Skeleton.vue.d.ts +3 -0
  52. package/dist/components/primitives/tag/Tag.vue.d.ts +2 -2
  53. package/dist/components/primitives/toast/ToastProvider.vue.d.ts +0 -2
  54. package/dist/components/primitives/toast/toast.d.ts +21 -1
  55. package/dist/components/primitives/tooltip/Tooltip.vue.d.ts +8 -3
  56. package/dist/components/primitives/tooltip/tooltipStyle.d.ts +11 -1
  57. package/dist/composables/backDismiss.d.ts +19 -0
  58. package/dist/composables/phone.d.ts +8 -0
  59. package/dist/composables/softKeyboard.d.ts +26 -4
  60. package/dist/composables/visualViewport.d.ts +31 -0
  61. package/dist/dnd/autoScroll.d.ts +57 -0
  62. package/dist/dnd/dragKit.d.ts +111 -0
  63. package/dist/dnd/index.d.ts +4 -0
  64. package/dist/dnd/layout.d.ts +57 -0
  65. package/dist/dnd/vue.d.ts +16 -0
  66. package/dist/feedback/StatusSlot.vue.d.ts +24 -0
  67. package/dist/feedback/showAfter.d.ts +15 -0
  68. package/dist/feedback/stackMotion.d.ts +13 -0
  69. package/dist/feedback/timing.d.ts +23 -0
  70. package/dist/grid/cells/draw.d.ts +57 -0
  71. package/dist/grid/cells/images.d.ts +5 -0
  72. package/dist/grid/cells/index.d.ts +7 -0
  73. package/dist/grid/cells/number.d.ts +5 -0
  74. package/dist/grid/cells/registry.d.ts +56 -0
  75. package/dist/grid/cells/renderers.d.ts +8 -0
  76. package/dist/grid/cells/status.d.ts +6 -0
  77. package/dist/grid/cells/types.d.ts +200 -0
  78. package/dist/grid/cells/use.d.ts +7 -0
  79. package/dist/grid/clipboard/copy.d.ts +17 -0
  80. package/dist/grid/clipboard/index.d.ts +4 -0
  81. package/dist/grid/clipboard/proposal.d.ts +35 -0
  82. package/dist/grid/clipboard/tsv.d.ts +14 -0
  83. package/dist/grid/clipboard/write.d.ts +21 -0
  84. package/dist/grid/contracts.d.ts +200 -0
  85. package/dist/grid/edit/cell-status.d.ts +33 -0
  86. package/dist/grid/edit/edit-session.d.ts +97 -0
  87. package/dist/grid/edit/use-grid-edit.d.ts +48 -0
  88. package/dist/grid/focus/editor-style.d.ts +67 -0
  89. package/dist/grid/host/GridHost.vue.d.ts +137 -0
  90. package/dist/grid/host/controller.d.ts +219 -0
  91. package/dist/grid/host/dom.d.ts +10 -0
  92. package/dist/grid/index.d.ts +8 -0
  93. package/dist/grid/range/block.d.ts +30 -0
  94. package/dist/grid/range/fill.d.ts +27 -0
  95. package/dist/grid/range/index.d.ts +2 -0
  96. package/dist/grid/theme.d.ts +61 -0
  97. package/dist/grid-core/auto-scroll.d.ts +67 -0
  98. package/dist/grid-core/column-resize.d.ts +43 -0
  99. package/dist/grid-core/config.d.ts +39 -0
  100. package/dist/grid-core/coordinate.d.ts +64 -0
  101. package/dist/grid-core/drag.d.ts +46 -0
  102. package/dist/grid-core/freeze.d.ts +11 -0
  103. package/dist/grid-core/index.d.ts +17 -0
  104. package/dist/grid-core/keyboard.d.ts +34 -0
  105. package/dist/grid-core/keys.d.ts +38 -0
  106. package/dist/grid-core/lru.d.ts +10 -0
  107. package/dist/grid-core/range.d.ts +28 -0
  108. package/dist/grid-core/region.d.ts +73 -0
  109. package/dist/grid-core/renderer/base.d.ts +92 -0
  110. package/dist/grid-core/renderer/layout.d.ts +129 -0
  111. package/dist/grid-core/selection-controller.d.ts +62 -0
  112. package/dist/grid-core/selection.d.ts +35 -0
  113. package/dist/grid-core/theme.d.ts +81 -0
  114. package/dist/grid-core/types.d.ts +178 -0
  115. package/dist/grid.js +6173 -0
  116. package/dist/i18n/text.d.ts +6 -0
  117. package/dist/index.d.ts +32 -2
  118. package/dist/index.js +17662 -13637
  119. package/dist/interaction/editSession.d.ts +49 -0
  120. package/dist/interaction/focus.d.ts +28 -0
  121. package/dist/interaction/gesture.d.ts +49 -0
  122. package/dist/interaction/ime.d.ts +55 -0
  123. package/dist/interaction/index.d.ts +6 -0
  124. package/dist/interaction/outside.d.ts +40 -0
  125. package/dist/interaction/vue.d.ts +21 -0
  126. package/dist/json-ai-prompt.md +1 -1
  127. package/dist/json-preview/preview.js +8529 -7812
  128. package/dist/json.js +10 -10
  129. package/dist/lib/adaptiveLayout.d.ts +32 -2
  130. package/dist/lib/deviceViewport.d.ts +2 -1
  131. package/dist/lib/motion.d.ts +12 -0
  132. package/dist/reka-runtime.json +1 -1
  133. package/dist/theme.css +1 -1
  134. package/dist/themes/dsm7-embedded-theme.css +1 -1
  135. package/dist/themes/dsm7-embedded.css +1 -1
  136. package/dist/themes/dsm7.css +1 -1
  137. package/dist/virtual/cardHeight.d.ts +39 -0
  138. package/dist/virtual/galleryColumns.d.ts +11 -0
  139. package/dist/virtual/layoutMotion.d.ts +30 -0
  140. package/docs/adaptive-layout.md +137 -16
  141. package/docs/callout.md +22 -1
  142. package/docs/list-detail.md +54 -0
  143. package/docs/mobile-native.md +214 -0
  144. package/docs/release-checks.md +23 -0
  145. package/docs/theme-hooks.md +28 -7
  146. package/docs/ux-foundation.md +133 -0
  147. package/docs/ux-grid.md +217 -0
  148. package/package.json +94 -3
@@ -46,14 +46,26 @@ here stay implementation details and can change in any release.
46
46
  | `data-ui-brand-action` | native button around the default brand when `AppLayout` has a `brand` listener | present |
47
47
  | `data-ui-sidebar-body` | wrapper of the `sidebar` slot | present |
48
48
  | `data-ui-sidebar-footer` | wrapper of the `sidebar-footer` slot | present |
49
- | `data-ui-sidebar-collapse` | the web rail's collapse button | present |
49
+ | `data-ui-sidebar-collapse` | the web sidebar's collapse button, at the end of its head row | present |
50
+ | `data-ui-layout-aside` | the right side region holding the `aside` slot | present; `data-ui-aside-state` is `open`, `collapsed`, `expanded` |
51
+ | `data-ui-aside` | `AppLayout` root, when it has an `aside` slot | `open`, `collapsed`, `expanded` |
52
+ | `data-ui-aside-overlay` | `AppLayout` root | `true` while the open aside floats over the page |
53
+ | `data-ui-aside-resize` | the aside's resize handle | present |
54
+ | `data-ui-aside-resizing` | `AppLayout` root | `true` while the aside's handle is dragged |
55
+ | `data-ui-side-moving` | `AppLayout` root | `true` while the sidebar or the aside changes width |
56
+ | `data-ui-aside-panel-root` | `AsidePanel` root inside the aside region | `data-ui-aside-mode` is `docked`, `floating`, `sheet`; `data-ui-aside-floating` while the panel floats or slides away from floating |
57
+ | `data-ui-aside-panel` | the panel (`complementary`, or `dialog` as a sheet) | `data-ui-aside-state` is `open`, `collapsed`, `expanded` |
58
+ | `data-ui-aside-rail` / `data-ui-aside-open` | the 48px rail and its button | present |
59
+ | `data-ui-aside-fab` / `data-ui-aside-scrim` | the phone button and the sheet's scrim | present |
60
+ | `data-ui-aside-collapse` / `data-ui-aside-expand` / `data-ui-aside-restore` | header buttons | present |
61
+ | `data-ui-aside-body` / `data-ui-aside-composer` | the thread scroller and the composer | present |
50
62
  | `data-ui-mobile-navigation` | the phone bar above the page | present |
51
63
  | `data-ui-mobile-menu` | the phone bar's menu (hamburger) button | present |
52
64
  | `data-ui-mobile-sidebar-header` | the drawer's header row | present |
53
65
  | `data-ui-mobile-backdrop` | the drawer's backdrop | present |
54
66
  | `data-ui-app-toolbar` | `AppToolbar` | present |
55
67
  | `data-ui-page-header` | `PageHeader` | present |
56
- | `data-ui-page-header-meta`, `data-ui-page-header-actions` | `PageHeader`'s meta and actions areas | present |
68
+ | `data-ui-page-header-meta`, `data-ui-page-header-actions`, `data-ui-page-header-description` | `PageHeader`'s meta, actions and description | present |
57
69
  | `data-page-surface` | `PageContent` | `auto`, `card`, `flat`, `none` |
58
70
  | `data-ui-motion` | set by the app on any ancestor (an `AppRoot`, `<html>`) | `off`: the library's motion is instant inside it, as under reduced motion ([Motion](#motion)) |
59
71
 
@@ -95,6 +107,7 @@ here stay implementation details and can change in any release.
95
107
  | `data-menu-description` | a `DropdownMenu` entry's second line | present |
96
108
  | `data-ui-segment-count` | a `SegmentItem`'s count pill | present |
97
109
  | `data-ui-segment-control` | `SegmentControl` | `segment`, `sheet` |
110
+ | `data-ui-segment-size` | `SegmentControl` with `size="sm"` | `sm` |
98
111
  | `data-ui-segment-moving` | `SegmentControl` while its selection slides (`.ui-segment-thumb` is shown, the new item's own fill waits) | present |
99
112
  | `data-ui-swap` | `MotionSwap` and `AppLayout`'s `main` | present |
100
113
  | `data-ui-swapped` | the same, after the first change: plays the rise-in | present |
@@ -103,7 +116,7 @@ here stay implementation details and can change in any release.
103
116
  | `data-ui-stat-tile` | `StatTile` (`data-tone`; a filter tile also `data-state` `on` / `off`) | present |
104
117
  | `data-ui-stat-tile-label`, `-value`, `-unit`, `-note` | `StatTile` parts | present |
105
118
  | `data-ui-stat-tile-group` | `StatTileGroup` | present |
106
- | `data-ui-page-toolbar`, `-start`, `-actions` | `PageToolbar` and its two sides | present |
119
+ | `data-ui-page-toolbar`, `-start`, `-actions` | `PageToolbar` and its two sides | present; the start side carries `data-overflow-start` and `data-overflow-end` while it continues past an edge |
107
120
  | `data-ui-summary-card` | `SummaryCard` (`data-state` `loading`, `error`, `ready`; `data-wide`) | present |
108
121
  | `data-ui-summary-card-title`, `-state`, `-figures`, `-figure`, `-body`, `-note`, `-error` | `SummaryCard` parts | present |
109
122
  | `data-ui-skeleton` | `SectionSkeleton` | `lines`, `figures`, `rows` (parts `data-ui-skeleton-line`, `-figure`, `-row`) |
@@ -188,14 +201,22 @@ does not animate its rows.
188
201
 
189
202
  | Token | Value | Use |
190
203
  | --- | --- | --- |
191
- | `--ui-motion-fast` | 120ms | colour and state changes, the tab notch and segment fill sliding, menus opening, anything closing |
192
- | `--ui-motion-normal` | 180ms | dialogs opening, a new row, a tab panel, switched content and pages, the loading mask |
204
+ | `--ui-motion-fast` | 150ms | colour and state changes, the tab notch and segment fill sliding, menus opening, a dialog closing |
205
+ | `--ui-motion-normal` | 200ms | dialogs opening, a new row, a tab panel, switched content and pages, the loading mask, a card's hover shadow |
193
206
  | `--ui-motion-slow` | 240ms | height, notifications and toasts arriving, a sheet sliding in |
207
+ | `--ui-motion-panel` | 300ms | a side region: the web sidebar and its rail, an aside, a record panel |
208
+ | `--ui-motion-exit` | 100ms | a popup, menu or tooltip closing |
209
+ | `--ui-motion-stack` | 200ms (`--ui-motion-stack-ease`) | a toast stack closing up |
210
+ | `--ui-motion-flash` | 1600ms | the tint on something changed elsewhere |
211
+ | `--ui-skeleton-pulse` | 1200ms (`--ui-skeleton-pulse-ease`) | one skeleton pulse |
212
+ | `--ui-motion-drag` | 200ms, `cubic-bezier(0.2, 0, 0, 1)` (`--ui-motion-drag-ease`) | siblings making room while dragging |
213
+ | `--ui-motion-drag-settle` | 260ms, `cubic-bezier(0.2, 1, 0.1, 1)` (`--ui-motion-drag-settle-ease`) | a dropped item settling |
214
+ | `--ui-motion-ease` | `cubic-bezier(0.4, 0, 0.2, 1)` | colour and hover fades |
194
215
  | `--ui-motion-ease-out` | `cubic-bezier(0.22, 1, 0.36, 1)` | arriving |
195
216
  | `--ui-motion-ease-in` | `cubic-bezier(0.4, 0, 1, 1)` | leaving |
196
- | `--ui-motion-ease-in-out` | `cubic-bezier(0.4, 0, 0.2, 1)` | height between two resting states |
217
+ | `--ui-motion-ease-in-out` | `cubic-bezier(0.4, 0, 0.2, 1)` | height or width between two resting states |
197
218
 
198
- Under `prefers-reduced-motion: reduce` all of it is instant. To turn it off
219
+ Under `prefers-reduced-motion: reduce` all of it is instant, and every duration token reads 0ms, so script that reads them waits for nothing either. To turn it off
199
220
  for part of a page, or the whole App, set `data-ui-motion="off"` on any
200
221
  ancestor:
201
222
 
@@ -0,0 +1,133 @@
1
+ # UX foundation: public contract
2
+
3
+ The building blocks for app views that settle once and do not move again:
4
+ interaction core, drag kit, side regions and panels, virtual cards, feedback
5
+ and motion. Everything here is imported from a package entry; an App never
6
+ imports a source path.
7
+
8
+ | Entry | What it holds | Load |
9
+ | --- | --- | --- |
10
+ | `@synopackageland/ui` | every name on this page except the grid | normally |
11
+ | `@synopackageland/ui/grid` | `GridHost`, cell renderers, cell theme, save-status store | lazily, when a grid view first opens |
12
+ | `@synopackageland/ui/theme.css` | tokens, chrome and motion | once, at startup |
13
+
14
+ ```ts
15
+ import {
16
+ AsidePanel, RecordPanel, AdaptivePanel, StatusSlot, VirtualCardGrid, CardCover, CardField,
17
+ cardHeight, createDragKit, useDragKit, registerOutsideLayer, createEditSession, motionMs,
18
+ } from '@synopackageland/ui'
19
+
20
+ const GridHost = defineAsyncComponent(() => import('@synopackageland/ui/grid').then(module => module.GridHost))
21
+ ```
22
+
23
+ A consumer fixture that type-checks and imports every name below from the
24
+ packed tarball is in `tests/ux-foundation-consumer/`
25
+ (`node scripts/check-ux-foundation-consumer.mjs`, after `pnpm build`).
26
+
27
+ ## Defaults and opt-in
28
+
29
+ Nothing here needs a flag. The compact chrome is the only density.
30
+
31
+ | What | Default | Opt in or change |
32
+ | --- | --- | --- |
33
+ | Web chrome rows (page header, toolbar, sidebar head and footer) | 48px; window and phone keep 44px | none |
34
+ | Chrome controls / header actions / web nav rows / icons | 28px / 32px / 32px / 16px; icon buttons 4px apart | `--ui-nav-row-height`, `--ui-icon-size` ([theme hooks](theme-hooks.md#layout-and-theme-tokens)) |
35
+ | Full-page (`web`) presentation | off; DSM windows stay `window` at any width | `AppLayout wide-layout="web"`: from a 960px container, sidebar starts as the 48px rail below 1200px |
36
+ | Font | system font stack in the `web` presentation | `--ui-font-sans` |
37
+ | Right side region | none | put an `AsidePanel` in `AppLayout`'s `aside` slot; `v-model:aside`, `v-model:aside-width` |
38
+ | Motion | on, with the tokens in [theme hooks](theme-hooks.md#motion) | `data-ui-motion="off"` on any ancestor; reduced motion reads every duration as 0ms |
39
+ | Popup motion inside a grid editor | 150ms in, 100ms out | `provideOverlayMotion('none')` or `motion="none"` |
40
+ | Canvas grid | not loaded | import `@synopackageland/ui/grid` |
41
+
42
+ ## Copy and accessibility
43
+
44
+ Built-in labels come from the library's nine locales (en, zh-TW, zh-CN, ja,
45
+ ko, de, fr, es, it) and follow `AppRoot locale` (else its `lang`). Names that only the App
46
+ knows are props, already translated: `AsidePanel title`, `RecordPanel label`,
47
+ `AdaptivePanel title`, `VirtualCardGrid label`, `CardField label`,
48
+ `CardCover alt`, `StatusSlot message`. Every built-in label can be replaced
49
+ (`openLabel`, `collapseLabel`, `expandLabel`, `restoreLabel`, `closeLabel`,
50
+ `resizeLabel`, `retryLabel`). No label ends with a period.
51
+
52
+ ## Side regions and panels
53
+
54
+ | Component | Use | Main props and events |
55
+ | --- | --- | --- |
56
+ | `AppLayout` `aside` slot | the right side region beside the page | `v-model:aside` (`open` / `collapsed` / `expanded`), `v-model:aside-width` (380, 320–560). The slot gets `{ state, overlay, width, presentation }`. An open aside that would leave the page under 480px floats over it; a phone has no region width |
57
+ | `AsidePanel` | the region's content: 48px rail, docked panel, floating panel, phone sheet | `title`, `icon`, `unread`, `fab`, `expandable`, `initialFocus`, `shortcut`; slots `tabs`, `actions`, default. Never unmounted, so scroll and drafts survive a collapse |
58
+ | `useAsidePreference(key, scope)` | the stored state and width, read before first paint | `readAsidePreference`, `writeAsidePreference`, `clampAsideWidth` |
59
+ | `RecordPanel` | a non-modal record panel docked over the view; the view never changes width | `v-model:open`, `v-model:width`, `storageKey`, `secondaryOpen`, `container`, `compactBelow`, `returnFocus`, `initialFocus`, `flush`; `close(reason)`, `resizeEnd`; slots `header`, default, `secondary` |
60
+ | `AdaptivePanel` | a popover in a wide host, a bottom sheet at 640px and narrower; the content is mounted once | `v-model:open`, `title`, `side`, `align`, `width`, `sheetSide`, `sheetSize`, `compactBelow`, `container`, `motion` |
61
+
62
+ `APP_ASIDE_KEY` and `APP_SIDE_MOTION_KEY` are injection keys for content that
63
+ must react to the region; `WEB_ASIDE_DEFAULT`, `WEB_ASIDE_MIN`,
64
+ `WEB_ASIDE_MAX`, `WEB_RAIL_WIDTH`, `WEB_RAIL_BELOW`, `WEB_LAYOUT_MIN` and
65
+ `MOBILE_LAYOUT_MAX` are the sizes.
66
+
67
+ ## Feedback
68
+
69
+ | Name | Contract |
70
+ | --- | --- |
71
+ | `SectionSkeleton` `variant="grid" \| "cards" \| "fields"` | the final geometry from the first frame; shapes show after 300ms |
72
+ | `useToast()` | success 3s, info and warning 5s (at least 5s with an action), danger stays; `pending()` shows after 300ms and becomes its result through `update(id)` |
73
+ | `StatusSlot` | a fixed box for `pending` / `saved` / `error` (with Retry); empty while idle, so nothing moves |
74
+ | `useShowAfter(active, { delay })` | true only once `active` has held for the delay (300ms, `PENDING_DELAY_MS`) |
75
+ | `TOAST_DURATION_MS` | the toast durations by tone |
76
+
77
+ ## Virtual cards
78
+
79
+ `VirtualCardGrid` renders only the rows on screen. Every card has the height
80
+ `cardHeight({ fields, cover, title })` returns, so nothing is measured, and
81
+ the column count (`galleryColumns(width)`, breakpoints `GALLERY_BREAKPOINTS`)
82
+ is known before the first card renders. The grid emits `range(start, end)` for
83
+ the window to load; an unloaded card is a placeholder of the same size. Put
84
+ `CardCover` in the `cover` slot and `CardField` rows in the body, with the same
85
+ value heights you passed to `cardHeight` (`CARD_VALUE_HEIGHT`, `CARD_METRICS`).
86
+
87
+ While a side region changes width, the column count holds and settles once at
88
+ the end. `AppLayout` drives this for its sidebar and aside. Anything else that
89
+ moves the width of the main area on purpose calls `useLayoutMotion().track(el)`
90
+ right after it changes the width, or `begin()` and the returned end; a layout
91
+ that only moves its own region gives its children `provideLayoutMotion()`.
92
+
93
+ ## Interaction core
94
+
95
+ | Name | Contract |
96
+ | --- | --- |
97
+ | `registerOutsideLayer` / `useOutsideLayer` | one window-capture listener; layers are walked from the top, everything above the pressed layer gets `onOutside` once; `'veto'` blocks the press and its click; Escape goes to the topmost layer only |
98
+ | `IGNORE_OUTSIDE_ATTR` (`data-ui-ignore-outside`) | put it on every portal root that belongs to an interactive surface |
99
+ | `createCompositionGuard`, `isComposingKey`, `isPrintableKey` | Enter, Escape and Tab belong to an IME while it composes and for 50ms after (`COMPOSITION_END_GRACE_MS`) |
100
+ | `focusSync`, `createFocusOwner`, `isInForeignDialog` | focus synchronously in the input handler, never out of a dialog the surface does not own |
101
+ | `trackPointerGesture` / `usePointerGesture`, `dropIndexAt` | 5px threshold (`DRAG_THRESHOLD_PX`), pointer capture, release on cancel, blur or dispose |
102
+ | `createEditSession` / `useEditSession`, `editSessionOutside` | `start` / `setDraft` / `setComposing` / `commit` / `cancel`; a validation veto keeps the editor; a commit waits for the composition to end |
103
+
104
+ ## Drag kit
105
+
106
+ `createDragKit` (Vue: `useDragKit`) runs one drag across registered lanes:
107
+ 5px mouse threshold, 250ms touch hold (`TOUCH_HOLD_MS`), Space and arrow keys,
108
+ edge auto-scroll, Escape rollback. Register a lane with
109
+ `kit.lane({ id, element, count, layout })`: give a virtual lane a fixed
110
+ `dragListGeometry` or `dragGridGeometry`; a rendered lane can use
111
+ `dragMeasuredGeometry`. Spread `kit.item(lane, index, key)` on each item.
112
+ `onDrop({ key, from, to })` runs in the release frame: change the local data
113
+ there and return the server write's promise; a rejection calls `onRestore` and
114
+ the item slides home. The geometry types are exported as `DragRect`,
115
+ `DragAxis`, `DragArrangement`, `DragLaneGeometry` and `DragLaneLayout`.
116
+ `createAutoScroll` and `edgeSpeed` give the same edge scrolling to other
117
+ surfaces.
118
+
119
+ ## Motion helpers
120
+
121
+ `motionMs(token, el?)` reads a duration token (`MotionToken`) as it resolves
122
+ on `el`, 0 when motion is off for it (`motionOff`, `MOTION_OFF_SELECTOR`).
123
+ `transitionMs(el)` is the longest transition on an element.
124
+ `useSwapMotion(target, source)` plays the switched-content rise-in when
125
+ `source` changes; `useInsertedKeys(keys, options)` returns the keys of rows
126
+ that were just added, so only those animate in.
127
+
128
+ ## Third-party code
129
+
130
+ Parts of this foundation are ports of MIT code from Teable and Baserow. The
131
+ notices are kept in each ported file and collected in
132
+ `LICENSES/ux-foundation.md`; the grid entry's notices are
133
+ `LICENSES/ux-grid-teable.md` and `LICENSES/ux-grid-baserow.md`; its contract is [ux-grid](ux-grid.md).
@@ -0,0 +1,217 @@
1
+ # Grid range, fill and clipboard
2
+
3
+ `@synopackageland/ui/grid` draws a canvas grid (a Vue port of Teable's MIT
4
+ grid core) and owns geometry, scroll, selection, focus and pointer feedback.
5
+ It never writes data. Every change a user asks for (an edit, a clear, a fill,
6
+ a paste, a reorder, a width) comes out as an event; the caller applies it to
7
+ its row model as one undoable change and saves in the background.
8
+
9
+ This page covers range selection, the fill handle, edge auto-scroll, column
10
+ and row drag, column width and the clipboard. Editing is in the `GridHost`
11
+ props (`getEditText`, `cell-commit`); cell painters are in `useGridCells`.
12
+
13
+ ## Range selection
14
+
15
+ | Gesture | Result |
16
+ |---|---|
17
+ | Press a cell and drag | The range follows every head change and paints in the same task (no frame hop) |
18
+ | Drag past an edge | Auto-scroll: 30 px zone at every edge, also past the grid; speed ramps from 0 to 2000 px/s over 1.2 s; the head is re-derived from the last pointer on every tick, so the range keeps growing while the pointer rests |
19
+ | Over a frozen column | No horizontal scroll, so frozen columns stay targets |
20
+ | Shift+click, Shift+arrow | Keep the anchor, move the head; Cmd/Ctrl+Shift+arrow jumps to the edge |
21
+ | Cmd/Ctrl+A | All cells |
22
+ | Column header click / Shift / Cmd | Select columns, extend, toggle |
23
+ | Row checkbox / Shift, corner checkbox | Select rows, a run of rows, all rows |
24
+ | Esc | Keeps the selection; cancels a drag, a resize or a fill in progress |
25
+
26
+ Hit-testing is arithmetic over cached column offsets and row heights: a
27
+ pointer move reads nothing from the DOM. Past the last column or row, over a
28
+ header or over blank space, a drag head clamps to the nearest data cell.
29
+
30
+ `selectionBlock(state, { rowCount, columnCount })` gives the rows and columns
31
+ a selection covers (a row selection covers every column, a column selection
32
+ every row); `cellRange` gives the rectangle of a cell selection.
33
+
34
+ ## Fill handle
35
+
36
+ Set `fillable`. A cell range shows a 7 px handle at its bottom-right corner
37
+ (21 px hit area, crosshair cursor). Dragging it is vertical only; the preview
38
+ tints the target rows and leaves their values visible; auto-scroll works as
39
+ for a range. On release outside the source rows the grid grows the selection
40
+ to source plus target and emits one `fill-request`:
41
+
42
+ ```ts
43
+ interface GridFillRequest {
44
+ source: GridCellRange // the selected block
45
+ target: GridCellRange // the new rows only, same columns
46
+ direction: 'down' | 'up'
47
+ }
48
+ ```
49
+
50
+ `fillSeries(values, count, direction)` builds one column's values: with two
51
+ or more different numbers the steps repeat (dates as epoch milliseconds step
52
+ the same way); anything else repeats the source block. The caller decides the
53
+ field rules (a read-only or computed column is skipped), writes the target as
54
+ one undo entry and may show a "filled N cells" toast with its own text.
55
+
56
+ Read-only grids and `selectable="none"` show no handle.
57
+
58
+ ## Column and row drag
59
+
60
+ `draggable` turns on column reorder (drag a header) and row reorder (the row
61
+ drag handle, with the `drag` row control). The drag starts after 5 px; a ghost
62
+ and a 2 px drop line follow the pointer directly. Esc cancels and the release
63
+ does nothing. A drop emits `column-reorder` or `row-reorder` with the dragged
64
+ runs and the drop index counted before they are removed.
65
+
66
+ ## Column width
67
+
68
+ While a resize is dragged, the width follows the pointer and `column-resize`
69
+ fires on every change. On release `column-resize-end` fires once.
70
+ `column-width-save` fires 300 ms after the last finished resize of a column:
71
+ persist the width there. Esc during a resize restores the width and saves
72
+ nothing. A pending save is sent when the grid unmounts.
73
+
74
+ ## Clipboard
75
+
76
+ The grid's focus owner (a hidden textarea over the active cell) receives the
77
+ browser's native `copy`, `cut` and `paste` events, so Cmd/Ctrl+C, X and V work
78
+ without permissions, including Safari.
79
+
80
+ **Copy** writes the selected block as tab-separated text (cells with a tab,
81
+ a line break or a quote are quoted, quotes doubled) plus an HTML table for
82
+ more than one cell. Text comes from `getCopyText`, else `getCellText`. If a
83
+ row of the block is not loaded (`isRowLoaded` false), `resolveCopy(block)`
84
+ must return the text; the write then goes through the async clipboard API
85
+ with promise-valued items, so the user gesture still counts. Without
86
+ `resolveCopy` such a copy fails. `clipboard-copy` reports the block, its cell
87
+ count and `ok`; a successful copy outlines the block with a dashed accent line
88
+ until the next edit, paste, fill or Esc.
89
+
90
+ **Cut** is a copy that, only when it succeeded, also emits `clear-request` for
91
+ the cut block. A failed or refused write clears nothing.
92
+
93
+ **Paste** parses `text/plain` into rows and emits `paste-request` with a
94
+ proposal. The grid applies nothing:
95
+
96
+ ```ts
97
+ interface GridPasteProposal {
98
+ start: GridCell // top-left of the selection
99
+ values: string[][] // fitted to the grid's columns
100
+ target: GridCellRange // may reach past the last row
101
+ cellCount: number
102
+ extraRows: number // rows past the last row ("Add N rows")
103
+ droppedColumns: number // clipboard columns past the last column
104
+ readonlyColumns: number[] // target columns to skip
105
+ repeated: boolean // one copied cell filled the selected range
106
+ }
107
+ ```
108
+
109
+ The paste policy is the caller's. Studio applies 50 cells or fewer with no
110
+ type errors directly, as one undo entry, and previews larger pastes or pastes
111
+ with errors.
112
+
113
+ Everything is data: text that starts with `=`, `+`, `-` or `@` is copied and
114
+ pasted as plain text and never evaluated or rewritten.
115
+
116
+ A paste goes to the grid only when no built-in editor is open and the event
117
+ target is the focus owner or a hidden element; a visible input, textarea or
118
+ contenteditable in a caller's editor keeps its native paste. A read-only grid
119
+ ignores pastes.
120
+
121
+ For menu commands, `ref.copy(cut?)` copies through the async clipboard API
122
+ (call it inside the click handler) and `ref.paste(text)` builds a proposal
123
+ from text the caller read itself.
124
+
125
+ ## Touch and phone
126
+
127
+ Interaction-spec §14, adapted to Teable's touch grid. Two switches, kept
128
+ apart:
129
+
130
+ - **Touch UI**: `(pointer: coarse)`. No fill handle and no column resize
131
+ handle are drawn or hit (a 5 px edge is no finger target).
132
+ - **Phone**: touch UI and a grid no wider than 640 px (the container, not
133
+ the window; `PHONE_MAX_WIDTH`).
134
+
135
+ Each gesture follows its own `pointerType`: a mouse or pen on a touch-capable
136
+ device keeps every mouse gesture above, and a finger on a desktop gets the
137
+ finger path below.
138
+
139
+ ### Finger gestures
140
+
141
+ | Gesture | Phone (≤640 px) | Touch tablet (>640 px) |
142
+ |---|---|---|
143
+ | Swipe, any direction | Native pan with momentum (`touch-action: pan-x pan-y`). Never selects, never drags | Same |
144
+ | A press that stops a fling (within 100 ms of a scroll frame) | Only stops it | Same |
145
+ | Tap a cell (released within 6 px and before 250 ms) | Selects the cell and emits `row-expand(row, column)`: open the record sheet scrolled to that field. No editor | Selects the cell. No focus, so no keyboard |
146
+ | Tap the selected cell again | `row-expand` again | Edits: `text` and `number` open in place, take focus and bring up the keyboard. `long-text` and `custom` emit `edit-request` with `trigger: 'tap'`. Read-only cells open the read-only view |
147
+ | Return in a finger-opened editor | — | Commits, moves down, and drops focus so the keyboard goes away |
148
+ | Long press 250 ms (cancelled by a 6 px move) | `context-menu` at the press point, selecting the cell unless it is inside the selection. The release is not a tap | Same |
149
+ | Tap a column header | `header-menu` when the column has a menu, else selects the column | Same |
150
+ | Tap the row header | `row-expand(row)` | `row-expand(row)` with the `expand` control, else toggles the row checkbox. The whole header is one target |
151
+ | Second finger | Cancels the tap and the long press (pinch, two-finger pan) | Same |
152
+ | Range drag, fill, column resize, column or row reorder | None | None |
153
+
154
+ The pointer is cancelled by the browser when it starts panning; selection
155
+ happens on `pointerup` only. The tapped row or header keeps a pressed tint for
156
+ 300 ms. A double tap never edits twice: the compatibility `dblclick`, and
157
+ Android's own long-press `contextmenu`, are ignored for 800 ms after a finger.
158
+
159
+ ### Phone layout
160
+
161
+ - No frozen data column (the row-number gutter still stays).
162
+ - The gutter is 50 px with row numbers only: `rowControls` are not drawn and
163
+ not hit; a row-header tap opens the record.
164
+ - Wider screens, touch or not, cap frozen columns so the scrolling part keeps
165
+ at least 240 px (Teable's `getMaxFreezeColumnCount`).
166
+ - Row and header height follow the density tokens: the DSM 7 theme gives
167
+ 36 px on a coarse pointer unless the app sets `data-ui-density`.
168
+ - The grid draws no append row; the host offers its own add button.
169
+
170
+ ### What the host does
171
+
172
+ - `row-expand(row, column?)`: open the record. On a phone it is a bottom
173
+ sheet; when `column` is set, scroll the sheet to that field. Do not focus a
174
+ field on open.
175
+ - `highlightedRow`: pass the row whose record is open so it stays tinted while
176
+ the sheet or panel is open; `null` when closed.
177
+ - `edit-request` with `trigger: 'tap'`: the editor must fit the screen;
178
+ otherwise open the record sheet at that field.
179
+ - `context-menu` from a long press: show the row menu as a bottom sheet on a
180
+ phone (`AdaptivePanel`), a popover elsewhere. Copy, cut and paste go through
181
+ `ref.copy(cut?)` and `ref.paste(text)` from that menu: a finger never gives
182
+ the grid focus, so the keyboard clipboard events do not reach it.
183
+ - `header-menu` from a tap: same sheet-or-popover rule.
184
+
185
+ ## Accessibility
186
+
187
+ The grid is `role="grid"` with `aria-multiselectable="true"`. One element
188
+ takes focus: the focus owner, labelled by its column header, row header and
189
+ cell text. Selection changes do not move focus out of the grid. Toasts for
190
+ copy, fill and paste come from the caller, translated.
191
+
192
+ ## Release checklist (grid entry)
193
+
194
+ Before a release that changes `@synopackageland/ui/grid`:
195
+
196
+ 1. `pnpm exec vitest run tests/ux-28-grid-range-clipboard.test.ts tests/ux-grid-touch.test.ts tests/grid tests/grid-core`.
197
+ 2. `pnpm typecheck` and `pnpm build`.
198
+ 3. Browser checks on the built entry: `node tests/grid/browser-check.mjs`,
199
+ `node tests/grid/edit-browser-check.mjs`,
200
+ `node tests/grid/cells-browser-check.mjs`, and
201
+ `node tests/grid/range-browser-check.mjs <out> chromium` and `... webkit`.
202
+ The range check fails on any DOM read in pointermove, a range that does
203
+ not paint in the same task, auto-scroll that stops while the pointer
204
+ rests past the edge, a moved element between +100 ms and +1500 ms, or
205
+ scroll frames slower than 50 ms at p95; it records the `dist/grid.js` size.
206
+ 4. Touch: `node tests/grid/touch-browser-check.mjs <out>` (Chromium touch
207
+ emulation at 390x844 and 768x1024, real CDP touch input). It fails when a
208
+ swipe selects or has no momentum, a tap focuses, a phone tap does not open
209
+ the record, a tablet second tap does not edit, a finger edit keeps focus,
210
+ or a long press does not open exactly one context menu.
211
+ 5. `node scripts/check-package-content.mjs`: `dist/grid.js` imports only
212
+ `vue` and `./index.js` and keeps the Teable notice.
213
+ 6. Notices: `LICENSES/ux-grid-teable.md` and `LICENSES/ux-grid-baserow.md`
214
+ ship in the package.
215
+ 7. Manual, not covered headless: IME first character in Safari, native
216
+ macOS Cmd+C/X/V into Excel, Numbers and Google Sheets, and on a real
217
+ iPhone that a second tap brings up the keyboard and Return puts it away.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/ui",
3
- "version": "0.20.2",
3
+ "version": "0.20.4",
4
4
  "description": "Shared Vue 3 UI component library with a DSM 7 inspired application theme",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,8 +21,13 @@
21
21
  "docs/component-defaults.md",
22
22
  "docs/json-ui.md",
23
23
  "docs/layout-rules.md",
24
+ "docs/list-detail.md",
25
+ "docs/mobile-native.md",
26
+ "docs/release-checks.md",
24
27
  "docs/size-and-split.md",
25
28
  "docs/theme-hooks.md",
29
+ "docs/ux-foundation.md",
30
+ "docs/ux-grid.md",
26
31
  "docs/wizard.md"
27
32
  ],
28
33
  "bin": {
@@ -44,6 +49,10 @@
44
49
  "types": "./dist/rich-text.d.ts",
45
50
  "import": "./dist/rich-text.js"
46
51
  },
52
+ "./grid": {
53
+ "types": "./dist/grid/index.d.ts",
54
+ "import": "./dist/grid.js"
55
+ },
47
56
  "./json-catalog.schema.json": "./dist/json-catalog.schema.json",
48
57
  "./json-ai-prompt.md": "./dist/json-ai-prompt.md",
49
58
  "./theme.css": "./dist/theme.css",
@@ -71,13 +80,95 @@
71
80
  "@tiptap/vue-3": "3.32.1",
72
81
  "zod": "^4.6.5"
73
82
  },
83
+ "devDependencies": {
84
+ "@eslint/js": "10.0.1",
85
+ "@histoire/plugin-vue": "1.0.0-beta.1",
86
+ "@tailwindcss/vite": "4.3.3",
87
+ "@tanstack/vue-virtual": "3.13.39",
88
+ "@vitejs/plugin-vue": "6.0.9",
89
+ "@vue/test-utils": "2.5.1",
90
+ "@vueuse/core": "14.4.0",
91
+ "eslint": "10.11.0",
92
+ "eslint-plugin-vue": "10.11.1",
93
+ "flexsearch": "0.8.212",
94
+ "globals": "17.12.0",
95
+ "histoire": "1.0.0-beta.1",
96
+ "jsdom": "30.1.1",
97
+ "playwright": "1.58.2",
98
+ "reka-ui": "2.10.5",
99
+ "tailwindcss": "4.3.3",
100
+ "typescript": "5.9.3",
101
+ "typescript-eslint": "8.71.0",
102
+ "vite": "7.3.6",
103
+ "vite-plugin-dts": "5.1.1",
104
+ "vitest": "5.0.2",
105
+ "vscode-oniguruma": "2.0.1",
106
+ "vscode-textmate": "9.3.2",
107
+ "vue": "3.5.43",
108
+ "vue-eslint-parser": "10.4.1",
109
+ "vue-tsc": "3.3.11"
110
+ },
74
111
  "engines": {
75
112
  "node": ">=20"
76
113
  },
77
114
  "config": {
78
115
  "bundledRuntime": {
116
+ "@tanstack/vue-virtual": "3.13.39",
117
+ "@vueuse/core": "14.4.0",
79
118
  "reka-ui": "2.10.5"
80
119
  }
81
120
  },
82
- "homepage": "https://www.varvis.me/docs/"
83
- }
121
+ "homepage": "https://www.varvis.me/docs/",
122
+ "scripts": {
123
+ "check:docs": "node --test scripts/check-doc-versions.test.mjs && node scripts/check-doc-versions.mjs",
124
+ "dev": "vite build --watch",
125
+ "build": "vite build && vite build --mode json && vite build --mode rich-text && vite build --mode grid && vite build --mode cli && vite build --mode preview && vite build --mode embedded && node scripts/postbuild.mjs",
126
+ "lint": "eslint .",
127
+ "typecheck": "vue-tsc --noEmit -p tsconfig.json",
128
+ "test": "vitest run",
129
+ "test:watch": "vitest",
130
+ "test:changed": "echo \"test:changed skips the tests the changes since ${SYNOPKGLAND_GATE_BASE:-origin/master} do not reach (vitest --changed); pnpm check runs them all\" && vitest run --changed ${SYNOPKGLAND_GATE_BASE:-origin/master}",
131
+ "story": "histoire dev",
132
+ "story:build": "histoire build",
133
+ "check:host": "node scripts/dsm-host-shots.mjs",
134
+ "check:switch": "node scripts/check-switch.mjs",
135
+ "check:tabs": "node scripts/check-tabs.mjs",
136
+ "check:mobile": "node scripts/check-mobile-overflow.mjs",
137
+ "check:narrow": "node scripts/check-narrow-controls.mjs",
138
+ "check:drawer-form": "node scripts/check-drawer-form.mjs",
139
+ "check:responsive": "node scripts/check-responsive.mjs",
140
+ "check:adaptive": "node scripts/check-adaptive.mjs",
141
+ "check:sidebar-focus": "node scripts/check-sidebar-focus.mjs",
142
+ "check:json-render": "node scripts/check-json-render.mjs",
143
+ "check:motion": "node scripts/check-motion.mjs",
144
+ "check:topology": "node scripts/check-topology.mjs",
145
+ "check:wizard": "node scripts/check-wizard.mjs",
146
+ "check:edges": "node scripts/check-edges.mjs",
147
+ "check:data-grid": "node scripts/check-data-grid.mjs",
148
+ "check:truncation": "node scripts/check-truncation.mjs",
149
+ "check:link": "node scripts/check-link.mjs",
150
+ "check:text-select": "node scripts/check-text-select.mjs",
151
+ "check:date-picker": "node scripts/check-date-picker.mjs",
152
+ "check:full-page": "node scripts/check-full-page.mjs",
153
+ "check:detail-surface": "node scripts/check-detail-surface.mjs",
154
+ "check:internal": "node --test scripts/check-internal-content.test.mjs scripts/check-npm-metadata.test.mjs && node scripts/check-internal-content.mjs --repo .",
155
+ "check:npm-metadata": "node scripts/check-npm-metadata.mjs",
156
+ "check:package": "node scripts/check-package-content.mjs",
157
+ "check:ux-foundation-consumer": "node scripts/check-ux-foundation-consumer.mjs",
158
+ "notices:update": "node scripts/third-party-notices.mjs --write",
159
+ "notices:check": "node scripts/third-party-notices.mjs --check",
160
+ "check": "bash scripts/docker-check.sh \"npm run gates\"",
161
+ "check:fast": "bash scripts/docker-check.sh \"npm run gates\"",
162
+ "check:local-browser": "node scripts/app-local-browser.mjs",
163
+ "check:infotip": "node scripts/check-infotip.mjs",
164
+ "release:stage": "node scripts/stage-release.mjs",
165
+ "check:polish": "bash scripts/docker-check.sh --build --out .polish-shots 'node scripts/check-polish.mjs'",
166
+ "check:acceptance": "node scripts/check-acceptance.mjs",
167
+ "check:palette": "node scripts/check-palette.mjs",
168
+ "gates": "node scripts/gates.mjs",
169
+ "gates:full": "node scripts/gates.mjs --full",
170
+ "check:layout": "node scripts/check-layout.mjs",
171
+ "release:smoke": "node scripts/release-smoke.mjs",
172
+ "check:browser-install": "playwright install chromium"
173
+ }
174
+ }