@llumi/design-system 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +51 -0
  2. package/NOTICE +66 -0
  3. package/README.md +15 -205
  4. package/custom-elements.json +1184 -18
  5. package/dist/LICENSE +51 -0
  6. package/dist/NOTICE +66 -0
  7. package/dist/README.md +29 -0
  8. package/dist/adopt-global-styles-DykTxdm8.js +15 -0
  9. package/dist/base.css +2 -0
  10. package/dist/base.md +59 -0
  11. package/dist/bpmn.md +113 -0
  12. package/dist/bpmn.mjs +5076 -0
  13. package/dist/copy-button.md +128 -0
  14. package/dist/copy-button.mjs +161 -0
  15. package/dist/download-button.md +106 -0
  16. package/dist/download-button.mjs +177 -0
  17. package/dist/highlighter.md +45 -0
  18. package/dist/highlighter.mjs +403 -0
  19. package/dist/icon-button-BeLaqJmK.js +5 -0
  20. package/dist/icons-DBLWcA83.js +42 -0
  21. package/dist/index-elWLeWLs.js +7733 -0
  22. package/dist/llumi-element-DTeRDxao.js +22 -0
  23. package/dist/mermaid.md +58 -0
  24. package/dist/mermaid.mjs +254 -0
  25. package/dist/review.md +141 -0
  26. package/dist/review.mjs +1039 -690
  27. package/dist/slot-text-DW3pDxNK.js +51 -0
  28. package/dist/theme.css +2 -0
  29. package/package.json +49 -26
  30. package/dist/review.cjs +0 -172
  31. package/dist/review.css +0 -98
  32. package/dist/review.js +0 -172
  33. package/dist/types/components/review/comment-dialog.element.d.ts +0 -21
  34. package/dist/types/components/review/custom-target.utils.d.ts +0 -16
  35. package/dist/types/components/review/default-editor.element.d.ts +0 -19
  36. package/dist/types/components/review/highlight-manager.d.ts +0 -21
  37. package/dist/types/components/review/index.d.ts +0 -8
  38. package/dist/types/components/review/overall-comment-dialog.element.d.ts +0 -22
  39. package/dist/types/components/review/review-bar.element.d.ts +0 -26
  40. package/dist/types/components/review/review.element.d.ts +0 -56
  41. package/dist/types/components/review/review.machine.d.ts +0 -234
  42. package/dist/types/components/review/review.persistence.d.ts +0 -16
  43. package/dist/types/components/review/review.type.d.ts +0 -37
  44. package/dist/types/components/review/selection-comment-icons.element.d.ts +0 -17
  45. package/dist/types/components/review/selection-range.utils.d.ts +0 -6
  46. package/dist/types/components/review/sort-comments.utils.d.ts +0 -6
  47. package/dist/types/index.d.ts +0 -1
  48. package/dist/types/shared/cn.d.ts +0 -2
  49. package/dist/types/shared/icons.d.ts +0 -7
  50. package/dist/types/shared/tailwind-element.d.ts +0 -4
