@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
@@ -0,0 +1,39 @@
1
+ /** Card box metrics in px (layout-spec §2.6). VirtualCardGrid and CardField draw with these. */
2
+ export declare const CARD_METRICS: {
3
+ readonly border: 1;
4
+ readonly padBlock: 8;
5
+ readonly padInline: 12;
6
+ /** One title line (14/20). */
7
+ readonly titleLine: 20;
8
+ /** Space under the title, and between fields. */
9
+ readonly gap: 8;
10
+ /** Field name row: 16 px icon, 12 px muted text. */
11
+ readonly label: 16;
12
+ /** Between the name row and the value row. */
13
+ readonly labelGap: 4;
14
+ };
15
+ /** Value-row heights in px for the common field kinds (interaction-spec §5.1). */
16
+ export declare const CARD_VALUE_HEIGHT: {
17
+ /** One line of 13/20 text, a checkbox or a 20 px select chip. */
18
+ readonly line: 20;
19
+ /** Long text, clamped to two lines. */
20
+ readonly longText: 40;
21
+ /** A 24 px link chip. */
22
+ readonly chip: 24;
23
+ /** A row of 28 px attachment thumbnails. */
24
+ readonly attachment: 28;
25
+ };
26
+ export type CardHeightInput = {
27
+ /** Value-row height of each visible field in order, in px; see CARD_VALUE_HEIGHT. */
28
+ fields: readonly number[];
29
+ /** Cover box height in px; 0 when the view shows no cover. */
30
+ cover?: number;
31
+ /** Whether the card shows its title row. */
32
+ title?: boolean;
33
+ };
34
+ /**
35
+ * The height of every card in a view, computed from its configuration so the
36
+ * grid knows its rows before any record or image arrives. Each field is a name
37
+ * row, a value row and the gap after it; the last gap is not counted.
38
+ */
39
+ export declare function cardHeight({ fields, cover, title }: CardHeightInput): number;
@@ -0,0 +1,11 @@
1
+ /** Container width (px) at which the gallery gains its 2nd…6th column (layout-spec §2.6). */
2
+ export declare const GALLERY_BREAKPOINTS: readonly [640, 768, 1024, 1280, 1536];
3
+ /** Gallery columns for a container `width` px wide; 0 while the width is unknown. */
4
+ export declare function galleryColumns(width: number): number;
5
+ /**
6
+ * Item indices from `startRow` to `endRow` (inclusive) in a grid of `columns`,
7
+ * clipped to `count`: `[start, end)`.
8
+ */
9
+ export declare function rowItems(startRow: number, endRow: number, columns: number, count: number): [number, number];
10
+ /** The index a grid arrow, Home/End or Page key moves focus to from `index`; null for other keys. */
11
+ export declare function cardKeyTarget(key: string, index: number, columns: number, count: number, pageRows: number, ctrl?: boolean): number | null;
@@ -0,0 +1,30 @@
1
+ import { Ref } from 'vue';
2
+ /**
3
+ * Sanctioned layout motion: a side region (AI panel, sidebar rail) is changing
4
+ * the width of the main area on purpose. Width-dependent layouts inside main,
5
+ * such as the gallery column count, hold still while it is active and settle
6
+ * once when it ends (layout-spec §3.3).
7
+ */
8
+ export type LayoutMotion = {
9
+ /** True from the first begin until the last motion has ended. */
10
+ readonly active: Readonly<Ref<boolean>>;
11
+ /**
12
+ * Starts a motion and returns its end. Calling the end again does nothing,
13
+ * so an interrupted motion may end from several paths. Under reduced motion
14
+ * nothing starts.
15
+ */
16
+ begin: (el?: Element | null) => () => void;
17
+ /**
18
+ * Begins a motion for the CSS transition `el` is about to run and ends it on
19
+ * `transitionend`, on a `transitioncancel` that no new transition replaces,
20
+ * or after the transition's own duration when neither event arrives. Calling
21
+ * it again for the same element while it moves restarts the wait instead of
22
+ * adding a second motion. Call it right after changing the property.
23
+ */
24
+ track: (el: Element) => void;
25
+ };
26
+ export declare function createLayoutMotion(): LayoutMotion;
27
+ /** Gives the components below a motion of their own, for a layout that only moves its own region. */
28
+ export declare function provideLayoutMotion(motion?: LayoutMotion): LayoutMotion;
29
+ /** The nearest provided motion, else the document-wide one. */
30
+ export declare function useLayoutMotion(): LayoutMotion;
@@ -6,8 +6,8 @@ The same AppRoot + AppLayout supports three presentations:
6
6
  | AppLayout container width | Presentation | Layout |
7
7
  | --- | --- | --- |
8
8
  | 1–640px | `mobile` | Slide-out accessible navigation, tight page gutters, wrapped header actions, narrow-table cards |
9
- | 641–1199px | `window` | Original dense DSM window interior, same as before |
10
- | 1200px or wider, with `wide-layout="web"` | `web` | Full page: collapsible and resizable sidebar, heavier dividers, page content flush beside the sidebar at DSM window padding (no page card), lifted cards on flat pages |
9
+ | 641–959px (any width above 640 without `wide-layout="web"`) | `window` | Original dense DSM window interior, same as before |
10
+ | 960px or wider, with `wide-layout="web"` | `web` | Full page: 48px chrome rows, collapsible and resizable sidebar (it starts as the 48px rail at 960–1199), page content flush beside the sidebar at DSM window padding (no page card), lifted cards on flat pages |
11
11
 
