web-remarq 0.1.9 → 0.2.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/README.md +41 -8
- package/dist/index.cjs +329 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +329 -4
- package/dist/index.js.map +1 -1
- package/dist/web-remarq.global.global.js +329 -4
- package/dist/web-remarq.global.global.js.map +1 -1
- package/package.json +13 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Visual annotation tool for design review workflows. Framework-agnostic, zero dependencies.
|
|
4
4
|
|
|
5
|
-
Designer annotates UI elements on staging/dev, exports a report. Developer imports the report and sees markers on the exact elements.
|
|
5
|
+
Designer annotates UI elements on staging/dev, exports a report. Developer imports the report and sees markers on the exact elements. Copy annotations as agent-friendly markdown with search hints for AI coding agents.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -47,19 +47,23 @@ Remove all DOM nodes, event listeners, and observers. Full cleanup.
|
|
|
47
47
|
|
|
48
48
|
Switch between `'light'` and `'dark'` themes.
|
|
49
49
|
|
|
50
|
+
### `WebRemarq.copy()`
|
|
51
|
+
|
|
52
|
+
Copy annotations as agent-friendly markdown to clipboard. Includes ranked search hints (CSS selectors, class names, text content, DOM path) so AI coding agents can grep and locate the source code.
|
|
53
|
+
|
|
50
54
|
### `WebRemarq.export(format)`
|
|
51
55
|
|
|
52
|
-
- `'md'` —
|
|
56
|
+
- `'md'` — downloads `.md` file with search hints (same content as `copy()`)
|
|
53
57
|
- `'json'` — downloads `.json` file with full annotation data
|
|
54
58
|
|
|
55
59
|
### `WebRemarq.import(file)`
|
|
56
60
|
|
|
57
|
-
Import annotations from a JSON file. Returns `Promise<{ total, matched, detached }>`.
|
|
61
|
+
Import annotations from a JSON file. Returns `Promise<{ total, matched, otherBreakpoint, detached }>`.
|
|
58
62
|
|
|
59
63
|
```ts
|
|
60
64
|
const input = document.querySelector('input[type="file"]')
|
|
61
65
|
const result = await WebRemarq.import(input.files[0])
|
|
62
|
-
// { total: 12, matched: 10, detached:
|
|
66
|
+
// { total: 12, matched: 10, otherBreakpoint: 1, detached: 1 }
|
|
63
67
|
```
|
|
64
68
|
|
|
65
69
|
### `WebRemarq.getAnnotations(route?)`
|
|
@@ -91,8 +95,9 @@ When a user clicks an element, a multi-signal fingerprint is captured:
|
|
|
91
95
|
|
|
92
96
|
- **Stable anchors** — `data-annotate`, `data-testid`, `id`
|
|
93
97
|
- **Semantics** — tag name, text content, ARIA role/label
|
|
94
|
-
- **Structure** — stable CSS classes (hashes stripped), DOM path, sibling index
|
|
98
|
+
- **Structure** — stable CSS classes (hashes stripped), DOM path with parent classes, sibling index
|
|
95
99
|
- **Parent context** — nearest ancestor's `data-annotate` value
|
|
100
|
+
- **Agent export** — raw classes, CSS Module decomposition (module hint + local class name)
|
|
96
101
|
|
|
97
102
|
### Matching
|
|
98
103
|
|
|
@@ -101,7 +106,34 @@ When loading annotations, elements are found via a fallback chain:
|
|
|
101
106
|
1. Exact match by `data-annotate` or `data-testid`
|
|
102
107
|
2. Exact match by `id`
|
|
103
108
|
3. Fuzzy match using weighted scoring (text similarity, ARIA, classes, DOM path)
|
|
104
|
-
4. Unmatched annotations
|
|
109
|
+
4. Unmatched annotations sorted into "other viewport" or "detached" panels
|
|
110
|
+
|
|
111
|
+
### Viewport Breakpoints
|
|
112
|
+
|
|
113
|
+
Annotations are tagged with a viewport bucket (width rounded to 100px). When resizing:
|
|
114
|
+
|
|
115
|
+
- **Attached** — element found in current viewport
|
|
116
|
+
- **Other viewport** — element not found, but annotation belongs to a different breakpoint (not an error)
|
|
117
|
+
- **Detached** — element not found even in its native breakpoint (real problem)
|
|
118
|
+
|
|
119
|
+
Automatic reconnection when returning to the annotation's native viewport.
|
|
120
|
+
|
|
121
|
+
### Agent-Friendly Copy
|
|
122
|
+
|
|
123
|
+
The Copy button produces markdown with ranked search hints:
|
|
124
|
+
|
|
125
|
+
```markdown
|
|
126
|
+
### 1. [pending] "Button too small on mobile"
|
|
127
|
+
Element: <button> "Submit"
|
|
128
|
+
Viewport: 300px
|
|
129
|
+
|
|
130
|
+
Search hints:
|
|
131
|
+
- `data-testid="submit-btn"` — in template files
|
|
132
|
+
- `"Submit"` — text content in templates
|
|
133
|
+
- `.submitButton` — in CSS Module file (likely `form.module.*`)
|
|
134
|
+
- DOM: div.form-wrapper > form > button.submit
|
|
135
|
+
- Classes: form__submitButton__cEqts flex items-center
|
|
136
|
+
```
|
|
105
137
|
|
|
106
138
|
### Hash Detection
|
|
107
139
|
|
|
@@ -122,11 +154,12 @@ Works without any markup changes, but for guaranteed stable matching add `data-a
|
|
|
122
154
|
|
|
123
155
|
## UI Components
|
|
124
156
|
|
|
125
|
-
- **Toolbar** — fixed bottom-right panel with inspect, export, import, clear, theme, minimize
|
|
157
|
+
- **Toolbar** — fixed bottom-right panel with inspect, copy, export, import, clear, theme, minimize
|
|
126
158
|
- **Inspect mode** — hover to highlight, click to annotate
|
|
127
159
|
- **Markers** — numbered circles (orange = pending, green = resolved)
|
|
128
160
|
- **Popup** — comment input for new annotations, detail view with Resolve/Delete for existing
|
|
129
|
-
- **
|
|
161
|
+
- **Other viewport panel** — annotations from different breakpoints, click to see required viewport
|
|
162
|
+
- **Detached panel** — annotations whose elements can't be found in their native viewport
|
|
130
163
|
|
|
131
164
|
## Build Outputs
|
|
132
165
|
|
package/dist/index.cjs
CHANGED
|
@@ -735,6 +735,61 @@ var CSS = `
|
|
|
735
735
|
.remarq-toast-fade {
|
|
736
736
|
opacity: 0;
|
|
737
737
|
}
|
|
738
|
+
|
|
739
|
+
.remarq-spacing {
|
|
740
|
+
position: fixed;
|
|
741
|
+
top: 0;
|
|
742
|
+
left: 0;
|
|
743
|
+
pointer-events: none;
|
|
744
|
+
z-index: 2147483646;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.remarq-spacing-margin {
|
|
748
|
+
position: fixed;
|
|
749
|
+
background: rgba(249, 115, 22, 0.2);
|
|
750
|
+
pointer-events: none;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.remarq-spacing-padding {
|
|
754
|
+
position: fixed;
|
|
755
|
+
background: rgba(34, 197, 94, 0.2);
|
|
756
|
+
pointer-events: none;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.remarq-spacing-content {
|
|
760
|
+
position: fixed;
|
|
761
|
+
background: rgba(59, 130, 246, 0.15);
|
|
762
|
+
border: 1px dashed rgba(59, 130, 246, 0.5);
|
|
763
|
+
pointer-events: none;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.remarq-spacing-gap {
|
|
767
|
+
position: fixed;
|
|
768
|
+
background: rgba(168, 85, 247, 0.25);
|
|
769
|
+
border: 1px dashed rgba(168, 85, 247, 0.5);
|
|
770
|
+
pointer-events: none;
|
|
771
|
+
display: flex;
|
|
772
|
+
align-items: center;
|
|
773
|
+
justify-content: center;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.remarq-spacing-label {
|
|
777
|
+
position: fixed;
|
|
778
|
+
font-size: 11px;
|
|
779
|
+
font-weight: 700;
|
|
780
|
+
pointer-events: none;
|
|
781
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
782
|
+
line-height: 1;
|
|
783
|
+
text-shadow: 0 0 3px var(--remarq-bg), 0 0 3px var(--remarq-bg);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.remarq-spacing-label-margin { color: #f97316; }
|
|
787
|
+
.remarq-spacing-label-padding { color: #22c55e; }
|
|
788
|
+
.remarq-spacing-label-content { color: #3b82f6; font-size: 10px; }
|
|
789
|
+
.remarq-spacing-label-gap { color: #a855f7; font-size: 10px; }
|
|
790
|
+
|
|
791
|
+
.remarq-toolbar-btn:disabled { opacity: 0.3; cursor: default; }
|
|
792
|
+
.remarq-toolbar-btn:disabled:hover { background: transparent; }
|
|
738
793
|
`;
|
|
739
794
|
function injectStyles() {
|
|
740
795
|
if (document.querySelector(`style[${STYLES_ID}]`)) return;
|
|
@@ -806,6 +861,7 @@ var ThemeManager = class {
|
|
|
806
861
|
// src/ui/toolbar.ts
|
|
807
862
|
var ICONS = {
|
|
808
863
|
inspect: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="7" cy="7" r="4"/><line x1="10" y1="10" x2="14" y2="14"/></svg>',
|
|
864
|
+
spacing: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 2h12M2 14h12M2 2v12M14 2v12"/><path d="M5 5h6v6H5z" stroke-dasharray="2 1"/></svg>',
|
|
809
865
|
copy: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="8" height="9" rx="1"/><path d="M3 11V3a1 1 0 0 1 1-1h6"/></svg>',
|
|
810
866
|
export: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 2v8M4 6l4-4 4 4M2 12h12"/></svg>',
|
|
811
867
|
import: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 10V2M4 6l4 4 4-4M2 12h12"/></svg>',
|
|
@@ -827,6 +883,8 @@ var Toolbar = class {
|
|
|
827
883
|
this.badgeEl.className = "remarq-badge";
|
|
828
884
|
this.badgeEl.style.display = "none";
|
|
829
885
|
this.inspectBtn.appendChild(this.badgeEl);
|
|
886
|
+
this.spacingBtn = this.createButton("spacing", ICONS.spacing, () => callbacks.onSpacingToggle());
|
|
887
|
+
this.spacingBtn.disabled = true;
|
|
830
888
|
const copyBtn = this.createButton("copy", ICONS.copy, () => callbacks.onCopy());
|
|
831
889
|
const exportBtn = this.createButton("export", ICONS.export, (e) => this.toggleExportMenu(e));
|
|
832
890
|
this.fileInput = document.createElement("input");
|
|
@@ -841,8 +899,9 @@ var Toolbar = class {
|
|
|
841
899
|
const clearBtn = this.createButton("clear", ICONS.clear, () => callbacks.onClear());
|
|
842
900
|
const themeBtn = this.createButton("theme", ICONS.theme, () => callbacks.onThemeToggle());
|
|
843
901
|
const minimizeBtn = this.createButton("minimize", ICONS.minimize, () => this.toggleMinimize());
|
|
844
|
-
this.buttons = [this.inspectBtn, copyBtn, exportBtn, importBtn, clearBtn, themeBtn];
|
|
902
|
+
this.buttons = [this.inspectBtn, this.spacingBtn, copyBtn, exportBtn, importBtn, clearBtn, themeBtn];
|
|
845
903
|
this.toolbarEl.appendChild(this.inspectBtn);
|
|
904
|
+
this.toolbarEl.appendChild(this.spacingBtn);
|
|
846
905
|
this.toolbarEl.appendChild(copyBtn);
|
|
847
906
|
this.toolbarEl.appendChild(exportBtn);
|
|
848
907
|
this.toolbarEl.appendChild(importBtn);
|
|
@@ -855,6 +914,15 @@ var Toolbar = class {
|
|
|
855
914
|
setInspectActive(active) {
|
|
856
915
|
this.inspectBtn.classList.toggle("remarq-active", active);
|
|
857
916
|
}
|
|
917
|
+
setSpacingActive(active) {
|
|
918
|
+
this.spacingBtn.classList.toggle("remarq-active", active);
|
|
919
|
+
}
|
|
920
|
+
setSpacingEnabled(enabled) {
|
|
921
|
+
this.spacingBtn.disabled = !enabled;
|
|
922
|
+
if (!enabled) {
|
|
923
|
+
this.spacingBtn.classList.remove("remarq-active");
|
|
924
|
+
}
|
|
925
|
+
}
|
|
858
926
|
setBadgeCount(count) {
|
|
859
927
|
this.badgeEl.textContent = String(count);
|
|
860
928
|
this.badgeEl.style.display = count > 0 ? "flex" : "none";
|
|
@@ -950,6 +1018,9 @@ var Overlay = class {
|
|
|
950
1018
|
this.tooltipEl.style.left = `${x + 12}px`;
|
|
951
1019
|
this.tooltipEl.style.top = `${y - 28}px`;
|
|
952
1020
|
}
|
|
1021
|
+
hideHighlight() {
|
|
1022
|
+
this.overlayEl.style.display = "none";
|
|
1023
|
+
}
|
|
953
1024
|
hide() {
|
|
954
1025
|
this.overlayEl.style.display = "none";
|
|
955
1026
|
this.tooltipEl.style.display = "none";
|
|
@@ -997,6 +1068,229 @@ function getDirectText(el) {
|
|
|
997
1068
|
return text.slice(0, 30);
|
|
998
1069
|
}
|
|
999
1070
|
|
|
1071
|
+
// src/ui/spacing-overlay.ts
|
|
1072
|
+
function parsePx(value) {
|
|
1073
|
+
return parseFloat(value) || 0;
|
|
1074
|
+
}
|
|
1075
|
+
var SpacingOverlay = class {
|
|
1076
|
+
constructor(parent) {
|
|
1077
|
+
this.parent = parent;
|
|
1078
|
+
this.labels = [];
|
|
1079
|
+
this.gapEls = [];
|
|
1080
|
+
this.lastTarget = null;
|
|
1081
|
+
this.containerEl = document.createElement("div");
|
|
1082
|
+
this.containerEl.className = "remarq-spacing";
|
|
1083
|
+
this.containerEl.style.display = "none";
|
|
1084
|
+
this.marginEl = document.createElement("div");
|
|
1085
|
+
this.marginEl.className = "remarq-spacing-margin";
|
|
1086
|
+
this.paddingEl = document.createElement("div");
|
|
1087
|
+
this.paddingEl.className = "remarq-spacing-padding";
|
|
1088
|
+
this.contentEl = document.createElement("div");
|
|
1089
|
+
this.contentEl.className = "remarq-spacing-content";
|
|
1090
|
+
this.containerEl.appendChild(this.marginEl);
|
|
1091
|
+
this.containerEl.appendChild(this.paddingEl);
|
|
1092
|
+
this.containerEl.appendChild(this.contentEl);
|
|
1093
|
+
parent.appendChild(this.containerEl);
|
|
1094
|
+
}
|
|
1095
|
+
show(target) {
|
|
1096
|
+
if (target === this.lastTarget) return;
|
|
1097
|
+
this.lastTarget = target;
|
|
1098
|
+
try {
|
|
1099
|
+
const rect = target.getBoundingClientRect();
|
|
1100
|
+
const cs = window.getComputedStyle(target);
|
|
1101
|
+
const margin = this.readSides(cs, "margin");
|
|
1102
|
+
const padding = this.readSides(cs, "padding");
|
|
1103
|
+
const border = this.readBorderSides(cs);
|
|
1104
|
+
const marginBox = {
|
|
1105
|
+
top: rect.top - margin.top,
|
|
1106
|
+
left: rect.left - margin.left,
|
|
1107
|
+
width: rect.width + margin.left + margin.right,
|
|
1108
|
+
height: rect.height + margin.top + margin.bottom
|
|
1109
|
+
};
|
|
1110
|
+
const paddingBox = {
|
|
1111
|
+
top: rect.top,
|
|
1112
|
+
left: rect.left,
|
|
1113
|
+
width: rect.width,
|
|
1114
|
+
height: rect.height
|
|
1115
|
+
};
|
|
1116
|
+
const contentBox = {
|
|
1117
|
+
top: rect.top + border.top + padding.top,
|
|
1118
|
+
left: rect.left + border.left + padding.left,
|
|
1119
|
+
width: rect.width - border.left - border.right - padding.left - padding.right,
|
|
1120
|
+
height: rect.height - border.top - border.bottom - padding.top - padding.bottom
|
|
1121
|
+
};
|
|
1122
|
+
this.positionEl(this.marginEl, marginBox);
|
|
1123
|
+
this.positionEl(this.paddingEl, paddingBox);
|
|
1124
|
+
this.positionEl(this.contentEl, contentBox);
|
|
1125
|
+
this.clearLabels();
|
|
1126
|
+
this.clearGaps();
|
|
1127
|
+
this.addSideLabels(margin, marginBox, paddingBox, "margin");
|
|
1128
|
+
this.addSideLabels(padding, paddingBox, contentBox, "padding");
|
|
1129
|
+
if (contentBox.width > 40 && contentBox.height > 14) {
|
|
1130
|
+
this.addLabel(
|
|
1131
|
+
`${Math.round(contentBox.width)} \xD7 ${Math.round(contentBox.height)}`,
|
|
1132
|
+
contentBox.top + contentBox.height / 2 - 6,
|
|
1133
|
+
contentBox.left + contentBox.width / 2,
|
|
1134
|
+
"content"
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
this.showGaps(target);
|
|
1138
|
+
this.containerEl.style.display = "block";
|
|
1139
|
+
} catch (e) {
|
|
1140
|
+
this.hide();
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
hide() {
|
|
1144
|
+
this.containerEl.style.display = "none";
|
|
1145
|
+
this.lastTarget = null;
|
|
1146
|
+
this.clearLabels();
|
|
1147
|
+
this.clearGaps();
|
|
1148
|
+
}
|
|
1149
|
+
destroy() {
|
|
1150
|
+
this.clearLabels();
|
|
1151
|
+
this.clearGaps();
|
|
1152
|
+
this.containerEl.remove();
|
|
1153
|
+
}
|
|
1154
|
+
readSides(cs, prop) {
|
|
1155
|
+
return {
|
|
1156
|
+
top: parsePx(cs[`${prop}Top`]),
|
|
1157
|
+
right: parsePx(cs[`${prop}Right`]),
|
|
1158
|
+
bottom: parsePx(cs[`${prop}Bottom`]),
|
|
1159
|
+
left: parsePx(cs[`${prop}Left`])
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
readBorderSides(cs) {
|
|
1163
|
+
return {
|
|
1164
|
+
top: parsePx(cs.borderTopWidth),
|
|
1165
|
+
right: parsePx(cs.borderRightWidth),
|
|
1166
|
+
bottom: parsePx(cs.borderBottomWidth),
|
|
1167
|
+
left: parsePx(cs.borderLeftWidth)
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
positionEl(el, box) {
|
|
1171
|
+
el.style.top = `${box.top}px`;
|
|
1172
|
+
el.style.left = `${box.left}px`;
|
|
1173
|
+
el.style.width = `${Math.max(0, box.width)}px`;
|
|
1174
|
+
el.style.height = `${Math.max(0, box.height)}px`;
|
|
1175
|
+
}
|
|
1176
|
+
addSideLabels(sides, outerBox, innerBox, type) {
|
|
1177
|
+
if (sides.top > 0) {
|
|
1178
|
+
const y = outerBox.top + (innerBox.top - outerBox.top) / 2 - 6;
|
|
1179
|
+
const x = outerBox.left + outerBox.width / 2;
|
|
1180
|
+
this.addLabel(String(Math.round(sides.top)), y, x, type);
|
|
1181
|
+
}
|
|
1182
|
+
if (sides.bottom > 0) {
|
|
1183
|
+
const innerBottom = innerBox.top + innerBox.height;
|
|
1184
|
+
const outerBottom = outerBox.top + outerBox.height;
|
|
1185
|
+
const y = innerBottom + (outerBottom - innerBottom) / 2 - 6;
|
|
1186
|
+
const x = outerBox.left + outerBox.width / 2;
|
|
1187
|
+
this.addLabel(String(Math.round(sides.bottom)), y, x, type);
|
|
1188
|
+
}
|
|
1189
|
+
if (sides.left > 0) {
|
|
1190
|
+
const y = outerBox.top + outerBox.height / 2 - 6;
|
|
1191
|
+
const x = outerBox.left + (innerBox.left - outerBox.left) / 2;
|
|
1192
|
+
this.addLabel(String(Math.round(sides.left)), y, x, type);
|
|
1193
|
+
}
|
|
1194
|
+
if (sides.right > 0) {
|
|
1195
|
+
const innerRight = innerBox.left + innerBox.width;
|
|
1196
|
+
const outerRight = outerBox.left + outerBox.width;
|
|
1197
|
+
const y = outerBox.top + outerBox.height / 2 - 6;
|
|
1198
|
+
const x = innerRight + (outerRight - innerRight) / 2;
|
|
1199
|
+
this.addLabel(String(Math.round(sides.right)), y, x, type);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
addLabel(text, top, left, type) {
|
|
1203
|
+
const label = document.createElement("div");
|
|
1204
|
+
label.className = `remarq-spacing-label remarq-spacing-label-${type}`;
|
|
1205
|
+
label.textContent = text;
|
|
1206
|
+
label.style.top = `${top}px`;
|
|
1207
|
+
label.style.left = `${left}px`;
|
|
1208
|
+
label.style.transform = "translateX(-50%)";
|
|
1209
|
+
this.containerEl.appendChild(label);
|
|
1210
|
+
this.labels.push(label);
|
|
1211
|
+
}
|
|
1212
|
+
clearLabels() {
|
|
1213
|
+
for (const label of this.labels) label.remove();
|
|
1214
|
+
this.labels = [];
|
|
1215
|
+
}
|
|
1216
|
+
showGaps(target) {
|
|
1217
|
+
const targetCs = window.getComputedStyle(target);
|
|
1218
|
+
if (targetCs.display.includes("flex")) {
|
|
1219
|
+
this.showContainerGaps(target, targetCs);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
const parent = target.parentElement;
|
|
1223
|
+
if (!parent) return;
|
|
1224
|
+
const parentCs = window.getComputedStyle(parent);
|
|
1225
|
+
if (!parentCs.display.includes("flex")) return;
|
|
1226
|
+
const rowGap = parsePx(parentCs.rowGap);
|
|
1227
|
+
const columnGap = parsePx(parentCs.columnGap);
|
|
1228
|
+
const direction = parentCs.flexDirection;
|
|
1229
|
+
const isRow = direction === "row" || direction === "row-reverse";
|
|
1230
|
+
const gap = isRow ? columnGap : rowGap;
|
|
1231
|
+
if (gap <= 0) return;
|
|
1232
|
+
const children = Array.from(parent.children);
|
|
1233
|
+
const targetIndex = children.indexOf(target);
|
|
1234
|
+
if (targetIndex === -1) return;
|
|
1235
|
+
if (targetIndex > 0) {
|
|
1236
|
+
this.renderGap(children[targetIndex - 1], target, gap, isRow);
|
|
1237
|
+
}
|
|
1238
|
+
if (targetIndex < children.length - 1) {
|
|
1239
|
+
this.renderGap(target, children[targetIndex + 1], gap, isRow);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
showContainerGaps(container, cs) {
|
|
1243
|
+
const rowGap = parsePx(cs.rowGap);
|
|
1244
|
+
const columnGap = parsePx(cs.columnGap);
|
|
1245
|
+
const direction = cs.flexDirection;
|
|
1246
|
+
const isRow = direction === "row" || direction === "row-reverse";
|
|
1247
|
+
const gap = isRow ? columnGap : rowGap;
|
|
1248
|
+
if (gap <= 0) return;
|
|
1249
|
+
const children = Array.from(container.children);
|
|
1250
|
+
for (let i = 0; i < children.length - 1; i++) {
|
|
1251
|
+
this.renderGap(children[i], children[i + 1], gap, isRow);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
renderGap(before, after, gap, isRow) {
|
|
1255
|
+
const rectBefore = before.getBoundingClientRect();
|
|
1256
|
+
const rectAfter = after.getBoundingClientRect();
|
|
1257
|
+
const gapEl = document.createElement("div");
|
|
1258
|
+
gapEl.className = "remarq-spacing-gap";
|
|
1259
|
+
if (isRow) {
|
|
1260
|
+
const left = Math.min(rectBefore.right, rectAfter.right);
|
|
1261
|
+
const right = Math.max(rectBefore.left, rectAfter.left);
|
|
1262
|
+
const top = Math.min(rectBefore.top, rectAfter.top);
|
|
1263
|
+
const height = Math.max(rectBefore.height, rectAfter.height);
|
|
1264
|
+
gapEl.style.top = `${top}px`;
|
|
1265
|
+
gapEl.style.left = `${left}px`;
|
|
1266
|
+
gapEl.style.width = `${Math.abs(right - left)}px`;
|
|
1267
|
+
gapEl.style.height = `${height}px`;
|
|
1268
|
+
} else {
|
|
1269
|
+
const top = Math.min(rectBefore.bottom, rectAfter.bottom);
|
|
1270
|
+
const bottom = Math.max(rectBefore.top, rectAfter.top);
|
|
1271
|
+
const left = Math.min(rectBefore.left, rectAfter.left);
|
|
1272
|
+
const width = Math.max(rectBefore.width, rectAfter.width);
|
|
1273
|
+
gapEl.style.top = `${top}px`;
|
|
1274
|
+
gapEl.style.left = `${left}px`;
|
|
1275
|
+
gapEl.style.width = `${width}px`;
|
|
1276
|
+
gapEl.style.height = `${Math.abs(bottom - top)}px`;
|
|
1277
|
+
}
|
|
1278
|
+
if (gap >= 10) {
|
|
1279
|
+
const label = document.createElement("span");
|
|
1280
|
+
label.className = "remarq-spacing-label-gap";
|
|
1281
|
+
label.textContent = `gap: ${Math.round(gap)}`;
|
|
1282
|
+
label.style.cssText = "font-size:10px;font-weight:700;pointer-events:none;";
|
|
1283
|
+
gapEl.appendChild(label);
|
|
1284
|
+
}
|
|
1285
|
+
this.containerEl.appendChild(gapEl);
|
|
1286
|
+
this.gapEls.push(gapEl);
|
|
1287
|
+
}
|
|
1288
|
+
clearGaps() {
|
|
1289
|
+
for (const el of this.gapEls) el.remove();
|
|
1290
|
+
this.gapEls = [];
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1000
1294
|
// src/ui/popup.ts
|
|
1001
1295
|
var POPUP_WIDTH = 300;
|
|
1002
1296
|
var POPUP_MARGIN = 8;
|
|
@@ -1383,6 +1677,8 @@ var markers;
|
|
|
1383
1677
|
var detachedPanel;
|
|
1384
1678
|
var routeObserver;
|
|
1385
1679
|
var inspecting = false;
|
|
1680
|
+
var spacingMode = false;
|
|
1681
|
+
var spacingOverlay;
|
|
1386
1682
|
var mutationObserver = null;
|
|
1387
1683
|
var unsubRoute = null;
|
|
1388
1684
|
var refreshScheduled = false;
|
|
@@ -1476,7 +1772,6 @@ function handleInspectClick(e) {
|
|
|
1476
1772
|
if (!target || target.closest("[data-remarq-theme]")) return;
|
|
1477
1773
|
e.preventDefault();
|
|
1478
1774
|
e.stopPropagation();
|
|
1479
|
-
overlay.hide();
|
|
1480
1775
|
setInspecting(false);
|
|
1481
1776
|
const rect = target.getBoundingClientRect();
|
|
1482
1777
|
popup.show(
|
|
@@ -1516,19 +1811,40 @@ function handleInspectHover(e) {
|
|
|
1516
1811
|
if (!inspecting) return;
|
|
1517
1812
|
const target = e.target;
|
|
1518
1813
|
if (!target || target.closest("[data-remarq-theme]")) return;
|
|
1519
|
-
|
|
1814
|
+
if (spacingMode) {
|
|
1815
|
+
overlay.show(target);
|
|
1816
|
+
overlay.hideHighlight();
|
|
1817
|
+
spacingOverlay.show(target);
|
|
1818
|
+
} else {
|
|
1819
|
+
overlay.show(target);
|
|
1820
|
+
}
|
|
1520
1821
|
overlay.updateTooltipPosition(e.clientX, e.clientY);
|
|
1521
1822
|
}
|
|
1522
1823
|
function handleInspectKeydown(e) {
|
|
1824
|
+
var _a, _b;
|
|
1825
|
+
const tag = (_a = e.target) == null ? void 0 : _a.tagName;
|
|
1826
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || ((_b = e.target) == null ? void 0 : _b.isContentEditable)) return;
|
|
1523
1827
|
if (e.key === "Escape" && inspecting) {
|
|
1524
1828
|
setInspecting(false);
|
|
1525
1829
|
overlay.hide();
|
|
1830
|
+
spacingOverlay.hide();
|
|
1831
|
+
}
|
|
1832
|
+
if (e.key === "s" && inspecting) {
|
|
1833
|
+
spacingMode = !spacingMode;
|
|
1834
|
+
toolbar.setSpacingActive(spacingMode);
|
|
1835
|
+
if (!spacingMode) spacingOverlay.hide();
|
|
1526
1836
|
}
|
|
1527
1837
|
}
|
|
1528
1838
|
function setInspecting(value) {
|
|
1529
1839
|
inspecting = value;
|
|
1530
1840
|
toolbar.setInspectActive(value);
|
|
1531
|
-
|
|
1841
|
+
toolbar.setSpacingEnabled(value);
|
|
1842
|
+
if (!value) {
|
|
1843
|
+
overlay.hide();
|
|
1844
|
+
spacingOverlay == null ? void 0 : spacingOverlay.hide();
|
|
1845
|
+
spacingMode = false;
|
|
1846
|
+
toolbar.setSpacingActive(false);
|
|
1847
|
+
}
|
|
1532
1848
|
}
|
|
1533
1849
|
function handleMarkerClick(annotationId) {
|
|
1534
1850
|
var _a;
|
|
@@ -1660,6 +1976,7 @@ var WebRemarq = {
|
|
|
1660
1976
|
storage = new AnnotationStorage();
|
|
1661
1977
|
themeManager = new ThemeManager(document.body, options.theme);
|
|
1662
1978
|
overlay = new Overlay(themeManager.container);
|
|
1979
|
+
spacingOverlay = new SpacingOverlay(themeManager.container);
|
|
1663
1980
|
popup = new Popup(themeManager.container);
|
|
1664
1981
|
markers = new MarkerManager(themeManager.container, handleMarkerClick);
|
|
1665
1982
|
detachedPanel = new DetachedPanel(themeManager.container, (id) => {
|
|
@@ -1669,6 +1986,12 @@ var WebRemarq = {
|
|
|
1669
1986
|
});
|
|
1670
1987
|
toolbar = new Toolbar(themeManager.container, {
|
|
1671
1988
|
onInspect: () => setInspecting(!inspecting),
|
|
1989
|
+
onSpacingToggle: () => {
|
|
1990
|
+
if (!inspecting) return;
|
|
1991
|
+
spacingMode = !spacingMode;
|
|
1992
|
+
toolbar.setSpacingActive(spacingMode);
|
|
1993
|
+
if (!spacingMode) spacingOverlay.hide();
|
|
1994
|
+
},
|
|
1672
1995
|
onCopy: copyToClipboard,
|
|
1673
1996
|
onExportMd: exportMarkdown,
|
|
1674
1997
|
onExportJson: exportJSON,
|
|
@@ -1718,11 +2041,13 @@ var WebRemarq = {
|
|
|
1718
2041
|
detachedPanel == null ? void 0 : detachedPanel.destroy();
|
|
1719
2042
|
popup == null ? void 0 : popup.destroy();
|
|
1720
2043
|
overlay == null ? void 0 : overlay.destroy();
|
|
2044
|
+
spacingOverlay == null ? void 0 : spacingOverlay.destroy();
|
|
1721
2045
|
toolbar == null ? void 0 : toolbar.destroy();
|
|
1722
2046
|
themeManager == null ? void 0 : themeManager.destroy();
|
|
1723
2047
|
removeStyles();
|
|
1724
2048
|
elementCache.clear();
|
|
1725
2049
|
inspecting = false;
|
|
2050
|
+
spacingMode = false;
|
|
1726
2051
|
initialized = false;
|
|
1727
2052
|
} catch (err) {
|
|
1728
2053
|
console.error("[web-remarq] Destroy failed:", err);
|