wimui 0.23.7 → 0.23.8

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.
@@ -1,2 +1,2 @@
1
1
  "use client";
2
- const e=require("../../../_virtual/_rolldown/runtime.cjs"),t=require("./affix.module.cjs");let n=require("react");n=e.__toESM(n,1);let r=require("classnames");r=e.__toESM(r,1);let i=require("react/jsx-runtime");var a=({offsetTop:e,offsetBottom:a,onChange:o,target:s=()=>window,children:c,className:l,style:u})=>{let[d,f]=(0,n.useState)({isAffixed:!1}),p=(0,n.useRef)(null),m=(0,n.useRef)(null),h=(0,n.useRef)(null),g=(0,n.useRef)(!1),_=(0,n.useCallback)(()=>{if(!p.current)return;let t=s();if(!t)return;let n=p.current.getBoundingClientRect(),r=t===window||t instanceof Window?{top:0,bottom:window.innerHeight}:t.getBoundingClientRect(),i=!1,c,l;e!==void 0&&n.top<r.top+e?(i=!0,c={position:`fixed`,top:r.top+e,width:n.width,height:n.height,zIndex:1e3},l={width:n.width,height:n.height}):a!==void 0&&n.bottom>r.bottom-a&&(i=!0,c={position:`fixed`,bottom:window.innerHeight-r.bottom+a,width:n.width,height:n.height,zIndex:1e3},l={width:n.width,height:n.height}),i!==g.current&&(g.current=i,o?.(i)),f(e=>e.isAffixed===i&&JSON.stringify(e.affixStyle)===JSON.stringify(c)&&JSON.stringify(e.placeholderStyle)===JSON.stringify(l)?e:{isAffixed:i,affixStyle:c,placeholderStyle:l})},[e,a,s,o]);return(0,n.useEffect)(()=>{let e=s();if(!e)return;let t=()=>{h.current&&window.cancelAnimationFrame(h.current),h.current=window.requestAnimationFrame(_)};return e.addEventListener(`scroll`,t),window.addEventListener(`resize`,t),_(),()=>{e.removeEventListener(`scroll`,t),window.removeEventListener(`resize`,t),h.current&&window.cancelAnimationFrame(h.current)}},[s,_]),(0,i.jsx)(`div`,{ref:p,style:{...d.placeholderStyle,...u},className:(0,r.default)(l),children:(0,i.jsx)(`div`,{ref:m,className:(0,r.default)(`wim-affix`,t.default.root,d.isAffixed&&t.default.affixed),style:d.affixStyle,children:c})})};exports.Affix=a,exports.default=a;
2
+ const e=require("../../../_virtual/_rolldown/runtime.cjs"),t=require("../../_internal/mergeRefs.cjs"),n=require("./affix.module.cjs");let r=require("react");r=e.__toESM(r,1);let i=require("classnames");i=e.__toESM(i,1);let a=require("react/jsx-runtime");var o=r.default.forwardRef(function({offsetTop:e,offsetBottom:o,onChange:s,target:c=()=>window,children:l,className:u,style:d},f){let[p,m]=(0,r.useState)({isAffixed:!1}),h=(0,r.useRef)(null),g=(0,r.useRef)(null),_=(0,r.useRef)(null),v=(0,r.useRef)(!1),y=(0,r.useCallback)(()=>{if(!h.current)return;let t=c();if(!t)return;let n=h.current.getBoundingClientRect(),r=t===window||t instanceof Window?{top:0,bottom:window.innerHeight}:t.getBoundingClientRect(),i=!1,a,l;e!==void 0&&n.top<r.top+e?(i=!0,a={position:`fixed`,top:r.top+e,width:n.width,height:n.height,zIndex:1e3},l={width:n.width,height:n.height}):o!==void 0&&n.bottom>r.bottom-o&&(i=!0,a={position:`fixed`,bottom:window.innerHeight-r.bottom+o,width:n.width,height:n.height,zIndex:1e3},l={width:n.width,height:n.height}),i!==v.current&&(v.current=i,s?.(i)),m(e=>e.isAffixed===i&&JSON.stringify(e.affixStyle)===JSON.stringify(a)&&JSON.stringify(e.placeholderStyle)===JSON.stringify(l)?e:{isAffixed:i,affixStyle:a,placeholderStyle:l})},[e,o,c,s]);return(0,r.useEffect)(()=>{let e=c();if(!e)return;let t=()=>{_.current&&window.cancelAnimationFrame(_.current),_.current=window.requestAnimationFrame(y)};return e.addEventListener(`scroll`,t),window.addEventListener(`resize`,t),y(),()=>{e.removeEventListener(`scroll`,t),window.removeEventListener(`resize`,t),_.current&&window.cancelAnimationFrame(_.current)}},[c,y]),(0,a.jsx)(`div`,{ref:t.mergeRefs(h,f),style:{...p.placeholderStyle,...d},className:(0,i.default)(u),children:(0,a.jsx)(`div`,{ref:g,className:(0,i.default)(`wim-affix`,n.default.root,p.isAffixed&&n.default.affixed),style:p.affixStyle,children:l})})});o.displayName=`Affix`,exports.Affix=o,exports.default=o;
@@ -15,5 +15,12 @@ export interface AffixProps {
15
15
  /** Style attribute */
16
16
  style?: React.CSSProperties;
17
17
  }