12
12
  A maximized DSM window is still a DSM window: without `wide-layout="web"`,
13
13
  AppLayout stays in `window` mode at any width above 640px. Only a full-page
@@ -41,15 +41,21 @@ scales it down, and AppLayout never sees a phone width. When the app owns the
41
41
  whole page, call `ensureDeviceViewport()` once and opt into `web`; leave both
42
42
  off inside a DSM desktop window:
43
43
 
44
- ```ts
45
- import { ensureDeviceViewport } from '@synopackageland/ui'
46
-
47
- // window.js passes whether DSM launched the AppWindow standalone
48
- // (appWindow.isStandaloneMainWindow?.() or _S('standalone')).
49
- const restoreViewport = fullPage ? ensureDeviceViewport() : () => {}
50
- // <AppLayout :wide-layout="fullPage ? 'web' : 'window'" sidebar-storage-key="myapp">
44
+ ```vue
45
+ <!-- window.js passes whether DSM launched the AppWindow standalone
46
+ (appWindow.isStandaloneMainWindow?.() or _S('standalone')). -->
47
+ <AppRoot :full-page="fullPage">
48
+ <AppLayout :wide-layout="fullPage ? 'web' : 'window'" sidebar-storage-key="myapp">
51
49
  ```
52
50
 
51
+ `AppRoot full-page` calls `ensureDeviceViewport()` itself and restores the
52
+ page on unmount; a host without an `AppRoot` can call it directly. The
53
+ viewport it writes is
54
+ `width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content`
55
+ (a device-width viewport already on the page keeps its keys and gains only the
56
+ missing two). Zoom is never locked; fields avoid iOS focus zoom by their 16px
57
+ text instead.
58
+
53
59
  A standalone AppWindow should also drop its desktop `minWidth` (for example
54
60
  `minWidth: fullPage ? 320 : 700`) so a phone-width page can reach mobile mode.
55
61
 
@@ -59,12 +65,66 @@ for a viewport change. `ensureDeviceViewport()` therefore fires one `resize`
59
65
  on the next frame after it changes the viewport (and after it restores it), so
60
66
  the window narrows to the phone's width.
61
67
 
68
+ ## Viewport, keyboard and Back on a phone
69
+
70
+ A full-page `AppRoot` is the page: it is `100dvh` tall, `html` and `body`
71
+ never scroll and have no overscroll (so no pull-to-reload), and only scrollers
72
+ inside the app scroll. There is no document scroll for iOS Safari to push up
73
+ behind the keyboard. Only use `full-page` when nothing else shares the page
74
+ (a Portal that hides DSM's banner, or a standalone launch).
75
+
76
+ Every `AppRoot`, full page or not, publishes the visual viewport on its root.
77
+ The variables are read-only and updated once per frame:
78
+
79
+ | Variable | Value |
80
+ |---|---|
81
+ | `--ui-vvh` | the visual viewport height (what is on screen above the keyboard) |
82
+ | `--ui-vv-top` | how far iOS has panned the visual viewport down |
83
+ | `--ui-kb-inset` | the height the keyboard covers at the bottom of the layout: the iOS keyboard; 0 on Android, where `interactive-widget=resizes-content` shrinks the layout itself |
84
+ | `data-ui-keyboard="open"` | a text field has focus and the viewport lost at least 60px to the keyboard (smaller changes are the URL bar) |
85
+
86
+ Rules for surfaces:
87
+
88
+ - A bar pinned to the bottom (sticky form actions, a composer, a footer) uses
89
+ `bottom: var(--ui-kb-inset, 0px)`; one formula works on iOS and Android.
90
+ - A scroller with a sticky footer sets `--ui-scroll-footer: <footer height>`.
91
+ While the keyboard is open, the scroller holding the focused field gets
92
+ `data-ui-kb-room`: its `scroll-padding-bottom` becomes keyboard, footer and
93
+ 16px, and on iOS it gets a spacer so the last field can scroll above the
94
+ keyboard. The field is scrolled into view inside that scroller, never by
95
+ scrolling the window. A container that aligns its own fields (the reka
96
+ Drawer keyboard provider) opts out with `data-ui-keyboard-reveal="off"`.
97
+ - Programmatic focus uses `focusWithoutScroll(element)`:
98
+ `focus({ preventScroll: true })`, then the same in-scroller reveal.
99
+ - `useSoftKeyboard()` is true at once when a text field gains focus on a touch
100
+ device, and stays true only if the viewport confirms a keyboard within
101
+ 800ms; with a hardware keyboard it turns false again.
102
+
103
+ `useBackDismiss(open, close, { enabled })` gives an open phone sheet, drawer
104
+ or pushed page one `history.state` entry (the URL does not change; a router's
105
+ state keys are copied through). The Android back button or the iOS edge swipe
106
+ calls `close` for the top surface only; closing by code removes the entry.
107
+ Pass `enabled: false` where the surface is a popover or inside a DSM window.
108
+
109
+ `node scripts/check-keyboard.mjs [out-dir]` (after `pnpm build`) checks all of
110
+ this in Chromium at 390 and 768 with touch, with Android emulated by a
111
+ viewport resize and iOS by a stand-in visual viewport. A real-device pass on
112
+ iOS Safari and Android Chrome is still needed (spec D6).
113
+
62
114
  ## Web presentation sidebar
63
115
 
64
- In `web`, the sidebar gets a collapse button at its foot and a resize handle
65
- on its right edge:
116
+ In `web`, the sidebar's 48px head row ends with a collapse button, and a
117
+ resize handle sits on its right edge:
118
+
119
+ - At 960–1199px the sidebar starts as the rail. Opening it there lasts until
120
+ the container leaves that band and is not stored, so the wide-screen
121
+ preference stays as the user left it.
122
+ - Only the sidebar's width moves (`--ui-motion-panel`, ease-in-out); its rows
123
+ keep the target width inside, so labels never re-wrap while it opens or
124
+ closes. Nothing moves before the first paint (the stored width and collapse
125
+ are read during setup) or while the handle is dragged.
66
126
 
67
- - Collapsed, AppSidebar shows icons only; each label becomes a right-side
127
+ - Collapsed (48px rail), the head row keeps only the expand button and AppSidebar shows icons only; each label becomes a right-side
68
128
  tooltip and stays the row's accessible name. Badges move to the icon corner.
69
129
  - The handle (`role="separator"`) resizes between 200 and 360px (default 248px)
70
130
  by dragging, or with Arrow Left/Right (16px), Home and End when focused.
@@ -79,14 +139,14 @@ on its right edge:
79
139
 
80
140
  A full page has no DSM title bar, so AppLayout shows the app's logo and name:
81
141
  at the top of the sidebar of a `wide-layout="web"` page at any width above a
82
- phone's (logo only when the web sidebar is collapsed), and in the phone bar
142
+ phone's (not in the collapsed rail, whose head row holds the expand button), and in the phone bar
83
143
  and at the top of the drawer. A DSM window shows neither, since its title bar
84
144
  already does.
85
145
 
86
146
  - `brand-name` and `brand-icon` (a URL) name the app. Without them AppLayout
87
147
  uses the page's `document.title` and `<link rel="icon">`; with none of these
88
148
  there is no brand row. The `#brand` slot replaces the default content.
