@wix/agent-skills 1.16.0 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +18 -3
  3. package/skills/wix-app/references/editor-react-component/A11Y-REVIEW.md +12 -7
  4. package/skills/wix-app/references/editor-react-component/COMPONENT-CONFIGURATION.md +60 -0
  5. package/skills/wix-app/references/editor-react-component/DIRECTIONALITY.md +6 -6
  6. package/skills/wix-app/references/editor-react-component/SITE-CONTEXT-HOOKS.md +187 -0
  7. package/skills/wix-app/scripts/scan-a11y-code.cjs +777 -0
  8. package/skills/wix-app/scripts/scan-a11y-eslint.cjs +201 -0
  9. package/skills/wix-headless/references/CAPABILITIES.md +16 -3
  10. package/skills/wix-headless/references/SDK_HANDOFF.md +1 -1
  11. package/skills/wix-headless/references/SEED.md +1 -1
  12. package/skills/wix-headless/references/inline-recipes/how-to-code-forms.md +560 -102
  13. package/skills/wix-headless/references/inline-recipes/setup-forms.md +210 -143
  14. package/skills/wix-manage/SKILL.md +8 -1
  15. package/skills/wix-manage/references/accessibility/scan-site-accessibility.md +98 -0
  16. package/skills/wix-manage/references/analytics/analytics-dashboard-navigation.md +1 -2
  17. package/skills/wix-manage/references/sites/site-import.md +67 -15
  18. package/skills/wix-replatform/resources/rp-target-wix/lib/wix-writers.js +1 -1
  19. package/skills/wix-vibe-headless/SKILL.md +2 -1
  20. package/skills/wix-vibe-headless/install/deploy.cjs +64 -3
  21. package/skills/wix-vibe-headless/platforms/base44.md +12 -37
  22. package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +7 -11
  23. package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +7 -11
  24. package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +1 -2
  25. package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +7 -12
  26. package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +7 -11
  27. package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +7 -11
  28. package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +7 -11
  29. package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +7 -11
  30. package/skills/wix-vibe-headless/references/shared/app/components/WixManageBanner.jsx +1 -1
  31. package/skills/wix-vibe-headless/references/shared/app/rest/wix-config.js +5 -3
  32. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +7 -11
  33. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.js +44 -5
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // oxlint-disable no-console preserve-caught-error
5
+
6
+ const path = require('path');
7
+ const { createRequire } = require('module');
8
+
9
+ const ROOT = process.cwd();
10
+ const LOCAL_REQUIRE = createRequire(__filename);
11
+ const ROOT_REQUIRE = createRequire(path.join(ROOT, 'package.json'));
12
+ const { ESLint } = loadModule('eslint');
13
+ const jsxA11y = loadModule('eslint-plugin-jsx-a11y');
14
+ const tsParser = loadModule('@typescript-eslint/parser');
15
+
16
+ const RULE_CONFIG = {
17
+ 'jsx-a11y/alt-text': 'error',
18
+ 'jsx-a11y/anchor-has-content': 'error',
19
+ 'jsx-a11y/anchor-is-valid': 'error',
20
+ 'jsx-a11y/aria-activedescendant-has-tabindex': 'error',
21
+ 'jsx-a11y/aria-props': 'error',
22
+ 'jsx-a11y/aria-proptypes': 'error',
23
+ 'jsx-a11y/aria-role': 'error',
24
+ 'jsx-a11y/aria-unsupported-elements': 'error',
25
+ 'jsx-a11y/click-events-have-key-events': 'error',
26
+ 'jsx-a11y/heading-has-content': 'error',
27
+ 'jsx-a11y/iframe-has-title': 'error',
28
+ 'jsx-a11y/img-redundant-alt': 'error',
29
+ 'jsx-a11y/interactive-supports-focus': 'error',
30
+ 'jsx-a11y/label-has-associated-control': 'error',
31
+ 'jsx-a11y/media-has-caption': 'error',
32
+ 'jsx-a11y/mouse-events-have-key-events': 'error',
33
+ 'jsx-a11y/no-access-key': 'error',
34
+ 'jsx-a11y/no-aria-hidden-on-focusable': 'error',
35
+ 'jsx-a11y/no-autofocus': 'error',
36
+ 'jsx-a11y/no-distracting-elements': 'error',
37
+ 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error',
38
+ 'jsx-a11y/no-noninteractive-element-interactions': 'error',
39
+ 'jsx-a11y/no-noninteractive-element-to-interactive-role': 'error',
40
+ 'jsx-a11y/no-noninteractive-tabindex': 'error',
41
+ 'jsx-a11y/no-redundant-roles': 'error',
42
+ 'jsx-a11y/no-static-element-interactions': 'error',
43
+ 'jsx-a11y/prefer-tag-over-role': 'error',
44
+ 'jsx-a11y/role-has-required-aria-props': 'error',
45
+ 'jsx-a11y/role-supports-aria-props': 'error',
46
+ 'jsx-a11y/scope': 'error',
47
+ 'jsx-a11y/tabindex-no-positive': 'error',
48
+ };
49
+
50
+ function toRelative(filePath) {
51
+ return path.relative(ROOT, filePath) || filePath;
52
+ }
53
+
54
+ function severityLabel(severity) {
55
+ if (severity === 2) return 'error';
56
+ if (severity === 1) return 'warning';
57
+ return 'off';
58
+ }
59
+
60
+ function loadModule(name) {
61
+ try {
62
+ return LOCAL_REQUIRE(name);
63
+ } catch (localError) {
64
+ try {
65
+ return ROOT_REQUIRE(name);
66
+ } catch (rootError) {
67
+ throw new Error(
68
+ `Missing dependency "${name}". Local resolution failed: ${localError.message}. Root resolution failed: ${rootError.message}`,
69
+ );
70
+ }
71
+ }
72
+ }
73
+
74
+ function createEslint() {
75
+ try {
76
+ return new ESLint({
77
+ overrideConfigFile: true,
78
+ overrideConfig: [
79
+ {
80
+ files: ['**/*.{tsx,jsx,ts,js}'],
81
+ languageOptions: {
82
+ parser: tsParser,
83
+ parserOptions: {
84
+ ecmaFeatures: { jsx: true },
85
+ ecmaVersion: 2022,
86
+ sourceType: 'module',
87
+ },
88
+ },
89
+ plugins: {
90
+ 'jsx-a11y': jsxA11y,
91
+ },
92
+ rules: RULE_CONFIG,
93
+ },
94
+ ],
95
+ });
96
+ } catch (flatConfigError) {
97
+ try {
98
+ return new ESLint({
99
+ useEslintrc: false,
100
+ overrideConfig: {
101
+ parser: '@typescript-eslint/parser',
102
+ plugins: ['jsx-a11y'],
103
+ parserOptions: {
104
+ ecmaFeatures: { jsx: true },
105
+ ecmaVersion: 2022,
106
+ sourceType: 'module',
107
+ },
108
+ rules: RULE_CONFIG,
109
+ },
110
+ extensions: ['.tsx', '.jsx', '.ts', '.js'],
111
+ });
112
+ } catch (legacyConfigError) {
113
+ throw new Error(
114
+ `Failed to initialize ESLint. Flat config error: ${flatConfigError.message}. Legacy config error: ${legacyConfigError.message}`,
115
+ );
116
+ }
117
+ }
118
+ }
119
+
120
+ async function main() {
121
+ const files = process.argv.slice(2);
122
+ if (files.length === 0) {
123
+ console.log(
124
+ JSON.stringify(
125
+ {
126
+ error: 'No files specified.',
127
+ usage: 'node <SKILL_ROOT>/scripts/scan-a11y-eslint.cjs <file1> [file2] ...',
128
+ },
129
+ null,
130
+ 2,
131
+ ),
132
+ );
133
+ process.exit(1);
134
+ }
135
+
136
+ const absoluteFiles = files.map((f) => path.resolve(f));
137
+
138
+ const eslint = createEslint();
139
+
140
+ const results = await eslint.lintFiles(absoluteFiles);
141
+
142
+ const findings = [];
143
+ const parseErrors = [];
144
+
145
+ for (const result of results) {
146
+ const relFile = toRelative(result.filePath);
147
+
148
+ for (const msg of result.messages) {
149
+ if (msg.fatal) {
150
+ parseErrors.push({
151
+ file: relFile,
152
+ line: msg.line,
153
+ column: msg.column,
154
+ message: msg.message,
155
+ });
156
+ continue;
157
+ }
158
+
159
+ if (!msg.ruleId || !msg.ruleId.startsWith('jsx-a11y/')) continue;
160
+
161
+ findings.push({
162
+ file: relFile,
163
+ line: msg.line,
164
+ column: msg.column,
165
+ endLine: msg.endLine ?? null,
166
+ endColumn: msg.endColumn ?? null,
167
+ rule: msg.ruleId,
168
+ severity: severityLabel(msg.severity),
169
+ message: msg.message,
170
+ });
171
+ }
172
+ }
173
+
174
+ const ruleBreakdown = {};
175
+ for (const f of findings) {
176
+ ruleBreakdown[f.rule] = (ruleBreakdown[f.rule] || 0) + 1;
177
+ }
178
+
179
+ const output = {
180
+ meta: {
181
+ filesScanned: files.length,
182
+ engine: 'eslint + eslint-plugin-jsx-a11y',
183
+ rulesEnabled: Object.keys(RULE_CONFIG).length,
184
+ parseErrors,
185
+ },
186
+ findings,
187
+ summary: {
188
+ totalFindings: findings.length,
189
+ filesWithFindings: new Set(findings.map((f) => f.file)).size,
190
+ cleanFiles: files.length - new Set(findings.map((f) => f.file)).size,
191
+ ruleBreakdown,
192
+ },
193
+ };
194
+
195
+ console.log(JSON.stringify(output, null, 2));
196
+ }
197
+
198
+ main().catch((err) => {
199
+ console.error(JSON.stringify({ error: err.message }, null, 2));
200
+ process.exit(1);
201
+ });
@@ -37,10 +37,23 @@ The verticals the skill operates end-to-end today: **stores · blog · cms · fo
37
37
  - **Required site features:** an index/list view of the collection; a detail page per item. (Visitor reads are public; visitor writes go through a form.) **(Opt-in, with members)** when the brief asks for per-user-private data ("my saved items", a personal list) or member-only content **and** members login is in the run, a collection can instead be **member-scoped** — each member sees/edits only their own rows, or any logged-in member reads shared member-only content. Added only on that intent, never by default; see the members cross-cutting entry and `setup-cms.md`.
