@artkit-ai/react 0.0.0-stage → 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.
- package/LICENSE +202 -0
- package/README.md +145 -2
- package/dist/cache.d.ts +35 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +116 -0
- package/dist/cache.js.map +1 -0
- package/dist/client.d.ts +79 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +260 -0
- package/dist/client.js.map +1 -0
- package/dist/components/approval.d.ts +34 -0
- package/dist/components/approval.d.ts.map +1 -0
- package/dist/components/approval.js +80 -0
- package/dist/components/approval.js.map +1 -0
- package/dist/components/card.d.ts +22 -0
- package/dist/components/card.d.ts.map +1 -0
- package/dist/components/card.js +16 -0
- package/dist/components/card.js.map +1 -0
- package/dist/components/editor.d.ts +28 -0
- package/dist/components/editor.d.ts.map +1 -0
- package/dist/components/editor.js +161 -0
- package/dist/components/editor.js.map +1 -0
- package/dist/components/icons.d.ts +8 -0
- package/dist/components/icons.d.ts.map +1 -0
- package/dist/components/icons.js +14 -0
- package/dist/components/icons.js.map +1 -0
- package/dist/components/panel.d.ts +34 -0
- package/dist/components/panel.d.ts.map +1 -0
- package/dist/components/panel.js +216 -0
- package/dist/components/panel.js.map +1 -0
- package/dist/components/shared.d.ts +71 -0
- package/dist/components/shared.d.ts.map +1 -0
- package/dist/components/shared.js +104 -0
- package/dist/components/shared.js.map +1 -0
- package/dist/components/version-diff.d.ts +35 -0
- package/dist/components/version-diff.d.ts.map +1 -0
- package/dist/components/version-diff.js +85 -0
- package/dist/components/version-diff.js.map +1 -0
- package/dist/components/view.d.ts +40 -0
- package/dist/components/view.d.ts.map +1 -0
- package/dist/components/view.js +75 -0
- package/dist/components/view.js.map +1 -0
- package/dist/context.d.ts +59 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +13 -0
- package/dist/context.js.map +1 -0
- package/dist/controls.d.ts +145 -0
- package/dist/controls.d.ts.map +1 -0
- package/dist/controls.js +188 -0
- package/dist/controls.js.map +1 -0
- package/dist/download.d.ts +4 -0
- package/dist/download.d.ts.map +1 -0
- package/dist/download.js +20 -0
- package/dist/download.js.map +1 -0
- package/dist/editor-session.d.ts +184 -0
- package/dist/editor-session.d.ts.map +1 -0
- package/dist/editor-session.js +614 -0
- package/dist/editor-session.js.map +1 -0
- package/dist/hooks.d.ts +85 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +297 -0
- package/dist/hooks.js.map +1 -0
- package/dist/i18n.d.ts +197 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +207 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/kit-css.d.ts +3 -0
- package/dist/kit-css.d.ts.map +1 -0
- package/dist/kit-css.js +5 -0
- package/dist/kit-css.js.map +1 -0
- package/dist/line-diff.d.ts +25 -0
- package/dist/line-diff.d.ts.map +1 -0
- package/dist/line-diff.js +96 -0
- package/dist/line-diff.js.map +1 -0
- package/dist/partial-json.d.ts +8 -0
- package/dist/partial-json.d.ts.map +1 -0
- package/dist/partial-json.js +176 -0
- package/dist/partial-json.js.map +1 -0
- package/dist/provider.d.ts +42 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +95 -0
- package/dist/provider.js.map +1 -0
- package/dist/styles.d.ts +25 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +203 -0
- package/dist/styles.js.map +1 -0
- package/dist/toolbar.d.ts +90 -0
- package/dist/toolbar.d.ts.map +1 -0
- package/dist/toolbar.js +233 -0
- package/dist/toolbar.js.map +1 -0
- package/package.json +50 -3
- package/src/cache.ts +132 -0
- package/src/client.ts +390 -0
- package/src/components/approval.tsx +219 -0
- package/src/components/card.tsx +87 -0
- package/src/components/editor.tsx +392 -0
- package/src/components/icons.tsx +14 -0
- package/src/components/panel.tsx +591 -0
- package/src/components/shared.tsx +207 -0
- package/src/components/version-diff.tsx +266 -0
- package/src/components/view.tsx +199 -0
- package/src/context.ts +82 -0
- package/src/controls.tsx +587 -0
- package/src/download.ts +23 -0
- package/src/editor-session.ts +738 -0
- package/src/hooks.ts +411 -0
- package/src/i18n.ts +217 -0
- package/src/index.ts +160 -0
- package/src/kit-css.ts +5 -0
- package/src/line-diff.ts +111 -0
- package/src/partial-json.ts +160 -0
- package/src/provider.tsx +176 -0
- package/src/styles.ts +222 -0
- package/src/toolbar.tsx +536 -0
package/src/toolbar.tsx
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { COLOR_TOKENS, type ColorToken, type Theme } from '@artkit-ai/core';
|
|
2
|
+
import { Check, ChevronDown, type LucideIcon, X } from 'lucide-react';
|
|
3
|
+
import {
|
|
4
|
+
type KeyboardEvent,
|
|
5
|
+
type MouseEvent,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useId,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from 'react';
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* Standard editor toolbar shared by every kind editor (document, sheet, slides, diagram) so all of them
|
|
16
|
+
* look and behave the same: icon buttons with tooltips and shortcuts, grouped with dividers, selects,
|
|
17
|
+
* dropdown menus and theme color palettes. Keyboard model follows the WAI-ARIA toolbar pattern: one tab
|
|
18
|
+
* stop, arrows/Home/End move between controls, menus open with Enter/Space/ArrowDown and close with Escape.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const ITEM = '[data-ak-tb-item]:not([disabled])';
|
|
22
|
+
|
|
23
|
+
function cx(...classes: (string | false | null | undefined)[]): string {
|
|
24
|
+
return classes.filter(Boolean).join(' ');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Keeps the editor selection (text, cells) when a toolbar control is clicked with the mouse. */
|
|
28
|
+
function keepSelection(event: MouseEvent) {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ToolbarProps {
|
|
33
|
+
label: string;
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
className?: string;
|
|
36
|
+
/** Stick to the top of the scrolling editor (default true). */
|
|
37
|
+
sticky?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function Toolbar({ label, children, className, sticky = true }: ToolbarProps) {
|
|
41
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
42
|
+
// Roving tab index: only the active control is tabbable.
|
|
43
|
+
const sync = useCallback((focusIndex?: number) => {
|
|
44
|
+
const items = [...(ref.current?.querySelectorAll<HTMLElement>(ITEM) ?? [])].filter(
|
|
45
|
+
(item) => !item.closest('[data-ak-tb-popover]'),
|
|
46
|
+
);
|
|
47
|
+
const active = focusIndex ?? Math.max(0, items.indexOf(document.activeElement as HTMLElement));
|
|
48
|
+
items.forEach((item, index) => {
|
|
49
|
+
item.tabIndex = index === active ? 0 : -1;
|
|
50
|
+
});
|
|
51
|
+
return items;
|
|
52
|
+
}, []);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
sync(0);
|
|
55
|
+
});
|
|
56
|
+
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
57
|
+
if ((event.target as HTMLElement).closest('[data-ak-tb-popover]')) return;
|
|
58
|
+
if (
|
|
59
|
+
(event.target as HTMLElement).tagName === 'SELECT' &&
|
|
60
|
+
(event.key === 'ArrowUp' || event.key === 'ArrowDown')
|
|
61
|
+
)
|
|
62
|
+
return;
|
|
63
|
+
const items = sync();
|
|
64
|
+
const current = items.indexOf(event.target as HTMLElement);
|
|
65
|
+
if (current < 0) return;
|
|
66
|
+
let next = -1;
|
|
67
|
+
if (event.key === 'ArrowRight') next = (current + 1) % items.length;
|
|
68
|
+
else if (event.key === 'ArrowLeft') next = (current - 1 + items.length) % items.length;
|
|
69
|
+
else if (event.key === 'Home') next = 0;
|
|
70
|
+
else if (event.key === 'End') next = items.length - 1;
|
|
71
|
+
if (next < 0) return;
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
sync(next);
|
|
74
|
+
items[next]?.focus();
|
|
75
|
+
};
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
ref={ref}
|
|
79
|
+
role="toolbar"
|
|
80
|
+
aria-label={label}
|
|
81
|
+
className={cx('ak-tb', sticky && 'ak-tb--sticky', className)}
|
|
82
|
+
onKeyDown={onKeyDown}
|
|
83
|
+
onFocus={(event) => {
|
|
84
|
+
if ((event.target as HTMLElement).matches(ITEM))
|
|
85
|
+
sync([...sync()].indexOf(event.target as HTMLElement));
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function ToolbarGroup({ label, children }: { label?: string; children: ReactNode }) {
|
|
94
|
+
return (
|
|
95
|
+
<fieldset className="ak-tb-group">
|
|
96
|
+
{label ? <legend className="ak-tb-sr-only">{label}</legend> : null}
|
|
97
|
+
{children}
|
|
98
|
+
</fieldset>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function ToolbarSpacer() {
|
|
103
|
+
return <span className="ak-tb-spacer" aria-hidden="true" />;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function tooltip(label: string, shortcut?: string) {
|
|
107
|
+
return shortcut ? `${label} (${shortcut})` : label;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface ToolbarButtonProps {
|
|
111
|
+
icon?: LucideIcon;
|
|
112
|
+
label: string;
|
|
113
|
+
onClick(): void;
|
|
114
|
+
/** Toggle state (`aria-pressed`). */
|
|
115
|
+
pressed?: boolean;
|
|
116
|
+
disabled?: boolean;
|
|
117
|
+
/** Shown in the tooltip, e.g. "Ctrl+B". */
|
|
118
|
+
shortcut?: string;
|
|
119
|
+
/** Show the text next to the icon (actions that need a word, e.g. "Present"). */
|
|
120
|
+
showLabel?: boolean;
|
|
121
|
+
variant?: 'default' | 'primary' | 'danger';
|
|
122
|
+
/** Keep the editor selection on mouse down (default true). */
|
|
123
|
+
keepFocus?: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function ToolbarButton({
|
|
127
|
+
icon: Icon,
|
|
128
|
+
label,
|
|
129
|
+
onClick,
|
|
130
|
+
pressed,
|
|
131
|
+
disabled,
|
|
132
|
+
shortcut,
|
|
133
|
+
showLabel,
|
|
134
|
+
variant = 'default',
|
|
135
|
+
keepFocus = true,
|
|
136
|
+
}: ToolbarButtonProps) {
|
|
137
|
+
return (
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
data-ak-tb-item=""
|
|
141
|
+
className={cx('ak-tb-button', (showLabel || !Icon) && 'ak-tb-button--text')}
|
|
142
|
+
data-variant={variant === 'default' ? undefined : variant}
|
|
143
|
+
aria-label={showLabel ? undefined : label}
|
|
144
|
+
title={tooltip(label, shortcut)}
|
|
145
|
+
aria-pressed={pressed}
|
|
146
|
+
aria-keyshortcuts={shortcut?.replace(/Ctrl/g, 'Control')}
|
|
147
|
+
disabled={disabled}
|
|
148
|
+
onMouseDown={keepFocus ? keepSelection : undefined}
|
|
149
|
+
onClick={onClick}
|
|
150
|
+
>
|
|
151
|
+
{Icon ? <Icon aria-hidden="true" /> : null}
|
|
152
|
+
{showLabel || !Icon ? <span>{label}</span> : null}
|
|
153
|
+
</button>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface ToolbarOption<T extends string> {
|
|
158
|
+
value: T;
|
|
159
|
+
label: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface ToolbarSelectProps<T extends string> {
|
|
163
|
+
label: string;
|
|
164
|
+
value: T;
|
|
165
|
+
options: readonly ToolbarOption<T>[];
|
|
166
|
+
onChange(value: T): void;
|
|
167
|
+
disabled?: boolean;
|
|
168
|
+
/** Fixed width in px for stable layouts. */
|
|
169
|
+
width?: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function ToolbarSelect<T extends string>({
|
|
173
|
+
label,
|
|
174
|
+
value,
|
|
175
|
+
options,
|
|
176
|
+
onChange,
|
|
177
|
+
disabled,
|
|
178
|
+
width,
|
|
179
|
+
}: ToolbarSelectProps<T>) {
|
|
180
|
+
return (
|
|
181
|
+
<select
|
|
182
|
+
data-ak-tb-item=""
|
|
183
|
+
className="ak-tb-select"
|
|
184
|
+
aria-label={label}
|
|
185
|
+
title={label}
|
|
186
|
+
value={value}
|
|
187
|
+
disabled={disabled}
|
|
188
|
+
style={width ? { width } : undefined}
|
|
189
|
+
onChange={(event) => onChange(event.target.value as T)}
|
|
190
|
+
>
|
|
191
|
+
{options.map((option) => (
|
|
192
|
+
<option key={option.value} value={option.value}>
|
|
193
|
+
{option.label}
|
|
194
|
+
</option>
|
|
195
|
+
))}
|
|
196
|
+
</select>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Popover anchored under a toolbar trigger; closes on Escape, outside click and selection. */
|
|
201
|
+
function usePopover() {
|
|
202
|
+
const [open, setOpen] = useState(false);
|
|
203
|
+
const root = useRef<HTMLSpanElement>(null);
|
|
204
|
+
const trigger = useRef<HTMLButtonElement>(null);
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (!open) return;
|
|
207
|
+
const onDown = (event: globalThis.MouseEvent) => {
|
|
208
|
+
if (!root.current?.contains(event.target as Node)) setOpen(false);
|
|
209
|
+
};
|
|
210
|
+
document.addEventListener('mousedown', onDown);
|
|
211
|
+
return () => document.removeEventListener('mousedown', onDown);
|
|
212
|
+
}, [open]);
|
|
213
|
+
const close = (refocus = true) => {
|
|
214
|
+
setOpen(false);
|
|
215
|
+
if (refocus) trigger.current?.focus();
|
|
216
|
+
};
|
|
217
|
+
return { open, setOpen, root, trigger, close };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function focusItems(container: HTMLElement | null, index: number | 'first' | 'last') {
|
|
221
|
+
const items = [...(container?.querySelectorAll<HTMLElement>('[role^="menuitem"]:not([disabled])') ?? [])];
|
|
222
|
+
if (items.length === 0) return;
|
|
223
|
+
const target =
|
|
224
|
+
index === 'first' ? 0 : index === 'last' ? items.length - 1 : (index + items.length) % items.length;
|
|
225
|
+
items[target]?.focus();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function menuKeys(event: KeyboardEvent<HTMLElement>, close: () => void) {
|
|
229
|
+
const items = [...event.currentTarget.querySelectorAll<HTMLElement>('[role^="menuitem"]:not([disabled])')];
|
|
230
|
+
const current = items.indexOf(document.activeElement as HTMLElement);
|
|
231
|
+
const columns = Number(event.currentTarget.dataset.columns ?? 1);
|
|
232
|
+
const step = {
|
|
233
|
+
ArrowDown: columns,
|
|
234
|
+
ArrowUp: -columns,
|
|
235
|
+
ArrowRight: columns > 1 ? 1 : 0,
|
|
236
|
+
ArrowLeft: columns > 1 ? -1 : 0,
|
|
237
|
+
}[event.key as 'ArrowDown'];
|
|
238
|
+
if (event.key === 'Escape') {
|
|
239
|
+
event.preventDefault();
|
|
240
|
+
event.stopPropagation();
|
|
241
|
+
close();
|
|
242
|
+
} else if (step) {
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
focusItems(event.currentTarget, current + step);
|
|
245
|
+
} else if (event.key === 'Home' || event.key === 'End') {
|
|
246
|
+
event.preventDefault();
|
|
247
|
+
focusItems(event.currentTarget, event.key === 'Home' ? 'first' : 'last');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface ToolbarMenuItem {
|
|
252
|
+
key: string;
|
|
253
|
+
label: string;
|
|
254
|
+
icon?: LucideIcon;
|
|
255
|
+
onSelect(): void;
|
|
256
|
+
disabled?: boolean;
|
|
257
|
+
/** Checked state for option menus (shows a check mark). */
|
|
258
|
+
checked?: boolean;
|
|
259
|
+
shortcut?: string;
|
|
260
|
+
danger?: boolean;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface ToolbarMenuProps {
|
|
264
|
+
icon?: LucideIcon;
|
|
265
|
+
label: string;
|
|
266
|
+
items: readonly (ToolbarMenuItem | 'separator')[];
|
|
267
|
+
disabled?: boolean;
|
|
268
|
+
showLabel?: boolean;
|
|
269
|
+
keepFocus?: boolean;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function ToolbarMenu({
|
|
273
|
+
icon: Icon,
|
|
274
|
+
label,
|
|
275
|
+
items,
|
|
276
|
+
disabled,
|
|
277
|
+
showLabel,
|
|
278
|
+
keepFocus = true,
|
|
279
|
+
}: ToolbarMenuProps) {
|
|
280
|
+
const { open, setOpen, root, trigger, close } = usePopover();
|
|
281
|
+
const list = useRef<HTMLDivElement>(null);
|
|
282
|
+
const id = useId();
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
if (open) focusItems(list.current, 'first');
|
|
285
|
+
}, [open]);
|
|
286
|
+
const hasChecks = items.some((item) => item !== 'separator' && item.checked !== undefined);
|
|
287
|
+
return (
|
|
288
|
+
<span ref={root} className="ak-tb-anchor">
|
|
289
|
+
<button
|
|
290
|
+
ref={trigger}
|
|
291
|
+
type="button"
|
|
292
|
+
data-ak-tb-item=""
|
|
293
|
+
className={cx('ak-tb-button', 'ak-tb-button--menu', (showLabel || !Icon) && 'ak-tb-button--text')}
|
|
294
|
+
aria-label={showLabel ? undefined : label}
|
|
295
|
+
title={label}
|
|
296
|
+
aria-haspopup="menu"
|
|
297
|
+
aria-expanded={open}
|
|
298
|
+
aria-controls={open ? id : undefined}
|
|
299
|
+
disabled={disabled}
|
|
300
|
+
onMouseDown={keepFocus ? keepSelection : undefined}
|
|
301
|
+
onClick={() => setOpen(!open)}
|
|
302
|
+
onKeyDown={(event) => {
|
|
303
|
+
if (event.key === 'ArrowDown' && !open) {
|
|
304
|
+
event.preventDefault();
|
|
305
|
+
setOpen(true);
|
|
306
|
+
}
|
|
307
|
+
}}
|
|
308
|
+
>
|
|
309
|
+
{Icon ? <Icon aria-hidden="true" /> : null}
|
|
310
|
+
{showLabel || !Icon ? <span>{label}</span> : null}
|
|
311
|
+
<ChevronDown aria-hidden="true" className="ak-tb-caret" />
|
|
312
|
+
</button>
|
|
313
|
+
{open ? (
|
|
314
|
+
<div
|
|
315
|
+
ref={list}
|
|
316
|
+
id={id}
|
|
317
|
+
role="menu"
|
|
318
|
+
aria-label={label}
|
|
319
|
+
className="ak-tb-popover"
|
|
320
|
+
data-ak-tb-popover=""
|
|
321
|
+
onKeyDown={(event) => menuKeys(event, () => close())}
|
|
322
|
+
>
|
|
323
|
+
{items.map((item, index) =>
|
|
324
|
+
item === 'separator' ? (
|
|
325
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: separators have no identity.
|
|
326
|
+
<hr key={`sep-${index}`} className="ak-tb-separator" />
|
|
327
|
+
) : (
|
|
328
|
+
<button
|
|
329
|
+
key={item.key}
|
|
330
|
+
type="button"
|
|
331
|
+
{...(item.checked === undefined
|
|
332
|
+
? { role: 'menuitem' }
|
|
333
|
+
: { role: 'menuitemradio', 'aria-checked': item.checked })}
|
|
334
|
+
tabIndex={-1}
|
|
335
|
+
className="ak-tb-menu-item"
|
|
336
|
+
data-danger={item.danger ? '' : undefined}
|
|
337
|
+
disabled={item.disabled}
|
|
338
|
+
onMouseDown={keepFocus ? keepSelection : undefined}
|
|
339
|
+
onClick={() => {
|
|
340
|
+
close(!keepFocus);
|
|
341
|
+
item.onSelect();
|
|
342
|
+
}}
|
|
343
|
+
>
|
|
344
|
+
{hasChecks ? (
|
|
345
|
+
<span className="ak-tb-check" aria-hidden="true">
|
|
346
|
+
{item.checked ? <Check /> : null}
|
|
347
|
+
</span>
|
|
348
|
+
) : null}
|
|
349
|
+
{item.icon ? <item.icon aria-hidden="true" /> : null}
|
|
350
|
+
<span className="ak-tb-menu-label">{item.label}</span>
|
|
351
|
+
{item.shortcut ? <kbd className="ak-tb-kbd">{item.shortcut}</kbd> : null}
|
|
352
|
+
</button>
|
|
353
|
+
),
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
) : null}
|
|
357
|
+
</span>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export interface ToolbarColorOption {
|
|
362
|
+
/** Theme token or hex. */
|
|
363
|
+
value: string;
|
|
364
|
+
label: string;
|
|
365
|
+
/** Hex used to paint the swatch. */
|
|
366
|
+
swatch: string;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Swatches for the theme color tokens (labels in pt-BR or English). */
|
|
370
|
+
export function themeSwatches(theme: Theme, locale = 'en'): ToolbarColorOption[] {
|
|
371
|
+
const pt = locale.toLowerCase().startsWith('pt');
|
|
372
|
+
const names: Record<ColorToken, [string, string]> = {
|
|
373
|
+
text: ['Text', 'Texto'],
|
|
374
|
+
muted: ['Muted', 'Suave'],
|
|
375
|
+
accent: ['Accent', 'Destaque'],
|
|
376
|
+
background: ['Background', 'Fundo'],
|
|
377
|
+
surface: ['Surface', 'Superfície'],
|
|
378
|
+
info: ['Info', 'Informação'],
|
|
379
|
+
warning: ['Warning', 'Atenção'],
|
|
380
|
+
success: ['Success', 'Sucesso'],
|
|
381
|
+
danger: ['Danger', 'Perigo'],
|
|
382
|
+
};
|
|
383
|
+
return COLOR_TOKENS.map((token) => ({
|
|
384
|
+
value: token,
|
|
385
|
+
label: names[token][pt ? 1 : 0],
|
|
386
|
+
swatch: theme.colors[token],
|
|
387
|
+
}));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export interface ToolbarColorProps {
|
|
391
|
+
icon: LucideIcon;
|
|
392
|
+
label: string;
|
|
393
|
+
value?: string | null;
|
|
394
|
+
colors: readonly ToolbarColorOption[];
|
|
395
|
+
onChange(value: string | null): void;
|
|
396
|
+
/** Label of the "no color" option; omitted when not allowed. */
|
|
397
|
+
noneLabel?: string;
|
|
398
|
+
disabled?: boolean;
|
|
399
|
+
keepFocus?: boolean;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function ToolbarColor({
|
|
403
|
+
icon: Icon,
|
|
404
|
+
label,
|
|
405
|
+
value,
|
|
406
|
+
colors,
|
|
407
|
+
onChange,
|
|
408
|
+
noneLabel,
|
|
409
|
+
disabled,
|
|
410
|
+
keepFocus = true,
|
|
411
|
+
}: ToolbarColorProps) {
|
|
412
|
+
const { open, setOpen, root, trigger, close } = usePopover();
|
|
413
|
+
const grid = useRef<HTMLDivElement>(null);
|
|
414
|
+
const id = useId();
|
|
415
|
+
const current = colors.find((color) => color.value === value);
|
|
416
|
+
useEffect(() => {
|
|
417
|
+
if (open) focusItems(grid.current, 'first');
|
|
418
|
+
}, [open]);
|
|
419
|
+
return (
|
|
420
|
+
<span ref={root} className="ak-tb-anchor">
|
|
421
|
+
<button
|
|
422
|
+
ref={trigger}
|
|
423
|
+
type="button"
|
|
424
|
+
data-ak-tb-item=""
|
|
425
|
+
className="ak-tb-button ak-tb-button--menu ak-tb-button--color"
|
|
426
|
+
aria-label={`${label}${current ? `: ${current.label}` : ''}`}
|
|
427
|
+
title={label}
|
|
428
|
+
aria-haspopup="menu"
|
|
429
|
+
aria-expanded={open}
|
|
430
|
+
aria-controls={open ? id : undefined}
|
|
431
|
+
disabled={disabled}
|
|
432
|
+
onMouseDown={keepFocus ? keepSelection : undefined}
|
|
433
|
+
onClick={() => setOpen(!open)}
|
|
434
|
+
>
|
|
435
|
+
<span className="ak-tb-color-icon">
|
|
436
|
+
<Icon aria-hidden="true" />
|
|
437
|
+
<span className="ak-tb-color-bar" style={{ background: current?.swatch ?? 'transparent' }} />
|
|
438
|
+
</span>
|
|
439
|
+
<ChevronDown aria-hidden="true" className="ak-tb-caret" />
|
|
440
|
+
</button>
|
|
441
|
+
{open ? (
|
|
442
|
+
<div
|
|
443
|
+
ref={grid}
|
|
444
|
+
id={id}
|
|
445
|
+
role="menu"
|
|
446
|
+
aria-label={label}
|
|
447
|
+
className="ak-tb-popover ak-tb-palette"
|
|
448
|
+
data-ak-tb-popover=""
|
|
449
|
+
data-columns="5"
|
|
450
|
+
onKeyDown={(event) => menuKeys(event, () => close())}
|
|
451
|
+
>
|
|
452
|
+
{colors.map((color) => (
|
|
453
|
+
<button
|
|
454
|
+
key={color.value}
|
|
455
|
+
type="button"
|
|
456
|
+
role="menuitemradio"
|
|
457
|
+
aria-checked={color.value === value}
|
|
458
|
+
aria-label={color.label}
|
|
459
|
+
title={color.label}
|
|
460
|
+
tabIndex={-1}
|
|
461
|
+
className="ak-tb-swatch"
|
|
462
|
+
style={{ background: color.swatch }}
|
|
463
|
+
onMouseDown={keepFocus ? keepSelection : undefined}
|
|
464
|
+
onClick={() => {
|
|
465
|
+
close(!keepFocus);
|
|
466
|
+
onChange(color.value);
|
|
467
|
+
}}
|
|
468
|
+
/>
|
|
469
|
+
))}
|
|
470
|
+
{noneLabel ? (
|
|
471
|
+
<button
|
|
472
|
+
type="button"
|
|
473
|
+
role="menuitemradio"
|
|
474
|
+
aria-checked={!value}
|
|
475
|
+
tabIndex={-1}
|
|
476
|
+
className="ak-tb-menu-item ak-tb-palette-none"
|
|
477
|
+
onMouseDown={keepFocus ? keepSelection : undefined}
|
|
478
|
+
onClick={() => {
|
|
479
|
+
close(!keepFocus);
|
|
480
|
+
onChange(null);
|
|
481
|
+
}}
|
|
482
|
+
>
|
|
483
|
+
<X aria-hidden="true" />
|
|
484
|
+
<span className="ak-tb-menu-label">{noneLabel}</span>
|
|
485
|
+
</button>
|
|
486
|
+
) : null}
|
|
487
|
+
</div>
|
|
488
|
+
) : null}
|
|
489
|
+
</span>
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Toolbar styles (Mouts design system); embedded in `artkitCss` and in every kind editor CSS. */
|
|
494
|
+
export const toolbarCss = `
|
|
495
|
+
.ak-tb{display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:6px;background:var(--ak-color-background,#fff);color:var(--ak-color-text,#1d2f3c);border:1px solid var(--ak-color-border,#dde0e2);border-radius:var(--ak-radius-md,8px);font-family:var(--ak-font-body,"Sora",system-ui,sans-serif);font-size:13px;line-height:1}
|
|
496
|
+
.ak-tb--sticky{position:sticky;top:0;z-index:5}
|
|
497
|
+
.ak-tb-group{display:inline-flex;align-items:center;gap:2px;margin:0;padding:0 0 0 6px;border:0;border-left:1px solid var(--ak-color-border,#dde0e2);min-inline-size:0}
|
|
498
|
+
.ak-tb-group:first-child{padding-left:0;border-left:0}
|
|
499
|
+
.ak-tb-spacer{flex:1}
|
|
500
|
+
.ak-tb-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
|
|
501
|
+
.ak-tb-button{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-width:32px;height:32px;padding:0 6px;border:1px solid transparent;border-radius:var(--ak-radius-sm,6px);background:none;color:var(--ak-color-muted,#56636d);font:inherit;font-weight:600;cursor:pointer;white-space:nowrap;transition:background-color .15s,color .15s,border-color .15s}
|
|
502
|
+
.ak-tb-button svg{width:16px;height:16px;flex-shrink:0}
|
|
503
|
+
.ak-tb-button--text{padding:0 10px;color:var(--ak-color-text,#1d2f3c)}
|
|
504
|
+
.ak-tb-button:hover:not(:disabled){background:var(--ak-color-hover,#f2f3f4);color:var(--ak-color-text,#1d2f3c)}
|
|
505
|
+
.ak-tb-button:active:not(:disabled){background:var(--ak-color-active,#e8eaec)}
|
|
506
|
+
.ak-tb-button[aria-pressed="true"],.ak-tb-button[aria-expanded="true"]{background:var(--ak-color-fill-accent,#e7f2ef);color:var(--ak-color-accent,#0f7a5a);border-color:var(--ak-color-accent,#0f7a5a)}
|
|
507
|
+
.ak-tb-button[data-variant="primary"]{background:var(--ak-gradient-button);color:var(--ak-color-on-primary,#1d2f3c);border-color:transparent}
|
|
508
|
+
.ak-tb-button[data-variant="primary"]:hover:not(:disabled){background:var(--ak-gradient-button-hover)}
|
|
509
|
+
.ak-tb-button[data-variant="danger"]{color:var(--ak-color-danger,#9f343a)}
|
|
510
|
+
.ak-tb-button[data-variant="danger"]:hover:not(:disabled){background:var(--ak-color-fill-danger,#f5ebeb)}
|
|
511
|
+
.ak-tb-button:disabled,.ak-tb-select:disabled{opacity:.4;cursor:not-allowed}
|
|
512
|
+
.ak-tb-button:focus-visible,.ak-tb-select:focus-visible,.ak-tb-menu-item:focus-visible,.ak-tb-swatch:focus-visible{outline:2px solid var(--ak-color-focus,#0f7a5a);outline-offset:2px}
|
|
513
|
+
.ak-tb-button--menu{padding:0 4px 0 6px;gap:2px}
|
|
514
|
+
.ak-tb-button--menu.ak-tb-button--text{padding:0 6px 0 10px}
|
|
515
|
+
.ak-tb-button .ak-tb-caret{width:12px;height:12px;opacity:.7}
|
|
516
|
+
.ak-tb-color-icon{position:relative;display:inline-flex;flex-direction:column;align-items:center;gap:1px}
|
|
517
|
+
.ak-tb-color-bar{display:block;width:16px;height:3px;border-radius:2px;border:1px solid var(--ak-color-border,#dde0e2)}
|
|
518
|
+
.ak-tb-select{height:32px;max-width:200px;padding:0 8px;font:inherit;font-size:13px;color:var(--ak-color-text,#1d2f3c);background:var(--ak-color-background,#fff);border:1px solid var(--ak-color-border-strong,#8e979d);border-radius:var(--ak-radius-sm,6px);cursor:pointer}
|
|
519
|
+
.ak-tb-anchor{position:relative;display:inline-flex}
|
|
520
|
+
.ak-tb-popover{position:absolute;top:calc(100% + 4px);left:0;z-index:30;display:flex;flex-direction:column;min-width:200px;max-height:360px;overflow:auto;padding:4px;background:var(--ak-color-background,#fff);border:1px solid var(--ak-color-border-strong,#8e979d);border-radius:var(--ak-radius-md,8px)}
|
|
521
|
+
.ak-tb-menu-item{display:flex;align-items:center;gap:8px;width:100%;min-height:32px;padding:0 10px;border:0;border-radius:var(--ak-radius-sm,6px);background:none;color:var(--ak-color-text,#1d2f3c);font:inherit;font-size:13px;text-align:left;cursor:pointer}
|
|
522
|
+
.ak-tb-menu-item svg{width:16px;height:16px;flex-shrink:0;color:var(--ak-color-muted,#56636d)}
|
|
523
|
+
.ak-tb-menu-item:hover:not(:disabled),.ak-tb-menu-item:focus{background:var(--ak-color-hover,#f2f3f4)}
|
|
524
|
+
.ak-tb-menu-item:disabled{opacity:.4;cursor:not-allowed}
|
|
525
|
+
.ak-tb-menu-item[data-danger]{color:var(--ak-color-danger,#9f343a)}
|
|
526
|
+
.ak-tb-menu-item[aria-checked="true"]{color:var(--ak-color-accent,#0f7a5a);font-weight:600}
|
|
527
|
+
.ak-tb-menu-label{flex:1}
|
|
528
|
+
.ak-tb-check{display:inline-flex;width:16px}
|
|
529
|
+
.ak-tb-check svg{color:var(--ak-color-accent,#0f7a5a)}
|
|
530
|
+
.ak-tb-kbd{font-family:var(--ak-font-mono,ui-monospace,monospace);font-size:11px;color:var(--ak-color-muted,#56636d)}
|
|
531
|
+
.ak-tb-separator{width:100%;height:1px;margin:4px 0;border:0;background:var(--ak-color-border,#dde0e2)}
|
|
532
|
+
.ak-tb-palette{display:grid;grid-template-columns:repeat(5,28px);gap:6px;min-width:0;padding:8px}
|
|
533
|
+
.ak-tb-swatch{width:28px;height:28px;padding:0;border:1px solid var(--ak-color-border-strong,#8e979d);border-radius:var(--ak-radius-sm,6px);cursor:pointer}
|
|
534
|
+
.ak-tb-swatch[aria-checked="true"]{box-shadow:inset 0 0 0 2px var(--ak-color-background,#fff);border:2px solid var(--ak-color-accent,#0f7a5a)}
|
|
535
|
+
.ak-tb-palette-none{grid-column:1/-1}
|
|
536
|
+
`;
|