@nick-skriabin/glyph 0.1.30 → 0.1.32
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 +82 -0
- package/dist/index.cjs +83 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -1
- package/dist/index.d.ts +64 -1
- package/dist/index.js +83 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -135,6 +135,16 @@ Text input field with cursor and placeholder support.
|
|
|
135
135
|
|
|
136
136
|
Supports `multiline` for multi-line editing, `autoFocus` for automatic focus on mount. The cursor is always visible when focused.
|
|
137
137
|
|
|
138
|
+
**Input types** for validation:
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
// Text input (default) - accepts any character
|
|
142
|
+
<Input type="text" value={name} onChange={setName} />
|
|
143
|
+
|
|
144
|
+
// Number input - only accepts digits, decimal point, minus sign
|
|
145
|
+
<Input type="number" value={age} onChange={setAge} placeholder="0" />
|
|
146
|
+
```
|
|
147
|
+
|
|
138
148
|
**Input masking** with `onBeforeChange` for validation/formatting:
|
|
139
149
|
|
|
140
150
|
```tsx
|
|
@@ -336,6 +346,25 @@ Declarative keyboard shortcut. Renders nothing.
|
|
|
336
346
|
<Keybind keypress="q" onPress={() => exit()} />
|
|
337
347
|
```
|
|
338
348
|
|
|
349
|
+
**Modifiers:** `ctrl`, `alt`, `shift`, `meta` (Cmd/Super). Combine with `+`: `"ctrl+shift+p"`, `"alt+return"`.
|
|
350
|
+
|
|
351
|
+
**Priority keybinds:** Use `priority` prop to run BEFORE focused input handlers. Useful for keybinds that should work even when an Input is focused:
|
|
352
|
+
|
|
353
|
+
```tsx
|
|
354
|
+
<Keybind keypress="ctrl+return" onPress={submit} priority />
|
|
355
|
+
<Keybind keypress="alt+return" onPress={submit} priority />
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**Terminal configuration:** Some keybinds like `ctrl+return` require terminal support:
|
|
359
|
+
|
|
360
|
+
| Terminal | Configuration |
|
|
361
|
+
|----------|---------------|
|
|
362
|
+
| **Ghostty** | Add to `~/.config/ghostty/config`: `keybind = ctrl+enter=text:\x1b[13;5~` |
|
|
363
|
+
| **iTerm2** | Profiles → Keys → General → Enable "CSI u" mode |
|
|
364
|
+
| **Kitty/WezTerm** | Works out of the box |
|
|
365
|
+
|
|
366
|
+
`alt+return` works universally without configuration.
|
|
367
|
+
|
|
339
368
|
### `<Progress>`
|
|
340
369
|
|
|
341
370
|
Determinate or indeterminate progress bar. Uses `useLayout` to measure actual width and renders block characters.
|
|
@@ -460,6 +489,59 @@ const { focused, focus } = useFocus(ref);
|
|
|
460
489
|
</Box>
|
|
461
490
|
```
|
|
462
491
|
|
|
492
|
+
### `useFocusable(options)`
|
|
493
|
+
|
|
494
|
+
Make any element focusable with full keyboard support. Perfect for building custom interactive components.
|
|
495
|
+
|
|
496
|
+
```tsx
|
|
497
|
+
import { useFocusable, Box, Text } from "@nick-skriabin/glyph";
|
|
498
|
+
|
|
499
|
+
function CustomPicker({ items, onSelect }) {
|
|
500
|
+
const [selected, setSelected] = useState(0);
|
|
501
|
+
|
|
502
|
+
const { ref, isFocused } = useFocusable({
|
|
503
|
+
onKeyPress: (key) => {
|
|
504
|
+
if (key.name === "up") {
|
|
505
|
+
setSelected(s => Math.max(0, s - 1));
|
|
506
|
+
return true; // Consume the key
|
|
507
|
+
}
|
|
508
|
+
if (key.name === "down") {
|
|
509
|
+
setSelected(s => Math.min(items.length - 1, s + 1));
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
if (key.name === "return") {
|
|
513
|
+
onSelect(items[selected]);
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
return false; // Let other handlers process
|
|
517
|
+
},
|
|
518
|
+
onFocus: () => console.log("Picker focused"),
|
|
519
|
+
onBlur: () => console.log("Picker blurred"),
|
|
520
|
+
disabled: false, // Set to true to skip in tab order
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
return (
|
|
524
|
+
<Box
|
|
525
|
+
ref={ref}
|
|
526
|
+
focusable
|
|
527
|
+
style={{
|
|
528
|
+
border: "round",
|
|
529
|
+
borderColor: isFocused ? "cyan" : "gray",
|
|
530
|
+
padding: 1,
|
|
531
|
+
}}
|
|
532
|
+
>
|
|
533
|
+
{items.map((item, i) => (
|
|
534
|
+
<Text key={i} style={{ inverse: i === selected }}>
|
|
535
|
+
{i === selected ? "> " : " "}{item}
|
|
536
|
+
</Text>
|
|
537
|
+
))}
|
|
538
|
+
</Box>
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
Returns `{ ref, isFocused, focus, focusId }`. The `ref` must be attached to an element with `focusable` prop.
|
|
544
|
+
|
|
463
545
|
### `useLayout(nodeRef)`
|
|
464
546
|
|
|
465
547
|
Subscribe to a node's computed layout.
|
package/dist/index.cjs
CHANGED
|
@@ -2394,7 +2394,8 @@ function Input(props) {
|
|
|
2394
2394
|
style,
|
|
2395
2395
|
focusedStyle,
|
|
2396
2396
|
multiline,
|
|
2397
|
-
autoFocus
|
|
2397
|
+
autoFocus,
|
|
2398
|
+
type = "text"
|
|
2398
2399
|
} = props;
|
|
2399
2400
|
const [internalValue, setInternalValue] = React15.useState(defaultValue);
|
|
2400
2401
|
const [cursorPos, setCursorPos] = React15.useState(defaultValue.length);
|
|
@@ -2433,7 +2434,8 @@ function Input(props) {
|
|
|
2433
2434
|
onKeyPress,
|
|
2434
2435
|
onBeforeChange,
|
|
2435
2436
|
multiline: multiline ?? false,
|
|
2436
|
-
innerWidth
|
|
2437
|
+
innerWidth,
|
|
2438
|
+
type
|
|
2437
2439
|
});
|
|
2438
2440
|
stateRef.current = {
|
|
2439
2441
|
isControlled,
|
|
@@ -2441,7 +2443,8 @@ function Input(props) {
|
|
|
2441
2443
|
onKeyPress,
|
|
2442
2444
|
onBeforeChange,
|
|
2443
2445
|
multiline: multiline ?? false,
|
|
2444
|
-
innerWidth
|
|
2446
|
+
innerWidth,
|
|
2447
|
+
type
|
|
2445
2448
|
};
|
|
2446
2449
|
React15.useEffect(() => {
|
|
2447
2450
|
if (!focusCtx || !focusIdRef.current || !nodeRef.current) return;
|
|
@@ -2658,6 +2661,15 @@ function Input(props) {
|
|
|
2658
2661
|
if (key.name.length > 1) return false;
|
|
2659
2662
|
const ch = key.sequence;
|
|
2660
2663
|
if (ch.length === 1 && ch.charCodeAt(0) >= 32) {
|
|
2664
|
+
const { type: inputType } = stateRef.current;
|
|
2665
|
+
if (inputType === "number") {
|
|
2666
|
+
const isDigit = /[0-9]/.test(ch);
|
|
2667
|
+
const isDecimal = ch === "." && !val.includes(".");
|
|
2668
|
+
const isMinus = ch === "-" && pos === 0 && !val.includes("-");
|
|
2669
|
+
if (!isDigit && !isDecimal && !isMinus) {
|
|
2670
|
+
return true;
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2661
2673
|
const newVal = val.slice(0, pos) + ch + val.slice(pos);
|
|
2662
2674
|
updateValue(newVal, pos + 1);
|
|
2663
2675
|
return true;
|
|
@@ -4299,6 +4311,73 @@ function useFocus(nodeRef) {
|
|
|
4299
4311
|
}, [focusCtx, id]);
|
|
4300
4312
|
return { focused: isFocused, focus };
|
|
4301
4313
|
}
|
|
4314
|
+
function useFocusable(options = {}) {
|
|
4315
|
+
const { disabled, onFocus, onBlur, onKeyPress } = options;
|
|
4316
|
+
const focusCtx = React15.useContext(FocusContext);
|
|
4317
|
+
const inputCtx = React15.useContext(InputContext);
|
|
4318
|
+
const nodeRef = React15.useRef(null);
|
|
4319
|
+
const focusIdRef = React15.useRef(null);
|
|
4320
|
+
const [isFocused, setIsFocused] = React15.useState(false);
|
|
4321
|
+
const onFocusRef = React15.useRef(onFocus);
|
|
4322
|
+
const onBlurRef = React15.useRef(onBlur);
|
|
4323
|
+
const onKeyPressRef = React15.useRef(onKeyPress);
|
|
4324
|
+
onFocusRef.current = onFocus;
|
|
4325
|
+
onBlurRef.current = onBlur;
|
|
4326
|
+
onKeyPressRef.current = onKeyPress;
|
|
4327
|
+
const ref = React15.useCallback((node) => {
|
|
4328
|
+
nodeRef.current = node;
|
|
4329
|
+
if (node) {
|
|
4330
|
+
focusIdRef.current = node.focusId ?? null;
|
|
4331
|
+
} else {
|
|
4332
|
+
focusIdRef.current = null;
|
|
4333
|
+
}
|
|
4334
|
+
}, []);
|
|
4335
|
+
React15.useEffect(() => {
|
|
4336
|
+
if (!focusCtx || !focusIdRef.current || !nodeRef.current) return;
|
|
4337
|
+
return focusCtx.register(focusIdRef.current, nodeRef.current);
|
|
4338
|
+
}, [focusCtx]);
|
|
4339
|
+
React15.useEffect(() => {
|
|
4340
|
+
if (!focusCtx || !focusIdRef.current) return;
|
|
4341
|
+
focusCtx.setSkippable(focusIdRef.current, !!disabled);
|
|
4342
|
+
}, [focusCtx, disabled]);
|
|
4343
|
+
React15.useEffect(() => {
|
|
4344
|
+
if (!focusCtx || !focusIdRef.current) return;
|
|
4345
|
+
const fid = focusIdRef.current;
|
|
4346
|
+
const initiallyFocused = focusCtx.focusedId === fid;
|
|
4347
|
+
setIsFocused(initiallyFocused);
|
|
4348
|
+
return focusCtx.onFocusChange((newId) => {
|
|
4349
|
+
const nowFocused = newId === fid;
|
|
4350
|
+
setIsFocused((wasFocused) => {
|
|
4351
|
+
if (nowFocused && !wasFocused) {
|
|
4352
|
+
onFocusRef.current?.();
|
|
4353
|
+
} else if (!nowFocused && wasFocused) {
|
|
4354
|
+
onBlurRef.current?.();
|
|
4355
|
+
}
|
|
4356
|
+
return nowFocused;
|
|
4357
|
+
});
|
|
4358
|
+
});
|
|
4359
|
+
}, [focusCtx]);
|
|
4360
|
+
React15.useEffect(() => {
|
|
4361
|
+
if (!inputCtx || !focusIdRef.current || disabled) return;
|
|
4362
|
+
const fid = focusIdRef.current;
|
|
4363
|
+
const handler = (key) => {
|
|
4364
|
+
if (focusCtx?.focusedId !== fid) return false;
|
|
4365
|
+
return onKeyPressRef.current?.(key) === true;
|
|
4366
|
+
};
|
|
4367
|
+
return inputCtx.registerInputHandler(fid, handler);
|
|
4368
|
+
}, [inputCtx, focusCtx, disabled]);
|
|
4369
|
+
const focus = React15.useCallback(() => {
|
|
4370
|
+
if (focusCtx && focusIdRef.current) {
|
|
4371
|
+
focusCtx.requestFocus(focusIdRef.current);
|
|
4372
|
+
}
|
|
4373
|
+
}, [focusCtx]);
|
|
4374
|
+
return {
|
|
4375
|
+
ref,
|
|
4376
|
+
isFocused,
|
|
4377
|
+
focus,
|
|
4378
|
+
focusId: focusIdRef.current
|
|
4379
|
+
};
|
|
4380
|
+
}
|
|
4302
4381
|
function useApp() {
|
|
4303
4382
|
const ctx = React15.useContext(AppContext);
|
|
4304
4383
|
if (!ctx) {
|
|
@@ -4439,6 +4518,7 @@ exports.render = render;
|
|
|
4439
4518
|
exports.useApp = useApp;
|
|
4440
4519
|
exports.useDialog = useDialog;
|
|
4441
4520
|
exports.useFocus = useFocus;
|
|
4521
|
+
exports.useFocusable = useFocusable;
|
|
4442
4522
|
exports.useInput = useInput;
|
|
4443
4523
|
exports.useLayout = useLayout;
|
|
4444
4524
|
exports.useToast = useToast;
|