89
- - The `#sidebar-footer` slot sits beside the collapse button in web and at the
149
+ - The `#sidebar-footer` slot is the sidebar's 48px foot in web and the
90
150
  bottom of the phone drawer (not in a DSM window). `AppSidebarUser`
91
151
  (`name`, optional `detail`) shows the signed-in account as an initial avatar;
92
152
  collapsed, only the avatar remains, with a tooltip.
@@ -100,6 +160,67 @@ already does.
100
160
  </AppLayout>
101
161
  ```
102
162
 
163
+ ## Aside: the right side region
164
+
165
+ The `#aside` slot is a region at the right edge of the shell, full height,
166
+ for a work panel such as an assistant (`AsidePanel` draws the panel; the
167
+ layout only sizes and moves the region). `v-model:aside` is its state:
168
+
169
+ | State | Region width | Page |
170
+ | --- | --- | --- |
171
+ | `collapsed` (default) | the 48px rail | takes the rest |
172
+ | `open` | `v-model:aside-width` (380, 320–560; the handle on its start edge, Arrow keys, Home and End) | takes the rest |
173
+ | `open` that would leave the page under 480px, or outside `web` | the 48px rail; the panel floats over the page (`overlay`) | unchanged |
174
+ | `expanded` | the whole content area beside the sidebar | `inert`, kept mounted |
175
+
176
+ On a phone the region has no width; its content places itself (a button and a
177
+ bottom sheet). The slot receives `state`, `overlay`, `width` and
178
+ `presentation`; the region sets `--ui-aside-open-width` so the panel can keep
179
+ its width while the region moves. Dragging writes the width on release.
180
+
181
+ ### AsidePanel
182
+
183
+ `AsidePanel` is the slot's content. It reads the state from the layout (no
184
+ props to forward) and writes it back through `v-model:aside`:
185
+
186
+ ```vue
187
+ <script setup lang="ts">
188
+ import { AppLayout, AsidePanel, useAsidePreference } from '@synopackageland/ui'
189
+ // Read during setup, so the first paint has the stored state; the app picks the scope.
190
+ const { state, width } = useAsidePreference('studio.aside', 'editor', { state: 'open' })
191
+ </script>
192
+
193
+ <AppLayout v-model:aside="state" v-model:aside-width="width" wide-layout="web">
194
+ ...
195
+ <template #aside>
196
+ <AsidePanel :title="t('IT assistant')" :icon="Sparkles" shortcut="Ctrl+J">
197
+ <template #tabs>…a SegmentControl size="sm", only with more than one surface…</template>
198
+ <template #actions>…a ⋯ menu…</template>
199
+ …the thread…
200
+ <template #composer>…the composer…</template>
201
+ </AsidePanel>
202
+ </template>
203
+ </AppLayout>
204
+ ```
205
+
206
+ | Mode | When | Panel |
207
+ | --- | --- | --- |
208
+ | docked | `web`, the page keeps 480px | keeps `--ui-aside-open-width` and slides by `translate` while the region's width moves (`--ui-motion-panel`, ease-in-out both ways); the rail button fades in 150ms into a collapse |
209
+ | floating | an open panel the page has no room for, or a 641–959 container | slides over the page from the rail (in `panel` ease-out, out `normal` ease-in); the region keeps its 48px |
210
+ | sheet | a phone (≤640) | a 48px button at the end corner (above the tab bar and the safe area) opens a 90% bottom sheet; `expanded` is full screen; the scrim, Escape and a downward swipe on the header close it; the page is inert |
211
+
212
+ - The panel is never unmounted: collapsed, it is `inert` and `aria-hidden`, and the thread's scroll and the composer's draft stay. The page is `inert` while the panel is expanded or a sheet is up.
213
+ - Focus moves to the composer's first field on open (not on touch, `initial-focus` changes it) and back to the rail button or the phone button on collapse.
214
+ - Escape: docked and expanded leave it to the content (stop a reply). A floating panel with focus inside, or a sheet, collapses on Escape after any layer opened above it (a menu, a popover) has closed.
215
+ - No transition runs before the first paint has settled. The state needs no `transitionend`: only a floating panel's exit waits for one, with the `normal` token as its deadline, and a new state change or reduced motion ends it at once.
216
+ - Labels: "Open {name}", "Collapse {name}", "Expand {name}" and "Restore {name}" with the title, in the nine locales; `open-label`, `collapse-label`, `expand-label` and `restore-label` replace them. The app binds its shortcut and calls the exposed `toggle()`.
217
+ - `useAsidePreference(key, scope, defaults)` keeps `{"v":1,"<scope>":{"state","width"}}` in localStorage, written on each change (the width only when a drag ends); other fields in a scope's entry are kept. `readAsidePreference` reads it synchronously for a pre-paint script.
218
+
219
+ Both side regions share one motion signal: `inject(APP_SIDE_MOTION_KEY)` is
220
+ true while the sidebar or the aside changes width (the shell also carries
221
+ `data-ui-side-moving`). Content that measures itself (a canvas grid, a chart,
222
+ a column count) waits for false and then measures once.
223
+
103
224
  ## Usage
