@eocrm/design-system 0.8.1 → 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.
Files changed (34) hide show
  1. package/AI-PRIMER.md +1 -1
  2. package/dist/components/DashboardCanvas/DashboardCanvas.js +28 -5
  3. package/dist/components/LinkCard/LinkCard.css +4 -0
  4. package/dist/components/List/List.css +4 -0
  5. package/dist/components/List/List.d.ts +5 -4
  6. package/dist/components/List/List.js +6 -0
  7. package/dist/components/PersonDisplay/PersonDisplay.js +1 -0
  8. package/dist/components/RichText/RichText.d.ts +11 -0
  9. package/dist/components/RichText/RichText.js +3 -2
  10. package/dist/components/RichText/RichTextAttachment.css +14 -0
  11. package/dist/components/RichText/RichTextAttachment.d.ts +12 -1
  12. package/dist/components/RichText/RichTextAttachment.js +29 -12
  13. package/dist/components/RichText/RichTextAttachment.module.js +4 -2
  14. package/dist/components/RichText/engine/renderDoc.d.ts +6 -0
  15. package/dist/components/RichText/engine/renderDoc.js +8 -8
  16. package/dist/components/RichText/index.d.ts +1 -0
  17. package/dist/components/Text/Text.css +8 -0
  18. package/dist/components/Text/Text.d.ts +12 -3
  19. package/dist/components/Text/Text.js +4 -2
  20. package/dist/components/Text/Text.module.js +5 -1
  21. package/dist/components/Timeline/Timeline.css +17 -6
  22. package/dist/i18n/en.js +1 -0
  23. package/dist/i18n/messages.d.ts +7 -0
  24. package/dist/i18n/ru.js +1 -0
  25. package/dist/index.d.ts +1 -1
  26. package/dist/styles.css +47 -6
  27. package/docs/components/HighlightedText.md +1 -1
  28. package/docs/components/Lightbox.md +1 -1
  29. package/docs/components/LinkCard.md +2 -0
  30. package/docs/components/List.md +5 -4
  31. package/docs/components/RichText.md +11 -0
  32. package/docs/components/Text.md +2 -1
  33. package/docs/components/Timeline.md +1 -0
  34. 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 prose lists (ul / ol), optional badge step numbers
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 armPendingRelease = (drag) => {
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 === drag && !drag.moved) dragRef.current = null;
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
- armPendingRelease(drag);
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
- dragRef.current = {
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
- armPendingRelease(drag);
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);
@@ -67,6 +67,10 @@
67
67
  gap: var(--list-marker-gap);
68
68
  }
69
69
 
70
+ .List__list__u7QbW[data-variant=plain] > .List__item__DXe2D {
71
+ display: block;
72
+ }
73
+
70
74
  .List__marker__EqIaJ {
71
75
  display: flex;
72
76
  flex: none;
@@ -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; use `'heading'` when each item starts with a `Title size="xl"`. See `ListSize`. Default `'body'`. */
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"`. See `ListNumberStyle`. Default `'plain'`. */
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
- export declare function RichTextAttachment({ block, editable }: {
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) return /* @__PURE__ */ jsx("img", {
83
- className: RichTextAttachment_module_default.attachmentImg,
84
- src: href,
85
- alt: block.alt ?? name,
86
- width: block.width,
87
- height: block.height,
88
- onLoad: (e) => {
89
- const img = e.currentTarget;
90
- if (img.naturalWidth && img.naturalHeight) img.style.setProperty("--rich-text-image-aspect", String(img.naturalWidth / img.naturalHeight));
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, editable, renderLink, renderMention, build) {
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
- editable,
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.editable, opts.renderLink, opts.renderMention, () => buildBlock(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';
@@ -97,6 +97,14 @@
97
97
  color: var(--text-fg-warning);
98
98
  }
99
99
 
100
+ .Text__alignStart__iXgpi {
101
+ text-align: start;
102
+ }
103
+
104
+ .Text__alignEnd__I4Zil {
105
+ text-align: end;
106
+ }
107
+
100
108
  .Text__alignLeft__RyNbJ {
101
109
  text-align: left;
102
110
  }
@@ -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
- /** Text alignment. Defaults to `'left'`. */
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 = "left", truncate = false, lineClamp, className, style, children, ...rest }, ref) {
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
- position: relative;
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
- position: absolute;
40
- left: 50%;
41
- transform: translateX(-50%);
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
@@ -494,6 +494,7 @@ var en = {
494
494
  suggestions: "Suggestions",
495
495
  collectionTag: "list"
496
496
  },
497
+ richText: { openImage: ({ name }) => name ? `Open image: ${name}` : "Open image" },
497
498
  richTextEditor: {
498
499
  editorLabel: "Rich text editor",
499
500
  toolbar: "Formatting",
@@ -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
- position: relative;
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
- position: absolute;
6809
- left: 50%;
6810
- transform: translateX(-50%);
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. Defaults to `'left'`. |
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. For a single inline image use `<Image>`.
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
@@ -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; 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"`. See `ListNumberStyle`. Default `'plain'`. |
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`; interactive or selectable rows → `Card.List`; custom markers or icons (unsupported).
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`.
@@ -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. Defaults to `'left'`. |
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.1",
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.1",
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",