18
- export declare const Affix: ({ offsetTop, offsetBottom, onChange, target, children, className, style, }: AffixProps) => React.JSX.Element;
18
+ /**
19
+ * Pins children while scrolling.
20
+ *
21
+ * `ref` points at the in-flow placeholder (the outer element), not the
22
+ * `position: fixed` inner wrapper. After the inner content sticks, measure
23
+ * this node for document position — the children's rect is the viewport edge.
24
+ */
25
+ export declare const Affix: React.ForwardRefExoticComponent<AffixProps & React.RefAttributes<HTMLDivElement>>;
19
26
  export default Affix;
@@ -1,70 +1,72 @@
1
1
  "use client";
2
- import e from "./affix.module.js";
3
- import { useCallback as t, useEffect as n, useRef as r, useState as i } from "react";
4
- import a from "classnames";
5
- import { jsx as o } from "react/jsx-runtime";
2
+ import { mergeRefs as e } from "../../_internal/mergeRefs.js";
3
+ import t from "./affix.module.js";
4
+ import n, { useCallback as r, useEffect as i, useRef as a, useState as o } from "react";
5
+ import s from "classnames";
6
+ import { jsx as c } from "react/jsx-runtime";
6
7
  //#region src/components/navigation/Affix/Affix.tsx
7
- var s = ({ offsetTop: s, offsetBottom: c, onChange: l, target: u = () => window, children: d, className: f, style: p }) => {
8
- let [m, h] = i({ isAffixed: !1 }), g = r(null), _ = r(null), v = r(null), y = r(!1), b = t(() => {
9
- if (!g.current) return;
10
- let e = u();
8
+ var l = n.forwardRef(function({ offsetTop: n, offsetBottom: l, onChange: u, target: d = () => window, children: f, className: p, style: m }, h) {
9
+ let [g, _] = o({ isAffixed: !1 }), v = a(null), y = a(null), b = a(null), x = a(!1), S = r(() => {
10
+ if (!v.current) return;
11
+ let e = d();
11
12
  if (!e) return;
12
- let t = g.current.getBoundingClientRect(), n = e === window || e instanceof Window ? {
13
+ let t = v.current.getBoundingClientRect(), r = e === window || e instanceof Window ? {
13
14
  top: 0,
14
15
  bottom: window.innerHeight
15
- } : e.getBoundingClientRect(), r = !1, i, a;
16
- s !== void 0 && t.top < n.top + s ? (r = !0, i = {
16
+ } : e.getBoundingClientRect(), i = !1, a, o;
17
+ n !== void 0 && t.top < r.top + n ? (i = !0, a = {
17
18
  position: "fixed",
18
- top: n.top + s,
19
+ top: r.top + n,
19
20
  width: t.width,
20
21
  height: t.height,
21
22
  zIndex: 1e3
22
- }, a = {
23
+ }, o = {
23
24
  width: t.width,
24
25
  height: t.height
25
- }) : c !== void 0 && t.bottom > n.bottom - c && (r = !0, i = {
26
+ }) : l !== void 0 && t.bottom > r.bottom - l && (i = !0, a = {
26
27
  position: "fixed",
27
- bottom: window.innerHeight - n.bottom + c,
28
+ bottom: window.innerHeight - r.bottom + l,
28
29
  width: t.width,
29
30
  height: t.height,
30
31
  zIndex: 1e3
31
- }, a = {
32
+ }, o = {
32
33
  width: t.width,
33
34
  height: t.height
34
- }), r !== y.current && (y.current = r, l?.(r)), h((e) => e.isAffixed === r && JSON.stringify(e.affixStyle) === JSON.stringify(i) && JSON.stringify(e.placeholderStyle) === JSON.stringify(a) ? e : {
35
- isAffixed: r,
36
- affixStyle: i,
37
- placeholderStyle: a
35
+ }), i !== x.current && (x.current = i, u?.(i)), _((e) => e.isAffixed === i && JSON.stringify(e.affixStyle) === JSON.stringify(a) && JSON.stringify(e.placeholderStyle) === JSON.stringify(o) ? e : {
36
+ isAffixed: i,
37
+ affixStyle: a,
38
+ placeholderStyle: o
38
39
  });
39
40
  }, [
40
- s,
41
- c,
42
- u,
43
- l
41
+ n,
42
+ l,
43
+ d,
44
+ u
44
45
  ]);
45
- return n(() => {
46
- let e = u();
46
+ return i(() => {
47
+ let e = d();
47
48
  if (!e) return;
48
49
  let t = () => {
49
- v.current && window.cancelAnimationFrame(v.current), v.current = window.requestAnimationFrame(b);
50
+ b.current && window.cancelAnimationFrame(b.current), b.current = window.requestAnimationFrame(S);
50
51
  };
51
- return e.addEventListener("scroll", t), window.addEventListener("resize", t), b(), () => {
52
- e.removeEventListener("scroll", t), window.removeEventListener("resize", t), v.current && window.cancelAnimationFrame(v.current);
52
+ return e.addEventListener("scroll", t), window.addEventListener("resize", t), S(), () => {
53
+ e.removeEventListener("scroll", t), window.removeEventListener("resize", t), b.current && window.cancelAnimationFrame(b.current);
53
54
  };
54
- }, [u, b]), /* @__PURE__ */ o("div", {
55
- ref: g,
55
+ }, [d, S]), /* @__PURE__ */ c("div", {
56
+ ref: e(v, h),
56
57
  style: {
57
- ...m.placeholderStyle,
58
- ...p
58
+ ...g.placeholderStyle,
59
+ ...m
59
60
  },
60
- className: a(f),
61
- children: /* @__PURE__ */ o("div", {
62
- ref: _,
63
- className: a("wim-affix", e.root, m.isAffixed && e.affixed),
64
- style: m.affixStyle,
65
- children: d
61
+ className: s(p),
62
+ children: /* @__PURE__ */ c("div", {
63
+ ref: y,
64
+ className: s("wim-affix", t.root, g.isAffixed && t.affixed),
65
+ style: g.affixStyle,
66
+ children: f
66
67
  })
67
68
  });
68
- };
69
+ });
70
+ l.displayName = "Affix";
69
71
  //#endregion
70
- export { s as Affix, s as default };
72
+ export { l as Affix, l as default };
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## What this is
6
6
 
7
- **wimui** v0.23.7 — a React 19 component library: 218 documented components across 10 categories, with design tokens, dark mode, i18n (en/ja/pt-BR) and WAI-ARIA compliant a11y. Peer deps: react ^19, react-dom ^19 (plus optional peers for specific components — see package.json).
7
+ **wimui** v0.23.8 — a React 19 component library: 218 documented components across 10 categories, with design tokens, dark mode, i18n (en/ja/pt-BR) and WAI-ARIA compliant a11y. Peer deps: react ^19, react-dom ^19 (plus optional peers for specific components — see package.json).
8
8
 
9
9
  ## Install & required setup
10
10
 
@@ -75,13 +75,15 @@ Single components are judged by state/a11y/token compliance. **Composed screens*
75
75
  5. Keep one radius stance per screen (`radius.component` / `radius.container` / `radius.overlay`); don't mix 0 and lg arbitrarily. Narrow table cards must share the same radius token as the adjacent toolbar/pagination chrome — not jump to `radius.container` while chrome stays on `radius.component`.
76
76
  6. Limit accent color: primary surfaces (solid buttons, emphasis backgrounds) 1–2 per view.
77
77
  7. Give demo content real substance (product-context copy, internally consistent numbers/dates/names — active ≤ total, dates not evenly spaced).
78
- 8. Keep one elevation stance per screen: don't mix surfaces raised by shadow with surfaces cut by borders. `Card` and `Stats` default to `elevated`, so pass `variant="outline"` when the screen is border-led. If the outer container already casts a shadow, its contents need only a border. On admin/list screens, `Table`/`DataGrid` mobile cards must stay border-only like the toolbar and pagination row cards with `shadow-sm` while chrome is flat outline is the same mixed stance.
79
- 9. Never wrap a component that already draws its own frame. `<Card><Table card /></Card>` doubles the border and nests radii (12px inside 4px). `Table` takes a `card` prop and `Stats` is already a Card place them directly.
80
- 10. Always pass `intent` on `Badge` / `Tag` / `Chip`. Their default is `primary` (the accent), so an ordinary value like `pending` or `member` gets painted as loudly as an exception and the colour stops carrying information. Use `intent="neutral"` for ordinary values.
81
- 11. Pick colour by good/bad, not by direction. `Stats.Trend` ties the arrow and the colour together, so `up` is always the success colour using it for a metric where up is bad (failures, error rate) renders the bad news as good. Use a colourless form such as `Stats.Description` instead. Don't approximate a meaning the API cannot express.
82
- 12. Keep the chrome quiet. Breadcrumb, page title, summary line, toolbar and filter bar frame the content; they must not compete with the protagonist. Don't promote a one-line summary to an `Alert`/`Banner`, don't wrap the filter bar in a `Card` (its border doubles up with the data region's), keep primary surfaces out of the chrome, and don't stack a KPI tile row above a table screen. Corollary of protagonist and accent limits, but it is the rule most often broken on admin/list screens the test is whether the eye lands on the data first.
83
- 13. Add intentional "wobble": mix in 1–2 incomplete rows (a truncated long name, a missing optional field, an extreme value, an error/unread state) and show non-happy-path states (hover/focus/disabled/error/empty/loading).
84
- 14. Check the screen at 390px and 768px before calling it done. The page itself must never scroll sideways (`document.scrollWidth > document.documentElement.clientWidth` is a bug — the usual cause is a group of controls that cannot wrap, e.g. several buttons packed into one `Toolbar.Group`; split them into separate groups). Data tables need their narrow mode turned on (`<DataGrid mobileCard>` / `<Table mobileCard>`) rather than an inner scrollbar: without it columns collapse and a short string like a `Code` lot number breaks one character per line. The default card switch is `container-down(sm)` (576px). Wide grids that still overflow at tablet should pass `mobileCard="md"` so cards continue through 768px.
78
+ 8. Build realism inside a world the reader already knows. Chasing substance pushes toward niche expertise, and a screen set in one (a shipyard's watch orders, a trading desk, a clinical protocol) makes the reader decode the domain before they can see the components the pattern stops teaching. Pick an everyday situation (renting a car and returning it, a delivery, a hospital appointment) and keep specialist vocabulary out of the places that carry state and actions. The test: can a reader say what the screen is for without a paragraph of explanation?
79
+ 9. Never show the result of an action the screen cannot perform. Coverage-driven composition puts the component first and invents a state that justifies it, so the reader sees outcomes ("not in the record", "1 photo") with no control that could have produced them. Reachable state is (1) what the controls on this screen can do, and (2) values the subject already holds. Use the component inside a reachable moment; don't invent an unreachable one to justify the component. The test: can you arrive at what is on screen using only the controls that are on screen?
80
+ 10. Keep one elevation stance per screen: don't mix surfaces raised by shadow with surfaces cut by borders. `Card` and `Stats` default to `elevated`, so pass `variant="outline"` when the screen is border-led. If the outer container already casts a shadow, its contents need only a border. On admin/list screens, `Table`/`DataGrid` mobile cards must stay border-only like the toolbar and pagination row cards with `shadow-sm` while chrome is flat outline is the same mixed stance.
81
+ 11. Never wrap a component that already draws its own frame. `<Card><Table card /></Card>` doubles the border and nests radii (12px inside 4px). `Table` takes a `card` prop and `Stats` is already a Card place them directly.
82
+ 12. Always pass `intent` on `Badge` / `Tag` / `Chip`. Their default is `primary` (the accent), so an ordinary value like `pending` or `member` gets painted as loudly as an exception and the colour stops carrying information. Use `intent="neutral"` for ordinary values.
83
+ 13. Pick colour by good/bad, not by direction. `Stats.Trend` ties the arrow and the colour together, so `up` is always the success colour — using it for a metric where up is bad (failures, error rate) renders the bad news as good. Use a colourless form such as `Stats.Description` instead. Don't approximate a meaning the API cannot express.
84
+ 14. Keep the chrome quiet. Breadcrumb, page title, summary line, toolbar and filter bar frame the content; they must not compete with the protagonist. Don't promote a one-line summary to an `Alert`/`Banner`, don't wrap the filter bar in a `Card` (its border doubles up with the data region's), keep primary surfaces out of the chrome, and don't stack a KPI tile row above a table screen. Corollary of protagonist and accent limits, but it is the rule most often broken on admin/list screens the test is whether the eye lands on the data first.
85
+ 15. Add intentional "wobble": mix in 1–2 incomplete rows (a truncated long name, a missing optional field, an extreme value, an error/unread state) and show non-happy-path states (hover/focus/disabled/error/empty/loading).
86
+ 16. Check the screen at 390px and 768px before calling it done. The page itself must never scroll sideways (`document.scrollWidth > document.documentElement.clientWidth` is a bug — the usual cause is a group of controls that cannot wrap, e.g. several buttons packed into one `Toolbar.Group`; split them into separate groups). Data tables need their narrow mode turned on (`<DataGrid mobileCard>` / `<Table mobileCard>`) rather than an inner scrollbar: without it columns collapse and a short string like a `Code` lot number breaks one character per line. The default card switch is `container-down(sm)` (576px). Wide grids that still overflow at tablet should pass `mobileCard="md"` so cards continue through 768px.
85
87
 
86
88
  ## Recipes — copy-paste starting points
87
89
 
package/dist/llms.txt CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## What this is
6
6
 
7
- **wimui** v0.23.7 — a React 19 component library: 218 documented components across 10 categories, with design tokens, dark mode, i18n (en/ja/pt-BR) and WAI-ARIA compliant a11y. Peer deps: react ^19, react-dom ^19 (plus optional peers for specific components — see package.json).
7
+ **wimui** v0.23.8 — a React 19 component library: 218 documented components across 10 categories, with design tokens, dark mode, i18n (en/ja/pt-BR) and WAI-ARIA compliant a11y. Peer deps: react ^19, react-dom ^19 (plus optional peers for specific components — see package.json).
8
8
 
9
9
  ## Install & required setup
10
10
 
@@ -75,13 +75,15 @@ Single components are judged by state/a11y/token compliance. **Composed screens*
75
75
  5. Keep one radius stance per screen (`radius.component` / `radius.container` / `radius.overlay`); don't mix 0 and lg arbitrarily. Narrow table cards must share the same radius token as the adjacent toolbar/pagination chrome — not jump to `radius.container` while chrome stays on `radius.component`.
76
76
  6. Limit accent color: primary surfaces (solid buttons, emphasis backgrounds) 1–2 per view.
77
77
  7. Give demo content real substance (product-context copy, internally consistent numbers/dates/names — active ≤ total, dates not evenly spaced).
78
- 8. Keep one elevation stance per screen: don't mix surfaces raised by shadow with surfaces cut by borders. `Card` and `Stats` default to `elevated`, so pass `variant="outline"` when the screen is border-led. If the outer container already casts a shadow, its contents need only a border. On admin/list screens, `Table`/`DataGrid` mobile cards must stay border-only like the toolbar and pagination row cards with `shadow-sm` while chrome is flat outline is the same mixed stance.
79
- 9. Never wrap a component that already draws its own frame. `<Card><Table card /></Card>` doubles the border and nests radii (12px inside 4px). `Table` takes a `card` prop and `Stats` is already a Card place them directly.
80
- 10. Always pass `intent` on `Badge` / `Tag` / `Chip`. Their default is `primary` (the accent), so an ordinary value like `pending` or `member` gets painted as loudly as an exception and the colour stops carrying information. Use `intent="neutral"` for ordinary values.
81
- 11. Pick colour by good/bad, not by direction. `Stats.Trend` ties the arrow and the colour together, so `up` is always the success colour using it for a metric where up is bad (failures, error rate) renders the bad news as good. Use a colourless form such as `Stats.Description` instead. Don't approximate a meaning the API cannot express.
82
- 12. Keep the chrome quiet. Breadcrumb, page title, summary line, toolbar and filter bar frame the content; they must not compete with the protagonist. Don't promote a one-line summary to an `Alert`/`Banner`, don't wrap the filter bar in a `Card` (its border doubles up with the data region's), keep primary surfaces out of the chrome, and don't stack a KPI tile row above a table screen. Corollary of protagonist and accent limits, but it is the rule most often broken on admin/list screens the test is whether the eye lands on the data first.
83
- 13. Add intentional "wobble": mix in 1–2 incomplete rows (a truncated long name, a missing optional field, an extreme value, an error/unread state) and show non-happy-path states (hover/focus/disabled/error/empty/loading).
84
- 14. Check the screen at 390px and 768px before calling it done. The page itself must never scroll sideways (`document.scrollWidth > document.documentElement.clientWidth` is a bug — the usual cause is a group of controls that cannot wrap, e.g. several buttons packed into one `Toolbar.Group`; split them into separate groups). Data tables need their narrow mode turned on (`<DataGrid mobileCard>` / `<Table mobileCard>`) rather than an inner scrollbar: without it columns collapse and a short string like a `Code` lot number breaks one character per line. The default card switch is `container-down(sm)` (576px). Wide grids that still overflow at tablet should pass `mobileCard="md"` so cards continue through 768px.
78
+ 8. Build realism inside a world the reader already knows. Chasing substance pushes toward niche expertise, and a screen set in one (a shipyard's watch orders, a trading desk, a clinical protocol) makes the reader decode the domain before they can see the components the pattern stops teaching. Pick an everyday situation (renting a car and returning it, a delivery, a hospital appointment) and keep specialist vocabulary out of the places that carry state and actions. The test: can a reader say what the screen is for without a paragraph of explanation?
79
+ 9. Never show the result of an action the screen cannot perform. Coverage-driven composition puts the component first and invents a state that justifies it, so the reader sees outcomes ("not in the record", "1 photo") with no control that could have produced them. Reachable state is (1) what the controls on this screen can do, and (2) values the subject already holds. Use the component inside a reachable moment; don't invent an unreachable one to justify the component. The test: can you arrive at what is on screen using only the controls that are on screen?
80
+ 10. Keep one elevation stance per screen: don't mix surfaces raised by shadow with surfaces cut by borders. `Card` and `Stats` default to `elevated`, so pass `variant="outline"` when the screen is border-led. If the outer container already casts a shadow, its contents need only a border. On admin/list screens, `Table`/`DataGrid` mobile cards must stay border-only like the toolbar and pagination row cards with `shadow-sm` while chrome is flat outline is the same mixed stance.
81
+ 11. Never wrap a component that already draws its own frame. `<Card><Table card /></Card>` doubles the border and nests radii (12px inside 4px). `Table` takes a `card` prop and `Stats` is already a Card place them directly.
82
+ 12. Always pass `intent` on `Badge` / `Tag` / `Chip`. Their default is `primary` (the accent), so an ordinary value like `pending` or `member` gets painted as loudly as an exception and the colour stops carrying information. Use `intent="neutral"` for ordinary values.
83
+ 13. Pick colour by good/bad, not by direction. `Stats.Trend` ties the arrow and the colour together, so `up` is always the success colour — using it for a metric where up is bad (failures, error rate) renders the bad news as good. Use a colourless form such as `Stats.Description` instead. Don't approximate a meaning the API cannot express.
84
+ 14. Keep the chrome quiet. Breadcrumb, page title, summary line, toolbar and filter bar frame the content; they must not compete with the protagonist. Don't promote a one-line summary to an `Alert`/`Banner`, don't wrap the filter bar in a `Card` (its border doubles up with the data region's), keep primary surfaces out of the chrome, and don't stack a KPI tile row above a table screen. Corollary of protagonist and accent limits, but it is the rule most often broken on admin/list screens the test is whether the eye lands on the data first.
85
+ 15. Add intentional "wobble": mix in 1–2 incomplete rows (a truncated long name, a missing optional field, an extreme value, an error/unread state) and show non-happy-path states (hover/focus/disabled/error/empty/loading).
86
+ 16. Check the screen at 390px and 768px before calling it done. The page itself must never scroll sideways (`document.scrollWidth > document.documentElement.clientWidth` is a bug — the usual cause is a group of controls that cannot wrap, e.g. several buttons packed into one `Toolbar.Group`; split them into separate groups). Data tables need their narrow mode turned on (`<DataGrid mobileCard>` / `<Table mobileCard>`) rather than an inner scrollbar: without it columns collapse and a short string like a `Code` lot number breaks one character per line. The default card switch is `container-down(sm)` (576px). Wide grids that still overflow at tablet should pass `mobileCard="md"` so cards continue through 768px.
85
87
 
86
88
  ## Recipes — copy-paste starting points
87
89