38
38
  - **Implementation checklist:** show each item's main fields with clear labels; link the list to each item's detail page; show a sensible empty state when there are no items yet.
39
39
 
40
- ### forms — capture leads
41
- - **Intent:** contact / lead / signup / waitlist / "let people reach me" / generic custom data capture / nothing dynamic named. **Not for RSVP** — confirming attendance to an event or occasion (a wedding, party, or gathering) is the **events** vertical, which has a built-in RSVP registration form; route there even when it's a single occasion with no tickets. Use `forms` only when no event is involved.
40
+ ### forms — any visitor-fillable form
41
+ - **Intent:** contact / enquiry / lead / signup / waitlist / application / feedback / survey /
42
+ questionnaire / quote request / intake or registration form / "let people reach me" / generic custom
43
+ data capture — plus the **floor** when nothing dynamic is named. Lead capture is the most common
44
+ case, **not the boundary**: any form a visitor fills in and submits is this vertical, and whether
45
+ the submission also becomes a CRM contact is an optional per-field mapping.
46
+ - **Not forms — an RSVP is `events`:** confirming attendance to an event or occasion (a wedding,
47
+ party, or gathering) is the **events** vertical, which ships a built-in RSVP registration form —
48
+ route there even for a single occasion with no tickets.
49
+ - **Not forms — a per-service booking form is `bookings`:** the form attached to a bookable service
50
+ belongs to **bookings**. Use `forms` only when neither an event nor a bookable service is
51
+ involved.
42
52
  - **Required site features:** a visible form; a confirmation after submitting; basic field validation.
