@gunkin/uiify 0.0.17-alpha → 0.0.19-alpha

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 (91) hide show
  1. package/package.json +1 -1
  2. package/skills/uiify/SKILL.md +55 -33
  3. package/skills/uiify/references/compositions/card-rhythm.md +0 -2
  4. package/skills/uiify/references/compositions/destructive-confirmation.md +1 -3
  5. package/skills/uiify/references/compositions/form-grouping.md +0 -2
  6. package/skills/uiify/references/compositions/hero-actions.md +3 -5
  7. package/skills/uiify/references/compositions/page-frame.md +0 -2
  8. package/skills/uiify/references/compositions/pricing-actions.md +0 -2
  9. package/skills/uiify/references/compositions/save-feedback.md +1 -3
  10. package/skills/uiify/references/compositions/table-controls.md +1 -3
  11. package/skills/uiify/references/elements/accordion.md +38 -0
  12. package/skills/uiify/references/elements/alert-modal.md +32 -5
  13. package/skills/uiify/references/elements/avatar.md +31 -2
  14. package/skills/uiify/references/elements/badge.md +32 -3
  15. package/skills/uiify/references/elements/bar-chart.md +39 -7
  16. package/skills/uiify/references/elements/breadcrumb.md +38 -0
  17. package/skills/uiify/references/elements/button.md +38 -3
  18. package/skills/uiify/references/elements/candle-chart.md +38 -0
  19. package/skills/uiify/references/elements/card.md +38 -1
  20. package/skills/uiify/references/elements/carousel.md +38 -0
  21. package/skills/uiify/references/elements/checkbox.md +39 -0
  22. package/skills/uiify/references/elements/code-block.md +31 -2
  23. package/skills/uiify/references/elements/collapsible.md +38 -0
  24. package/skills/uiify/references/elements/color-picker.md +39 -0
  25. package/skills/uiify/references/elements/combobox.md +37 -0
  26. package/skills/uiify/references/elements/command-palette.md +33 -12
  27. package/skills/uiify/references/elements/context-menu.md +38 -0
  28. package/skills/uiify/references/elements/cookie-banner.md +30 -2
  29. package/skills/uiify/references/elements/date-picker.md +33 -9
  30. package/skills/uiify/references/elements/divider.md +30 -2
  31. package/skills/uiify/references/elements/drawer.md +38 -0
  32. package/skills/uiify/references/elements/dropdown-menu.md +38 -0
  33. package/skills/uiify/references/elements/empty-state.md +30 -2
  34. package/skills/uiify/references/elements/feature-grid.md +41 -1
  35. package/skills/uiify/references/elements/field.md +33 -3
  36. package/skills/uiify/references/elements/file-upload.md +40 -0
  37. package/skills/uiify/references/elements/footer.md +35 -4
  38. package/skills/uiify/references/elements/form.md +31 -4
  39. package/skills/uiify/references/elements/header.md +31 -3
  40. package/skills/uiify/references/elements/hero.md +43 -3
  41. package/skills/uiify/references/elements/hover-card.md +38 -0
  42. package/skills/uiify/references/elements/image-gallery.md +36 -4
  43. package/skills/uiify/references/elements/inline-alert.md +30 -2
  44. package/skills/uiify/references/elements/input.md +31 -4
  45. package/skills/uiify/references/elements/line-chart.md +38 -0
  46. package/skills/uiify/references/elements/logo-wall.md +31 -2
  47. package/skills/uiify/references/elements/modal.md +38 -0
  48. package/skills/uiify/references/elements/number-field.md +39 -0
  49. package/skills/uiify/references/elements/otp-input.md +39 -0
  50. package/skills/uiify/references/elements/pagination.md +31 -4
  51. package/skills/uiify/references/elements/password-input.md +33 -9
  52. package/skills/uiify/references/elements/popup.md +38 -0
  53. package/skills/uiify/references/elements/pricing-table.md +55 -2
  54. package/skills/uiify/references/elements/progress-bar.md +32 -4
  55. package/skills/uiify/references/elements/radio-group.md +39 -0
  56. package/skills/uiify/references/elements/rating.md +39 -0
  57. package/skills/uiify/references/elements/search-input.md +35 -4
  58. package/skills/uiify/references/elements/segmented-control.md +39 -0
  59. package/skills/uiify/references/elements/select.md +29 -1
  60. package/skills/uiify/references/elements/sidebar.md +33 -10
  61. package/skills/uiify/references/elements/skeleton.md +38 -0
  62. package/skills/uiify/references/elements/slider.md +39 -0
  63. package/skills/uiify/references/elements/spinner.md +38 -0
  64. package/skills/uiify/references/elements/stats-bar.md +30 -2
  65. package/skills/uiify/references/elements/stepper.md +32 -9
  66. package/skills/uiify/references/elements/switch.md +31 -4
  67. package/skills/uiify/references/elements/table-of-contents.md +39 -0
  68. package/skills/uiify/references/elements/table.md +32 -3
  69. package/skills/uiify/references/elements/tabs.md +38 -0
  70. package/skills/uiify/references/elements/tags-input.md +39 -0
  71. package/skills/uiify/references/elements/testimonial.md +35 -4
  72. package/skills/uiify/references/elements/textarea.md +30 -3
  73. package/skills/uiify/references/elements/timeline.md +31 -3
  74. package/skills/uiify/references/elements/toast.md +27 -1
  75. package/skills/uiify/references/elements/toggle-group.md +39 -0
  76. package/skills/uiify/references/elements/toggle.md +39 -0
  77. package/skills/uiify/references/elements/toolbar.md +39 -0
  78. package/skills/uiify/references/elements/tooltip.md +38 -0
  79. package/skills/uiify/references/elements/virtual-scroll.md +39 -0
  80. package/skills/uiify/references/examples/alert-confirmation.md +34 -0
  81. package/skills/uiify/references/examples/field-validation.md +37 -0
  82. package/skills/uiify/references/examples/select-basic.md +35 -0
  83. package/skills/uiify/references/examples/table-controls.md +40 -0
  84. package/skills/uiify/references/guides/assets.md +14 -0
  85. package/skills/uiify/references/guides/content.md +11 -0
  86. package/skills/uiify/references/guides/hierarchy.md +14 -0
  87. package/skills/uiify/references/guides/reference.md +9 -0
  88. package/skills/uiify/references/guides/states.md +12 -0
  89. package/skills/uiify/references/guides/surface.md +27 -0
  90. package/skills/uiify/scripts/lint.mjs +1 -1
  91. package/skills/uiify/scripts/static.mjs +71 -14
