@cel-tui/core 0.2.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cel-tui/core",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Core framework engine for cel-tui — primitives, layout, rendering, input",
5
5
  "type": "module",
6
6
  "module": "src/index.ts",
@@ -34,7 +34,7 @@
34
34
  "layout"
35
35
  ],
36
36
  "dependencies": {
37
- "@cel-tui/types": "0.2.1",
37
+ "@cel-tui/types": "0.3.0",
38
38
  "get-east-asian-width": "^1.5.0"
39
39
  },
40
40
  "peerDependencies": {
package/src/cel.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  collectKeyPressHandlers,
9
9
  collectFocusable,
10
10
  } from "./hit-test.js";
11
- import { parseKey, isEditingKey, normalizeKey } from "./keys.js";
11
+ import { parseKey, isEditingKey } from "./keys.js";
12
12
  import { layout, type LayoutNode } from "./layout.js";
13
13
  import {
14
14
  paint,
@@ -555,11 +555,15 @@ function handleMouseEvent(event: MouseEvent): void {
555
555
  if (props.onScroll) {
556
556
  // Controlled scroll: notify app.
557
557
  // Use batch accumulator if available (multiple events in one chunk),
558
- // otherwise read from props.
559
- const baseOffset =
558
+ // otherwise read from props. Clamp to maxOffset first so that
559
+ // Infinity (sticky-bottom) resolves to a finite value before
560
+ // applying the delta — otherwise Infinity + (-1) = Infinity
561
+ // and scrolling up never unsticks.
562
+ const rawBase =
560
563
  batchScrollOffsets?.get(props) ??
561
564
  (props as any).scrollOffset ??
562
565
  0;
566
+ const baseOffset = Math.min(rawBase, maxOffset);
563
567
  const newOffset = Math.max(
564
568
  0,
565
569
  Math.min(maxOffset, baseOffset + delta),
@@ -685,12 +689,13 @@ function handleKeyEvent(key: string, rawData?: string): void {
685
689
  const props = focusedInput.node
686
690
  .props as import("@cel-tui/types").TextInputProps;
687
691
 
688
- // Check submitKey
689
- const submitKey = normalizeKey(props.submitKey ?? "enter");
690
- if (key === submitKey && props.onSubmit) {
691
- props.onSubmit();
692
- cel.render();
693
- return;
692
+ // onKeyPress fires before editing — return false prevents the default action
693
+ if (props.onKeyPress) {
694
+ const result = props.onKeyPress(key);
695
+ if (result === false) {
696
+ cel.render();
697
+ return;
698
+ }
694
699
  }
695
700
 
696
701
  // Editing keys are consumed by TextInput
@@ -728,6 +733,7 @@ function handleKeyEvent(key: string, rawData?: string): void {
728
733
  }
729
734
  break;
730
735
  case "enter":
736
+ case "shift+enter":
731
737
  newState = insertChar(editState, "\n");
732
738
  break;
733
739
  case "tab":
@@ -766,7 +772,16 @@ function handleKeyEvent(key: string, rawData?: string): void {
766
772
  for (let i = currentLayouts.length - 1; i >= 0; i--) {
767
773
  const path = findPathTo(currentLayouts[i]!, focused);
768
774
  if (path) {
769
- const handlers = collectKeyPressHandlers(path);
775
+ let handlers = collectKeyPressHandlers(path);
776
+ // If a TextInput's onKeyPress was already called in the pre-editing
777
+ // hook above, exclude it from bubbling to avoid calling it twice.
778
+ if (
779
+ focusedInput &&
780
+ handlers.length > 0 &&
781
+ handlers[0]!.layoutNode === focusedInput
782
+ ) {
783
+ handlers = handlers.slice(1);
784
+ }
770
785
  if (handlers.length > 0) {
771
786
  let consumed = false;
772
787
  for (const h of handlers) {
package/src/keys.ts CHANGED
@@ -248,6 +248,7 @@ export function isEditingKey(key: string): boolean {
248
248
  "end",
249
249
  "space",
250
250
  "plus",
251
+ "shift+enter",
251
252
  ]);
252
253
 
253
254
  return editingKeys.has(key);
@@ -12,9 +12,12 @@ import type { TextInputNode, TextInputProps } from "@cel-tui/types";
12
12
  * responds to mouse wheel automatically.
13
13
  *
14
14
  * TextInput is always focusable. When focused, text-editing keys
15
- * (printable characters, arrows, backspace, Tab) are consumed.
15
+ * (printable characters, arrows, backspace, Enter, Tab) are consumed.
16
16
  * Modifier combos (e.g., `ctrl+s`) bubble up to ancestor `onKeyPress` handlers.
17
17
  *
18
+ * Use `onKeyPress` to intercept keys before editing. Return `false` to
19
+ * prevent the default editing action for that key.
20
+ *
18
21
  * @param props - Value, callbacks, sizing, styling, and focus props.
19
22
  * @returns A text input node for the UI tree.
20
23
  *
@@ -26,14 +29,15 @@ import type { TextInputNode, TextInputProps } from "@cel-tui/types";
26
29
  * })
27
30
  *
28
31
  * @example
29
- * // Growing input with max height
32
+ * // Growing input with max height, Enter submits
30
33
  * TextInput({
31
34
  * flex: 1,
32
35
  * maxHeight: 10,
33
36
  * value: text,
34
37
  * onChange: handleChange,
35
- * onSubmit: handleSend,
36
- * submitKey: "ctrl+enter",
38
+ * onKeyPress: (key) => {
39
+ * if (key === "enter") { handleSend(); return false; }
40
+ * },
37
41
  * placeholder: Text("type a message...", { fgColor: "color08" }),
38
42
  * })
39
43
  */