@evertonferreira96/agents 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/api.md +45 -0
- package/.claude/agents/app.md +41 -0
- package/.claude/agents/architect.md +49 -0
- package/.claude/agents/infra.md +27 -0
- package/.claude/agents/product.md +58 -0
- package/.claude/agents/quick.md +33 -0
- package/.claude/agents/security.md +39 -0
- package/.claude/agents/ui.md +54 -0
- package/.claude/agents/verifier.md +56 -0
- package/.claude/agents/web.md +41 -0
- package/.claude/hooks/config-update-check.mjs +14 -0
- package/.claude/hooks/context-budget.mjs +51 -0
- package/.claude/hooks/graphify-update.sh +16 -0
- package/.claude/hooks/guard-bash.sh +64 -0
- package/.claude/hooks/orchestrator-guard.sh +49 -0
- package/.claude/hooks/post-compact.sh +26 -0
- package/.claude/hooks/readonly-guard.sh +54 -0
- package/.claude/hooks/stop-verify.sh +52 -0
- package/.claude/rules/api.md +21 -0
- package/.claude/rules/db.md +9 -0
- package/.claude/rules/delivery-diagram.md +16 -0
- package/.claude/rules/git-workflow.md +34 -0
- package/.claude/rules/graphify.md +3 -0
- package/.claude/rules/mobile.md +22 -0
- package/.claude/rules/tickets.md +9 -0
- package/.claude/rules/ui.md +14 -0
- package/.claude/rules/web.md +27 -0
- package/.claude/settings.json +114 -0
- package/.claude/skills/animate/RECIPES.md +324 -0
- package/.claude/skills/animate/SKILL.md +208 -0
- package/.claude/skills/animate/agents/openai.yaml +5 -0
- package/.claude/skills/animate-expo/RECIPES.md +385 -0
- package/.claude/skills/animate-expo/SKILL.md +264 -0
- package/.claude/skills/animate-expo/agents/openai.yaml +5 -0
- package/.claude/skills/animation-performance/SKILL.md +223 -0
- package/.claude/skills/animation-performance/agents/openai.yaml +5 -0
- package/.claude/skills/apple-design/SKILL.md +291 -0
- package/.claude/skills/apple-design/agents/openai.yaml +5 -0
- package/.claude/skills/arch-api/SKILL.md +291 -0
- package/.claude/skills/arch-api/agents/openai.yaml +3 -0
- package/.claude/skills/arch-api/references/common.md +174 -0
- package/.claude/skills/arch-api/references/contract.md +289 -0
- package/.claude/skills/arch-api/references/domain.md +301 -0
- package/.claude/skills/arch-api/references/endpoints.md +184 -0
- package/.claude/skills/arch-api/references/review.md +228 -0
- package/.claude/skills/arch-api/references/security.md +151 -0
- package/.claude/skills/arch-app/SKILL.md +278 -0
- package/.claude/skills/arch-app/agents/openai.yaml +3 -0
- package/.claude/skills/arch-app/references/navigation-session.md +257 -0
- package/.claude/skills/arch-app/references/review.md +155 -0
- package/.claude/skills/arch-app/references/screens.md +214 -0
- package/.claude/skills/arch-app/references/services.md +301 -0
- package/.claude/skills/arch-app/references/ui-mobile.md +124 -0
- package/.claude/skills/arch-db/SKILL.md +181 -0
- package/.claude/skills/arch-db/agents/openai.yaml +3 -0
- package/.claude/skills/arch-db/references/indexes-queries.md +251 -0
- package/.claude/skills/arch-db/references/migrations.md +191 -0
- package/.claude/skills/arch-db/references/modeling.md +214 -0
- package/.claude/skills/arch-db/references/mysql.md +262 -0
- package/.claude/skills/arch-db/references/pooling-ops.md +112 -0
- package/.claude/skills/arch-db/references/review.md +143 -0
- package/.claude/skills/arch-db/references/transactions-locks.md +141 -0
- package/.claude/skills/arch-web/SKILL.md +307 -0
- package/.claude/skills/arch-web/agents/openai.yaml +3 -0
- package/.claude/skills/arch-web/references/auth-session-env.md +165 -0
- package/.claude/skills/arch-web/references/blocks.md +198 -0
- package/.claude/skills/arch-web/references/cache.md +124 -0
- package/.claude/skills/arch-web/references/review.md +142 -0
- package/.claude/skills/arch-web/references/services.md +297 -0
- package/.claude/skills/arch-web/references/tests.md +159 -0
- package/.claude/skills/arch-web/references/ui-forms.md +143 -0
- package/.claude/skills/break-ui/CATALOG.md +135 -0
- package/.claude/skills/break-ui/SKILL.md +176 -0
- package/.claude/skills/break-ui/agents/openai.yaml +5 -0
- package/.claude/skills/brief/SKILL.md +91 -0
- package/.claude/skills/brief/agents/openai.yaml +3 -0
- package/.claude/skills/diagnosing-bugs/SKILL.md +138 -0
- package/.claude/skills/diagnosing-bugs/agents/openai.yaml +3 -0
- package/.claude/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
- package/.claude/skills/emil-design-eng/SKILL.md +675 -0
- package/.claude/skills/emil-design-eng/agents/openai.yaml +5 -0
- package/.claude/skills/find-animation-opportunities/SKILL.md +141 -0
- package/.claude/skills/find-animation-opportunities/agents/openai.yaml +5 -0
- package/.claude/skills/graphify/.graphify_version +1 -0
- package/.claude/skills/graphify/SKILL.md +742 -0
- package/.claude/skills/graphify/agents/openai.yaml +3 -0
- package/.claude/skills/graphify/references/add-watch.md +56 -0
- package/.claude/skills/graphify/references/exports.md +91 -0
- package/.claude/skills/graphify/references/extraction-spec.md +70 -0
- package/.claude/skills/graphify/references/github-and-merge.md +46 -0
- package/.claude/skills/graphify/references/hooks.md +33 -0
- package/.claude/skills/graphify/references/query.md +317 -0
- package/.claude/skills/graphify/references/transcribe.md +52 -0
- package/.claude/skills/graphify/references/update.md +221 -0
- package/.claude/skills/grill-me/SKILL.md +7 -0
- package/.claude/skills/grill-me/agents/openai.yaml +5 -0
- package/.claude/skills/grill-with-docs/SKILL.md +64 -0
- package/.claude/skills/grill-with-docs/agents/openai.yaml +5 -0
- package/.claude/skills/grilling/SKILL.md +30 -0
- package/.claude/skills/grilling/agents/openai.yaml +3 -0
- package/.claude/skills/handoff/SKILL.md +28 -0
- package/.claude/skills/handoff/agents/openai.yaml +5 -0
- package/.claude/skills/impeccable/SKILL.md +89 -0
- package/.claude/skills/impeccable/agents/openai.yaml +5 -0
- package/.claude/skills/impeccable/reference/adapt.md +318 -0
- package/.claude/skills/impeccable/reference/adapt.native.md +58 -0
- package/.claude/skills/impeccable/reference/android.md +46 -0
- package/.claude/skills/impeccable/reference/animate.md +89 -0
- package/.claude/skills/impeccable/reference/audit.md +137 -0
- package/.claude/skills/impeccable/reference/audit.native.md +139 -0
- package/.claude/skills/impeccable/reference/bolder.md +33 -0
- package/.claude/skills/impeccable/reference/clarify.md +94 -0
- package/.claude/skills/impeccable/reference/colorize.md +86 -0
- package/.claude/skills/impeccable/reference/component-review.md +63 -0
- package/.claude/skills/impeccable/reference/craft-floor.md +44 -0
- package/.claude/skills/impeccable/reference/craft.md +5 -0
- package/.claude/skills/impeccable/reference/critique.md +809 -0
- package/.claude/skills/impeccable/reference/degraded/asset-producer.md +42 -0
- package/.claude/skills/impeccable/reference/degraded/documenter.md +24 -0
- package/.claude/skills/impeccable/reference/degraded/finish-reviewer.md +38 -0
- package/.claude/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/.claude/skills/impeccable/reference/delight.md +70 -0
- package/.claude/skills/impeccable/reference/distill.md +111 -0
- package/.claude/skills/impeccable/reference/doctor.md +54 -0
- package/.claude/skills/impeccable/reference/document.md +416 -0
- package/.claude/skills/impeccable/reference/extract.md +69 -0
- package/.claude/skills/impeccable/reference/generate.md +101 -0
- package/.claude/skills/impeccable/reference/harden.md +345 -0
- package/.claude/skills/impeccable/reference/hooks.md +113 -0
- package/.claude/skills/impeccable/reference/init.md +131 -0
- package/.claude/skills/impeccable/reference/ios.md +51 -0
- package/.claude/skills/impeccable/reference/layout.md +84 -0
- package/.claude/skills/impeccable/reference/live-setup.md +104 -0
- package/.claude/skills/impeccable/reference/live.md +325 -0
- package/.claude/skills/impeccable/reference/mode-operate.md +21 -0
- package/.claude/skills/impeccable/reference/mode-persuade.md +19 -0
- package/.claude/skills/impeccable/reference/mode-read.md +21 -0
- package/.claude/skills/impeccable/reference/new-work.md +154 -0
- package/.claude/skills/impeccable/reference/onboard.md +234 -0
- package/.claude/skills/impeccable/reference/operate.md +61 -0
- package/.claude/skills/impeccable/reference/optimize.md +258 -0
- package/.claude/skills/impeccable/reference/overdrive.md +127 -0
- package/.claude/skills/impeccable/reference/polish.md +105 -0
- package/.claude/skills/impeccable/reference/quieter.md +99 -0
- package/.claude/skills/impeccable/reference/region-map.md +26 -0
- package/.claude/skills/impeccable/reference/routing.md +24 -0
- package/.claude/skills/impeccable/reference/shape.md +59 -0
- package/.claude/skills/impeccable/reference/typeset.md +80 -0
- package/.claude/skills/impeccable/reference/visualize.md +47 -0
- package/.claude/skills/impeccable/scripts/VERSION +1 -0
- package/.claude/skills/impeccable/scripts/command-metadata.json +98 -0
- package/.claude/skills/impeccable/scripts/data/font-index-failures.json +121 -0
- package/.claude/skills/impeccable/scripts/data/font-index.json +1 -0
- package/.claude/skills/impeccable/scripts/impeccable +206 -0
- package/.claude/skills/impeccable/scripts/impeccable.cmd +214 -0
- package/.claude/skills/impeccable/scripts/live-browser-dom.js +281 -0
- package/.claude/skills/impeccable/scripts/live-browser-ignores.js +242 -0
- package/.claude/skills/impeccable/scripts/live-browser-session.js +148 -0
- package/.claude/skills/impeccable/scripts/live-browser.js +13532 -0
- package/.claude/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
- package/.claude/skills/improve-animations/AUDIT.md +115 -0
- package/.claude/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
- package/.claude/skills/improve-animations/SKILL.md +110 -0
- package/.claude/skills/improve-animations/agents/openai.yaml +5 -0
- package/.claude/skills/mobile-native/SKILL.md +311 -0
- package/.claude/skills/mobile-native/agents/openai.yaml +3 -0
- package/.claude/skills/orchestrate/SKILL.md +181 -0
- package/.claude/skills/orchestrate/agents/openai.yaml +3 -0
- package/.claude/skills/pick-ui-library/SKILL.md +85 -0
- package/.claude/skills/pick-ui-library/agents/openai.yaml +5 -0
- package/.claude/skills/ponytail/SKILL.md +52 -0
- package/.claude/skills/ponytail/agents/openai.yaml +3 -0
- package/.claude/skills/ponytail-audit/SKILL.md +103 -0
- package/.claude/skills/ponytail-audit/agents/openai.yaml +3 -0
- package/.claude/skills/ponytail-debt/SKILL.md +44 -0
- package/.claude/skills/ponytail-debt/agents/openai.yaml +3 -0
- package/.claude/skills/ponytail-review/SKILL.md +101 -0
- package/.claude/skills/ponytail-review/agents/openai.yaml +3 -0
- package/.claude/skills/postgres-best-practices/SKILL.md +42 -0
- package/.claude/skills/postgres-best-practices/agents/openai.yaml +3 -0
- package/.claude/skills/postgres-best-practices/references/backup-restore.md +372 -0
- package/.claude/skills/postgres-best-practices/references/bulk-loading.md +315 -0
- package/.claude/skills/postgres-best-practices/references/connection-pooling.md +310 -0
- package/.claude/skills/postgres-best-practices/references/hot-standby.md +221 -0
- package/.claude/skills/postgres-best-practices/references/indexing.md +318 -0
- package/.claude/skills/postgres-best-practices/references/logical-replication.md +610 -0
- package/.claude/skills/postgres-best-practices/references/major-version-upgrades.md +334 -0
- package/.claude/skills/postgres-best-practices/references/performance-diagnostics.md +553 -0
- package/.claude/skills/postgres-best-practices/references/query-optimization.md +294 -0
- package/.claude/skills/postgres-best-practices/references/query-patterns.md +566 -0
- package/.claude/skills/postgres-best-practices/references/schema-design.md +362 -0
- package/.claude/skills/postgres-best-practices/references/security-roles.md +489 -0
- package/.claude/skills/postgres-best-practices/references/transaction-isolation.md +344 -0
- package/.claude/skills/project-docs/SKILL.md +330 -0
- package/.claude/skills/project-docs/agents/openai.yaml +3 -0
- package/.claude/skills/prototype/PICKER.md +197 -0
- package/.claude/skills/prototype/SKILL.md +98 -0
- package/.claude/skills/prototype/agents/openai.yaml +5 -0
- package/.claude/skills/retro/SKILL.md +43 -0
- package/.claude/skills/retro/agents/openai.yaml +5 -0
- package/.claude/skills/review/SKILL.md +74 -0
- package/.claude/skills/review/agents/openai.yaml +3 -0
- package/.claude/skills/review/references/smells.md +23 -0
- package/.claude/skills/review-animations/SKILL.md +120 -0
- package/.claude/skills/review-animations/STANDARDS.md +187 -0
- package/.claude/skills/review-animations/agents/openai.yaml +5 -0
- package/.claude/skills/tdd/SKILL.md +38 -0
- package/.claude/skills/tdd/agents/openai.yaml +3 -0
- package/.claude/skills/tdd/mocking.md +59 -0
- package/.claude/skills/tdd/tests.md +77 -0
- package/.claude/skills/triage/SKILL.md +58 -0
- package/.claude/skills/triage/agents/openai.yaml +3 -0
- package/.claude/skills/ui-ux-pro-max/SKILL.md +378 -0
- package/.claude/skills/ui-ux-pro-max/agents/openai.yaml +5 -0
- package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.claude/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.claude/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.claude/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/.claude/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/.claude/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/.claude/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/.claude/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/.claude/skills/writing-for-agents/SKILL.md +81 -0
- package/.claude/skills/writing-for-agents/agents/openai.yaml +3 -0
- package/.worktreeinclude +3 -0
- package/AGENTS.md +44 -0
- package/README.md +14 -2
- package/VERSION +1 -0
- package/bin/agents.mjs +45 -0
- package/docs/adr/README.md +31 -0
- package/docs/brief-template.md +3 -0
- package/docs/plans/README.md +73 -0
- package/package.json +46 -4
- package/scripts/bootstrap.mjs +188 -0
- package/scripts/check-catalog.mjs +43 -0
- package/scripts/skills-update.mjs +51 -0
- package/skills-lock.json +269 -0
- package/templates/CLAUDE.project.md +41 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side DOM helpers for Impeccable live mode.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from live-browser.js so future browser script parts can share
|
|
5
|
+
* chrome mounting, lookup, focus, and picker helpers without depending on the
|
|
6
|
+
* full overlay UI bundle.
|
|
7
|
+
*/
|
|
8
|
+
(function (root) {
|
|
9
|
+
'use strict';
|
|
10
|
+
if (!root) return;
|
|
11
|
+
|
|
12
|
+
function createLiveBrowserDomHelpers({
|
|
13
|
+
prefix,
|
|
14
|
+
skipTags,
|
|
15
|
+
document: doc = root.document,
|
|
16
|
+
css = root.CSS,
|
|
17
|
+
crypto = root.crypto,
|
|
18
|
+
} = {}) {
|
|
19
|
+
if (!prefix) throw new Error('prefix required');
|
|
20
|
+
if (!doc) throw new Error('document required');
|
|
21
|
+
const tagsToSkip = skipTags || new Set();
|
|
22
|
+
|
|
23
|
+
function own(el) {
|
|
24
|
+
return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function pickable(el) {
|
|
28
|
+
if (!el || el.nodeType !== 1) return false;
|
|
29
|
+
if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false;
|
|
30
|
+
if (own(el)) return false;
|
|
31
|
+
const r = el.getBoundingClientRect();
|
|
32
|
+
return r.width >= 20 && r.height >= 20;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function desc(el) {
|
|
36
|
+
if (!el) return '';
|
|
37
|
+
let s = el.tagName.toLowerCase();
|
|
38
|
+
if (el.id) s += '#' + el.id;
|
|
39
|
+
else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.');
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function rectIsUsableAnchor(rect) {
|
|
44
|
+
return !!rect && rect.width > 0.5 && rect.height > 0.5;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeFrozenAnchor(el) {
|
|
48
|
+
if (!el || !el.getBoundingClientRect) return null;
|
|
49
|
+
const r = el.getBoundingClientRect();
|
|
50
|
+
if (!rectIsUsableAnchor(r)) return null;
|
|
51
|
+
const rect = {
|
|
52
|
+
x: r.x, y: r.y,
|
|
53
|
+
top: r.top, left: r.left,
|
|
54
|
+
right: r.right, bottom: r.bottom,
|
|
55
|
+
width: r.width, height: r.height,
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
__impeccableFrozenAnchor: true,
|
|
59
|
+
tagName: el.tagName || 'DIV',
|
|
60
|
+
id: el.id || '',
|
|
61
|
+
classList: el.classList ? [...el.classList] : [],
|
|
62
|
+
hasAttribute: () => false,
|
|
63
|
+
getBoundingClientRect: () => rect,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hasFrameworkHmrOwnership(el) {
|
|
68
|
+
for (let node = el; node; node = node.parentElement) {
|
|
69
|
+
let keys = [];
|
|
70
|
+
try { keys = Object.getOwnPropertyNames(node); } catch {}
|
|
71
|
+
if (keys.some((key) => (
|
|
72
|
+
key.startsWith('__reactFiber$')
|
|
73
|
+
|| key.startsWith('__reactProps$')
|
|
74
|
+
|| key.startsWith('__reactContainer$')
|
|
75
|
+
|| key === '_reactRootContainer'
|
|
76
|
+
|| key === '__vueParentComponent'
|
|
77
|
+
|| key === '__vue_app__'
|
|
78
|
+
|| key === '__vnode'
|
|
79
|
+
|| key === '__svelte_meta'
|
|
80
|
+
))) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function id8() {
|
|
88
|
+
if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8);
|
|
89
|
+
return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function cssId(id) {
|
|
93
|
+
if (css?.escape) return css.escape(id);
|
|
94
|
+
return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function liveUiRoot() {
|
|
98
|
+
const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__;
|
|
99
|
+
if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot;
|
|
100
|
+
return doc.body;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// A modal <dialog> (showModal) paints in the top layer and makes every
|
|
104
|
+
// node outside its subtree inert, so chrome on <body> can be neither seen
|
|
105
|
+
// nor clicked while one is open, and no z-index reaches it (issue #879).
|
|
106
|
+
// While a modal is open the chrome parks inside the topmost one, in one
|
|
107
|
+
// popover: inside the dialog it is not inert, and a popover shown after
|
|
108
|
+
// the dialog paints above it. One popover for all of it, because
|
|
109
|
+
// top-layer entries stack by show order, which would drop the z-index
|
|
110
|
+
// order between the roots. The detect overlay (browser-bundle/
|
|
111
|
+
// 40-overlay.js) mounts its outlines here too: it finds the host by this
|
|
112
|
+
// id and places its outlines again on the event of the same name, fired
|
|
113
|
+
// on every move.
|
|
114
|
+
const topLayerHost = doc.createElement('div');
|
|
115
|
+
topLayerHost.id = prefix + '-top-layer';
|
|
116
|
+
// Same box as the SvelteKit shadow host: 0x0, fixed, chrome overflows it.
|
|
117
|
+
// `all: initial` also clears the popover UA box.
|
|
118
|
+
for (const [name, value] of Object.entries({
|
|
119
|
+
all: 'initial', position: 'fixed', top: '0', left: '0', width: '0', height: '0', overflow: 'visible',
|
|
120
|
+
})) topLayerHost.style.setProperty(name, value, 'important');
|
|
121
|
+
const pageRoots = new WeakSet(); // chrome nodes mounted on the page itself
|
|
122
|
+
const openModals = []; // in the order they opened; last is topmost
|
|
123
|
+
|
|
124
|
+
// Modals that were open before watching began have no readable top-layer
|
|
125
|
+
// order, but the topmost one's backdrop covers the viewport, so a hit
|
|
126
|
+
// test lands in it. Raise that one to the top of the stack.
|
|
127
|
+
function raiseHitModal() {
|
|
128
|
+
const i = openModals.indexOf(doc.elementFromPoint(0, 0)?.closest('dialog:modal'));
|
|
129
|
+
if (i !== -1) openModals.push(...openModals.splice(i, 1));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function syncTopLayerHost(records = []) {
|
|
133
|
+
for (const { type, target, oldValue } of records) {
|
|
134
|
+
if (type !== 'attributes' || oldValue !== null || !target.matches('dialog:modal')) continue;
|
|
135
|
+
// Opened, or closed and reopened in one task (the observer sees only
|
|
136
|
+
// the end state): either way it is back on top of the top layer, so
|
|
137
|
+
// a host shown inside it earlier has to be shown again. A write to an
|
|
138
|
+
// `open` that was already set (oldValue not null) moved nothing.
|
|
139
|
+
const i = openModals.indexOf(target);
|
|
140
|
+
if (i !== -1) openModals.splice(i, 1);
|
|
141
|
+
openModals.push(target);
|
|
142
|
+
if (topLayerHost.parentNode === target) topLayerHost.remove();
|
|
143
|
+
}
|
|
144
|
+
// A closed dialog, or one removed from the document while open, stops
|
|
145
|
+
// matching :modal. Whichever is now on top may predate the watch.
|
|
146
|
+
const before = openModals.length;
|
|
147
|
+
for (let i = openModals.length - 1; i >= 0; i--) {
|
|
148
|
+
if (!openModals[i].matches('dialog:modal')) openModals.splice(i, 1);
|
|
149
|
+
}
|
|
150
|
+
if (openModals.length < before && openModals.length > 1) raiseHitModal();
|
|
151
|
+
const modal = openModals[openModals.length - 1] || null;
|
|
152
|
+
if (topLayerHost.parentNode === modal) return;
|
|
153
|
+
if (modal) {
|
|
154
|
+
modal.appendChild(topLayerHost);
|
|
155
|
+
for (const el of [...doc.body.children]) if (pageRoots.has(el)) topLayerHost.appendChild(el);
|
|
156
|
+
topLayerHost.showPopover();
|
|
157
|
+
} else {
|
|
158
|
+
doc.body.append(...topLayerHost.childNodes);
|
|
159
|
+
topLayerHost.remove();
|
|
160
|
+
}
|
|
161
|
+
doc.dispatchEvent(new Event(topLayerHost.id));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Returns the function that stops watching and puts the chrome back.
|
|
165
|
+
function watchModalDialogs() {
|
|
166
|
+
if (typeof topLayerHost.showPopover !== 'function') return () => {};
|
|
167
|
+
topLayerHost.popover = 'manual';
|
|
168
|
+
// Parked in the page's dialog, chrome clicks would bubble into its own
|
|
169
|
+
// handlers, such as a click-outside-the-box close. Live listens in capture.
|
|
170
|
+
topLayerHost.addEventListener('click', (e) => e.stopPropagation());
|
|
171
|
+
openModals.push(...doc.querySelectorAll('dialog:modal'));
|
|
172
|
+
raiseHitModal();
|
|
173
|
+
const observer = new MutationObserver(syncTopLayerHost);
|
|
174
|
+
observer.observe(doc, { subtree: true, childList: true, attributes: true, attributeOldValue: true, attributeFilter: ['open'] });
|
|
175
|
+
syncTopLayerHost();
|
|
176
|
+
return () => {
|
|
177
|
+
observer.disconnect();
|
|
178
|
+
openModals.length = 0;
|
|
179
|
+
syncTopLayerHost();
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// A copy of a page element without the chrome parked inside it, which a
|
|
184
|
+
// picked modal dialog contains.
|
|
185
|
+
function cloneWithoutChrome(el) {
|
|
186
|
+
const clone = el.cloneNode(true);
|
|
187
|
+
if (el.contains(topLayerHost)) clone.querySelector('#' + cssId(topLayerHost.id)).remove();
|
|
188
|
+
return clone;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Mount a chrome node on the page itself: <body>, or the top-layer host
|
|
192
|
+
// while the chrome is parked in a modal.
|
|
193
|
+
function uiAppendToPage(el) {
|
|
194
|
+
pageRoots.add(el);
|
|
195
|
+
(topLayerHost.parentNode ? topLayerHost : doc.body).appendChild(el);
|
|
196
|
+
return el;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function uiAppend(el) {
|
|
200
|
+
const uiRoot = liveUiRoot();
|
|
201
|
+
if (uiRoot === doc.body) return uiAppendToPage(el);
|
|
202
|
+
// An adapter shadow root: its host is the node that sits on the page.
|
|
203
|
+
if (uiRoot.host) pageRoots.add(uiRoot.host);
|
|
204
|
+
uiRoot.appendChild(el);
|
|
205
|
+
return el;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function uiAppendStyle(styleEl) {
|
|
209
|
+
const uiRoot = liveUiRoot();
|
|
210
|
+
if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl);
|
|
211
|
+
else doc.head.appendChild(styleEl);
|
|
212
|
+
return styleEl;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function uiGetById(id) {
|
|
216
|
+
const uiRoot = liveUiRoot();
|
|
217
|
+
if (uiRoot?.getElementById) {
|
|
218
|
+
const found = uiRoot.getElementById(id);
|
|
219
|
+
if (found) return found;
|
|
220
|
+
}
|
|
221
|
+
if (uiRoot?.querySelector) {
|
|
222
|
+
const found = uiRoot.querySelector('#' + cssId(id));
|
|
223
|
+
if (found) return found;
|
|
224
|
+
}
|
|
225
|
+
return doc.getElementById(id);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function activeElementDeep() {
|
|
229
|
+
let active = doc.activeElement;
|
|
230
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
231
|
+
return active;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) {
|
|
235
|
+
if (!rootEl) return;
|
|
236
|
+
if (setPointerEvents) {
|
|
237
|
+
rootEl.style.setProperty('pointer-events', 'auto', 'important');
|
|
238
|
+
}
|
|
239
|
+
const stop = (e) => e.stopPropagation();
|
|
240
|
+
rootEl.addEventListener('pointerdown', stop);
|
|
241
|
+
rootEl.addEventListener('mousedown', stop);
|
|
242
|
+
rootEl.addEventListener('focusin', stop);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// The half of the defang that cannot sit on the chrome: a focus trap (Radix /
|
|
246
|
+
// Reka FocusScope) also hears the page's own focusout as focus leaves its
|
|
247
|
+
// modal for our chrome, and pulls it back. Register on the document in
|
|
248
|
+
// capture, ahead of the trap. The page's own focusout handlers miss that
|
|
249
|
+
// one transition too; blur still fires.
|
|
250
|
+
function stopFocusOutIntoChrome(e) {
|
|
251
|
+
if (own(e.relatedTarget)) e.stopPropagation();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
own,
|
|
256
|
+
pickable,
|
|
257
|
+
desc,
|
|
258
|
+
rectIsUsableAnchor,
|
|
259
|
+
makeFrozenAnchor,
|
|
260
|
+
hasFrameworkHmrOwnership,
|
|
261
|
+
id8,
|
|
262
|
+
cssId,
|
|
263
|
+
liveUiRoot,
|
|
264
|
+
uiAppend,
|
|
265
|
+
uiAppendToPage,
|
|
266
|
+
topLayerHost,
|
|
267
|
+
watchModalDialogs,
|
|
268
|
+
cloneWithoutChrome,
|
|
269
|
+
uiAppendStyle,
|
|
270
|
+
uiGetById,
|
|
271
|
+
activeElementDeep,
|
|
272
|
+
defangOutsideHandlers,
|
|
273
|
+
stopFocusOutIntoChrome,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
root.__IMPECCABLE_LIVE_DOM__ = {
|
|
278
|
+
version: 1,
|
|
279
|
+
createLiveBrowserDomHelpers,
|
|
280
|
+
};
|
|
281
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side resolution of project detector waivers for Impeccable live mode.
|
|
3
|
+
*
|
|
4
|
+
* The live server serializes `.impeccable/config.json` + `config.local.json`
|
|
5
|
+
* detector ignores (plus the served-root prefixes from the inject config's
|
|
6
|
+
* `files` globs) into `window.__IMPECCABLE_PROJECT_IGNORES__`. This part
|
|
7
|
+
* resolves that config against the current page's URL path when a detect scan
|
|
8
|
+
* starts, so the overlay suppresses the same findings the CLI and the edit
|
|
9
|
+
* hook do (issue #639).
|
|
10
|
+
*
|
|
11
|
+
* Mirrors filterDetectionFindings in cli/lib/impeccable-config.mjs:
|
|
12
|
+
* 1. `ignoreRules` suppress a rule project-wide.
|
|
13
|
+
* 2. `ignoreValues` entries with `value: "*"` suppress their rule in the
|
|
14
|
+
* files their globs name. The CLI never applies an unscoped wildcard
|
|
15
|
+
* (isIgnoredFindingValue returns false for it), so neither does this.
|
|
16
|
+
* 3. Remaining `ignoreValues` entries match on the finding's own value;
|
|
17
|
+
* those are forwarded as `disabledValues` for the detector bundle to
|
|
18
|
+
* apply where the findings are assembled.
|
|
19
|
+
* 4. `ignoreFiles` globs that name the page waive it wholesale: the
|
|
20
|
+
* resolver reports `skipScan: true` and the detector answers the scan
|
|
21
|
+
* with zero findings, mirroring shouldIgnoreDetectionFile in the CLI
|
|
22
|
+
* and the edit hook's own ignoreFiles gate.
|
|
23
|
+
*
|
|
24
|
+
* `pageFiles`, when the server could resolve it, lists the real project
|
|
25
|
+
* files the inject config serves. A URL that suffix-matches exactly one of
|
|
26
|
+
* them takes that file as its only project identity; an ambiguous or absent
|
|
27
|
+
* match falls back to the served-root common ancestor below.
|
|
28
|
+
*
|
|
29
|
+
* Known gap, unchanged from PR #645: framework apps inject into source files
|
|
30
|
+
* (src/routes/about/+page.svelte) while scans see route URLs (/about), so
|
|
31
|
+
* entries scoped to source or asset paths never match a page candidate and
|
|
32
|
+
* are dropped. That shows the finding, which is the conservative direction.
|
|
33
|
+
*
|
|
34
|
+
* Kept separate from live-browser.js so the glob and page-scope logic can be
|
|
35
|
+
* unit tested in Node (tests/live-browser-ignores.test.mjs) without the full
|
|
36
|
+
* overlay UI bundle.
|
|
37
|
+
*/
|
|
38
|
+
(function (root) {
|
|
39
|
+
'use strict';
|
|
40
|
+
if (!root) return;
|
|
41
|
+
|
|
42
|
+
// Keep in step with normalizeIgnoreRule / normalizeIgnoreValue in
|
|
43
|
+
// cli/lib/impeccable-config.mjs.
|
|
44
|
+
function normalizeIgnoreRule(rule) {
|
|
45
|
+
return String(rule || '').trim().toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeIgnoreValue(value) {
|
|
49
|
+
return String(value || '')
|
|
50
|
+
.trim()
|
|
51
|
+
.replace(/^["']|["']$/g, '')
|
|
52
|
+
.replace(/\+/g, ' ')
|
|
53
|
+
.replace(/\s+/g, ' ')
|
|
54
|
+
.toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Glob -> RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation.
|
|
58
|
+
// Keep in step with globToRegex in cli/lib/impeccable-config.mjs.
|
|
59
|
+
function globToRegex(glob) {
|
|
60
|
+
let re = '^';
|
|
61
|
+
let i = 0;
|
|
62
|
+
while (i < glob.length) {
|
|
63
|
+
const c = glob[i];
|
|
64
|
+
if (c === '*') {
|
|
65
|
+
if (glob[i + 1] === '*') {
|
|
66
|
+
re += '.*';
|
|
67
|
+
i += 2;
|
|
68
|
+
if (glob[i] === '/') i += 1;
|
|
69
|
+
} else {
|
|
70
|
+
re += '[^/]*';
|
|
71
|
+
i += 1;
|
|
72
|
+
}
|
|
73
|
+
} else if (c === '?') {
|
|
74
|
+
re += '[^/]';
|
|
75
|
+
i += 1;
|
|
76
|
+
} else if (c === '{') {
|
|
77
|
+
const end = glob.indexOf('}', i);
|
|
78
|
+
if (end === -1) { re += '\\{'; i += 1; continue; }
|
|
79
|
+
const parts = glob.slice(i + 1, end).split(',').map((p) => p.replace(/[.+^$()|[\]\\]/g, '\\$&'));
|
|
80
|
+
re += `(?:${parts.join('|')})`;
|
|
81
|
+
i = end + 1;
|
|
82
|
+
} else if (/[.+^$()|[\]\\]/.test(c)) {
|
|
83
|
+
re += `\\${c}`;
|
|
84
|
+
i += 1;
|
|
85
|
+
} else {
|
|
86
|
+
re += c;
|
|
87
|
+
i += 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
re += '$';
|
|
91
|
+
return new RegExp(re);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The project-relative paths this page could be known as. Ignore globs are
|
|
95
|
+
// project-relative (prototype/foo.html) and the URL is site-relative
|
|
96
|
+
// (/foo.html), because a static server's root usually sits inside the
|
|
97
|
+
// project; `roots` carries that prefix. The server reads it from the inject
|
|
98
|
+
// config's own `files` globs, which already state where the served pages
|
|
99
|
+
// are. Do not derive it from the ignore globs: a single entry scoped to
|
|
100
|
+
// prototype/library/** would then lend prototype/library/ as a candidate
|
|
101
|
+
// prefix to every page, and that rule would suppress site-wide.
|
|
102
|
+
//
|
|
103
|
+
// Each prefixed path also contributes its slash suffixes, mirroring
|
|
104
|
+
// findingMatchesScopedIgnoreFile in cli/lib/impeccable-config.mjs (which
|
|
105
|
+
// matches globs against every path suffix of the finding's file).
|
|
106
|
+
//
|
|
107
|
+
// One live session is served by one server, so a single document root must
|
|
108
|
+
// sit at or above every configured page. The only prefix that can safely
|
|
109
|
+
// be asserted is therefore the deepest common ancestor of the glob roots.
|
|
110
|
+
// Treating each glob's own prefix as an identity goes wrong in both
|
|
111
|
+
// directions: disjoint roots (src/ and public/) invent simultaneous
|
|
112
|
+
// identities for one URL, so a waiver scoped to src/foo.html hides a
|
|
113
|
+
// finding on a page served from public/foo.html; nested roots (prototype/
|
|
114
|
+
// and prototype/library/, from globs at two depths in one tree) are not
|
|
115
|
+
// alternatives at all, and demanding a waiver match under both stops
|
|
116
|
+
// prototype/index.html from applying anywhere. When the globs share no
|
|
117
|
+
// common root, no prefix is asserted and only the URL path itself matches.
|
|
118
|
+
function pageCandidates(pathname, roots, pageFiles) {
|
|
119
|
+
let pagePath = String(pathname || '');
|
|
120
|
+
try {
|
|
121
|
+
pagePath = decodeURIComponent(pagePath);
|
|
122
|
+
} catch {
|
|
123
|
+
// Malformed percent-escape: match on the raw path rather than throwing.
|
|
124
|
+
}
|
|
125
|
+
pagePath = pagePath.replace(/^\/+/, '');
|
|
126
|
+
// A directory URL serves that directory's index, and the ignore globs
|
|
127
|
+
// name files. Without this, /news/ never matches prototype/news/index.html.
|
|
128
|
+
if (pagePath === '' || pagePath.endsWith('/')) pagePath += 'index.html';
|
|
129
|
+
|
|
130
|
+
const candidates = new Set();
|
|
131
|
+
const addSuffixes = (fullPath) => {
|
|
132
|
+
const parts = fullPath.split('/').filter(Boolean);
|
|
133
|
+
for (let i = 0; i < parts.length; i++) {
|
|
134
|
+
candidates.add(parts.slice(i).join('/'));
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
addSuffixes(pagePath);
|
|
138
|
+
|
|
139
|
+
// The served page list names the real files the inject config serves.
|
|
140
|
+
// A URL that suffix-matches exactly one of them has an unambiguous
|
|
141
|
+
// project identity; assert that identity and stop guessing from roots
|
|
142
|
+
// (PR #645 review: with src/ and public/ both served, /foo.html must not
|
|
143
|
+
// borrow src/foo.html's waivers while actually serving public/foo.html).
|
|
144
|
+
// Zero matches or several fall through to the common-ancestor fallback:
|
|
145
|
+
// ambiguity resolves toward showing the finding.
|
|
146
|
+
const knownPages = [];
|
|
147
|
+
for (const entry of Array.isArray(pageFiles) ? pageFiles : []) {
|
|
148
|
+
if (typeof entry !== 'string' || !entry) continue;
|
|
149
|
+
if (entry === pagePath || entry.endsWith('/' + pagePath)) knownPages.push(entry);
|
|
150
|
+
}
|
|
151
|
+
if (knownPages.length === 1) {
|
|
152
|
+
addSuffixes(knownPages[0]);
|
|
153
|
+
return [...candidates];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const prefixes = [];
|
|
157
|
+
for (const entry of Array.isArray(roots) ? roots : []) {
|
|
158
|
+
if (typeof entry !== 'string') continue;
|
|
159
|
+
prefixes.push(entry.split('/').filter(Boolean));
|
|
160
|
+
}
|
|
161
|
+
let common = prefixes.length > 0 ? prefixes[0] : [];
|
|
162
|
+
for (const segments of prefixes.slice(1)) {
|
|
163
|
+
let i = 0;
|
|
164
|
+
while (i < common.length && i < segments.length && common[i] === segments[i]) i += 1;
|
|
165
|
+
common = common.slice(0, i);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (common.length > 0) addSuffixes(common.join('/') + '/' + pagePath);
|
|
169
|
+
return [...candidates];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function matchesScope(globs, candidates) {
|
|
173
|
+
return globs.some((glob) => {
|
|
174
|
+
let re;
|
|
175
|
+
try {
|
|
176
|
+
re = globToRegex(String(glob));
|
|
177
|
+
} catch {
|
|
178
|
+
// Malformed glob: skip it, as matchesAnyGlob does in the CLI.
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return candidates.some((candidate) => re.test(candidate));
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resolve the serialized project ignores for one page.
|
|
187
|
+
*
|
|
188
|
+
* @param {object} options
|
|
189
|
+
* @param {object} options.ignores window.__IMPECCABLE_PROJECT_IGNORES__,
|
|
190
|
+
* in whatever state it arrived: absent, null, or hand-edited into the
|
|
191
|
+
* wrong shape. Every read tolerates that and degrades to no filtering.
|
|
192
|
+
* @param {string} options.pathname location.pathname of the scanned page.
|
|
193
|
+
* @returns {{ disabledRules: string[], disabledValues: Array<{rule: string, value: string}>, skipScan: boolean }}
|
|
194
|
+
*/
|
|
195
|
+
function resolveDetectIgnores({ ignores, pathname } = {}) {
|
|
196
|
+
const config = ignores && typeof ignores === 'object' ? ignores : {};
|
|
197
|
+
const asArray = (value) => (Array.isArray(value) ? value : []);
|
|
198
|
+
const candidates = pageCandidates(pathname, config.roots, config.pageFiles);
|
|
199
|
+
|
|
200
|
+
// detector.ignoreFiles waives whole files. When any glob names this
|
|
201
|
+
// page, the scan itself is skipped; rule and value lists are returned
|
|
202
|
+
// empty because nothing will run.
|
|
203
|
+
const ignoreFileGlobs = asArray(config.ignoreFiles)
|
|
204
|
+
.filter((glob) => typeof glob === 'string' && glob.trim());
|
|
205
|
+
if (ignoreFileGlobs.length > 0 && matchesScope(ignoreFileGlobs, candidates)) {
|
|
206
|
+
return { disabledRules: [], disabledValues: [], skipScan: true };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const disabledRules = new Set(
|
|
210
|
+
asArray(config.ignoreRules)
|
|
211
|
+
.filter((rule) => typeof rule === 'string')
|
|
212
|
+
.map(normalizeIgnoreRule)
|
|
213
|
+
.filter(Boolean),
|
|
214
|
+
);
|
|
215
|
+
const disabledValues = [];
|
|
216
|
+
|
|
217
|
+
for (const entry of asArray(config.ignoreValues)) {
|
|
218
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
219
|
+
const rule = normalizeIgnoreRule(entry.rule);
|
|
220
|
+
const value = normalizeIgnoreValue(entry.value);
|
|
221
|
+
if (!rule || !value) continue;
|
|
222
|
+
const files = [
|
|
223
|
+
...(typeof entry.file === 'string' && entry.file.trim() ? [entry.file.trim()] : []),
|
|
224
|
+
...asArray(entry.files).filter((glob) => typeof glob === 'string' && glob.trim()),
|
|
225
|
+
];
|
|
226
|
+
if (value === '*') {
|
|
227
|
+
// Wildcards suppress their rule only inside the files they name.
|
|
228
|
+
if (files.length > 0 && matchesScope(files, candidates)) disabledRules.add(rule);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (files.length > 0 && !matchesScope(files, candidates)) continue;
|
|
232
|
+
disabledValues.push({ rule, value });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { disabledRules: [...disabledRules], disabledValues, skipScan: false };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
root.__IMPECCABLE_LIVE_IGNORES__ = {
|
|
239
|
+
version: 1,
|
|
240
|
+
resolveDetectIgnores,
|
|
241
|
+
};
|
|
242
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side durable session helpers for Impeccable live mode.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from live-browser.js so recovery state can be tested without
|
|
5
|
+
* booting the full overlay UI. Served before live-browser.js and attached to
|
|
6
|
+
* window.__IMPECCABLE_LIVE_SESSION__.
|
|
7
|
+
*/
|
|
8
|
+
(function (root) {
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
function createLiveBrowserSessionState({ prefix, storage, idFactory }) {
|
|
12
|
+
if (!prefix) throw new Error('prefix required');
|
|
13
|
+
const store = storage || root.localStorage;
|
|
14
|
+
const makeId = idFactory || function () { return Math.random().toString(16).slice(2, 10); };
|
|
15
|
+
const sessionKey = prefix + '-session';
|
|
16
|
+
const handledKey = sessionKey + '-handled';
|
|
17
|
+
const scrollKey = sessionKey + '-scroll';
|
|
18
|
+
let checkpointRevision = 0;
|
|
19
|
+
const owner = makeId();
|
|
20
|
+
|
|
21
|
+
function safeRead(key) {
|
|
22
|
+
try { return store.getItem(key); } catch { return null; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function safeWrite(key, value) {
|
|
26
|
+
try { store.setItem(key, value); } catch { /* quota exceeded or private mode */ }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function safeRemove(key) {
|
|
30
|
+
try { store.removeItem(key); } catch { /* unavailable storage */ }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadSession() {
|
|
34
|
+
try {
|
|
35
|
+
const raw = safeRead(sessionKey);
|
|
36
|
+
if (!raw) return null;
|
|
37
|
+
const parsed = JSON.parse(raw);
|
|
38
|
+
if (Number.isInteger(parsed.checkpointRevision)) {
|
|
39
|
+
checkpointRevision = Math.max(checkpointRevision, parsed.checkpointRevision);
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
} catch { return null; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function saveSession(session) {
|
|
46
|
+
if (!session || !session.id) return;
|
|
47
|
+
// Adopt a revision another tab stored so this write does not lower it.
|
|
48
|
+
// localStorage has no compare-and-set, so a save from another tab that
|
|
49
|
+
// lands between this read and the write below can still be overwritten.
|
|
50
|
+
loadSession();
|
|
51
|
+
const payload = {
|
|
52
|
+
...session,
|
|
53
|
+
checkpointRevision,
|
|
54
|
+
};
|
|
55
|
+
safeWrite(sessionKey, JSON.stringify(payload));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function clearSession() {
|
|
59
|
+
safeRemove(sessionKey);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function nextCheckpointRevision() {
|
|
63
|
+
const existing = loadSession();
|
|
64
|
+
checkpointRevision += 1;
|
|
65
|
+
if (existing?.id) saveSession(existing);
|
|
66
|
+
return checkpointRevision;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function seedCheckpointRevision(value) {
|
|
70
|
+
if (Number.isInteger(value)) checkpointRevision = Math.max(checkpointRevision, value);
|
|
71
|
+
return checkpointRevision;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function currentCheckpointRevision() {
|
|
75
|
+
return checkpointRevision;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readHandledIds() {
|
|
79
|
+
const raw = safeRead(handledKey);
|
|
80
|
+
if (!raw) return [];
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(raw);
|
|
83
|
+
if (Array.isArray(parsed)) {
|
|
84
|
+
return parsed.filter(id => typeof id === 'string' && id);
|
|
85
|
+
}
|
|
86
|
+
if (typeof parsed === 'string' && parsed) return [parsed];
|
|
87
|
+
} catch { /* legacy values were stored as a plain session id */ }
|
|
88
|
+
return [raw];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function markHandled(id) {
|
|
92
|
+
if (!id) return;
|
|
93
|
+
const ids = readHandledIds().filter(existing => existing !== id);
|
|
94
|
+
ids.push(id);
|
|
95
|
+
safeWrite(handledKey, JSON.stringify(ids.slice(-8)));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isHandled(id) {
|
|
99
|
+
return !!id && readHandledIds().includes(id);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function clearHandled(id) {
|
|
103
|
+
if (!id) {
|
|
104
|
+
safeRemove(handledKey);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const remaining = readHandledIds().filter(existing => existing !== id);
|
|
108
|
+
if (remaining.length > 0) safeWrite(handledKey, JSON.stringify(remaining));
|
|
109
|
+
else safeRemove(handledKey);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function writeScrollY(y) {
|
|
113
|
+
safeWrite(scrollKey, String(y));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readScrollY() {
|
|
117
|
+
const raw = safeRead(scrollKey);
|
|
118
|
+
if (raw == null) return null;
|
|
119
|
+
const n = parseFloat(raw);
|
|
120
|
+
return isFinite(n) ? n : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function clearScrollY() {
|
|
124
|
+
safeRemove(scrollKey);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
owner,
|
|
129
|
+
sessionKey,
|
|
130
|
+
handledKey,
|
|
131
|
+
scrollKey,
|
|
132
|
+
saveSession,
|
|
133
|
+
loadSession,
|
|
134
|
+
clearSession,
|
|
135
|
+
nextCheckpointRevision,
|
|
136
|
+
seedCheckpointRevision,
|
|
137
|
+
currentCheckpointRevision,
|
|
138
|
+
markHandled,
|
|
139
|
+
isHandled,
|
|
140
|
+
clearHandled,
|
|
141
|
+
writeScrollY,
|
|
142
|
+
readScrollY,
|
|
143
|
+
clearScrollY,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
root.__IMPECCABLE_LIVE_SESSION__ = { createLiveBrowserSessionState };
|
|
148
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|