@hauktui/registry 0.0.1

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.
Files changed (139) hide show
  1. package/components/accordion/accordion.tsx +146 -0
  2. package/components/accordion/index.ts +2 -0
  3. package/components/alert/alert.tsx +69 -0
  4. package/components/alert/index.ts +2 -0
  5. package/components/alert-dialog/alert-dialog.tsx +185 -0
  6. package/components/alert-dialog/index.ts +2 -0
  7. package/components/avatar/avatar.tsx +57 -0
  8. package/components/avatar/index.ts +2 -0
  9. package/components/avatar-group/avatar-group.tsx +144 -0
  10. package/components/avatar-group/index.ts +2 -0
  11. package/components/badge/badge.tsx +52 -0
  12. package/components/badge/index.ts +2 -0
  13. package/components/banner/banner.tsx +407 -0
  14. package/components/banner/index.ts +2 -0
  15. package/components/breadcrumb/breadcrumb.tsx +58 -0
  16. package/components/breadcrumb/index.ts +2 -0
  17. package/components/button/button.tsx +114 -0
  18. package/components/button/index.ts +2 -0
  19. package/components/calendar/calendar.tsx +250 -0
  20. package/components/calendar/index.ts +2 -0
  21. package/components/card/card.tsx +88 -0
  22. package/components/card/index.ts +2 -0
  23. package/components/carousel/carousel.tsx +185 -0
  24. package/components/carousel/index.ts +2 -0
  25. package/components/chart/chart.tsx +189 -0
  26. package/components/chart/index.ts +2 -0
  27. package/components/checkbox/checkbox.tsx +98 -0
  28. package/components/checkbox/index.ts +2 -0
  29. package/components/code-block/code-block.tsx +214 -0
  30. package/components/code-block/index.ts +2 -0
  31. package/components/collapsible/collapsible.tsx +123 -0
  32. package/components/collapsible/index.ts +2 -0
  33. package/components/color-picker/color-picker.tsx +211 -0
  34. package/components/color-picker/index.ts +2 -0
  35. package/components/combobox/combobox.tsx +275 -0
  36. package/components/combobox/index.ts +2 -0
  37. package/components/command/command.tsx +304 -0
  38. package/components/command/index.ts +2 -0
  39. package/components/confirm-dialog/confirm-dialog.tsx +140 -0
  40. package/components/confirm-dialog/index.ts +2 -0
  41. package/components/context-menu/context-menu.tsx +188 -0
  42. package/components/context-menu/index.ts +2 -0
  43. package/components/countdown/countdown.tsx +165 -0
  44. package/components/countdown/index.ts +2 -0
  45. package/components/data-table/data-table.tsx +256 -0
  46. package/components/data-table/index.ts +2 -0
  47. package/components/date-picker/date-picker.tsx +280 -0
  48. package/components/date-picker/index.ts +2 -0
  49. package/components/dialog/dialog.tsx +84 -0
  50. package/components/dialog/index.ts +2 -0
  51. package/components/drawer/drawer.tsx +141 -0
  52. package/components/drawer/index.ts +2 -0
  53. package/components/dropdown-menu/dropdown-menu.tsx +188 -0
  54. package/components/dropdown-menu/index.ts +2 -0
  55. package/components/empty/empty.tsx +107 -0
  56. package/components/empty/index.ts +2 -0
  57. package/components/field/field.tsx +83 -0
  58. package/components/field/index.ts +2 -0
  59. package/components/form/form.tsx +202 -0
  60. package/components/form/index.ts +8 -0
  61. package/components/hover-card/hover-card.tsx +72 -0
  62. package/components/hover-card/index.ts +2 -0
  63. package/components/input-otp/index.ts +2 -0
  64. package/components/input-otp/input-otp.tsx +176 -0
  65. package/components/kbd/index.ts +2 -0
  66. package/components/kbd/kbd.tsx +30 -0
  67. package/components/label/index.ts +2 -0
  68. package/components/label/label.tsx +56 -0
  69. package/components/list/index.ts +2 -0
  70. package/components/list/list.tsx +247 -0
  71. package/components/menubar/index.ts +2 -0
  72. package/components/menubar/menubar.tsx +220 -0
  73. package/components/navigation-menu/index.ts +6 -0
  74. package/components/navigation-menu/navigation-menu.tsx +216 -0
  75. package/components/pagination/index.ts +2 -0
  76. package/components/pagination/pagination.tsx +158 -0
  77. package/components/password-input/index.ts +2 -0
  78. package/components/password-input/password-input.tsx +198 -0
  79. package/components/popover/index.ts +2 -0
  80. package/components/popover/popover.tsx +102 -0
  81. package/components/progress/index.ts +2 -0
  82. package/components/progress/progress.tsx +73 -0
  83. package/components/radio-group/index.ts +2 -0
  84. package/components/radio-group/radio-group.tsx +167 -0
  85. package/components/resizable/index.ts +2 -0
  86. package/components/resizable/resizable.tsx +141 -0
  87. package/components/scroll-area/index.ts +2 -0
  88. package/components/scroll-area/scroll-area.tsx +133 -0
  89. package/components/select/index.ts +2 -0
  90. package/components/select/select.tsx +185 -0
  91. package/components/separator/index.ts +2 -0
  92. package/components/separator/separator.tsx +63 -0
  93. package/components/sheet/index.ts +2 -0
  94. package/components/sheet/sheet.tsx +137 -0
  95. package/components/sidebar/index.ts +2 -0
  96. package/components/sidebar/sidebar.tsx +225 -0
  97. package/components/skeleton/index.ts +2 -0
  98. package/components/skeleton/skeleton.tsx +64 -0
  99. package/components/slider/index.ts +2 -0
  100. package/components/slider/slider.tsx +128 -0
  101. package/components/spinner/index.ts +2 -0
  102. package/components/spinner/spinner.tsx +57 -0
  103. package/components/stat/index.ts +2 -0
  104. package/components/stat/stat.tsx +138 -0
  105. package/components/stepper/index.ts +2 -0
  106. package/components/stepper/stepper.tsx +219 -0
  107. package/components/switch/index.ts +2 -0
  108. package/components/switch/switch.tsx +102 -0
  109. package/components/table/index.ts +2 -0
  110. package/components/table/table.tsx +242 -0
  111. package/components/tabs/index.ts +2 -0
  112. package/components/tabs/tabs.tsx +240 -0
  113. package/components/tag-input/index.ts +2 -0
  114. package/components/tag-input/tag-input.tsx +180 -0
  115. package/components/terminal/index.ts +2 -0
  116. package/components/terminal/terminal.tsx +162 -0
  117. package/components/text-input/index.ts +2 -0
  118. package/components/text-input/text-input.tsx +179 -0
  119. package/components/textarea/index.ts +2 -0
  120. package/components/textarea/textarea.tsx +206 -0
  121. package/components/timeline/index.ts +2 -0
  122. package/components/timeline/timeline.tsx +167 -0
  123. package/components/toast/index.ts +2 -0
  124. package/components/toast/toast.tsx +93 -0
  125. package/components/toggle/index.ts +2 -0
  126. package/components/toggle/toggle.tsx +114 -0
  127. package/components/toggle-group/index.ts +2 -0
  128. package/components/toggle-group/toggle-group.tsx +176 -0
  129. package/components/tooltip/index.ts +2 -0
  130. package/components/tooltip/tooltip.tsx +65 -0
  131. package/components/tree-view/index.ts +2 -0
  132. package/components/tree-view/tree-view.tsx +245 -0
  133. package/components/typography/index.ts +12 -0
  134. package/components/typography/typography.tsx +154 -0
  135. package/dist/index.d.ts +102 -0
  136. package/dist/index.js +938 -0
  137. package/dist/index.js.map +1 -0
  138. package/package.json +41 -0
  139. package/registry.json +923 -0
