@k3-universe/react-kit 0.0.37 → 0.0.39

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.
@@ -0,0 +1,4 @@
1
+ export { Keyboard } from './Keyboard';
2
+ export type { KeyboardProps, KeyboardLayout } from './Keyboard';
3
+ export default void 0;
4
+
@@ -0,0 +1,377 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Delete, ChevronLeft } from "lucide-react";
5
+ import { cn } from "../../../shadcn/lib/utils";
6
+ import { Button } from "../../../shadcn/ui/button";
7
+
8
+ // Constants
9
+ const NUMBER_BUTTON_BASE_CLASSES =
10
+ "rounded-xl border-2 border-teal-200 bg-white text-3xl font-semibold text-teal-600 hover:text-teal-900 transition-colors hover:bg-teal-50 h-auto";
11
+ const CLEAR_BUTTON_CLASSES =
12
+ "rounded-xl border-2 border-teal-200 bg-white text-2xl font-semibold text-teal-600 hover:text-teal-900 transition-colors hover:bg-teal-50 h-auto";
13
+ const BACKSPACE_BUTTON_CLASSES =
14
+ "rounded-xl bg-red-600 text-white transition-colors hover:bg-red-700 h-auto";
15
+ const SUBMIT_BUTTON_CLASSES =
16
+ "row-span-3 rounded-xl bg-green-400 transition-colors hover:bg-green-500 h-auto";
17
+
18
+ export interface NumpadProps
19
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
20
+ value?: string;
21
+ onChange?: (value: string) => void;
22
+ disabled?: boolean;
23
+ allowDecimal?: boolean;
24
+ allowDoubleZero?: boolean;
25
+ maxLength?: number;
26
+ className?: string;
27
+ buttonClassName?: string;
28
+ customKeyStyle?: React.CSSProperties;
29
+ customKeyClassName?: string;
30
+ customSubmitStyle?: React.CSSProperties;
31
+ customSubmitClassName?: string;
32
+ showBackspaceButton?: boolean;
33
+ showClearButton?: boolean;
34
+ showSubmitButton?: boolean;
35
+ onSubmit?: () => void;
36
+ inputRef?: React.RefObject<HTMLInputElement | HTMLTextAreaElement | null>;
37
+ }
38
+
39
+ export function Numpad({
40
+ value: controlledValue,
41
+ onChange,
42
+ disabled = false,
43
+ allowDecimal = false,
44
+ allowDoubleZero = true,
45
+ maxLength,
46
+ className,
47
+ buttonClassName,
48
+ customKeyStyle,
49
+ customKeyClassName,
50
+ customSubmitStyle,
51
+ customSubmitClassName,
52
+ showBackspaceButton = true,
53
+ showClearButton = true,
54
+ showSubmitButton = false,
55
+ onSubmit,
56
+ inputRef,
57
+ ...props
58
+ }: NumpadProps) {
59
+ const [internalValue, setInternalValue] = React.useState("");
60
+ const isControlled = controlledValue !== undefined;
61
+ const value = isControlled ? controlledValue : internalValue;
62
+
63
+ // Helper function to update input value with cursor position handling
64
+ const updateInputValue = React.useCallback(
65
+ (
66
+ input: HTMLInputElement | HTMLTextAreaElement,
67
+ newValue: string,
68
+ cursorPos: number
69
+ ) => {
70
+ input.value = newValue;
71
+ input.setSelectionRange(cursorPos, cursorPos);
72
+ const event = new Event("input", { bubbles: true });
73
+ input.dispatchEvent(event);
74
+
75
+ if (!isControlled) {
76
+ setInternalValue(newValue);
77
+ }
78
+ onChange?.(newValue);
79
+ },
80
+ [isControlled, onChange]
81
+ );
82
+
83
+ const handleValueChange = React.useCallback(
84
+ (newValue: string) => {
85
+ if (disabled) return;
86
+ if (maxLength && newValue.length > maxLength) return;
87
+
88
+ if (!isControlled) {
89
+ setInternalValue(newValue);
90
+ }
91
+ onChange?.(newValue);
92
+ },
93
+ [disabled, maxLength, isControlled, onChange]
94
+ );
95
+
96
+ const handleNumberClick = React.useCallback(
97
+ (num: string) => {
98
+ if (disabled) return;
99
+
100
+ if (inputRef?.current) {
101
+ const input = inputRef.current;
102
+ const start = input.selectionStart ?? 0;
103
+ const end = input.selectionEnd ?? 0;
104
+ const newValue =
105
+ input.value.slice(0, start) + num + input.value.slice(end);
106
+ const newCursorPos = start + num.length;
107
+
108
+ if (maxLength && newValue.length > maxLength) return;
109
+
110
+ updateInputValue(input, newValue, newCursorPos);
111
+ return;
112
+ }
113
+
114
+ handleValueChange(value + num);
115
+ },
116
+ [disabled, value, handleValueChange, inputRef, maxLength, updateInputValue]
117
+ );
118
+
119
+ const handleDecimalClick = React.useCallback(() => {
120
+ if (disabled || !allowDecimal) return;
121
+
122
+ if (inputRef?.current) {
123
+ const input = inputRef.current;
124
+ if (input.value.includes(".")) return;
125
+
126
+ const start = input.selectionStart ?? 0;
127
+ const end = input.selectionEnd ?? 0;
128
+ const newValue = input.value.slice(0, start) + "." + input.value.slice(end);
129
+ const newCursorPos = start + 1;
130
+
131
+ if (maxLength && newValue.length > maxLength) return;
132
+
133
+ updateInputValue(input, newValue, newCursorPos);
134
+ return;
135
+ }
136
+
137
+ if (value.includes(".")) return;
138
+ handleValueChange(value + ".");
139
+ }, [
140
+ disabled,
141
+ allowDecimal,
142
+ value,
143
+ handleValueChange,
144
+ inputRef,
145
+ maxLength,
146
+ updateInputValue,
147
+ ]);
148
+
149
+ const handleBackspace = React.useCallback(() => {
150
+ if (disabled) return;
151
+
152
+ if (inputRef?.current) {
153
+ const input = inputRef.current;
154
+ const start = input.selectionStart ?? 0;
155
+ const end = input.selectionEnd ?? 0;
156
+
157
+ if (start === end && start === 0) return;
158
+
159
+ const newValue =
160
+ start !== end
161
+ ? input.value.slice(0, start) + input.value.slice(end)
162
+ : input.value.slice(0, start - 1) + input.value.slice(start);
163
+ const newCursorPos = start !== end ? start : Math.max(0, start - 1);
164
+
165
+ updateInputValue(input, newValue, newCursorPos);
166
+ return;
167
+ }
168
+
169
+ if (value.length > 0) {
170
+ handleValueChange(value.slice(0, -1));
171
+ }
172
+ }, [disabled, value, handleValueChange, inputRef, updateInputValue]);
173
+
174
+ const handleClear = React.useCallback(() => {
175
+ handleValueChange("");
176
+ }, [handleValueChange]);
177
+
178
+ const handleSubmit = React.useCallback(() => {
179
+ if (disabled) return;
180
+
181
+ // If inputRef is provided, handle Enter like a real keyboard
182
+ if (inputRef?.current) {
183
+ const input = inputRef.current;
184
+ const isTextarea = input.tagName === 'TEXTAREA';
185
+ const isInput = input.tagName === 'INPUT';
186
+
187
+ if (isTextarea) {
188
+ const start = input.selectionStart ?? 0;
189
+ const end = input.selectionEnd ?? 0;
190
+ const newValue =
191
+ input.value.slice(0, start) + "\n" + input.value.slice(end);
192
+ const newCursorPos = start + 1;
193
+ updateInputValue(input, newValue, newCursorPos);
194
+ } else if (isInput) {
195
+ // For input: trigger Enter key events like real keyboard
196
+ // This will trigger form submission if inside a form
197
+
198
+ // Create and dispatch keydown event
199
+ const keyDownEvent = new KeyboardEvent('keydown', {
200
+ key: 'Enter',
201
+ code: 'Enter',
202
+ keyCode: 13,
203
+ which: 13,
204
+ bubbles: true,
205
+ cancelable: true,
206
+ });
207
+ input.dispatchEvent(keyDownEvent);
208
+
209
+ // Only trigger keypress and keyup if keydown wasn't prevented
210
+ if (!keyDownEvent.defaultPrevented) {
211
+ // Create and dispatch keypress event
212
+ const keyPressEvent = new KeyboardEvent('keypress', {
213
+ key: 'Enter',
214
+ code: 'Enter',
215
+ keyCode: 13,
216
+ which: 13,
217
+ bubbles: true,
218
+ cancelable: true,
219
+ });
220
+ input.dispatchEvent(keyPressEvent);
221
+
222
+ // Create and dispatch keyup event
223
+ const keyUpEvent = new KeyboardEvent('keyup', {
224
+ key: 'Enter',
225
+ code: 'Enter',
226
+ keyCode: 13,
227
+ which: 13,
228
+ bubbles: true,
229
+ cancelable: true,
230
+ });
231
+ input.dispatchEvent(keyUpEvent);
232
+
233
+ // If input is inside a form, trigger form submit
234
+ const form = input.closest('form');
235
+ if (form && !keyPressEvent.defaultPrevented) {
236
+ // Trigger form submit event
237
+ const submitEvent = new Event('submit', {
238
+ bubbles: true,
239
+ cancelable: true,
240
+ });
241
+ form.dispatchEvent(submitEvent);
242
+
243
+ // If submit wasn't prevented, actually submit the form
244
+ if (!submitEvent.defaultPrevented) {
245
+ form.requestSubmit();
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ onSubmit?.();
253
+ }, [disabled, onSubmit, inputRef, updateInputValue]);
254
+
255
+ // Helper component for number buttons
256
+ const NumberButton = React.useCallback(
257
+ ({ num, onClick }: { num: string; onClick: () => void }) => (
258
+ <Button
259
+ type="button"
260
+ disabled={disabled}
261
+ onClick={onClick}
262
+ style={customKeyStyle}
263
+ className={cn(
264
+ NUMBER_BUTTON_BASE_CLASSES,
265
+ buttonClassName,
266
+ customKeyClassName
267
+ )}
268
+ >
269
+ {num}
270
+ </Button>
271
+ ),
272
+ [disabled, customKeyStyle, buttonClassName, customKeyClassName]
273
+ );
274
+
275
+ return (
276
+ <div className={cn("grid grid-cols-4 gap-3 w-fit", className)} {...props}>
277
+ {/* Layout: 1,2,3,Delete | 4,5,6,Submit | 7,8,9 | 0,00,000 */}
278
+ {/* Row 1: 1, 2, 3, Delete */}
279
+ <NumberButton num="1" onClick={() => handleNumberClick("1")} />
280
+ <NumberButton num="2" onClick={() => handleNumberClick("2")} />
281
+ <NumberButton num="3" onClick={() => handleNumberClick("3")} />
282
+ {showBackspaceButton && (
283
+ <button
284
+ type="button"
285
+ disabled={disabled || value.length === 0}
286
+ onClick={handleBackspace}
287
+ style={customKeyStyle}
288
+ className={cn(
289
+ BACKSPACE_BUTTON_CLASSES,
290
+ buttonClassName,
291
+ customKeyClassName
292
+ )}
293
+ >
294
+ <Delete className="mx-auto h-8 w-8" />
295
+ </button>
296
+ )}
297
+
298
+ {/* Row 2: 4, 5, 6, Clear */}
299
+ <NumberButton num="4" onClick={() => handleNumberClick("4")} />
300
+ <NumberButton num="5" onClick={() => handleNumberClick("5")} />
301
+ <NumberButton num="6" onClick={() => handleNumberClick("6")} />
302
+ {showClearButton && (
303
+ <Button
304
+ type="button"
305
+ disabled={disabled || value.length === 0}
306
+ onClick={handleClear}
307
+ style={customKeyStyle}
308
+ className={cn(
309
+ CLEAR_BUTTON_CLASSES,
310
+ buttonClassName,
311
+ customKeyClassName
312
+ )}
313
+ >
314
+ Clear
315
+ </Button>
316
+ )}
317
+ {showSubmitButton && (
318
+ <>
319
+ <div />
320
+ <div />
321
+ <div />
322
+ <button
323
+ type="button"
324
+ disabled={disabled}
325
+ onClick={handleSubmit}
326
+ style={customSubmitStyle}
327
+ className={cn(
328
+ SUBMIT_BUTTON_CLASSES,
329
+ buttonClassName,
330
+ customSubmitClassName
331
+ )}
332
+ >
333
+ <ChevronLeft className="mx-auto h-12 w-12 rotate-180 text-gray-800" />
334
+ </button>
335
+ </>
336
+ )}
337
+
338
+ {/* Row 3: 7, 8, 9 */}
339
+ <NumberButton num="7" onClick={() => handleNumberClick("7")} />
340
+ <NumberButton num="8" onClick={() => handleNumberClick("8")} />
341
+ <NumberButton num="9" onClick={() => handleNumberClick("9")} />
342
+
343
+ {/* Row 4: 0, 00, 000 */}
344
+ <NumberButton num="0" onClick={() => handleNumberClick("0")} />
345
+ {allowDoubleZero ? (
346
+ <>
347
+ <NumberButton num="00" onClick={() => handleNumberClick("00")} />
348
+ <NumberButton num="000" onClick={() => handleNumberClick("000")} />
349
+ </>
350
+ ) : allowDecimal ? (
351
+ <Button
352
+ type="button"
353
+ disabled={disabled}
354
+ onClick={handleDecimalClick}
355
+ style={customKeyStyle}
356
+ className={cn(
357
+ NUMBER_BUTTON_BASE_CLASSES,
358
+ buttonClassName,
359
+ customKeyClassName
360
+ )}
361
+ >
362
+ .
363
+ </Button>
364
+ ) : (
365
+ <>
366
+ <div />
367
+ <div />
368
+ </>
369
+ )}
370
+ {showSubmitButton && <div />}
371
+ </div>
372
+ );
373
+ }
374
+
375
+ Numpad.displayName = "Numpad";
376
+
377
+ export default Numpad;
@@ -0,0 +1,4 @@
1
+ export { Numpad } from './Numpad';
2
+ export type { NumpadProps } from './Numpad';
3
+ export default void 0;
4
+
@@ -133,7 +133,7 @@ function Calendar({
133
133
  return (
134
134
  <div
135
135
  data-slot="calendar"
136
- ref={rootRef}
136
+ ref={rootRef as React.Ref<HTMLDivElement>}
137
137
  className={cn(className)}
138
138
  {...props}
139
139
  />
@@ -0,0 +1,263 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import * as React from "react";
3
+ import { Keyboard } from "../../../kit/components/keyboard/Keyboard";
4
+ import { Input } from "../../../shadcn/ui/input";
5
+
6
+ const meta: Meta<typeof Keyboard> = {
7
+ title: "Kit/Components/Keyboard",
8
+ component: Keyboard,
9
+ parameters: {
10
+ controls: { expanded: true },
11
+ backgrounds: { disable: true },
12
+ },
13
+ argTypes: {
14
+ layout: {
15
+ control: "select",
16
+ options: ["qwerty", "qwertz", "azerty"],
17
+ },
18
+ },
19
+ };
20
+
21
+ export default meta;
22
+
23
+ type Story = StoryObj<typeof Keyboard>;
24
+
25
+ export const FullKeyboard: Story = {
26
+ name: "Full Keyboard",
27
+ render: (args) => {
28
+ const [value, setValue] = React.useState("");
29
+ const inputRef = React.useRef<HTMLInputElement>(null);
30
+
31
+ return (
32
+ <div className="p-6 space-y-4">
33
+ <div>
34
+ <div className="text-sm text-muted-foreground mb-2">Text Input</div>
35
+ <Input
36
+ ref={inputRef}
37
+ type="text"
38
+ value={value}
39
+ onChange={(e) => {
40
+ setValue((e.target as any).value);
41
+ }}
42
+ placeholder="Type here..."
43
+ className="min-h-[60px]"
44
+ />
45
+ </div>
46
+ <Keyboard
47
+ {...args}
48
+ value={value}
49
+ onChange={setValue}
50
+ inputRef={inputRef}
51
+ showNumbers
52
+ showShift
53
+ showSpace
54
+ showBackspace
55
+ showEnter
56
+ />
57
+ </div>
58
+ );
59
+ },
60
+ };
61
+
62
+ export const Disabled: Story = {
63
+ name: "Disabled",
64
+ render: (args) => {
65
+ const [value, setValue] = React.useState("Hello World");
66
+
67
+ return (
68
+ <div className="p-6 space-y-4">
69
+ <div>
70
+ <div className="text-sm text-muted-foreground mb-2">Text Input</div>
71
+ <Input
72
+ type="text"
73
+ value={value}
74
+ onChange={(e) => setValue((e.target as any).value)}
75
+ placeholder="Type here..."
76
+ disabled
77
+ className="min-h-[60px]"
78
+ />
79
+ </div>
80
+ <Keyboard {...args} value={value} onChange={setValue} disabled />
81
+ </div>
82
+ );
83
+ },
84
+ };
85
+
86
+ export const CustomStyling: Story = {
87
+ name: "Custom Styling",
88
+ render: (args) => {
89
+ const [value, setValue] = React.useState("");
90
+
91
+ return (
92
+ <div className="flex-1 p-6 space-y-4">
93
+ <div>
94
+ <div className="text-sm text-muted-foreground mb-2">Text Input</div>
95
+ <Input
96
+ type="text"
97
+ value={value}
98
+ onChange={(e) => {
99
+ setValue((e.target as any).value);
100
+ }}
101
+ placeholder="Type here..."
102
+ className="min-h-[60px]"
103
+ />
104
+ </div>
105
+ <Keyboard
106
+ {...args}
107
+ value={value}
108
+ onChange={setValue}
109
+ showNumbers
110
+ showShift
111
+ showSpace
112
+ showBackspace
113
+ showEnter
114
+ customKeyStyle={{ minWidth: "50px", height: "45px" }}
115
+ customKeyClassName="text-base font-bold"
116
+ />
117
+ </div>
118
+ );
119
+ },
120
+ };
121
+
122
+ export const MultipleInputs: Story = {
123
+ name: "Multiple Inputs (Focused)",
124
+ render: (args) => {
125
+ const [value1, setValue1] = React.useState("");
126
+ const [value2, setValue2] = React.useState("");
127
+ const [value3, setValue3] = React.useState("");
128
+ const [value4, setValue4] = React.useState("");
129
+ const [value5, setValue5] = React.useState("");
130
+ const [focusedInput, setFocusedInput] = React.useState<
131
+ "input1" | "input2" | "input3" | "input4" | "input5"
132
+ >("input1");
133
+
134
+ const input1Ref = React.useRef<HTMLInputElement>(null);
135
+ const input2Ref = React.useRef<HTMLInputElement>(null);
136
+ const input3Ref = React.useRef<HTMLInputElement>(null);
137
+ const input4Ref = React.useRef<HTMLInputElement>(null);
138
+ const input5Ref = React.useRef<HTMLInputElement>(null);
139
+ const containerRef = React.useRef<HTMLDivElement>(null);
140
+
141
+ const values = {
142
+ input1: value1,
143
+ input2: value2,
144
+ input3: value3,
145
+ input4: value4,
146
+ input5: value5,
147
+ };
148
+ const setters = {
149
+ input1: setValue1,
150
+ input2: setValue2,
151
+ input3: setValue3,
152
+ input4: setValue4,
153
+ input5: setValue5,
154
+ };
155
+ const refs = {
156
+ input1: input1Ref,
157
+ input2: input2Ref,
158
+ input3: input3Ref,
159
+ input4: input4Ref,
160
+ input5: input5Ref,
161
+ };
162
+
163
+ const currentValue = values[focusedInput];
164
+ const setCurrentValue = setters[focusedInput];
165
+
166
+ const handleInputFocus = (
167
+ input: "input1" | "input2" | "input3" | "input4" | "input5"
168
+ ) => {
169
+ setFocusedInput(input);
170
+
171
+ // Scroll the focused input into view
172
+ setTimeout(() => {
173
+ const inputElement = refs[input].current;
174
+ if (inputElement && containerRef.current) {
175
+ (inputElement as any).scrollIntoView?.({
176
+ behavior: "smooth",
177
+ block: "center",
178
+ inline: "nearest",
179
+ });
180
+ }
181
+ }, 100);
182
+ };
183
+
184
+ const inputs = [
185
+ {
186
+ id: "input1" as const,
187
+ label: "Text Input 1",
188
+ value: value1,
189
+ setValue: setValue1,
190
+ ref: input1Ref,
191
+ },
192
+ {
193
+ id: "input2" as const,
194
+ label: "Text Input 2",
195
+ value: value2,
196
+ setValue: setValue2,
197
+ ref: input2Ref,
198
+ },
199
+ {
200
+ id: "input3" as const,
201
+ label: "Text Input 3",
202
+ value: value3,
203
+ setValue: setValue3,
204
+ ref: input3Ref,
205
+ },
206
+ {
207
+ id: "input4" as const,
208
+ label: "Text Input 4",
209
+ value: value4,
210
+ setValue: setValue4,
211
+ ref: input4Ref,
212
+ },
213
+ {
214
+ id: "input5" as const,
215
+ label: "Text Input 5",
216
+ value: value5,
217
+ setValue: setValue5,
218
+ ref: input5Ref,
219
+ },
220
+ ];
221
+
222
+ return (
223
+ <div
224
+ ref={containerRef}
225
+ className="h-screen p-6 pt-40 pb-96 space-y-4 overflow-y-auto relative flex-1"
226
+ >
227
+ <div className="flex-1 w-full space-y-4 flex flex-col">
228
+ {inputs.map((input) => (
229
+ <div key={input.id}>
230
+ <div className="text-sm text-muted-foreground mb-2">
231
+ {input.label}
232
+ </div>
233
+ <Input
234
+ ref={input.ref}
235
+ type="text"
236
+ value={input.value}
237
+ onChange={(e) => input.setValue((e.target as any).value)}
238
+ placeholder="Type here..."
239
+ className="min-h-[60px]"
240
+ onFocus={() => handleInputFocus(input.id)}
241
+ />
242
+ {focusedInput === input.id && (
243
+ <div className="text-xs text-teal-600 mt-1">✓ Connected</div>
244
+ )}
245
+ </div>
246
+ ))}
247
+ </div>
248
+ <Keyboard
249
+ {...args}
250
+ value={currentValue}
251
+ onChange={setCurrentValue}
252
+ inputRef={refs[focusedInput]}
253
+ className="fixed bottom-0 left-0 right-0 z-10"
254
+ showNumbers
255
+ showShift
256
+ showSpace
257
+ showBackspace
258
+ showEnter
259
+ />
260
+ </div>
261
+ );
262
+ },
263
+ };