@godxjp/ui 27.1.0 → 27.4.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 (74) hide show
  1. package/dist/components/data-display/badge.d.ts +10 -3
  2. package/dist/components/data-display/badge.js +14 -3
  3. package/dist/components/data-display/data-table.d.ts +20 -12
  4. package/dist/components/data-display/data-table.js +53 -5
  5. package/dist/components/data-display/range-timeline.d.ts +16 -0
  6. package/dist/components/data-display/range-timeline.js +115 -80
  7. package/dist/components/data-display/table.d.ts +16 -0
  8. package/dist/components/data-display/table.js +2 -0
  9. package/dist/components/data-entry/checkbox.js +23 -5
  10. package/dist/components/data-entry/command.d.ts +3 -1
  11. package/dist/components/data-entry/command.js +10 -2
  12. package/dist/components/data-entry/form-field.js +9 -8
  13. package/dist/components/data-entry/index.d.ts +1 -0
  14. package/dist/components/data-entry/search-select.js +145 -136
  15. package/dist/components/data-entry/select.js +38 -3
  16. package/dist/components/general/icon.d.ts +31 -0
  17. package/dist/components/general/icon.js +23 -0
  18. package/dist/components/general/index.d.ts +2 -0
  19. package/dist/components/general/index.js +2 -0
  20. package/dist/components/layout/flex.d.ts +2 -2
  21. package/dist/components/layout/flex.js +7 -1
  22. package/dist/components/layout/org-switcher.js +1 -1
  23. package/dist/components/layout/responsive-grid.d.ts +8 -2
  24. package/dist/components/layout/responsive-grid.js +2 -0
  25. package/dist/components/layout/topbar-item.d.ts +1 -0
  26. package/dist/components/layout/topbar-item.js +3 -1
  27. package/dist/contracts/measurement.json +1 -1
  28. package/dist/form/form-field-control.d.ts +1 -1
  29. package/dist/form/form-field-control.js +32 -23
  30. package/dist/i18n/messages/en.json +2 -1
  31. package/dist/i18n/messages/ja.json +2 -1
  32. package/dist/i18n/messages/vi.json +2 -1
  33. package/dist/props/components/data-display.prop.d.ts +12 -0
  34. package/dist/props/components/data-entry.prop.d.ts +10 -0
  35. package/dist/props/components/form.prop.d.ts +34 -2
  36. package/dist/props/components/general.prop.d.ts +42 -1
  37. package/dist/props/components/index.d.ts +1 -1
  38. package/dist/props/components/layout.prop.d.ts +49 -0
  39. package/dist/props/registry.d.ts +57 -3
  40. package/dist/props/registry.js +62 -2
  41. package/dist/props/vocabulary/data.prop.d.ts +27 -1
  42. package/dist/props/vocabulary/index.d.ts +2 -2
  43. package/dist/props/vocabulary/interaction.prop.d.ts +11 -0
  44. package/dist/styles/control.css +44 -1
  45. package/dist/styles/core.css +2 -0
  46. package/dist/styles/data-display-layout.css +58 -1
  47. package/dist/styles/form-layout.css +21 -0
  48. package/dist/styles/icon-layout.css +37 -0
  49. package/dist/styles/index.css +2 -0
  50. package/dist/styles/layout.css +7 -0
  51. package/dist/styles/navigation-layout.css +5 -1
  52. package/dist/styles/shell-layout.css +11 -14
  53. package/dist/styles/table-layout.css +80 -8
  54. package/dist/styles/text-layout.css +16 -8
  55. package/dist/tokens/components/control.css +5 -0
  56. package/dist/tokens/components/data-display.css +6 -0
  57. package/dist/tokens/components/navigation.css +1 -1
  58. package/dist/tokens/components/table.css +2 -0
  59. package/docs/CONSUMER-RULES.md +2 -2
  60. package/docs/DESIGN-AUTHORITY.md +32 -1
  61. package/docs/FRAME-COVERAGE-REPORT.md +3 -2
  62. package/docs/data-display/data-table/examples/server-paged.tsx +156 -0
  63. package/docs/data-display/data-table/examples/striped-list.tsx +173 -0
  64. package/docs/data-display/popover.tsx +43 -1
  65. package/docs/data-display/timeline.tsx +73 -9
  66. package/docs/data-entry/command.tsx +80 -1
  67. package/docs/data-entry/form-root.tsx +12 -2
  68. package/docs/general/icon.tsx +215 -0
  69. package/docs/layout/flex.tsx +59 -0
  70. package/docs/layout/responsive-grid.tsx +100 -0
  71. package/docs/layout/topbar-item.tsx +29 -1
  72. package/package.json +3 -2
  73. package/scripts/consumer-rule.md +18 -17
  74. package/scripts/ui-audit.mjs +128 -0
