use-kbd 0.3.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/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "use-kbd",
3
+ "version": "0.3.0",
4
+ "description": "Keyboard-first UX for React: action registration, shortcuts modal, omnibar, and sequences",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./styles.css": "./src/styles.css"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src/styles.css"
20
+ ],
21
+ "keywords": [
22
+ "keyboard",
23
+ "shortcuts",
24
+ "hotkeys",
25
+ "keybindings",
26
+ "omnibar",
27
+ "command-palette",
28
+ "accessibility",
29
+ "a11y",
30
+ "react",
31
+ "hooks",
32
+ "typescript"
33
+ ],
34
+ "author": "Ryan Williams",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/runsascoded/use-kbd"
39
+ },
40
+ "devDependencies": {
41
+ "@eslint/js": "^9.39.2",
42
+ "@rdub/eslint-config": "^0.0.3",
43
+ "@types/react": "^18.3.27",
44
+ "eslint": "^9.39.2",
45
+ "eslint-plugin-react-hooks": "^7.0.1",
46
+ "eslint-plugin-react-refresh": "^0.4.26",
47
+ "globals": "^16.5.0",
48
+ "react": "^18.3.1",
49
+ "tsup": "^8.3.5",
50
+ "typescript": "^5.7.2",
51
+ "typescript-eslint": "^8.50.1",
52
+ "vitest": "^2.1.8"
53
+ },
54
+ "peerDependencies": {
55
+ "react": ">=18.0.0"
56
+ },
57
+ "dependencies": {
58
+ "@rdub/base": "^0.8.1"
59
+ },
60
+ "scripts": {
61
+ "build": "tsup",
62
+ "dev": "tsup --watch",
63
+ "test": "vitest",
64
+ "lint": "eslint src"
65
+ }
66
+ }
package/src/styles.css ADDED
@@ -0,0 +1,526 @@
1
+ /* use-kbd default styles
2
+ *
3
+ * Override these CSS custom properties to customize the appearance:
4
+ *
5
+ * :root {
6
+ * --kbd-bg: #1f2937;
7
+ * --kbd-text: #f3f4f6;
8
+ * ...
9
+ * }
10
+ */
11
+
12
+ /* === CSS Custom Properties === */
13
+ .kbd-modal,
14
+ .kbd-omnibar {
15
+ /* Colors */
16
+ --kbd-bg: var(--kbd-bg, #ffffff);
17
+ --kbd-bg-secondary: var(--kbd-bg-secondary, #f9fafb);
18
+ --kbd-text: var(--kbd-text, #1f2937);
19
+ --kbd-text-secondary: var(--kbd-text-secondary, #6b7280);
20
+ --kbd-border: var(--kbd-border, #e5e7eb);
21
+ --kbd-accent: var(--kbd-accent, #3b82f6);
22
+ --kbd-accent-hover: var(--kbd-accent-hover, #2563eb);
23
+
24
+ /* Conflict/warning colors */
25
+ --kbd-conflict: var(--kbd-conflict, #ef4444);
26
+ --kbd-conflict-bg: var(--kbd-conflict-bg, #fef2f2);
27
+ --kbd-warning: var(--kbd-warning, #f59e0b);
28
+ --kbd-warning-bg: var(--kbd-warning-bg, #fef3c7);
29
+
30
+ /* Kbd element */
31
+ --kbd-kbd-bg: var(--kbd-kbd-bg, #f3f4f6);
32
+ --kbd-kbd-border: var(--kbd-kbd-border, #d1d5db);
33
+ --kbd-kbd-text: var(--kbd-kbd-text, #374151);
34
+
35
+ /* Spacing & sizing */
36
+ --kbd-radius: var(--kbd-radius, 8px);
37
+ --kbd-radius-sm: var(--kbd-radius-sm, 4px);
38
+ --kbd-gap: var(--kbd-gap, 8px);
39
+ --kbd-padding: var(--kbd-padding, 16px);
40
+
41
+ /* Animation */
42
+ --kbd-transition: var(--kbd-transition, 150ms ease);
43
+ }
44
+
45
+ /* === Backdrop === */
46
+ .kbd-backdrop {
47
+ position: fixed;
48
+ inset: 0;
49
+ background-color: rgba(0, 0, 0, 0.5);
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ z-index: 9999;
54
+ }
55
+
56
+ /* === Modal === */
57
+ .kbd-modal {
58
+ background-color: var(--kbd-bg);
59
+ border-radius: var(--kbd-radius);
60
+ padding: var(--kbd-padding);
61
+ width: 85vw;
62
+ max-width: 480px;
63
+ max-height: 80vh;
64
+ overflow: auto;
65
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
66
+ color: var(--kbd-text);
67
+ }
68
+
69
+ .kbd-modal-header {
70
+ display: flex;
71
+ justify-content: space-between;
72
+ align-items: center;
73
+ margin-bottom: var(--kbd-gap);
74
+ }
75
+
76
+ .kbd-modal-title {
77
+ margin: 0;
78
+ font-size: 1.125rem;
79
+ font-weight: 600;
80
+ }
81
+
82
+ .kbd-modal-close {
83
+ background: none;
84
+ border: none;
85
+ font-size: 1.5rem;
86
+ cursor: pointer;
87
+ padding: 4px;
88
+ line-height: 1;
89
+ color: var(--kbd-text-secondary);
90
+ transition: color var(--kbd-transition);
91
+ }
92
+
93
+ .kbd-modal-close:hover {
94
+ color: var(--kbd-text);
95
+ }
96
+
97
+ /* === Groups === */
98
+ .kbd-group {
99
+ margin-bottom: var(--kbd-padding);
100
+ }
101
+
102
+ .kbd-group-title {
103
+ margin: 0 0 var(--kbd-gap);
104
+ font-size: 0.75rem;
105
+ font-weight: 600;
106
+ text-transform: uppercase;
107
+ color: var(--kbd-text-secondary);
108
+ letter-spacing: 0.05em;
109
+ }
110
+
111
+ /* === Action rows === */
112
+ .kbd-action {
113
+ display: flex;
114
+ justify-content: space-between;
115
+ align-items: center;
116
+ padding: 6px 0;
117
+ border-bottom: 1px solid var(--kbd-border);
118
+ gap: var(--kbd-gap);
119
+ }
120
+
121
+ .kbd-action:last-child {
122
+ border-bottom: none;
123
+ }
124
+
125
+ .kbd-action-label {
126
+ flex: 1;
127
+ font-size: 0.875rem;
128
+ }
129
+
130
+ .kbd-action-bindings {
131
+ display: flex;
132
+ flex-wrap: wrap;
133
+ gap: 6px;
134
+ align-items: center;
135
+ }
136
+
137
+ /* === Kbd element === */
138
+ .kbd-kbd {
139
+ display: inline-flex;
140
+ align-items: center;
141
+ gap: 2px;
142
+ background-color: var(--kbd-kbd-bg);
143
+ border: 1px solid var(--kbd-kbd-border);
144
+ border-radius: var(--kbd-radius-sm);
145
+ padding: 3px 6px;
146
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
147
+ font-size: 0.75rem;
148
+ color: var(--kbd-kbd-text);
149
+ min-height: 24px;
150
+ white-space: nowrap;
151
+ }
152
+
153
+ .kbd-kbd.editable {
154
+ cursor: pointer;
155
+ transition: all var(--kbd-transition);
156
+ }
157
+
158
+ .kbd-kbd.editable:hover,
159
+ .kbd-kbd.editable:focus {
160
+ border-color: var(--kbd-accent);
161
+ background-color: var(--kbd-bg-secondary);
162
+ outline: none;
163
+ }
164
+
165
+ .kbd-kbd.editable:focus-visible {
166
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
167
+ }
168
+
169
+ .kbd-kbd.editing {
170
+ border-color: var(--kbd-accent);
171
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
172
+ }
173
+
174
+ .kbd-kbd.conflict {
175
+ border-color: var(--kbd-conflict);
176
+ background-color: var(--kbd-conflict-bg);
177
+ color: var(--kbd-conflict);
178
+ }
179
+
180
+ .kbd-kbd.pending-conflict {
181
+ border-color: var(--kbd-warning);
182
+ background-color: var(--kbd-warning-bg);
183
+ color: #92400e;
184
+ animation: kbd-pulse 1s ease-in-out infinite;
185
+ }
186
+
187
+ .kbd-kbd.default-binding {
188
+ border-bottom: 1px solid var(--kbd-text-secondary);
189
+ border-bottom-left-radius: 0;
190
+ border-bottom-right-radius: 0;
191
+ }
192
+
193
+ @keyframes kbd-pulse {
194
+ 0%, 100% { opacity: 1; }
195
+ 50% { opacity: 0.7; }
196
+ }
197
+
198
+ /* === Modifier icons === */
199
+ .kbd-modifier-icon {
200
+ width: 12px;
201
+ height: 12px;
202
+ flex-shrink: 0;
203
+ }
204
+
205
+ /* === Sequence separator === */
206
+ .kbd-sequence-sep {
207
+ color: var(--kbd-text-secondary);
208
+ margin: 0 1px;
209
+ }
210
+
211
+ /* === Add binding button === */
212
+ .kbd-add-btn {
213
+ background: none;
214
+ border: 1px dashed var(--kbd-border);
215
+ border-radius: var(--kbd-radius-sm);
216
+ padding: 3px 8px;
217
+ font-size: 0.875rem;
218
+ color: var(--kbd-text-secondary);
219
+ cursor: pointer;
220
+ transition: all var(--kbd-transition);
221
+ }
222
+
223
+ .kbd-add-btn:hover {
224
+ border-color: var(--kbd-accent);
225
+ color: var(--kbd-accent);
226
+ }
227
+
228
+ /* === Remove binding button === */
229
+ .kbd-remove-btn {
230
+ background: none;
231
+ border: none;
232
+ padding: 0;
233
+ margin-left: 2px;
234
+ font-size: 0.875rem;
235
+ color: var(--kbd-text-secondary);
236
+ cursor: pointer;
237
+ opacity: 0;
238
+ transition: opacity var(--kbd-transition);
239
+ }
240
+
241
+ .kbd-kbd:hover .kbd-remove-btn {
242
+ opacity: 1;
243
+ }
244
+
245
+ .kbd-remove-btn:hover {
246
+ color: var(--kbd-conflict);
247
+ }
248
+
249
+ /* === Timeout bar === */
250
+ .kbd-timeout-bar {
251
+ position: absolute;
252
+ bottom: 0;
253
+ left: 0;
254
+ height: 2px;
255
+ background-color: var(--kbd-accent);
256
+ animation: kbd-timeout linear forwards;
257
+ }
258
+
259
+ @keyframes kbd-timeout {
260
+ from { width: 100%; }
261
+ to { width: 0%; }
262
+ }
263
+
264
+ /* === Omnibar === */
265
+ .kbd-omnibar-backdrop {
266
+ position: fixed;
267
+ inset: 0;
268
+ background-color: rgba(0, 0, 0, 0.5);
269
+ display: flex;
270
+ align-items: flex-start;
271
+ justify-content: center;
272
+ padding-top: 20vh;
273
+ z-index: 9999;
274
+ }
275
+
276
+ .kbd-omnibar {
277
+ background-color: var(--kbd-bg);
278
+ border-radius: var(--kbd-radius);
279
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
280
+ width: 90vw;
281
+ max-width: 500px;
282
+ overflow: hidden;
283
+ }
284
+
285
+ .kbd-omnibar-input {
286
+ width: 100%;
287
+ padding: 12px var(--kbd-padding);
288
+ border: none;
289
+ border-bottom: 1px solid var(--kbd-border);
290
+ font-size: 1rem;
291
+ background-color: var(--kbd-bg);
292
+ color: var(--kbd-text);
293
+ outline: none;
294
+ }
295
+
296
+ .kbd-omnibar-input::placeholder {
297
+ color: var(--kbd-text-secondary);
298
+ }
299
+
300
+ .kbd-omnibar-results {
301
+ max-height: 300px;
302
+ overflow-y: auto;
303
+ }
304
+
305
+ .kbd-omnibar-result {
306
+ display: flex;
307
+ align-items: center;
308
+ padding: 10px var(--kbd-padding);
309
+ cursor: pointer;
310
+ gap: var(--kbd-gap);
311
+ transition: background-color var(--kbd-transition);
312
+ }
313
+
314
+ .kbd-omnibar-result:hover,
315
+ .kbd-omnibar-result.selected {
316
+ background-color: var(--kbd-bg-secondary);
317
+ }
318
+
319
+ .kbd-omnibar-result.selected {
320
+ border-left: 3px solid var(--kbd-accent);
321
+ padding-left: calc(var(--kbd-padding) - 3px);
322
+ }
323
+
324
+ .kbd-omnibar-result-label {
325
+ flex: 1;
326
+ font-size: 0.875rem;
327
+ }
328
+
329
+ .kbd-omnibar-result-category {
330
+ font-size: 0.75rem;
331
+ color: var(--kbd-text-secondary);
332
+ }
333
+
334
+ .kbd-omnibar-result-bindings {
335
+ display: flex;
336
+ gap: 4px;
337
+ }
338
+
339
+ .kbd-omnibar-no-results {
340
+ padding: var(--kbd-padding);
341
+ text-align: center;
342
+ color: var(--kbd-text-secondary);
343
+ font-size: 0.875rem;
344
+ }
345
+
346
+ /* === Sequence Modal === */
347
+ .kbd-sequence-backdrop {
348
+ position: fixed;
349
+ inset: 0;
350
+ display: flex;
351
+ align-items: flex-start;
352
+ justify-content: center;
353
+ padding-top: 20vh;
354
+ z-index: 9998;
355
+ background-color: rgba(0, 0, 0, 0.2);
356
+ animation: kbd-fade-in 0.1s ease;
357
+ }
358
+
359
+ @keyframes kbd-fade-in {
360
+ from { opacity: 0; }
361
+ to { opacity: 1; }
362
+ }
363
+
364
+ .kbd-sequence {
365
+ /* Inherit CSS custom properties */
366
+ --kbd-bg: var(--kbd-bg, #ffffff);
367
+ --kbd-bg-secondary: var(--kbd-bg-secondary, #f9fafb);
368
+ --kbd-text: var(--kbd-text, #1f2937);
369
+ --kbd-text-secondary: var(--kbd-text-secondary, #6b7280);
370
+ --kbd-border: var(--kbd-border, #e5e7eb);
371
+ --kbd-kbd-bg: var(--kbd-kbd-bg, #f3f4f6);
372
+ --kbd-kbd-border: var(--kbd-kbd-border, #d1d5db);
373
+ --kbd-kbd-text: var(--kbd-kbd-text, #374151);
374
+ --kbd-radius: var(--kbd-radius, 8px);
375
+ --kbd-accent: var(--kbd-accent, #3b82f6);
376
+
377
+ background-color: var(--kbd-bg);
378
+ border: 1px solid var(--kbd-border);
379
+ border-radius: 12px;
380
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
381
+ min-width: 280px;
382
+ max-width: 400px;
383
+ overflow: hidden;
384
+ animation: kbd-slide-down 0.15s ease;
385
+ color: var(--kbd-text);
386
+ }
387
+
388
+ @keyframes kbd-slide-down {
389
+ from {
390
+ opacity: 0;
391
+ transform: translateY(-10px);
392
+ }
393
+ to {
394
+ opacity: 1;
395
+ transform: translateY(0);
396
+ }
397
+ }
398
+
399
+ .kbd-sequence-current {
400
+ display: flex;
401
+ align-items: center;
402
+ justify-content: center;
403
+ gap: 4px;
404
+ padding: 16px 20px;
405
+ border-bottom: 1px solid var(--kbd-border);
406
+ background-color: var(--kbd-bg-secondary);
407
+ }
408
+
409
+ .kbd-sequence-keys {
410
+ display: inline-flex;
411
+ align-items: center;
412
+ gap: 4px;
413
+ padding: 6px 12px;
414
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
415
+ font-size: 1.25rem;
416
+ font-weight: 600;
417
+ background-color: var(--kbd-bg);
418
+ border: 1px solid var(--kbd-border);
419
+ border-radius: 6px;
420
+ color: var(--kbd-text);
421
+ }
422
+
423
+ .kbd-sequence-ellipsis {
424
+ font-size: 1.5rem;
425
+ color: var(--kbd-text-secondary);
426
+ animation: kbd-ellipsis-pulse 0.8s ease-in-out infinite;
427
+ }
428
+
429
+ @keyframes kbd-ellipsis-pulse {
430
+ 0%, 100% { opacity: 1; }
431
+ 50% { opacity: 0.4; }
432
+ }
433
+
434
+ .kbd-sequence-timeout {
435
+ height: 3px;
436
+ background: linear-gradient(90deg, var(--kbd-accent), #60a5fa);
437
+ transform-origin: left;
438
+ animation: kbd-shrink linear forwards;
439
+ }
440
+
441
+ @keyframes kbd-shrink {
442
+ from { transform: scaleX(1); }
443
+ to { transform: scaleX(0); }
444
+ }
445
+
446
+ .kbd-sequence-completions {
447
+ padding: 12px 16px;
448
+ display: flex;
449
+ flex-direction: column;
450
+ gap: 8px;
451
+ max-height: 300px;
452
+ overflow-y: auto;
453
+ }
454
+
455
+ .kbd-sequence-completion {
456
+ display: flex;
457
+ align-items: center;
458
+ gap: 8px;
459
+ padding: 8px 12px;
460
+ background-color: var(--kbd-bg-secondary);
461
+ border-radius: 6px;
462
+ transition: background-color 0.1s ease;
463
+ }
464
+
465
+ .kbd-sequence-completion:hover {
466
+ background-color: var(--kbd-bg);
467
+ }
468
+
469
+ .kbd-sequence-arrow {
470
+ color: var(--kbd-text-secondary);
471
+ font-size: 0.9rem;
472
+ }
473
+
474
+ .kbd-sequence-actions {
475
+ flex: 1;
476
+ font-size: 0.9rem;
477
+ color: var(--kbd-text);
478
+ }
479
+
480
+ .kbd-sequence-empty {
481
+ padding: 16px;
482
+ text-align: center;
483
+ color: var(--kbd-text-secondary);
484
+ font-style: italic;
485
+ font-size: 0.9rem;
486
+ }
487
+
488
+ /* === Table layout for two-column group renderers === */
489
+ .kbd-table {
490
+ width: 100%;
491
+ border-collapse: collapse;
492
+ }
493
+
494
+ .kbd-table th,
495
+ .kbd-table td {
496
+ padding: 6px var(--kbd-gap);
497
+ text-align: left;
498
+ border-bottom: 1px solid var(--kbd-border);
499
+ }
500
+
501
+ .kbd-table th {
502
+ font-weight: 600;
503
+ font-size: 0.85rem;
504
+ color: var(--kbd-text-secondary);
505
+ }
506
+
507
+ .kbd-table td:not(:first-child) {
508
+ text-align: center;
509
+ }
510
+
511
+ /* === Dark mode preset === */
512
+ [data-theme="dark"] .kbd-modal,
513
+ [data-theme="dark"] .kbd-omnibar,
514
+ [data-theme="dark"] .kbd-sequence,
515
+ .dark .kbd-modal,
516
+ .dark .kbd-omnibar,
517
+ .dark .kbd-sequence {
518
+ --kbd-bg: var(--kbd-bg, #1f2937);
519
+ --kbd-bg-secondary: var(--kbd-bg-secondary, #374151);
520
+ --kbd-text: var(--kbd-text, #f3f4f6);
521
+ --kbd-text-secondary: var(--kbd-text-secondary, #9ca3af);
522
+ --kbd-border: var(--kbd-border, #4b5563);
523
+ --kbd-kbd-bg: var(--kbd-kbd-bg, #374151);
524
+ --kbd-kbd-border: var(--kbd-kbd-border, #4b5563);
525
+ --kbd-kbd-text: var(--kbd-kbd-text, #e5e7eb);
526
+ }