@llumi/design-system 1.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +51 -0
- package/NOTICE +66 -0
- package/README.md +15 -205
- package/adopt-global-styles-D9108lqB.js +15 -0
- package/base.css +2 -0
- package/base.md +59 -0
- package/bpmn.md +113 -0
- package/bpmn.mjs +5076 -0
- package/copy-button.md +128 -0
- package/copy-button.mjs +161 -0
- package/custom-elements.json +1135 -18
- package/download-button.md +106 -0
- package/download-button.mjs +177 -0
- package/highlighter.md +45 -0
- package/highlighter.mjs +403 -0
- package/icon-button-BwB11RGC.js +5 -0
- package/icons-BaUbjt-v.js +42 -0
- package/index-BEhA78nX.js +7733 -0
- package/llumi-element-BWRwgzW5.js +22 -0
- package/mermaid.md +58 -0
- package/mermaid.mjs +254 -0
- package/package.json +16 -50
- package/review.md +141 -0
- package/review.mjs +2044 -0
- package/slot-text-mSWBdWBc.js +51 -0
- package/theme.css +2 -0
- package/dist/review.cjs +0 -172
- package/dist/review.css +0 -98
- package/dist/review.js +0 -172
- package/dist/review.mjs +0 -1710
- package/dist/types/components/review/comment-dialog.element.d.ts +0 -29
- package/dist/types/components/review/custom-target.utils.d.ts +0 -16
- package/dist/types/components/review/default-editor.element.d.ts +0 -19
- package/dist/types/components/review/highlight-manager.d.ts +0 -21
- package/dist/types/components/review/index.d.ts +0 -8
- package/dist/types/components/review/overall-comment-dialog.element.d.ts +0 -22
- package/dist/types/components/review/review-bar.element.d.ts +0 -26
- package/dist/types/components/review/review.element.d.ts +0 -60
- package/dist/types/components/review/review.machine.d.ts +0 -234
- package/dist/types/components/review/review.persistence.d.ts +0 -16
- package/dist/types/components/review/review.type.d.ts +0 -37
- package/dist/types/components/review/selection-comment-icons.element.d.ts +0 -17
- package/dist/types/components/review/selection-range.utils.d.ts +0 -6
- package/dist/types/components/review/sort-comments.utils.d.ts +0 -6
- package/dist/types/index.d.ts +0 -1
- package/dist/types/shared/cn.d.ts +0 -2
- package/dist/types/shared/icons.d.ts +0 -7
- 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.1.0 | (c) 2026 pAIrprog SAS | license: https://cdn.jsdelivr.net/npm/@llumi/design-system@2.1.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
|
+
};
|
package/mermaid.md
ADDED
|
@@ -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"`.
|
package/mermaid.mjs
ADDED
|
@@ -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-BwB11RGC.js";
|
|
8
|
+
import { i as S } from "./icons-BaUbjt-v.js";
|
|
9
|
+
import { L as f } from "./llumi-element-BWRwgzW5.js";
|
|
10
|
+
import { S as E } from "./slot-text-mSWBdWBc.js";
|
|
11
|
+
/*! @llumi/design-system v2.1.0 | (c) 2026 pAIrprog SAS | license: https://cdn.jsdelivr.net/npm/@llumi/design-system@2.1.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/package.json
CHANGED
|
@@ -1,51 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"import": "./dist/review.mjs",
|
|
19
|
-
"require": "./dist/review.cjs",
|
|
20
|
-
"default": "./dist/review.mjs"
|
|
21
|
-
},
|
|
22
|
-
"./review.mjs": "./dist/review.mjs",
|
|
23
|
-
"./review.cjs": "./dist/review.cjs",
|
|
24
|
-
"./review.js": "./dist/review.js",
|
|
25
|
-
"./review.css": "./dist/review.css",
|
|
26
|
-
"./package.json": "./package.json"
|
|
27
|
-
},
|
|
28
|
-
"dependencies": {
|
|
29
|
-
"@floating-ui/dom": "^1.6.13",
|
|
30
|
-
"clsx": "^2.1.1",
|
|
31
|
-
"lit": "^3.2.1",
|
|
32
|
-
"tailwind-merge": "^3.0.2",
|
|
33
|
-
"tinykeys": "^3.0.0",
|
|
34
|
-
"xstate": "^5.28.0"
|
|
35
|
-
},
|
|
36
|
-
"devDependencies": {
|
|
37
|
-
"@tailwindcss/vite": "^4.1.18",
|
|
38
|
-
"@vitest/browser": "^3.0.0",
|
|
39
|
-
"playwright": "^1.50.0",
|
|
40
|
-
"tailwindcss": "^4.1.18",
|
|
41
|
-
"vite": "^6.0.0",
|
|
42
|
-
"vite-plugin-dts": "^4.5.0",
|
|
43
|
-
"vitest": "^3.0.0"
|
|
44
|
-
},
|
|
45
|
-
"customElements": "custom-elements.json",
|
|
46
|
-
"scripts": {
|
|
47
|
-
"build": "vite build -c vite.lib.config.ts && vite build -c vite.umd.config.ts && cp src/components/review/review.css dist/review.css",
|
|
48
|
-
"analyze": "cem analyze --config custom-elements-manifest.config.mjs",
|
|
49
|
-
"test:browser": "vitest run"
|
|
50
|
-
}
|
|
51
|
-
}
|
|
2
|
+
"name": "@llumi/design-system",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": [
|
|
7
|
+
"**/*.css",
|
|
8
|
+
"**/*.mjs"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
"./*": "./*"
|
|
12
|
+
},
|
|
13
|
+
"customElements": "custom-elements.json",
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
}
|
|
17
|
+
}
|
package/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"`.
|