omnira-ui 0.1.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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/cli/omnira-init.mjs +260 -0
  4. package/cli/presets.mjs +386 -0
  5. package/components/ui/AppStoreButton/AppStoreButton.module.css +87 -0
  6. package/components/ui/AppStoreButton/AppStoreButton.tsx +114 -0
  7. package/components/ui/AppStoreButton/index.ts +2 -0
  8. package/components/ui/Avatar/Avatar.module.css +76 -0
  9. package/components/ui/Avatar/Avatar.tsx +102 -0
  10. package/components/ui/Avatar/index.ts +2 -0
  11. package/components/ui/Badge/Badge.module.css +120 -0
  12. package/components/ui/Badge/Badge.tsx +25 -0
  13. package/components/ui/Badge/index.ts +2 -0
  14. package/components/ui/BadgeGroup/BadgeGroup.module.css +214 -0
  15. package/components/ui/BadgeGroup/BadgeGroup.tsx +88 -0
  16. package/components/ui/BadgeGroup/index.ts +2 -0
  17. package/components/ui/Browser/Browser.module.css +105 -0
  18. package/components/ui/Browser/Browser.tsx +54 -0
  19. package/components/ui/Browser/index.ts +2 -0
  20. package/components/ui/Button/Button.module.css +188 -0
  21. package/components/ui/Button/Button.tsx +74 -0
  22. package/components/ui/Button/index.ts +2 -0
  23. package/components/ui/ButtonUtility/ButtonUtility.module.css +154 -0
  24. package/components/ui/ButtonUtility/ButtonUtility.tsx +65 -0
  25. package/components/ui/ButtonUtility/index.ts +2 -0
  26. package/components/ui/Card/Card.module.css +80 -0
  27. package/components/ui/Card/Card.tsx +39 -0
  28. package/components/ui/Card/index.ts +2 -0
  29. package/components/ui/Checkbox/Checkbox.module.css +92 -0
  30. package/components/ui/Checkbox/Checkbox.tsx +95 -0
  31. package/components/ui/Checkbox/index.ts +2 -0
  32. package/components/ui/Collapse/Collapse.module.css +60 -0
  33. package/components/ui/Collapse/Collapse.tsx +100 -0
  34. package/components/ui/Collapse/index.ts +2 -0
  35. package/components/ui/CreditCard/CreditCard.module.css +117 -0
  36. package/components/ui/CreditCard/CreditCard.tsx +86 -0
  37. package/components/ui/CreditCard/index.ts +2 -0
  38. package/components/ui/Dropdown/Dropdown.module.css +269 -0
  39. package/components/ui/Dropdown/Dropdown.tsx +419 -0
  40. package/components/ui/Dropdown/index.ts +31 -0
  41. package/components/ui/GridLines/GridLines.module.css +95 -0
  42. package/components/ui/GridLines/GridLines.tsx +10 -0
  43. package/components/ui/GridLines/index.ts +1 -0
  44. package/components/ui/Illustration/Illustration.module.css +10 -0
  45. package/components/ui/Illustration/Illustration.tsx +96 -0
  46. package/components/ui/Illustration/index.ts +2 -0
  47. package/components/ui/Input/Input.module.css +217 -0
  48. package/components/ui/Input/Input.tsx +93 -0
  49. package/components/ui/Input/index.ts +2 -0
  50. package/components/ui/Phone/Phone.module.css +104 -0
  51. package/components/ui/Phone/Phone.tsx +54 -0
  52. package/components/ui/Phone/index.ts +2 -0
  53. package/components/ui/PinInput/PinInput.module.css +104 -0
  54. package/components/ui/PinInput/PinInput.tsx +210 -0
  55. package/components/ui/PinInput/index.ts +1 -0
  56. package/components/ui/ProgressBar/ProgressBar.module.css +180 -0
  57. package/components/ui/ProgressBar/ProgressBar.tsx +206 -0
  58. package/components/ui/ProgressBar/index.ts +2 -0
  59. package/components/ui/QRCode/QRCode.module.css +13 -0
  60. package/components/ui/QRCode/QRCode.tsx +128 -0
  61. package/components/ui/QRCode/index.ts +2 -0
  62. package/components/ui/RadioButton/RadioButton.module.css +92 -0
  63. package/components/ui/RadioButton/RadioButton.tsx +134 -0
  64. package/components/ui/RadioButton/index.ts +2 -0
  65. package/components/ui/RadioGroup/RadioGroup.module.css +382 -0
  66. package/components/ui/RadioGroup/RadioGroup.tsx +292 -0
  67. package/components/ui/RadioGroup/index.ts +1 -0
  68. package/components/ui/Rating/Rating.module.css +99 -0
  69. package/components/ui/Rating/Rating.tsx +123 -0
  70. package/components/ui/Rating/index.ts +2 -0
  71. package/components/ui/Select/Select.module.css +371 -0
  72. package/components/ui/Select/Select.tsx +507 -0
  73. package/components/ui/Select/index.ts +2 -0
  74. package/components/ui/SidebarNavigation/SidebarDual.tsx +71 -0
  75. package/components/ui/SidebarNavigation/SidebarFeatureCard.module.css +351 -0
  76. package/components/ui/SidebarNavigation/SidebarFeatureCard.tsx +388 -0
  77. package/components/ui/SidebarNavigation/SidebarNavigation.module.css +610 -0
  78. package/components/ui/SidebarNavigation/SidebarParts.tsx +215 -0
  79. package/components/ui/SidebarNavigation/SidebarSectionDividers.tsx +46 -0
  80. package/components/ui/SidebarNavigation/SidebarSectionHeadings.tsx +52 -0
  81. package/components/ui/SidebarNavigation/SidebarSimple.tsx +46 -0
  82. package/components/ui/SidebarNavigation/SidebarSlim.tsx +48 -0
  83. package/components/ui/SidebarNavigation/index.ts +29 -0
  84. package/components/ui/SidebarNavigation/types.ts +27 -0
  85. package/components/ui/Slider/Slider.module.css +93 -0
  86. package/components/ui/Slider/Slider.tsx +118 -0
  87. package/components/ui/Slider/index.ts +2 -0
  88. package/components/ui/SocialButton/SocialButton.module.css +223 -0
  89. package/components/ui/SocialButton/SocialButton.tsx +185 -0
  90. package/components/ui/SocialButton/index.ts +2 -0
  91. package/components/ui/Tag/Tag.module.css +203 -0
  92. package/components/ui/Tag/Tag.tsx +161 -0
  93. package/components/ui/Tag/index.ts +2 -0
  94. package/components/ui/TextEditor/TextEditor.module.css +182 -0
  95. package/components/ui/TextEditor/TextEditor.tsx +323 -0
  96. package/components/ui/TextEditor/index.ts +1 -0
  97. package/components/ui/Textarea/Textarea.module.css +75 -0
  98. package/components/ui/Textarea/Textarea.tsx +59 -0
  99. package/components/ui/Textarea/index.ts +2 -0
  100. package/components/ui/Toggle/Toggle.module.css +118 -0
  101. package/components/ui/Toggle/Toggle.tsx +77 -0
  102. package/components/ui/Toggle/index.ts +2 -0
  103. package/components/ui/Tooltip/Tooltip.module.css +95 -0
  104. package/components/ui/Tooltip/Tooltip.tsx +55 -0
  105. package/components/ui/Tooltip/index.ts +2 -0
  106. package/components/ui/VideoPlayer/VideoPlayer.module.css +120 -0
  107. package/components/ui/VideoPlayer/VideoPlayer.tsx +229 -0
  108. package/components/ui/VideoPlayer/index.ts +2 -0
  109. package/lib/cn.ts +3 -0
  110. package/lib/copy-to-clipboard.ts +21 -0
  111. package/package.json +61 -0
