@kud/ink-ui 0.6.0 → 0.8.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 CHANGED
@@ -13,11 +13,18 @@
13
13
 
14
14
  ## Features
15
15
 
16
- - **Ten ready-made components** — Banner, Header, Badge, Spinner, Table, Tabs, FooterHints, LoadingScreen, KeyValue, and SelectableRow, pre-styled and ready to drop in
16
+ - **25 ready-made components**, pre-styled and ready to drop in:
17
+ - **Inputs** — `TextInput`, `EmailInput` (domain completion), `PasswordInput` (masked), `ConfirmInput`
18
+ - **Lists** — `UnorderedList`, `OrderedList` (both nestable), `Table`
19
+ - **Selection & navigation** — `Select`, `MultiSelect`, `Tabs`, `Switch`, `Toggle`, `SelectableRow`
20
+ - **Status & feedback** — `Spinner`, `ProgressBar`, `StatusMessage`, `Alert`, `Badge`, `Toast`
21
+ - **Layout & chrome** — `Banner`, `Header`, `FooterHints`, `KeyValue`, `LoadingScreen`, `ScrollView`
22
+ - **Full [@inkjs/ui](https://github.com/vadimdemedes/ink-ui) parity** — every upstream component has an equivalent, plus a dozen more the design system adds on top
23
+ - **Colourblind-safe by design** — state is signalled by shape, case, and glyph, never colour alone
17
24
  - **Design tokens included** — a shared colour palette (`colors`) and spacing scale (`spacing`) to keep every screen consistent
18
25
  - **Full TypeScript support** — ships compiled output with `.d.ts` declarations for every component and token type
19
26
  - **ESM only, zero config** — no runtime bundling step; just import and render
20
- - **Peer-dependency light** — only requires `ink ≥ 4` and `react ≥ 18`; `ink-spinner` is the sole production dependency
27
+ - **Peer-dependency light** — only requires `ink ≥ 7` and `react ≥ 19`
21
28
 
22
29
  ## Install
23
30
 
@@ -50,6 +57,33 @@ const App = () => (
50
57
  render(<App />)
51
58
  ```
52
59
 
60
+ Inputs report their value through `onChange`/`onSubmit`, and lists nest with a shape-distinct marker per level:
61
+
62
+ ```tsx
63
+ import { Text } from "ink"
64
+ import { EmailInput, PasswordInput, UnorderedList } from "@kud/ink-ui"
65
+
66
+ const SignUp = () => (
67
+ <>
68
+ <EmailInput placeholder="you@example.com" onSubmit={setEmail} />
69
+ <PasswordInput placeholder="Password" onSubmit={setPassword} />
70
+ <UnorderedList>
71
+ <UnorderedList.Item>
72
+ <Text>Choose a plan</Text>
73
+ <UnorderedList>
74
+ <UnorderedList.Item>
75
+ <Text>Free</Text>
76
+ </UnorderedList.Item>
77
+ <UnorderedList.Item>
78
+ <Text>Pro</Text>
79
+ </UnorderedList.Item>
80
+ </UnorderedList>
81
+ </UnorderedList.Item>
82
+ </UnorderedList>
83
+ </>
84
+ )
85
+ ```
86
+
53
87
  All components accept only the props they need — no theme provider or context required. Design tokens are plain objects:
54
88
 
55
89
  ```ts
package/dist/index.d.ts CHANGED
@@ -94,6 +94,25 @@ type TextInputProps = {
94
94
  };
95
95
  declare const TextInput: ({ placeholder, defaultValue, onChange, onSubmit, isDisabled, }: TextInputProps) => React.JSX.Element;
96
96
 
97
+ type EmailInputProps = {
98
+ placeholder?: string;
99
+ defaultValue?: string;
100
+ domains?: string[];
101
+ onChange?: (value: string) => void;
102
+ onSubmit?: (value: string) => void;
103
+ isDisabled?: boolean;
104
+ };
105
+ declare const EmailInput: ({ placeholder, defaultValue, domains, onChange, onSubmit, isDisabled, }: EmailInputProps) => React.JSX.Element;
106
+
107
+ type PasswordInputProps = {
108
+ placeholder?: string;
109
+ mask?: string;
110
+ onChange?: (value: string) => void;
111
+ onSubmit?: (value: string) => void;
112
+ isDisabled?: boolean;
113
+ };
114
+ declare const PasswordInput: ({ placeholder, mask, onChange, onSubmit, isDisabled, }: PasswordInputProps) => React.JSX.Element;
115
+
97
116
  type ConfirmInputProps = {
98
117
  defaultChoice?: "confirm" | "cancel";
99
118
  onConfirm?: () => void;
@@ -102,6 +121,27 @@ type ConfirmInputProps = {
102
121
  };
103
122
  declare const ConfirmInput: ({ defaultChoice, onConfirm, onCancel, isDisabled, }: ConfirmInputProps) => React.JSX.Element;
104
123
 
124
+ type ItemProps$1 = {
125
+ children: React.ReactNode;
126
+ };
127
+ type UnorderedListProps = {
128
+ children: React.ReactNode;
129
+ };
130
+ declare const UnorderedList: (({ children }: UnorderedListProps) => React.JSX.Element) & {
131
+ Item: ({ children }: ItemProps$1) => React.JSX.Element;
132
+ };
133
+
134
+ type ItemProps = {
135
+ children: React.ReactNode;
136
+ _index?: number;
137
+ };
138
+ type OrderedListProps = {
139
+ children: React.ReactNode;
140
+ };
141
+ declare const OrderedList: (({ children }: OrderedListProps) => React.JSX.Element) & {
142
+ Item: ({ children, _index }: ItemProps) => React.JSX.Element;
143
+ };
144
+
105
145
  type Span = {
106
146
  text: string;
107
147
  color?: string;
@@ -120,8 +160,9 @@ type ScrollViewProps = {
120
160
  lines: StyledLine[];
121
161
  showIndicator?: boolean;
122
162
  initialStart?: number;
163
+ isActive?: boolean;
123
164
  };
124
- declare const ScrollView: ({ lines, showIndicator, initialStart, }: ScrollViewProps) => React.JSX.Element;
165
+ declare const ScrollView: ({ lines, showIndicator, initialStart, isActive, }: ScrollViewProps) => React.JSX.Element;
125
166
 
126
167
  type TabItem<T extends string = string> = {
127
168
  value: T;
@@ -207,4 +248,4 @@ declare const spacing: {
207
248
  };
208
249
  type Spacing = (typeof spacing)[keyof typeof spacing];
209
250
 
210
- export { Alert, Badge, type BadgeVariant, Banner, type Color, type Column, ConfirmInput, FooterHints, Header, type Hint, type IconMode, KeyValue, LoadingScreen, MultiSelect, type NotifyOptions, ProgressBar, ScrollView, Select, type SelectOption, SelectableRow, type Spacing, type Span, Spinner, StatusMessage, type StatusVariant, type StyledLine, Switch, type SwitchValue, type TabItem, Table, Tabs, TextInput, Toast, Toggle, colors, getIconMode, notify, setIconMode, spacing };
251
+ export { Alert, Badge, type BadgeVariant, Banner, type Color, type Column, ConfirmInput, EmailInput, FooterHints, Header, type Hint, type IconMode, KeyValue, LoadingScreen, MultiSelect, type NotifyOptions, OrderedList, PasswordInput, ProgressBar, ScrollView, Select, type SelectOption, SelectableRow, type Spacing, type Span, Spinner, StatusMessage, type StatusVariant, type StyledLine, Switch, type SwitchValue, type TabItem, Table, Tabs, TextInput, Toast, Toggle, UnorderedList, colors, getIconMode, notify, setIconMode, spacing };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Box, Text, useInput, measureElement } from 'ink';
2
- import { jsxs, jsx } from 'react/jsx-runtime';
3
- import { useState, useEffect, useRef, useLayoutEffect } from 'react';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+ import React8, { createContext, useContext, useState, useEffect, useRef, useLayoutEffect } from 'react';
4
4
  import cliSpinners from 'cli-spinners';
5
5
  import { glyphs } from '@kud/glyphs';
6
6
  import { spawn } from 'child_process';
@@ -115,7 +115,10 @@ var Select = ({
115
115
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
116
116
  visible.map((option, i) => {
117
117
  const active = start + i === cursor;
118
- return /* @__PURE__ */ jsx(SelectableRow, { active, children: /* @__PURE__ */ jsx(Text, { bold: active, color: active ? colors.accent : void 0, children: option.label }) }, option.value);
118
+ return /* @__PURE__ */ jsxs(SelectableRow, { active, children: [
119
+ /* @__PURE__ */ jsx(Text, { bold: active, color: active ? colors.info : void 0, children: option.label }),
120
+ active ? /* @__PURE__ */ jsx(Text, { color: colors.success, children: " \u2713" }) : null
121
+ ] }, option.value);
119
122
  }),
120
123
  options.length > count && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u2026 ${options.length} total` })
121
124
  ] });
@@ -159,8 +162,8 @@ var MultiSelect = ({
159
162
  const active = start + i === cursor;
160
163
  const on = selected.has(option.value);
161
164
  return /* @__PURE__ */ jsxs(SelectableRow, { active, children: [
162
- /* @__PURE__ */ jsx(Text, { color: on ? colors.success : void 0, children: on ? "\u25C9" : "\u25CB" }),
163
- /* @__PURE__ */ jsx(Text, { bold: active, color: active ? colors.accent : void 0, children: " " + option.label })
165
+ /* @__PURE__ */ jsx(Text, { bold: active, color: active ? colors.info : void 0, children: option.label }),
166
+ on ? /* @__PURE__ */ jsx(Text, { color: colors.success, children: " \u2713" }) : null
164
167
  ] }, option.value);
165
168
  }),
166
169
  options.length > count && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u2026 ${options.length} total` })
@@ -223,6 +226,148 @@ var TextInput = ({
223
226
  after
224
227
  ] });
225
228
  };
229
+ var defaultDomains = [
230
+ "gmail.com",
231
+ "outlook.com",
232
+ "hotmail.com",
233
+ "yahoo.com",
234
+ "icloud.com",
235
+ "proton.me"
236
+ ];
237
+ var EmailInput = ({
238
+ placeholder = "",
239
+ defaultValue = "",
240
+ domains = defaultDomains,
241
+ onChange,
242
+ onSubmit,
243
+ isDisabled = false
244
+ }) => {
245
+ const [value, setValue] = useState(defaultValue);
246
+ const [cursor, setCursor] = useState(defaultValue.length);
247
+ const at = value.indexOf("@");
248
+ const domainFragment = at === -1 ? "" : value.slice(at + 1);
249
+ const match = at !== -1 && domainFragment.length > 0 ? domains.find(
250
+ (d) => d.startsWith(domainFragment) && d !== domainFragment
251
+ ) : void 0;
252
+ const suggestion = match ? match.slice(domainFragment.length) : "";
253
+ const showSuggestion = suggestion.length > 0 && cursor === value.length;
254
+ const update = (nextValue, nextCursor) => {
255
+ setValue(nextValue);
256
+ setCursor(nextCursor);
257
+ onChange?.(nextValue);
258
+ };
259
+ useInput(
260
+ (input, key) => {
261
+ if (key.return) {
262
+ onSubmit?.(value);
263
+ return;
264
+ }
265
+ if (key.tab && showSuggestion) {
266
+ update(value + suggestion, value.length + suggestion.length);
267
+ return;
268
+ }
269
+ if (key.leftArrow) {
270
+ setCursor((c) => Math.max(0, c - 1));
271
+ return;
272
+ }
273
+ if (key.rightArrow) {
274
+ setCursor((c) => Math.min(value.length, c + 1));
275
+ return;
276
+ }
277
+ if (key.backspace || key.delete) {
278
+ if (cursor === 0) return;
279
+ update(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
280
+ return;
281
+ }
282
+ if (input && !key.ctrl && !key.meta && !key.tab) {
283
+ update(
284
+ value.slice(0, cursor) + input + value.slice(cursor),
285
+ cursor + input.length
286
+ );
287
+ }
288
+ },
289
+ { isActive: !isDisabled }
290
+ );
291
+ if (value.length === 0) {
292
+ return /* @__PURE__ */ jsxs(Text, { children: [
293
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: " " }),
294
+ placeholder ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: placeholder }) : null
295
+ ] });
296
+ }
297
+ if (showSuggestion) {
298
+ return /* @__PURE__ */ jsxs(Text, { children: [
299
+ value,
300
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: suggestion.slice(0, 1) }),
301
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: suggestion.slice(1) })
302
+ ] });
303
+ }
304
+ const before = value.slice(0, cursor);
305
+ const atChar = value.slice(cursor, cursor + 1) || " ";
306
+ const after = value.slice(cursor + 1);
307
+ return /* @__PURE__ */ jsxs(Text, { children: [
308
+ before,
309
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: atChar }),
310
+ after
311
+ ] });
312
+ };
313
+ var PasswordInput = ({
314
+ placeholder = "",
315
+ mask = "*",
316
+ onChange,
317
+ onSubmit,
318
+ isDisabled = false
319
+ }) => {
320
+ const [value, setValue] = useState("");
321
+ const [cursor, setCursor] = useState(0);
322
+ const update = (nextValue, nextCursor) => {
323
+ setValue(nextValue);
324
+ setCursor(nextCursor);
325
+ onChange?.(nextValue);
326
+ };
327
+ useInput(
328
+ (input, key) => {
329
+ if (key.return) {
330
+ onSubmit?.(value);
331
+ return;
332
+ }
333
+ if (key.leftArrow) {
334
+ setCursor((c) => Math.max(0, c - 1));
335
+ return;
336
+ }
337
+ if (key.rightArrow) {
338
+ setCursor((c) => Math.min(value.length, c + 1));
339
+ return;
340
+ }
341
+ if (key.backspace || key.delete) {
342
+ if (cursor === 0) return;
343
+ update(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
344
+ return;
345
+ }
346
+ if (input && !key.ctrl && !key.meta && !key.tab) {
347
+ update(
348
+ value.slice(0, cursor) + input + value.slice(cursor),
349
+ cursor + input.length
350
+ );
351
+ }
352
+ },
353
+ { isActive: !isDisabled }
354
+ );
355
+ if (value.length === 0) {
356
+ return /* @__PURE__ */ jsxs(Text, { children: [
357
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: " " }),
358
+ placeholder ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: placeholder }) : null
359
+ ] });
360
+ }
361
+ const masked = mask.repeat(value.length);
362
+ const before = masked.slice(0, cursor);
363
+ const at = masked.slice(cursor, cursor + 1) || " ";
364
+ const after = masked.slice(cursor + 1);
365
+ return /* @__PURE__ */ jsxs(Text, { children: [
366
+ before,
367
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: at }),
368
+ after
369
+ ] });
370
+ };
226
371
  var ConfirmInput = ({
227
372
  defaultChoice = "confirm",
228
373
  onConfirm,
@@ -249,10 +394,54 @@ var ConfirmInput = ({
249
394
  const hint = defaultChoice === "confirm" ? "(Y/n)" : "(y/N)";
250
395
  return /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint });
251
396
  };
397
+ var markers = ["\u25CF", "\u25CB", "\u25AA", "\u25AB"];
398
+ var DepthContext = createContext(0);
399
+ var UnorderedListItem = ({ children }) => {
400
+ const depth = useContext(DepthContext);
401
+ const marker = markers[Math.max(0, depth - 1) % markers.length];
402
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
403
+ /* @__PURE__ */ jsxs(Text, { children: [
404
+ marker,
405
+ " "
406
+ ] }),
407
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", children })
408
+ ] });
409
+ };
410
+ var UnorderedListRoot = ({ children }) => {
411
+ const depth = useContext(DepthContext);
412
+ return /* @__PURE__ */ jsx(DepthContext.Provider, { value: depth + 1, children: /* @__PURE__ */ jsx(Box, { flexDirection: "column", children }) });
413
+ };
414
+ var UnorderedList = Object.assign(UnorderedListRoot, {
415
+ Item: UnorderedListItem
416
+ });
417
+ var OrderedListItem = ({ children, _index = 1 }) => /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
418
+ /* @__PURE__ */ jsxs(Text, { children: [
419
+ _index,
420
+ ". "
421
+ ] }),
422
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", children })
423
+ ] });
424
+ var OrderedListRoot = ({ children }) => {
425
+ let n = 0;
426
+ const numbered = React8.Children.map(children, (child) => {
427
+ if (React8.isValidElement(child) && child.type === OrderedListItem) {
428
+ n += 1;
429
+ return React8.cloneElement(child, {
430
+ _index: n
431
+ });
432
+ }
433
+ return child;
434
+ });
435
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: numbered });
436
+ };
437
+ var OrderedList = Object.assign(OrderedListRoot, {
438
+ Item: OrderedListItem
439
+ });
252
440
  var ScrollView = ({
253
441
  lines,
254
442
  showIndicator = true,
255
- initialStart = 0
443
+ initialStart = 0,
444
+ isActive = true
256
445
  }) => {
257
446
  const ref = useRef(null);
258
447
  const [height, setHeight] = useState(1);
@@ -265,16 +454,19 @@ var ScrollView = ({
265
454
  });
266
455
  const maxStart = Math.max(0, lines.length - height);
267
456
  const clamped = Math.min(start, maxStart);
268
- useInput((input, key) => {
269
- if (key.downArrow || input === "j")
270
- setStart((s) => Math.min(maxStart, s + 1));
271
- if (key.upArrow || input === "k") setStart((s) => Math.max(0, s - 1));
272
- if (input === " " || key.pageDown || input === "f")
273
- setStart((s) => Math.min(maxStart, s + height));
274
- if (key.pageUp || input === "b") setStart((s) => Math.max(0, s - height));
275
- if (input === "g") setStart(0);
276
- if (input === "G") setStart(maxStart);
277
- });
457
+ useInput(
458
+ (input, key) => {
459
+ if (key.downArrow || input === "j")
460
+ setStart((s) => Math.min(maxStart, s + 1));
461
+ if (key.upArrow || input === "k") setStart((s) => Math.max(0, s - 1));
462
+ if (input === " " || key.pageDown || input === "f")
463
+ setStart((s) => Math.min(maxStart, s + height));
464
+ if (key.pageUp || input === "b") setStart((s) => Math.max(0, s - height));
465
+ if (input === "g") setStart(0);
466
+ if (input === "G") setStart(maxStart);
467
+ },
468
+ { isActive }
469
+ );
278
470
  const visible = lines.slice(clamped, clamped + height);
279
471
  const hasMore = lines.length > height;
280
472
  const atEnd = clamped >= maxStart;
@@ -475,4 +667,4 @@ var notify = (message, options = {}) => {
475
667
  child.unref();
476
668
  };
477
669
 
478
- export { Alert, Badge, Banner, ConfirmInput, FooterHints, Header, KeyValue, LoadingScreen, MultiSelect, ProgressBar, ScrollView, Select, SelectableRow, Spinner, StatusMessage, Switch, Table, Tabs, TextInput, Toast, Toggle, colors, getIconMode, notify, setIconMode, spacing };
670
+ export { Alert, Badge, Banner, ConfirmInput, EmailInput, FooterHints, Header, KeyValue, LoadingScreen, MultiSelect, OrderedList, PasswordInput, ProgressBar, ScrollView, Select, SelectableRow, Spinner, StatusMessage, Switch, Table, Tabs, TextInput, Toast, Toggle, UnorderedList, colors, getIconMode, notify, setIconMode, spacing };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/ink-ui",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "React component library for Ink CLIs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -20,6 +20,7 @@
20
20
  "scripts": {
21
21
  "build": "tsup",
22
22
  "dev": "tsup --watch",
23
+ "demo": "tsx src/demo/index.tsx",
23
24
  "typecheck": "tsc --noEmit",
24
25
  "test": "vitest run",
25
26
  "test:watch": "vitest"
@@ -41,6 +42,7 @@
41
42
  "react": "19.2.7",
42
43
  "react-devtools-core": "7.0.1",
43
44
  "tsup": "8.5.1",
45
+ "tsx": "4.23.1",
44
46
  "typescript": "5.9.3",
45
47
  "vitest": "4.1.10"
46
48
  },