@ahmednawaz/crank 0.1.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.
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Crank UI helpers — reused from Vizpatch tool-ui / vizpatch-ui-helpers.
3
+ */
4
+ (function (root) {
5
+ 'use strict';
6
+
7
+ function escapeHtml(value) {
8
+ return String(value == null ? '' : value)
9
+ .replace(/&/g, '&')
10
+ .replace(/</g, '&lt;')
11
+ .replace(/>/g, '&gt;')
12
+ .replace(/"/g, '&quot;')
13
+ .replace(/'/g, '&#39;');
14
+ }
15
+
16
+ function escapeAttr(value) {
17
+ return escapeHtml(value).replace(/\n/g, ' ');
18
+ }
19
+
20
+ /* VizPatch UI markup helpers — mirrors tool-ui InputField / Button structure */
21
+
22
+ function vpJoinClasses() {
23
+ const parts = [];
24
+ for (let i = 0; i < arguments.length; i++) {
25
+ const value = arguments[i];
26
+ if (value) {
27
+ parts.push(String(value));
28
+ }
29
+ }
30
+ return parts.join(' ');
31
+ }
32
+
33
+ function vpShellSizeClass(size) {
34
+ return size === 'compact'
35
+ ? 'vp-compact-control-shell vp-compact-control-typography'
36
+ : 'vp-control-shell vp-control-typography';
37
+ }
38
+
39
+ function vpBuildTagAttrs(attrs) {
40
+ if (!attrs || typeof attrs !== 'object') {
41
+ return '';
42
+ }
43
+ let html = '';
44
+ Object.keys(attrs).forEach((key) => {
45
+ const value = attrs[key];
46
+ if (value === undefined || value === null || value === false) {
47
+ return;
48
+ }
49
+ if (value === true) {
50
+ html += ' ' + key;
51
+ return;
52
+ }
53
+ html += ' ' + key + '="' + escapeAttr(String(value)) + '"';
54
+ });
55
+ return html;
56
+ }
57
+
58
+ function vpInputSurface(attrs, innerHtml, options) {
59
+ const opts = options || {};
60
+ const size = opts.size === 'compact' ? 'compact' : 'default';
61
+ const shellClass = vpShellSizeClass(size);
62
+ const widthClass = opts.width ? '' : ' style="width:100%;"';
63
+ const widthStyle = opts.width ? ' style="width:' + escapeAttr(String(opts.width)) + ';"' : widthClass;
64
+ const extraClass = opts.className ? ' ' + opts.className : '';
65
+ const fixedClass = opts.width ? ' vp-input-surface--fixed' : '';
66
+ const disabledClass = attrs && attrs.disabled ? ' vp-input-surface-disabled' : '';
67
+ return '<div class="vp-input-surface vp-control-surface' + fixedClass + disabledClass + ' ' + shellClass + extraClass + '"' + widthStyle + '>'
68
+ + innerHtml
69
+ + '</div>';
70
+ }
71
+
72
+ function vpInputHtml(attrs, options) {
73
+ const opts = options || {};
74
+ const size = opts.size === 'compact' ? 'compact' : 'default';
75
+ const typeClass = size === 'compact' ? 'vp-compact-control-typography' : 'vp-control-typography';
76
+ const inputAttrs = Object.assign({}, attrs || {}, { class: vpJoinClasses('vp-input-native', typeClass, attrs && attrs.class) });
77
+ return vpInputSurface(attrs, '<input' + vpBuildTagAttrs(inputAttrs) + ' />', opts);
78
+ }
79
+
80
+ function vpChevronSvgHtml() {
81
+ return '<svg class="vp-field-select-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>';
82
+ }
83
+
84
+ function vpParseSelectOptions(optionsHtml) {
85
+ const options = [];
86
+ const pattern = /<option([^>]*)>([\s\S]*?)<\/option>/gi;
87
+ let match = pattern.exec(optionsHtml || '');
88
+ while (match) {
89
+ const attrStr = match[1] || '';
90
+ const label = match[2] || '';
91
+ const valueMatch = attrStr.match(/value="([^"]*)"/i);
92
+ const value = valueMatch ? valueMatch[1] : label;
93
+ options.push({
94
+ value: value,
95
+ label: label,
96
+ selected: /\bselected\b/i.test(attrStr),
97
+ disabled: /\bdisabled\b/i.test(attrStr)
98
+ });
99
+ match = pattern.exec(optionsHtml || '');
100
+ }
101
+ return options;
102
+ }
103
+
104
+ function vpNormalizeSelectOptions(optionList, optionsHtml) {
105
+ if (Array.isArray(optionList) && optionList.length) {
106
+ return optionList.map(function (entry) {
107
+ return {
108
+ value: String(entry.value != null ? entry.value : ''),
109
+ label: String(entry.label != null ? entry.label : entry.value != null ? entry.value : ''),
110
+ selected: !!entry.selected,
111
+ disabled: !!entry.disabled
112
+ };
113
+ });
114
+ }
115
+ return vpParseSelectOptions(optionsHtml || '');
116
+ }
117
+
118
+ function vpFieldLabelHtml(label, options) {
119
+ const opts = options || {};
120
+ const size = opts.size === 'compact' ? 'compact' : 'default';
121
+ const sizeClass = size === 'compact' ? 'vp-field-label-grid--compact' : 'vp-field-label-grid--default';
122
+ const text = String(label || '');
123
+ if (!text || opts.hideLabel) {
124
+ return '';
125
+ }
126
+ return '<span class="vp-field-label-grid ' + sizeClass + ' vp-compact-control-typography">'
127
+ + '<span class="vp-field-label-reserve" aria-hidden="true">' + escapeHtml(text) + '</span>'
128
+ + '<span class="vp-field-label-visible">' + escapeHtml(text) + '</span>'
129
+ + '</span>';
130
+ }
131
+
132
+ function vpFieldHtml(label, controlHtml, options) {
133
+ const opts = options || {};
134
+ const labelHtml = vpFieldLabelHtml(label, opts);
135
+ const extraClass = opts.className ? ' ' + opts.className : '';
136
+ const metaHtml = opts.metaHtml ? '<div class="vp-field-meta">' + opts.metaHtml + '</div>' : '';
137
+ return '<div class="vp-field' + extraClass + '">'
138
+ + labelHtml
139
+ + (controlHtml || '')
140
+ + metaHtml
141
+ + '</div>';
142
+ }
143
+
144
+ function vpFieldSelectHtml(attrs, optionsHtml, options) {
145
+ const opts = options || {};
146
+ const size = opts.size === 'compact' ? 'compact' : 'default';
147
+ const shellClass = vpShellSizeClass(size);
148
+ const typeClass = size === 'compact' ? 'vp-compact-control-typography' : 'vp-control-typography';
149
+ const optionEntries = vpNormalizeSelectOptions(opts.options, optionsHtml);
150
+ let selectedValue = attrs && attrs.value != null ? String(attrs.value) : '';
151
+ if (!selectedValue) {
152
+ const selectedEntry = optionEntries.find(function (entry) { return entry.selected; });
153
+ if (selectedEntry) {
154
+ selectedValue = selectedEntry.value;
155
+ } else if (optionEntries.length) {
156
+ selectedValue = optionEntries[0].value;
157
+ }
158
+ }
159
+ const selectedEntry = optionEntries.find(function (entry) {
160
+ return entry.value === selectedValue;
161
+ }) || optionEntries[0];
162
+ const selectedLabel = selectedEntry ? selectedEntry.label : (opts.placeholder || 'Select');
163
+ const hiddenAttrs = Object.assign({}, attrs || {}, {
164
+ type: 'hidden',
165
+ class: 'vp-field-select-value',
166
+ value: selectedValue
167
+ });
168
+ const menuItems = optionEntries.map(function (entry) {
169
+ const isChecked = entry.value === selectedValue;
170
+ return '<button type="button" class="vp-menu-item' + (isChecked ? ' is-checked' : '') + '" role="menuitemradio" aria-checked="' + (isChecked ? 'true' : 'false') + '" data-value="' + escapeAttr(entry.value) + '"' + (entry.disabled ? ' disabled' : '') + '>'
171
+ + '<span class="vp-menu-item__label">' + escapeHtml(entry.label) + '</span>'
172
+ + (isChecked ? '<svg class="vp-menu-item__check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12L9 17L20 6"/></svg>' : '')
173
+ + '</button>';
174
+ }).join('');
175
+ const triggerLabel = escapeHtml(selectedLabel);
176
+ const widthStyle = opts.width ? ' style="width:' + escapeAttr(String(opts.width)) + ';"' : ' style="width:100%;"';
177
+ return '<div class="vp-field-select"' + widthStyle + '>'
178
+ + '<input' + vpBuildTagAttrs(hiddenAttrs) + ' />'
179
+ + '<div class="vp-input-surface vp-control-surface ' + shellClass + '" data-vizpatch-part="control-surface" style="width:100%;">'
180
+ + '<button type="button" class="vp-field-select-trigger ' + typeClass + '" aria-haspopup="listbox" aria-expanded="false">'
181
+ + '<span class="vp-field-select-label">' + triggerLabel + '</span>'
182
+ + vpChevronSvgHtml()
183
+ + '</button>'
184
+ + '</div>'
185
+ + '<div class="vp-dropdown-menu vizpatch-hidden" role="menu">' + menuItems + '</div>'
186
+ + '</div>';
187
+ }
188
+
189
+ function vpSelectHtml(attrs, optionsHtml, options) {
190
+ return vpFieldSelectHtml(attrs, optionsHtml, options);
191
+ }
192
+
193
+ function vpTextareaHtml(attrs, content, options) {
194
+ const opts = options || {};
195
+ const size = opts.size === 'compact' ? 'compact' : 'default';
196
+ const typeClass = size === 'compact' ? 'vp-compact-control-typography' : 'vp-control-typography';
197
+ const areaAttrs = Object.assign({}, attrs || {}, { class: vpJoinClasses('vp-textarea-native', typeClass, attrs && attrs.class) });
198
+ const shellClass = vpShellSizeClass(size);
199
+ return '<div class="vp-input-surface vp-input-surface--textarea vp-control-surface ' + shellClass + '" data-vizpatch-part="control-surface" style="width:100%;">'
200
+ + '<textarea' + vpBuildTagAttrs(areaAttrs) + '>' + escapeHtml(content || '') + '</textarea>'
201
+ + '</div>';
202
+ }
203
+
204
+ function vpButtonShellClasses(variant, size) {
205
+ const isCompact = size === 'compact';
206
+ const shellClass = isCompact ? 'vp-compact-control-shell' : 'vp-control-shell';
207
+ const typeClass = isCompact ? 'vp-compact-control-typography' : 'vp-control-typography';
208
+ return vpJoinClasses(shellClass, typeClass);
209
+ }
210
+
211
+ function vpButtonHtml(variant, label, attrs, options) {
212
+ const opts = options || {};
213
+ const size = opts.size === 'compact' ? 'compact' : 'default';
214
+ let surfaceClass = 'vp-button-secondary-surface';
215
+ if (variant === 'primary') {
216
+ surfaceClass = 'vp-button-primary-surface';
217
+ } else if (variant === 'ghost') {
218
+ surfaceClass = 'vp-button-ghost-surface';
219
+ }
220
+ const iconClass = opts.icon ? ' vp-btn-icon' : '';
221
+ const activeClass = opts.active ? ' is-active' : '';
222
+ const surfaceControlClass = variant === 'ghost' ? '' : 'vp-button-surface-control';
223
+ const buttonAttrs = Object.assign({}, attrs || {}, {
224
+ type: (attrs && attrs.type) || 'button',
225
+ class: vpJoinClasses(
226
+ surfaceClass,
227
+ surfaceControlClass,
228
+ vpButtonShellClasses(variant, size),
229
+ iconClass,
230
+ activeClass,
231
+ opts.className,
232
+ attrs && attrs.class
233
+ )
234
+ });
235
+ const style = opts.flex ? ' style="flex:1;min-width:0;"' : '';
236
+ return '<button' + vpBuildTagAttrs(buttonAttrs) + style + '>' + (label || '') + '</button>';
237
+ }
238
+
239
+ function vpButtonGroupHtml(buttonsHtml, options) {
240
+ const opts = options || {};
241
+ const groupClass = vpJoinClasses(
242
+ 'vp-button-group',
243
+ opts.stretch ? 'vp-button-group--stretch' : '',
244
+ opts.className
245
+ );
246
+ return '<div class="' + groupClass + '">' + (buttonsHtml || '') + '</div>';
247
+ }
248
+
249
+ function vpTabLabelHtml(label, active) {
250
+ const text = String(label || '');
251
+ const activeClass = active ? ' is-active' : '';
252
+ return '<span class="vp-tab-label-grid vp-type-support">'
253
+ + '<span class="vp-tab-label-reserve" aria-hidden="true">' + escapeHtml(text) + '</span>'
254
+ + '<span class="vp-tab-label-visible' + activeClass + '">' + escapeHtml(text) + '</span>'
255
+ + '</span>';
256
+ }
257
+
258
+ function vpTabsHtml(tabs) {
259
+ const items = (tabs || []).map((tab, index) => {
260
+ const active = !!tab.active;
261
+ const activeClass = active ? ' is-active' : '';
262
+ const attrs = Object.assign({}, tab.attrs || {}, {
263
+ type: 'button',
264
+ role: 'tab',
265
+ class: 'vp-tab' + activeClass,
266
+ id: tab.id,
267
+ 'aria-selected': active ? 'true' : 'false',
268
+ 'data-proximity-index': String(index)
269
+ });
270
+ return '<button' + vpBuildTagAttrs(attrs) + '>' + vpTabLabelHtml(tab.label || '', active) + '</button>';
271
+ }).join('');
272
+ return '<div class="vp-tabs vp-control-surface vp-list-surface" role="tablist">'
273
+ + '<div class="vp-tab-indicator vp-tab-indicator--selected vp-control-surface-overlay" aria-hidden="true"></div>'
274
+ + '<div class="vp-tab-indicator vp-tab-indicator--hover vp-control-surface-overlay" aria-hidden="true"></div>'
275
+ + items
276
+ + '</div>';
277
+ }
278
+
279
+ function vpChipHtml(text) {
280
+ return '<span class="vp-chip">' + escapeHtml(text || '') + '</span>';
281
+ }
282
+
283
+ function vpCapabilityBadgeHtml(mode, text) {
284
+ const cls = mode === 'editable'
285
+ ? 'vp-cap-badge vp-cap-badge--editable'
286
+ : mode === 'read-only'
287
+ ? 'vp-cap-badge vp-cap-badge--readonly'
288
+ : 'vp-cap-badge vp-cap-badge--preview';
289
+ return '<span class="' + cls + '">' + escapeHtml(text || '') + '</span>';
290
+ }
291
+
292
+ function vpCapabilityLabel(mode) {
293
+ if (mode === 'editable') {
294
+ return 'Can save';
295
+ }
296
+ if (mode === 'read-only') {
297
+ return 'Read only';
298
+ }
299
+ return 'Preview only';
300
+ }
301
+
302
+ function vpSegmentButtonHtml(label, attrs, selected, compact) {
303
+ const buttonAttrs = Object.assign({}, attrs || {}, {
304
+ type: 'button',
305
+ class: vpJoinClasses(
306
+ 'vp-segment-btn',
307
+ compact ? 'vp-segment-btn--compact' : '',
308
+ selected ? 'is-selected' : '',
309
+ attrs && attrs.class
310
+ )
311
+ });
312
+ return '<button' + vpBuildTagAttrs(buttonAttrs) + '>' + label + '</button>';
313
+ }
314
+
315
+ function vpErrorBannerHtml(message) {
316
+ return '<div class="vp-banner vp-banner--error vp-type-support">' + escapeHtml(message || '') + '</div>';
317
+ }
318
+
319
+ function vpDividerHtml() {
320
+ return '<div class="vp-divider"></div>';
321
+ }
322
+
323
+ function vpSectionTitleHtml(text) {
324
+ return '<div class="vp-section-title"><span>' + escapeHtml(text || '') + '</span></div>';
325
+ }
326
+
327
+ function vpListButtonHtml(innerHtml, attrs) {
328
+ const buttonAttrs = Object.assign({}, attrs || {}, {
329
+ type: 'button',
330
+ class: vpJoinClasses('vp-list-button', attrs && attrs.class)
331
+ });
332
+ return '<button' + vpBuildTagAttrs(buttonAttrs) + '>' + innerHtml + '</button>';
333
+ }
334
+
335
+
336
+ root.CrankUI = {
337
+ escapeHtml: escapeHtml,
338
+ escapeAttr: escapeAttr,
339
+ vpFieldHtml: vpFieldHtml,
340
+ vpFieldLabelHtml: vpFieldLabelHtml,
341
+ vpInputHtml: vpInputHtml,
342
+ vpTextareaHtml: vpTextareaHtml,
343
+ vpButtonHtml: vpButtonHtml,
344
+ vpButtonGroupHtml: vpButtonGroupHtml,
345
+ vpFieldSelectHtml: vpFieldSelectHtml,
346
+ vpSelectHtml: vpSelectHtml,
347
+ vpTabsHtml: vpTabsHtml,
348
+ vpChipHtml: vpChipHtml,
349
+ vpCapabilityBadgeHtml: vpCapabilityBadgeHtml,
350
+ vpCapabilityLabel: vpCapabilityLabel,
351
+ vpSectionTitleHtml: vpSectionTitleHtml,
352
+ vpDividerHtml: vpDividerHtml,
353
+ vpErrorBannerHtml: vpErrorBannerHtml
354
+ };
355
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,5 @@
1
+ Drag this to your bookmarks bar (companion must be running):
2
+
3
+ javascript:(function(){var s=document.createElement('script');s.src='http://127.0.0.1:3344/bookmarklet.js?t='+Date.now();document.documentElement.appendChild(s);})();
4
+
5
+ Or open http://127.0.0.1:3344 and drag the Crank button.
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Text source classification — ported from Vizpatch bookmarklet classifyTextSource.
3
+ * Hardcoded / static labels are editable; dynamic/bound data is locked.
4
+ */
5
+ (function (root) {
6
+ 'use strict';
7
+
8
+ function normalizeTextForCompare(value) {
9
+ return String(value || '')
10
+ .replace(/\s+/g, ' ')
11
+ .trim()
12
+ .toLowerCase();
13
+ }
14
+
15
+ function looksLikeDataValue(text) {
16
+ const value = String(text || '').trim();
17
+ if (!value) return false;
18
+ if (/^[\d,.$%+-]+$/.test(value)) return true;
19
+ if (/\b\d{4}-\d{2}-\d{2}\b/.test(value) || /\b\d{1,2}\/\d{1,2}\/\d{2,4}\b/.test(value)) return true;
20
+ if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return true;
21
+ if (/^https?:\/\//i.test(value)) return true;
22
+ if (/^[A-Z]{2,}-\d{2,}$/.test(value) || /^#?[A-F0-9]{8,}$/i.test(value)) return true;
23
+ return false;
24
+ }
25
+
26
+ function looksLikeStaticLabel(text) {
27
+ const value = String(text || '').trim();
28
+ if (!value) return false;
29
+ const normalized = value
30
+ .replace(/^[\[\(\{<\s]+/, '')
31
+ .replace(/[\]\)\}>\s]+$/, '')
32
+ .replace(/[.:;!?]+$/, '')
33
+ .trim();
34
+ const candidate = normalized || value;
35
+ if (candidate.length > 40) return false;
36
+ if (looksLikeDataValue(candidate)) return false;
37
+ if (/^[A-Z0-9_\-.]+$/.test(candidate) && candidate.length <= 6) return false;
38
+ return /[A-Za-z]/.test(candidate);
39
+ }
40
+
41
+ function getDisplayNameFromFiber(fiber) {
42
+ if (!fiber) return '';
43
+ try {
44
+ const t = fiber.type || fiber.elementType;
45
+ if (!t) return '';
46
+ if (typeof t === 'string') return t;
47
+ if (typeof t.displayName === 'string' && t.displayName) return t.displayName;
48
+ if (typeof t.name === 'string' && t.name) return t.name;
49
+ } catch (_err) {}
50
+ return '';
51
+ }
52
+
53
+ function findReactFiber(element) {
54
+ if (!element) return null;
55
+ const keys = Object.keys(element);
56
+ for (let i = 0; i < keys.length; i++) {
57
+ if (keys[i].indexOf('__reactFiber$') === 0 || keys[i].indexOf('__reactInternalInstance$') === 0) {
58
+ return element[keys[i]];
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ function getNearestComponentFiber(element) {
65
+ let fiber = findReactFiber(element);
66
+ let hops = 0;
67
+ while (fiber && hops < 24) {
68
+ const name = getDisplayNameFromFiber(fiber);
69
+ if (name && name[0] && name[0] === name[0].toUpperCase() && name.indexOf('http') !== 0) {
70
+ return fiber;
71
+ }
72
+ fiber = fiber.return || null;
73
+ hops += 1;
74
+ }
75
+ return findReactFiber(element);
76
+ }
77
+
78
+ function getMatchingComponentTextProps(componentFiber, text) {
79
+ const startFiber = componentFiber || null;
80
+ if (!startFiber) return [];
81
+
82
+ const textNorm = normalizeTextForCompare(text);
83
+ if (!textNorm) return [];
84
+
85
+ const maxFiberHops = looksLikeStaticLabel(textNorm) ? 2 : 8;
86
+ const matches = [];
87
+ const seen = {};
88
+
89
+ function visit(value, keyPath, depth) {
90
+ if (depth > 3 || value === null || value === undefined) return;
91
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
92
+ const str = normalizeTextForCompare(String(value));
93
+ if (!str) return;
94
+ const exactMatch = str === textNorm;
95
+ const allowContainment = textNorm.length >= 24 && str.length >= 24;
96
+ const containmentMatch =
97
+ allowContainment && (textNorm.indexOf(str) !== -1 || str.indexOf(textNorm) !== -1);
98
+ if (exactMatch || containmentMatch) {
99
+ const key = String(keyPath || '').trim() || 'value';
100
+ if (!seen[key]) {
101
+ seen[key] = true;
102
+ matches.push({ key: key, value: String(value) });
103
+ }
104
+ }
105
+ return;
106
+ }
107
+ if (Array.isArray(value)) {
108
+ value.slice(0, 8).forEach(function (item, index) {
109
+ visit(item, keyPath + '[' + index + ']', depth + 1);
110
+ });
111
+ return;
112
+ }
113
+ if (typeof value === 'object') {
114
+ Object.keys(value).slice(0, 20).forEach(function (key) {
115
+ if (/^(children|className|style|ref|key|on[A-Z])/.test(key)) return;
116
+ visit(value[key], keyPath ? keyPath + '.' + key : key, depth + 1);
117
+ });
118
+ }
119
+ }
120
+
121
+ let cursor = startFiber;
122
+ let hops = 0;
123
+ while (cursor && hops < maxFiberHops) {
124
+ const props =
125
+ cursor && (cursor.memoizedProps || cursor.pendingProps)
126
+ ? cursor.memoizedProps || cursor.pendingProps
127
+ : null;
128
+ if (props && typeof props === 'object') {
129
+ const ownerName = getDisplayNameFromFiber(cursor) || 'Component';
130
+ Object.keys(props).forEach(function (key) {
131
+ if (/^(children|className|style|ref|key|on[A-Z])/.test(key)) return;
132
+ visit(props[key], ownerName + '.' + key, 0);
133
+ });
134
+ }
135
+ cursor = cursor.return || null;
136
+ hops += 1;
137
+ }
138
+ return matches;
139
+ }
140
+
141
+ function classifyTextSource(element, textOverride) {
142
+ const text = String(
143
+ textOverride != null ? textOverride : (element && element.textContent) || ''
144
+ )
145
+ .replace(/\s+/g, ' ')
146
+ .trim();
147
+
148
+ if (!text) {
149
+ return {
150
+ type: 'unknown',
151
+ label: 'No editable text detected',
152
+ editable: false,
153
+ reason: 'Selected node has no direct text target to edit.'
154
+ };
155
+ }
156
+
157
+ const componentFiber = getNearestComponentFiber(element);
158
+ const matchedProps = getMatchingComponentTextProps(componentFiber, text);
159
+
160
+ if (matchedProps.length) {
161
+ const dataLikeText = looksLikeDataValue(text);
162
+ const hasStaticLabelPattern = looksLikeStaticLabel(text);
163
+ const dataKeyMatches = matchedProps.filter(function (entry) {
164
+ return /(id|uuid|code|vin|plate|license|count|qty|amount|total|price|cost|date|time|email|phone|created|updated|api|data|record|order|row|item|entity|seed|metric|score|value|number)/i.test(
165
+ entry.key
166
+ );
167
+ }).length;
168
+ const labelKeyMatches = matchedProps.filter(function (entry) {
169
+ return /(label|title|text|heading|caption|placeholder|button|cta|copy|tab|menu|nav|section|subtitle|description)/i.test(
170
+ entry.key
171
+ );
172
+ }).length;
173
+
174
+ let dynamicScore = 0;
175
+ let staticScore = 0;
176
+ if (dataLikeText) dynamicScore += 3;
177
+ if (hasStaticLabelPattern) staticScore += 2;
178
+ dynamicScore += Math.min(3, dataKeyMatches);
179
+ staticScore += Math.min(3, labelKeyMatches);
180
+ if (matchedProps.length >= 3 && dataKeyMatches >= 2) dynamicScore += 1;
181
+ if (matchedProps.length === 1 && labelKeyMatches > 0) staticScore += 1;
182
+ if (hasStaticLabelPattern && !dataLikeText && dataKeyMatches === 0 && matchedProps.length <= 2) {
183
+ staticScore += 2;
184
+ }
185
+ if (text.length <= 20 && !dataLikeText) staticScore += 1;
186
+
187
+ if (dynamicScore >= 4 && dynamicScore >= staticScore + 1) {
188
+ return {
189
+ type: 'dynamic-data',
190
+ label: 'Dynamic data (bound via props)',
191
+ editable: false,
192
+ reason: 'This text matches component data props and is likely API/seeder driven.',
193
+ matchedProps: matchedProps
194
+ };
195
+ }
196
+ if (staticScore >= 4 && staticScore >= dynamicScore + 1) {
197
+ return {
198
+ type: 'static-label',
199
+ label: 'Static label (high confidence)',
200
+ editable: true,
201
+ reason: 'Signals strongly indicate this is UI copy/label text defined in frontend source.',
202
+ matchedProps: matchedProps
203
+ };
204
+ }
205
+ return {
206
+ type: 'bound-unknown',
207
+ label: 'Bound text (needs confirmation)',
208
+ editable: false,
209
+ reason:
210
+ 'Text is prop-bound but confidence is mixed. Confirm in component source before enabling edits.',
211
+ matchedProps: matchedProps
212
+ };
213
+ }
214
+
215
+ if (looksLikeDataValue(text)) {
216
+ return {
217
+ type: 'dynamic-data',
218
+ label: 'Dynamic data pattern',
219
+ editable: false,
220
+ reason: 'Looks like runtime data (number/date/email/url/id) rather than fixed label copy.'
221
+ };
222
+ }
223
+
224
+ return {
225
+ type: 'static-label',
226
+ label: 'Static/default label',
227
+ editable: true,
228
+ reason: 'No runtime data binding detected for this text.'
229
+ };
230
+ }
231
+
232
+ root.CrankTextSource = {
233
+ classifyTextSource: classifyTextSource,
234
+ looksLikeDataValue: looksLikeDataValue,
235
+ looksLikeStaticLabel: looksLikeStaticLabel,
236
+ getNearestComponentFiber: getNearestComponentFiber,
237
+ getDisplayNameFromFiber: getDisplayNameFromFiber
238
+ };
239
+ })(typeof window !== 'undefined' ? window : globalThis);