43
- - **Implementation checklist:** render every field with a clear label; mark which fields are required; a clear submit button; show a thank-you / success state after sending; show a friendly message if the submit fails.
53
+ A site may have **more than one** form, each with its own purpose and field set.
54
+ - **Implementation checklist:** render every field with a clear label; mark which fields are required;
55
+ a clear submit button; show a thank-you / success state after sending; show a friendly message if
56
+ the submit fails.
44
57
 
45
58
  ### events — events and registration
46
59
  - **Intent:** event / ticket / RSVP / registration / attendees / "confirm attendance" / a wedding, party, or gathering people respond to. **Covers RSVP to a single occasion** (one wedding, one party) — not only multi-event listings, and not only ticketed events. An invitation with an RSVP is this vertical, not `forms`.
@@ -58,7 +58,7 @@ The host binds to **schema**, not to a frozen list of content IDs. **Content is
58
58
 
59
59
  Carry only the **structural** carve-outs the coding step genuinely reads — the collection/form *names and keys* you must know to query and bind, which do **not** change when an owner adds a row:
60
60
  - **cms:** `collectionId` + field keys per collection
61
- - **forms:** **`formId` only.** The field set, order, labels, `required`, validation format, and dropdown options are **read live from the form schema at render time** (visitor token, no elevate — `how-to-code-forms.md`), so the handoff no longer carries per-field `target` keys. Reading the schema live is the source of truth: it closes the seed↔site drift by construction (a field the owner adds/removes/relabels in the dashboard reflects on the site with no code change) and is the more v2-aligned "host resolves everything else live" shape. (For a bespoke, design-led form where dashboard-editability isn't the value prop, the host may still hardcode inputs from the known `target`s — `how-to-code-forms.md` covers that mode — but the default and the carry is `formId` only.)
61
+ - **forms:** `formId` + each form's field `target` keys (the submission keys the frontend binds `name` = target). Field set, labels, options, and validation (`required`, format, length/pattern) are read live from the schema (`how-to-code-forms.md`).
62
62
 
63
63
  Everything else the host resolves live from the queries in §3. (Static platform constants — Stores/Bookings app IDs, the Blog appDefId — and the public `clientId` are needed too, but they're identical for every site, not seed output; they're in §2/§3.)
64
64
 
@@ -34,7 +34,7 @@ Each entry states only the entities, where their count/content comes from, the p
34
34
  - **stores** — *A product catalog.* Recipe: **`setup-online-store.md`**. `intent.stores.productCount` products whose names/prices fit `brand`. If `intent.stores.categoriesNamed` is non-empty, create exactly those categories and assign products into them; **if empty, create none** (skill policy — overrides any docs default). **Variant cardinality — keep it small by default:** unless `intent.stores` calls for specific options, give each product **at most one option with ≤3 choices** (so ≤3 variants/product); many products legitimately have **no** options (a single variant). Seeding cost scales with the variant Cartesian product and the field count per variant, so don't manufacture options the brief doesn't need — but **honor a larger/explicit option set when intent names one** (this default is a floor, not a cap). Text-only by default (images only when `imagery` is on — §1, §4). **Keep (seed-time only):** `categoryIds[]` to assign products into their categories; **nothing goes to the handoff** — the frontend lists products and categories **live** (`queryProducts` / `searchProducts` / `queryCategories`), so owner-added products/categories appear with no code change.
35
35
  - **blog** — *Initial posts.* Recipe: **`setup-blog.md`**. `intent.blog.postCount` posts on `intent.blog.topics` (or brand-derived topics). Text-only (no covers). **Keep:** nothing for the handoff — posts are discovered **live** (`queryPosts`, `[...slug]` routes). Track created post ids only transiently (to confirm success / assign categories).
36
36
  - **cms** — *Content collections.* Recipe: **`setup-cms.md`**. One collection per `intent.cms.collections` entry; `itemCount` items each, content from `brand`. Collections are **public-read** (visitor reads on the frontend) by default. **(Opt-in, members only)** if the brief calls for per-user-private or member-only data **and** members login is in the run, seed that collection **member-scoped** instead (per-user or member-only — see `setup-cms.md`); a per-user collection is seeded **empty** (members populate it), since admin-seeded rows are owned by the admin. **Keep (→ handoff):** `collectionIds{<name>}` + each collection's **field keys** — this is *structural* (the frontend must know the collection name and its fields to query and bind; it doesn't change when an owner adds a row). `itemIds{<name>:[]}` only transiently, for wiring multi-references during seeding.