@@ -0,0 +1,146 @@
1
+ import React, { useState, useCallback } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import type { Tokens } from "@hauktui/tokens";
4
+ import { useTokens, useFocusable } from "@hauktui/primitives-ink";
5
+ import { stableId } from "@hauktui/core";
6
+
7
+ export interface AccordionItem {
8
+ /** Unique key for the item */
9
+ key: string;
10
+ /** Item title */
11
+ title: string;
12
+ /** Item content */
13
+ content: string;
14
+ /** Whether this item is disabled */
15
+ disabled?: boolean;
16
+ }
17
+
18
+ export interface AccordionProps {
19
+ /** Accordion items */
20
+ items: AccordionItem[];
21
+ /** Allow multiple items to be expanded */
22
+ multiple?: boolean;
23
+ /** Controlled expanded keys */
24
+ expandedKeys?: string[];
25
+ /** Default expanded keys for uncontrolled mode */
26
+ defaultExpandedKeys?: string[];
27
+ /** Callback when expansion changes */
28
+ onChange?: (keys: string[]) => void;
29
+ /** Whether the accordion is disabled */
30
+ disabled?: boolean;
31
+ /** Custom tokens override */
32
+ tokens?: Tokens;
33
+ /** Focus ID for focus management */
34
+ focusId?: string;
35
+ }
36
+
37
+ export function Accordion({
38
+ items,
39
+ multiple = false,
40
+ expandedKeys: controlledExpandedKeys,
41
+ defaultExpandedKeys = [],
42
+ onChange,
43
+ disabled = false,
44
+ tokens: propTokens,
45
+ focusId,
46
+ }: AccordionProps): React.ReactElement {
47
+ const contextTokens = useTokens();
48
+ const tokens = propTokens ?? contextTokens;
49
+ const id = focusId ?? stableId("accordion");
50
+ const { isFocused } = useFocusable(id);
51
+
52
+ const [internalExpandedKeys, setInternalExpandedKeys] =
53
+ useState<string[]>(defaultExpandedKeys);
54
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
55
+
56
+ const isControlled = controlledExpandedKeys !== undefined;
57
+ const expandedKeys = isControlled
58
+ ? controlledExpandedKeys
59
+ : internalExpandedKeys;
60
+
61
+ const toggleItem = useCallback(
62
+ (key: string) => {
63
+ const item = items.find((i) => i.key === key);
64
+ if (!item || item.disabled) return;
65
+
66
+ let newKeys: string[];
67
+ if (expandedKeys.includes(key)) {
68
+ newKeys = expandedKeys.filter((k) => k !== key);
69
+ } else {
70
+ newKeys = multiple ? [...expandedKeys, key] : [key];
71
+ }
72
+
73
+ if (!isControlled) {
74
+ setInternalExpandedKeys(newKeys);
75
+ }
76
+ onChange?.(newKeys);
77
+ },
78
+ [items, expandedKeys, multiple, isControlled, onChange]
79
+ );
80
+
81
+ useInput(
82
+ (input, key) => {
83
+ if (!isFocused || disabled) return;
84
+
85
+ if (key.upArrow || input === "k") {
86
+ setHighlightedIndex((prev) => Math.max(0, prev - 1));
87
+ } else if (key.downArrow || input === "j") {
88
+ setHighlightedIndex((prev) => Math.min(items.length - 1, prev + 1));
89
+ } else if (key.return || input === " ") {
90
+ const item = items[highlightedIndex];
91
+ if (item) {
92
+ toggleItem(item.key);
93
+ }
94
+ }
95
+ },
96
+ { isActive: isFocused }
97
+ );
98
+
99
+ return React.createElement(
100
+ Box,
101
+ {
102
+ flexDirection: "column",
103
+ borderStyle: isFocused ? "round" : "single",
104
+ borderColor: isFocused ? tokens.colors.focus : tokens.colors.border,
105
+ },
106
+ items.map((item, index) => {
107
+ const isExpanded = expandedKeys.includes(item.key);
108
+ const isHighlighted = index === highlightedIndex && isFocused;
109
+ const isDisabled = item.disabled || disabled;
110
+
111
+ const icon = isExpanded ? "▼" : "▶";
112
+
113
+ return React.createElement(
114
+ Box,
115
+ { key: item.key, flexDirection: "column", paddingX: 1 },
116
+ React.createElement(
117
+ Text,
118
+ {
119
+ color: isDisabled
120
+ ? tokens.colors.disabled
121
+ : isHighlighted
122
+ ? tokens.colors.accent
123
+ : tokens.colors.fg,
124
+ bold: isHighlighted,
125
+ inverse: isHighlighted,
126
+ dimColor: isDisabled,
127
+ },
128
+ icon,
129
+ " ",
130
+ item.title
131
+ ),
132
+ isExpanded
133
+ ? React.createElement(
134
+ Box,
135
+ { paddingLeft: 2, paddingY: 0 },
136
+ React.createElement(
137
+ Text,
138
+ { color: tokens.colors.muted },
139
+ item.content
140
+ )
141
+ )
142
+ : null
143
+ );
144
+ })
145
+ );
146
+ }
@@ -0,0 +1,2 @@
1
+ export { Accordion } from "./accordion.js";
2
+ export type { AccordionProps, AccordionItem } from "./accordion.js";
@@ -0,0 +1,69 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import type { Tokens } from "@hauktui/tokens";
4
+ import { useTokens } from "@hauktui/primitives-ink";
5
+
6
+ export type AlertVariant = "info" | "success" | "warning" | "error";
7
+
8
+ export interface AlertProps {
9
+ /** Alert message content */
10
+ children: React.ReactNode;
11
+ /** Alert title */
12
+ title?: string;
13
+ /** Alert variant */
14
+ variant?: AlertVariant;
15
+ /** Custom tokens override */
16
+ tokens?: Tokens;
17
+ /** Whether to show an icon */
18
+ showIcon?: boolean;
19
+ }
20
+
21
+ const ICONS: Record<AlertVariant, string> = {
22
+ info: "ℹ",
23
+ success: "✓",
24
+ warning: "⚠",
25
+ error: "✗",
26
+ };
27
+
28
+ export function Alert({
29
+ children,
30
+ title,
31
+ variant = "info",
32
+ tokens: propTokens,
33
+ showIcon = true,
34
+ }: AlertProps): React.ReactElement {
35
+ const contextTokens = useTokens();
36
+ const tokens = propTokens ?? contextTokens;
37
+
38
+ const variantColors: Record<AlertVariant, string> = {
39
+ info: tokens.colors.accent,
40
+ success: tokens.colors.success,
41
+ warning: tokens.colors.warning,
42
+ error: tokens.colors.danger,
43
+ };
44
+
45
+ const color = variantColors[variant];
46
+ const icon = ICONS[variant];
47
+
48
+ return React.createElement(
49
+ Box,
50
+ {
51
+ flexDirection: "column",
52
+ borderStyle: "round",
53
+ borderColor: color,
54
+ paddingX: 1,
55
+ paddingY: 0,
56
+ },
57
+ React.createElement(
58
+ Box,
59
+ { gap: 1 },
60
+ showIcon ? React.createElement(Text, { color }, icon) : null,
61
+ title ? React.createElement(Text, { color, bold: true }, title) : null
62
+ ),
63
+ React.createElement(
64
+ Box,
65
+ { paddingLeft: showIcon ? 2 : 0 },
66
+ React.createElement(Text, { color: tokens.colors.fg }, children)
67
+ )
68
+ );
69
+ }
@@ -0,0 +1,2 @@
1
+ export { Alert } from "./alert.js";
2
+ export type { AlertProps, AlertVariant } from "./alert.js";
@@ -0,0 +1,185 @@
1
+ import React from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import type { Tokens } from "@hauktui/tokens";
4
+ import { useTokens, useFocusable } from "@hauktui/primitives-ink";
5
+ import { stableId } from "@hauktui/core";
6
+
7
+ export type AlertDialogVariant = "default" | "destructive";
8
+
9
+ export interface AlertDialogProps {
10
+ /** Whether the dialog is open */
11
+ open: boolean;
12
+ /** Callback when dialog should close */
13
+ onClose: () => void;
14
+ /** Dialog title */
15
+ title: string;
16
+ /** Dialog description/message */
17
+ description: string;
18
+ /** Confirm button label */
19
+ confirmLabel?: string;
20
+ /** Cancel button label */
21
+ cancelLabel?: string;
22
+ /** Callback when confirmed */
23
+ onConfirm: () => void;
24
+ /** Callback when cancelled */
25
+ onCancel?: () => void;
26
+ /** Dialog variant */
27
+ variant?: AlertDialogVariant;
28
+ /** Custom tokens override */
29
+ tokens?: Tokens;
30
+ /** Focus ID for focus management */
31
+ focusId?: string;
32
+ }
33
+
34
+ export function AlertDialog({
35
+ open,
36
+ onClose,
37
+ title,
38
+ description,
39
+ confirmLabel = "Continue",
40
+ cancelLabel = "Cancel",
41
+ onConfirm,
42
+ onCancel,
43
+ variant = "default",
44
+ tokens: propTokens,
45
+ focusId,
46
+ }: AlertDialogProps): React.ReactElement | null {
47
+ const contextTokens = useTokens();
48
+ const tokens = propTokens ?? contextTokens;
49
+ const id = focusId ?? stableId("alert-dialog");
50
+ const { isFocused } = useFocusable(id);
51
+
52
+ const [focused, setFocused] = React.useState<"cancel" | "confirm">("cancel");
53
+
54
+ useInput(
55
+ (input, key) => {
56
+ if (!open) return;
57
+
58
+ if (key.escape) {
59
+ onCancel?.();
60
+ onClose();
61
+ return;
62
+ }
63
+
64
+ if (key.leftArrow || input === "h") {
65
+ setFocused("cancel");
66
+ } else if (key.rightArrow || input === "l") {
67
+ setFocused("confirm");
68
+ } else if (key.tab) {
69
+ setFocused((prev) => (prev === "cancel" ? "confirm" : "cancel"));
70
+ } else if (key.return) {
71
+ if (focused === "confirm") {
72
+ onConfirm();
73
+ onClose();
74
+ } else {
75
+ onCancel?.();
76
+ onClose();
77
+ }
78
+ } else if (input === "y" || input === "Y") {
79
+ onConfirm();
80
+ onClose();
81
+ } else if (input === "n" || input === "N") {
82
+ onCancel?.();
83
+ onClose();
84
+ }
85
+ },
86
+ { isActive: open && isFocused }
87
+ );
88
+
89
+ if (!open) return null;
90
+
91
+ const confirmColor =
92
+ variant === "destructive" ? tokens.colors.danger : tokens.colors.accent;
93
+
94
+ return React.createElement(
95
+ Box,
96
+ {
97
+ flexDirection: "column",
98
+ borderStyle: "round",
99
+ borderColor:
100
+ variant === "destructive" ? tokens.colors.danger : tokens.colors.border,
101
+ paddingX: 2,
102
+ paddingY: 1,
103
+ width: 50,
104
+ },
105
+ // Icon + Title
106
+ React.createElement(
107
+ Box,
108
+ { gap: 1, marginBottom: 1 },
109
+ React.createElement(
110
+ Text,
111
+ {
112
+ color:
113
+ variant === "destructive"
114
+ ? tokens.colors.danger
115
+ : tokens.colors.warning,
116
+ },
117
+ variant === "destructive" ? "⚠" : "ℹ"
118
+ ),
119
+ React.createElement(Text, { bold: true, color: tokens.colors.fg }, title)
120
+ ),
121
+ // Description
122
+ React.createElement(
123
+ Box,
124
+ { marginBottom: 1 },
125
+ React.createElement(Text, { color: tokens.colors.muted }, description)
126
+ ),
127
+ // Separator
128
+ React.createElement(
129
+ Box,
130
+ { marginBottom: 1 },
131
+ React.createElement(Text, { color: tokens.colors.border }, "─".repeat(46))
132
+ ),
133
+ // Buttons
134
+ React.createElement(
135
+ Box,
136
+ { gap: 2, justifyContent: "flex-end" },
137
+ // Cancel button
138
+ React.createElement(
139
+ Box,
140
+ {
141
+ borderStyle: focused === "cancel" ? "round" : "single",
142
+ borderColor:
143
+ focused === "cancel" ? tokens.colors.focus : tokens.colors.border,
144
+ paddingX: 2,
145
+ },
146
+ React.createElement(
147
+ Text,
148
+ {
149
+ color:
150
+ focused === "cancel" ? tokens.colors.fg : tokens.colors.muted,
151
+ },
152
+ cancelLabel
153
+ )
154
+ ),
155
+ // Confirm button
156
+ React.createElement(
157
+ Box,
158
+ {
159
+ borderStyle: focused === "confirm" ? "round" : "single",
160
+ borderColor:
161
+ focused === "confirm" ? confirmColor : tokens.colors.border,
162
+ paddingX: 2,
163
+ },
164
+ React.createElement(
165
+ Text,
166
+ {
167
+ color: focused === "confirm" ? confirmColor : tokens.colors.muted,
168
+ bold: focused === "confirm",
169
+ },
170
+ confirmLabel
171
+ )
172
+ )
173
+ ),
174
+ // Hint
175
+ React.createElement(
176
+ Box,
177
+ { marginTop: 1 },
178
+ React.createElement(
179
+ Text,
180
+ { color: tokens.colors.muted, dimColor: true },
181
+ "Y/N to respond • Tab to switch • Enter to confirm"
182
+ )
183
+ )
184
+ );
185
+ }
@@ -0,0 +1,2 @@
1
+ export { AlertDialog } from "./alert-dialog.js";
2
+ export type { AlertDialogProps, AlertDialogVariant } from "./alert-dialog.js";
@@ -0,0 +1,57 @@
1
+ import React from "react";
2
+ import { Text } from "ink";
3
+ import type { Tokens } from "@hauktui/tokens";
4
+ import { useTokens } from "@hauktui/primitives-ink";
5
+
6
+ export interface AvatarProps {
7
+ /** Name to generate initials from */
8
+ name?: string;
9
+ /** Fallback character when no name is provided */
10
+ fallback?: string;
11
+ /** Size of the avatar */
12
+ size?: "sm" | "md" | "lg";
13
+ /** Custom tokens override */
14
+ tokens?: Tokens;
15
+ }
16
+
17
+ function getInitials(name: string): string {
18
+ const parts = name.trim().split(/\s+/);
19
+ if (parts.length === 1) {
20
+ return parts[0]!.charAt(0).toUpperCase();
21
+ }
22
+ return (
23
+ parts[0]!.charAt(0) + parts[parts.length - 1]!.charAt(0)
24
+ ).toUpperCase();
25
+ }
26
+
27
+ export function Avatar({
28
+ name,
29
+ fallback = "?",
30
+ size = "md",
31
+ tokens: propTokens,
32
+ }: AvatarProps): React.ReactElement {
33
+ const contextTokens = useTokens();
34
+ const tokens = propTokens ?? contextTokens;
35
+
36
+ const initials = name ? getInitials(name) : fallback;
37
+
38
+ const sizeConfig = {
39
+ sm: { padding: "" },
40
+ md: { padding: " " },
41
+ lg: { padding: " " },
42
+ };
43
+
44
+ const config = sizeConfig[size];
45
+
46
+ return React.createElement(
47
+ Text,
48
+ {
49
+ backgroundColor: tokens.colors.accent,
50
+ color: tokens.colors.bg,
51
+ bold: true,
52
+ },
53
+ config.padding,
54
+ initials,
55
+ config.padding
56
+ );
57
+ }
@@ -0,0 +1,2 @@
1
+ export { Avatar } from "./avatar.js";
2
+ export type { AvatarProps } from "./avatar.js";
@@ -0,0 +1,144 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import type { Tokens } from "@hauktui/tokens";
4
+ import { useTokens } from "@hauktui/primitives-ink";
5
+
6
+ export interface AvatarGroupProps {
7
+ /** Avatar names or initials */
8
+ avatars: string[];
9
+ /** Maximum avatars to show */
10
+ max?: number;
11
+ /** Size variant */
12
+ size?: "sm" | "md" | "lg";
13
+ /** Custom tokens override */
14
+ tokens?: Tokens;
15
+ /** Stacking direction */
16
+ direction?: "left" | "right";
17
+ }
18
+
19
+ // Color palette for avatars
20
+ const AVATAR_COLORS = [
21
+ "#e91e63",
22
+ "#9c27b0",
23
+ "#673ab7",
24
+ "#3f51b5",
25
+ "#2196f3",
26
+ "#03a9f4",
27
+ "#00bcd4",
28
+ "#009688",
29
+ "#4caf50",
30
+ "#8bc34a",
31
+ "#cddc39",
32
+ "#ffeb3b",
33
+ "#ffc107",
34
+ "#ff9800",
35
+ "#ff5722",
36
+ ];
37
+
38
+ export function AvatarGroup({
39
+ avatars,
40
+ max = 5,
41
+ size = "md",
42
+ tokens: propTokens,
43
+ direction = "right",
44
+ }: AvatarGroupProps): React.ReactElement {
45
+ const contextTokens = useTokens();
46
+ const tokens = propTokens ?? contextTokens;
47
+
48
+ // Get avatar dimensions based on size
49
+ const getDimensions = () => {
50
+ switch (size) {
51
+ case "sm":
52
+ return { width: 3, chars: 1 };
53
+ case "lg":
54
+ return { width: 5, chars: 2 };
55
+ default:
56
+ return { width: 4, chars: 2 };
57
+ }
58
+ };
59
+
60
+ const dims = getDimensions();
61
+
62
+ // Get initials from name
63
+ const getInitials = (name: string): string => {
64
+ const parts = name.trim().split(/\s+/);
65
+ if (parts.length === 1) {
66
+ return parts[0].substring(0, dims.chars).toUpperCase();
67
+ }
68
+ return parts
69
+ .slice(0, dims.chars)
70
+ .map((p) => p[0])
71
+ .join("")
72
+ .toUpperCase();
73
+ };
74
+
75
+ // Get color for name (deterministic based on name)
76
+ const getColor = (name: string): string => {
77
+ let hash = 0;
78
+ for (let i = 0; i < name.length; i++) {
79
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
80
+ }
81
+ return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];
82
+ };
83
+
84
+ // Avatars to display
85
+ const displayAvatars = avatars.slice(0, max);
86
+ const remaining = avatars.length - max;
87
+
88
+ // Render single avatar
89
+ const renderAvatar = (name: string, index: number) => {
90
+ const initials = getInitials(name);
91
+ const bgColor = getColor(name);
92
+ const padding = " ".repeat(Math.floor((dims.width - initials.length) / 2));
93
+
94
+ return React.createElement(
95
+ Box,
96
+ {
97
+ key: index,
98
+ marginLeft: index > 0 ? -1 : 0,
99
+ },
100
+ React.createElement(
101
+ Text,
102
+ {
103
+ backgroundColor: bgColor,
104
+ color: "#ffffff",
105
+ bold: true,
106
+ },
107
+ `${padding}${initials}${padding}`
108
+ )
109
+ );
110
+ };
111
+
112
+ // Render overflow indicator
113
+ const renderOverflow = () => {
114
+ if (remaining <= 0) return null;
115
+
116
+ const text = `+${remaining}`;
117
+ const padding = " ".repeat(
118
+ Math.max(0, Math.floor((dims.width - text.length) / 2))
119
+ );
120
+
121
+ return React.createElement(
122
+ Box,
123
+ { marginLeft: -1 },
124
+ React.createElement(
125
+ Text,
126
+ {
127
+ backgroundColor: tokens.colors.border,
128
+ color: tokens.colors.fg,
129
+ },
130
+ `${padding}${text}${padding}`
131
+ )
132
+ );
133
+ };
134
+
135
+ const avatarElements =
136
+ direction === "right" ? displayAvatars : displayAvatars.reverse();
137
+
138
+ return React.createElement(
139
+ Box,
140
+ { flexDirection: "row" },
141
+ avatarElements.map(renderAvatar),
142
+ renderOverflow()
143
+ );
144
+ }
@@ -0,0 +1,2 @@
1
+ export { AvatarGroup } from "./avatar-group.js";
2
+ export type { AvatarGroupProps } from "./avatar-group.js";
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import { Text } from "ink";
3
+ import type { Tokens } from "@hauktui/tokens";
4
+ import { useTokens } from "@hauktui/primitives-ink";
5
+
6
+ export type BadgeVariant =
7
+ | "default"
8
+ | "primary"
9
+ | "success"
10
+ | "warning"
11
+ | "danger"
12
+ | "muted";
13
+
14
+ export interface BadgeProps {
15
+ /** Badge content */
16
+ children: React.ReactNode;
17
+ /** Badge variant */
18
+ variant?: BadgeVariant;
19
+ /** Custom tokens override */
20
+ tokens?: Tokens;
21
+ }
22
+
23
+ export function Badge({
24
+ children,
25
+ variant = "default",
26
+ tokens: propTokens,
27
+ }: BadgeProps): React.ReactElement {
28
+ const contextTokens = useTokens();
29
+ const tokens = propTokens ?? contextTokens;
30
+
31
+ const variantColors: Record<BadgeVariant, { bg: string; fg: string }> = {
32
+ default: { bg: tokens.colors.border, fg: tokens.colors.fg },
33
+ primary: { bg: tokens.colors.accent, fg: tokens.colors.bg },
34
+ success: { bg: tokens.colors.success, fg: tokens.colors.bg },
35
+ warning: { bg: tokens.colors.warning, fg: tokens.colors.bg },
36
+ danger: { bg: tokens.colors.danger, fg: tokens.colors.bg },
37
+ muted: { bg: tokens.colors.muted, fg: tokens.colors.bg },
38
+ };
39
+
40
+ const colors = variantColors[variant];
41
+
42
+ return React.createElement(
43
+ Text,
44
+ {
45
+ backgroundColor: colors.bg,
46
+ color: colors.fg,
47
+ },
48
+ " ",
49
+ children,
50
+ " "
51
+ );
52
+ }
@@ -0,0 +1,2 @@
1
+ export { Badge } from "./badge.js";
2
+ export type { BadgeProps, BadgeVariant } from "./badge.js";