104
225
 
105
226
  ```vue
@@ -124,7 +245,7 @@ import {
124
245
  ```
125
246
 
126
247
  - `AppLayout` defaults to `wide-layout="window"`. Set `wide-layout="web"` on
127
- a full-page Portal to enable the full-page layout at 1200px or wider.
248
+ a full-page Portal to enable the full-page layout at 960px or wider.
128
249
  - `AppLayout` defaults to `responsive=true`. Set `:responsive="false"`
129
250
  only if the host application intentionally implements its own complete responsive shell.
130
251
  - `PageContent surface="auto"` puts the page straight on the content area: flush
@@ -235,7 +356,7 @@ any ancestor. Light defaults and the dark values the workbench uses:
235
356
  | `--ui-app-web-surface` | `var(--ui-surface)` | `var(--ui-surface)` (`#1f2832`) |
236
357
  | `--ui-app-web-border` | `var(--ui-border)` | `#34404d` |
237
358
  | `--ui-app-web-shadow` | `var(--ui-shadow-card)` | `var(--ui-shadow-card)` |
238
- | `--ui-web-sidebar-collapsed-width` | `64px` | same |
359
+ | `--ui-web-sidebar-collapsed-width` | `48px` | same |
239
360
 
240
361
  `AppLayout navigation="top"` puts the navigation in a bar above the page in the
241
362
  window and web presentations (a phone keeps the drawer); its colours and the
package/docs/callout.md CHANGED
@@ -6,7 +6,7 @@ has its own component, and none of them is a full-width banner.
6
6
 
7
7
  | The message | Use | Shape |
8
8
  | --- | --- | --- |
9
- | A passing result: "Settings applied", "The check passed", "Copied" | `useToast()` | A toast that fades after 4.5 s. Do not leave a Callout behind for it. |
9
+ | A passing result: "Settings applied", "The check passed", "Copied" | `useToast()` | A toast at the bottom right: success fades after 3 s, info and warning after 5 s (at least 5 s with an action), an error stays until closed. Do not leave a Callout behind for it. |
10
10
  | Something the user has to act on: a missing setting, a failed connection, a sign-in that waits | `<Callout>` | One-line title, at most one line of text, one action. |
11
11
  | A risk or a limit of a feature | `<Callout variant="inline">` | One short sentence; longer text in `#details` behind "Learn more", or a link. |
12
12
  | A state of the page that fits its toolbar row: "Two devices are offline", "Read only" | `<Callout variant="hint">` | The icon and a few words on one line; the details in a tooltip. |
@@ -160,6 +160,27 @@ and without an action, light and dark, and on a settings page:
160
160
  toast.success('Settings applied', 'Added port 6690')