@@ -0,0 +1,22 @@
1
+ import { LitElement as t, css as o } from "https://cdn.jsdelivr.net/npm/lit@3.3.3";
2
+ /*! @llumi/design-system v2.0.0 | (c) 2026 pAIrprog SAS | license: https://cdn.jsdelivr.net/npm/@llumi/design-system@2.0.0/LICENSE | bundled third-party components under MIT, see NOTICE | @license */
3
+ const e = o`
4
+ :host {
5
+ box-sizing: border-box;
6
+ font-family: var(--llumi-font-sans, ui-sans-serif, system-ui, sans-serif);
7
+ }
8
+ *,
9
+ *::before,
10
+ *::after {
11
+ box-sizing: inherit;
12
+ }
13
+ :host([hidden]) {
14
+ display: none !important;
15
+ }
16
+ `, s = class s extends t {
17
+ };
18
+ s.styles = e;
19
+ let i = s;
20
+ export {
21
+ i as L
22
+ };
@@ -0,0 +1,58 @@
1
+ # `<llumi-mermaid>`
2
+
3
+ Renders a [Mermaid](https://mermaid.js.org) diagram from a definition string, with
4
+ trackpad/mouse pan + zoom and a fit-to-view button.
5
+
6
+ > **Breaking change (v2):** The `code` property has been removed. Pass the diagram
7
+ > definition as the element's text content (light-DOM slot) instead.
8
+
9
+ ## Usage
10
+
11
+ ```html
12
+ <llumi-mermaid theme="dark">
13
+ graph TD
14
+ A[Start] --> B{Decision}
15
+ B -->|Yes| C[Done]
16
+ B -->|No| D[Retry]
17
+ </llumi-mermaid>
18
+ ```
19
+
20
+ The diagram source is the element's **light-DOM text content** — whitespace is
21
+ automatically dedented. Re-renders whenever the text content changes.
22
+
23
+ ## Properties / Attributes
24
+
25
+ | Name | Attribute | Type | Default | Description |
26
+ |------|-----------|------|---------|-------------|
27
+ | `theme` | `theme` | `string` | `"default"` | Built-in mermaid theme: `default`, `neutral`, `dark`, `forest`, `base`, `null`. |
28
+
29
+ ### Custom theming
30
+
31
+ There is no `themeVariables` property. Use Mermaid's native in-diagram directive, which
32
+ overrides the element's `theme`:
33
+
34
+ ```
35
+ %%{init: {'theme':'base','themeVariables':{'primaryColor':'#dbeafe'}}}%%
36
+ graph TD
37
+ A --> B
38
+ ```
39
+
40
+ ## Events
41
+
42
+ | Event | Detail | When |
43
+ |-------|--------|------|
44
+ | `llumi-mermaid-render` | — | A diagram rendered successfully. |
45
+ | `llumi-mermaid-error` | `{ error: string }` | The definition failed to parse; the error box is shown instead of the diagram. |
46
+
47
+ Both bubble and cross shadow boundaries (`composed: true`).
48
+
49
+ ## Interaction
50
+
51
+ - **Pan:** two-finger scroll (trackpad) / drag.
52
+ - **Zoom:** pinch (trackpad) or ctrl+wheel; scale clamped to `[0.25, 4]`.
53
+ - **Fit:** the top-right button resets the view.
54
+
55
+ ## Dependencies
56
+
57
+ `mermaid`, `d3-selection`, `d3-zoom`, `d3-transition`. This package ships ESM only; consume
58
+ this component via `import "@llumi/design-system/mermaid"`.
@@ -0,0 +1,254 @@
1
+ import { select as _ } from "https://cdn.jsdelivr.net/npm/d3-selection@3.0.0";
2
+ import { zoomIdentity as l, zoom as C } from "https://cdn.jsdelivr.net/npm/d3-zoom@3.0.0";
3
+ import { nothing as m, html as c, unsafeCSS as d } from "https://cdn.jsdelivr.net/npm/lit@3.3.3";
4
+ import { property as b, state as u, query as y, customElement as T } from "https://cdn.jsdelivr.net/npm/lit@3.3.3/decorators.js";
5
+ import p from "https://cdn.jsdelivr.net/npm/mermaid@11.16.0";
6
+ import "https://cdn.jsdelivr.net/npm/d3-transition@3.0.1";
7
+ import { i as w } from "./icon-button-BeLaqJmK.js";
8
+ import { i as S } from "./icons-DBLWcA83.js";
9
+ import { L as f } from "./llumi-element-DTeRDxao.js";
10
+ import { S as E } from "./slot-text-DW3pDxNK.js";
11
+ /*! @llumi/design-system v2.0.0 | (c) 2026 pAIrprog SAS | license: https://cdn.jsdelivr.net/npm/@llumi/design-system@2.0.0/LICENSE | bundled third-party components under MIT, see NOTICE | @license */
12
+ const x = 0.01, z = 150;
13
+ function P(r, t, e) {
14
+ return l.translate(r.x - t, r.y - e).scale(r.k);
15
+ }
16
+ function D(r, t, e, s) {
17
+ const i = 1 - s * x;
18
+ return l.translate(
19
+ t - (t - r.x) * i,
20
+ e - (e - r.y) * i
21
+ ).scale(r.k * i);
22
+ }
23
+ function L(r, t, e, s) {
24
+ return l.translate(t - (t - r.x) * s, e - (e - r.y) * s).scale(r.k * s);
25
+ }
26
+ function k() {
27
+ return typeof navigator < "u" && (/Mac|iPhone|iPad|iPod/.test(navigator.platform) || /Macintosh/.test(navigator.userAgent));
28
+ }
29
+ class M {
30
+ constructor(t) {
31
+ this.enabled = k(), this.target = null, this.lastPinchTime = 0, this.lastScale = 1, this.onWheel = (e) => {
32
+ if (!this.target) return;
33
+ e.preventDefault(), e.stopPropagation();
34
+ const s = this.getTransform(), { svgSelection: i, zoomBehavior: o } = this.target;
35
+ this.isPinch(e) ? (this.lastPinchTime = Date.now(), i.call(
36
+ o.transform,
37
+ D(s, e.offsetX, e.offsetY, e.deltaY)
38
+ )) : i.call(
39
+ o.transform,
40
+ P(s, e.deltaX, e.deltaY)
41
+ );
42
+ }, this.onGestureStart = (e) => {
43
+ e.preventDefault(), e.stopPropagation(), this.lastScale = e.scale;
44
+ }, this.onGestureChange = (e) => {
45
+ if (!this.target) return;
46
+ e.preventDefault(), e.stopPropagation();
47
+ const s = e, i = s.scale - this.lastScale;
48
+ this.lastScale = s.scale;
49
+ const o = this.getTransform(), n = this.target.svgEl.getBoundingClientRect(), { svgSelection: g, zoomBehavior: v } = this.target;
50
+ g.call(
51
+ v.transform,
52
+ L(
53
+ o,
54
+ s.clientX - n.left,
55
+ s.clientY - n.top,
56
+ 1 + i
57
+ )
58
+ );
59
+ }, this.onGestureEnd = (e) => {
60
+ e.preventDefault();
61
+ }, t.addController(this);
62
+ }
63
+ hostConnected() {
64
+ }
65
+ hostDisconnected() {
66
+ this.setTarget(null);
67
+ }
68
+ setTarget(t) {
69
+ this.detach(), this.target = t, this.attach();
70
+ }
71
+ getTransform() {
72
+ var e;
73
+ const t = (e = this.target) == null ? void 0 : e.svgEl;
74
+ return (t == null ? void 0 : t.__zoom) ?? l;
75
+ }
76
+ isPinch(t) {
77
+ return t.ctrlKey ? !0 : Date.now() - this.lastPinchTime < z;
78
+ }
79
+ attach() {
80
+ if (!this.enabled || !this.target) return;
81
+ const { svgEl: t, svgSelection: e } = this.target;
82
+ e.on("wheel.zoom", null), t.addEventListener("wheel", this.onWheel, {
83
+ passive: !1,
84
+ capture: !0
85
+ }), t.addEventListener("gesturestart", this.onGestureStart, {
86
+ capture: !0
87
+ }), t.addEventListener("gesturechange", this.onGestureChange, {
88
+ capture: !0
89
+ }), t.addEventListener("gestureend", this.onGestureEnd, { capture: !0 });
90
+ }
91
+ detach() {
92
+ if (!this.target) return;
93
+ const { svgEl: t } = this.target;
94
+ t.removeEventListener("wheel", this.onWheel, { capture: !0 }), t.removeEventListener("gesturestart", this.onGestureStart, {
95
+ capture: !0
96
+ }), t.removeEventListener("gesturechange", this.onGestureChange, {
97
+ capture: !0
98
+ }), t.removeEventListener("gestureend", this.onGestureEnd, {
99
+ capture: !0
100
+ });
101
+ }
102
+ }
103
+ const O = ":host{display:block}.root{position:relative;width:100%;height:100%}.container{width:100%;height:100%;overflow:hidden}.container.hidden{display:none}.error{padding:1rem;font-size:.875rem;color:var(--llumi-color-danger, oklch(57.7% .245 27.325));border:1px solid color-mix(in oklab,var(--llumi-color-danger, oklch(57.7% .245 27.325)) 35%,transparent);border-radius:var(--llumi-radius, .5rem)}.error p{margin:0;font-weight:500}.error pre{margin:.5rem 0 0;font-size:.75rem;white-space:pre-wrap;font-family:var(--llumi-font-mono, ui-monospace, monospace)}.fit{position:absolute;top:.5rem;right:.5rem;z-index:1;background:color-mix(in oklab,var(--llumi-color-surface, oklch(100% 0 0)) 80%,transparent);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}";
104
+ function G(r = "default") {
105
+ return typeof r == "string" ? { startOnLoad: !1, theme: r } : { startOnLoad: !1, theme: "base", themeVariables: r };
106
+ }
107
+ var q = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, h = (r, t, e, s) => {
108
+ for (var i = s > 1 ? void 0 : s ? $(t, e) : t, o = r.length - 1, n; o >= 0; o--)
109
+ (n = r[o]) && (i = (s ? n(t, e, i) : n(i)) || i);
110
+ return s && i && q(t, e, i), i;
111
+ };
112
+ let I = 0, a = class extends f {
113
+ constructor() {
114
+ super(...arguments), this.theme = "default", this._source = "", this._error = null, this._hasDiagram = !1, this._slot = new E(this, (r) => {
115
+ this._source = r;
116
+ }), this._uid = `llumi-mermaid-${++I}`, this._seq = 0, this._renderTask = Promise.resolve(), this._gestures = new M(this), this._zoomCtx = null, this.fitToContent = () => {
117
+ const r = this._zoomCtx;
118
+ r && r.svgSelection.transition().duration(300).call(r.zoomBehavior.transform, l);
119
+ };
120
+ }
121
+ /** Diagram source: dedented light-DOM text content. */
122
+ get resolvedCode() {
123
+ return this._source;
124
+ }
125
+ /** Test hook: resolves when the in-flight render has settled.
126
+ * Chains through `updateComplete` so that calling this immediately after a
127
+ * property change (before the next Lit update cycle) still awaits the render
128
+ * that `willUpdate` schedules for that change. */
129
+ get whenRendered() {
130
+ return this.updateComplete.then(() => this._renderTask);
131
+ }
132
+ connectedCallback() {
133
+ super.connectedCallback(), this._source = this._slot.text;
134
+ }
135
+ willUpdate(r) {
136
+ (r.has("_source") || r.has("theme")) && (this._renderTask = this.renderDiagram());
137
+ }
138
+ disconnectedCallback() {
139
+ super.disconnectedCallback(), this._seq++, this.teardownZoom(), this._container && (this._container.innerHTML = ""), this.cleanupOrphans();
140
+ }
141
+ cleanupOrphans() {
142
+ var r;
143
+ for (const t of document.querySelectorAll(
144
+ `#d${this._uid}, #${this._uid}`
145
+ ))
146
+ (r = this._container) != null && r.contains(t) || t.remove();
147
+ }
148
+ /** Attach d3-zoom + gestures to the freshly rendered SVG. */
149
+ onSvgRendered(r) {
150
+ var o;
151
+ const t = r.querySelector("svg");
152
+ if (!t) return;
153
+ const e = _(t), s = e.append("g");
154
+ for (const n of Array.from(t.childNodes))
155
+ n !== s.node() && ((o = s.node()) == null || o.appendChild(n));
156
+ t.style.cursor = "grab", t.style.maxWidth = "100%", t.style.maxHeight = "100%", t.setAttribute("height", "100%");
157
+ const i = C().scaleExtent([0.25, 4]).on("zoom", (n) => {
158
+ s.attr("transform", n.transform);
159
+ });
160
+ e.call(i), this._zoomCtx = { svgSelection: e, zoomBehavior: i }, this._gestures.setTarget({
161
+ svgEl: t,
162
+ svgSelection: e,
163
+ zoomBehavior: i
164
+ });
165
+ }
166
+ /** Tear down zoom/gesture state on error/disconnect. */
167
+ teardownZoom() {
168
+ this._gestures.setTarget(null), this._zoomCtx = null;
169
+ }
170
+ /** Test hook: jump the zoom to scale `k` (no transition). */
171
+ zoomTo(r) {
172
+ const t = this._zoomCtx;
173
+ t && t.svgSelection.call(
174
+ // biome-ignore lint/suspicious/noExplicitAny: d3-zoom transform overload
175
+ t.zoomBehavior.transform,
176
+ l.scale(r)
177
+ );
178
+ }
179
+ async renderDiagram() {
180
+ const r = this.resolvedCode, t = ++this._seq;
181
+ if (!r) {
182
+ this._container && (this._container.innerHTML = ""), this.teardownZoom(), this._error = null, this._hasDiagram = !1;
183
+ return;
184
+ }
185
+ try {
186
+ p.initialize(G(this.theme));
187
+ const { svg: e } = await p.render(this._uid, r);
188
+ if (t !== this._seq) return;
189
+ await this.updateComplete;
190
+ const s = this._container;
191
+ if (!s) return;
192
+ s.innerHTML = e, this._error = null, this._hasDiagram = !0, this.onSvgRendered(s), this.dispatchEvent(
193
+ new CustomEvent("llumi-mermaid-render", {
194
+ bubbles: !0,
195
+ composed: !0
196
+ })
197
+ );
198
+ } catch (e) {
199
+ if (t !== this._seq) return;
200
+ const s = e instanceof Error ? e.message : String(e);
201
+ this._container && (this._container.innerHTML = ""), this.teardownZoom(), this._error = s, this._hasDiagram = !1, this.dispatchEvent(
202
+ new CustomEvent("llumi-mermaid-error", {
203
+ detail: { error: s },
204
+ bubbles: !0,
205
+ composed: !0
206
+ })
207
+ );
208
+ } finally {
209
+ this.cleanupOrphans();
210
+ }
211
+ }
212
+ render() {
213
+ return c`<div class="root">
214
+ <div class="container ${this._error ? "hidden" : ""}"></div>
215
+ ${this._hasDiagram && !this._error ? c`<button
216
+ class="fit"
217
+ @click=${this.fitToContent}
218
+ aria-label="Fit diagram to view"
219
+ >
220
+ <i>${S.maximize}</i>
221
+ </button>` : m}
222
+ ${this._error ? c`<div class="error" role="alert">
223
+ <p>Invalid Mermaid syntax</p>
224
+ <pre>${this._error}</pre>
225
+ </div>` : m}
226
+ </div>`;
227
+ }
228
+ };
229
+ a.styles = [
230
+ f.styles,
231
+ d(w),
232
+ d(O)
233
+ ];
234
+ h([
235
+ b({ reflect: !0 })
236
+ ], a.prototype, "theme", 2);
237
+ h([
238
+ u()
239
+ ], a.prototype, "_source", 2);
240
+ h([
241
+ u()
242
+ ], a.prototype, "_error", 2);
243
+ h([
244
+ u()
245
+ ], a.prototype, "_hasDiagram", 2);
246
+ h([
247
+ y(".container")
248
+ ], a.prototype, "_container", 2);
249
+ a = h([
250
+ T("llumi-mermaid")
251
+ ], a);
252
+ export {
253
+ a as LlumiMermaid
254
+ };
package/dist/review.md ADDED
@@ -0,0 +1,141 @@
1
+ # `<llumi-review>`
2
+
3
+ Review wrapper for commenting on light-DOM content. Wrap any content in
4
+ `<llumi-review>`; mark commentable elements with `data-review-id` and selectable
5
+ text regions with `data-review-selectable`. Commenters click an element to leave
6
+ an element-level comment, or select text to leave a selection-level comment
7
+ anchored to the chosen words.
8
+
9
+ ## What it is
10
+
11
+ A `<llumi-review>` element wraps arbitrary light-DOM content and drives an
12
+ XState-driven annotation flow: a floating review bar (toggle/comment-count/
13
+ submit), floating comment dialogs anchored to the edited element or selection,
14
+ and CSS Custom Highlight API painting for commented/edited/hovered ranges.
15
+
16
+ - **Element comments** — click any `[data-review-id]` element to comment on it.
17
+ - **Selection comments** — select text inside any `[data-review-selectable]`
18
+ element; the comment dialog anchors beside the selected range.
19
+ - **Overall comment** — a free-form note covering the whole document, opened from
20
+ the review bar.
21
+ - **Persistence** — set the `persist` attribute to a localStorage key to keep
22
+ the draft across reloads.
23
+
24
+ ## Usage
25
+
26
+ > **Requires the base theme.** Load `@llumi/design-system/base.css` (or
27
+ > `base.css`) on the page for the document-level styles the component adopts.
28
+
29
+ ```html
30
+ <!-- 1. Register the element (auto side-effect) -->
31
+ <script type="module">
32
+ import "@llumi/design-system/review";
33
+ </script>
34
+
35
+ <!-- 2. Wrap content and mark the commentable parts -->
36
+ <llumi-review default-mode="annotating" bar-position="bottom-center">
37
+ <h1 data-review-id="title">Project Proposal</h1>
38
+ <p data-review-selectable="intro">
39
+ Select a sentence in this paragraph to comment on it, or click the heading.
40
+ </p>
41
+ <div data-review-id="metric-1" data-review-selectable="metric-1">
42
+ <strong>42</strong> open issues
43
+ </div>
44
+ </llumi-review>
45
+ ```
46
+
47
+ Or with a named import:
48
+
49
+ ```js
50
+ import { LlumiReview } from "@llumi/design-system/review";
51
+ ```
52
+
53
+ ## Marking up commentable content
54
+
55
+ | Attribute | Applies to | Meaning |
56
+ |-----------|-----------|---------|
57
+ | `data-review-id` | any element | The element is commentable as a whole; clicks open an element-level comment. The value is the `targetId`. |
58
+ | `data-review-selectable` | any element containing text | Text inside the element can be selected to open a selection-level comment anchored to the chosen range. The value is the `targetId`. |
59
+
60
+ Both attributes may be set on the same element (e.g. a paragraph you want both
61
+ click- and selection-commentable). Elements without either attribute are not
62
+ commentable. `targetId` values are arbitrary strings; the component does not
63
+ generate them for you.
64
+
65
+ ## Attributes and properties
66
+
67
+ | Name | Attribute | Type | Default | Notes |
68
+ |------|-----------|------|---------|-------|
69
+ | `defaultMode` | `default-mode` | `"idle" \| "annotating"` | `"idle"` | Whether the component starts in annotation mode. Set `default-mode="annotating"` to enter review mode immediately on connect. |
70
+ | `barPosition` | `bar-position` | `ReviewBarPosition` | `"bottom-center"` | Position of the floating review bar. One of: `top-left`, `top-center`, `top-right`, `bottom-left`, `bottom-center`, `bottom-right`. |
71
+ | `shortcut` | `shortcut` | `string` | `"$mod+Shift+M"` | Keyboard shortcut (tinykeys syntax) to toggle annotation mode. `$mod` is `Cmd` on macOS, `Ctrl` elsewhere. |
72
+ | `persist` | `persist` | `string` | — (off) | When set to a non-empty value, the review draft is persisted to `localStorage` under that key and restored on load. Read once on connect. |
73
+ | `initialData` | — (property) | `ReviewResult` | `undefined` | Pre-loaded review state (existing comments). No HTML attribute. Set before connect. |
74
+ | `customTargets` | — (property) | `CustomTarget[]` | `undefined` | Additional non-DOM comment targets. No HTML attribute. |
75
+
76
+ ## Events
77
+
78
+ Both events bubble and are composed (they cross shadow boundaries).
79
+
80
+ ### `llumi-review-change`
81
+
82
+ Fires on every edit — a comment added, edited, or deleted, or the overall
83
+ comment changed. `detail` is a `ReviewResult`:
84
+
85
+ ```ts
86
+ element.addEventListener("llumi-review-change", (e: CustomEvent<ReviewResult>) => {
87
+ console.log("current review:", e.detail);
88
+ });
89
+ ```
90
+
91
+ ### `llumi-review-submit`
92
+
93
+ Fires when the user clicks Submit / Approve on the review bar. `detail` is
94
+ `{ result, clearPersisted }`:
95
+
96
+ ```ts
97
+ element.addEventListener(
98
+ "llumi-review-submit",
99
+ (e: CustomEvent<ReviewSubmitDetail>) => {
100
+ const { result, clearPersisted } = e.detail;
101
+ // Persist `result` to your server, then:
102
+ clearPersisted(); // discard the localStorage draft for this element
103
+ },
104
+ );
105
+ ```
106
+
107
+ Call `clearPersisted()` only after a confirmed server save; it is a no-op when
108
+ persistence is off. You can also call `element.clearPersisted()` directly.
109
+
110
+ ## Data shapes
111
+
112
+ ```ts
113
+ interface SelectionRange {
114
+ startOffset: number;
115
+ endOffset: number;
116
+ selectedText: string;
117
+ matchIndex: number;
118
+ }
119
+
120
+ interface ReviewComment {
121
+ commentId: string;
122
+ targetId: string; // matches a data-review-id / data-review-selectable value
123
+ content: string;
124
+ createdAt: string; // ISO timestamp
125
+ selection?: SelectionRange; // present for selection-level comments
126
+ }
127
+
128
+ type ReviewResult =
129
+ | { status: "approved" }
130
+ | { status: "commented"; comments: ReviewComment[]; overallComment?: string };
131
+
132
+ interface ReviewSubmitDetail {
133
+ result: ReviewResult;
134
+ clearPersisted: () => void;
135
+ }
136
+ ```
137
+
138
+ ## Dependencies
139
+
140
+ `xstate`, `@floating-ui/dom`, `tinykeys`. This package ships ESM only; consume
141
+ this component via `import "@llumi/design-system/review"`.