@@ -9,7 +9,7 @@ import {
9
9
  CardHeader,
10
10
  CardTitle,
11
11
  } from "@godxjp/ui/data-display";
12
- import { Button, Text } from "@godxjp/ui/general";
12
+ import { Button, Icon, Text } from "@godxjp/ui/general";
13
13
  import { AppShell, Flex, PageContainer, Sidebar, Topbar, TopbarItem } from "@godxjp/ui/layout";
14
14
  import type { SidebarSectionProp } from "@godxjp/ui/layout";
15
15
  import {
@@ -22,6 +22,7 @@ import {
22
22
  } from "@godxjp/ui/navigation";
23
23
  import {
24
24
  Bell,
25
+ Building2,
25
26
  FileText,
26
27
  LayoutDashboard,
27
28
  LogOut,
@@ -150,6 +151,33 @@ export default function Demo() {
150
151
  />
151
152
  </CardContent>
152
153
  </Card>
154
+
155
+ <Card>
156
+ <CardHeader>
157
+ <CardTitle level={2}>包んだグリフ(icon スロットと Icon)</CardTitle>
158
+ <CardDescription>
159
+ `.ui-topbar-item &gt; svg` は直接の子だけを見ます。ブレークポイントで隠すために
160
+ グリフを包むと、その規則は届かず lucide 既定の 24px で描かれます(390px
161
+ のセルで実測)。 セル自身が持つ `icon` スロットに渡すか、包むなら `Icon`
162
+ を使えば、どちらもバーの段 (--topbar-icon-size)に載ります。
163
+ </CardDescription>
164
+ </CardHeader>
165
+ <CardContent>
166
+ <Topbar
167
+ start={
168
+ <>
169
+ <TopbarItem icon={<Building2 aria-hidden="true" />}>経理部</TopbarItem>
170
+ <TopbarItem aria-label="拠点を切り替える">
171
+ <Flex hideBelow="sm">
172
+ <Icon as={Building2} size="md" />
173
+ </Flex>
174
+ 東京本社
175
+ </TopbarItem>
176
+ </>
177
+ }
178
+ />
179
+ </CardContent>
180
+ </Card>
153
181
  </Flex>
154
182
  </PageContainer>
155
183
  </AppShell>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "27.1.0",
4
- "godxUiMcp": "27.1.0",
3
+ "version": "27.4.1",
4
+ "godxUiMcp": "27.4.1",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -317,6 +317,7 @@
317
317
  "test:visual:topbar-collision": "node scripts/topbar-collision-visual.mjs",
318
318
  "test:visual:table-collection-cjk": "node scripts/table-collection-cjk-visual.mjs",
319
319
  "test:visual:chart-cjk-axis": "node scripts/chart-cjk-axis-visual.mjs",
320
+ "test:visual:kanban-select": "node scripts/kanban-select-visual.mjs",
320
321
  "test:watch": "vitest",
321
322
  "test:coverage": "vitest run --coverage",
322
323
  "check:example-imports": "node scripts/check-example-imports.mjs",
@@ -98,23 +98,24 @@ Slack thì đặt lại **một dòng**, không fork `.app-nav-rail`.
98
98
  Hỏi MCP `godxjp-ui` (`search_components`, `get_component`). Đo được trong một
99
99
  ngày: năm thứ cần đều ĐÃ CÓ và vẫn bị dựng lại bằng thứ khác —
100
100
 
101
- | Cần | Đã có |
102
- | ----------------------------------------------- | ---------------------------------------------------------------------------------- |
103
- | đường kẻ chạm mép Card | `<CardContent flush>` |
104
- | header có kẻ khi thân là danh sách flush | `<CardHeader banded>` |
105
- | một hàng LÀ liên kết (thay cho nút rời) | `<ListRow asChild>` |
106
- | kẻ ô từng ngày trong lịch | `<Calendar bordered>` |
107
- | dải giữa hai vùng, tự kẻ theo VỊ TRÍ | `<CardBar>` (`border` để ép khi xếp chồng) |
108
- | chip "điều kiện đang bật" có dấu × để bỏ | `<Badge onRemove>` — KHÔNG phải `TagInput` |
109
- | dải tab nằm TRONG đầu Card | `<Card tabList activeTabKey onTabChange>` |
110
- | tiêu đề / đoạn văn / liên kết trong văn bản | `<Title>` · `<Paragraph>` · `<Link>` (`Text` 33 prop) |
111
- | nút nổi góc màn (quay lên đầu, hành động nhanh) | `<FloatButton>` |
112
- | khung xương của một FORM khi đang tải | `<SkeletonForm>` |
113
- | ô màu chỉ để xem, màu do NGƯỜI DÙNG chọn | `<Swatch>` |
114
- | ảnh đại diện của một tệp / bản xem trước | `<Thumbnail>` |
115
- | danh sách "có gì trong gói này" | `<FeatureList>` |
116
- | panel kéo giãn được (chia đôi màn) | `<DraggablePanel>` |
117
- | màn hội thoại AI | `<Welcome>` · `<Conversations>` · `<Attachments>` · `<ThoughtChain>` · `<Actions>` |
101
+ | Cần | Đã có |
102
+ | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
103
+ | đường kẻ chạm mép Card | `<CardContent flush>` |
104
+ | header có kẻ khi thân là danh sách flush | `<CardHeader banded>` |
105
+ | một hàng LÀ liên kết (thay cho nút rời) | `<ListRow asChild>` |
106
+ | kẻ ô từng ngày trong lịch | `<Calendar bordered>` |
107
+ | dải giữa hai vùng, tự kẻ theo VỊ TRÍ | `<CardBar>` (`border` để ép khi xếp chồng) |
108
+ | chip "điều kiện đang bật" có dấu × để bỏ | `<Badge onRemove>` — KHÔNG phải `TagInput` |
109
+ | dải tab nằm TRONG đầu Card | `<Card tabList activeTabKey onTabChange>` |
110
+ | tiêu đề / đoạn văn / liên kết trong văn bản | `<Title>` · `<Paragraph>` · `<Link>` (`Text` 33 prop) |
111
+ | nút nổi góc màn (quay lên đầu, hành động nhanh) | `<FloatButton>` |
112
+ | khung xương của một FORM khi đang tải | `<SkeletonForm>` |
113
+ | ô màu chỉ để xem, màu do NGƯỜI DÙNG chọn | `<Swatch>` |
114
+ | ảnh đại diện của một tệp / bản xem trước | `<Thumbnail>` |
115
+ | danh sách "có gì trong gói này" | `<FeatureList>` |
116
+ | panel kéo giãn được (chia đôi màn) | `<DraggablePanel>` |
117
+ | màn hội thoại AI | `<Welcome>` · `<Conversations>` · `<Attachments>` · `<ThoughtChain>` · `<Actions>` |
118
+ | một glyph lucide đứng một mình (trong `Text`, ô bảng, thẻ `<a>`) | `<Icon as={Lock} size="sm">` — KHÔNG phải `size-4` hay `size={16}` |
118
119
 
119
120
  Lỗi không phải "đoán sai tên prop" mà là **cho rằng nó không tồn tại nên không
120
121
  hỏi**.
@@ -590,6 +590,68 @@ const RULES = [
590
590
  message:
591
591
  "No em-dash (—) in product copy. Use a middot `·` for JP/EN label pairs, or restructure into two calm sentences. Keep copy factual and quiet.",
592
592
  },
593
+ {
594
+ id: "lucide-icon-needs-size",
595
+ replacement: '<Icon as={Glyph} size="sm" />',
596
+ // CONSUMER scope, like the other rules about what an app may write: inside this package a bare
597
+ // glyph is normal — Tree, Pagination, Attachments and a dozen others size their own svg from
598
+ // their own stylesheet, which is exactly the capability a consumer does not have. Measured:
599
+ // 90 findings here with the scope off, every one of them a component sizing its own glyph.
600
+ scope: "consumer",
601
+ matches: lucideGlyphMatches,
602
+ spansElement: true,
603
+ severity: "warn",
604
+ /*
605
+ * A lucide element rendered where NOTHING will size it.
606
+ *
607
+ * A lucide component ships `width="24" height="24"`, and exactly four rules in this library
608
+ * override that: `.ui-button svg`, `.ui-dropdown-menu-item > svg`, `.ui-topbar-item svg` and
609
+ * `[data-slot="list-row-leading"] > svg`. Outside them — in a `Text`, a table cell, an `<a>`,
610
+ * a `Flex` — the glyph draws at 24px beside 14px type. It is the one defect class that is
611
+ * INVISIBLE in review: the JSX is correct, the import is correct, and only the screen is
612
+ * wrong. A consumer swept one app and found 38 (gh#712).
613
+ *
614
+ * `warn`, not `error`, and deliberately so: the exemptions below are lexical, so a glyph
615
+ * inside a consumer's own wrapper component that happens to size it would be reported. The
616
+ * fix is cheap (`<Icon as={Lock} size="sm" />`) and the finding is worth reading even then.
617
+ */
618
+ standard: "WCAG 2.2 SC 1.4.4 · @godxjp/ui icon scale (docs/TOKENS.md, --icon-size-*)",
619
+ message:
620
+ 'A lucide glyph with nothing to size it renders at its intrinsic 24px — 1.7× a 14px label. Put it on the scale with the primitive: <Icon as={Lock} size="sm" tone="muted" />, which is aria-hidden by default. A size-4 / w-[16px] utility is what docs/CONSUMER-RULES.md §3/§8 forbid, and size={16} hard-codes a number the theme owns.',
621
+ },
622
+ {
623
+ id: "no-hand-rolled-list",
624
+ replacement: 'Flex as="ul" marker="none" + ListRow as="li"',
625
+ scope: "consumer",
626
+ severity: "warn",
627
+ spansElement: true,
628
+ /*
629
+ * A list built by hand instead of by the primitives, in the three shapes a consumer reaches for:
630
+ *
631
+ * 1. a raw `<ul>`/`<ol>` — it cannot carry the gap token, so the spacing goes back into
632
+ * utilities, and until `marker="none"` (gh#714) it was the only way to get a list without
633
+ * a bullet and a --space-5 indent. It no longer is.
634
+ * 2. `role="list"` / `role="listitem"` on a div — ARIA re-describing markup that HTML already
635
+ * has a element for (WAI-ARIA 1.2 §Using ARIA in HTML: prefer the native element).
636
+ * 3. a `<li>` (or any wrapper) around a library ROW instead of BEING it. This is the one that
637
+ * cost real money: `[data-slot="list-row"]:not(:last-child)` draws the divider, so a row
638
+ * alone in a wrapper of its own is always `:last-child` and the rule never matches — a
639
+ * consumer's settings menu and dashboard lost EVERY divider, silently, and code review saw
640
+ * nothing. `ListRow` has `as="li"` (and `as` + `asChild` for a row of links) precisely so
641
+ * the rows stay siblings.
642
+ *
643
+ * WARN, not error, and the measurement says why: of 15 `role="list"` wrappers one consumer
644
+ * audited, 3 survived as deliberate exceptions (a drag-and-drop Kanban column of Cards, two
645
+ * evidence lists inside a TableCell). A rule wrong one time in five is a rule that gets
646
+ * silenced wholesale at `error`; at `warn` the three exceptions take an
647
+ * `ui-audit-disable-line` that states the reason and the other twelve still get fixed.
648
+ */
649
+ test: /<(?:ul|ol)(?=[\s/>])[^\n]*|\brole=["'](?:list|listitem)["']|<li(?=[\s>])(?:(?!<\/li>)[\s\S]){0,400}?<(?:ListRow|Card)(?=[\s/>])/g,
650
+ standard:
651
+ "WAI-ARIA 1.2 (list / listitem) · HTML Living Standard (ul/ol/li) · WCAG 2.2 SC 1.3.1",
652
+ message:
653
+ 'Hand-rolled list — a raw <ul>/<ol>, an ARIA role="list"/"listitem", or a <li> wrapped around a library row. Build it from <Flex as="ul" marker="none"> (keeps the element, the <li> semantics and the gap token; no bullet, no indent) with <ListRow as="li"> as the rows. A row inside a wrapper of its own is an only child, so its :not(:last-child) divider never matches and every divider disappears silently (docs/CONSUMER-RULES.md §4, gh#714).',
654
+ },
593
655
  ];
594
656
 
595
657
  /**
@@ -946,6 +1008,72 @@ function insideStringLiteral(source, index) {
946
1008
  return quote !== "";
947
1009
  }
948
1010
 
1011
+ /**
1012
+ * The LOCAL names bound to `lucide-react` icons in this file — `import { Lock, Pencil as Edit }`
1013
+ * gives `Lock` and `Edit`. Reading the import rather than matching "any PascalCase tag" is what
1014
+ * keeps the rule from reporting every component in the file.
1015
+ */
1016
+ function lucideLocalNames(source) {
1017
+ const names = new Set();
1018
+ for (const imp of source.matchAll(
1019
+ /import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*["']lucide-react["']/g,
1020
+ )) {
1021
+ for (const spec of imp[1].split(",")) {
1022
+ const local =
1023
+ spec
1024
+ .trim()
1025
+ .split(/\s+as\s+/)
1026
+ .pop()
1027
+ ?.trim() ?? "";
1028
+ if (/^[A-Z][A-Za-z0-9]*$/.test(local)) names.add(local);
1029
+ }
1030
+ }
1031
+ return names;
1032
+ }
1033
+
1034
+ /**
1035
+ * Character ranges in which a glyph is SOMEBODY ELSE'S to size, so a glyph inside one is quiet:
1036
+ *
1037
+ * 1. the body of a component whose CSS sizes its own `svg` — the four rules named on the rule
1038
+ * below, which are the only four that exist;
1039
+ * 2. the value of a slot PROP (`icon={<Lock />}`, `leading={<Lock />}`). A component handed a
1040
+ * glyph through a named slot owns the metric for that slot — that is what a slot IS — and
1041
+ * reporting the call site would push consumers away from the slot and toward a bare glyph.
1042
+ */
1043
+ function iconSizingRanges(source) {
1044
+ const ranges = [];
1045
+ for (const m of source.matchAll(
1046
+ /<(Button|DropdownMenuItem|DropdownMenuCheckboxItem|DropdownMenuRadioItem|TopbarItem)\b[\s\S]*?<\/\1>/g,
1047
+ )) {
1048
+ ranges.push([m.index, m.index + m[0].length]);
1049
+ }
1050
+ for (const m of source.matchAll(
1051
+ /\b(?:icon|leading|trailing|mark|indicator|avatar|prefix|suffix|addonBefore|addonAfter)\s*=\s*\{/g,
1052
+ )) {
1053
+ const open = m.index + m[0].length - 1;
1054
+ const close = matchBracket(source, open);
1055
+ ranges.push([open, close < 0 ? source.length : close + 1]);
1056
+ }
1057
+ return ranges;
1058
+ }
1059
+
1060
+ /** A lucide element that no rule, no slot and no author-supplied size will ever measure. */
1061
+ function* lucideGlyphMatches(source) {
1062
+ const names = lucideLocalNames(source);
1063
+ if (names.size === 0) return;
1064
+ const ranges = iconSizingRanges(source);
1065
+ for (const m of source.matchAll(new RegExp(`<(?:${[...names].join("|")})(?=[\\s/>])`, "g"))) {
1066
+ if (insideStringLiteral(source, m.index)) continue;
1067
+ if (ranges.some(([from, to]) => m.index > from && m.index < to)) continue;
1068
+ const end = jsxOpeningEnd(source, m.index);
1069
+ if (end >= source.length) continue;
1070
+ // An explicit size, or ANY className — a class list is the author saying something about this
1071
+ // element's box, and second-guessing which utility sizes it is how a warning becomes noise.
1072
+ if (/\s(?:size|width|height|className|style)\s*=/.test(source.slice(m.index, end))) continue;
1073
+ yield { 0: source.slice(m.index, end + 1), index: m.index };
1074
+ }
1075
+ }
1076
+
949
1077
  /** Match real JSX text after a balanced opening tag, including props with comparisons. */
950
1078
  function* currencyMatches(source) {
951
1079
  for (const opening of source.matchAll(/<(?:[A-Za-z][\w.:]*\b|(?=>))/g)) {