@@ -0,0 +1,39 @@
1
+ # toolbar
2
+
3
+ Binding: toolbar
4
+
5
+ Group three or more related controls with a compact keyboard navigation model.
6
+
7
+ ## Use when
8
+
9
+ Group at least three related controls that act on the same object or task.
10
+
11
+ ## Avoid when
12
+
13
+ Do not use toolbar semantics for a small set of unrelated actions or when controls need their own ordinary Tab stops.
14
+
15
+ ## Structure
16
+
17
+ Use the documented Toolbar.Root with an accessible name and a consistent orientation. Use buttons for actions, links for navigation, and selected state only for toggle buttons.
18
+
19
+ ## Responsive
20
+
21
+ Let controls wrap only if arrow-key movement still follows a predictable order; otherwise provide a scrollable group or switch to ordinary controls with Tab navigation.
22
+
23
+ ## States
24
+
25
+ The toolbar uses one Tab stop with arrow-key movement among enabled controls. Disabled controls are skipped; expose toggle state with the documented selected button behavior.
26
+
27
+ ## Verify
28
+
29
+ Test Tab entry/exit, arrow movement, orientation, disabled items, toggle announcements, and operation with a keyboard.
30
+
31
+ ## Common mistake
32
+
33
+ Adding role="toolbar" without implementing its expected arrow-key focus management.
34
+
35
+ ## Sources
36
+
37
+ - [www.w3.org](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/) (verified 2026-10-17)
38
+ - [www.w3.org](https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html) (verified 2026-10-17)
39
+ - [www.w3.org](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html) (verified 2026-10-17)
@@ -0,0 +1,38 @@
1
+ # tooltip
2
+
3
+ Binding: tooltip
4
+
5
+ Add a brief supplemental label to a control without replacing its name.
6
+
7
+ ## Use when
8
+
9
+ Add a short, supplemental explanation to an icon-only control or unfamiliar action. Keep the control's accessible name on the trigger; tooltip text does not replace it.
10
+
11
+ ## Avoid when
12
+
13
+ Do not put essential instructions or interactive content in a tooltip. Keep field labels visible; use `popup` or `hover-card` for content that needs interaction.
14
+
15
+ ## Structure
16
+
17
+ Nest `Tooltip.Root`, `Tooltip.Trigger`, and `Tooltip.Content` inside `Tooltip.Provider`. Content is a non-focusable tooltip associated with its trigger. For touch users, make the same information available without hover.
18
+
19
+ ## Responsive
20
+
21
+ Keep tooltip text brief and ensure it remains within the viewport. Touch has no hover, so essential information must remain available through visible text or another deliberate interaction.
22
+
23
+ ## States
24
+
25
+ UIify opens on mouse hover after the provider delay and immediately on keyboard focus; Escape closes it. Focus stays on the trigger. The WAI-ARIA APG tooltip pattern is still work in progress, so treat it as guidance rather than a settled standard.
26
+
27
+ ## Verify
28
+
29
+ Focus the trigger with the keyboard and confirm the tooltip appears and is referenced by the trigger. Confirm Escape dismisses it, pointer movement between trigger and content does not close it prematurely, and the content is not clipped at viewport edges. Check the equivalent touch interaction.
30
+
31
+ ## Common mistake
32
+
33
+ Using a tooltip as the only label for a field leaves the field unnamed when the tooltip is unavailable.
34
+
35
+ ## Sources
36
+
37
+ - [www.w3.org](https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/) (verified 2026-10-17)
38
+ - [www.w3.org](https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html) (verified 2026-10-17)
@@ -0,0 +1,39 @@
1
+ # virtual-scroll
2
+
3
+ Binding: virtual-scroll
4
+
5
+ A genuinely large list or grid needs to render only the visible range and nearby rows to keep scrolling responsive
6
+
7
+ ## Use when
8
+
9
+ A genuinely large list or grid needs to render only the visible range and nearby rows to keep scrolling responsive.
10
+
11
+ ## Avoid when
12
+
13
+ Do not virtualize short lists. Avoid it when users need browser find-in-page, a complete static/print view, or reliable access to all rows without JavaScript unless you provide an alternative.
14
+
15
+ ## Structure
16
+
17
+ Use `VirtualScroll` with stable unique item ids and a renderRow function. Keep the default list role for lists; use grid only when each row supplies the correct grid structure and keyboard model. Name the scroll region and provide a useful height/size estimate.
18
+
19
+ ## Responsive
20
+
21
+ Choose vertical or horizontal orientation deliberately, set rowHeight or estimateRowHeight to match actual content, and test at narrow widths and zoom.
22
+
23
+ ## States
24
+
25
+ This is a Tier 2 client component. It exposes visible rows with aria-posinset/aria-setsize; rows outside the rendered window are absent from the DOM. For high-value data, provide a non-virtualized route or export.
26
+
27
+ ## Verify
28
+
29
+ Check keyboard scrolling/focus, screen-reader item positions and total size, first/last rows, dynamic row measurement, list updates with stable ids, anchor/follow behavior if used, and no-JS/print alternatives.
30
+
31
+ ## Common mistake
32
+
33
+ A scroll container that exposes only visible rows can make the rest of the collection unreachable to find-in-page and some assistive workflows.
34
+
35
+ ## Sources
36
+
37
+ - [developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-posinset) (verified 2026-10-17)
38
+ - [developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-setsize) (verified 2026-10-17)
39
+ - [www.w3.org](https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html) (verified 2026-10-17)
@@ -0,0 +1,34 @@
1
+ # Example: alert-confirmation
2
+
3
+ Element: references/elements/alert-modal.md
4
+
5
+ Export: `AlertConfirmation`
6
+
7
+ ## Source
8
+
9
+ ```tsx
10
+ import { AlertModal } from "@gunkin/uiify";
11
+
12
+ export function AlertConfirmation() {
13
+ return (
14
+ <AlertModal
15
+ id="delete-draft"
16
+ title="Delete draft?"
17
+ description="This permanently removes the draft."
18
+ trigger="Delete draft"
19
+ cancelLabel="Keep draft"
20
+ actionLabel="Delete"
21
+ />
22
+ );
23
+ }
24
+ ```
25
+
26
+ ## CSS
27
+
28
+ ```css
29
+ [data-uiify-field] { display: grid; gap: 0.5rem; }
30
+ [data-uiify-field] input,
31
+ [data-uiify-select] { min-block-size: 2.75rem; padding-inline: 0.75rem; }
32
+ table { border-collapse: collapse; inline-size: 100%; }
33
+ th, td { padding: 0.5rem; text-align: start; }
34
+ ```
@@ -0,0 +1,37 @@
1
+ # Example: field-validation
2
+
3
+ Element: references/elements/field.md
4
+
5
+ Export: `FieldValidation`
6
+
7
+ ## Source
8
+
9
+ ```tsx
10
+ "use client";
11
+
12
+ import { useState } from "react";
13
+ import { Field } from "@gunkin/uiify";
14
+
15
+ export function FieldValidation() {
16
+ const [email, setEmail] = useState("");
17
+ const invalid = email.length > 0 && !email.includes("@");
18
+ return (
19
+ <Field.Root id="email" invalid={invalid}>
20
+ <Field.Label fieldId="email">Email</Field.Label>
21
+ <Field.Control fieldId="email" as="input" type="email" invalid={invalid} value={email} onChange={(event) => setEmail(event.currentTarget.value)} />
22
+ <Field.Description fieldId="email">We will send your receipt here.</Field.Description>
23
+ {invalid ? <Field.Error fieldId="email" invalid>Enter a valid email address.</Field.Error> : null}
24
+ </Field.Root>
25
+ );
26
+ }
27
+ ```
28
+
29
+ ## CSS
30
+
31
+ ```css
32
+ [data-uiify-field] { display: grid; gap: 0.5rem; }
33
+ [data-uiify-field] input,
34
+ [data-uiify-select] { min-block-size: 2.75rem; padding-inline: 0.75rem; }
35
+ table { border-collapse: collapse; inline-size: 100%; }
36
+ th, td { padding: 0.5rem; text-align: start; }
37
+ ```
@@ -0,0 +1,35 @@
1
+ # Example: select-basic
2
+
3
+ Element: references/elements/select.md
4
+
5
+ Export: `SelectBasic`
6
+
7
+ ## Source
8
+
9
+ ```tsx
10
+ import { Select } from "@gunkin/uiify";
11
+
12
+ export function SelectBasic() {
13
+ return (
14
+ <label>
15
+ Plan
16
+ <Select.Root name="plan" defaultValue="monthly">
17
+ <Select.Content>
18
+ <Select.Item value="monthly">Monthly</Select.Item>
19
+ <Select.Item value="annual">Annual</Select.Item>
20
+ </Select.Content>
21
+ </Select.Root>
22
+ </label>
23
+ );
24
+ }
25
+ ```
26
+
27
+ ## CSS
28
+
29
+ ```css
30
+ [data-uiify-field] { display: grid; gap: 0.5rem; }
31
+ [data-uiify-field] input,
32
+ [data-uiify-select] { min-block-size: 2.75rem; padding-inline: 0.75rem; }
33
+ table { border-collapse: collapse; inline-size: 100%; }
34
+ th, td { padding: 0.5rem; text-align: start; }
35
+ ```
@@ -0,0 +1,40 @@
1
+ # Example: table-controls
2
+
3
+ Element: references/elements/table.md
4
+
5
+ Export: `TableControls`
6
+
7
+ ## Source
8
+
9
+ ```tsx
10
+ "use client";
11
+
12
+ import { useState } from "react";
13
+ import { Table } from "@gunkin/uiify";
14
+
15
+ const people = ["Ada Lovelace", "Grace Hopper", "Katherine Johnson"];
16
+
17
+ export function TableControls() {
18
+ const [query, setQuery] = useState("");
19
+ const visible = people.filter((person) => person.toLowerCase().includes(query.toLowerCase()));
20
+ return (
21
+ <section>
22
+ <label>Filter people <input value={query} onChange={(event) => setQuery(event.currentTarget.value)} /></label>
23
+ <Table.Root>
24
+ <Table.Header><Table.Row id="header"><Table.ColumnHeader id="name-heading">Name</Table.ColumnHeader></Table.Row></Table.Header>
25
+ <Table.Body>{visible.map((person) => <Table.Row id={person} key={person}><Table.Cell>{person}</Table.Cell></Table.Row>)}</Table.Body>
26
+ </Table.Root>
27
+ </section>
28
+ );
29
+ }
30
+ ```
31
+
32
+ ## CSS
33
+
34
+ ```css
35
+ [data-uiify-field] { display: grid; gap: 0.5rem; }
36
+ [data-uiify-field] input,
37
+ [data-uiify-select] { min-block-size: 2.75rem; padding-inline: 0.75rem; }
38
+ table { border-collapse: collapse; inline-size: 100%; }
39
+ th, td { padding: 0.5rem; text-align: start; }
40
+ ```
@@ -0,0 +1,14 @@
1
+ # Choose image and type material
2
+
3
+ Read this guide only when images or typography carry the page's composition. Start with project assets and the requested brand; do not require image generation, network access or new font downloads.
4
+
5
+ | Material | Use when | Composition and responsive check |
6
+ | --- | --- | --- |
7
+ | Photography | A real person, place or physical subject is evidence for the message. | Preserve the subject's focal point in each crop; use a ratio that suits the section and stack or reframe at narrow widths. |
8
+ | Product screenshot | The actual interface explains the product or task. | Keep essential UI legible and intact; do not replace a product screenshot with decorative imagery. At mobile widths, stack it or crop only if the important controls remain visible. |
9
+ | Illustration | An abstract idea benefits from a simple visual explanation. | Keep style and color consistent with the product; avoid inventing interface details, customer logos or testimonials. |
10
+ | Typographic composition | The words themselves are the main visual material. | Preserve supplied brand fonts and established roles. Check line breaks, long strings and fallback metrics at narrow widths; never rewrite factual copy just to fit. |
11
+
12
+ When assets are missing, do not invent reviews, testimonials, customer logos or product screens. Use existing fonts first and preserve supplied brand fonts. A font fallback should remain close enough in width and weight to avoid a layout jump while the chosen font loads. Test long headings and labels; use tabular numerals when aligned values benefit from equal-width digits, not as a universal setting.
13
+
14
+ Motion should explain a state change or guide attention. Keep it brief and honor `prefers-reduced-motion`; decorative movement should not compete with reading or controls.
@@ -0,0 +1,11 @@
1
+ # Does the copy say what this page and each action actually do?
2
+
3
+ Read the page at its default viewport and in its relevant form or overlay states. Check that:
4
+
5
+ - Headings identify the subject or decision on this page, not a generic slogan. A settings page should name the setting group; a pricing page should name the offer.
6
+ - Action labels name the result. “Save changes” tells the user more than “Submit”; verify the label still fits at 390×844.
7
+ - An automatically detected input error identifies what happened and, when known, how to correct it. This follows [WCAG 2.2 Success Criterion 3.3.3](https://www.w3.org/WAI/WCAG22/Understanding/error-suggestion.html); do not invent a correction when the cause is unknown.
8
+ - Example content is plausible for the page and its audience. Replace lorem ipsum and repeated placeholder values, then inspect the populated state for wrapping and density.
9
+ - One term names one concept throughout the page. Compare navigation, headings, controls, confirmation, and errors for competing labels.
10
+
11
+ Use a real task sentence and realistic values to test the copy. If the same label could describe several different outcomes, make the result explicit.
@@ -0,0 +1,14 @@
1
+ # What leads the eye, and do the parts read in the right order?
2
+
3
+ At 1440×900 and 390×844, inspect the initial viewport and one scrolled section. Check these visible relationships:
4
+
5
+ - One item leads each screen. Squint at the screenshot or blur it; the primary heading or task should remain the first clear anchor.
6
+ - Neighboring text roles differ by size or weight, not color alone. Compare computed font size and weight in the heading, body, label, and helper text.
7
+ - A supplied brand font and existing type roles remain intact. Test long headings and labels at narrow widths; use tabular numerals only when aligned values need them.
8
+ - Main text lines average 45–75 characters as a readability heuristic and do not exceed 80 characters as a ceiling. [WCAG 1.4.8](https://www.w3.org/WAI/WCAG22/Understanding/visual-presentation.html) describes 80 characters as one available text-presentation requirement; it does not make 45–75 a rule.
9
+ - Related items sit closer together than unrelated groups. Check the grouping against the `proximity` law in the relevant page design.
10
+ - Space above a heading exceeds the space below it; treat this as a grouping heuristic and compare computed margins.
11
+ - Density matches the page surface in `surface.md`; a work screen can be denser than a reading page without losing clear groups.
12
+ - Heading levels follow the visible and document order without skipped levels. Inspect the heading outline as well as the screenshot.
13
+
14
+ If one relation fails, name the viewport and state, then make one correction and inspect both viewports again.
@@ -0,0 +1,9 @@
1
+ # Compare against a visual reference
2
+
3
+ Use this guide only when matching a supplied screenshot or comparing an existing page. Keep the requested scope: a reference is evidence for the requested work, not permission to redesign unrelated parts.
4
+
5
+ Compare the same route at the same viewport, the same state and the same scale. Note the page's grid and proportions, text roles, spacing rhythm, color roles, section order, image role and crop. Separate observed differences from assumptions; keep the notes concise and actionable.
6
+
7
+ Inspect project assets before choosing replacements. Use local assets such as images and fonts when available, and preserve recognizable content and crop where the reference calls for it. Do not invent an asset, brand detail or unavailable content. A mobile adaptation inferred from a desktop screenshot is not verified until checked at a real narrow viewport.
8
+
9
+ If the reference cannot be opened, continue with the accessible request and existing project. Say that visual fidelity could not be checked; do not claim to have inspected an unavailable image.
@@ -0,0 +1,12 @@
1
+ # Does the page still work outside the happy path?
2
+
3
+ Use the browser to create these test inputs and states, then record which you checked:
4
+
5
+ - Double the content length in headings, button labels, and primary text. Add a long unbroken value such as a URL or identifier and inspect wrapping at 390×844.
6
+ - Set `dir="rtl"` on `html`; inspect alignment, order, icons, and navigation at 1440×900 and 390×844.
7
+ - Check a 320 CSS pixel viewport and browser zoom at 200%. Confirm content and actions remain available without unintended horizontal scrolling. [WCAG 2.2 Success Criterion 1.4.10](https://www.w3.org/WAI/WCAG22/Understanding/reflow.html) requires reflow at a 320 CSS pixel width for vertically scrolling content, except content that needs two-dimensional layout.
8
+ - Use only the keyboard: follow Tab order, activate with Enter or Space, dismiss overlays with Escape where supported, and confirm focus returns to the opener.
9
+ - Exercise loading, empty, error, and disabled states where they exist. Submit an invalid form and check the message, focus location, and preservation of valid values.
10
+ - If motion communicates a state or transition, verify its reduced-motion behavior; do not make essential information depend on animation.
11
+
12
+ Record the viewport, input, and state for each check. A screenshot shows appearance only; it does not prove keyboard operation, focus return, or touch and pointer behavior.
@@ -0,0 +1,27 @@
1
+ # What kind of page is this, and how expressive should it be?
2
+
3
+ Treat this classification as a heuristic about the requested page, not the product. Choose the surface that best describes the page being built; a product may contain several kinds.
4
+
5
+ Follow the request's scope: a reference sets a visual target, a narrow fix stays narrow, and a new page within an existing product follows that project's design decisions. Use surface guidance for the part being created or changed; an audit reports evidence without expanding into a redesign.
6
+
7
+ ## Decide
8
+
9
+ For a landing, pricing, or campaign page, prioritize one clear next step per screen and make the evidence supporting its claim easy to find. Avoid competing primary actions and decorative imagery that does not support the message. Typical catalog choices include `hero`, `hero-actions`, `pricing-table`, `testimonial`, `feature-grid`, and `logo-wall`.
10
+
11
+ ## Work
12
+
13
+ For settings, dashboards, admin tools, and tables, prioritize scanning, predictable controls, and information density. Avoid turning navigation or controls into decoration. Use typography, semantic color roles, density, and one distinctive detail for character. Typical choices include `header`, `sidebar`, `table`, `form`, `field`, `tabs`, `toolbar`, and `pagination`.
14
+
15
+ ## Read
16
+
17
+ For documentation, articles, and changelogs, prioritize a readable line length, clear heading structure, and navigation within the page. Avoid narrow columns that make code or data unusable and headings that skip levels. Typical choices include `table-of-contents`, `code-block`, `accordion`, `breadcrumb`, and `pagination`.
18
+
19
+ ## View
20
+
21
+ For galleries and portfolios, let the work lead and keep surrounding chrome quiet. Avoid controls or labels that compete with the work, while keeping meaningful descriptions and navigation available. Typical choices include `image-gallery`, `carousel`, `card`, `badge`, and `divider`.
22
+
23
+ ## Review with evidence
24
+
25
+ At 320 CSS pixels wide, confirm that content and functionality reflow without two-dimensional scrolling except where the content genuinely requires it. This reflects [WCAG 2.2 Success Criterion 1.4.10](https://www.w3.org/WAI/WCAG22/Understanding/reflow.html); it does not prescribe a visual style.
26
+
27
+ After inspecting a rendered page, change a detail only when you can name the visible problem, the state and viewport where it appears, and one correction. If you cannot point to all three, keep the design as it is.
@@ -44,7 +44,7 @@ async function lint() {
44
44
  }
45
45
  }
46
46
  }
47
- console.log(`${count} findings (${checks.length} accepted static checks).`);
47
+ console.log(`${count} findings (${checks.length} accepted static checks: ${checks.map((check) => check.id).join(", ") || "none"}).`);
48
48
  process.exitCode = count > 0 ? 1 : 0;
49
49
  }
50
50
 
@@ -41,6 +41,62 @@ function positiveLiteral(attribute, typescript) {
41
41
  );
42
42
  }