@@ -0,0 +1,161 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { CloseCircle, TickSquare, MinusSquare } from "iconsax-react";
5
+ import { cn } from "@/lib/cn";
6
+ import styles from "./Tag.module.css";
7
+
8
+ export type TagSize = "sm" | "md" | "lg";
9
+
10
+ export interface TagProps {
11
+ children: React.ReactNode;
12
+ size?: TagSize;
13
+ dot?: boolean;
14
+ avatarSrc?: string;
15
+ count?: number;
16
+ onRemove?: () => void;
17
+ checkbox?: boolean;
18
+ checked?: boolean;
19
+ onCheckedChange?: (checked: boolean) => void;
20
+ disabled?: boolean;
21
+ className?: string;
22
+ }
23
+
24
+ export function Tag({
25
+ children,
26
+ size = "md",
27
+ dot = false,
28
+ avatarSrc,
29
+ count,
30
+ onRemove,
31
+ checkbox = false,
32
+ checked: controlledChecked,
33
+ onCheckedChange,
34
+ disabled = false,
35
+ className,
36
+ }: TagProps) {
37
+ const [internalChecked, setInternalChecked] = useState(false);
38
+ const isChecked = controlledChecked !== undefined ? controlledChecked : internalChecked;
39
+
40
+ const handleCheckboxToggle = () => {
41
+ if (disabled) return;
42
+ const next = !isChecked;
43
+ if (controlledChecked === undefined) setInternalChecked(next);
44
+ onCheckedChange?.(next);
45
+ };
46
+
47
+ const isInteractive = checkbox || !!onRemove;
48
+
49
+ return (
50
+ <span
51
+ className={cn(
52
+ styles.tag,
53
+ styles[size],
54
+ checkbox && styles.checkable,
55
+ checkbox && isChecked && styles.checked,
56
+ disabled && styles.disabled,
57
+ className,
58
+ )}
59
+ role={checkbox ? "checkbox" : undefined}
60
+ aria-checked={checkbox ? isChecked : undefined}
61
+ aria-disabled={disabled || undefined}
62
+ tabIndex={isInteractive ? 0 : undefined}
63
+ onClick={checkbox ? handleCheckboxToggle : undefined}
64
+ onKeyDown={
65
+ checkbox
66
+ ? (e) => {
67
+ if (e.key === " " || e.key === "Enter") {
68
+ e.preventDefault();
69
+ handleCheckboxToggle();
70
+ }
71
+ }
72
+ : undefined
73
+ }
74
+ >
75
+ {checkbox && (
76
+ <span className={styles.checkbox}>
77
+ {isChecked ? (
78
+ <TickSquare size={size === "sm" ? 14 : size === "lg" ? 18 : 16} variant="Bulk" color="var(--color-lime)" />
79
+ ) : (
80
+ <MinusSquare size={size === "sm" ? 14 : size === "lg" ? 18 : 16} variant="Linear" color="var(--color-text-tertiary)" />
81
+ )}
82
+ </span>
83
+ )}
84
+
85
+ {dot && <span className={styles.dot} />}
86
+
87
+ {avatarSrc && (
88
+ <img
89
+ src={avatarSrc}
90
+ alt=""
91
+ className={styles.avatar}
92
+ loading="lazy"
93
+ />
94
+ )}
95
+
96
+ <span className={styles.label}>{children}</span>
97
+
98
+ {count !== undefined && (
99
+ <span className={styles.count}>{count}</span>
100
+ )}
101
+
102
+ {onRemove && (
103
+ <button
104
+ type="button"
105
+ className={styles.removeBtn}
106
+ onClick={(e) => {
107
+ e.stopPropagation();
108
+ if (!disabled) onRemove();
109
+ }}
110
+ aria-label="Remove tag"
111
+ tabIndex={-1}
112
+ disabled={disabled}
113
+ >
114
+ <CloseCircle
115
+ size={size === "sm" ? 12 : size === "lg" ? 16 : 14}
116
+ variant="Bold"
117
+ color="currentColor"
118
+ />
119
+ </button>
120
+ )}
121
+ </span>
122
+ );
123
+ }
124
+
125
+ export interface TagGroupProps {
126
+ children: React.ReactNode;
127
+ label?: string;
128
+ size?: TagSize;
129
+ className?: string;
130
+ }
131
+
132
+ export function TagGroup({
133
+ children,
134
+ label,
135
+ size = "md",
136
+ className,
137
+ }: TagGroupProps) {
138
+ return (
139
+ <div
140
+ className={cn(styles.tagGroup, className)}
141
+ role="group"
142
+ aria-label={label}
143
+ >
144
+ {label && <span className={cn(styles.tagGroupLabel, styles[`label${size}`])}>{label}</span>}
145
+ {children}
146
+ </div>
147
+ );
148
+ }
149
+
150
+ export interface TagListProps {
151
+ children: React.ReactNode;
152
+ className?: string;
153
+ }
154
+
155
+ export function TagList({ children, className }: TagListProps) {
156
+ return (
157
+ <div className={cn(styles.tagList, className)}>
158
+ {children}
159
+ </div>
160
+ );
161
+ }
@@ -0,0 +1,2 @@
1
+ export { Tag, TagGroup, TagList } from "./Tag";
2
+ export type { TagProps, TagGroupProps, TagListProps, TagSize } from "./Tag";
@@ -0,0 +1,182 @@
1
+ /* ── Root ── */
2
+ .root {
3
+ display: flex;
4
+ flex-direction: column;
5
+ width: 100%;
6
+ border-radius: var(--radius-lg);
7
+ background: var(--color-bg-input);
8
+ border: 1px solid var(--color-border-standard);
9
+ transition: border-color 0.25s;
10
+ }
11
+
12
+ .root:focus-within {
13
+ border-color: var(--color-border-lime-strong);
14
+ }
15
+
16
+ .root_sm {
17
+ font-size: 13px;
18
+ }
19
+
20
+ .root_md {
21
+ font-size: 14px;
22
+ }
23
+
24
+ /* ── Toolbar ── */
25
+ .toolbar {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 2px;
29
+ padding: 8px 12px;
30
+ border-bottom: 1px solid var(--color-border-subtle);
31
+ flex-wrap: wrap;
32
+ }
33
+
34
+ .toolbar_sm {
35
+ padding: 6px 10px;
36
+ }
37
+
38
+ .toolbar_md {
39
+ padding: 8px 12px;
40
+ }
41
+
42
+ .toolbarFloating {
43
+ position: sticky;
44
+ top: 0;
45
+ z-index: 10;
46
+ background: var(--color-bg-card);
47
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
48
+ backdrop-filter: blur(12px);
49
+ border-bottom: 1px solid var(--color-border-standard);
50
+ }
51
+
52
+ .toolbarBtn {
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ width: 30px;
57
+ height: 30px;
58
+ border: none;
59
+ border-radius: var(--radius-sm);
60
+ background: transparent;
61
+ color: var(--color-text-secondary);
62
+ cursor: pointer;
63
+ transition: all 0.15s;
64
+ flex-shrink: 0;
65
+ }
66
+
67
+ .toolbarBtn:hover {
68
+ background: var(--color-bg-hover);
69
+ color: var(--color-text-primary);
70
+ }
71
+
72
+ .toolbarBtn:active {
73
+ background: var(--color-bg-lime-subtle);
74
+ color: var(--color-lime);
75
+ }
76
+
77
+ .toolbarDivider {
78
+ width: 1px;
79
+ height: 18px;
80
+ background: var(--color-border-subtle);
81
+ margin: 0 4px;
82
+ flex-shrink: 0;
83
+ }
84
+
85
+ /* ── Content ── */
86
+ .content {
87
+ flex: 1;
88
+ padding: 14px 16px;
89
+ color: var(--color-text-primary);
90
+ line-height: 1.7;
91
+ outline: none;
92
+ overflow-y: auto;
93
+ min-height: 120px;
94
+ font-family: inherit;
95
+ word-break: break-word;
96
+ }
97
+
98
+ .content:empty::before {
99
+ content: "Start writing...";
100
+ color: var(--color-text-tertiary);
101
+ opacity: 0.5;
102
+ pointer-events: none;
103
+ }
104
+
105
+ .content_sm {
106
+ padding: 10px 12px;
107
+ min-height: 100px;
108
+ max-height: 200px;
109
+ font-size: 13px;
110
+ }
111
+
112
+ .content_md {
113
+ padding: 14px 16px;
114
+ min-height: 180px;
115
+ max-height: 400px;
116
+ font-size: 14px;
117
+ }
118
+
119
+ /* Content formatting */
120
+ .content p {
121
+ margin: 0 0 8px;
122
+ }
123
+
124
+ .content p:last-child {
125
+ margin-bottom: 0;
126
+ }
127
+
128
+ .content blockquote {
129
+ margin: 8px 0;
130
+ padding: 8px 16px;
131
+ border-left: 3px solid var(--color-border-lime-strong);
132
+ color: var(--color-text-secondary);
133
+ font-style: italic;
134
+ }
135
+
136
+ .content ul,
137
+ .content ol {
138
+ margin: 8px 0;
139
+ padding-left: 24px;
140
+ }
141
+
142
+ .content li {
143
+ margin-bottom: 4px;
144
+ }
145
+
146
+ .content a {
147
+ color: var(--color-lime);
148
+ text-decoration: underline;
149
+ }
150
+
151
+ .content code {
152
+ padding: 2px 6px;
153
+ border-radius: var(--radius-sm);
154
+ background: var(--color-bg-elevated);
155
+ font-size: 0.9em;
156
+ font-family: monospace;
157
+ }
158
+
159
+ /* ── Hint Row ── */
160
+ .hintRow {
161
+ display: flex;
162
+ align-items: center;
163
+ justify-content: space-between;
164
+ padding: 8px 16px;
165
+ border-top: 1px solid var(--color-border-subtle);
166
+ }
167
+
168
+ .hintText {
169
+ font-size: 12px;
170
+ color: var(--color-text-tertiary);
171
+ }
172
+
173
+ .charCount {
174
+ font-size: 12px;
175
+ color: var(--color-text-tertiary);
176
+ margin-left: auto;
177
+ font-variant-numeric: tabular-nums;
178
+ }
179
+
180
+ .charCountOver {
181
+ color: var(--color-error);
182
+ }
@@ -0,0 +1,323 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useRef,
7
+ useState,
8
+ useCallback,
9
+ useEffect,
10
+ } from "react";
11
+ import { cn } from "@/lib/cn";
12
+ import styles from "./TextEditor.module.css";
13
+
14
+ /* ── Context ── */
15
+ interface EditorCtx {
16
+ contentRef: React.RefObject<HTMLDivElement | null>;
17
+ content: string;
18
+ setContent: (html: string) => void;
19
+ limit?: number;
20
+ charCount: number;
21
+ size: "sm" | "md";
22
+ inputClassName?: string;
23
+ }
24
+
25
+ const Ctx = createContext<EditorCtx>({
26
+ contentRef: { current: null },
27
+ content: "",
28
+ setContent: () => {},
29
+ charCount: 0,
30
+ size: "md",
31
+ });
32
+
33
+ /* ── Root ── */
34
+ interface RootProps {
35
+ content?: string;
36
+ onUpdate?: (payload: { editor: { getHTML: () => string } }) => void;
37
+ limit?: number;
38
+ size?: "sm" | "md";
39
+ className?: string;
40
+ inputClassName?: string;
41
+ children: React.ReactNode;
42
+ }
43
+
44
+ function Root({
45
+ content: initialContent = "",
46
+ onUpdate,
47
+ limit,
48
+ size = "md",
49
+ className,
50
+ inputClassName,
51
+ children,
52
+ }: RootProps) {
53
+ const contentRef = useRef<HTMLDivElement | null>(null);
54
+ const [content, setContentState] = useState(initialContent);
55
+ const [charCount, setCharCount] = useState(0);
56
+
57
+ const setContent = useCallback(
58
+ (html: string) => {
59
+ setContentState(html);
60
+ if (onUpdate) {
61
+ onUpdate({ editor: { getHTML: () => html } });
62
+ }
63
+ },
64
+ [onUpdate],
65
+ );
66
+
67
+ useEffect(() => {
68
+ const text = contentRef.current?.textContent || "";
69
+ setCharCount(text.length);
70
+ }, [content]);
71
+
72
+ return (
73
+ <Ctx.Provider value={{ contentRef, content, setContent, limit, charCount, size, inputClassName }}>
74
+ <div className={cn(styles.root, styles[`root_${size}`], className)}>{children}</div>
75
+ </Ctx.Provider>
76
+ );
77
+ }
78
+
79
+ /* ── Toolbar Button ── */
80
+ function ToolbarBtn({
81
+ cmd,
82
+ arg,
83
+ icon,
84
+ title,
85
+ }: {
86
+ cmd: string;
87
+ arg?: string;
88
+ icon: React.ReactNode;
89
+ title: string;
90
+ }) {
91
+ const { contentRef } = useContext(Ctx);
92
+
93
+ const handleClick = () => {
94
+ contentRef.current?.focus();
95
+ document.execCommand(cmd, false, arg);
96
+ };
97
+
98
+ return (
99
+ <button
100
+ type="button"
101
+ className={styles.toolbarBtn}
102
+ onClick={handleClick}
103
+ title={title}
104
+ aria-label={title}
105
+ >
106
+ {icon}
107
+ </button>
108
+ );
109
+ }
110
+
111
+ /* ── SVG Icons (inline, no deps) ── */
112
+ const icons = {
113
+ bold: (
114
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
115
+ <path d="M4 2.5h5a3 3 0 0 1 0 6H4V2.5Z" stroke="currentColor" strokeWidth="1.5" />
116
+ <path d="M4 8.5h6a3 3 0 0 1 0 6H4V8.5Z" stroke="currentColor" strokeWidth="1.5" />
117
+ </svg>
118
+ ),
119
+ italic: (
120
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
121
+ <path d="M10 2H6.5M9.5 14H6M8.5 2L7 14" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
122
+ </svg>
123
+ ),
124
+ underline: (
125
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
126
+ <path d="M4 2v5a4 4 0 0 0 8 0V2M3 14h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
127
+ </svg>
128
+ ),
129
+ strikethrough: (
130
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
131
+ <path d="M2 8h12M10.5 5C10.5 3.5 9.5 2.5 8 2.5S5.5 3.5 5.5 5M5.5 11c0 1.5 1 2.5 2.5 2.5s2.5-1 2.5-2.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
132
+ </svg>
133
+ ),
134
+ ul: (
135
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
136
+ <circle cx="3" cy="4" r="1" fill="currentColor" />
137
+ <circle cx="3" cy="8" r="1" fill="currentColor" />
138
+ <circle cx="3" cy="12" r="1" fill="currentColor" />
139
+ <path d="M6 4h8M6 8h8M6 12h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
140
+ </svg>
141
+ ),
142
+ ol: (
143
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
144
+ <text x="1.5" y="5.5" fontSize="5" fill="currentColor" fontWeight="700">1</text>
145
+ <text x="1.5" y="9.5" fontSize="5" fill="currentColor" fontWeight="700">2</text>
146
+ <text x="1.5" y="13.5" fontSize="5" fill="currentColor" fontWeight="700">3</text>
147
+ <path d="M6 4h8M6 8h8M6 12h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
148
+ </svg>
149
+ ),
150
+ link: (
151
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
152
+ <path d="M6.5 9.5l3-3M7 11l-1.5 1.5a2.12 2.12 0 0 1-3-3L4 8M9 5l1.5-1.5a2.12 2.12 0 0 1 3 3L12 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
153
+ </svg>
154
+ ),
155
+ quote: (
156
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
157
+ <path d="M3 14V8h3V3H3M10 14V8h3V3h-3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
158
+ </svg>
159
+ ),
160
+ code: (
161
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
162
+ <path d="M5 4L1 8l4 4M11 4l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
163
+ </svg>
164
+ ),
165
+ undo: (
166
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
167
+ <path d="M3 6h7a3 3 0 0 1 0 6H8M3 6l3-3M3 6l3 3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
168
+ </svg>
169
+ ),
170
+ redo: (
171
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
172
+ <path d="M13 6H6a3 3 0 0 0 0 6h2M13 6l-3-3M13 6l-3 3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
173
+ </svg>
174
+ ),
175
+ };
176
+
177
+ /* ── Link Button (prompts on click, not render) ── */
178
+ function LinkBtn() {
179
+ const { contentRef } = useContext(Ctx);
180
+
181
+ const handleClick = () => {
182
+ const url = window.prompt("URL:");
183
+ if (url) {
184
+ contentRef.current?.focus();
185
+ document.execCommand("createLink", false, url);
186
+ }
187
+ };
188
+
189
+ return (
190
+ <button
191
+ type="button"
192
+ className={styles.toolbarBtn}
193
+ onClick={handleClick}
194
+ title="Link"
195
+ aria-label="Link"
196
+ >
197
+ {icons.link}
198
+ </button>
199
+ );
200
+ }
201
+
202
+ /* ── Toolbar ── */
203
+ interface ToolbarProps {
204
+ type?: "basic" | "advanced";
205
+ floating?: boolean;
206
+ className?: string;
207
+ }
208
+
209
+ function Toolbar({ type = "basic", floating = false, className }: ToolbarProps) {
210
+ const { size } = useContext(Ctx);
211
+
212
+ const basicBtns = (
213
+ <>
214
+ <ToolbarBtn cmd="bold" icon={icons.bold} title="Bold" />
215
+ <ToolbarBtn cmd="italic" icon={icons.italic} title="Italic" />
216
+ <ToolbarBtn cmd="underline" icon={icons.underline} title="Underline" />
217
+ <ToolbarBtn cmd="strikeThrough" icon={icons.strikethrough} title="Strikethrough" />
218
+ </>
219
+ );
220
+
221
+ const advancedBtns = (
222
+ <>
223
+ {basicBtns}
224
+ <span className={styles.toolbarDivider} />
225
+ <ToolbarBtn cmd="insertUnorderedList" icon={icons.ul} title="Bullet list" />
226
+ <ToolbarBtn cmd="insertOrderedList" icon={icons.ol} title="Numbered list" />
227
+ <span className={styles.toolbarDivider} />
228
+ <ToolbarBtn cmd="formatBlock" arg="blockquote" icon={icons.quote} title="Quote" />
229
+ <LinkBtn />
230
+ <span className={styles.toolbarDivider} />
231
+ <ToolbarBtn cmd="undo" icon={icons.undo} title="Undo" />
232
+ <ToolbarBtn cmd="redo" icon={icons.redo} title="Redo" />
233
+ </>
234
+ );
235
+
236
+ return (
237
+ <div
238
+ className={cn(
239
+ styles.toolbar,
240
+ styles[`toolbar_${size}`],
241
+ floating && styles.toolbarFloating,
242
+ className,
243
+ )}
244
+ >
245
+ {type === "advanced" ? advancedBtns : basicBtns}
246
+ </div>
247
+ );
248
+ }
249
+
250
+ /* ── Content ── */
251
+ interface ContentProps {
252
+ className?: string;
253
+ }
254
+
255
+ function Content({ className }: ContentProps) {
256
+ const { contentRef, content, setContent, size, inputClassName } = useContext(Ctx);
257
+ const initialized = useRef(false);
258
+
259
+ useEffect(() => {
260
+ if (!initialized.current && contentRef.current && content) {
261
+ contentRef.current.innerHTML = content;
262
+ initialized.current = true;
263
+ }
264
+ }, [content, contentRef]);
265
+
266
+ const handleInput = () => {
267
+ if (contentRef.current) {
268
+ setContent(contentRef.current.innerHTML);
269
+ }
270
+ };
271
+
272
+ return (
273
+ <div
274
+ ref={contentRef}
275
+ className={cn(styles.content, styles[`content_${size}`], inputClassName, className)}
276
+ contentEditable
277
+ suppressContentEditableWarning
278
+ onInput={handleInput}
279
+ role="textbox"
280
+ aria-multiline="true"
281
+ />
282
+ );
283
+ }
284
+
285
+ /* ── HintText ── */
286
+ interface HintTextProps {
287
+ className?: string;
288
+ children?: React.ReactNode;
289
+ }
290
+
291
+ function HintText({ className, children }: HintTextProps) {
292
+ const { charCount, limit } = useContext(Ctx);
293
+
294
+ return (
295
+ <div className={cn(styles.hintRow, className)}>
296
+ {children && <span className={styles.hintText}>{children}</span>}
297
+ {limit !== undefined && (
298
+ <span
299
+ className={cn(
300
+ styles.charCount,
301
+ charCount > limit && styles.charCountOver,
302
+ )}
303
+ >
304
+ {charCount}/{limit}
305
+ </span>
306
+ )}
307
+ {limit === undefined && (
308
+ <span className={styles.charCount}>{charCount} characters</span>
309
+ )}
310
+ </div>
311
+ );
312
+ }
313
+
314
+ /* ── Compound export ── */
315
+ export const TextEditor = Object.assign(
316
+ {},
317
+ {
318
+ Root,
319
+ Toolbar,
320
+ Content,
321
+ HintText,
322
+ },
323
+ );
@@ -0,0 +1 @@
1
+ export { TextEditor } from "./TextEditor";