@dryui/feedback 0.0.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 (79) hide show
  1. package/dist/components/annotation-marker.svelte +163 -0
  2. package/dist/components/annotation-marker.svelte.d.ts +11 -0
  3. package/dist/components/annotation-popup.svelte +669 -0
  4. package/dist/components/annotation-popup.svelte.d.ts +42 -0
  5. package/dist/components/highlight-overlay.svelte +48 -0
  6. package/dist/components/highlight-overlay.svelte.d.ts +8 -0
  7. package/dist/components/settings-panel.svelte +446 -0
  8. package/dist/components/settings-panel.svelte.d.ts +24 -0
  9. package/dist/components/toolbar.svelte +1111 -0
  10. package/dist/components/toolbar.svelte.d.ts +46 -0
  11. package/dist/constants.d.ts +9 -0
  12. package/dist/constants.js +37 -0
  13. package/dist/feedback.svelte +2879 -0
  14. package/dist/feedback.svelte.d.ts +4 -0
  15. package/dist/index.d.ts +10 -0
  16. package/dist/index.js +7 -0
  17. package/dist/layout-mode/catalog.d.ts +16 -0
  18. package/dist/layout-mode/catalog.js +81 -0
  19. package/dist/layout-mode/component-actions.svelte +84 -0
  20. package/dist/layout-mode/component-actions.svelte.d.ts +18 -0
  21. package/dist/layout-mode/component-picker.svelte +73 -0
  22. package/dist/layout-mode/component-picker.svelte.d.ts +10 -0
  23. package/dist/layout-mode/design-mode.svelte +1115 -0
  24. package/dist/layout-mode/design-mode.svelte.d.ts +24 -0
  25. package/dist/layout-mode/design-palette.svelte +396 -0
  26. package/dist/layout-mode/design-palette.svelte.d.ts +20 -0
  27. package/dist/layout-mode/element-heuristics.d.ts +5 -0
  28. package/dist/layout-mode/element-heuristics.js +51 -0
  29. package/dist/layout-mode/freeze.d.ts +6 -0
  30. package/dist/layout-mode/freeze.js +163 -0
  31. package/dist/layout-mode/generated-library.d.ts +940 -0
  32. package/dist/layout-mode/generated-library.js +1445 -0
  33. package/dist/layout-mode/geometry.d.ts +38 -0
  34. package/dist/layout-mode/geometry.js +133 -0
  35. package/dist/layout-mode/history.d.ts +10 -0
  36. package/dist/layout-mode/history.js +45 -0
  37. package/dist/layout-mode/index.d.ts +23 -0
  38. package/dist/layout-mode/index.js +18 -0
  39. package/dist/layout-mode/live-mount.d.ts +20 -0
  40. package/dist/layout-mode/live-mount.js +70 -0
  41. package/dist/layout-mode/output.d.ts +26 -0
  42. package/dist/layout-mode/output.js +550 -0
  43. package/dist/layout-mode/placement-skeleton.d.ts +9 -0
  44. package/dist/layout-mode/placement-skeleton.js +535 -0
  45. package/dist/layout-mode/rearrange-overlay.svelte +1293 -0
  46. package/dist/layout-mode/rearrange-overlay.svelte.d.ts +18 -0
  47. package/dist/layout-mode/responsive-bar.svelte +39 -0
  48. package/dist/layout-mode/responsive-bar.svelte.d.ts +8 -0
  49. package/dist/layout-mode/route-creator.svelte +70 -0
  50. package/dist/layout-mode/route-creator.svelte.d.ts +8 -0
  51. package/dist/layout-mode/section-detection.d.ts +6 -0
  52. package/dist/layout-mode/section-detection.js +214 -0
  53. package/dist/layout-mode/spatial.d.ts +42 -0
  54. package/dist/layout-mode/spatial.js +156 -0
  55. package/dist/layout-mode/types.d.ts +144 -0
  56. package/dist/layout-mode/types.js +84 -0
  57. package/dist/types.d.ts +157 -0
  58. package/dist/types.js +1 -0
  59. package/dist/utils/dryui-detection.d.ts +1 -0
  60. package/dist/utils/dryui-detection.js +219 -0
  61. package/dist/utils/element-id.d.ts +12 -0
  62. package/dist/utils/element-id.js +333 -0
  63. package/dist/utils/freeze.d.ts +7 -0
  64. package/dist/utils/freeze.js +168 -0
  65. package/dist/utils/output.d.ts +15 -0
  66. package/dist/utils/output.js +245 -0
  67. package/dist/utils/selection.d.ts +22 -0
  68. package/dist/utils/selection.js +58 -0
  69. package/dist/utils/shadow-dom.d.ts +4 -0
  70. package/dist/utils/shadow-dom.js +39 -0
  71. package/dist/utils/storage.d.ts +30 -0
  72. package/dist/utils/storage.js +206 -0
  73. package/dist/utils/svelte-detection.d.ts +8 -0
  74. package/dist/utils/svelte-detection.js +86 -0
  75. package/dist/utils/svelte-meta.d.ts +6 -0
  76. package/dist/utils/svelte-meta.js +69 -0
  77. package/dist/utils/sync.d.ts +18 -0
  78. package/dist/utils/sync.js +62 -0
  79. package/package.json +65 -0
