diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..b6d3433d07 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,3 @@ +# Lint with the webkit rules before every commit. +npx eslint . +npx stylelint "**/*.{css,scss,vue}" diff --git a/.stylelintrc.json b/.stylelintrc.json index be556c2d2c..f862c5900a 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,19 +1,12 @@ { - "extends": [ - "@aziontech/webkit/stylelint-config" - ], + "extends": ["@aziontech/webkit/stylelint-config"], "overrides": [ { - "files": [ - "**/*.vue", - "**/*.astro" - ], + "files": ["**/*.vue", "**/*.astro"], "customSyntax": "postcss-html" }, { - "files": [ - "**/*.scss" - ], + "files": ["**/*.scss"], "customSyntax": "postcss-scss" } ] diff --git a/CLAUDE.md b/CLAUDE.md index 3a4e212886..674af03d02 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,3 +1,73 @@ + + +## @aziontech/webkit design system + +This project uses the Azion design system. Its guidance is organized in four layers — know which +layer you're in, because only one of them is optional: + +1. **Invariants (always-on, below)** — the non-negotiables. Follow them in every UI change. +2. **Enforcement (the machine — non-optional)** — ESLint blocks the mechanical invariants at lint/CI; + the `webkit` MCP grounds every component choice; the `webkit-ui-verifier` agent checks the rest at + runtime (axe, both themes, states). If lint or the verifier fails, the change is not done. +3. **Skills (on-demand guidance)** — invoke `/webkit-*` skills for how to do a task well. +4. **Reference (depth)** — `.claude/rules/webkit-*.md` carry the full standards behind the invariants. + +### Invariants — always follow + +- **Components first.** Reach for an `@aziontech/webkit` component before writing custom UI. Find one + via the `webkit` MCP (`suggest_component`) or `node_modules/@aziontech/webkit/catalog.json`. +- **Flat imports.** `import Button from '@aziontech/webkit/button'` (PascalCase binding = the subpath). + Never category-prefixed, `/src/`, deep-internal, or a bare-package barrel. +- **Tokens only.** Color, typography, shape, spacing come from `@aziontech/theme` + (`var(--primary)`, `var(--spacing-md)`, `text-button-lg`) — never hex, `rgb`, `hsl`, or Tailwind palette. +- **Never restyle a webkit component.** No `class`/`:class`/`style` on its tag — compose inside its + slots or use its props. +- **Styles + theme.** Load `src/webkit.css` once at the app entry — it `@import`s the theme's + Tailwind v4 stylesheet (tokens + `@import "tailwindcss"` + fonts) and `@source`s webkit so its + component classes compile. Import `webkit.css`, not `@aziontech/theme` bare (the `@source` is what + compiles webkit's classes). Light is the default; for dark set ``. +- **Accessible & keyboard-operable.** Label every field, keep a visible focus ring, mirror state in + ARIA, no click handler without a key handler. +- **Small bundles.** Prefer the `-root` path (or specific sub-components), import icons + individually, lazy-load heavy overlays. +- **App-level setup at first use.** A few components declare a one-time wiring in their catalog + `setup` field (via the MCP's `get_component`). Toast: before the first `toast(...)` call, wire + `import { ToastPlugin } from '@aziontech/webkit/toast'` + `.use(ToastPlugin)` on `createApp()` + in the entry (the plugin mounts the region automatically); skip if already wired. `webkit doctor` + flags usage with missing setup. + +### Enforced by lint (blocks — nothing here is a suggestion) + +ESLint (`@aziontech/webkit/eslint-plugin` + `vuejs-accessibility`, wired by `webkit init`) fails the +build on: category-prefixed / deep-internal / barrel imports · hardcoded color · restyling a webkit +component · a custom element where a webkit component exists · a hand-rolled `modelValue`+`update` +instead of `defineModel` · a deprecated component · a whole-icon-set import · missing the +tree-shakeable root · and the static a11y set (alt-text, aria-props, aria-role, click-without-key, +label-has-for, no-autofocus). The rest — states, motion-reduce, contrast/behavioral a11y, both +themes, microcopy — is verified at runtime by `/webkit-ui-verify` (the `webkit-ui-verifier` agent +runs axe + screenshots), and taste is a review concern. + +### Skills — invoke for how to build UI well + +Start with `/webkit-ui-craft` (the umbrella that routes the rest). Get structure and states right +before any polish: + +- **Structure:** `/webkit-ux-heuristics` (right component per moment) · `/webkit-ui-states` (state surface + async behavior) · `/webkit-form` · `/webkit-create-surface` (where a create lives: page vs drawer, Advanced, the commit bar) · `/webkit-errors` (where a failure goes: field, section Message, toast, auth card) · `/webkit-tables` (data-driven `` + cell recipes) · `/webkit-lists` (the index page around it + the filter system) · `/webkit-navigation` +- **Words:** `/webkit-microcopy` (Azion product copy: punctuation, sentence case, label vs message, one word per concept) +- **Foundation:** `/webkit-baseline-ui` (tokens, hierarchy, containers, responsive widths) +- **Cross-cutting quality:** `/webkit-theming-dark-mode` · `/webkit-data-viz` +- **Polish:** `/webkit-motion-polish` · `/webkit-impeccable-polish` (finish + earned delight) +- **Verify + migrate:** `/webkit-ui-verify` (runtime axe + both themes + states) · `/webkit-ds-adoption` + +Mechanics of consuming the package (imports, tokens, tree-shaking) live in `/webkit-usage`. + +### Agents — delegate a whole task + +`webkit-expert` (which component + how) · `webkit-adopter` (the init/adoption flow) · `webkit-reviewer` +(review a diff for correct/performant usage) · `webkit-ui-verifier` (drive the screen: both themes, +widths, console, axe, states) · `webkit-adoption-auditor` (coverage scorecard: custom → webkit). + +Full standards behind the invariants: `.claude/rules/webkit-*.md`. ## @aziontech/webkit design system diff --git a/astro.config.ts b/astro.config.ts index 50bc61cbe5..2d7f828a59 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -22,7 +22,7 @@ import rehypeSlug from './plugins/rehype-slug-config' import { rehypei18nAutolinkHeadings } from './plugins/rehype-i18n-autolink-headings'; import { rehypeOptimizeStatic } from './plugins/rehype-optimize-static'; import { rehypeTasklistEnhancer } from './plugins/rehype-tasklist-enhancer'; -import rehypeScrollableTables from './plugins/rehype-scrollable-tables.js' +import rehypeDocTable from './plugins/rehype-doc-table' type MarkdownConfig = NonNullable; @@ -58,7 +58,7 @@ export default defineConfig({ ] as MarkdownConfig['remarkPlugins'], rehypePlugins: [ rehypeSlug, - rehypeScrollableTables, + rehypeDocTable, // This adds links to headings // rehypeLinks, // disabling target Blank [rehypeAutolinkHeadings, autoLinks], diff --git a/azion.config.mjs b/azion.config.mjs index 91aa8aae5a..9ed768265e 100644 --- a/azion.config.mjs +++ b/azion.config.mjs @@ -50,8 +50,12 @@ export default { variable: '${uri}', conditional: 'if', operator: 'matches', + // NOTE: this is a string, not a regex literal, so the original + // `\.` was already collapsing to a bare `.` (any character) before + // reaching the edge. Kept as-is to preserve routing; to match a + // literal dot the escape has to be doubled (`\\.`). argument: - '\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|ttf|otf|woff|woff2|eot|pdf|doc|docx|xls|xlsx|ppt|pptx|mp4|webm|mp3|wav|ogg|css|js|json|xml|html|txt|csv|zip|rar|7z|tar|gz|webmanifest|map|md|yaml|yml)$' + '.(jpg|jpeg|png|gif|bmp|webp|svg|ico|ttf|otf|woff|woff2|eot|pdf|doc|docx|xls|xlsx|ppt|pptx|mp4|webm|mp3|wav|ogg|css|js|json|xml|html|txt|csv|zip|rar|7z|tar|gz|webmanifest|map|md|yaml|yml)$' } ] ], @@ -112,7 +116,11 @@ export default { variable: '${uri}', conditional: 'if', operator: 'matches', - argument: '^(?!.*\/$)(?![\s\S]*\.[a-zA-Z0-9]+$).*' + // NOTE: a string, not a regex literal — `\s\S` collapsed to `sS` + // and `\.` to `.` before the edge saw them, so the second lookahead + // has never excluded file-like URIs. Kept verbatim to preserve + // routing; doubling the escapes (`[\\s\\S]`, `\\.`) is the real fix. + argument: '^(?!.*/$)(?![sS]*.[a-zA-Z0-9]+$).*' } ] ], diff --git a/backend/semrush-replace-url/helpers/csv.js b/backend/semrush-replace-url/helpers/csv.js index 1c77f9f689..592834151a 100644 --- a/backend/semrush-replace-url/helpers/csv.js +++ b/backend/semrush-replace-url/helpers/csv.js @@ -23,6 +23,6 @@ export async function read(filepath) { return redirects; } catch (error) { - throw new Error(`Error loading redirects: ${error.message}`); + throw new Error(`Error loading redirects: ${error.message}`, { cause: error }); } } diff --git a/backend/semrush-replace-url/index.i18n.js b/backend/semrush-replace-url/index.i18n.js index 013e821968..f731cca789 100644 --- a/backend/semrush-replace-url/index.i18n.js +++ b/backend/semrush-replace-url/index.i18n.js @@ -40,20 +40,20 @@ async function processFile(filePath, redirects) { rgxDoubleQuote: rgxDoubleQuote.toString(), url30x, url200, - contentMatchCount: contentMatch.length, + contentMatchCount: (contentMatchSingleQuote?.length ?? 0) + (contentMatchDoubleQuote?.length ?? 0), processedCount: counterFoundLinks }); if(contentMatchSingleQuote.length) { newContent = newContent.replace( - isRoot ? /'https\:\/\/www\.azion\.com\/'/ : rgxSingleQuote, + isRoot ? /'https:\/\/www\.azion\.com\/'/ : rgxSingleQuote, `'${url200}'` ); } if(contentMatchDoubleQuote.length) { newContent = newContent.replace( - isRoot ? /"https\:\/\/www\.azion\.com\/"/ : rgxDoubleQuote, + isRoot ? /"https:\/\/www\.azion\.com\/"/ : rgxDoubleQuote, `'${url200}'` ); } diff --git a/backend/semrush-replace-url/index.js b/backend/semrush-replace-url/index.js index cb31a756b6..c8c0952171 100644 --- a/backend/semrush-replace-url/index.js +++ b/backend/semrush-replace-url/index.js @@ -3,7 +3,7 @@ import { promises as fs } from 'fs'; import path from 'path' import { read as readcsv } from './helpers/csv.js'; -import { removeHostFromUrl, removeHostAndLangFromUrl, wwwazioncom, isFromRoot } from './helpers/url.js'; +import { removeHostFromUrl, wwwazioncom, isFromRoot } from './helpers/url.js'; let counterFoundLinks = 0 let PATH = { @@ -40,10 +40,10 @@ async function processFile(filePath, redirects) { fileModified = true if(contentMatchMdLinkAnchor.length) - newContent = findReplace(newContent, isRoot ? /\\(https\:\/\/www\.azion\.com\/\\)/ : rgxMdLink, `(${url200})`) + newContent = findReplace(newContent, isRoot ? /\\(https:\/\/www\.azion\.com\/\\)/ : rgxMdLink, `(${url200})`) if(contentMatchMdLinkAnchor.length) - newContent = findReplace(newContent, isRoot ? /\\(https\:\/\/www\.azion\.com\/\\)/ : rgxMdLinkAnchor, `(${url200}#`) + newContent = findReplace(newContent, isRoot ? /\\(https:\/\/www\.azion\.com\/\\)/ : rgxMdLinkAnchor, `(${url200}#`) console.log(`{ isRoot: ${isRoot}, diff --git a/cicd/replace-links/markdown.js b/cicd/replace-links/markdown.js index b7a93f1b5b..a6f4a40613 100644 --- a/cicd/replace-links/markdown.js +++ b/cicd/replace-links/markdown.js @@ -24,22 +24,6 @@ function readJsonFile(filePath) { return json; } -function jsonToFile(jsonObj, filePath) { - const jsonString = JSON.stringify(jsonObj, null, 2); - - fs.writeFile(filePath, jsonString, 'utf8', (err) => { - if (err) { - console.error('An error occurred while writing JSON to file:', err); - } else { - console.log('JSON has been written successfully to', filePath); - } - }); -}; - -async function touchJsonFileResult(data, fileDist) { - await jsonToFile(data, fileDist); -}; - ////////////////// // end utils.js // ////////////////// diff --git a/integrations/astro-asides.ts b/integrations/astro-asides.ts index b547b52afe..ad658827a6 100644 --- a/integrations/astro-asides.ts +++ b/integrations/astro-asides.ts @@ -11,27 +11,8 @@ export const asideAutoImport: Record = { '~/components/Aside.astro': [['default', AsideTagname]], }; -/** - * remark plugin that converts blocks delimited with `:::` into instances of - * the `