@icomponents/react 0.1.0

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 (117) hide show
  1. package/README.md +40 -0
  2. package/package.json +165 -0
  3. package/src/assets/custom/icon-dir.tsx +105 -0
  4. package/src/assets/custom/icon-layout-compact.tsx +84 -0
  5. package/src/assets/custom/icon-layout-default.tsx +48 -0
  6. package/src/assets/custom/icon-layout-full.tsx +68 -0
  7. package/src/assets/custom/icon-sidebar-floating.tsx +62 -0
  8. package/src/assets/custom/icon-sidebar-inset.tsx +53 -0
  9. package/src/assets/custom/icon-sidebar-sidebar.tsx +48 -0
  10. package/src/assets/custom/icon-theme-dark.tsx +36 -0
  11. package/src/assets/custom/icon-theme-light.tsx +35 -0
  12. package/src/assets/custom/icon-theme-system.tsx +113 -0
  13. package/src/components/admin-page-header.tsx +29 -0
  14. package/src/components/color-theme-switch.tsx +93 -0
  15. package/src/components/coming-soon.tsx +16 -0
  16. package/src/components/command-menu.tsx +98 -0
  17. package/src/components/config-drawer.tsx +401 -0
  18. package/src/components/confirm-dialog.dom.test.tsx +60 -0
  19. package/src/components/confirm-dialog.tsx +75 -0
  20. package/src/components/data-table/bulk-actions.tsx +193 -0
  21. package/src/components/data-table/column-header.tsx +66 -0
  22. package/src/components/data-table/data-table.dom.test.tsx +54 -0
  23. package/src/components/data-table/data-table.tsx +189 -0
  24. package/src/components/data-table/faceted-filter.tsx +156 -0
  25. package/src/components/data-table/index.ts +5 -0
  26. package/src/components/data-table/pagination.tsx +127 -0
  27. package/src/components/data-table/tanstack-table.d.ts +12 -0
  28. package/src/components/data-table/toolbar.tsx +88 -0
  29. package/src/components/data-table/view-options.tsx +49 -0
  30. package/src/components/date-picker.tsx +39 -0
  31. package/src/components/detail-card.tsx +63 -0
  32. package/src/components/language-switch.tsx +38 -0
  33. package/src/components/layout/app-sidebar.tsx +59 -0
  34. package/src/components/layout/app-title.tsx +62 -0
  35. package/src/components/layout/header.tsx +61 -0
  36. package/src/components/layout/main.tsx +34 -0
  37. package/src/components/layout/nav-group.tsx +185 -0
  38. package/src/components/layout/nav-user.tsx +115 -0
  39. package/src/components/layout/screen-header.tsx +119 -0
  40. package/src/components/layout/team-switcher.tsx +84 -0
  41. package/src/components/layout/top-nav.tsx +68 -0
  42. package/src/components/layout/types.ts +44 -0
  43. package/src/components/learn-more.tsx +35 -0
  44. package/src/components/long-text.tsx +80 -0
  45. package/src/components/navigation-progress.tsx +18 -0
  46. package/src/components/password-input.tsx +36 -0
  47. package/src/components/profile-dropdown.tsx +106 -0
  48. package/src/components/search.tsx +38 -0
  49. package/src/components/select-dropdown.tsx +62 -0
  50. package/src/components/sign-out-dialog.tsx +31 -0
  51. package/src/components/skip-to-main.tsx +10 -0
  52. package/src/components/stat-tile.tsx +56 -0
  53. package/src/components/status-badge.test.ts +23 -0
  54. package/src/components/status-badge.tsx +49 -0
  55. package/src/components/theme-switch.tsx +61 -0
  56. package/src/components/ui/alert-dialog.tsx +134 -0
  57. package/src/components/ui/alert.tsx +59 -0
  58. package/src/components/ui/avatar.tsx +38 -0
  59. package/src/components/ui/badge.tsx +39 -0
  60. package/src/components/ui/base-ui-state-attrs.dom.test.tsx +80 -0
  61. package/src/components/ui/button.tsx +51 -0
  62. package/src/components/ui/calendar.tsx +174 -0
  63. package/src/components/ui/card.tsx +74 -0
  64. package/src/components/ui/checkbox.tsx +26 -0
  65. package/src/components/ui/collapsible.tsx +18 -0
  66. package/src/components/ui/command.tsx +160 -0
  67. package/src/components/ui/dialog.tsx +129 -0
  68. package/src/components/ui/dropdown-menu.tsx +254 -0
  69. package/src/components/ui/form.tsx +155 -0
  70. package/src/components/ui/input-otp.tsx +67 -0
  71. package/src/components/ui/input.tsx +20 -0
  72. package/src/components/ui/label.tsx +21 -0
  73. package/src/components/ui/popover.tsx +54 -0
  74. package/src/components/ui/radio-group.tsx +37 -0
  75. package/src/components/ui/scroll-area.tsx +57 -0
  76. package/src/components/ui/select.tsx +213 -0
  77. package/src/components/ui/separator.tsx +23 -0
  78. package/src/components/ui/sheet.tsx +128 -0
  79. package/src/components/ui/sidebar.tsx +702 -0
  80. package/src/components/ui/skeleton.tsx +13 -0
  81. package/src/components/ui/sonner.tsx +26 -0
  82. package/src/components/ui/switch.tsx +25 -0
  83. package/src/components/ui/table.tsx +89 -0
  84. package/src/components/ui/tabs.tsx +51 -0
  85. package/src/components/ui/textarea.tsx +17 -0
  86. package/src/components/ui/tooltip.tsx +65 -0
  87. package/src/config/fonts.ts +2 -0
  88. package/src/context/direction-provider.tsx +61 -0
  89. package/src/context/font-provider.tsx +70 -0
  90. package/src/context/language-provider.tsx +73 -0
  91. package/src/context/layout-provider.tsx +81 -0
  92. package/src/context/search-provider.tsx +48 -0
  93. package/src/context/theme-provider.tsx +108 -0
  94. package/src/hooks/sort-url.test.ts +16 -0
  95. package/src/hooks/use-dialog-state.tsx +15 -0
  96. package/src/hooks/use-mobile.tsx +16 -0
  97. package/src/hooks/use-table-url-state.ts +243 -0
  98. package/src/i18n/common.ts +164 -0
  99. package/src/i18n/create-i18n.test.ts +50 -0
  100. package/src/i18n/create-i18n.ts +67 -0
  101. package/src/i18n/types.ts +22 -0
  102. package/src/i18n/use-translation.ts +6 -0
  103. package/src/lib/clipboard.test.ts +46 -0
  104. package/src/lib/clipboard.ts +29 -0
  105. package/src/lib/cookies.ts +39 -0
  106. package/src/lib/format-cell.test.ts +57 -0
  107. package/src/lib/format-cell.ts +63 -0
  108. package/src/lib/handle-server-error.test.ts +85 -0
  109. package/src/lib/handle-server-error.ts +39 -0
  110. package/src/lib/random-id.ts +13 -0
  111. package/src/lib/show-submitted-data.tsx +14 -0
  112. package/src/lib/utils.test.ts +35 -0
  113. package/src/lib/utils.ts +78 -0
  114. package/src/secure-context.contract.test.ts +63 -0
  115. package/src/testids.contract.test.ts +113 -0
  116. package/src/testids.ts +8 -0
  117. package/src/theme-contract.test.ts +72 -0
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { Glob } from "bun";
4
+
5
+ import { TESTID } from "./testids";
6
+
7
+ /**
8
+ * Страж контракта вёрстки.
9
+ *
10
+ * Контракт (`testids.ts`) имеет смысл, только если его КТО-ТО НЕСЁТ. Заменили вёрстку целиком на
11
+ * прототип, забыли навесить `data-testid` — и сеть тестов обманчиво «позеленеет»: e2e не найдёт экран и
12
+ * упадёт, а DOM-тесты компонентов, которые перестали использоваться, будут бодро проходить на мёртвом
13
+ * коде. Этот тест ловит ровно такую ситуацию: он проверяет, что ключевые id ЖИВУТ в исходниках UI, а не
14
+ * только в файле контракта.
15
+ *
16
+ * Покраснел после замены вёрстки — вёрстка не выполнила контракт. Это НЕ «тест устарел».
17
+ */
18
+ const REPO = new URL("../../..", import.meta.url).pathname;
19
+
20
+ /**
21
+ * Где искать вёрстку. В репе КИТА это его собственные компоненты: контракт держит тот, кто рисует
22
+ * таблицу, диалог подтверждения и оболочку. У приложения свой такой же страж — он сторожит уже
23
+ * ЕГО экраны, потому что `screen`/`field` навешивает продуктовая вёрстка, а не кит.
24
+ */
25
+ const UI_ROOTS = ["packages/react/src"];
26
+
27
+ /** Без этих id тест не может ни найти экран, ни адресовать строку, ни подтвердить действие. */
28
+ const REQUIRED = [
29
+ TESTID.screen,
30
+ TESTID.screenTitle,
31
+ TESTID.table,
32
+ TESTID.tableEmpty,
33
+ TESTID.confirmDialog,
34
+ TESTID.confirmAccept,
35
+ TESTID.confirmCancel,
36
+ ];
37
+
38
+ async function uiSources(): Promise<string> {
39
+ let all = "";
40
+ for (const root of UI_ROOTS) {
41
+ const cwd = `${REPO}/${root}`;
42
+ try {
43
+ for await (const rel of new Glob("**/*.tsx").scan({ cwd })) {
44
+ if (rel.endsWith(".test.tsx")) continue;
45
+ all += await Bun.file(`${cwd}/${rel}`).text();
46
+ }
47
+ } catch {
48
+ // каталога нет в этом продукте — пропускаем, а не роняем прогон
49
+ }
50
+ }
51
+ return all;
52
+ }
53
+
54
+ /** Обратное отображение значение → имя ключа: тест ищет ИСПОЛЬЗОВАНИЕ константы, а не строку-литерал. */
55
+ function nameOf(value: string): string {
56
+ const entry = Object.entries(TESTID).find(([, v]) => v === value);
57
+ return entry ? entry[0] : value;
58
+ }
59
+
60
+ describe("контракт вёрстки: data-testid живут в UI, а не только в testids.ts", () => {
61
+ it("каждый обязательный testid где-то навешен", async () => {
62
+ const src = await uiSources();
63
+ expect(REQUIRED.filter((id) => !src.includes(`TESTID.${nameOf(id)}`))).toEqual([]);
64
+ });
65
+
66
+ it("строку списка можно адресовать по id сущности (row-<id>)", async () => {
67
+ // Без этого тест сможет проверить только «третью строку сверху» — а порядок сортировки не контракт.
68
+ expect(await uiSources()).toContain("TESTID.tableRow(");
69
+ });
70
+
71
+ /**
72
+ * `field-<name>` и `form-submit` здесь НЕ проверяются, и это разделение ответственности, а не
73
+ * послабление: поля рисуют формы продукта (вход, экраны слайсов), кит даёт только примитив
74
+ * `<Input>`, которому нечего называть. Требование живёт в страже приложения — там, где есть формы.
75
+ * Если однажды кит начнёт собирать формы сам, проверку возвращать сюда.
76
+ */
77
+ it("страж покрывает ровно ту вёрстку, за которую отвечает кит", async () => {
78
+ const src = await uiSources();
79
+ expect(src).toContain("TESTID.confirmDialog");
80
+ expect(src).toContain("TESTID.navItem(");
81
+ });
82
+
83
+ /**
84
+ * Хваталки СВОИХ действий объявляет слайс, который ими владеет, — в своём пакете. Проверять их здесь
85
+ * поимённо было бы неверно: список пришлось бы править в ядре при каждом новом слайсе, а ядро
86
+ * перезаписывается синком. Поэтому страж находит модули `testids.ts` слайсов САМ и требует, чтобы
87
+ * каждый объявленный в них ключ действительно использовался. Объявленный и не навешенный id — это
88
+ * действие, которое никакой тест не проверяет.
89
+ */
90
+ it("каждый ключ, объявленный слайсом в своём testids.ts, используется в вёрстке", async () => {
91
+ const src = await uiSources();
92
+ const unused: string[] = [];
93
+
94
+ for (const root of UI_ROOTS) {
95
+ if (root.startsWith("packages/ui")) continue; // ядро проверено списком REQUIRED выше
96
+ const cwd = `${REPO}/${root}`;
97
+ try {
98
+ for await (const rel of new Glob("**/testids.ts").scan({ cwd })) {
99
+ const text = await Bun.file(`${cwd}/${rel}`).text();
100
+ // Имена ключей объявления: `noteAction: (…)` / `statusFilter: "…"`.
101
+ for (const match of text.matchAll(/^\s{2}(\w+):/gm)) {
102
+ const key = match[1]!;
103
+ if (!src.includes(`.${key}`)) unused.push(`${root}/${rel} → ${key}`);
104
+ }
105
+ }
106
+ } catch {
107
+ // каталога нет — пропускаем
108
+ }
109
+ }
110
+
111
+ expect(unused).toEqual([]);
112
+ });
113
+ });
package/src/testids.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Контракт вёрстки живёт в `@icomponents/tokens` — он платформо-независим: те же `data-testid`
3
+ * понадобятся и React Native-реализации, а дублировать контракт значит развести две правды.
4
+ *
5
+ * Здесь остаётся реэкспорт, потому что путь `@icomponents/react/testids` — публичная поверхность,
6
+ * на которую уже ссылаются приложения и их тесты.
7
+ */
8
+ export * from "@icomponents/tokens/testids";
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { Glob } from "bun";
4
+
5
+ /**
6
+ * Контракт темы: цвет живёт в ТОКЕНАХ, а не в компонентах.
7
+ *
8
+ * Этот тест написан перед перекраской в тему прототипа — и именно ради неё. Перекраска меняет
9
+ * значения токенов в `theme.css`; компоненты при этом трогать не нужно. Но ровно в такой момент
10
+ * появляется соблазн «быстренько» вписать нужный оттенок прямо в компонент — и тогда экран, который
11
+ * забыли, навсегда останется прежнего цвета, а в тёмной теме станет нечитаемым. Снаружи это выглядит
12
+ * не как ошибка, а как «дизайн такой».
13
+ *
14
+ * Поэтому: `#hex`, `rgb(...)` и `hsl(...)` в компонентах запрещены. Исключение — SVG-ассеты: там цвет
15
+ * часть самой картинки, а не оформления.
16
+ */
17
+ /**
18
+ * Контракт живёт в дизайн-системе (ядро, синкается в продукты), но сканирует ВЕСЬ UI: правило про
19
+ * цвет бессмысленно, если его соблюдают только компоненты kit'а, а экраны слайсов зашивают свои
20
+ * оттенки. Каталоги, которых в продукте нет, просто пропускаются.
21
+ */
22
+ const REPO = new URL("../../..", import.meta.url).pathname;
23
+ const UI_ROOTS = [
24
+ "packages/ui/src",
25
+ "packages/iam/src/features",
26
+ "packages/billing/src/ui",
27
+ "packages/dashboard/src/ui",
28
+ "apps/web/src/client",
29
+ ];
30
+
31
+ /**
32
+ * Сырой цвет — это `#hex` и `rgb()/hsl()` с ЧИСЛАМИ. А `hsl(var(--sidebar-border))` — это токен:
33
+ * функция лишь разворачивает переменную, и при перекраске он поменяется вместе с темой.
34
+ */
35
+ const RAW_HEX = /#[0-9a-fA-F]{3,8}\b/;
36
+ const RAW_FN = /\b(rgba?|hsla?)\(\s*(?!var\()/;
37
+ const hasRawColor = (src: string): boolean => RAW_HEX.test(src) || RAW_FN.test(src);
38
+
39
+ /** Ассеты — картинки: их цвета не «оформление», а содержимое. Тесты — не продакшен-код. */
40
+ const EXEMPT = /(^|\/)assets\/|\.test\.tsx?$|\.css$/;
41
+
42
+ async function componentSources(): Promise<{ file: string; text: string }[]> {
43
+ const out: { file: string; text: string }[] = [];
44
+ for (const root of UI_ROOTS) {
45
+ const cwd = `${REPO}/${root}`;
46
+ try {
47
+ for await (const rel of new Glob("**/*.{ts,tsx}").scan({ cwd })) {
48
+ if (EXEMPT.test(rel)) continue;
49
+ out.push({ file: `${root}/${rel}`, text: await Bun.file(`${cwd}/${rel}`).text() });
50
+ }
51
+ } catch {
52
+ // каталога нет в этом продукте — правило к нему не применяется
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+
58
+ describe("тема — цвет в токенах, а не в компонентах (NFR-DESIGN)", () => {
59
+ it("ни один компонент не зашивает сырой цвет (#hex / rgb() / hsl())", async () => {
60
+ const offenders = (await componentSources())
61
+ .filter((f) => hasRawColor(stripComments(f.text)))
62
+ .map((f) => f.file);
63
+
64
+ // Если этот тест покраснел во время перекраски — значит цвет прототипа попал в компонент.
65
+ // Правильное место для него — переменная в theme.css, иначе экран, который забыли, останется
66
+ // старого цвета навсегда.
67
+ expect(offenders).toEqual([]);
68
+ });
69
+ });
70
+
71
+ const stripComments = (src: string): string =>
72
+ src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");