@remcostoeten/use-shortcut 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -34
- package/dist/constants.d.ts +36 -0
- package/dist/constants.js +1 -0
- package/dist/constants.mjs +1 -0
- package/dist/formatter.d.ts +30 -0
- package/dist/formatter.js +1 -0
- package/dist/formatter.mjs +1 -0
- package/dist/index.d.ts +5 -415
- package/dist/index.js +1 -946
- package/dist/index.mjs +1 -928
- package/dist/parser.d.ts +47 -0
- package/dist/parser.js +1 -0
- package/dist/parser.mjs +1 -0
- package/dist/react.d.ts +79 -0
- package/dist/react.js +1 -0
- package/dist/react.mjs +1 -0
- package/dist/{index.d.mts → types-Cg7uyv1m.d.ts} +65 -166
- package/package.json +49 -9
- package/CHANGELOG.md +0 -66
- package/dist/cli/index.mjs +0 -455
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
package/CHANGELOG.md
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## [2.0.1] - 2026-03-11
|
|
9
|
-
|
|
10
|
-
### Fixed
|
|
11
|
-
|
|
12
|
-
- Re-exported `useShortcutMap`, `registerShortcutMap`, `createShortcutGroup`, and `useShortcutGroup` from the public package entrypoint.
|
|
13
|
-
- Re-exported shortcut map and shortcut group types from the public package entrypoint.
|
|
14
|
-
|
|
15
|
-
### Changed
|
|
16
|
-
|
|
17
|
-
- Updated the README to document the public shortcut map API.
|
|
18
|
-
|
|
19
|
-
## [2.0.0] - 2026-03-04
|
|
20
|
-
|
|
21
|
-
### Changed
|
|
22
|
-
|
|
23
|
-
- Removed non-React public entry points (`createShortcut`, `createShortcutMap`) to focus package API on React-first usage.
|
|
24
|
-
|
|
25
|
-
## [1.3.0] - 2026-02-28
|
|
26
|
-
|
|
27
|
-
### Added
|
|
28
|
-
|
|
29
|
-
- Shortcut groups with `createShortcutGroup()` and `useShortcutGroup()` for bulk cleanup
|
|
30
|
-
- Priority-based ordering and `stopOnMatch` for overlapping shortcuts
|
|
31
|
-
- Global `eventFilter` hook option to guard shortcut handling per event
|
|
32
|
-
|
|
33
|
-
## [1.2.0] - 2026-02-28
|
|
34
|
-
|
|
35
|
-
### Added
|
|
36
|
-
|
|
37
|
-
- CLI `scaffold` command to generate a scalable React/Next shortcut architecture
|
|
38
|
-
- Scaffolded architecture with typed registry, scopes, provider state/actions, runtime mapping, and persistence adapter
|
|
39
|
-
- `init --architecture` alias for architecture scaffolding workflow
|
|
40
|
-
- Human architecture docs (`docs/app-architecture.md`) and LLM-focused architecture contract (`docs/app-architecture.llm.md`)
|
|
41
|
-
- `LLM.txt` updates covering the new architecture scaffolding model
|
|
42
|
-
|
|
43
|
-
## [1.1.0] - 2026-02-28
|
|
44
|
-
|
|
45
|
-
### Added
|
|
46
|
-
|
|
47
|
-
- Sequence/chord support with `.then()` for multi-step shortcuts (`$.key("g").then("d")`)
|
|
48
|
-
- Named scope/context system with `.in()` and runtime scope controls (`setScopes`, `enableScope`, `disableScope`)
|
|
49
|
-
- Conflict detection for exact overlaps and sequence-prefix collisions, including `mod` cross-platform normalization
|
|
50
|
-
- `useShortcutMap`, `createShortcutMap`, and `registerShortcutMap` bulk registration helpers
|
|
51
|
-
- Recording mode via `$.record()` to capture the next key combo
|
|
52
|
-
- Test suite using Vitest + jsdom for advanced feature coverage
|
|
53
|
-
|
|
54
|
-
## [1.0.0] - 2026-01-22
|
|
55
|
-
|
|
56
|
-
### Added
|
|
57
|
-
|
|
58
|
-
- Chainable API for fluent shortcut definitions (`$.cmd.key("s").on(handler)`)
|
|
59
|
-
- Cross-platform `mod` modifier (⌘ on Mac, Ctrl on Windows/Linux)
|
|
60
|
-
- Context-aware exceptions with `.except()` for inputs, modals, and custom predicates
|
|
61
|
-
- Full TypeScript intellisense at every step of the chain
|
|
62
|
-
- Platform-aware display formatting (⌘S on Mac, Ctrl+S on Windows)
|
|
63
|
-
- `useShortcut` hook for React with automatic cleanup
|
|
64
|
-
- `createShortcut` for non-React usage
|
|
65
|
-
- CLI for shadcn-style copy-paste installation
|
|
66
|
-
- Shortcut result object with `enable()`, `disable()`, `trigger()`, and `unbind()`
|
package/dist/cli/index.mjs
DELETED
|
@@ -1,455 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import{existsSync as h,mkdirSync as f,readFileSync as w,writeFileSync as x}from"fs";import{join as l,dirname as B}from"path";import{fileURLToPath as I}from"url";function m(t){return{"scopes.ts":`/** App-defined scope catalog used by the scaffolded provider. */
|
|
3
|
-
export const shortcutScopes = ["global", "navigation", "editor", "modal"] as const
|
|
4
|
-
|
|
5
|
-
export type ShortcutScope = (typeof shortcutScopes)[number]
|
|
6
|
-
|
|
7
|
-
/** Default active scopes at provider boot. */
|
|
8
|
-
export const defaultActiveScopes: ShortcutScope[] = ["global", "navigation"]
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Normalizes a scope input into an array form.
|
|
12
|
-
*
|
|
13
|
-
* @param scopes - One or many scope names
|
|
14
|
-
* @returns Array of scope names
|
|
15
|
-
*/
|
|
16
|
-
export function normalizeScopes(scopes: ShortcutScope | ShortcutScope[]): ShortcutScope[] {
|
|
17
|
-
return Array.isArray(scopes) ? scopes : [scopes]
|
|
18
|
-
}
|
|
19
|
-
`,"registry.ts":`import type { HandlerOptions } from "@remcostoeten/use-shortcut"
|
|
20
|
-
import type { ShortcutScope } from "./scopes"
|
|
21
|
-
|
|
22
|
-
export type ShortcutDefinition = {
|
|
23
|
-
description: string
|
|
24
|
-
defaultKeys: string | string[]
|
|
25
|
-
scopes: ShortcutScope[]
|
|
26
|
-
options?: Omit<HandlerOptions, "scopes">
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const shortcutRegistry = {
|
|
30
|
-
openCommandPalette: {
|
|
31
|
-
description: "Open global command palette",
|
|
32
|
-
defaultKeys: "mod+k",
|
|
33
|
-
scopes: ["global", "navigation"],
|
|
34
|
-
},
|
|
35
|
-
saveDocument: {
|
|
36
|
-
description: "Save the active editor document",
|
|
37
|
-
defaultKeys: "mod+s",
|
|
38
|
-
scopes: ["editor"],
|
|
39
|
-
},
|
|
40
|
-
goDashboard: {
|
|
41
|
-
description: "Navigate to dashboard (vim style sequence)",
|
|
42
|
-
defaultKeys: ["g", "d"],
|
|
43
|
-
scopes: ["navigation"],
|
|
44
|
-
},
|
|
45
|
-
closeOverlay: {
|
|
46
|
-
description: "Close active overlay",
|
|
47
|
-
defaultKeys: "escape",
|
|
48
|
-
scopes: ["global", "modal"],
|
|
49
|
-
},
|
|
50
|
-
} as const satisfies Record<string, ShortcutDefinition>
|
|
51
|
-
|
|
52
|
-
export type ShortcutActionId = keyof typeof shortcutRegistry
|
|
53
|
-
export type ShortcutBindings = Record<ShortcutActionId, string | string[]>
|
|
54
|
-
`,"types.ts":`import type { ShortcutActionId, ShortcutBindings } from "./registry"
|
|
55
|
-
import type { ShortcutScope } from "./scopes"
|
|
56
|
-
|
|
57
|
-
export type ShortcutHandlers = Record<ShortcutActionId, (event: KeyboardEvent) => void>
|
|
58
|
-
|
|
59
|
-
export type ShortcutState = {
|
|
60
|
-
activeScopes: ShortcutScope[]
|
|
61
|
-
bindings: ShortcutBindings
|
|
62
|
-
enabled: boolean
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export type ShortcutActions = {
|
|
66
|
-
setScopes: (scopes: ShortcutScope | ShortcutScope[]) => void
|
|
67
|
-
enableScope: (scope: ShortcutScope) => void
|
|
68
|
-
disableScope: (scope: ShortcutScope) => void
|
|
69
|
-
setBinding: (actionId: ShortcutActionId, keys: string | string[]) => void
|
|
70
|
-
resetBinding: (actionId: ShortcutActionId) => void
|
|
71
|
-
resetBindings: () => void
|
|
72
|
-
setEnabled: (enabled: boolean) => void
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export type ShortcutMeta = {
|
|
76
|
-
hasBindingOverrides: boolean
|
|
77
|
-
availableActions: ShortcutActionId[]
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export type ShortcutContextValue = {
|
|
81
|
-
state: ShortcutState
|
|
82
|
-
actions: ShortcutActions
|
|
83
|
-
meta: ShortcutMeta
|
|
84
|
-
}
|
|
85
|
-
`,"runtime.ts":`import type { ShortcutMap } from "@remcostoeten/use-shortcut"
|
|
86
|
-
import { shortcutRegistry, type ShortcutActionId, type ShortcutBindings } from "./registry"
|
|
87
|
-
import type { ShortcutHandlers } from "./types"
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Returns a fresh binding object seeded from \`shortcutRegistry\` defaults.
|
|
91
|
-
*
|
|
92
|
-
* @returns Default shortcut bindings for every registered action
|
|
93
|
-
*/
|
|
94
|
-
export function createDefaultShortcutBindings(): ShortcutBindings {
|
|
95
|
-
const bindings = {} as ShortcutBindings
|
|
96
|
-
|
|
97
|
-
for (const actionId of Object.keys(shortcutRegistry) as ShortcutActionId[]) {
|
|
98
|
-
bindings[actionId] = shortcutRegistry[actionId].defaultKeys
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return bindings
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Builds a \`ShortcutMap\` by combining current bindings with action handlers.
|
|
106
|
-
* Actions without handlers are skipped.
|
|
107
|
-
*
|
|
108
|
-
* @param bindings - Current binding state (defaults + user overrides)
|
|
109
|
-
* @param handlers - Runtime action handlers from the consuming app
|
|
110
|
-
* @returns Runtime shortcut map consumable by \`registerShortcutMap\`
|
|
111
|
-
*/
|
|
112
|
-
export function buildShortcutMap(bindings: ShortcutBindings, handlers: ShortcutHandlers): ShortcutMap {
|
|
113
|
-
const map: ShortcutMap = {}
|
|
114
|
-
|
|
115
|
-
for (const actionId of Object.keys(shortcutRegistry) as ShortcutActionId[]) {
|
|
116
|
-
const definition = shortcutRegistry[actionId]
|
|
117
|
-
const handler = handlers[actionId]
|
|
118
|
-
|
|
119
|
-
if (!handler) {
|
|
120
|
-
continue
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
map[actionId] = {
|
|
124
|
-
keys: bindings[actionId],
|
|
125
|
-
handler,
|
|
126
|
-
options: {
|
|
127
|
-
...definition.options,
|
|
128
|
-
scopes: definition.scopes,
|
|
129
|
-
description: definition.description,
|
|
130
|
-
},
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return map
|
|
135
|
-
}
|
|
136
|
-
`,"storage.ts":`import type { ShortcutActionId, ShortcutBindings } from "./registry"
|
|
137
|
-
|
|
138
|
-
/** Default localStorage key used by the scaffolded shortcut provider. */
|
|
139
|
-
export const DEFAULT_SHORTCUT_STORAGE_KEY = "app-shortcut-bindings"
|
|
140
|
-
|
|
141
|
-
function _isBindingValue(value: unknown): value is string | string[] {
|
|
142
|
-
if (typeof value === "string") return true
|
|
143
|
-
return Array.isArray(value) && value.every((entry) => typeof entry === "string")
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Loads persisted shortcut binding overrides from localStorage.
|
|
148
|
-
*
|
|
149
|
-
* @param storageKey - Storage key namespace
|
|
150
|
-
* @returns Partial binding overrides keyed by action id
|
|
151
|
-
*/
|
|
152
|
-
export function loadShortcutBindings(storageKey: string): Partial<ShortcutBindings> {
|
|
153
|
-
if (typeof window === "undefined") return {}
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
const raw = window.localStorage.getItem(storageKey)
|
|
157
|
-
if (!raw) return {}
|
|
158
|
-
|
|
159
|
-
const parsed = JSON.parse(raw) as unknown
|
|
160
|
-
if (!parsed || typeof parsed !== "object") return {}
|
|
161
|
-
|
|
162
|
-
const result: Partial<ShortcutBindings> = {}
|
|
163
|
-
|
|
164
|
-
for (const [actionId, value] of Object.entries(parsed as Record<string, unknown>)) {
|
|
165
|
-
if (!_isBindingValue(value)) continue
|
|
166
|
-
result[actionId as ShortcutActionId] = value
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return result
|
|
170
|
-
} catch {
|
|
171
|
-
return {}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Persists all shortcut bindings to localStorage.
|
|
177
|
-
*
|
|
178
|
-
* @param storageKey - Storage key namespace
|
|
179
|
-
* @param bindings - Full current binding set
|
|
180
|
-
*/
|
|
181
|
-
export function saveShortcutBindings(storageKey: string, bindings: ShortcutBindings): void {
|
|
182
|
-
if (typeof window === "undefined") return
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
window.localStorage.setItem(storageKey, JSON.stringify(bindings))
|
|
186
|
-
} catch {
|
|
187
|
-
// Ignore quota/security errors.
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Removes persisted shortcut bindings from localStorage.
|
|
193
|
-
*
|
|
194
|
-
* @param storageKey - Storage key namespace
|
|
195
|
-
*/
|
|
196
|
-
export function clearShortcutBindings(storageKey: string): void {
|
|
197
|
-
if (typeof window === "undefined") return
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
window.localStorage.removeItem(storageKey)
|
|
201
|
-
} catch {
|
|
202
|
-
// Ignore quota/security errors.
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
`,"provider.tsx":`"use client"
|
|
206
|
-
|
|
207
|
-
import {
|
|
208
|
-
createContext,
|
|
209
|
-
useCallback,
|
|
210
|
-
useContext,
|
|
211
|
-
useEffect,
|
|
212
|
-
useMemo,
|
|
213
|
-
useState,
|
|
214
|
-
type ReactNode,
|
|
215
|
-
} from "react"
|
|
216
|
-
import { registerShortcutMap, useShortcut, type UseShortcutOptions } from "@remcostoeten/use-shortcut"
|
|
217
|
-
import { shortcutRegistry, type ShortcutActionId, type ShortcutBindings } from "./registry"
|
|
218
|
-
import { buildShortcutMap, createDefaultShortcutBindings } from "./runtime"
|
|
219
|
-
import { defaultActiveScopes, normalizeScopes, type ShortcutScope } from "./scopes"
|
|
220
|
-
import {
|
|
221
|
-
DEFAULT_SHORTCUT_STORAGE_KEY,
|
|
222
|
-
clearShortcutBindings,
|
|
223
|
-
loadShortcutBindings,
|
|
224
|
-
saveShortcutBindings,
|
|
225
|
-
} from "./storage"
|
|
226
|
-
import type { ShortcutContextValue, ShortcutHandlers } from "./types"
|
|
227
|
-
|
|
228
|
-
const _ShortcutContext = createContext<ShortcutContextValue | null>(null)
|
|
229
|
-
|
|
230
|
-
function _mergeBindings(defaultBindings: ShortcutBindings, persisted: Partial<ShortcutBindings>): ShortcutBindings {
|
|
231
|
-
const merged = { ...defaultBindings }
|
|
232
|
-
|
|
233
|
-
for (const actionId of Object.keys(defaultBindings) as ShortcutActionId[]) {
|
|
234
|
-
const value = persisted[actionId]
|
|
235
|
-
if (typeof value === "string" || Array.isArray(value)) {
|
|
236
|
-
merged[actionId] = value
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return merged
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function _isSameBinding(a: string | string[], b: string | string[]): boolean {
|
|
244
|
-
const left = Array.isArray(a) ? a.join("|") : a
|
|
245
|
-
const right = Array.isArray(b) ? b.join("|") : b
|
|
246
|
-
return left === right
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function _hasBindingOverrides(bindings: ShortcutBindings): boolean {
|
|
250
|
-
const defaults = createDefaultShortcutBindings()
|
|
251
|
-
|
|
252
|
-
for (const actionId of Object.keys(defaults) as ShortcutActionId[]) {
|
|
253
|
-
if (!_isSameBinding(bindings[actionId], defaults[actionId])) {
|
|
254
|
-
return true
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return false
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** Props for the scaffolded \`ShortcutProvider\`. */
|
|
262
|
-
export type ShortcutProviderProps = {
|
|
263
|
-
children: ReactNode
|
|
264
|
-
handlers: ShortcutHandlers
|
|
265
|
-
initialScopes?: ShortcutScope[]
|
|
266
|
-
initialEnabled?: boolean
|
|
267
|
-
persistBindings?: boolean
|
|
268
|
-
storageKey?: string
|
|
269
|
-
shortcutOptions?: Omit<UseShortcutOptions, "activeScopes" | "disabled">
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* App-level provider that binds action handlers, scope state, and optional binding persistence.
|
|
274
|
-
*/
|
|
275
|
-
export function ShortcutProvider({
|
|
276
|
-
children,
|
|
277
|
-
handlers,
|
|
278
|
-
initialScopes = defaultActiveScopes,
|
|
279
|
-
initialEnabled = true,
|
|
280
|
-
persistBindings = true,
|
|
281
|
-
storageKey = DEFAULT_SHORTCUT_STORAGE_KEY,
|
|
282
|
-
shortcutOptions,
|
|
283
|
-
}: ShortcutProviderProps) {
|
|
284
|
-
const [activeScopes, setActiveScopes] = useState<ShortcutScope[]>(initialScopes)
|
|
285
|
-
const [enabled, setEnabled] = useState(initialEnabled)
|
|
286
|
-
const [bindings, setBindings] = useState<ShortcutBindings>(() => createDefaultShortcutBindings())
|
|
287
|
-
|
|
288
|
-
useEffect(() => {
|
|
289
|
-
setEnabled(initialEnabled)
|
|
290
|
-
}, [initialEnabled])
|
|
291
|
-
|
|
292
|
-
useEffect(() => {
|
|
293
|
-
if (!persistBindings) return
|
|
294
|
-
|
|
295
|
-
const persisted = loadShortcutBindings(storageKey)
|
|
296
|
-
setBindings((current) => _mergeBindings(current, persisted))
|
|
297
|
-
}, [persistBindings, storageKey])
|
|
298
|
-
|
|
299
|
-
useEffect(() => {
|
|
300
|
-
if (!persistBindings) return
|
|
301
|
-
saveShortcutBindings(storageKey, bindings)
|
|
302
|
-
}, [bindings, persistBindings, storageKey])
|
|
303
|
-
|
|
304
|
-
const shortcutMap = useMemo(() => buildShortcutMap(bindings, handlers), [bindings, handlers])
|
|
305
|
-
|
|
306
|
-
const $ = useShortcut({
|
|
307
|
-
...shortcutOptions,
|
|
308
|
-
activeScopes,
|
|
309
|
-
disabled: !enabled,
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
useEffect(() => {
|
|
313
|
-
const registrations = registerShortcutMap($, shortcutMap)
|
|
314
|
-
|
|
315
|
-
return () => {
|
|
316
|
-
for (const result of Object.values(registrations)) {
|
|
317
|
-
result.unbind()
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}, [$, shortcutMap])
|
|
321
|
-
|
|
322
|
-
const setScopes = useCallback((scopes: ShortcutScope | ShortcutScope[]) => {
|
|
323
|
-
setActiveScopes(normalizeScopes(scopes))
|
|
324
|
-
}, [])
|
|
325
|
-
|
|
326
|
-
const enableScope = useCallback((scope: ShortcutScope) => {
|
|
327
|
-
setActiveScopes((current) => {
|
|
328
|
-
if (current.includes(scope)) return current
|
|
329
|
-
return [...current, scope]
|
|
330
|
-
})
|
|
331
|
-
}, [])
|
|
332
|
-
|
|
333
|
-
const disableScope = useCallback((scope: ShortcutScope) => {
|
|
334
|
-
setActiveScopes((current) => current.filter((item) => item !== scope))
|
|
335
|
-
}, [])
|
|
336
|
-
|
|
337
|
-
const setBinding = useCallback((actionId: ShortcutActionId, keys: string | string[]) => {
|
|
338
|
-
setBindings((current) => ({
|
|
339
|
-
...current,
|
|
340
|
-
[actionId]: keys,
|
|
341
|
-
}))
|
|
342
|
-
}, [])
|
|
343
|
-
|
|
344
|
-
const resetBinding = useCallback((actionId: ShortcutActionId) => {
|
|
345
|
-
setBindings((current) => ({
|
|
346
|
-
...current,
|
|
347
|
-
[actionId]: shortcutRegistry[actionId].defaultKeys,
|
|
348
|
-
}))
|
|
349
|
-
}, [])
|
|
350
|
-
|
|
351
|
-
const resetBindings = useCallback(() => {
|
|
352
|
-
setBindings(createDefaultShortcutBindings())
|
|
353
|
-
if (persistBindings) clearShortcutBindings(storageKey)
|
|
354
|
-
}, [persistBindings, storageKey])
|
|
355
|
-
|
|
356
|
-
const contextValue = useMemo<ShortcutContextValue>(
|
|
357
|
-
() => ({
|
|
358
|
-
state: {
|
|
359
|
-
activeScopes,
|
|
360
|
-
bindings,
|
|
361
|
-
enabled,
|
|
362
|
-
},
|
|
363
|
-
actions: {
|
|
364
|
-
setScopes,
|
|
365
|
-
enableScope,
|
|
366
|
-
disableScope,
|
|
367
|
-
setBinding,
|
|
368
|
-
resetBinding,
|
|
369
|
-
resetBindings,
|
|
370
|
-
setEnabled,
|
|
371
|
-
},
|
|
372
|
-
meta: {
|
|
373
|
-
hasBindingOverrides: _hasBindingOverrides(bindings),
|
|
374
|
-
availableActions: Object.keys(shortcutRegistry) as ShortcutActionId[],
|
|
375
|
-
},
|
|
376
|
-
}),
|
|
377
|
-
[
|
|
378
|
-
activeScopes,
|
|
379
|
-
bindings,
|
|
380
|
-
enabled,
|
|
381
|
-
setScopes,
|
|
382
|
-
enableScope,
|
|
383
|
-
disableScope,
|
|
384
|
-
setBinding,
|
|
385
|
-
resetBinding,
|
|
386
|
-
resetBindings,
|
|
387
|
-
],
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
return <_ShortcutContext.Provider value={contextValue}>{children}</_ShortcutContext.Provider>
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Reads the shortcut manager context exposed by \`ShortcutProvider\`.
|
|
395
|
-
*/
|
|
396
|
-
export function useShortcutManager(): ShortcutContextValue {
|
|
397
|
-
const context = useContext(_ShortcutContext)
|
|
398
|
-
|
|
399
|
-
if (!context) {
|
|
400
|
-
throw new Error("useShortcutManager must be used within <ShortcutProvider>")
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return context
|
|
404
|
-
}
|
|
405
|
-
`,"index.ts":`export { ShortcutProvider, useShortcutManager, type ShortcutProviderProps } from "./provider"
|
|
406
|
-
export { shortcutRegistry, type ShortcutActionId, type ShortcutBindings } from "./registry"
|
|
407
|
-
export { defaultActiveScopes, shortcutScopes, type ShortcutScope } from "./scopes"
|
|
408
|
-
export type {
|
|
409
|
-
ShortcutContextValue,
|
|
410
|
-
ShortcutActions,
|
|
411
|
-
ShortcutHandlers,
|
|
412
|
-
ShortcutMeta,
|
|
413
|
-
ShortcutState,
|
|
414
|
-
} from "./types"
|
|
415
|
-
`,"README.md":`# Shortcut Architecture Scaffold
|
|
416
|
-
|
|
417
|
-
This folder was generated by \`use-shortcut scaffold\`.
|
|
418
|
-
|
|
419
|
-
It follows a scalable architecture with a strict split between:
|
|
420
|
-
|
|
421
|
-
- **Registry (data)**: \`registry.ts\` is the single source of truth for action ids, default keys, scope ownership, and metadata.
|
|
422
|
-
- **Runtime assembly**: \`runtime.ts\` converts registry + current bindings + handlers into a runtime map.
|
|
423
|
-
- **State and actions provider**: \`provider.tsx\` owns scope state, binding overrides, and persistence.
|
|
424
|
-
- **Storage adapter**: \`storage.ts\` isolates persistence so you can swap \`localStorage\` for API/DB.
|
|
425
|
-
- **Typed contract**: \`types.ts\` exposes \`state/actions/meta\` for UI and feature modules.
|
|
426
|
-
|
|
427
|
-
## How To Extend
|
|
428
|
-
|
|
429
|
-
1. Add a new action in \`registry.ts\`.
|
|
430
|
-
2. Implement its handler in your app and pass it via \`handlers\` to \`<ShortcutProvider>\`.
|
|
431
|
-
3. Optionally expose a user-configurable key in your settings UI through \`useShortcutManager().actions.setBinding\`.
|
|
432
|
-
4. Activate scopes from feature boundaries (for example editor route enters \`editor\` scope).
|
|
433
|
-
|
|
434
|
-
${t==="next"?"## Next.js Integration\n\n1. Create a client provider wrapper at `app/shortcut-provider.tsx` and render `<ShortcutProvider />` there.\n2. Render that provider inside `app/layout.tsx` around your app shell.\n3. Keep page/server components pure; shortcut handlers stay in client components.\n":"## React Integration\n\n1. Wrap your app root (for example in `main.tsx`) with `<ShortcutProvider />`.\n2. Keep handlers in a top-level client component and pass them to the provider via `handlers`.\n3. Use `useShortcutManager()` inside feature components to toggle scopes and bindings.\n"}
|
|
435
|
-
## Rules For Scale
|
|
436
|
-
|
|
437
|
-
- Keep handlers side-effect focused and feature-owned; keep the registry declarative.
|
|
438
|
-
- Use scopes instead of conditionals in handlers.
|
|
439
|
-
- Persist only key bindings, not executable handlers.
|
|
440
|
-
- Treat \`registry.ts\` as your architectural contract.
|
|
441
|
-
`}}var k=I(import.meta.url),_=B(k),e={reset:"\x1B[0m",green:"\x1B[32m",cyan:"\x1B[36m",yellow:"\x1B[33m",dim:"\x1B[2m",red:"\x1B[31m"};function r(t,o=e.reset){console.log(`${o}${t}${e.reset}`)}function O(){return l(_,"..","src")}function R(t){return l(process.cwd(),t,"use-shortcut")}function C(t,o){return l(process.cwd(),t,o)}function p(t,o,a){let s=t.indexOf(o);if(s===-1)return a;let n=t[s+1];return!n||n.startsWith("--")?a:n}function g(t,o){return t.includes(o)}var P=["index.ts","hook.ts","builder.ts","types.ts","parser.ts","constants.ts","formatter.ts","runtime/types.ts","runtime/binding.ts","runtime/conflicts.ts","runtime/debug.ts","runtime/guards.ts","runtime/keys.ts","runtime/listener.ts","runtime/recording.ts"];function E(t="hooks",o=!1){let a=O(),s=R(t);if(r(`
|
|
442
|
-
use-shortcut CLI
|
|
443
|
-
`,e.cyan),h(s)&&!o){r(`Directory already exists: ${s}`,e.yellow),r(`Use --force to overwrite existing files
|
|
444
|
-
`,e.dim);return}f(s,{recursive:!0});let n=0;for(let i of P){let c=l(a,i),d=l(s,i);if(f(B(d),{recursive:!0}),!h(c)){r(`Source file not found: ${i}`,e.yellow);continue}let u=w(c,"utf-8");x(d,u),n+=1,r(` wrote ${i}`,e.green)}r(`
|
|
445
|
-
Copied ${n} files to:`,e.green),r(` ${s}
|
|
446
|
-
`,e.dim),r("Usage:",e.cyan),r(` import { useShortcut } from "@/${t}/use-shortcut"`,e.dim),r(" const $ = useShortcut()",e.dim),r(` $.mod.key("k").on(() => console.log("Search"))
|
|
447
|
-
`,e.dim)}function y(t,o="src",a="shortcuts",s=!1){let n=C(o,a),i=m(t);r(`
|
|
448
|
-
use-shortcut CLI
|
|
449
|
-
`,e.cyan),r(`Scaffolding ${t} architecture in ${n}
|
|
450
|
-
`,e.dim),f(n,{recursive:!0});let c=0,d=0;for(let[u,A]of Object.entries(i)){let S=l(n,u);if(h(S)&&!s){d+=1,r(` skipped ${u} (already exists)`,e.yellow);continue}x(S,A),c+=1,r(` wrote ${u}`,e.green)}r("",e.reset),r(`Architecture scaffold complete: ${c} written, ${d} skipped.`,e.green),r(`Location: ${n}
|
|
451
|
-
`,e.dim),r("Next steps:",e.cyan),r(` 1. Open ${l(o,a,"registry.ts")} and define your action catalog`,e.dim),r(" 2. Wire app handlers into <ShortcutProvider handlers={...} />",e.dim),r(" 3. Toggle scopes from feature boundaries via useShortcutManager()",e.dim),r(` 4. Optionally expose setBinding/resetBinding in your settings UI
|
|
452
|
-
`,e.dim)}function b(){r(`
|
|
453
|
-
use-shortcut CLI
|
|
454
|
-
`,e.cyan),r("Commands:",e.yellow),r(" init [--target hooks] [--force]",e.dim),r(" Copy source files into your project (shadcn-style).",e.dim),r("",e.dim),r(" scaffold [--framework next|react] [--target src] [--dir shortcuts] [--force]",e.dim),r(" Generate a scalable app shortcut architecture.",e.dim),r("",e.dim),r(" init --architecture",e.dim),r(` Alias for scaffold with defaults.
|
|
455
|
-
`,e.dim)}function v(t){if(t==="next"||t==="react")return t;r(`Invalid framework: ${t}. Expected "next" or "react".`,e.red),process.exit(1)}function K(){let t=process.argv.slice(2),o=t[0];if(!o||o==="--help"||o==="-h"||o==="help"){b();return}if(o==="init"){if(g(t,"--architecture")||g(t,"--app")||g(t,"--scaffold")){let i=v(p(t,"--framework","next")),c=p(t,"--target","src"),d=p(t,"--dir","shortcuts"),u=g(t,"--force");y(i,c,d,u);return}let s=p(t,"--target","hooks"),n=g(t,"--force");E(s,n);return}if(o==="scaffold"||o==="architecture"){let s=v(p(t,"--framework","next")),n=p(t,"--target","src"),i=p(t,"--dir","shortcuts"),c=g(t,"--force");y(s,n,i,c);return}b(),process.exit(1)}K();
|