@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.
- package/dist/components/annotation-marker.svelte +163 -0
- package/dist/components/annotation-marker.svelte.d.ts +11 -0
- package/dist/components/annotation-popup.svelte +669 -0
- package/dist/components/annotation-popup.svelte.d.ts +42 -0
- package/dist/components/highlight-overlay.svelte +48 -0
- package/dist/components/highlight-overlay.svelte.d.ts +8 -0
- package/dist/components/settings-panel.svelte +446 -0
- package/dist/components/settings-panel.svelte.d.ts +24 -0
- package/dist/components/toolbar.svelte +1111 -0
- package/dist/components/toolbar.svelte.d.ts +46 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +37 -0
- package/dist/feedback.svelte +2879 -0
- package/dist/feedback.svelte.d.ts +4 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/dist/layout-mode/catalog.d.ts +16 -0
- package/dist/layout-mode/catalog.js +81 -0
- package/dist/layout-mode/component-actions.svelte +84 -0
- package/dist/layout-mode/component-actions.svelte.d.ts +18 -0
- package/dist/layout-mode/component-picker.svelte +73 -0
- package/dist/layout-mode/component-picker.svelte.d.ts +10 -0
- package/dist/layout-mode/design-mode.svelte +1115 -0
- package/dist/layout-mode/design-mode.svelte.d.ts +24 -0
- package/dist/layout-mode/design-palette.svelte +396 -0
- package/dist/layout-mode/design-palette.svelte.d.ts +20 -0
- package/dist/layout-mode/element-heuristics.d.ts +5 -0
- package/dist/layout-mode/element-heuristics.js +51 -0
- package/dist/layout-mode/freeze.d.ts +6 -0
- package/dist/layout-mode/freeze.js +163 -0
- package/dist/layout-mode/generated-library.d.ts +940 -0
- package/dist/layout-mode/generated-library.js +1445 -0
- package/dist/layout-mode/geometry.d.ts +38 -0
- package/dist/layout-mode/geometry.js +133 -0
- package/dist/layout-mode/history.d.ts +10 -0
- package/dist/layout-mode/history.js +45 -0
- package/dist/layout-mode/index.d.ts +23 -0
- package/dist/layout-mode/index.js +18 -0
- package/dist/layout-mode/live-mount.d.ts +20 -0
- package/dist/layout-mode/live-mount.js +70 -0
- package/dist/layout-mode/output.d.ts +26 -0
- package/dist/layout-mode/output.js +550 -0
- package/dist/layout-mode/placement-skeleton.d.ts +9 -0
- package/dist/layout-mode/placement-skeleton.js +535 -0
- package/dist/layout-mode/rearrange-overlay.svelte +1293 -0
- package/dist/layout-mode/rearrange-overlay.svelte.d.ts +18 -0
- package/dist/layout-mode/responsive-bar.svelte +39 -0
- package/dist/layout-mode/responsive-bar.svelte.d.ts +8 -0
- package/dist/layout-mode/route-creator.svelte +70 -0
- package/dist/layout-mode/route-creator.svelte.d.ts +8 -0
- package/dist/layout-mode/section-detection.d.ts +6 -0
- package/dist/layout-mode/section-detection.js +214 -0
- package/dist/layout-mode/spatial.d.ts +42 -0
- package/dist/layout-mode/spatial.js +156 -0
- package/dist/layout-mode/types.d.ts +144 -0
- package/dist/layout-mode/types.js +84 -0
- package/dist/types.d.ts +157 -0
- package/dist/types.js +1 -0
- package/dist/utils/dryui-detection.d.ts +1 -0
- package/dist/utils/dryui-detection.js +219 -0
- package/dist/utils/element-id.d.ts +12 -0
- package/dist/utils/element-id.js +333 -0
- package/dist/utils/freeze.d.ts +7 -0
- package/dist/utils/freeze.js +168 -0
- package/dist/utils/output.d.ts +15 -0
- package/dist/utils/output.js +245 -0
- package/dist/utils/selection.d.ts +22 -0
- package/dist/utils/selection.js +58 -0
- package/dist/utils/shadow-dom.d.ts +4 -0
- package/dist/utils/shadow-dom.js +39 -0
- package/dist/utils/storage.d.ts +30 -0
- package/dist/utils/storage.js +206 -0
- package/dist/utils/svelte-detection.d.ts +8 -0
- package/dist/utils/svelte-detection.js +86 -0
- package/dist/utils/svelte-meta.d.ts +6 -0
- package/dist/utils/svelte-meta.js +69 -0
- package/dist/utils/sync.d.ts +18 -0
- package/dist/utils/sync.js +62 -0
- package/package.json +65 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import { CANVAS_WIDTHS, COMPONENT_MAP } from './types.js';
|
|
2
|
+
import { analyzeLayoutPatterns, formatCSSPosition, formatPositionSummary, formatSpatialLines, getElementCSSContext, getPageLayout, getSpatialContext } from './spatial.js';
|
|
3
|
+
import { getCatalog } from './catalog.js';
|
|
4
|
+
function definitionFor(type) {
|
|
5
|
+
return COMPONENT_MAP[type];
|
|
6
|
+
}
|
|
7
|
+
function formatSourceReference(type) {
|
|
8
|
+
const definition = definitionFor(type);
|
|
9
|
+
if (!definition)
|
|
10
|
+
return type;
|
|
11
|
+
if (definition.sourceKind === 'block') {
|
|
12
|
+
return `${definition.sourceName} block${definition.routePath ? ` (\`${definition.routePath}\`)` : ''}`;
|
|
13
|
+
}
|
|
14
|
+
return `${definition.sourceName}${definition.sourceImport ? ` from \`${definition.sourceImport}\`` : ''}`;
|
|
15
|
+
}
|
|
16
|
+
function formatStructureReference(type) {
|
|
17
|
+
return definitionFor(type)?.structure ?? null;
|
|
18
|
+
}
|
|
19
|
+
function formatGuidance(type) {
|
|
20
|
+
return definitionFor(type)?.guidance ?? null;
|
|
21
|
+
}
|
|
22
|
+
function formatReferenceFrame(layout) {
|
|
23
|
+
let output = '### Reference Frame\n';
|
|
24
|
+
output += `- Viewport: \`${layout.viewport.width}x${layout.viewport.height}px\`\n`;
|
|
25
|
+
if (layout.contentArea) {
|
|
26
|
+
const area = layout.contentArea;
|
|
27
|
+
output += `- Content area: \`${area.width}px\` wide, left edge at \`x=${area.left}\`, right at \`x=${area.right}\` (\`${area.selector}\`)\n`;
|
|
28
|
+
output += `- Pixel -> CSS translation:\n`;
|
|
29
|
+
output += ` - Horizontal position in container: \`element.x - ${area.left}\`\n`;
|
|
30
|
+
output += ` - Width as % of container: \`element.width / ${area.width} x 100\`\n`;
|
|
31
|
+
output += ` - Centered: \`|element.centerX - ${Math.round(area.centerX)}| < 20px\`\n`;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
output += `- No distinct content container - elements positioned relative to full viewport\n`;
|
|
35
|
+
output += `- Pixel -> CSS translation:\n`;
|
|
36
|
+
output += ` - Width as % of viewport: \`element.width / ${layout.viewport.width} x 100\`\n`;
|
|
37
|
+
}
|
|
38
|
+
return `${output}\n`;
|
|
39
|
+
}
|
|
40
|
+
function formatPlacementList(placements, detailLevel, viewport) {
|
|
41
|
+
const layout = getPageLayout(viewport);
|
|
42
|
+
let output = '### Components\n';
|
|
43
|
+
placements.forEach((placement, index) => {
|
|
44
|
+
const label = COMPONENT_MAP[placement.type]?.label ?? placement.type;
|
|
45
|
+
const rect = { x: placement.x, y: placement.y, width: placement.width, height: placement.height };
|
|
46
|
+
output += `${index + 1}. **${label}** - \`${Math.round(placement.width)}x${Math.round(placement.height)}px\` at \`(${Math.round(placement.x)}, ${Math.round(placement.y)})\`\n`;
|
|
47
|
+
const context = getSpatialContext(rect);
|
|
48
|
+
const lines = formatSpatialLines(context, { includeLeftRight: detailLevel === 'detailed' || detailLevel === 'forensic' });
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
output += ` - ${line}\n`;
|
|
51
|
+
}
|
|
52
|
+
const cssPosition = formatCSSPosition(rect, layout);
|
|
53
|
+
if (cssPosition && !cssPosition.includes('Infinity') && !cssPosition.includes('NaN')) {
|
|
54
|
+
output += ` - CSS: ${cssPosition}\n`;
|
|
55
|
+
}
|
|
56
|
+
output += ` - DryUI: ${formatSourceReference(placement.type)}\n`;
|
|
57
|
+
const guidance = formatGuidance(placement.type);
|
|
58
|
+
if (guidance) {
|
|
59
|
+
output += ` - Guidance: ${guidance}\n`;
|
|
60
|
+
}
|
|
61
|
+
if (detailLevel === 'detailed' || detailLevel === 'forensic') {
|
|
62
|
+
const structure = formatStructureReference(placement.type);
|
|
63
|
+
if (structure) {
|
|
64
|
+
output += ` - Structure: ${structure}\n`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return output;
|
|
69
|
+
}
|
|
70
|
+
function labelFor(type) {
|
|
71
|
+
return COMPONENT_MAP[type]?.label ?? type;
|
|
72
|
+
}
|
|
73
|
+
function groupRows(placements) {
|
|
74
|
+
const rows = [];
|
|
75
|
+
for (const placement of placements) {
|
|
76
|
+
const row = rows.find((candidate) => Math.abs(candidate.y - placement.y) < 30);
|
|
77
|
+
if (row) {
|
|
78
|
+
row.items.push(placement);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
rows.push({ y: placement.y, items: [placement] });
|
|
82
|
+
}
|
|
83
|
+
rows.sort((a, b) => a.y - b.y);
|
|
84
|
+
rows.forEach((row) => row.items.sort((a, b) => a.x - b.x));
|
|
85
|
+
return rows;
|
|
86
|
+
}
|
|
87
|
+
function formatRowAnalysis(placements, viewport) {
|
|
88
|
+
const rows = groupRows(placements);
|
|
89
|
+
let output = '\n### Layout Analysis\n';
|
|
90
|
+
rows.forEach((row, index) => {
|
|
91
|
+
const labels = row.items.map((placement) => labelFor(placement.type));
|
|
92
|
+
if (row.items.length === 1) {
|
|
93
|
+
const placement = row.items[0];
|
|
94
|
+
const isFullWidth = placement ? placement.width > viewport.width * 0.8 : false;
|
|
95
|
+
output += `- Row ${index + 1} (y~${Math.round(row.y)}): ${labels[0]}${isFullWidth ? ' - full width' : ''}\n`;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
output += `- Row ${index + 1} (y~${Math.round(row.y)}): ${labels.join(' | ')} - ${row.items.length} items side by side\n`;
|
|
99
|
+
});
|
|
100
|
+
return output;
|
|
101
|
+
}
|
|
102
|
+
function formatSpacingGaps(placements, detailLevel) {
|
|
103
|
+
if (placements.length < 2 || (detailLevel !== 'detailed' && detailLevel !== 'forensic')) {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
let output = '\n### Spacing & Gaps\n';
|
|
107
|
+
for (let index = 0; index < placements.length - 1; index += 1) {
|
|
108
|
+
const current = placements[index];
|
|
109
|
+
const next = placements[index + 1];
|
|
110
|
+
if (!current || !next)
|
|
111
|
+
continue;
|
|
112
|
+
const currentLabel = labelFor(current.type);
|
|
113
|
+
const nextLabel = labelFor(next.type);
|
|
114
|
+
const verticalGap = Math.round(next.y - (current.y + current.height));
|
|
115
|
+
const horizontalGap = Math.round(next.x - (current.x + current.width));
|
|
116
|
+
if (Math.abs(current.y - next.y) < 30) {
|
|
117
|
+
output += `- ${currentLabel} -> ${nextLabel}: ${horizontalGap}px horizontal gap\n`;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
output += `- ${currentLabel} -> ${nextLabel}: ${verticalGap}px vertical gap\n`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (detailLevel !== 'forensic' || placements.length <= 2) {
|
|
124
|
+
return output;
|
|
125
|
+
}
|
|
126
|
+
output += '\n### All Pairwise Gaps\n';
|
|
127
|
+
for (let index = 0; index < placements.length; index += 1) {
|
|
128
|
+
for (let nextIndex = index + 1; nextIndex < placements.length; nextIndex += 1) {
|
|
129
|
+
const current = placements[index];
|
|
130
|
+
const next = placements[nextIndex];
|
|
131
|
+
if (!current || !next)
|
|
132
|
+
continue;
|
|
133
|
+
const currentLabel = labelFor(current.type);
|
|
134
|
+
const nextLabel = labelFor(next.type);
|
|
135
|
+
const verticalGap = Math.round(next.y - (current.y + current.height));
|
|
136
|
+
const horizontalGap = Math.round(next.x - (current.x + current.width));
|
|
137
|
+
output += `- ${currentLabel} <-> ${nextLabel}: h=${horizontalGap}px v=${verticalGap}px\n`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
output += '\n### Z-Order (placement order)\n';
|
|
141
|
+
placements.forEach((placement, index) => {
|
|
142
|
+
output += `${index}. ${labelFor(placement.type)} at (${Math.round(placement.x)}, ${Math.round(placement.y)})\n`;
|
|
143
|
+
});
|
|
144
|
+
return output;
|
|
145
|
+
}
|
|
146
|
+
function formatSuggestedImplementation(placements) {
|
|
147
|
+
let output = '\n### Suggested Implementation\n';
|
|
148
|
+
const counts = new Map();
|
|
149
|
+
for (const placement of placements) {
|
|
150
|
+
counts.set(placement.type, (counts.get(placement.type) ?? 0) + 1);
|
|
151
|
+
}
|
|
152
|
+
const emitted = new Set();
|
|
153
|
+
for (const placement of placements) {
|
|
154
|
+
if (emitted.has(placement.type))
|
|
155
|
+
continue;
|
|
156
|
+
emitted.add(placement.type);
|
|
157
|
+
const definition = definitionFor(placement.type);
|
|
158
|
+
if (!definition)
|
|
159
|
+
continue;
|
|
160
|
+
const count = counts.get(placement.type) ?? 1;
|
|
161
|
+
output += `- ${definition.label}${count > 1 ? ` x${count}` : ''}: ${definition.guidance}\n`;
|
|
162
|
+
if (definition.structure) {
|
|
163
|
+
output += ` - Structure: ${definition.structure}\n`;
|
|
164
|
+
}
|
|
165
|
+
if (definition.sourceKind === 'block' && definition.routePath) {
|
|
166
|
+
output += ` - Catalog: \`${definition.routePath}\`\n`;
|
|
167
|
+
}
|
|
168
|
+
else if (definition.sourceImport) {
|
|
169
|
+
output += ` - Import: \`${definition.sourceImport}\`\n`;
|
|
170
|
+
}
|
|
171
|
+
if (count > 1 && ['card', 'productCard', 'testimonial', 'feature', 'pricing'].includes(placement.type)) {
|
|
172
|
+
output += ' - Layout: use `Grid` or `Stack` to manage repeated items instead of manual absolute positioning.\n';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const placement of placements) {
|
|
176
|
+
if (!placement.note)
|
|
177
|
+
continue;
|
|
178
|
+
output += `- Note: ${labelFor(placement.type)} - ${placement.note}\n`;
|
|
179
|
+
}
|
|
180
|
+
return output;
|
|
181
|
+
}
|
|
182
|
+
function formatLayoutPrimitiveSuggestions(placements) {
|
|
183
|
+
let output = '\n### DryUI Layout Primitives\n';
|
|
184
|
+
const cards = placements.filter((placement) => placement.type === 'card' || placement.type === 'productCard');
|
|
185
|
+
const sidebar = placements.find((placement) => placement.type === 'sidebar');
|
|
186
|
+
if (sidebar) {
|
|
187
|
+
output += `- Use \`<PageLayout>\`, \`<Sidebar>\`, or \`<Grid>\` for the rail-plus-content split. Equivalent CSS: \`grid-template-columns: ${Math.round(sidebar.width)}px 1fr;\`\n`;
|
|
188
|
+
}
|
|
189
|
+
if (cards.length > 1 && cards[0]) {
|
|
190
|
+
output += `- Use \`<Grid>\` for repeated card tiles instead of manual card coordinates. Equivalent CSS: \`grid-template-columns: repeat(${cards.length}, ${Math.round(cards[0].width)}px); gap: 16px;\`\n`;
|
|
191
|
+
}
|
|
192
|
+
if (placements.some((placement) => placement.type === 'navigation')) {
|
|
193
|
+
output += '- Use `Navbar` or `AppBar` for sticky top-of-page chrome. Equivalent CSS: `position: sticky; top: 0; z-index: 50;`\n';
|
|
194
|
+
}
|
|
195
|
+
return output === '\n### DryUI Layout Primitives\n' ? '' : output;
|
|
196
|
+
}
|
|
197
|
+
function formatParentContext(selector) {
|
|
198
|
+
const context = getElementCSSContext(selector);
|
|
199
|
+
if (!context)
|
|
200
|
+
return null;
|
|
201
|
+
let description = `\`${context.parentDisplay}\``;
|
|
202
|
+
if (context.flexDirection)
|
|
203
|
+
description += `, flex-direction: \`${context.flexDirection}\``;
|
|
204
|
+
if (context.gridCols)
|
|
205
|
+
description += `, grid-template-columns: \`${context.gridCols}\``;
|
|
206
|
+
if (context.gap)
|
|
207
|
+
description += `, gap: \`${context.gap}\``;
|
|
208
|
+
return `Parent: ${description} (\`${context.parentSelector}\`)`;
|
|
209
|
+
}
|
|
210
|
+
function formatParentDryUIHint(selector) {
|
|
211
|
+
const context = getElementCSSContext(selector);
|
|
212
|
+
if (!context)
|
|
213
|
+
return null;
|
|
214
|
+
if (context.parentDisplay === 'grid') {
|
|
215
|
+
return 'DryUI: prefer `Grid` or `PageLayout` so the section order is expressed in layout, not raw offsets.';
|
|
216
|
+
}
|
|
217
|
+
if (context.parentDisplay === 'flex' && context.flexDirection === 'column') {
|
|
218
|
+
return 'DryUI: prefer `Stack` for vertical section ordering and spacing instead of manual y-offset changes.';
|
|
219
|
+
}
|
|
220
|
+
if (context.parentDisplay === 'flex') {
|
|
221
|
+
return 'DryUI: prefer `Flex` for row alignment before reaching for manual x/y adjustments.';
|
|
222
|
+
}
|
|
223
|
+
return 'DryUI: consider `Container`, `Stack`, or a catalog block wrapper before hard-coding the section position.';
|
|
224
|
+
}
|
|
225
|
+
function getRearrangeChanges(state, detailLevel) {
|
|
226
|
+
const changes = [];
|
|
227
|
+
for (const section of state.sections) {
|
|
228
|
+
const moved = Math.abs(section.originalRect.x - section.currentRect.x) > 1 ||
|
|
229
|
+
Math.abs(section.originalRect.y - section.currentRect.y) > 1;
|
|
230
|
+
const resized = Math.abs(section.originalRect.width - section.currentRect.width) > 1 ||
|
|
231
|
+
Math.abs(section.originalRect.height - section.currentRect.height) > 1;
|
|
232
|
+
if (!moved && !resized && detailLevel !== 'forensic') {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
changes.push({ section, moved, resized });
|
|
236
|
+
}
|
|
237
|
+
return changes;
|
|
238
|
+
}
|
|
239
|
+
export function generateDesignOutput(placements, viewport, options, detailLevel = 'standard') {
|
|
240
|
+
if (placements.length === 0 && !options?.wireframePurpose)
|
|
241
|
+
return '';
|
|
242
|
+
const sorted = [...placements].sort((a, b) => (Math.abs(a.y - b.y) < 20 ? a.x - b.x : a.y - b.y));
|
|
243
|
+
let output = '';
|
|
244
|
+
if (options?.blankCanvas) {
|
|
245
|
+
output += '## Wireframe: New Page\n\n';
|
|
246
|
+
if (options.wireframePurpose) {
|
|
247
|
+
output += `> **Purpose:** ${options.wireframePurpose}\n>\n`;
|
|
248
|
+
}
|
|
249
|
+
output += `> ${placements.length} component${placements.length === 1 ? '' : 's'} placed - this is a standalone wireframe, not related to the current page.\n>\n> This wireframe is a rough sketch for exploring ideas.\n\n`;
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
output += `## Design Layout\n\n> ${placements.length} component${placements.length === 1 ? '' : 's'} placed\n\n`;
|
|
253
|
+
}
|
|
254
|
+
if (detailLevel === 'compact') {
|
|
255
|
+
output += '### Components\n';
|
|
256
|
+
sorted.forEach((placement, index) => {
|
|
257
|
+
const label = labelFor(placement.type);
|
|
258
|
+
output += `${index + 1}. **${label}** - \`${Math.round(placement.width)}x${Math.round(placement.height)}px\` at \`(${Math.round(placement.x)}, ${Math.round(placement.y)})\`\n`;
|
|
259
|
+
});
|
|
260
|
+
return output.trim();
|
|
261
|
+
}
|
|
262
|
+
if (placements.length === 0) {
|
|
263
|
+
return output.trim();
|
|
264
|
+
}
|
|
265
|
+
output += formatReferenceFrame(getPageLayout(viewport));
|
|
266
|
+
output += formatPlacementList(sorted, detailLevel, viewport);
|
|
267
|
+
output += formatRowAnalysis(sorted, viewport);
|
|
268
|
+
output += formatSpacingGaps(sorted, detailLevel);
|
|
269
|
+
output += formatSuggestedImplementation(sorted);
|
|
270
|
+
if (detailLevel === 'detailed' || detailLevel === 'forensic') {
|
|
271
|
+
output += formatLayoutPrimitiveSuggestions(sorted);
|
|
272
|
+
}
|
|
273
|
+
return output.trim();
|
|
274
|
+
}
|
|
275
|
+
export function generateRearrangeOutput(state, detailLevel = 'standard', viewport) {
|
|
276
|
+
const changes = getRearrangeChanges(state, detailLevel);
|
|
277
|
+
if (changes.length === 0)
|
|
278
|
+
return '';
|
|
279
|
+
const effectiveViewport = viewport ??
|
|
280
|
+
(typeof window === 'undefined'
|
|
281
|
+
? { width: 0, height: 0 }
|
|
282
|
+
: { width: window.innerWidth, height: window.innerHeight });
|
|
283
|
+
let output = '## Suggested Layout Changes\n\n';
|
|
284
|
+
if (detailLevel !== 'compact') {
|
|
285
|
+
output += formatReferenceFrame(getPageLayout(effectiveViewport));
|
|
286
|
+
}
|
|
287
|
+
if (detailLevel === 'forensic') {
|
|
288
|
+
output += `> Detected at: \`${new Date(state.detectedAt).toISOString()}\`\n`;
|
|
289
|
+
output += `> Total sections: ${state.sections.length}\n\n`;
|
|
290
|
+
}
|
|
291
|
+
output += '**Changes:**\n';
|
|
292
|
+
for (const { section, moved, resized } of changes) {
|
|
293
|
+
const original = section.originalRect;
|
|
294
|
+
const current = section.currentRect;
|
|
295
|
+
if (!moved && !resized) {
|
|
296
|
+
output += `- ${section.label} - unchanged at (${Math.round(current.x)}, ${Math.round(current.y)}) ${Math.round(current.width)}x${Math.round(current.height)}px\n`;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (detailLevel === 'compact') {
|
|
300
|
+
if (moved && resized) {
|
|
301
|
+
output += `- Suggested: move and resize **${section.label}** to (${Math.round(current.x)}, ${Math.round(current.y)}) ${Math.round(current.width)}x${Math.round(current.height)}px\n`;
|
|
302
|
+
}
|
|
303
|
+
else if (moved) {
|
|
304
|
+
output += `- Suggested: move **${section.label}** to (${Math.round(current.x)}, ${Math.round(current.y)})\n`;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
output += `- Suggested: resize **${section.label}** to ${Math.round(current.width)}x${Math.round(current.height)}px\n`;
|
|
308
|
+
}
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (moved && resized) {
|
|
312
|
+
output += `- Suggested: move and resize **${section.label}**\n`;
|
|
313
|
+
}
|
|
314
|
+
else if (moved) {
|
|
315
|
+
output += `- Suggested: move **${section.label}**\n`;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
output += `- Suggested: resize **${section.label}** from ${Math.round(original.width)}x${Math.round(original.height)}px to ${Math.round(current.width)}x${Math.round(current.height)}px\n`;
|
|
319
|
+
}
|
|
320
|
+
output += ` - Current: ${formatPositionSummary(original)}\n`;
|
|
321
|
+
output += ` - Suggested: ${formatPositionSummary(current)}\n`;
|
|
322
|
+
const currentContext = getSpatialContext(current, effectiveViewport);
|
|
323
|
+
const contextLines = formatSpatialLines(currentContext, {
|
|
324
|
+
includeLeftRight: detailLevel === 'detailed' || detailLevel === 'forensic',
|
|
325
|
+
});
|
|
326
|
+
if (contextLines.length > 0) {
|
|
327
|
+
output += ` - Spatial: ${contextLines[0]}\n`;
|
|
328
|
+
for (let index = 1; index < contextLines.length; index += 1) {
|
|
329
|
+
output += ` ${contextLines[index]}\n`;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const cssPosition = formatCSSPosition(current, getPageLayout(effectiveViewport));
|
|
333
|
+
if (cssPosition) {
|
|
334
|
+
output += ` - CSS: ${cssPosition}\n`;
|
|
335
|
+
}
|
|
336
|
+
const parentContext = formatParentContext(section.selector);
|
|
337
|
+
if (parentContext) {
|
|
338
|
+
output += ` - ${parentContext}\n`;
|
|
339
|
+
}
|
|
340
|
+
const dryuiHint = formatParentDryUIHint(section.selector);
|
|
341
|
+
if (dryuiHint) {
|
|
342
|
+
output += ` - ${dryuiHint}\n`;
|
|
343
|
+
}
|
|
344
|
+
output += ` - Selector: \`${section.selector}\`\n`;
|
|
345
|
+
if (section.note)
|
|
346
|
+
output += ` - Note: ${section.note}\n`;
|
|
347
|
+
if (detailLevel === 'detailed' || detailLevel === 'forensic') {
|
|
348
|
+
const identifier = section.className ? `${section.tagName}.${section.className.split(' ')[0]}` : section.tagName;
|
|
349
|
+
if (identifier !== section.selector) {
|
|
350
|
+
output += ` - Element: \`${identifier}\`\n`;
|
|
351
|
+
}
|
|
352
|
+
if (section.role)
|
|
353
|
+
output += ` - Role: \`${section.role}\`\n`;
|
|
354
|
+
if (detailLevel === 'forensic' && section.textSnippet) {
|
|
355
|
+
output += ` - Text: "${section.textSnippet}"\n`;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (detailLevel === 'forensic') {
|
|
359
|
+
output += ` - Original rect: \`{ x: ${Math.round(original.x)}, y: ${Math.round(original.y)}, w: ${Math.round(original.width)}, h: ${Math.round(original.height)} }\`\n`;
|
|
360
|
+
output += ` - Current rect: \`{ x: ${Math.round(current.x)}, y: ${Math.round(current.y)}, w: ${Math.round(current.width)}, h: ${Math.round(current.height)} }\`\n`;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (detailLevel !== 'compact') {
|
|
364
|
+
const patterns = analyzeLayoutPatterns(changes
|
|
365
|
+
.filter((change) => change.moved || change.resized)
|
|
366
|
+
.map((change) => change.section.currentRect));
|
|
367
|
+
if (patterns.length > 0) {
|
|
368
|
+
output += '\n### Layout Summary\n';
|
|
369
|
+
for (const pattern of patterns) {
|
|
370
|
+
output += `- ${pattern}\n`;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (detailLevel !== 'compact' && state.sections.length > 1) {
|
|
375
|
+
output += '\n### All Sections (current positions)\n';
|
|
376
|
+
const sortedSections = [...state.sections].sort((a, b) => Math.abs(a.currentRect.y - b.currentRect.y) < 20
|
|
377
|
+
? a.currentRect.x - b.currentRect.x
|
|
378
|
+
: a.currentRect.y - b.currentRect.y);
|
|
379
|
+
for (const section of sortedSections) {
|
|
380
|
+
const changed = Math.abs(section.currentRect.x - section.originalRect.x) > 1 ||
|
|
381
|
+
Math.abs(section.currentRect.y - section.originalRect.y) > 1 ||
|
|
382
|
+
Math.abs(section.currentRect.width - section.originalRect.width) > 1 ||
|
|
383
|
+
Math.abs(section.currentRect.height - section.originalRect.height) > 1;
|
|
384
|
+
output += `- ${section.label}: \`${Math.round(section.currentRect.width)}x${Math.round(section.currentRect.height)}px\` at \`(${Math.round(section.currentRect.x)}, ${Math.round(section.currentRect.y)})\`${changed ? ' <- suggested' : ''}\n`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return output.trim();
|
|
388
|
+
}
|
|
389
|
+
const CANVAS_LABELS = Object.fromEntries(CANVAS_WIDTHS.map((w) => [w.value, w.label]));
|
|
390
|
+
function canvasLabel(width) {
|
|
391
|
+
return CANVAS_LABELS[width];
|
|
392
|
+
}
|
|
393
|
+
function describeWidth(placementWidth, canvasWidth) {
|
|
394
|
+
const ratio = placementWidth / canvasWidth;
|
|
395
|
+
if (ratio > 0.9)
|
|
396
|
+
return 'full width';
|
|
397
|
+
if (ratio >= 0.45 && ratio <= 0.55)
|
|
398
|
+
return '~half width';
|
|
399
|
+
if (ratio >= 0.3 && ratio <= 0.37)
|
|
400
|
+
return '~third width';
|
|
401
|
+
return `~${Math.round(ratio * 100)}% width`;
|
|
402
|
+
}
|
|
403
|
+
function sketchGroupRows(placements) {
|
|
404
|
+
const rows = [];
|
|
405
|
+
for (const placement of placements) {
|
|
406
|
+
const existing = rows.find((row) => Math.abs(row.y - placement.y) <= 40);
|
|
407
|
+
if (existing) {
|
|
408
|
+
existing.items.push(placement);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
rows.push({ y: placement.y, items: [placement] });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
rows.sort((a, b) => a.y - b.y);
|
|
415
|
+
for (const row of rows)
|
|
416
|
+
row.items.sort((a, b) => a.x - b.x);
|
|
417
|
+
return rows;
|
|
418
|
+
}
|
|
419
|
+
function findCatalogEntry(catalog, componentType) {
|
|
420
|
+
const lower = componentType.toLowerCase();
|
|
421
|
+
return catalog.find((e) => e.name.toLowerCase() === lower || e.tags.some((t) => t.toLowerCase() === lower));
|
|
422
|
+
}
|
|
423
|
+
function formatLayoutSketch(placements, canvasWidth, catalog) {
|
|
424
|
+
if (placements.length === 0)
|
|
425
|
+
return '';
|
|
426
|
+
const rows = sketchGroupRows(placements);
|
|
427
|
+
let out = '### Layout sketch\n\n';
|
|
428
|
+
for (const row of rows) {
|
|
429
|
+
if (row.items.length === 1) {
|
|
430
|
+
const p = row.items[0];
|
|
431
|
+
if (!p)
|
|
432
|
+
continue;
|
|
433
|
+
const widthDesc = describeWidth(p.width, canvasWidth);
|
|
434
|
+
const posDesc = row.y === 0 || row.y < 80 ? 'top' : '';
|
|
435
|
+
const spatial = posDesc ? `${widthDesc}, ${posDesc}` : widthDesc;
|
|
436
|
+
out += `- **${p.type}** — ${spatial}\n`;
|
|
437
|
+
const entry = findCatalogEntry(catalog, p.type);
|
|
438
|
+
if (entry) {
|
|
439
|
+
out += ` - import: \`${entry.importPath}\`\n`;
|
|
440
|
+
if (entry.structure)
|
|
441
|
+
out += ` - structure: \`${entry.structure.split('\n')[0]}\`\n`;
|
|
442
|
+
}
|
|
443
|
+
if (p.text)
|
|
444
|
+
out += ` - text: "${p.text}"\n`;
|
|
445
|
+
if (p.note)
|
|
446
|
+
out += ` - note: ${p.note}\n`;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
out += `- Row (${row.items.length} items, in a row):\n`;
|
|
450
|
+
for (const p of row.items) {
|
|
451
|
+
const widthDesc = describeWidth(p.width, canvasWidth);
|
|
452
|
+
out += ` - **${p.type}** — ${widthDesc}, in a row\n`;
|
|
453
|
+
const entry = findCatalogEntry(catalog, p.type);
|
|
454
|
+
if (entry) {
|
|
455
|
+
out += ` - import: \`${entry.importPath}\`\n`;
|
|
456
|
+
if (entry.structure)
|
|
457
|
+
out += ` - structure: \`${entry.structure.split('\n')[0]}\`\n`;
|
|
458
|
+
}
|
|
459
|
+
if (p.text)
|
|
460
|
+
out += ` - text: "${p.text}"\n`;
|
|
461
|
+
if (p.note)
|
|
462
|
+
out += ` - note: ${p.note}\n`;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return out;
|
|
467
|
+
}
|
|
468
|
+
function formatCompositionHints(placements) {
|
|
469
|
+
const rows = sketchGroupRows(placements);
|
|
470
|
+
const hints = [];
|
|
471
|
+
for (const row of rows) {
|
|
472
|
+
if (row.items.length < 3)
|
|
473
|
+
continue;
|
|
474
|
+
const typeCounts = new Map();
|
|
475
|
+
for (const item of row.items)
|
|
476
|
+
typeCounts.set(item.type, (typeCounts.get(item.type) ?? 0) + 1);
|
|
477
|
+
for (const [type, count] of typeCounts) {
|
|
478
|
+
if (count >= 3)
|
|
479
|
+
hints.push(`${count}× **${type}** in a row — wrap with \`<Grid>\``);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (hints.length === 0)
|
|
483
|
+
return '';
|
|
484
|
+
let out = '### Composition hints\n\n';
|
|
485
|
+
for (const hint of hints)
|
|
486
|
+
out += `- ${hint}\n`;
|
|
487
|
+
return out;
|
|
488
|
+
}
|
|
489
|
+
const LAYOUT_PRIMITIVES_FOOTER = `### DryUI layout primitives
|
|
490
|
+
|
|
491
|
+
- \`<Stack>\` — vertical flex column with consistent gap
|
|
492
|
+
- \`<Flex>\` — horizontal flex row with alignment control
|
|
493
|
+
- \`<Grid>\` — responsive grid for repeated items or two-column layouts
|
|
494
|
+
- \`<Container>\` — centered max-width content wrapper
|
|
495
|
+
`;
|
|
496
|
+
function routeFilePath(route) {
|
|
497
|
+
const clean = route.replace(/^\//, '').replace(/\/$/, '');
|
|
498
|
+
return clean ? `src/routes/${clean}/+page.svelte` : `src/routes/+page.svelte`;
|
|
499
|
+
}
|
|
500
|
+
export function generateSketchBrief(options) {
|
|
501
|
+
const { placements, route, canvasWidth, recipeName } = options;
|
|
502
|
+
const catalog = getCatalog();
|
|
503
|
+
const label = canvasLabel(canvasWidth);
|
|
504
|
+
let out = `## New page: ${route}\n\n**Designed at:** ${canvasWidth}px (${label})\n`;
|
|
505
|
+
if (recipeName)
|
|
506
|
+
out += `**Recipe base:** ${recipeName}\n`;
|
|
507
|
+
out += `**Route file:** \`${routeFilePath(route)}\`\n\n`;
|
|
508
|
+
out += formatLayoutSketch(placements, canvasWidth, catalog);
|
|
509
|
+
const hints = formatCompositionHints(placements);
|
|
510
|
+
if (hints)
|
|
511
|
+
out += '\n' + hints;
|
|
512
|
+
out += '\n' + LAYOUT_PRIMITIVES_FOOTER;
|
|
513
|
+
return out.trim();
|
|
514
|
+
}
|
|
515
|
+
export function generateEditBrief(options) {
|
|
516
|
+
const { actions, annotations, currentUrl, canvasWidth } = options;
|
|
517
|
+
const catalog = getCatalog();
|
|
518
|
+
const label = canvasLabel(canvasWidth);
|
|
519
|
+
let path = currentUrl;
|
|
520
|
+
try {
|
|
521
|
+
path = new URL(currentUrl).pathname;
|
|
522
|
+
}
|
|
523
|
+
catch { }
|
|
524
|
+
let out = `## Feedback: Page modifications at ${path}\n\n**Viewport:** ${canvasWidth}px (${label})\n\n`;
|
|
525
|
+
if (actions.length > 0) {
|
|
526
|
+
out += '### Component Actions\n\n';
|
|
527
|
+
for (const action of actions) {
|
|
528
|
+
if (action.kind === 'swap') {
|
|
529
|
+
out += `- **swap** \`${action.targetSelector}\`: ${action.fromComponent} → ${action.toComponent}\n`;
|
|
530
|
+
out += ` - reason: ${action.reason}\n`;
|
|
531
|
+
const entry = findCatalogEntry(catalog, action.toComponent);
|
|
532
|
+
if (entry)
|
|
533
|
+
out += ` - import: \`${entry.importPath}\`\n`;
|
|
534
|
+
}
|
|
535
|
+
else if (action.kind === 'delete') {
|
|
536
|
+
out += `- **delete** \`${action.targetSelector}\`: remove ${action.component}\n`;
|
|
537
|
+
}
|
|
538
|
+
else if (action.kind === 'refine') {
|
|
539
|
+
out += `- **refine** \`${action.targetSelector}\` (${action.component}): ${action.comment}\n`;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
out += '\n';
|
|
543
|
+
}
|
|
544
|
+
if (annotations.length > 0) {
|
|
545
|
+
out += '### Annotations\n\n';
|
|
546
|
+
for (const annotation of annotations)
|
|
547
|
+
out += `- \`${annotation.selector}\`: ${annotation.note}\n`;
|
|
548
|
+
}
|
|
549
|
+
return out.trim();
|
|
550
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LayoutModeComponentType } from './types.js';
|
|
2
|
+
interface PlacementSkeletonOptions {
|
|
3
|
+
type: LayoutModeComponentType;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
text?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function renderPlacementSkeleton(options: PlacementSkeletonOptions): string;
|
|
9
|
+
export {};
|