161
161
  ```
162
162
 
163
+ - Work in progress keeps one toast id from start to result. A pending toast
164
+ shows only if the work takes longer than 300 ms, has no timer, and turns
165
+ into its result in place; an error's Retry can turn it back into pending:
166
+
167
+ ```ts
168
+ const id = toast.pending('Deleting')
169
+ try {
170
+ await remove()
171
+ toast.update(id, { title: 'Deleted', tone: 'success', actionLabel: 'Undo', onAction: restore })
172
+ } catch {
173
+ toast.update(id, { title: 'Could not delete', tone: 'danger', actionLabel: 'Retry', onAction: retry })
174
+ }
175
+ ```
176
+
177
+ An action click closes the toast unless its handler updated it. Timers
178
+ pause while the pointer is over the stack or focus is in it; F8 moves
179
+ focus to the stack, so its actions are reachable by keyboard.
180
+ - A status that belongs to a place on the page (a form's "Saving" / "Saved",
181
+ a field's error with Retry) goes in a `StatusSlot`: a fixed box rendered
182
+ from the start and empty while idle, so the status never pushes content.
183
+
163
184
  - Put a machine error code in `code`, not in the text.
164
185
  - Stack at most two Callouts in one section. More means the page should show
165
186
  the state in its content (a `Badge`, a `KeyValueRow`) instead.
@@ -0,0 +1,54 @@
1
+ # ListDetail
2
+
3
+ A list with the selected item's detail beside it, as DSM's own list pages show
4
+ one (a Recycle Bin page with the selected shared folder's oldest items, a
5
+ Packages page with the selected package's advice).
6
+
7
+ ```vue
8
+ <ListDetail :title="selected?.name" storage-key="recycle" @close="selectedKeys = []">
9
+ <DataGrid v-model:selected-keys="selectedKeys" :columns="columns" :rows="rows" row-key="id" single-select footer />
10
+ <template v-if="selected" #detail>
11
+ <p>{{ selected.summary }}</p>
12
+ <DataGrid :fill="false" :columns="itemColumns" :rows="items" row-key="id" footer />
13
+ </template>
14
+ </ListDetail>
15
+ ```
16
+
17
+ - The default slot is the list; it takes the room the pane leaves and keeps at
18
+ least `list-min-width` (360px).
19
+ - The pane shows while the `detail` slot is given (a `v-if` on the slot opens
20
+ and closes it), or while `open` is true. With `title` it has a header with the
21
+ title and a Close button that emits `close`; `:closable="false"` drops the
22
+ button and a `header` slot replaces the title.
23
+ - The pane is white with a 1px border and no shadow. It is not a `Surface`, so
24
+ an `auto` page stays white when it opens and only the selected row is
25
+ tinted, even when another App built with an older release shares the DSM
26
+ document ([adaptive layout](adaptive-layout.md)).
27
+ - The separator between list and pane is dragged with the pointer
28
+ (`col-resize` cursor) or moved with the keyboard: ArrowLeft widens the pane
29
+ by 16px and ArrowRight narrows it (64px with Shift), Home makes it as wide
30
+ as the list allows and End as narrow as `min-width` (280px). A double click
31
+ goes back to the default. It is a `role="separator"` with
32
+ `aria-orientation="vertical"`, `aria-controls` naming the pane, and the
33
+ pane's width in px as `aria-valuenow` between `aria-valuemin` and
34
+ `aria-valuemax`.
35
+ - `default-width` is the width before the user sets one: a share of the room
36
+ (default `0.4`, kept between 400px and 640px) or a width in px.
37
+ - The width the user sets is remembered in `localStorage` under
38
+ `syno-pkg-land-ui:detail-width:<app>:<storage-key>`, where `<app>` is the
39
+ DSM package (or the page's host and directory) as for
40
+ [closed callouts](callout.md). Without `storage-key` the App's panes share
41
+ one width. An unreadable or blocked storage falls back to the default.
42
+ - Narrower than 640px of room the pane goes above the list, full width, and
43
+ the separator is hidden.
44
+ - In the pane, give tables column widths that fit the default width and dates
45
+ in a one-line short format; a table wider than the pane scrolls sideways.
46
+
47
+ Hooks: `data-ui-list-detail` (with `data-open` and `data-dragging`),
48
+ `data-ui-list-detail-list`, `data-ui-list-detail-separator`,
49
+ `data-ui-list-detail-pane`, `data-ui-list-detail-close`; the pane's width is
50
+ `--ui-list-detail-width`.
51
+
52
+ | Default width (1320px window) | Dragged wider | Wide window (1860px) |
53
+ |---|---|---|
54
+ | ![](screenshots/detail-after.png) | ![](screenshots/detail-dragged.png) | ![](screenshots/detail-wide.png) |
@@ -0,0 +1,214 @@
1
+ # Mobile-native rules
2
+
3
+ How the UI SDK behaves on a phone (390×844) and a touch tablet (768×1024).
4
+
5
+
6
+ ## Signals
7
+
8
+ | Signal | Where it comes from | What keys on it |
9
+ |---|---|---|
10
+ | Narrow | the app root is 640px wide or narrower: AppRoot sets `data-ui-presentation="mobile"`, AppLayout `data-ui-layout-mode="mobile"` | layout shape: drawer, sheets (as before) |
11
+ | Phone | narrow **and** `(pointer: coarse)` | phone tokens (type, control height, spacing, rows, header) |
12
+ | Touch | `@media (pointer: coarse)` | 16px fields, 44px tap boxes, tap feedback, touch-tablet control sizes |
13
+ | Hover | `@media (hover: hover)` | every hand-written `:hover` rule in the theme |
14
+
15
+ A touch tablet at 768px gets the touch rules but not the phone tokens. A
16
+ desktop gets neither, so it looks exactly as before. That includes a DSM
17
+ window or browser window narrowed below 640px and used with a mouse: it keeps
18
+ the narrow layout with desktop sizes. The phone tokens need a coarse pointer
19
+ too, because without that gate the 1280 gallery's narrow solo shots changed.
20
+
21
+ Every touch rule is keyed on `[data-ui-app]`, the root every app has (AppRoot).
22
+ Before this change they were keyed on `[data-ui-theme='dsm7']`. Nothing sets
23
+ that attribute (Studio sets its own theme name), so the rules never applied.
24
+
25
+ ## Tokens and base guards (M-1)
26
+
27
+ ### Type and size tokens
28
+
29
+ Each size is one token, so changing a size changes one line. Phone values are
30
+ set in `src/theme/adaptive.css` (the phone block), touch values in the
31
+ `@media (pointer: coarse)` blocks of `adaptive.css` and `dsm7.css`, and
32
+ desktop values in `dsm7.css`. An explicit `data-ui-density` keeps the app's
33
+ control and grid sizes; it does not change the 16px field rule.
34
+
35
+ | Token | Desktop | Touch tablet | Phone | Used for |
36
+ |---|---|---|---|---|
37
+ | `--ui-font-size` | 13 | 13 | **15** | primary text: list and record titles, field values, body copy, menu items |
38
+ | `--ui-font-size-sm` | 12 | 12 | **13** | secondary text: labels, meta, captions, helper text, badges |
39
+ | `--ui-font-size-xs` (new) | 12 | 12 | 12 | the smallest text anywhere |
40
+ | `--ui-line-height` | 20 | 20 | **22** | body line |
41
+ | `--ui-font-size-title` | 16 | 16 | **17** | header and dialog titles |
42
+ | `--ui-grid-font-size` (new) | 13 | 13 | **14** | grid cell text (not yet read by the canvas grid; see below) |
43
+ | `--ui-font-size-field` (new) | 16 | 16 | 16 | every field on a coarse pointer, to stop iOS focus zoom |
44
+ | `--ui-control-md` | 28 | **36** | **44** | fields, selects, form and sheet buttons |
45
+ | `--ui-control-sm` | 24 | **32** | **36** | inline and toolbar buttons (the tap box comes from `--ui-hit-min`) |
46
+ | `--ui-hit-min` (new) | 0 | **44** | **44** | smallest tap box |
47
+ | `--ui-tap-gap` (new) | 0 | **8** | **8** | room between tap targets; `--ui-icon-button-gap` reads it on touch |
48
+ | `--ui-grid-row-height` / `-header-height` | 28 | **36** | **32** | canvas grid rows (docs/ux-grid.md) |
49
+ | `--ui-list-row-height` (new) | 32 | **44** | **48** | list rows |
50
+ | `--ui-menu-row-height` | 32 | **40** | **48** | menu, select and sheet list rows |
51
+ | `--ui-page-padding-x` / `-y` | 16 (narrow layout 12) | 16 | **16** | page gutter |
52
+ | `--ui-section-gap` | 12 | 12 | **20** | space between sections |
53
+ | `--ui-card-padding` | 20 (Card default) | 20 | **16** | card inset |
54
+ | `--ui-chrome-height` | 44 (web 48, narrow layout 44) | – | **48** | the phone header |
55
+ | `--ui-radius-sheet` (new) | 12 | 12 | 12 | top corners of a bottom sheet |
56
+ | `--ui-safe-top/right/bottom/left` (new) | `env(safe-area-inset-*, 0px)` | same | same | notch and home indicator; non-zero only once the viewport meta has `viewport-fit=cover` (M-2) |
57
+ | `--ui-pressed` (new) | 8% of the text colour | same | same | the pressed tint |
58
+
59
+ Density comes from spacing, not smaller text: phone rows are 44–48px, and
60
+ components add no padding beyond the tokens.
61
+
62
+ Phone motion tokens (all read 0ms under reduced motion or `data-ui-motion="off"`):
63
+
64
+ | Token | Value | Use |
65
+ |---|---|---|
66
+ | `--ui-motion-sheet-in` / `-out` | 280 / 200ms | bottom sheet |
67
+ | `--ui-motion-sheet-ease` | `cubic-bezier(0.32, 0.72, 0, 1)` | sheet, push |
68
+ | `--ui-motion-drawer-in` / `-out` | 240 / 180ms | side drawer |
69
+ | `--ui-motion-push` / `-pop` | 280 / 240ms | push navigation |
70
+ | `--ui-motion-spring-back` | 180ms | a sheet released above its close threshold |
71
+ | `--ui-motion-press` | 150ms | pressed tint fading out (it shows at once) |
72
+
73
+ ### Base guards (`preflight.css`)
74
+
75
+ These rules are in the base layer at zero specificity (`:where()`), so any
76
+ component rule overrides them.
77
+
78
+ - No blanket `min-width: 0`. The spec asked for one, but the 1280 gallery
79
+ diff showed it squeezing nowrap rows (Space "wrap off" went one letter per
80
+ line) and a fixed-size brand icon on the desktop. A component whose flex
81
+ child must shrink sets `min-w-0` on it instead (M-13 handles the remaining
82
+ width offenders).
83
+ - `overflow-wrap: break-word` on the app root. A word longer than its line
84
+ breaks. We use `break-word`, not `anywhere`, because it only breaks text
85
+ that would overflow and never changes how wide a table column or flex item
86
+ wants to be. Desktop layouts stay the same.
87
+ - `img`, `video` and `iframe` get `max-width: 100%`, and `img` and `video` also
88
+ get `height: auto`. `svg` is left alone so icons keep their size.
89
+ - Don't write `100vw` in components. Use `100%` or the viewport variables from
90
+ M-2. The `check:mobile` script (M-12) enforces this.
91
+
92
+ ### Tap feedback (`touch.css`)
93
+
94
+ - Buttons, links, labels and every element with a button, tab, option,
95
+ menuitem, switch, checkbox, radio or treeitem role get
96
+ `-webkit-tap-highlight-color: transparent` and `touch-action: manipulation`.
97
+ This removes the grey flash and the 300ms double-tap wait. Pinch zoom still
98
+ works. A component's own `touch-none` overrides it.
99
+ - Pressed tint on a coarse pointer: an inset `--ui-pressed` shadow appears
100
+ while the finger is down and fades over `--ui-motion-press`. Button and
101
+ IconButton draw their own pressed state (`data-variant`), so they're
102
+ skipped. A component that sets its own shadow or `active:` style also keeps
103
+ its own.
104
+ - Hover: every hand-written `:hover` rule in the theme sits inside
105
+ `@media (hover: hover)`, so a tap leaves no stuck hover look. Tailwind's
106
+ `hover:` utilities already do this.
107
+
108
+ ### Hit area (`hit-area.css`)
109
+
110
+ On a coarse pointer, a control smaller than `--ui-hit-min` keeps its drawn
111
+ size, and an invisible `::before` grows its tap box evenly on all sides to the
112
+ minimum:
113
+
114
+ ```css
115
+ inset-block: min(0px, calc((100% - var(--ui-hit-min)) / 2));
116
+ inset-inline: min(0px, calc((100% - var(--ui-hit-min)) / 2));
117
+ ```
118
+
119
+ These controls get the hit area:
120
+
121
+ - **Marked with `data-ui-hit`:** `Button`, `IconButton`
122
+ - **Matched by role or attribute:** `button[role=checkbox]` (Checkbox),
123
+ `button[role=radio]` (Radio, ToggleButton), `button[role=switch]` (Switch),
124
+ `[data-tag][role=button]` (a tappable Tag), `[data-tag-remove]`,
125
+ `[data-ui-password-toggle]`
126
+
127
+ A new tappable control can opt in by adding `data-ui-hit`. When two tap boxes
128
+ overlap, the later control wins the overlap. Clusters keep `--ui-tap-gap`
129
+ between controls. The rule sits in the base layer, so a component that sets
130
+ its own `position` or draws its own `::before` keeps it, and loses the larger
131
+ tap box.
132
+
133
+ ### Not done in this layer
134
+
135
+ - **Hardcoded font sizes:** components that write a literal size, such as
136
+ `text-[13px]` in Checkbox, Switch, Radio, Input and the menu rows, don't
137
+ pick up the 15px phone text until they read `--ui-font-size`. Their owners
138
+ (M-5, M-10, M-13) switch them over. Fields are already 16px on touch.
139
+ - **Canvas grid:** the grid draws text at fixed sizes from
140
+ `src/grid-core/theme.ts`. Reading `--ui-grid-font-size` needs a change in
141
+ the grid.
142
+ - **Form gap:** `Form` writes its own `--ui-form-gap` inline, from
143
+ `--ui-field-gap`. The 16px phone form gap belongs to Form (M-10).
144
+ - **Hover rules in `adaptive.css`:** the brand action, sidebar collapse and
145
+ resize handles still use plain `:hover`. They belong to the shell tasks.
146
+
147
+ ## Floating layer (M-3a)
148
+
149
+ Every anchored popup is placed by one preset, `src/components/primitives/popover/floating.ts`, on Reka's popper (floating-ui flip, shift, size and hide). A component binds it on its Reka `*Content` and never picks its own collision props.
150
+
151
+ | Setting | Value |
152
+ |---|---|
153
+ | Boundary | the app window in app scope (`useCollisionBoundary`), else the visual viewport (Reka's root boundary for fixed popups) |
154
+ | Padding | 8px from each edge, plus `--ui-safe-*` (or `env(safe-area-inset-*)`), plus `--ui-kb-inset` at the bottom. Read when the popup opens and again on a visual-viewport resize |
155
+ | Shift | `sticky: 'partial'` |
156
+ | Detached anchor | `hideWhenDetached`: the popup hides once its anchor has left the boundary |
157
+ | Max size | `FLOATING_SIZE_CLASS`: `--reka-popper-available-width` and `-height`. Never `100vw` |
158
+ | Autofocus | none on a coarse pointer (`skipAutoFocusOnTouch` in `overlay.ts`) |
159
+
160
+ ```vue
161
+ <script setup lang="ts">
162
+ import { FLOATING_SIZE_CLASS, useFloating } from '@/components/primitives/popover/floating'
163
+ const floating = useFloating(() => trigger.value?.$el, () => open.value)
164
+ </script>
165
+ <SomeContent v-bind="floating" :class="[ownClasses, FLOATING_SIZE_CLASS]" />
166
+ ```
167
+
168
+ **Portal layer.** `src/theme/floating.css` makes the AppRoot `[data-ui-portals]` layer `position: absolute; inset: 0; overflow: clip; pointer-events: none`. It covers the app root exactly, so app-local dialogs keep the app as their containing block, and nothing in it can add page width or height. Its direct children take presses again; Reka's inline `pointer-events` still wins while a modal surface is open.
169
+
170
+ **Tooltip on touch.** `openOnTap` defaults to on with a coarse pointer: a tap toggles the tooltip and a tap anywhere else closes it. Pass `:open-on-tap="false"` when the tap must only run the trigger's action.
171
+
172
+ **Uses it today:** Popover, Tooltip, OverflowTooltip and all anchored popups listed below.
173
+
174
+ ## All anchored popups (M-3b)
175
+
176
+ Menus (including nested menus and Breadcrumb), Select and the shared ComboboxField,
177
+ DatePicker, ColorPicker, InputSteppers presets, InfoTip, ValidationPopup,
178
+ MentionList and OverflowToolbar now use the floating preset above. SplitButton,
179
+ MultipleSelect and SuperboxSelect inherit it through their shared primitives.
180
+ Menu sublevels inherit the parent's live padding, so an open submenu follows
181
+ safe-area and keyboard changes. Preferred and trigger widths are capped by
182
+ `--reka-popper-available-width`; lists scroll within the available height.
183
+ Opening a searchable Select, calendar or palette on touch does not autofocus
184
+ an input or swatch. Phone bottom sheets and inline validation remain M-5 work.
185
+
186
+ ## Toast, notification and hover-only controls (M-11)
187
+
188
+ `usePhone()` (`src/composables/phone.ts`) is the phone signal in script: the
189
+ enclosing AppRoot is narrow and the pointer is coarse, the same test as the
190
+ phone tokens. A provider sets `data-ui-phone` from it, and the placement rules
191
+ are in `src/feedback/feedback.css`.
192
+
193
+ | | Desktop, tablet, narrow window with a mouse | Phone |
194
+ |---|---|---|
195
+ | Toast | bottom right, 360px, swipe right, a stack | bottom, 8px from each side, above the visible tab bar or the keyboard (`--ui-kb-inset`), whichever is higher, plus `--ui-safe-bottom` and 8px; swipe down; only the newest shows, with a `+N` count of the others still open |
196
+ | Notification | the chosen corner, 360px | top, 8px from each side, below `--ui-safe-top` plus 8px; `alignment` is ignored |
197
+
198
+ - Toasts behind the front one keep their own timers, so a short one can
199
+ expire unseen. Danger toasts stay until closed and come forward in turn.
200
+ - Toast and notification text reads `--ui-font-size` and
201
+ `--ui-font-size-sm` (13/12 on a desktop, 15/13 on a phone). A static
202
+ Notification card no longer has a 320px minimum width.
203
+ - Neither provider writes `100vw` any more.
204
+
205
+ **Hover-only controls.** VirtualCardGrid card actions, the ScheduleTable edit
206
+ pen and the TypoHeader anchor carry `[@media(hover:none)]:opacity-100`, so a
207
+ touch screen shows them at rest. A component that hides a control until hover
208
+ does the same.
209
+
210
+ **Card long press.** On touch, holding a VirtualCardGrid card for 250ms opens
211
+ the card's context menu (`menuItems`) through Reka's long press
212
+ (`ContextMenu` `pressOpenDelay`). The click that ends that press doesn't open
213
+ the card. A card without entries opens no menu. The menu is the plain
214
+ context menu for now; it becomes a phone sheet through M-5's rule.
@@ -0,0 +1,23 @@
1
+ # Release checks
2
+
3
+ `npm run gates` runs basic release checks. `npm run gates:full` adds browser,
4
+ workbench and registry consumer integrations only when explicitly requested.
5
+ `SPL_FULL_GATE=1` aliases `--full`. `npm run gates -- --plan` prints the exact
6
+ selection without running checks. `pnpm check` and `pnpm check:fast` run basic
7
+ checks through the optional build-isolation wrapper.
8
+
9
+ | Entry point | Default | Explicit full |
10
+ | --- | --- | --- |
11
+ | `npm run gates`, `scripts/gates.mjs` | Internal-content checks, build, lint, typecheck, unit tests, notices, isolated pack and tarball content audit | Adds Chromium installation, responsive/adaptive/date-picker/JSON/host/switch/layout/InfoTip/motion browser checks, Histoire build, exact-tarball registry consumer smoke |
12
+ | `pnpm check`, `check:fast` | Basic, with optional Docker host build isolation | `SPL_FULL_GATE=1` selects the same full plan |
13
+ | `fast-ci.yml`, composite verify action | Basic | No automatic extended checks |
14
+ | Disabled CI/release workflow templates | Basic, version/tag/ancestry checks, stage, pack and actual archive audit | No implicit browser or registry consumer smoke |
15
+ | `scripts/stage-release.mjs`, `release:stage` | Validate existing dist and copy allowlisted publication inputs | No gate invocation |
16
+ | `scripts/ci-release.mjs preflight/audit/dry-run` | Release metadata, archive checks or credential-free npm validation | No browser/workbench/consumer integration |
17
+ | `scripts/ci-release.mjs smoke`, `release:smoke` | Explicit manual consumer integration | Included in full; release:smoke requires full opt-in |
18
+ | `prepack`, `prepublishOnly` | No hooks defined | No hidden gate invocation |
19
+
20
+ Stage the package only after a successful build/basic check. Preserve version,
21
+ ancestry, immutable-publication, metadata and credential boundaries. Registry
22
+ availability checks needed to publish are distinct from optional consumer tests.
23
+ No full-gate stamp is required for publication.