43
43
 
44
+ const UIIFY_MODULE = /^@gunkin\/uiify\/(?:components|elements)(?:\/[a-z0-9-]+)?$/;
45
+
46
+ function uiifyBindings(tree, typescript) {
47
+ const bindings = new Set();
48
+ for (const statement of tree.statements) {
49
+ if (
50
+ !typescript.isImportDeclaration(statement) ||
51
+ !typescript.isStringLiteral(statement.moduleSpecifier) ||
52
+ !UIIFY_MODULE.test(statement.moduleSpecifier.text)
53
+ ) continue;
54
+ const clause = statement.importClause;
55
+ if (!clause || clause.isTypeOnly) continue;
56
+ if (clause.name) bindings.add(clause.name.text);
57
+ const namedBindings = clause.namedBindings;
58
+ if (namedBindings && typescript.isNamespaceImport(namedBindings)) {
59
+ bindings.add(namedBindings.name.text);
60
+ } else if (namedBindings && typescript.isNamedImports(namedBindings)) {
61
+ for (const specifier of namedBindings.elements) {
62
+ if (!specifier.isTypeOnly) bindings.add(specifier.name.text);
63
+ }
64
+ }
65
+ }
66
+
67
+ const shadowable = new Set([
68
+ typescript.SyntaxKind.VariableDeclaration,
69
+ typescript.SyntaxKind.BindingElement,
70
+ typescript.SyntaxKind.Parameter,
71
+ typescript.SyntaxKind.FunctionDeclaration,
72
+ typescript.SyntaxKind.FunctionExpression,
73
+ typescript.SyntaxKind.ClassDeclaration,
74
+ typescript.SyntaxKind.ClassExpression,
75
+ ]);
76
+ const visit = (node) => {
77
+ if (shadowable.has(node.kind) && node.name && typescript.isIdentifier(node.name)) {
78
+ bindings.delete(node.name.text);
79
+ }
80
+ typescript.forEachChild(node, visit);
81
+ };
82
+ visit(tree);
83
+ return bindings;
84
+ }
85
+
86
+ function jsxTarget(tagName, bindings, typescript) {
87
+ if (typescript.isIdentifier(tagName)) {
88
+ if (/^[a-z][a-z0-9-]*$/.test(tagName.text)) return { kind: "intrinsic", tag: tagName.text };
89
+ if (bindings.has(tagName.text)) return { kind: "uiify", name: tagName.text };
90
+ return null;
91
+ }
92
+ if (!typescript.isPropertyAccessExpression(tagName)) return null;
93
+ let root = tagName.expression;
94
+ while (typescript.isPropertyAccessExpression(root)) root = root.expression;
95
+ return typescript.isIdentifier(root) && bindings.has(root.text)
96
+ ? { kind: "uiify", name: tagName.getText() }
97
+ : null;
98
+ }
99
+
44
100
  function sourceCheck(id, law, message, violates) {
45
101
  return {
46
102
  id,
@@ -49,20 +105,19 @@ function sourceCheck(id, law, message, violates) {
49
105
  run(input) {
50
106
  const tree = parse(input);
51
107
  const { file, typescript } = input;
108
+ const bindings = uiifyBindings(tree, typescript);
52
109
  const findings = [];
53
110
  const visit = (node) => {
54
- if (
55
- (typescript.isJsxOpeningElement(node) || typescript.isJsxSelfClosingElement(node)) &&
56
- typescript.isIdentifier(node.tagName) &&
57
- /^[a-z][a-z0-9-]*$/.test(node.tagName.text) &&
58
- violates(node.tagName.text, attributes(node, typescript), typescript)
59
- ) {
60
- findings.push({
61
- file,
62
- line: tree.getLineAndCharacterOfPosition(node.getStart(tree)).line + 1,
63
- rule: id,
64
- message,
65
- });
111
+ if (typescript.isJsxOpeningElement(node) || typescript.isJsxSelfClosingElement(node)) {
112
+ const target = jsxTarget(node.tagName, bindings, typescript);
113
+ if (target && violates(target, attributes(node, typescript), typescript)) {
114
+ findings.push({
115
+ file,
116
+ line: tree.getLineAndCharacterOfPosition(node.getStart(tree)).line + 1,
117
+ rule: id,
118
+ message,
119
+ });
120
+ }
66
121
  }
67
122
  typescript.forEachChild(node, visit);
68
123
  };
@@ -84,12 +139,14 @@ export const staticChecks = [
84
139
  "button-type",
85
140
  "error-prevention",
86
141
  "Give the intrinsic button an explicit type.",
87
- (tag, attrs) => tag === "button" && !attrs.some((attr) => attr.name.text === "type"),
142
+ (target, attrs) =>
143
+ target.kind === "intrinsic" && target.tag === "button" && !attrs.some((attr) => attr.name.text === "type"),
88
144
  ),
89
145
  sourceCheck(
90
146
  "image-alt",
91
147
  "accessibility",
92
148
  "Give the intrinsic image an alt attribute (empty for decoration).",
93
- (tag, attrs) => tag === "img" && !attrs.some((attr) => attr.name.text === "alt"),
149
+ (target, attrs) =>
150
+ target.kind === "intrinsic" && target.tag === "img" && !attrs.some((attr) => attr.name.text === "alt"),
94
151
  ),
95
152
  ];