37
- - **forms** — *Lead-capture forms.* Recipe: **`setup-forms.md`**. One form per `intent.forms.forms` entry; fields from the entry; `purpose` names the form. **Keep (→ handoff):** `formIds[]` + each form's field **`target`** keys — *structural* (the frontend sets input `name` = target to submit).
37
+ - **forms** — *Visitor-fillable forms.* Recipe: **`setup-forms.md`**. One form per `intent.forms.forms` entry; fields from the entry, `purpose` names the form. **Keep (→ handoff):** `formIds[]` + each form's field **`target`** keys — *structural* (the frontend binds each input's `name` = target to submit; targets are **immutable**, so they don't go stale when an owner relabels a field).
38
38
  - **events** — *A ticketed or RSVP occasion, or upcoming events.* Recipe: **`setup-events.md`**. Each event is **either `TICKETING`** (paid — it has ticket tiers) **or `RSVP`** (free — the registration form is built-in, so don't seed form fields) per intent. For an **RSVP occasion** (a wedding, party, gathering): one RSVP event with the occasion's real details (title, a **future** date/time, location). For a **ticketed** event: the event plus its ticket tiers (default a single `"General Admission"` tier if none named). For a **listing** site: `intent.events.eventCount` events with brand-appropriate titles and **future** start dates (a default location/timezone is fine). Text-only. **Keep:** nothing for the handoff — events and tiers are discovered **live** (`queryEvents`, `getEventBySlug`, `queryAvailableTickets`); track `eventIds`/`ticketDefinitionIds` only transiently.
39
39
  - **bookings** — *Bookable services.* Recipe: **`setup-bookings.md`**. `intent.bookings.serviceCount` services (name + short description fitting `brand`, a simple duration and price); keep the schedule minimal. **Keep:** `serviceIds[]` as **seed-time state** (pricing-plans' coverage step keys on them, below); nothing for the handoff — services are discovered **live** (`queryServices`).
40
40
  - **pricing-plans** — *Membership tiers.* Recipe: **`setup-pricing-plans.md`**. `intent.pricing-plans.planCount` recurring plans (name, price, a monthly billing cycle) fitting `brand`. **When bookings is also in this run and the intent is a membership that covers services** (a studio/gym/class pass), also **attach the covered bookings services to the plan** — seed the bookings services first, since coverage keys on their service ids (see the recipe). **Keep:** the bookings coverage only transiently, to wire it during seeding; nothing for the handoff — plans are discovered **live** (`queryPlans`) and plan→service coverage is read **live** at checkout, never from a frozen map (see `how-to-code-pricing-plans.md`).