@vc-shell/vc-app-skill 2.5.0 → 2.6.0-pr370.de147ea
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/VERSION +1 -1
- package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
- package/runtime/knowledge/docs/core/blade-navigation/blade-nav-composables.docs.md +28 -1
- package/runtime/knowledge/docs/core/composables/useBlade/useBlade.docs.md +1 -1
- package/runtime/knowledge/docs/core/composables/useKeyboardNavigation/useKeyboardNavigation.docs.md +8 -8
- package/runtime/knowledge/docs/core/composables/useModificationTracker/useModificationTracker.docs.md +11 -0
- package/runtime/knowledge/docs/core/composables/usePopup/usePopup.docs.md +2 -0
- package/runtime/knowledge/docs/core/composables/useSidebarState/useSidebarState.docs.md +15 -4
- package/runtime/knowledge/docs/core/composables/useSlowNetworkDetection/useSlowNetworkDetection.docs.md +22 -18
- package/runtime/knowledge/docs/core/composables/useToolbar/useToolbar.docs.md +4 -0
- package/runtime/knowledge/docs/core/services/services.docs.md +0 -1
- package/runtime/knowledge/docs/ui/components/atoms/vc-button/vc-button.docs.md +1 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-color-input/vc-color-input.docs.md +10 -9
- package/runtime/knowledge/docs/ui/components/molecules/vc-date-picker/vc-date-picker.docs.md +21 -20
- package/runtime/knowledge/docs/ui/components/molecules/vc-input/vc-input.docs.md +27 -26
- package/runtime/knowledge/docs/ui/components/molecules/vc-input-dropdown/vc-input-dropdown.docs.md +1 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-popover/vc-popover.docs.md +3 -2
- package/runtime/knowledge/docs/ui/components/molecules/vc-rating/vc-rating.docs.md +16 -11
- package/runtime/knowledge/docs/ui/components/molecules/vc-textarea/vc-textarea.docs.md +1 -0
- package/runtime/knowledge/docs/ui/components/organisms/vc-app/vc-app.docs.md +1 -1
- package/runtime/knowledge/docs/ui/components/organisms/vc-data-table/vc-data-table.docs.md +77 -0
- package/runtime/knowledge/docs/ui/components/organisms/vc-dynamic-property/vc-dynamic-property.docs.md +20 -19
- package/runtime/knowledge/docs/ui/components/organisms/vc-scheduler/vc-scheduler.docs.md +4 -2
- package/runtime/vc-app.md +4 -2
- package/runtime/knowledge/docs/core/composables/useKeyboardShortcuts/useKeyboardShortcuts.docs.md +0 -219
- package/runtime/knowledge/docs/core/composables/useLatestRequest/useLatestRequest.docs.md +0 -149
- package/runtime/knowledge/docs/core/composables/useMenuExpanded/index.docs.md +0 -93
|
@@ -211,6 +211,83 @@ For any rendering not covered by built-in types, use the `#body` slot:
|
|
|
211
211
|
</VcColumn>
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
+
### Custom Cell Types
|
|
215
|
+
|
|
216
|
+
A `#body` slot covers one-off formatting. When the same rendering should apply
|
|
217
|
+
to a column `type` across every table in the app, register a cell component
|
|
218
|
+
instead. Registration is global and reactive — already-mounted tables pick the
|
|
219
|
+
new component up.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
// src/cells/register.ts — imported once from your app entry point
|
|
223
|
+
import { useCellRegistry } from "@vc-shell/framework";
|
|
224
|
+
import CellRating from "./CellRating.vue";
|
|
225
|
+
|
|
226
|
+
const { register } = useCellRegistry();
|
|
227
|
+
|
|
228
|
+
register({
|
|
229
|
+
type: "rating",
|
|
230
|
+
component: CellRating,
|
|
231
|
+
config: { editable: false },
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
```vue
|
|
236
|
+
<VcColumn id="rating" field="rating" title="Rating" type="rating" />
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The cell component receives a `value` prop holding the resolved field value.
|
|
240
|
+
When `config.editable` is `true`, it also receives `editable`, `label`,
|
|
241
|
+
`fieldName`, `fieldId`, `rules` and `rowIndex` while the row is being edited —
|
|
242
|
+
mirror `CellDefault.vue` for that contract.
|
|
243
|
+
|
|
244
|
+
Registering an existing type replaces the built-in for the whole app:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
register({ type: "money", component: MyMoneyCell, config: { editable: true } });
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The 12 built-ins are registered by `registerBuiltinCells()`, which runs once
|
|
251
|
+
when the table's cell renderer module is loaded — that is, as soon as anything
|
|
252
|
+
imports `VcDataTable`. Because ES modules finish evaluating before the module
|
|
253
|
+
that imported them runs, that always happens before your own registration code,
|
|
254
|
+
and `register()` is last-write-wins — so a type you register replaces the
|
|
255
|
+
built-in. `registerBuiltinCells()` itself skips types that are already present,
|
|
256
|
+
which keeps it idempotent and stops it clobbering a registration that did manage
|
|
257
|
+
to land first (for instance when the table is loaded through a dynamic
|
|
258
|
+
`import()`). `BUILTIN_CELL_TYPES` lists the names that are taken.
|
|
259
|
+
|
|
260
|
+
A `type` that resolves to nothing falls back to `CellDefault`, which renders the
|
|
261
|
+
raw value. Custom names are allowed, so a misspelt built-in is not a type error;
|
|
262
|
+
in development builds an unresolved type logs a warning naming the type.
|
|
263
|
+
|
|
264
|
+
#### Cell registry API
|
|
265
|
+
|
|
266
|
+
| Member | Signature | Description |
|
|
267
|
+
| -------------------- | ------------------------------------------------- | -------------------------------------------------------- |
|
|
268
|
+
| `register` | `(registration: CellRegistration) => void` | Register or replace a cell type. |
|
|
269
|
+
| `get` | `(type: string) => CellRegistration \| undefined` | Look up a registration. |
|
|
270
|
+
| `has` | `(type: string) => boolean` | Whether a type is registered. |
|
|
271
|
+
| `getRegisteredTypes` | `() => string[]` | All registered type names. |
|
|
272
|
+
| `unregister` | `(type: string) => boolean` | Remove a type. Returns `false` if it was not registered. |
|
|
273
|
+
| `clear` | `() => void` | Remove every registration, including the built-ins. |
|
|
274
|
+
|
|
275
|
+
Two module-level exports come with it:
|
|
276
|
+
|
|
277
|
+
| Export | Signature | Description |
|
|
278
|
+
| ---------------------- | --------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
279
|
+
| `registerBuiltinCells` | `() => void` | Register the 12 built-in cell types. Idempotent and non-destructive; called for you when the table loads. |
|
|
280
|
+
| `BUILTIN_CELL_TYPES` | `readonly CellType[]` | The names of the 12 built-in types. |
|
|
281
|
+
|
|
282
|
+
`CellRegistration` is `{ type: CellType | string; component: Component; config?: { editable?: boolean; additionalProps?: string[] } }`.
|
|
283
|
+
|
|
284
|
+
`clear()` empties the registry permanently — `registerBuiltinCells()` runs once
|
|
285
|
+
per module load, so it will not repopulate on its own. Call it again yourself if
|
|
286
|
+
you need the built-ins back.
|
|
287
|
+
|
|
288
|
+
`CellType` accepts the 12 built-in names with autocomplete plus any custom
|
|
289
|
+
string. `BuiltinCellType` is the narrow union of just the built-ins.
|
|
290
|
+
|
|
214
291
|
---
|
|
215
292
|
|
|
216
293
|
## Sorting
|
|
@@ -46,25 +46,26 @@ The prop names match the `DynamicObjectProperty` shape returned by the platform
|
|
|
46
46
|
|
|
47
47
|
## Key Props
|
|
48
48
|
|
|
49
|
-
| Prop | Type | Default | Description
|
|
50
|
-
| -------------------- | ---------------------------- | ---------- |
|
|
51
|
-
| `property` | `T` | _required_ | Property object with `id` and metadata
|
|
52
|
-
| `modelValue` | `any` | _required_ | Current property value (v-model)
|
|
53
|
-
| `valueType` | `string` | _required_ | Type: ShortText, LongText, Number, Integer, DateTime, Boolean, Measure, Color
|
|
54
|
-
| `name` | `string` | _required_ | Property name for display and field identification
|
|
55
|
-
| `required` | `boolean` | _required_ | Whether the field is required
|
|
56
|
-
| `disabled` | `boolean` | `false` | Disables the input
|
|
57
|
-
| `dictionary` | `boolean` | `false` | Uses VcSelect/VcMultivalue with options
|
|
58
|
-
| `multivalue` | `boolean` | `false` | Supports multiple values
|
|
59
|
-
| `multilanguage` | `boolean` | `false` | Supports localized values
|
|
60
|
-
| `currentLanguage` | `string` | - | Active locale used for localized values
|
|
61
|
-
| `optionsGetter` | `Function` | _required_ | Async loader for dictionary options, called as `(propertyId, keyword?, locale?)`
|
|
62
|
-
| `measurementsGetter` | `Function` | - | Async loader for measurement units
|
|
63
|
-
| `optionsValue` | `string` | `"id"` | Field on each option used as its value (VcSelect mapping)
|
|
64
|
-
| `optionsLabel` | `string` | `"value"` | Inert. The option label is fixed to `"value"` when `multilanguage` is true and `"alias"` otherwise; only `optionsValue` is honored for VcSelect mapping.
|
|
65
|
-
| `displayNames` | `{ name?; languageCode? }[]` | - | Localized display names used to resolve the field label per locale
|
|
66
|
-
| `placeholder` | `string` | - | Placeholder text for the input
|
|
67
|
-
| `rules` | `object` | - | Validation rules: `{ min, max, regex }`
|
|
49
|
+
| Prop | Type | Default | Description |
|
|
50
|
+
| -------------------- | ---------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `property` | `T` | _required_ | Property object with `id` and metadata |
|
|
52
|
+
| `modelValue` | `any` | _required_ | Current property value (v-model) |
|
|
53
|
+
| `valueType` | `string` | _required_ | Type: ShortText, LongText, Number, Integer, DateTime, Boolean, Measure, Color |
|
|
54
|
+
| `name` | `string` | _required_ | Property name for display and field identification |
|
|
55
|
+
| `required` | `boolean` | _required_ | Whether the field is required |
|
|
56
|
+
| `disabled` | `boolean` | `false` | Disables the input |
|
|
57
|
+
| `dictionary` | `boolean` | `false` | Uses VcSelect/VcMultivalue with options |
|
|
58
|
+
| `multivalue` | `boolean` | `false` | Supports multiple values |
|
|
59
|
+
| `multilanguage` | `boolean` | `false` | Supports localized values |
|
|
60
|
+
| `currentLanguage` | `string` | - | Active locale used for localized values |
|
|
61
|
+
| `optionsGetter` | `Function` | _required_ | Async loader for dictionary options, called as `(propertyId, keyword?, locale?)` |
|
|
62
|
+
| `measurementsGetter` | `Function` | - | Async loader for measurement units |
|
|
63
|
+
| `optionsValue` | `string` | `"id"` | Field on each option used as its value (VcSelect mapping) |
|
|
64
|
+
| `optionsLabel` | `string` | `"value"` | Inert. The option label is fixed to `"value"` when `multilanguage` is true and `"alias"` otherwise; only `optionsValue` is honored for VcSelect mapping. |
|
|
65
|
+
| `displayNames` | `{ name?; languageCode? }[]` | - | Localized display names used to resolve the field label per locale |
|
|
66
|
+
| `placeholder` | `string` | - | Placeholder text for the input |
|
|
67
|
+
| `rules` | `object` | - | Validation rules: `{ min, max, regex }` |
|
|
68
|
+
| `hideLabel` | `boolean` | `false` | Hides the control's own visible label so the consumer can render its own label row (e.g. to place an action next to the property name). Validation and the placeholder fallback are unaffected. The display name is forwarded as the control's accessible name for nine control families: short text, number, integer, long text, boolean, datetime, colour, Measure, and the fallback used for an unrecognized `valueType`. Dictionary and multivalue properties (VcSelect, VcMultivalue) are **not** covered — their focusable triggers carry no accessible name, so those properties still need a visible label. |
|
|
68
69
|
|
|
69
70
|
::storybook id="data-display-vcdynamicproperty--property-form" height="500"
|
|
70
71
|
|
|
@@ -162,6 +162,8 @@ With `editable`, `VcScheduler` ships a complete built-in create/edit flow -- no
|
|
|
162
162
|
|
|
163
163
|
The editor modal additionally exposes all-day toggle, start/end date-time, and color, plus a Delete action in edit mode (commits via `event-delete`). Saving from either surface emits the public `event-create`/`event-update` event -- see [Events](#events) for the exact payloads.
|
|
164
164
|
|
|
165
|
+
For an **all-day** event the editor's End field is the last day the event covers, matching what the chip and the quick-info popover announce -- so a one-day event has End equal to Start. The `end` in the emitted payload is still the exclusive midnight boundary the layout works in; the editor converts on save. An End before Start is refused rather than saved.
|
|
166
|
+
|
|
165
167
|
This entire flow is internal state: no `v-model` or extra event wiring is needed to make it work, beyond `editable` and the `event-create`/`event-update`/`event-delete` handlers you already have for persisting the result.
|
|
166
168
|
|
|
167
169
|
### `editorMode`: opting out of the built-in UI
|
|
@@ -498,8 +500,8 @@ const events = [{ id: "a", title: "Promo", start, end, allDay: true, color: "#a2
|
|
|
498
500
|
- The Month grid uses `role="grid"` / `role="row"` / `role="gridcell"` / `role="columnheader"` for the weekday header and day cells, each `gridcell` carrying a full formatted-date `aria-label`.
|
|
499
501
|
- Each event bar and timed chip is a focusable `role="button"` element (`tabindex="0"`) with an `aria-label` built from its title and formatted start/end dates -- activate with click or `Enter`. For an all-day event the announced end is the **last inclusive day**, not the exclusive `end` you pass: an event with `end` at midnight runs through the previous day, so a one-day event is announced as a single date rather than a two-day range. Timed events are announced with their real times.
|
|
500
502
|
- The "+N more" overflow control is a `<button>` whose visible text is its accessible name; the date it belongs to comes from the enclosing `gridcell`. Activating it moves focus to the first event in the popover, and closing the popover (Escape, or the close button) returns focus to the "+N more" button. Each event inside the popover is a `<button>` too, so a day's 4th and later events are reachable and operable by keyboard.
|
|
501
|
-
-
|
|
502
|
-
- The editor modal and the This event/All events scope dialog are `VcPopup`, which is a Headless UI `Dialog` -- those do get accessible naming, a focus trap, focus restore and `inert` on the background from the library.
|
|
503
|
+
- All three anchored popovers -- "+N more" overflow, quick-info and quick-create -- are `VcPopover` panels carrying `role="dialog"` and an accessible name (the formatted date, the event title, and "New event" respectively). They do **not** trap focus: `VcPopover` has no focus management of its own, so each one moves focus into itself on open and hands it back to whatever opened it on close. Focus is repaired, not dictated -- a user who moves focus out of the panel keeps it there when the panel closes.
|
|
504
|
+
- The editor modal and the This event/All events scope dialog are `VcPopup`, which is a Headless UI `Dialog` -- those do get accessible naming, a focus trap, focus restore and `inert` on the background from the library.
|
|
503
505
|
- The recurrence editor's weekday toggle buttons expose `aria-pressed` (selected state) and an `aria-label` with the full weekday name, since their visible narrow labels ("S", "M", "T", ...) collide (Sun/Sat, Tue/Thu). The "↻" recurring-occurrence marker is `aria-hidden` -- purely visual, redundant with the event's own accessible name.
|
|
504
506
|
- The toolbar's prev/next buttons carry an explicit `aria-label` ("Previous" / "Next") since they are icon-only; the today/view-switch buttons show text labels.
|
|
505
507
|
- Event move/resize (Month view's `editable`) is a pointer-drag interaction with no keyboard equivalent yet; use a form outside VcScheduler for keyboard-only date edits. Timeline view is read-only (click still opens the quick-info popover).
|
package/runtime/vc-app.md
CHANGED
|
@@ -101,6 +101,7 @@ Collect project parameters through dialog, then run CLI in **non-interactive mod
|
|
|
101
101
|
- `--dashboard` — Include a dashboard page
|
|
102
102
|
- `--tenant-routes` — Include tenant-aware routing
|
|
103
103
|
- `--ai-agent` — Include AI agent integration scaffold
|
|
104
|
+
- `--module-federation` — Include the Module Federation host runtime (load remote modules)
|
|
104
105
|
- `--mocks` — Include sample modules with mock data (demo content to explore the framework)
|
|
105
106
|
|
|
106
107
|
### Execution
|
|
@@ -110,7 +111,7 @@ Collect project parameters through dialog, then run CLI in **non-interactive mod
|
|
|
110
111
|
```bash
|
|
111
112
|
npx @vc-shell/create-vc-app@alpha <projectName> \
|
|
112
113
|
--type standalone \
|
|
113
|
-
[--dashboard] [--tenant-routes] [--ai-agent] [--mocks]
|
|
114
|
+
[--dashboard] [--tenant-routes] [--ai-agent] [--module-federation] [--mocks]
|
|
114
115
|
```
|
|
115
116
|
|
|
116
117
|
**IMPORTANT:** Do NOT pass `--module-name` — the starter module is opt-in and unnecessary because this skill generates custom modules via `/vc-app generate`.
|
|
@@ -125,6 +126,7 @@ Available CLI flags (see `cli/create-vc-app/README.md` for full list):
|
|
|
125
126
|
| `--dashboard` | Include Dashboard |
|
|
126
127
|
| `--tenant-routes` | Include tenant routing |
|
|
127
128
|
| `--ai-agent` | Include AI Agent config |
|
|
129
|
+
| `--module-federation` | Include Module Federation host runtime |
|
|
128
130
|
| `--mocks` | Include sample modules with mock data (demo content) |
|
|
129
131
|
| `--overwrite` | Overwrite existing files |
|
|
130
132
|
|
|
@@ -1198,7 +1200,7 @@ After the user confirms the plan:
|
|
|
1198
1200
|
If `DESIGN_PLAN.needsScaffold` is true, run the `/vc-app create` flow:
|
|
1199
1201
|
|
|
1200
1202
|
- Use `DESIGN_PLAN.appName` as the project name
|
|
1201
|
-
- Enable default options (no dashboard, no tenant-routes, no ai-agent, no mocks)
|
|
1203
|
+
- Enable default options (no dashboard, no tenant-routes, no ai-agent, no module-federation, no mocks)
|
|
1202
1204
|
- After scaffold completes, `cd` into the new project directory
|
|
1203
1205
|
- Update project root for subsequent steps
|
|
1204
1206
|
|
package/runtime/knowledge/docs/core/composables/useKeyboardShortcuts/useKeyboardShortcuts.docs.md
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: useKeyboardShortcuts
|
|
3
|
-
category: composables
|
|
4
|
-
group: utilities
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
!!! tip "Long page"
|
|
8
|
-
Use the section headings to jump directly to what you need: [Primary pattern: declare a shortcut on a toolbar item](#primary-pattern-declare-a-shortcut-on-a-toolbar-item), [Quick Start](#quick-start), [Built-in blade shortcuts](#built-in-blade-shortcuts), or [API Reference](#api-reference).
|
|
9
|
-
|
|
10
|
-
# useKeyboardShortcuts
|
|
11
|
-
|
|
12
|
-
`useKeyboardShortcuts` is the framework's keyboard-shortcut toolkit: the `hotkey` fluent builder for describing a combination, and OS-aware formatting for the tooltip and `aria-keyshortcuts` that a toolbar button shows automatically once it declares one. There is no manual `keydown` wiring in module code -- a shortcut is a plain field on a toolbar item, and the blade navigation layer dispatches it globally.
|
|
13
|
-
|
|
14
|
-
## Primary pattern: declare a shortcut on a toolbar item
|
|
15
|
-
|
|
16
|
-
Add `shortcut: hotkey.mod.s` to any `IBladeToolbar` entry. `hotkey.mod.s` resolves to `{ key: "s", mod: true }` -- `mod` is the platform's primary modifier (Cmd on macOS, Ctrl elsewhere), so one definition covers both operating systems.
|
|
17
|
-
|
|
18
|
-
```vue title="orders-details.vue"
|
|
19
|
-
<script setup lang="ts">
|
|
20
|
-
import { ref } from "vue";
|
|
21
|
-
import { hotkey, VcBlade, type IBladeToolbar } from "@vc-shell/framework";
|
|
22
|
-
|
|
23
|
-
const bladeToolbar = ref<IBladeToolbar[]>([
|
|
24
|
-
{
|
|
25
|
-
id: "save",
|
|
26
|
-
title: "Save",
|
|
27
|
-
icon: "lucide-save",
|
|
28
|
-
shortcut: hotkey.mod.s,
|
|
29
|
-
clickHandler: () => save(),
|
|
30
|
-
},
|
|
31
|
-
]);
|
|
32
|
-
</script>
|
|
33
|
-
|
|
34
|
-
<template>
|
|
35
|
-
<VcBlade :toolbar-items="bladeToolbar" />
|
|
36
|
-
</template>
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
That single field is the whole integration. The rendered "Save" button now shows a `⌘S` / `Ctrl+S` tooltip, sets `aria-keyshortcuts="Meta+S"`, and the blade navigation layer fires `clickHandler` when the user presses the combination while the blade is active -- no listener, no cleanup.
|
|
40
|
-
|
|
41
|
-
## When to reach for it
|
|
42
|
-
|
|
43
|
-
- **Declaring a shortcut on a toolbar item.** The common case: add `shortcut: hotkey.mod.s` next to `clickHandler` on an `IBladeToolbar` entry (array pattern or `useToolbar().registerToolbarItem`, see [useToolbar](../useToolbar/)).
|
|
44
|
-
- **Formatting a shortcut for custom UI.** Call `useKeyboardShortcuts()` and use `formatShortcut(def)` when you need the OS-aware label somewhere other than the built-in toolbar tooltip (a help panel, a command palette row).
|
|
45
|
-
- **Detecting the platform.** `isMac` is exposed for any OS-conditional copy or icon choice tied to a shortcut.
|
|
46
|
-
- **Not for building your own key listener.** There is no public "register a global shortcut" API outside toolbar items -- see [Common mistakes](#common-mistakes).
|
|
47
|
-
|
|
48
|
-
## Quick Start
|
|
49
|
-
|
|
50
|
-
### Import vs `useKeyboardShortcuts()` destructure
|
|
51
|
-
|
|
52
|
-
`hotkey` is a context-free singleton -- import it directly wherever you only need to build a definition:
|
|
53
|
-
|
|
54
|
-
```typescript
|
|
55
|
-
import { hotkey } from "@vc-shell/framework";
|
|
56
|
-
|
|
57
|
-
const saveShortcut = hotkey.mod.s; // { key: "s", mod: true }
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Reach for the composable form only when you also need `formatShortcut` or `isMac` (both are bound together so `formatShortcut` doesn't need an `isMac` argument at the call site):
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
import { useKeyboardShortcuts } from "@vc-shell/framework";
|
|
64
|
-
|
|
65
|
-
const { hotkey, formatShortcut, isMac } = useKeyboardShortcuts();
|
|
66
|
-
|
|
67
|
-
const saveShortcut = hotkey.mod.s;
|
|
68
|
-
const { parts, aria } = formatShortcut(saveShortcut);
|
|
69
|
-
// isMac ? parts = ["⌘", "S"] : parts = ["Ctrl", "S"]
|
|
70
|
-
// aria = "Meta+S" on macOS, "Control+S" elsewhere
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Both forms return the same `hotkey` singleton -- the composable exists for convenience when a component already needs `isMac`/`formatShortcut`, not because `hotkey` itself carries state.
|
|
74
|
-
|
|
75
|
-
## Built-in blade shortcuts
|
|
76
|
-
|
|
77
|
-
Two shortcuts are wired up by the framework at the blade navigation root -- no client code required, and no toolbar item needed to get them:
|
|
78
|
-
|
|
79
|
-
| Shortcut | Effect | Gate |
|
|
80
|
-
| ------------ | ----------------------- | --------------------------------------------------------------------------- |
|
|
81
|
-
| `Esc` | Closes the active blade | Only if the blade is closable (has a `parentId`; workspace roots ignore it) |
|
|
82
|
-
| `Cmd/Ctrl+\` | Toggles expand/maximize | Only on desktop and only if the blade is closable |
|
|
83
|
-
|
|
84
|
-
Both are evaluated against the currently active blade, and both are skipped while the corresponding condition doesn't hold -- `Esc` on a non-closable workspace root does nothing, and `Cmd/Ctrl+\` does nothing on mobile viewports even on a closable blade.
|
|
85
|
-
|
|
86
|
-
**An explicit toolbar shortcut always wins.** The dispatcher checks the active blade's toolbar items before either built-in, so a toolbar item declared with `shortcut: hotkey.escape` (for example, a "Cancel" button) intercepts `Esc` and the built-in close never runs.
|
|
87
|
-
|
|
88
|
-
## OS adaptation
|
|
89
|
-
|
|
90
|
-
`formatShortcut(def)` returns two independent representations from one `ShortcutDefinition`, so you never branch on OS yourself:
|
|
91
|
-
|
|
92
|
-
- `parts` -- an array of OS-native keycap labels for display. macOS renders modifier glyphs in Apple's canonical order (`⌃ ⌥ ⇧ ⌘`) with no separator; Windows/Linux render words (`Ctrl`, `Alt`, `Shift`) with a `+` between chips.
|
|
93
|
-
- `aria` -- a single canonical string in W3C `aria-keyshortcuts` format (`"Control+Shift+S"`), OS-independent by design -- this is what assistive tech reads, not what's painted on screen.
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
import { useKeyboardShortcuts } from "@vc-shell/framework";
|
|
97
|
-
|
|
98
|
-
const { hotkey, formatShortcut, isMac } = useKeyboardShortcuts();
|
|
99
|
-
const { parts } = formatShortcut(hotkey.mod.s);
|
|
100
|
-
|
|
101
|
-
// isMac === true → parts = ["⌘", "S"] → tooltip renders "⌘S" (no separator)
|
|
102
|
-
// isMac === false → parts = ["Ctrl", "S"] → tooltip renders "Ctrl+S" (with separator)
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
The toolbar button itself (`ToolbarBaseButton`) already does this for you: it calls `formatShortcut` internally and renders the result through the `ShortcutKbd` chip component inside a tooltip. `isMac` also determines `matchesEvent`'s runtime resolution of `mod` -- the same flag drives both what the user sees and what actually fires.
|
|
106
|
-
|
|
107
|
-
## Accessibility
|
|
108
|
-
|
|
109
|
-
- **`aria-keyshortcuts`.** Set automatically on the button element whenever `shortcut` is present, using the canonical `aria` string from `formatShortcut` (for example `aria-keyshortcuts="Meta+S"`). No shortcut, no attribute -- it's never rendered empty.
|
|
110
|
-
- **Tooltip discovery.** A sighted user discovers the shortcut through the `<kbd>` chips shown on hover/focus of the toolbar button (`ShortcutKbd`, wrapped in `VcTooltip`). This is the only visual affordance; there is no separate "shortcuts list" surface in v1.
|
|
111
|
-
- **No button, no discovery.** Both `aria-keyshortcuts` and the tooltip are properties of the rendered toolbar button -- they exist only because `shortcut` lives on an `IBladeToolbar` item that renders as a button. There is currently no way to advertise a shortcut that isn't attached to a visible button (see [Common mistakes](#common-mistakes)).
|
|
112
|
-
- **Accessible name still comes from the title.** `shortcut` does not change how the button's accessible name is computed -- it still comes from the visible `title` text. Keep `title` non-empty; `shortcut` is additive, not a substitute for a labeled button.
|
|
113
|
-
- **Suppressed behind a modal.** Blade shortcuts (toolbar and built-in) do not fire while a modal (`aria-modal="true"`) is open, so a shortcut never acts on a blade hidden behind it.
|
|
114
|
-
|
|
115
|
-
## Common mistakes
|
|
116
|
-
|
|
117
|
-
### Expecting a bare key to fire while an input is focused
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
// Wrong -- assumes "e" fires globally
|
|
121
|
-
registerToolbarItem({ id: "edit", title: "Edit", shortcut: { key: "e" }, clickHandler: () => edit() });
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
A shortcut with no modifier is suppressed while focus is inside a text input, textarea, select, or a `contenteditable` element -- otherwise typing the letter "e" into a form field would trigger the button. This guard only applies to bare keys; add a modifier (`hotkey.mod.e`) if the shortcut must also fire while a field is focused.
|
|
125
|
-
|
|
126
|
-
```typescript
|
|
127
|
-
// Correct -- a modified combination is not suppressed by input focus
|
|
128
|
-
registerToolbarItem({ id: "edit", title: "Edit", shortcut: hotkey.mod.e, clickHandler: () => edit() });
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### Two toolbar items sharing the same combination
|
|
132
|
-
|
|
133
|
-
```typescript
|
|
134
|
-
// Wrong -- both items declare mod+s on the same blade
|
|
135
|
-
const bladeToolbar = ref<IBladeToolbar[]>([
|
|
136
|
-
{ id: "save", title: "Save", shortcut: hotkey.mod.s, clickHandler: () => save() },
|
|
137
|
-
{ id: "save-as", title: "Save As", shortcut: hotkey.mod.s, clickHandler: () => saveAs() },
|
|
138
|
-
]);
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
The dispatcher walks a blade's toolbar items in declaration order and fires the _first_ item whose shortcut matches, then stops -- `save-as` never receives `Cmd/Ctrl+S` as long as `save` is registered ahead of it. In development, the dispatcher emits a deduplicated `console.warn` on keydown for this case (first item wins, second is named in the warning), for a toolbar item with an unrecognized `shortcut.key`, and for a toolbar item that overrides a built-in (`Esc`, `Cmd/Ctrl+\`). Production stays silent -- these are warnings, not thrown errors -- so still treat shortcut collisions within a blade's toolbar as a review-time concern: keep combinations unique per blade.
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
// Correct -- distinct combinations
|
|
145
|
-
const bladeToolbar = ref<IBladeToolbar[]>([
|
|
146
|
-
{ id: "save", title: "Save", shortcut: hotkey.mod.s, clickHandler: () => save() },
|
|
147
|
-
{ id: "save-as", title: "Save As", shortcut: hotkey.mod.shift.s, clickHandler: () => saveAs() },
|
|
148
|
-
]);
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### Expecting a button-less shortcut
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
// Wrong -- there is no API to register a shortcut without a toolbar item
|
|
155
|
-
onMounted(() => {
|
|
156
|
-
window.addEventListener("keydown", (e) => {
|
|
157
|
-
if (e.metaKey && e.key === "s") save();
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
Rolling your own `keydown` listener bypasses `isTextInputFocused` suppression, the active-blade scoping, and the tooltip/`aria-keyshortcuts` discovery that toolbar shortcuts get for free -- and it risks double-firing alongside the framework dispatcher. In v1, every shortcut must be attached to a visible `IBladeToolbar` item; there is no headless/button-less registration path. If the action genuinely has no natural button, add one (it can be low-priority/overflowed) rather than reaching for a manual listener.
|
|
163
|
-
|
|
164
|
-
## API Reference
|
|
165
|
-
|
|
166
|
-
### `hotkey`
|
|
167
|
-
|
|
168
|
-
A fluent builder, importable directly or via the composable. Modifier and key names are finite string unions, so a typo (`hotkey.mdo.s`) is a compile error, not a silent no-op.
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
hotkey.mod.s; // { key: "s", mod: true }
|
|
172
|
-
hotkey.mod.shift.s; // { key: "s", mod: true, shift: true }
|
|
173
|
-
hotkey.ctrl.alt.delete; // { key: "delete", ctrl: true, alt: true }
|
|
174
|
-
hotkey.escape; // { key: "escape" }
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
Chainable modifiers: `mod`, `ctrl`, `meta`, `alt`, `shift` (any order, any combination). Terminal keys: letters `a`-`z`, digits `0`-`9`, `enter`, `escape`, `tab`, `space`, `delete`, `backspace`, `arrowup`/`arrowdown`/`arrowleft`/`arrowright`, `f1`-`f12`, `backslash`, `period`, `comma`, `slash`.
|
|
178
|
-
|
|
179
|
-
### `useKeyboardShortcuts()`
|
|
180
|
-
|
|
181
|
-
Context-free -- no inject/lifecycle, usable in or out of a component `setup()`.
|
|
182
|
-
|
|
183
|
-
#### Returns: `UseKeyboardShortcutsReturn`
|
|
184
|
-
|
|
185
|
-
| Property | Type | Description |
|
|
186
|
-
| ---------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
187
|
-
| `hotkey` | `HotkeyBuilder` | The same singleton as the named `hotkey` import |
|
|
188
|
-
| `formatShortcut` | `(def: ShortcutDefinition) => { parts: string[]; aria: string }` | `isMac`-bound: formats a definition into display chips and an aria string |
|
|
189
|
-
| `isMac` | `boolean` | `true` when the user agent reports macOS/iOS |
|
|
190
|
-
|
|
191
|
-
### `formatShortcut(def, isMac)`
|
|
192
|
-
|
|
193
|
-
The standalone function (also exported directly) if you need to format for an explicit `isMac` value instead of the detected one:
|
|
194
|
-
|
|
195
|
-
```typescript
|
|
196
|
-
import { formatShortcut } from "@vc-shell/framework";
|
|
197
|
-
|
|
198
|
-
formatShortcut({ key: "s", mod: true }, true); // { parts: ["⌘", "S"], aria: "Meta+S" }
|
|
199
|
-
formatShortcut({ key: "s", mod: true }, false); // { parts: ["Ctrl", "S"], aria: "Control+S" }
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### `ShortcutDefinition`
|
|
203
|
-
|
|
204
|
-
| Property | Type | Required | Description |
|
|
205
|
-
| -------- | --------- | -------- | ----------------------------------------------------------------------------------- |
|
|
206
|
-
| `key` | `string` | Yes | Single key: `"s"`, `"1"`, `"enter"`, `"escape"`, `"f2"`, `"arrowup"`, `"backslash"` |
|
|
207
|
-
| `mod` | `boolean` | No | Platform primary modifier -- Cmd on macOS, Ctrl elsewhere |
|
|
208
|
-
| `ctrl` | `boolean` | No | Literal Ctrl, regardless of platform |
|
|
209
|
-
| `meta` | `boolean` | No | Literal Cmd/Win key, regardless of platform |
|
|
210
|
-
| `alt` | `boolean` | No | Alt/Option |
|
|
211
|
-
| `shift` | `boolean` | No | Shift |
|
|
212
|
-
|
|
213
|
-
Building definitions by hand (`{ key: "s", mod: true }`) works identically to `hotkey.mod.s` -- the builder is sugar over this shape, useful when a definition needs to be constructed dynamically.
|
|
214
|
-
|
|
215
|
-
## Related
|
|
216
|
-
|
|
217
|
-
- [useToolbar](../useToolbar/) -- registers the toolbar items that carry `shortcut`; see its `IBladeToolbar`/`IToolbarItem` reference for the full item shape
|
|
218
|
-
- [Blade Navigation Composables](../../blade-navigation/) -- the blade stack that scopes shortcut dispatch to the active blade and hosts the two built-in shortcuts
|
|
219
|
-
- `IBladeToolbar` in [Core types](../../types/) -- the `shortcut?: ShortcutDefinition` field lives here
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: useLatestRequest
|
|
3
|
-
category: composables
|
|
4
|
-
group: utilities
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# useLatestRequest
|
|
8
|
-
|
|
9
|
-
Latest-wins sequencing for overlapping async work: lets a caller drop a response
|
|
10
|
-
that a newer request already superseded.
|
|
11
|
-
|
|
12
|
-
`useAsync` covers loading state, error state and error notifications, but it has
|
|
13
|
-
no notion of a superseded call — a slow earlier response can overwrite a newer
|
|
14
|
-
one. This composable fills that gap.
|
|
15
|
-
|
|
16
|
-
## When to Use
|
|
17
|
-
|
|
18
|
-
- A search field that fires a request per keystroke, where the slowest response must not win
|
|
19
|
-
- A master/detail pair, where clicking row A then row B must not leave A's details on screen
|
|
20
|
-
- Any load that can still be in flight when its blade closes
|
|
21
|
-
- When NOT to use: for a single request with no concurrent sibling — plain `useAsync` is enough
|
|
22
|
-
|
|
23
|
-
!!! note "Discards, does not cancel"
|
|
24
|
-
The superseded request still completes; its result is thrown away. The generated
|
|
25
|
-
API clients build their own `RequestInit` and accept no `AbortSignal`, so there
|
|
26
|
-
is nothing to cancel through. Aborting would additionally save the round trip and
|
|
27
|
-
belongs with a client that takes a signal.
|
|
28
|
-
|
|
29
|
-
## Quick Start
|
|
30
|
-
|
|
31
|
-
```typescript
|
|
32
|
-
import { useLatestRequest } from "@vc-shell/framework";
|
|
33
|
-
|
|
34
|
-
const search = useLatestRequest();
|
|
35
|
-
const items = ref([]);
|
|
36
|
-
|
|
37
|
-
async function load(criteria) {
|
|
38
|
-
const request = search.begin();
|
|
39
|
-
try {
|
|
40
|
-
const result = await client.search(criteria);
|
|
41
|
-
if (!request.isCurrent()) return; // a newer search already won
|
|
42
|
-
items.value = result;
|
|
43
|
-
} finally {
|
|
44
|
-
request.complete();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Two rules make it correct:
|
|
50
|
-
|
|
51
|
-
1. Check `isCurrent()` **after** the await and before writing any state.
|
|
52
|
-
2. Call `complete()` in a `finally`, so a throwing request still releases `pending`.
|
|
53
|
-
|
|
54
|
-
## API Reference
|
|
55
|
-
|
|
56
|
-
### Returns
|
|
57
|
-
|
|
58
|
-
| Member | Type | Description |
|
|
59
|
-
| ------------ | ------------------------ | --------------------------------------------------------------------------------- |
|
|
60
|
-
| `begin` | `() => LatestRequest` | Starts a request and supersedes any earlier one |
|
|
61
|
-
| `invalidate` | `() => void` | Supersedes the in-flight request without starting a new one |
|
|
62
|
-
| `dispose` | `() => void` | Permanently supersedes everything. Runs automatically when the owning scope stops |
|
|
63
|
-
| `pending` | `Readonly<Ref<boolean>>` | `true` while the newest request is still running |
|
|
64
|
-
|
|
65
|
-
### `LatestRequest`
|
|
66
|
-
|
|
67
|
-
| Member | Type | Description |
|
|
68
|
-
| ----------- | --------------- | ---------------------------------------------------------------------------------- |
|
|
69
|
-
| `isCurrent` | `() => boolean` | `false` once a newer request started, or after `invalidate()` / `dispose()` |
|
|
70
|
-
| `complete` | `() => void` | Marks this request finished. Idempotent; only the current request clears `pending` |
|
|
71
|
-
|
|
72
|
-
## Features
|
|
73
|
-
|
|
74
|
-
### `pending` tracks the newest request only
|
|
75
|
-
|
|
76
|
-
A superseded request finishing does **not** clear `pending` — the newer one is
|
|
77
|
-
still running, and clearing there would hide the spinner while the screen is
|
|
78
|
-
still waiting for data.
|
|
79
|
-
|
|
80
|
-
### Automatic disposal
|
|
81
|
-
|
|
82
|
-
When called inside a component or effect scope, the tracker disposes itself when
|
|
83
|
-
that scope stops, so a response landing after its blade closed can never write
|
|
84
|
-
into a dead scope. Call `dispose()` by hand only outside a scope.
|
|
85
|
-
|
|
86
|
-
## Recipes
|
|
87
|
-
|
|
88
|
-
### Driving a spinner
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
const details = useLatestRequest();
|
|
92
|
-
// `pending` is a ref, so watch it, render it, or hand it to useLoading.
|
|
93
|
-
const loading = details.pending;
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### Dropping a request on selection change
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
watch(selectedId, () => {
|
|
100
|
-
// Nothing new starts yet; whatever is in flight stops being current.
|
|
101
|
-
details.invalidate();
|
|
102
|
-
});
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## Common Mistakes
|
|
106
|
-
|
|
107
|
-
**Wrong: checking before the await**
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
const request = search.begin();
|
|
111
|
-
if (!request.isCurrent()) return; // always true here — nothing has superseded it yet
|
|
112
|
-
const result = await client.search(criteria);
|
|
113
|
-
items.value = result; // still overwrites newer data
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
**Right: checking after**
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
const request = search.begin();
|
|
120
|
-
const result = await client.search(criteria);
|
|
121
|
-
if (!request.isCurrent()) return;
|
|
122
|
-
items.value = result;
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
**Wrong: completing only on success**
|
|
126
|
-
|
|
127
|
-
```typescript
|
|
128
|
-
const request = search.begin();
|
|
129
|
-
const result = await client.search(criteria); // throws → pending stays true forever
|
|
130
|
-
request.complete();
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
**Right: completing in `finally`**
|
|
134
|
-
|
|
135
|
-
```typescript
|
|
136
|
-
const request = search.begin();
|
|
137
|
-
try {
|
|
138
|
-
const result = await client.search(criteria);
|
|
139
|
-
if (!request.isCurrent()) return;
|
|
140
|
-
items.value = result;
|
|
141
|
-
} finally {
|
|
142
|
-
request.complete();
|
|
143
|
-
}
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
## Related
|
|
147
|
-
|
|
148
|
-
- [useAsync](./useAsync.md) — loading state, error state and error notifications for a single call
|
|
149
|
-
- [useLoading](./useLoading.md) — aggregates several loading flags
|