@@ -0,0 +1,18 @@
1
+ import type { RearrangeState, Rect } from './types.js';
2
+ interface Props {
3
+ rearrangeState: RearrangeState;
4
+ onChange: (state: RearrangeState) => void;
5
+ blankCanvas?: boolean;
6
+ extraSnapRects?: Rect[];
7
+ deselectSignal?: number;
8
+ clearSignal?: number;
9
+ exiting?: boolean;
10
+ class?: string;
11
+ onSelectionChange?: (selectedIds: Set<string>, isShift: boolean) => void;
12
+ onInteractionChange?: (active: boolean) => void;
13
+ onDragMove?: (dx: number, dy: number) => void;
14
+ onDragEnd?: (dx: number, dy: number, committed: boolean) => void;
15
+ }
16
+ declare const RearrangeOverlay: import("svelte").Component<Props, {}, "">;
17
+ type RearrangeOverlay = ReturnType<typeof RearrangeOverlay>;
18
+ export default RearrangeOverlay;
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ import { SegmentedControl } from '@dryui/ui';
3
+ import { CANVAS_WIDTHS, type CanvasWidth } from './types.js';
4
+
5
+ interface Props {
6
+ value: CanvasWidth;
7
+ onchange: (width: CanvasWidth) => void;
8
+ }
9
+
10
+ let { value, onchange }: Props = $props();
11
+
12
+ const stringValue = $derived(String(value));
13
+
14
+ function handleChange(next: string) {
15
+ const num = Number(next) as CanvasWidth;
16
+ if (num !== value) onchange(num);
17
+ }
18
+ </script>
19
+
20
+ <div class="responsive-bar" data-dryui-feedback>
21
+ <SegmentedControl.Root value={stringValue} onValueChange={handleChange}>
22
+ {#each CANVAS_WIDTHS as option (option.value)}
23
+ <SegmentedControl.Item value={String(option.value)}>
24
+ {option.label}
25
+ </SegmentedControl.Item>
26
+ {/each}
27
+ </SegmentedControl.Root>
28
+ </div>
29
+
30
+ <style>
31
+ .responsive-bar {
32
+ position: fixed;
33
+ inset-block-start: 0.75rem;
34
+ inset-inline-start: 50%;
35
+ transform: translateX(-50%);
36
+ z-index: 10001;
37
+ pointer-events: auto;
38
+ }
39
+ </style>
@@ -0,0 +1,8 @@
1
+ import { type CanvasWidth } from './types.js';
2
+ interface Props {
3
+ value: CanvasWidth;
4
+ onchange: (width: CanvasWidth) => void;
5
+ }
6
+ declare const ResponsiveBar: import("svelte").Component<Props, {}, "">;
7
+ type ResponsiveBar = ReturnType<typeof ResponsiveBar>;
8
+ export default ResponsiveBar;
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ import { CommandPalette, Badge, Flex, Text } from '@dryui/ui';
3
+ import { compositionRecipes } from '@dryui/mcp/composition-data';
4
+
5
+ interface Props {
6
+ open: boolean;
7
+ onCreate: (routePath: string, recipeName: string | null) => void;
8
+ onClose: () => void;
9
+ }
10
+
11
+ let { open = $bindable(false), onCreate, onClose }: Props = $props();
12
+
13
+ let routePath = $state('');
14
+
15
+ const isRoute = $derived(routePath.startsWith('/'));
16
+
17
+ function handleInput(e: Event) {
18
+ routePath = (e.target as HTMLInputElement).value;
19
+ }
20
+ </script>
21
+
22
+ <CommandPalette.Root bind:open onclose={onClose}>
23
+ <CommandPalette.Input
24
+ placeholder="Type route path, e.g. /dashboard/settings"
25
+ oninput={handleInput}
26
+ />
27
+ <CommandPalette.List>
28
+ <CommandPalette.Empty>No matching recipes found.</CommandPalette.Empty>
29
+ <CommandPalette.Group heading="Options">
30
+ {#if isRoute}
31
+ <CommandPalette.Item
32
+ value="blank {routePath}"
33
+ onSelect={() => {
34
+ onCreate(routePath, null);
35
+ open = false;
36
+ }}
37
+ >
38
+ <Flex align="center" gap="sm">
39
+ <Text weight="semibold" size="sm">Blank canvas</Text>
40
+ <Badge variant="outline" size="sm">{routePath}</Badge>
41
+ </Flex>
42
+ </CommandPalette.Item>
43
+ {/if}
44
+
45
+ {#each compositionRecipes as recipe (recipe.name)}
46
+ <CommandPalette.Item
47
+ value="{recipe.name} {recipe.description} {recipe.tags.join(' ')}"
48
+ onSelect={() => {
49
+ onCreate(routePath || '/new-page', recipe.name);
50
+ open = false;
51
+ }}
52
+ >
53
+ <Flex direction="column" gap="sm">
54
+ <Text weight="semibold" size="sm">{recipe.name}</Text>
55
+ {#if recipe.description}
56
+ <Text size="xs" color="secondary">{recipe.description}</Text>
57
+ {/if}
58
+ {#if recipe.components.length > 0}
59
+ <Flex gap="sm" wrap="wrap">
60
+ {#each recipe.components.slice(0, 5) as component (component)}
61
+ <Badge variant="soft" size="sm">{component}</Badge>
62
+ {/each}
63
+ </Flex>
64
+ {/if}
65
+ </Flex>
66
+ </CommandPalette.Item>
67
+ {/each}
68
+ </CommandPalette.Group>
69
+ </CommandPalette.List>
70
+ </CommandPalette.Root>
@@ -0,0 +1,8 @@
1
+ interface Props {
2
+ open: boolean;
3
+ onCreate: (routePath: string, recipeName: string | null) => void;
4
+ onClose: () => void;
5
+ }
6
+ declare const RouteCreator: import("svelte").Component<Props, {}, "open">;
7
+ type RouteCreator = ReturnType<typeof RouteCreator>;
8
+ export default RouteCreator;
@@ -0,0 +1,6 @@
1
+ import type { DetectedSection } from './types.js';
2
+ export declare function generateSelector(el: HTMLElement): string;
3
+ export declare function labelSection(el: HTMLElement): string;
4
+ export declare function detectPageSections(): DetectedSection[];
5
+ export declare function captureElement(el: HTMLElement): DetectedSection;
6
+ export declare function getSectionLabels(sections: readonly DetectedSection[]): string[];
@@ -0,0 +1,214 @@
1
+ const SECTION_TAGS = new Set(['nav', 'header', 'main', 'section', 'article', 'footer', 'aside']);
2
+ const SECTION_ROLES = {
3
+ banner: 'Header',
4
+ navigation: 'Navigation',
5
+ main: 'Main Content',
6
+ contentinfo: 'Footer',
7
+ complementary: 'Sidebar',
8
+ region: 'Section',
9
+ };
10
+ const TAG_LABELS = {
11
+ nav: 'Navigation',
12
+ header: 'Header',
13
+ main: 'Main Content',
14
+ section: 'Section',
15
+ article: 'Article',
16
+ footer: 'Footer',
17
+ aside: 'Sidebar',
18
+ };
19
+ const SKIP_TAGS = new Set(['script', 'style', 'noscript', 'link', 'meta']);
20
+ const MIN_SECTION_HEIGHT = 40;
21
+ const MAX_UNWRAP_DEPTH = 6;
22
+ function isElementFixed(el) {
23
+ let current = el;
24
+ while (current && current !== document.body && current !== document.documentElement) {
25
+ const pos = window.getComputedStyle(current).position;
26
+ if (pos === 'fixed' || pos === 'sticky')
27
+ return true;
28
+ current = current.parentElement;
29
+ }
30
+ return false;
31
+ }
32
+ function cleanClassName(el) {
33
+ const className = el.className;
34
+ if (typeof className !== 'string' || !className)
35
+ return null;
36
+ const meaningful = className
37
+ .split(/\s+/)
38
+ .map((value) => value.replace(/[_][a-zA-Z0-9]{5,}.*$/, ''))
39
+ .find((value) => value.length > 2 && !/^[a-z]{1,2}$/.test(value));
40
+ return meaningful || null;
41
+ }
42
+ function textSnippet(el) {
43
+ const text = el.textContent?.trim();
44
+ if (!text)
45
+ return null;
46
+ const clean = text.replace(/\s+/g, ' ');
47
+ return clean.length <= 30 ? clean : `${clean.slice(0, 30)}...`;
48
+ }
49
+ function isSectionElement(el) {
50
+ const tag = el.tagName.toLowerCase();
51
+ if (SECTION_TAGS.has(tag))
52
+ return true;
53
+ const role = el.getAttribute('role');
54
+ return Boolean(role && SECTION_ROLES[role]);
55
+ }
56
+ function expandTopLevelCandidates(elements, depth = 0) {
57
+ const expanded = [];
58
+ for (const el of elements) {
59
+ if (!(el instanceof HTMLElement))
60
+ continue;
61
+ const tag = el.tagName.toLowerCase();
62
+ if (SKIP_TAGS.has(tag))
63
+ continue;
64
+ if (el.closest('[data-dryui-feedback]'))
65
+ continue;
66
+ const style = window.getComputedStyle(el);
67
+ if (style.display === 'none' || style.visibility === 'hidden')
68
+ continue;
69
+ if (style.display === 'contents') {
70
+ expanded.push(...expandTopLevelCandidates(Array.from(el.children), depth + 1));
71
+ continue;
72
+ }
73
+ expanded.push(el);
74
+ }
75
+ if (expanded.length !== 1 || depth >= MAX_UNWRAP_DEPTH) {
76
+ return expanded;
77
+ }
78
+ const [only] = expanded;
79
+ if (!only) {
80
+ return expanded;
81
+ }
82
+ if (isSectionElement(only) || isElementFixed(only)) {
83
+ return expanded;
84
+ }
85
+ const children = Array.from(only.children).filter((child) => child instanceof HTMLElement);
86
+ if (children.length === 0) {
87
+ return expanded;
88
+ }
89
+ return expandTopLevelCandidates(children, depth + 1);
90
+ }
91
+ export function generateSelector(el) {
92
+ const tag = el.tagName.toLowerCase();
93
+ if (['nav', 'header', 'footer', 'main'].includes(tag) && document.querySelectorAll(tag).length === 1) {
94
+ return tag;
95
+ }
96
+ if (el.id) {
97
+ return `#${CSS.escape(el.id)}`;
98
+ }
99
+ if (typeof el.className === 'string' && el.className) {
100
+ const classes = el.className.split(/\s+/).filter(Boolean);
101
+ const meaningful = classes.find((value) => value.length > 2 && !/^[a-zA-Z0-9]{6,}$/.test(value) && !/^[a-z]{1,2}$/.test(value));
102
+ if (meaningful) {
103
+ const selector = `${tag}.${CSS.escape(meaningful)}`;
104
+ if (document.querySelectorAll(selector).length === 1) {
105
+ return selector;
106
+ }
107
+ }
108
+ }
109
+ const parent = el.parentElement;
110
+ if (parent) {
111
+ const children = Array.from(parent.children);
112
+ const index = children.indexOf(el) + 1;
113
+ const parentSelector = parent === document.body ? 'body' : generateSelector(parent);
114
+ return `${parentSelector} > ${tag}:nth-child(${index})`;
115
+ }
116
+ return tag;
117
+ }
118
+ export function labelSection(el) {
119
+ const tag = el.tagName.toLowerCase();
120
+ const ariaLabel = el.getAttribute('aria-label');
121
+ if (ariaLabel)
122
+ return ariaLabel;
123
+ const role = el.getAttribute('role');
124
+ if (role && SECTION_ROLES[role])
125
+ return SECTION_ROLES[role];
126
+ if (TAG_LABELS[tag])
127
+ return TAG_LABELS[tag];
128
+ const heading = el.querySelector('h1, h2, h3, h4, h5, h6');
129
+ if (heading) {
130
+ const text = heading.textContent?.trim();
131
+ if (text && text.length <= 50)
132
+ return text;
133
+ if (text)
134
+ return `${text.slice(0, 47)}...`;
135
+ }
136
+ const text = el.textContent?.trim();
137
+ return text ? (text.split(/\s+/)[0] ?? tag) : tag;
138
+ }
139
+ export function detectPageSections() {
140
+ const root = document.querySelector('main') || document.body;
141
+ const candidates = Array.from(root.children);
142
+ const allCandidates = root !== document.body && candidates.length < 3 ? Array.from(document.body.children) : candidates;
143
+ const topLevelCandidates = expandTopLevelCandidates(allCandidates);
144
+ const sections = [];
145
+ topLevelCandidates.forEach((el, index) => {
146
+ if (!(el instanceof HTMLElement))
147
+ return;
148
+ const tag = el.tagName.toLowerCase();
149
+ if (SKIP_TAGS.has(tag))
150
+ return;
151
+ if (el.closest('[data-dryui-feedback]'))
152
+ return;
153
+ const style = window.getComputedStyle(el);
154
+ if (style.display === 'none' || style.visibility === 'hidden')
155
+ return;
156
+ const rect = el.getBoundingClientRect();
157
+ if (rect.height < MIN_SECTION_HEIGHT)
158
+ return;
159
+ const isSemantic = SECTION_TAGS.has(tag);
160
+ const hasRole = Boolean(el.getAttribute('role') && SECTION_ROLES[el.getAttribute('role') ?? '']);
161
+ const isSignificantDiv = tag === 'div' && rect.height >= 60;
162
+ if (!isSemantic && !hasRole && !isSignificantDiv)
163
+ return;
164
+ const isFixed = isElementFixed(el);
165
+ const sectionRect = {
166
+ x: rect.x,
167
+ y: isFixed ? rect.y : rect.y + window.scrollY,
168
+ width: rect.width,
169
+ height: rect.height,
170
+ };
171
+ sections.push({
172
+ id: `rs-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
173
+ label: labelSection(el),
174
+ tagName: tag,
175
+ selector: generateSelector(el),
176
+ role: el.getAttribute('role'),
177
+ className: cleanClassName(el),
178
+ textSnippet: textSnippet(el),
179
+ originalRect: sectionRect,
180
+ currentRect: { ...sectionRect },
181
+ originalIndex: index,
182
+ isFixed,
183
+ });
184
+ });
185
+ return sections;
186
+ }
187
+ export function captureElement(el) {
188
+ const rect = el.getBoundingClientRect();
189
+ const isFixed = isElementFixed(el);
190
+ const sectionRect = {
191
+ x: rect.x,
192
+ y: isFixed ? rect.y : rect.y + window.scrollY,
193
+ width: rect.width,
194
+ height: rect.height,
195
+ };
196
+ const parent = el.parentElement;
197
+ const originalIndex = parent ? Array.from(parent.children).indexOf(el) : 0;
198
+ return {
199
+ id: `rs-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
200
+ label: labelSection(el),
201
+ tagName: el.tagName.toLowerCase(),
202
+ selector: generateSelector(el),
203
+ role: el.getAttribute('role'),
204
+ className: cleanClassName(el),
205
+ textSnippet: textSnippet(el),
206
+ originalRect: sectionRect,
207
+ currentRect: { ...sectionRect },
208
+ originalIndex,
209
+ isFixed,
210
+ };
211
+ }
212
+ export function getSectionLabels(sections) {
213
+ return sections.map((section) => section.label);
214
+ }
@@ -0,0 +1,42 @@
1
+ import type { CSSContext, Rect, SpatialContext, ViewportSize } from './types.js';
2
+ export declare function intersectsRect(a: Rect, b: Rect): boolean;
3
+ export declare function rectFromPoints(a: {
4
+ x: number;
5
+ y: number;
6
+ }, b: {
7
+ x: number;
8
+ y: number;
9
+ }): Rect;
10
+ export declare function hasMeaningfulArea(rect: Rect, threshold?: number): boolean;
11
+ export declare function toRect(rect: DOMRect | Rect): Rect;
12
+ export declare function unionRects(rects: readonly Rect[]): Rect | null;
13
+ export declare function uniqueLabels(labels: readonly string[]): string[];
14
+ export declare function normalizeText(value: string): string;
15
+ export declare function getPopupPosition(rect: Rect, viewportWidth: number, popupWidth?: number): {
16
+ x: number;
17
+ y: number;
18
+ };
19
+ export declare function getSpatialContext(rect: Rect, viewport?: ViewportSize): SpatialContext;
20
+ export declare function formatSpatialLines(context: SpatialContext, options?: {
21
+ includeLeftRight?: boolean;
22
+ }): string[];
23
+ export declare function formatPositionSummary(rect: Rect): string;
24
+ export declare function analyzeLayoutPatterns(rects: readonly Rect[]): string[];
25
+ export declare function getPageLayout(viewport: ViewportSize): {
26
+ viewport: ViewportSize;
27
+ contentArea: {
28
+ selector: string;
29
+ width: number;
30
+ left: number;
31
+ right: number;
32
+ centerX: number;
33
+ } | null;
34
+ };
35
+ export declare function getElementCSSContext(selector: string): CSSContext | null;
36
+ export declare function formatCSSPosition(rect: Rect, layout: {
37
+ viewport: ViewportSize;
38
+ contentArea: {
39
+ left: number;
40
+ width: number;
41
+ } | null;
42
+ }): string | null;
@@ -0,0 +1,156 @@
1
+ function round(value) {
2
+ return Math.round(value);
3
+ }
4
+ export function intersectsRect(a, b) {
5
+ return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
6
+ }
7
+ export function rectFromPoints(a, b) {
8
+ return {
9
+ x: Math.min(a.x, b.x),
10
+ y: Math.min(a.y, b.y),
11
+ width: Math.abs(b.x - a.x),
12
+ height: Math.abs(b.y - a.y),
13
+ };
14
+ }
15
+ export function hasMeaningfulArea(rect, threshold = 8) {
16
+ return rect.width >= threshold && rect.height >= threshold;
17
+ }
18
+ export function toRect(rect) {
19
+ return {
20
+ x: rect.x,
21
+ y: rect.y,
22
+ width: rect.width,
23
+ height: rect.height,
24
+ };
25
+ }
26
+ export function unionRects(rects) {
27
+ if (rects.length === 0)
28
+ return null;
29
+ const x = Math.min(...rects.map((rect) => rect.x));
30
+ const y = Math.min(...rects.map((rect) => rect.y));
31
+ const right = Math.max(...rects.map((rect) => rect.x + rect.width));
32
+ const bottom = Math.max(...rects.map((rect) => rect.y + rect.height));
33
+ return { x, y, width: right - x, height: bottom - y };
34
+ }
35
+ export function uniqueLabels(labels) {
36
+ return Array.from(new Set(labels.filter(Boolean)));
37
+ }
38
+ export function normalizeText(value) {
39
+ return value.replace(/\s+/g, ' ').trim();
40
+ }
41
+ export function getPopupPosition(rect, viewportWidth, popupWidth = 340) {
42
+ const x = rect.x + rect.width + 16 > viewportWidth ? Math.max(16, rect.x - popupWidth - 16) : Math.min(rect.x + rect.width + 16, viewportWidth - popupWidth - 16);
43
+ const y = Math.max(16, rect.y);
44
+ return { x, y };
45
+ }
46
+ export function getSpatialContext(rect, viewport = {
47
+ width: typeof window === 'undefined' ? 0 : window.innerWidth,
48
+ height: typeof window === 'undefined' ? 0 : window.innerHeight,
49
+ }) {
50
+ const centerX = rect.x + rect.width / 2;
51
+ const centerY = rect.y + rect.height / 2;
52
+ return {
53
+ top: rect.y,
54
+ right: viewport.width - (rect.x + rect.width),
55
+ bottom: viewport.height - (rect.y + rect.height),
56
+ left: rect.x,
57
+ centerX,
58
+ centerY,
59
+ rowCount: Math.max(1, Math.round(rect.height / 96)),
60
+ columnCount: Math.max(1, Math.round(rect.width / 160)),
61
+ };
62
+ }
63
+ export function formatSpatialLines(context, options) {
64
+ const lines = [
65
+ `Top: ${round(context.top ?? 0)}px`,
66
+ `Bottom: ${round(context.bottom ?? 0)}px`,
67
+ `Center: ${round(context.centerX)} x ${round(context.centerY)}`,
68
+ `Approx layout: ${context.rowCount} row${context.rowCount === 1 ? '' : 's'} x ${context.columnCount} column${context.columnCount === 1 ? '' : 's'}`,
69
+ ];
70
+ if (options?.includeLeftRight) {
71
+ lines.splice(1, 0, `Left: ${round(context.left ?? 0)}px`, `Right: ${round(context.right ?? 0)}px`);
72
+ }
73
+ return lines;
74
+ }
75
+ export function formatPositionSummary(rect) {
76
+ return `${round(rect.width)}x${round(rect.height)} at (${round(rect.x)}, ${round(rect.y)})`;
77
+ }
78
+ export function analyzeLayoutPatterns(rects) {
79
+ if (rects.length === 0)
80
+ return [];
81
+ const ordered = [...rects].sort((a, b) => (Math.abs(a.y - b.y) < 20 ? a.x - b.x : a.y - b.y));
82
+ const rows = [];
83
+ for (const rect of ordered) {
84
+ const row = rows.find((group) => {
85
+ const first = group[0];
86
+ return first ? Math.abs(first.y - rect.y) < 30 : false;
87
+ });
88
+ if (row) {
89
+ row.push(rect);
90
+ }
91
+ else {
92
+ rows.push([rect]);
93
+ }
94
+ }
95
+ return rows.map((row, index) => {
96
+ if (row.length === 1) {
97
+ return `Row ${index + 1}: single element`;
98
+ }
99
+ return `Row ${index + 1}: ${row.length} items side by side`;
100
+ });
101
+ }
102
+ export function getPageLayout(viewport) {
103
+ if (typeof document === 'undefined') {
104
+ return { viewport, contentArea: null };
105
+ }
106
+ const roots = Array.from(document.body.children).filter((el) => el instanceof HTMLElement);
107
+ const content = roots
108
+ .map((el) => ({ el, rect: el.getBoundingClientRect() }))
109
+ .filter(({ rect }) => rect.width > viewport.width * 0.35 && rect.width < viewport.width * 0.95 && rect.left > 0 && rect.right < viewport.width)
110
+ .sort((a, b) => Math.abs(a.rect.width - viewport.width * 0.7) - Math.abs(b.rect.width - viewport.width * 0.7))[0];
111
+ if (!content)
112
+ return { viewport, contentArea: null };
113
+ return {
114
+ viewport,
115
+ contentArea: {
116
+ selector: content.el.tagName.toLowerCase(),
117
+ width: content.rect.width,
118
+ left: content.rect.left,
119
+ right: content.rect.right,
120
+ centerX: content.rect.left + content.rect.width / 2,
121
+ },
122
+ };
123
+ }
124
+ export function getElementCSSContext(selector) {
125
+ if (typeof document === 'undefined' ||
126
+ typeof window === 'undefined' ||
127
+ typeof HTMLElement === 'undefined') {
128
+ return null;
129
+ }
130
+ const element = document.querySelector(selector);
131
+ if (!(element instanceof HTMLElement))
132
+ return null;
133
+ const parent = element.parentElement;
134
+ if (!(parent instanceof HTMLElement))
135
+ return null;
136
+ const style = window.getComputedStyle(parent);
137
+ return {
138
+ parentSelector: parent === document.body ? 'body' : parent.tagName.toLowerCase(),
139
+ parentDisplay: style.display || 'block',
140
+ flexDirection: style.flexDirection || null,
141
+ gridCols: style.gridTemplateColumns || null,
142
+ gap: style.gap || null,
143
+ };
144
+ }
145
+ export function formatCSSPosition(rect, layout) {
146
+ if (!layout.contentArea) {
147
+ if (layout.viewport.width <= 0)
148
+ return null;
149
+ return `width: ${Math.max(0, Math.round((rect.width / layout.viewport.width) * 100))}%`;
150
+ }
151
+ if (layout.contentArea.width <= 0)
152
+ return null;
153
+ const x = round(rect.x - layout.contentArea.left);
154
+ const width = Math.max(0, round((rect.width / layout.contentArea.width) * 100));
155
+ return `left: ${x}px; width: ${width}%`;
156
+ }