@eocrm/design-system 0.8.0 → 0.8.2
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/AI-PRIMER.md +1 -1
- package/dist/components/DashboardCanvas/DashboardCanvas.js +28 -5
- package/dist/components/LinkCard/LinkCard.css +4 -0
- package/dist/components/List/List.css +4 -0
- package/dist/components/List/List.d.ts +5 -4
- package/dist/components/List/List.js +6 -0
- package/dist/components/PersonDisplay/PersonDisplay.js +1 -0
- package/dist/components/RichText/RichText.d.ts +11 -0
- package/dist/components/RichText/RichText.js +3 -2
- package/dist/components/RichText/RichTextAttachment.css +14 -0
- package/dist/components/RichText/RichTextAttachment.d.ts +12 -1
- package/dist/components/RichText/RichTextAttachment.js +29 -12
- package/dist/components/RichText/RichTextAttachment.module.js +4 -2
- package/dist/components/RichText/engine/renderDoc.d.ts +6 -0
- package/dist/components/RichText/engine/renderDoc.js +8 -8
- package/dist/components/RichText/index.d.ts +1 -0
- package/dist/components/Text/Text.css +8 -0
- package/dist/components/Text/Text.d.ts +12 -3
- package/dist/components/Text/Text.js +4 -2
- package/dist/components/Text/Text.module.js +5 -1
- package/dist/components/Timeline/Timeline.css +17 -6
- package/dist/i18n/en.js +1 -0
- package/dist/i18n/messages.d.ts +7 -0
- package/dist/i18n/ru.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/styles.css +47 -6
- package/docs/components/HighlightedText.md +1 -1
- package/docs/components/Lightbox.md +1 -1
- package/docs/components/LinkCard.md +2 -0
- package/docs/components/List.md +5 -4
- package/docs/components/RichText.md +11 -0
- package/docs/components/Text.md +2 -1
- package/docs/components/Timeline.md +1 -0
- package/package.json +2 -2
package/AI-PRIMER.md
CHANGED
|
@@ -96,7 +96,7 @@ Each `<Name>` below is documented in `docs/components/<Name>.md`.
|
|
|
96
96
|
- `ImageCrop` — controlled image cropper
|
|
97
97
|
- `Card` — bordered container
|
|
98
98
|
- `DefinitionList` — semantic key/value pairs (dl / dt / dd)
|
|
99
|
-
- `List` — bulleted / numbered
|
|
99
|
+
- `List` — bulleted / numbered / `plain` (marker-less, for interactive rows) lists (ul / ol), optional badge step numbers
|
|
100
100
|
- `Stack` — vertical layout
|
|
101
101
|
- `Page` — page-root layout primitive
|
|
102
102
|
- `Cluster` — horizontal layout that wraps
|
|
@@ -150,12 +150,21 @@ var DashboardCanvas = forwardRef(function DashboardCanvas({ value, onChange, ren
|
|
|
150
150
|
};
|
|
151
151
|
const isPortalEvent = (event) => !(event.target instanceof Node) || !rootRef.current?.contains(event.target);
|
|
152
152
|
const underOpenOverlay = () => overlayStack.topDepth() > 0 && !rootRef.current?.closest(OVERLAY_HOST_SELECTOR);
|
|
153
|
-
const
|
|
153
|
+
const armRelease = (drag) => {
|
|
154
154
|
const release = (event) => {
|
|
155
155
|
if (event.pointerId !== drag.pointerId) return;
|
|
156
156
|
window.removeEventListener("pointerup", release, true);
|
|
157
157
|
window.removeEventListener("pointercancel", release, true);
|
|
158
|
-
if (dragRef.current
|
|
158
|
+
if (dragRef.current !== drag) return;
|
|
159
|
+
if (drag.kind !== "resize" && !drag.moved) {
|
|
160
|
+
dragRef.current = null;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
setTimeout(() => {
|
|
164
|
+
if (dragRef.current !== drag) return;
|
|
165
|
+
dragRef.current = null;
|
|
166
|
+
setLive(null);
|
|
167
|
+
});
|
|
159
168
|
};
|
|
160
169
|
window.addEventListener("pointerup", release, true);
|
|
161
170
|
window.addEventListener("pointercancel", release, true);
|
|
@@ -185,13 +194,14 @@ var DashboardCanvas = forwardRef(function DashboardCanvas({ value, onChange, ren
|
|
|
185
194
|
preview: null
|
|
186
195
|
};
|
|
187
196
|
dragRef.current = drag;
|
|
188
|
-
|
|
197
|
+
armRelease(drag);
|
|
189
198
|
};
|
|
190
199
|
const onResizePointerDown = (event, placement, container, edge) => {
|
|
191
200
|
if (!editingEnabled || event.button !== 0 || dragRef.current || pick) return;
|
|
192
201
|
if (isPortalEvent(event) || underOpenOverlay()) return;
|
|
193
202
|
event.stopPropagation();
|
|
194
|
-
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
const drag = {
|
|
195
205
|
kind: "resize",
|
|
196
206
|
pointerId: event.pointerId,
|
|
197
207
|
id: placement.id,
|
|
@@ -206,11 +216,14 @@ var DashboardCanvas = forwardRef(function DashboardCanvas({ value, onChange, ren
|
|
|
206
216
|
lastH: placement.h,
|
|
207
217
|
preview: null
|
|
208
218
|
};
|
|
219
|
+
dragRef.current = drag;
|
|
220
|
+
armRelease(drag);
|
|
209
221
|
capturePointer(rootRef.current, event.pointerId);
|
|
210
222
|
};
|
|
211
223
|
const onHeaderPointerDown = (event, sectionId) => {
|
|
212
224
|
if (!editingEnabled || event.button !== 0 || dragRef.current || pick) return;
|
|
213
225
|
if (isPortalEvent(event) || underOpenOverlay()) return;
|
|
226
|
+
event.preventDefault();
|
|
214
227
|
const fromIndex = value.sections.findIndex((section) => section.id === sectionId);
|
|
215
228
|
if (fromIndex === -1) return;
|
|
216
229
|
const drag = {
|
|
@@ -224,7 +237,7 @@ var DashboardCanvas = forwardRef(function DashboardCanvas({ value, onChange, ren
|
|
|
224
237
|
slot: fromIndex
|
|
225
238
|
};
|
|
226
239
|
dragRef.current = drag;
|
|
227
|
-
|
|
240
|
+
armRelease(drag);
|
|
228
241
|
};
|
|
229
242
|
const handleRootPointerMove = (event) => {
|
|
230
243
|
onPointerMoveProp?.(event);
|
|
@@ -234,6 +247,7 @@ var DashboardCanvas = forwardRef(function DashboardCanvas({ value, onChange, ren
|
|
|
234
247
|
if (!drag.moved) {
|
|
235
248
|
if (isPortalEvent(event) || Math.abs(event.clientX - drag.startX) + Math.abs(event.clientY - drag.startY) < DRAG_ACTIVATION_PX) return;
|
|
236
249
|
drag.moved = true;
|
|
250
|
+
window.getSelection()?.removeAllRanges();
|
|
237
251
|
capturePointer(rootRef.current, event.pointerId);
|
|
238
252
|
}
|
|
239
253
|
for (const entry of containersRef.current.values()) {
|
|
@@ -299,6 +313,7 @@ var DashboardCanvas = forwardRef(function DashboardCanvas({ value, onChange, ren
|
|
|
299
313
|
if (!drag.moved) {
|
|
300
314
|
if (isPortalEvent(event) || Math.abs(event.clientX - drag.startX) + Math.abs(event.clientY - drag.startY) < DRAG_ACTIVATION_PX) return;
|
|
301
315
|
drag.moved = true;
|
|
316
|
+
window.getSelection()?.removeAllRanges();
|
|
302
317
|
capturePointer(rootRef.current, event.pointerId);
|
|
303
318
|
}
|
|
304
319
|
let slot = 0;
|
|
@@ -491,6 +506,14 @@ var DashboardCanvas = forwardRef(function DashboardCanvas({ value, onChange, ren
|
|
|
491
506
|
};
|
|
492
507
|
const dragging = live != null;
|
|
493
508
|
useFloatingSurface(dragging || pick != null);
|
|
509
|
+
useEffect(() => {
|
|
510
|
+
const onSelectStart = (event) => {
|
|
511
|
+
const d = dragRef.current;
|
|
512
|
+
if (d && (d.kind === "resize" || d.moved)) event.preventDefault();
|
|
513
|
+
};
|
|
514
|
+
document.addEventListener("selectstart", onSelectStart);
|
|
515
|
+
return () => document.removeEventListener("selectstart", onSelectStart);
|
|
516
|
+
}, []);
|
|
494
517
|
useEffect(() => {
|
|
495
518
|
if (!dragging && !pick) return void 0;
|
|
496
519
|
const onKeyDown = (event) => {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
.LinkCard__linkCard__wrwBJ {
|
|
38
38
|
--link-card-elevation: var(--link-card-shadow);
|
|
39
39
|
display: block;
|
|
40
|
+
text-align: start;
|
|
40
41
|
background: var(--card-bg);
|
|
41
42
|
border: var(--border-width) solid var(--card-border-color);
|
|
42
43
|
border-radius: var(--card-radius);
|
|
@@ -47,6 +48,9 @@
|
|
|
47
48
|
cursor: pointer;
|
|
48
49
|
transition: var(--link-card-transition);
|
|
49
50
|
}
|
|
51
|
+
.LinkCard__linkCard__wrwBJ:where(button) {
|
|
52
|
+
width: 100%;
|
|
53
|
+
}
|
|
50
54
|
|
|
51
55
|
.LinkCard__linkCard__wrwBJ:hover {
|
|
52
56
|
--link-card-elevation: var(--link-card-hover-shadow);
|
|
@@ -3,8 +3,9 @@ import { type HTMLAttributes, type ReactNode } from 'react';
|
|
|
3
3
|
* Marker style.
|
|
4
4
|
* - `'bullet'` (default) — a dot per item, rendered in a `<ul>`.
|
|
5
5
|
* - `'number'` — 1, 2, 3 per item, rendered in an `<ol>`.
|
|
6
|
+
* - `'plain'` — no marker, rendered in a `<ul>`; for rows of interactive items (buttons, links) that still need list semantics. `numberStyle` and `size` do not apply.
|
|
6
7
|
*/
|
|
7
|
-
export type ListVariant = 'bullet' | 'number';
|
|
8
|
+
export type ListVariant = 'bullet' | 'number' | 'plain';
|
|
8
9
|
/**
|
|
9
10
|
* Space between items.
|
|
10
11
|
* - `'xs'` — `var(--space-1)`; dense reference lists.
|
|
@@ -15,7 +16,7 @@ export type ListGap = 'xs' | 'sm' | 'md';
|
|
|
15
16
|
/**
|
|
16
17
|
* How `variant="number"` draws its numbers.
|
|
17
18
|
* - `'plain'` (default) — a muted figure.
|
|
18
|
-
* - `'badge'` — an accent badge for numbered steps: circular at body size, a rounded square at `size="heading"`. Ignored for bullets
|
|
19
|
+
* - `'badge'` — an accent badge for numbered steps: circular at body size, a rounded square at `size="heading"`. Ignored for bullets and `'plain'`.
|
|
19
20
|
*/
|
|
20
21
|
export type ListNumberStyle = 'plain' | 'badge';
|
|
21
22
|
/**
|
|
@@ -29,9 +30,9 @@ export interface ListProps extends HTMLAttributes<HTMLElement> {
|
|
|
29
30
|
variant?: ListVariant;
|
|
30
31
|
/** Space between items. See `ListGap`. Default `'sm'`. */
|
|
31
32
|
gap?: ListGap;
|
|
32
|
-
/** Marker scale;
|
|
33
|
+
/** Marker scale; no effect on `variant="plain"`. Use `'heading'` when each item starts with a `Title size="xl"`. See `ListSize`. Default `'body'`. */
|
|
33
34
|
size?: ListSize;
|
|
34
|
-
/** Number look; only applies to `variant="number"
|
|
35
|
+
/** Number look; only applies to `variant="number"` (not `'plain'`). See `ListNumberStyle`. Default `'plain'`. */
|
|
35
36
|
numberStyle?: ListNumberStyle;
|
|
36
37
|
/** `List.Item` elements. A nested `List` goes inside a `List.Item`. */
|
|
37
38
|
children?: ReactNode;
|
|
@@ -40,6 +40,12 @@ var ListRoot = forwardRef(function ListRoot({ variant = "bullet", gap = "sm", si
|
|
|
40
40
|
/** One list entry; the marker is drawn here and hidden from assistive tech. */
|
|
41
41
|
var ListItem = forwardRef(function ListItem({ className, children, ...rest }, ref) {
|
|
42
42
|
const { variant, numberStyle } = useContext(Ctx);
|
|
43
|
+
if (variant === "plain") return /* @__PURE__ */ jsx("li", {
|
|
44
|
+
ref,
|
|
45
|
+
className: clsx(List_module_default.item, className),
|
|
46
|
+
...rest,
|
|
47
|
+
children
|
|
48
|
+
});
|
|
43
49
|
const inner = variant === "bullet" ? /* @__PURE__ */ jsx("span", { className: List_module_default.bullet }) : /* @__PURE__ */ jsx("span", { className: numberStyle === "badge" ? List_module_default.badge : List_module_default.number });
|
|
44
50
|
return /* @__PURE__ */ jsxs("li", {
|
|
45
51
|
ref,
|
|
@@ -132,6 +132,7 @@ var PersonDisplayOnBehalfOf = forwardRef(function PersonDisplayOnBehalfOf({ clas
|
|
|
132
132
|
}), " "] }),
|
|
133
133
|
/* @__PURE__ */ jsx(Text, {
|
|
134
134
|
as: "span",
|
|
135
|
+
align: "start",
|
|
135
136
|
className: PersonDisplay_module_default.onBehalfOfMember,
|
|
136
137
|
title: text,
|
|
137
138
|
...textProps,
|
|
@@ -2,6 +2,7 @@ import { type HTMLAttributes } from 'react';
|
|
|
2
2
|
import type { RichDoc } from './engine/model.js';
|
|
3
3
|
import type { RenderLink } from './engine/renderLink.js';
|
|
4
4
|
import type { RenderMention } from './engine/renderMention.js';
|
|
5
|
+
import type { RichTextImageActivateHandler } from './RichTextAttachment.js';
|
|
5
6
|
export interface RichTextProps extends HTMLAttributes<HTMLDivElement> {
|
|
6
7
|
/** The document to render. Build one with `emptyDoc()` / `createBlock()` or the transforms. */
|
|
7
8
|
value: RichDoc;
|
|
@@ -40,6 +41,16 @@ export interface RichTextProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
40
41
|
* pass a large value to effectively remove the cap.
|
|
41
42
|
*/
|
|
42
43
|
imageMaxHeight?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Make inline images keyboard-operable triggers (e.g. to open a `Lightbox`).
|
|
46
|
+
* When set, each ready image attachment renders inside a native
|
|
47
|
+
* `<button type="button">` named "Open image: <alt or file name>"; click,
|
|
48
|
+
* Enter and Space focus the button, then call this with `{ src, alt }` and
|
|
49
|
+
* it — so an overlay that restores focus on close (e.g. `Lightbox`) returns there. Unset: the plain `<img>`, unchanged.
|
|
50
|
+
* Pass a stable reference (`useCallback`): a new function each render
|
|
51
|
+
* rebuilds every attachment block.
|
|
52
|
+
*/
|
|
53
|
+
onImageActivate?: RichTextImageActivateHandler;
|
|
43
54
|
}
|
|
44
55
|
/**
|
|
45
56
|
* Read-only renderer for a `RichDoc` (paragraphs, headings, lists, quotes, code blocks, inline marks) on the in-house engine.
|
|
@@ -10,7 +10,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
10
10
|
* Read-only renderer for a `RichDoc` (paragraphs, headings, lists, quotes, code blocks, inline marks) on the in-house engine.
|
|
11
11
|
* @see docs/components/RichText.md
|
|
12
12
|
*/
|
|
13
|
-
var RichText = forwardRef(function RichText({ value, renderLink, renderMention, imageMaxHeight, className, style, ...props }, ref) {
|
|
13
|
+
var RichText = forwardRef(function RichText({ value, renderLink, renderMention, onImageActivate, imageMaxHeight, className, style, ...props }, ref) {
|
|
14
14
|
return /* @__PURE__ */ jsx("div", {
|
|
15
15
|
ref,
|
|
16
16
|
className: clsx(RichText_module_default.root, className),
|
|
@@ -18,7 +18,8 @@ var RichText = forwardRef(function RichText({ value, renderLink, renderMention,
|
|
|
18
18
|
...props,
|
|
19
19
|
children: renderDoc(value, {
|
|
20
20
|
renderLink,
|
|
21
|
-
renderMention
|
|
21
|
+
renderMention,
|
|
22
|
+
onImageActivate
|
|
22
23
|
})
|
|
23
24
|
});
|
|
24
25
|
});
|
|
@@ -48,4 +48,18 @@ figure[data-attachment][data-align=center] {
|
|
|
48
48
|
|
|
49
49
|
figure[data-attachment][data-align=right] {
|
|
50
50
|
text-align: right;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.RichTextAttachment__imageTrigger__5_u8P {
|
|
54
|
+
display: inline-block;
|
|
55
|
+
padding: 0;
|
|
56
|
+
border: 0;
|
|
57
|
+
background: none;
|
|
58
|
+
font: inherit;
|
|
59
|
+
line-height: 0;
|
|
60
|
+
cursor: zoom-in;
|
|
61
|
+
}
|
|
62
|
+
.RichTextAttachment__imageTrigger__5_u8P:focus-visible {
|
|
63
|
+
outline: var(--ring-width) solid var(--ring-accent);
|
|
64
|
+
outline-offset: var(--ring-offset);
|
|
51
65
|
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { Block } from './engine/model.js';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Called when a reader activates an inline image (click, Enter or Space) in the
|
|
4
|
+
* read-only `<RichText>`. `src` is the sanitized URL; `alt` is the block's alt
|
|
5
|
+
* as stored (may be `''` or absent). `trigger` is the DS-owned button, already
|
|
6
|
+
* focused when this runs, so an overlay that restores focus on close returns it there.
|
|
7
|
+
*/
|
|
8
|
+
export type RichTextImageActivateHandler = (image: {
|
|
9
|
+
src: string;
|
|
10
|
+
alt?: string;
|
|
11
|
+
}, trigger: HTMLElement) => void;
|
|
12
|
+
export declare function RichTextAttachment({ block, editable, onImageActivate, }: {
|
|
3
13
|
block: Block;
|
|
4
14
|
editable?: boolean;
|
|
15
|
+
onImageActivate?: RichTextImageActivateHandler;
|
|
5
16
|
}): import("react").JSX.Element;
|
|
@@ -11,7 +11,7 @@ import { useId } from "react";
|
|
|
11
11
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
12
12
|
import { RotateCw, X } from "lucide-react";
|
|
13
13
|
// src/components/RichText/RichTextAttachment.tsx
|
|
14
|
-
function RichTextAttachment({ block, editable }) {
|
|
14
|
+
function RichTextAttachment({ block, editable, onImageActivate }) {
|
|
15
15
|
const t = useTranslation();
|
|
16
16
|
const name = block.name ?? "";
|
|
17
17
|
const errorId = useId();
|
|
@@ -79,17 +79,34 @@ function RichTextAttachment({ block, editable }) {
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
const href = safeHref(block.src ?? "");
|
|
82
|
-
if (attachmentIsImage(block) && href)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
82
|
+
if (attachmentIsImage(block) && href) {
|
|
83
|
+
const img = /* @__PURE__ */ jsx("img", {
|
|
84
|
+
className: RichTextAttachment_module_default.attachmentImg,
|
|
85
|
+
src: href,
|
|
86
|
+
alt: onImageActivate ? "" : block.alt ?? name,
|
|
87
|
+
width: block.width,
|
|
88
|
+
height: block.height,
|
|
89
|
+
onLoad: (e) => {
|
|
90
|
+
const img = e.currentTarget;
|
|
91
|
+
if (img.naturalWidth && img.naturalHeight) img.style.setProperty("--rich-text-image-aspect", String(img.naturalWidth / img.naturalHeight));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
if (!onImageActivate) return img;
|
|
95
|
+
return /* @__PURE__ */ jsx("button", {
|
|
96
|
+
type: "button",
|
|
97
|
+
className: RichTextAttachment_module_default.imageTrigger,
|
|
98
|
+
"aria-label": t("richText.openImage", { name: block.alt || name }),
|
|
99
|
+
onClick: (e) => {
|
|
100
|
+
const trigger = e.currentTarget;
|
|
101
|
+
trigger.focus();
|
|
102
|
+
onImageActivate({
|
|
103
|
+
src: href,
|
|
104
|
+
alt: block.alt
|
|
105
|
+
}, trigger);
|
|
106
|
+
},
|
|
107
|
+
children: img
|
|
108
|
+
});
|
|
109
|
+
}
|
|
93
110
|
return /* @__PURE__ */ jsxs("a", {
|
|
94
111
|
className: RichTextAttachment_module_default.attachmentChip,
|
|
95
112
|
href,
|
|
@@ -7,6 +7,7 @@ var attachmentChip = "RichTextAttachment__attachmentChip__pQhOg";
|
|
|
7
7
|
var attachmentName = "RichTextAttachment__attachmentName__qEYIR";
|
|
8
8
|
var attachmentLabel = "RichTextAttachment__attachmentLabel__RBGKX";
|
|
9
9
|
var attachmentReason = "RichTextAttachment__attachmentReason__6Es5S";
|
|
10
|
+
var imageTrigger = "RichTextAttachment__imageTrigger__5_u8P";
|
|
10
11
|
var RichTextAttachment_module_default = {
|
|
11
12
|
attachmentImg,
|
|
12
13
|
attachmentLoading,
|
|
@@ -14,6 +15,7 @@ var RichTextAttachment_module_default = {
|
|
|
14
15
|
attachmentChip,
|
|
15
16
|
attachmentName,
|
|
16
17
|
attachmentLabel,
|
|
17
|
-
attachmentReason
|
|
18
|
+
attachmentReason,
|
|
19
|
+
imageTrigger
|
|
18
20
|
};
|
|
19
|
-
export { attachmentChip, attachmentError, attachmentImg, attachmentLabel, attachmentLoading, attachmentName, attachmentReason, RichTextAttachment_module_default as default };
|
|
21
|
+
export { attachmentChip, attachmentError, attachmentImg, attachmentLabel, attachmentLoading, attachmentName, attachmentReason, RichTextAttachment_module_default as default, imageTrigger };
|
|
@@ -2,6 +2,7 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
import type { RichDoc } from './model.js';
|
|
3
3
|
import type { RenderLink } from './renderLink.js';
|
|
4
4
|
import type { RenderMention } from './renderMention.js';
|
|
5
|
+
import { type RichTextImageActivateHandler } from '../RichTextAttachment.js';
|
|
5
6
|
export interface RenderDocOptions {
|
|
6
7
|
/** Editable surface: add `data-block-id` anchors + render empty blocks with a `<br>`. */
|
|
7
8
|
editable?: boolean;
|
|
@@ -22,6 +23,11 @@ export interface RenderDocOptions {
|
|
|
22
23
|
* standard non-interactive mention span. Composes with `renderLink`.
|
|
23
24
|
*/
|
|
24
25
|
renderMention?: RenderMention;
|
|
26
|
+
/**
|
|
27
|
+
* Make each ready image attachment a native `<button>` that calls this on
|
|
28
|
+
* activation (read-only viewer). Unset: the plain `<img>`, unchanged.
|
|
29
|
+
*/
|
|
30
|
+
onImageActivate?: RichTextImageActivateHandler;
|
|
25
31
|
}
|
|
26
32
|
/**
|
|
27
33
|
* Render a `RichDoc` to React nodes.
|
|
@@ -9,14 +9,12 @@ import { Fragment } from "react";
|
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
// src/components/RichText/engine/renderDoc.tsx
|
|
11
11
|
var blockCache = /* @__PURE__ */ new WeakMap();
|
|
12
|
-
function cachedBlockEl(block,
|
|
12
|
+
function cachedBlockEl(block, opts, build) {
|
|
13
13
|
const hit = blockCache.get(block);
|
|
14
|
-
if (hit && hit.editable === editable && hit.renderLink === renderLink && hit.renderMention === renderMention) return hit.el;
|
|
14
|
+
if (hit && hit.editable === opts.editable && hit.renderLink === opts.renderLink && hit.renderMention === opts.renderMention && hit.onImageActivate === opts.onImageActivate) return hit.el;
|
|
15
15
|
const el = build();
|
|
16
16
|
blockCache.set(block, {
|
|
17
|
-
|
|
18
|
-
renderLink,
|
|
19
|
-
renderMention,
|
|
17
|
+
...opts,
|
|
20
18
|
el
|
|
21
19
|
});
|
|
22
20
|
return el;
|
|
@@ -173,7 +171,8 @@ function buildBlock(block, opts) {
|
|
|
173
171
|
"data-align": block.align || void 0,
|
|
174
172
|
children: /* @__PURE__ */ jsx(RichTextAttachment, {
|
|
175
173
|
block,
|
|
176
|
-
editable: opts.editable
|
|
174
|
+
editable: opts.editable,
|
|
175
|
+
onImageActivate: opts.onImageActivate
|
|
177
176
|
})
|
|
178
177
|
}, block.id);
|
|
179
178
|
default: return /* @__PURE__ */ jsx("p", {
|
|
@@ -183,7 +182,7 @@ function buildBlock(block, opts) {
|
|
|
183
182
|
}
|
|
184
183
|
}
|
|
185
184
|
function renderBlock(block, opts) {
|
|
186
|
-
return cachedBlockEl(block, opts
|
|
185
|
+
return cachedBlockEl(block, opts, () => buildBlock(block, opts));
|
|
187
186
|
}
|
|
188
187
|
function collectList(blocks, start, eff, opts) {
|
|
189
188
|
const baseDepth = eff[start];
|
|
@@ -238,7 +237,8 @@ function renderDoc(doc, options = {}) {
|
|
|
238
237
|
const opts = {
|
|
239
238
|
editable: options.editable ?? false,
|
|
240
239
|
renderLink: options.renderLink,
|
|
241
|
-
renderMention: options.renderMention
|
|
240
|
+
renderMention: options.renderMention,
|
|
241
|
+
onImageActivate: options.onImageActivate
|
|
242
242
|
};
|
|
243
243
|
const eff = effectiveDepths(doc.blocks);
|
|
244
244
|
const out = [];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { RichText } from './RichText.js';
|
|
2
2
|
export type { RichTextProps } from './RichText.js';
|
|
3
|
+
export type { RichTextImageActivateHandler } from './RichTextAttachment.js';
|
|
3
4
|
export type { RichDoc, Block as RichBlock, BlockType as RichBlockType, Inline as RichInline, Mark as RichMark, MarkType as RichMarkType, Point as RichPoint, Range as RichRange, } from './engine/model.js';
|
|
4
5
|
export { emptyDoc, createBlock, docFromText } from './engine/model.js';
|
|
5
6
|
export { insertText, deleteRange, splitBlock, mergeBlockBackward, applyMark, removeMark, toggleMark, setBlockType, } from './engine/transforms.js';
|
|
@@ -14,8 +14,8 @@ export type TextTone = 'default' | 'muted'
|
|
|
14
14
|
| 'subtle' | 'accent' | 'danger' | 'success' | 'warning';
|
|
15
15
|
/** Font weight. */
|
|
16
16
|
export type TextWeight = 'regular' | 'medium' | 'semibold' | 'bold';
|
|
17
|
-
/** Text alignment. */
|
|
18
|
-
export type TextAlign = 'left' | 'center' | 'right';
|
|
17
|
+
/** Text alignment. Logical `start` / `end` follow writing direction; `left` / `right` are physical. */
|
|
18
|
+
export type TextAlign = 'start' | 'end' | 'left' | 'center' | 'right';
|
|
19
19
|
export interface TextProps extends HTMLAttributes<HTMLElement> {
|
|
20
20
|
/**
|
|
21
21
|
* Associates a `<label>` with its form control. Only meaningful when
|
|
@@ -66,7 +66,16 @@ export interface TextProps extends HTMLAttributes<HTMLElement> {
|
|
|
66
66
|
tone?: TextTone;
|
|
67
67
|
/** Font weight. Defaults to `'regular'`. */
|
|
68
68
|
weight?: TextWeight;
|
|
69
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Text alignment. No default — omitted, Text sets no `text-align` and
|
|
71
|
+
* inherits it from its container (e.g. a `Table.Cell align="end"` keeps a
|
|
72
|
+
* Text value at the end). Pass `align` only to override the container.
|
|
73
|
+
* - `start` — start of the line in the writing direction (left in LTR, right in RTL). Prefer over `left`.
|
|
74
|
+
* - `end` — end of the line in the writing direction (right in LTR, left in RTL). Prefer over `right`.
|
|
75
|
+
* - `center` — centred.
|
|
76
|
+
* - `left` — physical left in every direction; wrong in RTL, use `start`.
|
|
77
|
+
* - `right` — physical right in every direction; wrong in RTL, use `end`.
|
|
78
|
+
*/
|
|
70
79
|
align?: TextAlign;
|
|
71
80
|
/**
|
|
72
81
|
* Truncate to a single line with ellipsis. Defaults to `false`. Use inside
|
|
@@ -28,6 +28,8 @@ var WEIGHT_CLASS = {
|
|
|
28
28
|
bold: Text_module_default.weightBold
|
|
29
29
|
};
|
|
30
30
|
var ALIGN_CLASS = {
|
|
31
|
+
start: Text_module_default.alignStart,
|
|
32
|
+
end: Text_module_default.alignEnd,
|
|
31
33
|
left: Text_module_default.alignLeft,
|
|
32
34
|
center: Text_module_default.alignCenter,
|
|
33
35
|
right: Text_module_default.alignRight
|
|
@@ -36,7 +38,7 @@ var ALIGN_CLASS = {
|
|
|
36
38
|
* Body / inline text primitive for all non-heading text (`p`, `span`, `div` or `label`).
|
|
37
39
|
* @see docs/components/Text.md
|
|
38
40
|
*/
|
|
39
|
-
var Text = forwardRef(function Text({ as = "p", size = "md", tone = "default", weight = "regular", align
|
|
41
|
+
var Text = forwardRef(function Text({ as = "p", size = "md", tone = "default", weight = "regular", align, truncate = false, lineClamp, className, style, children, ...rest }, ref) {
|
|
40
42
|
const Component = as;
|
|
41
43
|
const useLineClamp = typeof lineClamp === "number" && lineClamp > 0;
|
|
42
44
|
const useTruncate = !useLineClamp && truncate;
|
|
@@ -46,7 +48,7 @@ var Text = forwardRef(function Text({ as = "p", size = "md", tone = "default", w
|
|
|
46
48
|
} : style;
|
|
47
49
|
return /* @__PURE__ */ jsx(Component, {
|
|
48
50
|
ref,
|
|
49
|
-
className: clsx(Text_module_default.text, SIZE_CLASS[size], TONE_CLASS[tone], WEIGHT_CLASS[weight], ALIGN_CLASS[align], useTruncate && Text_module_default.truncate, useLineClamp && Text_module_default.lineClamp, className),
|
|
51
|
+
className: clsx(Text_module_default.text, SIZE_CLASS[size], TONE_CLASS[tone], WEIGHT_CLASS[weight], align && ALIGN_CLASS[align], useTruncate && Text_module_default.truncate, useLineClamp && Text_module_default.lineClamp, className),
|
|
50
52
|
style: mergedStyle,
|
|
51
53
|
...rest,
|
|
52
54
|
children
|
|
@@ -18,6 +18,8 @@ var toneAccent = "Text__toneAccent__MhCvt";
|
|
|
18
18
|
var toneDanger = "Text__toneDanger___Z5Uc";
|
|
19
19
|
var toneSuccess = "Text__toneSuccess__hgcOm";
|
|
20
20
|
var toneWarning = "Text__toneWarning___Qj9v";
|
|
21
|
+
var alignStart = "Text__alignStart__iXgpi";
|
|
22
|
+
var alignEnd = "Text__alignEnd__I4Zil";
|
|
21
23
|
var alignLeft = "Text__alignLeft__RyNbJ";
|
|
22
24
|
var alignCenter = "Text__alignCenter__hKaav";
|
|
23
25
|
var alignRight = "Text__alignRight__Cx6EZ";
|
|
@@ -42,10 +44,12 @@ var Text_module_default = {
|
|
|
42
44
|
toneDanger,
|
|
43
45
|
toneSuccess,
|
|
44
46
|
toneWarning,
|
|
47
|
+
alignStart,
|
|
48
|
+
alignEnd,
|
|
45
49
|
alignLeft,
|
|
46
50
|
alignCenter,
|
|
47
51
|
alignRight,
|
|
48
52
|
truncate,
|
|
49
53
|
lineClamp
|
|
50
54
|
};
|
|
51
|
-
export { alignCenter, alignLeft, alignRight, Text_module_default as default, lineClamp, sizeInherit, sizeLg, sizeMd, sizeSm, sizeXl, sizeXs, text, toneAccent, toneDanger, toneDefault, toneMuted, toneSubtle, toneSuccess, toneWarning, truncate, weightBold, weightMedium, weightRegular, weightSemibold };
|
|
55
|
+
export { alignCenter, alignEnd, alignLeft, alignRight, alignStart, Text_module_default as default, lineClamp, sizeInherit, sizeLg, sizeMd, sizeSm, sizeXl, sizeXs, text, toneAccent, toneDanger, toneDefault, toneMuted, toneSubtle, toneSuccess, toneWarning, truncate, weightBold, weightMedium, weightRegular, weightSemibold };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
@charset "UTF-8";
|
|
1
2
|
:root {
|
|
2
3
|
--timeline-gutter: var(--size-md);
|
|
3
4
|
--timeline-node-size: var(--size-md);
|
|
@@ -5,6 +6,8 @@
|
|
|
5
6
|
--timeline-content-gap: var(--space-3);
|
|
6
7
|
--timeline-line-width: var(--border-width);
|
|
7
8
|
--timeline-line-color: var(--color-border);
|
|
9
|
+
--timeline-first-line-font-size: var(--font-size-sm);
|
|
10
|
+
--timeline-first-line-height: var(--line-height-normal);
|
|
8
11
|
--timeline-gutter-compact: var(--size-sm);
|
|
9
12
|
--timeline-node-size-compact: var(--size-sm);
|
|
10
13
|
--timeline-row-gap-compact: var(--space-2);
|
|
@@ -21,26 +24,33 @@
|
|
|
21
24
|
.Timeline__item__OFAEY {
|
|
22
25
|
display: grid;
|
|
23
26
|
grid-template-columns: var(--timeline-gutter) 1fr;
|
|
27
|
+
grid-template-rows: auto 1fr;
|
|
24
28
|
column-gap: var(--timeline-content-gap);
|
|
29
|
+
align-items: first baseline;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
.Timeline__gutter__G51XE {
|
|
28
|
-
|
|
33
|
+
display: contents;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
.Timeline__nodeBox__3ZPJu {
|
|
37
|
+
grid-area: 1/1;
|
|
32
38
|
height: var(--timeline-node-size);
|
|
33
39
|
display: flex;
|
|
34
40
|
align-items: center;
|
|
35
41
|
justify-content: center;
|
|
36
42
|
}
|
|
43
|
+
.Timeline__nodeBox__3ZPJu::before {
|
|
44
|
+
content: "";
|
|
45
|
+
content: ""/"";
|
|
46
|
+
font-size: var(--timeline-first-line-font-size);
|
|
47
|
+
line-height: var(--timeline-first-line-height);
|
|
48
|
+
}
|
|
37
49
|
|
|
38
50
|
.Timeline__connector__QoIUd {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
top: var(--timeline-node-size);
|
|
43
|
-
bottom: 0;
|
|
51
|
+
grid-area: 2/1;
|
|
52
|
+
align-self: stretch;
|
|
53
|
+
justify-self: center;
|
|
44
54
|
width: var(--timeline-line-width);
|
|
45
55
|
background: var(--timeline-line-color);
|
|
46
56
|
}
|
|
@@ -50,6 +60,7 @@
|
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
.Timeline__content__0ee3p {
|
|
63
|
+
grid-area: 1/2/span 2;
|
|
53
64
|
min-width: 0;
|
|
54
65
|
padding-block-end: var(--timeline-row-gap);
|
|
55
66
|
}
|
package/dist/i18n/en.js
CHANGED
package/dist/i18n/messages.d.ts
CHANGED
|
@@ -1193,6 +1193,13 @@ export interface Messages {
|
|
|
1193
1193
|
/** Muted tag marking a collection (array) variable in menus. */
|
|
1194
1194
|
collectionTag: string;
|
|
1195
1195
|
};
|
|
1196
|
+
richText: {
|
|
1197
|
+
/** Function leaf — aria-label on an inline-image trigger (`onImageActivate`).
|
|
1198
|
+
* `name` is the image's alt, else its file name; may be empty. */
|
|
1199
|
+
openImage: (params: {
|
|
1200
|
+
name: string;
|
|
1201
|
+
}) => string;
|
|
1202
|
+
};
|
|
1196
1203
|
richTextEditor: {
|
|
1197
1204
|
/** Default aria-label for the editable region when none is supplied. */
|
|
1198
1205
|
editorLabel: string;
|
package/dist/i18n/ru.js
CHANGED
|
@@ -511,6 +511,7 @@ var ru = {
|
|
|
511
511
|
suggestions: "Подсказки",
|
|
512
512
|
collectionTag: "список"
|
|
513
513
|
},
|
|
514
|
+
richText: { openImage: ({ name }) => name ? `Открыть изображение: ${name}` : "Открыть изображение" },
|
|
514
515
|
richTextEditor: {
|
|
515
516
|
editorLabel: "Редактор форматированного текста",
|
|
516
517
|
toolbar: "Форматирование",
|
package/dist/index.d.ts
CHANGED
|
@@ -206,7 +206,7 @@ export type { CodeBlockProps, CodeBlockSample, CodeBlockHighlightLines, CodeBloc
|
|
|
206
206
|
export { HighlightedText } from './components/HighlightedText/index.js';
|
|
207
207
|
export type { HighlightedTextProps, HighlightRange } from './components/HighlightedText/index.js';
|
|
208
208
|
export { RichText } from './components/RichText/index.js';
|
|
209
|
-
export type { RichTextProps, RichDoc, RichBlock, RichBlockType, RichInline, RichMark, RichMarkType, RichPoint, RichRange, } from './components/RichText/index.js';
|
|
209
|
+
export type { RichTextProps, RichTextImageActivateHandler, RichDoc, RichBlock, RichBlockType, RichInline, RichMark, RichMarkType, RichPoint, RichRange, } from './components/RichText/index.js';
|
|
210
210
|
export type { RichTextLink, RenderLink } from './components/RichText/engine/renderLink.js';
|
|
211
211
|
export type { RichTextMention, RenderMention } from './components/RichText/engine/renderMention.js';
|
|
212
212
|
export { emptyDoc, createBlock, docFromText, insertText, deleteRange, splitBlock, mergeBlockBackward, applyMark, removeMark, toggleMark, setBlockType, fromHtml, fromMarkdown, toHtml, toMarkdown, } from './components/RichText/index.js';
|
package/dist/styles.css
CHANGED
|
@@ -6767,6 +6767,7 @@ pre {
|
|
|
6767
6767
|
flex-shrink: 0;
|
|
6768
6768
|
font-weight: var(--pill-menu-caption-font-weight);
|
|
6769
6769
|
}
|
|
6770
|
+
@charset "UTF-8";
|
|
6770
6771
|
:root {
|
|
6771
6772
|
--timeline-gutter: var(--size-md);
|
|
6772
6773
|
--timeline-node-size: var(--size-md);
|
|
@@ -6774,6 +6775,8 @@ pre {
|
|
|
6774
6775
|
--timeline-content-gap: var(--space-3);
|
|
6775
6776
|
--timeline-line-width: var(--border-width);
|
|
6776
6777
|
--timeline-line-color: var(--color-border);
|
|
6778
|
+
--timeline-first-line-font-size: var(--font-size-sm);
|
|
6779
|
+
--timeline-first-line-height: var(--line-height-normal);
|
|
6777
6780
|
--timeline-gutter-compact: var(--size-sm);
|
|
6778
6781
|
--timeline-node-size-compact: var(--size-sm);
|
|
6779
6782
|
--timeline-row-gap-compact: var(--space-2);
|
|
@@ -6790,26 +6793,33 @@ pre {
|
|
|
6790
6793
|
.Timeline__item__OFAEY {
|
|
6791
6794
|
display: grid;
|
|
6792
6795
|
grid-template-columns: var(--timeline-gutter) 1fr;
|
|
6796
|
+
grid-template-rows: auto 1fr;
|
|
6793
6797
|
column-gap: var(--timeline-content-gap);
|
|
6798
|
+
align-items: first baseline;
|
|
6794
6799
|
}
|
|
6795
6800
|
|
|
6796
6801
|
.Timeline__gutter__G51XE {
|
|
6797
|
-
|
|
6802
|
+
display: contents;
|
|
6798
6803
|
}
|
|
6799
6804
|
|
|
6800
6805
|
.Timeline__nodeBox__3ZPJu {
|
|
6806
|
+
grid-area: 1/1;
|
|
6801
6807
|
height: var(--timeline-node-size);
|
|
6802
6808
|
display: flex;
|
|
6803
6809
|
align-items: center;
|
|
6804
6810
|
justify-content: center;
|
|
6805
6811
|
}
|
|
6812
|
+
.Timeline__nodeBox__3ZPJu::before {
|
|
6813
|
+
content: "";
|
|
6814
|
+
content: ""/"";
|
|
6815
|
+
font-size: var(--timeline-first-line-font-size);
|
|
6816
|
+
line-height: var(--timeline-first-line-height);
|
|
6817
|
+
}
|
|
6806
6818
|
|
|
6807
6819
|
.Timeline__connector__QoIUd {
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
top: var(--timeline-node-size);
|
|
6812
|
-
bottom: 0;
|
|
6820
|
+
grid-area: 2/1;
|
|
6821
|
+
align-self: stretch;
|
|
6822
|
+
justify-self: center;
|
|
6813
6823
|
width: var(--timeline-line-width);
|
|
6814
6824
|
background: var(--timeline-line-color);
|
|
6815
6825
|
}
|
|
@@ -6819,6 +6829,7 @@ pre {
|
|
|
6819
6829
|
}
|
|
6820
6830
|
|
|
6821
6831
|
.Timeline__content__0ee3p {
|
|
6832
|
+
grid-area: 1/2/span 2;
|
|
6822
6833
|
min-width: 0;
|
|
6823
6834
|
padding-block-end: var(--timeline-row-gap);
|
|
6824
6835
|
}
|
|
@@ -7332,6 +7343,14 @@ pre {
|
|
|
7332
7343
|
color: var(--text-fg-warning);
|
|
7333
7344
|
}
|
|
7334
7345
|
|
|
7346
|
+
.Text__alignStart__iXgpi {
|
|
7347
|
+
text-align: start;
|
|
7348
|
+
}
|
|
7349
|
+
|
|
7350
|
+
.Text__alignEnd__I4Zil {
|
|
7351
|
+
text-align: end;
|
|
7352
|
+
}
|
|
7353
|
+
|
|
7335
7354
|
.Text__alignLeft__RyNbJ {
|
|
7336
7355
|
text-align: left;
|
|
7337
7356
|
}
|
|
@@ -12691,6 +12710,20 @@ figure[data-attachment][data-align=center] {
|
|
|
12691
12710
|
figure[data-attachment][data-align=right] {
|
|
12692
12711
|
text-align: right;
|
|
12693
12712
|
}
|
|
12713
|
+
|
|
12714
|
+
.RichTextAttachment__imageTrigger__5_u8P {
|
|
12715
|
+
display: inline-block;
|
|
12716
|
+
padding: 0;
|
|
12717
|
+
border: 0;
|
|
12718
|
+
background: none;
|
|
12719
|
+
font: inherit;
|
|
12720
|
+
line-height: 0;
|
|
12721
|
+
cursor: zoom-in;
|
|
12722
|
+
}
|
|
12723
|
+
.RichTextAttachment__imageTrigger__5_u8P:focus-visible {
|
|
12724
|
+
outline: var(--ring-width) solid var(--ring-accent);
|
|
12725
|
+
outline-offset: var(--ring-offset);
|
|
12726
|
+
}
|
|
12694
12727
|
.RichTextEditor__root__-1u5M {
|
|
12695
12728
|
display: flex;
|
|
12696
12729
|
flex-direction: column;
|
|
@@ -14655,6 +14688,10 @@ figure[data-attachment][data-align=right] {
|
|
|
14655
14688
|
gap: var(--list-marker-gap);
|
|
14656
14689
|
}
|
|
14657
14690
|
|
|
14691
|
+
.List__list__u7QbW[data-variant=plain] > .List__item__DXe2D {
|
|
14692
|
+
display: block;
|
|
14693
|
+
}
|
|
14694
|
+
|
|
14658
14695
|
.List__marker__EqIaJ {
|
|
14659
14696
|
display: flex;
|
|
14660
14697
|
flex: none;
|
|
@@ -14834,6 +14871,7 @@ figure[data-attachment][data-align=right] {
|
|
|
14834
14871
|
.LinkCard__linkCard__wrwBJ {
|
|
14835
14872
|
--link-card-elevation: var(--link-card-shadow);
|
|
14836
14873
|
display: block;
|
|
14874
|
+
text-align: start;
|
|
14837
14875
|
background: var(--card-bg);
|
|
14838
14876
|
border: var(--border-width) solid var(--card-border-color);
|
|
14839
14877
|
border-radius: var(--card-radius);
|
|
@@ -14844,6 +14882,9 @@ figure[data-attachment][data-align=right] {
|
|
|
14844
14882
|
cursor: pointer;
|
|
14845
14883
|
transition: var(--link-card-transition);
|
|
14846
14884
|
}
|
|
14885
|
+
.LinkCard__linkCard__wrwBJ:where(button) {
|
|
14886
|
+
width: 100%;
|
|
14887
|
+
}
|
|
14847
14888
|
|
|
14848
14889
|
.LinkCard__linkCard__wrwBJ:hover {
|
|
14849
14890
|
--link-card-elevation: var(--link-card-hover-shadow);
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'inherit'` | no | Visual size. Defaults to `'md'` (body text). - `xs` — 11px, dense metadata / captions - `sm` — 12px, small body / labels - `md` — 14px, body text (default) - `lg` — 16px, large body / lead text - `xl` — 20px, very large body (rare) - `inherit` — no fixed size; font-size AND line-height inherit from the parent. For inline runs inside a heading (`as="span"` inside a `<Title>` / `<PageHeader.Title>`) that must keep the heading's size — e.g. a muted task-key prefix. Tone / weight still apply — font-weight stays Text's own (default `regular`), it does NOT inherit; pass `weight` to match the heading if needed. |
|
|
27
27
|
| `tone` | `'default' \| 'muted' \| 'subtle' \| 'accent' \| 'danger' \| 'success' \| 'warning'` | no | Color tone. Defaults to `'default'`. - `default` — `--color-fg` - `muted` — `--color-fg-muted` (for secondary copy) - `subtle` — **@deprecated: resolves to `muted`. Use `muted`.** In LIGHT theme the two neutrals were indistinguishable: OKLab ΔE 0.0261 when the deprecation was filed, 0.0365 after `--color-fg-muted` was retuned, against the 0.065 floor this library's perceptual gates use. In DARK they were 0.0707 apart — a real step — so **this deprecation changes dark-theme appearance**: `subtle` text in dark gets lighter, moving from `--color-fg-subtle` to `--color-fg-muted`. That is the accepted trade (a tier that exists in one theme only is not a tier), but it is a visual change, not the removal of a duplicate. `--text-fg-subtle` now aliases `--text-fg-muted`, so nothing breaks at the type or build level; if you need the old dark value back, override `--text-fg-subtle`. - `accent` — `--color-accent` - `danger` / `success` / `warning` — state-coded text |
|
|
28
28
|
| `weight` | `'regular' \| 'medium' \| 'semibold' \| 'bold'` | no | Font weight. Defaults to `'regular'`. |
|
|
29
|
-
| `align` | `'left' \| 'center' \| 'right'` | no | Text alignment.
|
|
29
|
+
| `align` | `'start' \| 'end' \| 'left' \| 'center' \| 'right'` | no | Text alignment. No default — omitted, Text sets no `text-align` and inherits it from its container (e.g. a `Table.Cell align="end"` keeps a Text value at the end). Pass `align` only to override the container. - `start` — start of the line in the writing direction (left in LTR, right in RTL). Prefer over `left`. - `end` — end of the line in the writing direction (right in LTR, left in RTL). Prefer over `right`. - `center` — centred. - `left` — physical left in every direction; wrong in RTL, use `start`. - `right` — physical right in every direction; wrong in RTL, use `end`. |
|
|
30
30
|
| `truncate` | `boolean` | no | Truncate to a single line with ellipsis. Defaults to `false`. Use inside narrow containers (table cells, card list rows). Mutually exclusive with `lineClamp` — if both are set, `lineClamp` wins. |
|
|
31
31
|
| `lineClamp` | `number` | no | Clamp to N lines with ellipsis (uses `-webkit-line-clamp`). Defaults to `undefined`. Overrides `truncate` when set. Example: `lineClamp={2}` for a 2-line description that ellipses on the third. If you also pass `style.WebkitLineClamp`, the `lineClamp` prop takes precedence — the component merges the dynamic line-clamp value into `style` AFTER spreading your `style`, so the prop wins. |
|
|
32
32
|
| …native | | | plus native HTML attributes |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `<Lightbox>` — full-screen image & document gallery overlay
|
|
2
2
|
|
|
3
|
-
`<Lightbox open onOpenChange items>` shows one large item at a time (an image, or a PDF in an `<iframe>`) with prev/next chevrons, ← → keys, a thumbnail strip, a position counter, and an optional caption. Controlled `open` like `<Modal>`; the current index is uncontrolled (`defaultIndex`) unless you pass `index` + `onIndexChange`. `loop` (default true) wraps at the ends. The consumer owns the trigger — typically a row of interactive `<Image>` thumbnails
|
|
3
|
+
`<Lightbox open onOpenChange items>` shows one large item at a time (an image, or a PDF in an `<iframe>`) with prev/next chevrons, ← → keys, a thumbnail strip, a position counter, and an optional caption. Controlled `open` like `<Modal>`; the current index is uncontrolled (`defaultIndex`) unless you pass `index` + `onIndexChange`. `loop` (default true) wraps at the ends. The consumer owns the trigger — typically a row of interactive `<Image>` thumbnails, or an inline image in `<RichText onImageActivate>`. For a single inline image use `<Image>`.
|
|
4
4
|
|
|
5
5
|
```tsx
|
|
6
6
|
const [open, setOpen] = useState(false);
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
<LinkCard as="button" type="button" onClick={openImporter}>Import contacts</LinkCard>
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
The `as="button"` form start-aligns text like the anchor form and fills its container's width (a `<button>` would otherwise shrink-wrap); the anchor form keeps normal block sizing, so it can still sit in a flex row such as `Cluster`.
|
|
17
|
+
|
|
16
18
|
<!-- props:start -->
|
|
17
19
|
|
|
18
20
|
## Props
|
package/docs/components/List.md
CHANGED
|
@@ -35,10 +35,10 @@ TL;DR: `<List>` for bullets, `<List variant="number">` for ordered steps, `numbe
|
|
|
35
35
|
<!-- prettier-ignore -->
|
|
36
36
|
| Prop | Type | Required | Description |
|
|
37
37
|
|---|---|---|---|
|
|
38
|
-
| `variant` | `'bullet' \| 'number'` | no | Marker style. See `ListVariant`. Default `'bullet'`. |
|
|
38
|
+
| `variant` | `'bullet' \| 'number' \| 'plain'` | no | Marker style. See `ListVariant`. Default `'bullet'`. |
|
|
39
39
|
| `gap` | `'xs' \| 'sm' \| 'md'` | no | Space between items. See `ListGap`. Default `'sm'`. |
|
|
40
|
-
| `size` | `'body' \| 'heading'` | no | Marker scale;
|
|
41
|
-
| `numberStyle` | `'plain' \| 'badge'` | no | Number look; only applies to `variant="number"
|
|
40
|
+
| `size` | `'body' \| 'heading'` | no | Marker scale; no effect on `variant="plain"`. Use `'heading'` when each item starts with a `Title size="xl"`. See `ListSize`. Default `'body'`. |
|
|
41
|
+
| `numberStyle` | `'plain' \| 'badge'` | no | Number look; only applies to `variant="number"` (not `'plain'`). See `ListNumberStyle`. Default `'plain'`. |
|
|
42
42
|
| `children` | `ReactNode` | no | `List.Item` elements. A nested `List` goes inside a `List.Item`. |
|
|
43
43
|
| …native | | | plus native HTML attributes |
|
|
44
44
|
|
|
@@ -58,10 +58,11 @@ TL;DR: `<List>` for bullets, `<List variant="number">` for ordered steps, `numbe
|
|
|
58
58
|
- Nest by putting a `List` inside a `List.Item`; it indents one level. A nested `List` uses its own `size`, so a body list inside a `size="heading"` step keeps body markers.
|
|
59
59
|
- On a `size="heading"` List, marker size, shape and font size come from the `--list-heading-*` tokens: overriding `--list-marker-height`/`-width`, `--list-bullet-size`, `--list-number-font-size` or `--list-badge-size`/`-radius`/`-font-size` has no effect there. Colour, weight and gap tokens (`--list-marker-fg`, `--list-badge-bg`/`-fg`, `--list-*-font-weight`, `--list-marker-gap`) apply to both sizes.
|
|
60
60
|
- `numberStyle` has no effect on bullets.
|
|
61
|
+
- `variant="plain"` renders a marker-less `<ul>` for rows of interactive items (title button, timestamp, icon button) that need list semantics; `numberStyle` and `size` do not apply. Put a `Cluster` in each `List.Item` for the row layout.
|
|
61
62
|
- With `numberStyle="badge"` the numbers are visual only; screen readers announce the list position ("1 of 3") rather than the badge text.
|
|
62
63
|
- `size="heading"` fits markers to a `Title size="xl"` first line; the badge turns into a rounded square.
|
|
63
64
|
|
|
64
|
-
When NOT to use: key–value pairs → `DefinitionList`; rows of records → `Card.List` / `Table`;
|
|
65
|
+
When NOT to use: key–value pairs → `DefinitionList`; rows of records → `Card.List` / `Table`; selectable rows → `Card.List`; interactive rows that only need list semantics → `variant="plain"`; custom markers or icons (unsupported).
|
|
65
66
|
|
|
66
67
|
- ❌ Raw `<ul>` / `<ol>` or `Stack` + `Dot` composites for prose lists: use `List`.
|
|
67
68
|
- ❌ Children other than `List.Item` (a `div`, text, a raw `li`): they get no marker and are not counted. Wrapper components are fine if they render `List.Item`.
|
|
@@ -23,6 +23,7 @@ const doc = {
|
|
|
23
23
|
| `renderLink` | `RenderLink` | no | Substitute how a link renders. Called per link with `{ href, text }` and the default `<a>` node; return your own node (e.g. a task/member chip) or the `defaultNode` to keep the standard anchor. Render-time only — the document model is unchanged, so serialization still emits a plain link. Prose link styling (accent colour + underline) applies to BARE anchors only: an `<a>` you return with any `className` — including a DS `<Link>` — keeps its own decoration and colour entirely. |
|
|
24
24
|
| `renderMention` | `RenderMention` | no | Substitute how an `@`-mention renders. Called per mention with `{ id, label }` and the default mention span; return your own node (e.g. an interactive member chip / popover trigger) or the `defaultNode` to keep the standard non-interactive span. Render-time only — the document model is unchanged, so serialization still emits the mention mark. Composes with `renderLink`. |
|
|
25
25
|
| `imageMaxHeight` | `number` | no | Maximum DISPLAYED height of image attachments, in px. Default: the `--size-rich-text-image-max-height` token (480px). A taller image shrinks (width follows, aspect preserved) — render-time only; stored `width`/`height` are unchanged, so existing documents need no migration. There is no "off": pass a large value to effectively remove the cap. |
|
|
26
|
+
| `onImageActivate` | `RichTextImageActivateHandler` | no | Make inline images keyboard-operable triggers (e.g. to open a `Lightbox`). When set, each ready image attachment renders inside a native `<button type="button">` named "Open image: <alt or file name>"; click, Enter and Space focus the button, then call this with `{ src, alt }` and it — so an overlay that restores focus on close (e.g. `Lightbox`) returns there. Unset: the plain `<img>`, unchanged. Pass a stable reference (`useCallback`): a new function each render rebuilds every attachment block. |
|
|
26
27
|
| …native | | | plus native `<div>` attributes |
|
|
27
28
|
|
|
28
29
|
<!-- props:end -->
|
|
@@ -39,6 +40,16 @@ const renderLink: RenderLink = ({ href }, fallback) => {
|
|
|
39
40
|
|
|
40
41
|
**`renderMention`** (optional) — `(mention: { id, label }, defaultNode) => ReactNode` — same contract as `renderLink` but for `@`-mention marks (render an interactive member chip/popover trigger), or return `defaultNode` for the standard non-interactive mention span. Composes with `renderLink`. Render-time only — `toHtml`/`toMarkdown` and the model still emit the mention mark. Works in both `<RichText>` and `<RichTextEditor>` (where a substituted mention becomes an atomic chip).
|
|
41
42
|
|
|
43
|
+
**`onImageActivate`** (optional) — `(image: { src, alt? }, trigger) => void` — makes inline images keyboard-operable triggers, typically to open a `<Lightbox>`. When set, each ready image attachment renders inside a native `<button type="button">` (Tab-focusable, Enter/Space, library focus ring) named "Open image: <alt, else file name>"; the inner `<img>` gets `alt=""` so the name isn't read twice. File chips get no trigger. `trigger` is that button, already focused when the handler runs (the library focuses it, since a click doesn't in Safari), so an overlay that restores focus on close — `Lightbox` does — returns it there with no extra code. `alt` is the stored value (may be `''` or absent) — `LightboxItem.alt` is required, so supply your own fallback. Pass a stable reference (`useCallback`) — a new function each render rebuilds every attachment block. Unset, images render as the plain `<img>`, unchanged. Read-only `<RichText>` only.
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
const [image, setImage] = useState<{ src: string; alt?: string } | null>(null);
|
|
47
|
+
const onImageActivate = useCallback<RichTextImageActivateHandler>((img) => setImage(img), []);
|
|
48
|
+
<RichText value={doc} onImageActivate={onImageActivate} />
|
|
49
|
+
<Lightbox open={image !== null} onOpenChange={(o) => !o && setImage(null)} items={image ? [{ src: image.src, alt: image.alt || 'Attached image' }] : []} />;
|
|
50
|
+
```
|
|
51
|
+
|
|
42
52
|
When NOT to use: plain text → `Text`. For editing → `<RichTextEditor>`. The model is immutable; render the doc returned by a transform, never mutate in place.
|
|
43
53
|
|
|
44
54
|
- ❌ Hand-writing HTML to display rich content — feed a `RichDoc` to `<RichText>`.
|
|
55
|
+
- ❌ A delegated `onClick` on the `<RichText>` root to open images — mouse/touch only: the `<img>` isn't focusable, has no button role and leaves nothing to return focus to. Use `onImageActivate`.
|
package/docs/components/Text.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'inherit'` | no | Visual size. Defaults to `'md'` (body text). - `xs` — 11px, dense metadata / captions - `sm` — 12px, small body / labels - `md` — 14px, body text (default) - `lg` — 16px, large body / lead text - `xl` — 20px, very large body (rare) - `inherit` — no fixed size; font-size AND line-height inherit from the parent. For inline runs inside a heading (`as="span"` inside a `<Title>` / `<PageHeader.Title>`) that must keep the heading's size — e.g. a muted task-key prefix. Tone / weight still apply — font-weight stays Text's own (default `regular`), it does NOT inherit; pass `weight` to match the heading if needed. |
|
|
21
21
|
| `tone` | `'default' \| 'muted' \| 'subtle' \| 'accent' \| 'danger' \| 'success' \| 'warning'` | no | Color tone. Defaults to `'default'`. - `default` — `--color-fg` - `muted` — `--color-fg-muted` (for secondary copy) - `subtle` — **@deprecated: resolves to `muted`. Use `muted`.** In LIGHT theme the two neutrals were indistinguishable: OKLab ΔE 0.0261 when the deprecation was filed, 0.0365 after `--color-fg-muted` was retuned, against the 0.065 floor this library's perceptual gates use. In DARK they were 0.0707 apart — a real step — so **this deprecation changes dark-theme appearance**: `subtle` text in dark gets lighter, moving from `--color-fg-subtle` to `--color-fg-muted`. That is the accepted trade (a tier that exists in one theme only is not a tier), but it is a visual change, not the removal of a duplicate. `--text-fg-subtle` now aliases `--text-fg-muted`, so nothing breaks at the type or build level; if you need the old dark value back, override `--text-fg-subtle`. - `accent` — `--color-accent` - `danger` / `success` / `warning` — state-coded text |
|
|
22
22
|
| `weight` | `'regular' \| 'medium' \| 'semibold' \| 'bold'` | no | Font weight. Defaults to `'regular'`. |
|
|
23
|
-
| `align` | `'left' \| 'center' \| 'right'` | no | Text alignment.
|
|
23
|
+
| `align` | `'start' \| 'end' \| 'left' \| 'center' \| 'right'` | no | Text alignment. No default — omitted, Text sets no `text-align` and inherits it from its container (e.g. a `Table.Cell align="end"` keeps a Text value at the end). Pass `align` only to override the container. - `start` — start of the line in the writing direction (left in LTR, right in RTL). Prefer over `left`. - `end` — end of the line in the writing direction (right in LTR, left in RTL). Prefer over `right`. - `center` — centred. - `left` — physical left in every direction; wrong in RTL, use `start`. - `right` — physical right in every direction; wrong in RTL, use `end`. |
|
|
24
24
|
| `truncate` | `boolean` | no | Truncate to a single line with ellipsis. Defaults to `false`. Use inside narrow containers (table cells, card list rows). Mutually exclusive with `lineClamp` — if both are set, `lineClamp` wins. |
|
|
25
25
|
| `lineClamp` | `number` | no | Clamp to N lines with ellipsis (uses `-webkit-line-clamp`). Defaults to `undefined`. Overrides `truncate` when set. Example: `lineClamp={2}` for a 2-line description that ellipses on the third. If you also pass `style.WebkitLineClamp`, the `lineClamp` prop takes precedence — the component merges the dynamic line-clamp value into `style` AFTER spreading your `style`, so the prop wins. |
|
|
26
26
|
| `children` | `ReactNode` | yes | Text content. |
|
|
@@ -61,6 +61,7 @@ If you need a size or tone the primitive doesn't expose, that is a token-vocabul
|
|
|
61
61
|
- `<Text as="h2">`: Text doesn't accept heading tags; use `<Title order={2}>`.
|
|
62
62
|
- Wrapping a `<Title>` in `<Text>` for tone/weight tweaks: pass tone/weight to the `<Title>`. `size="inherit"` is for runs INSIDE a heading, not for wrapping it.
|
|
63
63
|
- Nesting `<Text>` in another `<Text>` with the default `as="p"`: the inner `<p>` inside the outer `<p>` is invalid HTML and triggers React's DOM nesting warning. Use `<Text as="span" tone="...">` for an inline override inside a paragraph.
|
|
64
|
+
- `<Text align="right">` inside `<Table.Cell align="end">`: Text inherits its container's alignment when `align` is omitted, so drop the prop. When you do need to override, use logical `start` / `end`; `left` / `right` are physical and wrong in RTL.
|
|
64
65
|
|
|
65
66
|
## Typography hard rule
|
|
66
67
|
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
<!-- props:end -->
|
|
43
43
|
|
|
44
44
|
- The last item's connector stops automatically (CSS `:last-child`); `compact` flows via CSS vars.
|
|
45
|
+
- The node centres on the item's **first line**, not the content block — a header that wraps, or a `<Cluster>` holding `Text size="sm"` and an inline `<PersonDisplay>`, still lines up on line 1. It works by first-baseline alignment against a strut set in `Text size="sm"` type, so **start each item with a text line**: content that opens with an image or other baseline-less box aligns the node to that box's bottom edge. If the first line is not `sm` text, set `--timeline-first-line-font-size` / `--timeline-first-line-height` on the `<Timeline>` to its type.
|
|
45
46
|
|
|
46
47
|
```tsx
|
|
47
48
|
// Compact sidebar widget with dot nodes:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eocrm/design-system",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Design system for the EOCRM product — React 19 + CSS custom properties.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"qrcode-generator": "^2.0.4"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@eocrm/design-tokens": "0.8.
|
|
54
|
+
"@eocrm/design-tokens": "0.8.2",
|
|
55
55
|
"@testing-library/jest-dom": "^6.9.1",
|
|
56
56
|
"@testing-library/react": "^16.3.2",
|
|
57
57
|
"@testing-library/user-event": "^14.6.1",
|