@olddevs/ui 4.0.3 → 4.1.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 (98) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/components/actions/Toggle.d.ts.map +1 -1
  3. package/dist/components/actions/Toggle.js +1 -1
  4. package/dist/components/actions/Toggle.js.map +1 -1
  5. package/dist/components/actions/Truncate.d.ts.map +1 -1
  6. package/dist/components/actions/Truncate.js +2 -2
  7. package/dist/components/actions/Truncate.js.map +1 -1
  8. package/dist/components/app-shell/MobileTabBar.d.ts.map +1 -1
  9. package/dist/components/app-shell/MobileTabBar.js +1 -1
  10. package/dist/components/app-shell/MobileTabBar.js.map +1 -1
  11. package/dist/components/app-shell/NavItem.d.ts.map +1 -1
  12. package/dist/components/app-shell/NavItem.js +3 -1
  13. package/dist/components/app-shell/NavItem.js.map +1 -1
  14. package/dist/components/app-shell/SideNav.d.ts +17 -0
  15. package/dist/components/app-shell/SideNav.d.ts.map +1 -1
  16. package/dist/components/app-shell/SideNav.js +2 -1
  17. package/dist/components/app-shell/SideNav.js.map +1 -1
  18. package/dist/components/content/AvatarGroup.d.ts.map +1 -1
  19. package/dist/components/content/AvatarGroup.js +2 -1
  20. package/dist/components/content/AvatarGroup.js.map +1 -1
  21. package/dist/components/content/Table.d.ts.map +1 -1
  22. package/dist/components/content/Table.js +2 -1
  23. package/dist/components/content/Table.js.map +1 -1
  24. package/dist/components/content/VirtualGrid.d.ts.map +1 -1
  25. package/dist/components/content/VirtualGrid.js +4 -1
  26. package/dist/components/content/VirtualGrid.js.map +1 -1
  27. package/dist/components/inputs/Checkbox.d.ts.map +1 -1
  28. package/dist/components/inputs/Checkbox.js +3 -0
  29. package/dist/components/inputs/Checkbox.js.map +1 -1
  30. package/dist/components/inputs/CheckboxCard.d.ts.map +1 -1
  31. package/dist/components/inputs/CheckboxCard.js +2 -2
  32. package/dist/components/inputs/CheckboxCard.js.map +1 -1
  33. package/dist/components/inputs/RadioGroup.d.ts.map +1 -1
  34. package/dist/components/inputs/RadioGroup.js +5 -3
  35. package/dist/components/inputs/RadioGroup.js.map +1 -1
  36. package/dist/components/inputs/Select.d.ts.map +1 -1
  37. package/dist/components/inputs/Select.js +6 -6
  38. package/dist/components/inputs/Select.js.map +1 -1
  39. package/dist/components/inputs/Slider.d.ts.map +1 -1
  40. package/dist/components/inputs/Slider.js +2 -2
  41. package/dist/components/inputs/Slider.js.map +1 -1
  42. package/dist/components/navigation/Accordion.d.ts.map +1 -1
  43. package/dist/components/navigation/Accordion.js +1 -1
  44. package/dist/components/navigation/Accordion.js.map +1 -1
  45. package/dist/components/navigation/TreeView.d.ts.map +1 -1
  46. package/dist/components/navigation/TreeView.js +6 -3
  47. package/dist/components/navigation/TreeView.js.map +1 -1
  48. package/dist/editor/RichTextEditor.d.ts.map +1 -1
  49. package/dist/editor/RichTextEditor.js +4 -2
  50. package/dist/editor/RichTextEditor.js.map +1 -1
  51. package/dist/editor/styles/prose.d.ts +7 -0
  52. package/dist/editor/styles/prose.d.ts.map +1 -1
  53. package/dist/editor/styles/prose.js +1 -1
  54. package/dist/editor/styles/prose.js.map +1 -1
  55. package/dist/editor/ui/FormatMenus.d.ts.map +1 -1
  56. package/dist/editor/ui/FormatMenus.js +2 -1
  57. package/dist/editor/ui/FormatMenus.js.map +1 -1
  58. package/dist/index.d.ts +1 -1
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +1 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/lib/choice-card.d.ts +13 -0
  63. package/dist/lib/choice-card.d.ts.map +1 -1
  64. package/dist/lib/choice-card.js +2 -0
  65. package/dist/lib/choice-card.js.map +1 -1
  66. package/docs/components/app-shell/mobile-tab-bar.en.md +3 -3
  67. package/docs/components/app-shell/mobile-tab-bar.md +2 -2
  68. package/docs/components/app-shell/side-nav.en.md +56 -6
  69. package/docs/components/app-shell/side-nav.md +55 -5
  70. package/docs/components/index.en.md +2 -2
  71. package/docs/components/index.md +1 -1
  72. package/docs/components/inputs/form.en.md +2 -2
  73. package/docs/components/inputs/form.md +1 -1
  74. package/docs/components/inputs/rich-text-editor.en.md +2 -2
  75. package/docs/components/inputs/rich-text-editor.md +1 -1
  76. package/docs/getting-started.en.md +3 -3
  77. package/docs/getting-started.md +2 -2
  78. package/package.json +1 -1
  79. package/src/components/actions/Toggle.tsx +10 -3
  80. package/src/components/actions/Truncate.tsx +5 -3
  81. package/src/components/app-shell/MobileTabBar.tsx +9 -1
  82. package/src/components/app-shell/NavItem.tsx +6 -1
  83. package/src/components/app-shell/SideNav.tsx +27 -2
  84. package/src/components/content/AvatarGroup.tsx +6 -2
  85. package/src/components/content/Table.tsx +15 -3
  86. package/src/components/content/VirtualGrid.tsx +11 -1
  87. package/src/components/inputs/Checkbox.tsx +5 -0
  88. package/src/components/inputs/CheckboxCard.tsx +2 -0
  89. package/src/components/inputs/RadioGroup.tsx +10 -1
  90. package/src/components/inputs/Select.tsx +113 -106
  91. package/src/components/inputs/Slider.tsx +6 -2
  92. package/src/components/navigation/Accordion.tsx +4 -1
  93. package/src/components/navigation/TreeView.tsx +70 -49
  94. package/src/editor/RichTextEditor.tsx +6 -3
  95. package/src/editor/styles/prose.ts +1 -1
  96. package/src/editor/ui/FormatMenus.tsx +16 -1
  97. package/src/index.ts +1 -1
  98. package/src/lib/choice-card.ts +17 -0
@@ -1123,7 +1123,7 @@ ARIA сетки — шаблон `grid` из APG: `role="grid"` → `row` → `g
1123
1123
 
1124
1124
  **Что видно.** HTML отформатирован: Блок верхнего уровня — на своей строке, дети контейнеров Блоков (пункты списков, строки и ячейки таблицы, колонки Раскладки, части `figure`, пункт чек-листа) — с отступом в два пробела, содержимое Блока — на строке его тега (пробелы текста не меняются), содержимое `pre` — как есть. Форматирование без потерь: неизменённый текст при применении даёт тот же Документ. Пустой Документ открывается пустым полем, а не `<p></p>`. Подсветки синтаксиса нет.
1125
1125
 
1126
- **Вид.** Поле **всегда тёмное, цветом кита**, а не палитрой подсветки синтаксиса Блока кода: в `dark` и `dim` — самая глубокая подложка темы с её текстом и рамкой, в `light` и на Листе `paper` — те же значения тёмной темы (переменные `--rte-source-background`, `--rte-source-foreground`, `--rte-source-border`, их можно перекрыть). Скругление — как у кода, шрифт — моноширинный 13 px, как у `CodeView`; отступы — как у области Документа. Высота при входе — не меньше высоты Документа, дальше поле растёт по содержимому и растягивается по вертикали вручную. С `maxHeight` или `resizable` поле живёт в той же области прокрутки, что и Документ (раздел «Высота поля»). Выделение текста в поле — тоже тёмной темы текущего акцента (`--selection-dark`) со светлым текстом поля: светлое выделение светлой темы на тёмном поле не читалось.
1126
+ **Вид.** Поле **всегда тёмное, цветом кита**, а не палитрой подсветки синтаксиса Блока кода: в `dark` и `dim` — самая глубокая подложка темы с её текстом и рамкой, в `light` и на Листе `paper` — те же значения тёмной темы (переменные `--rte-source-background`, `--rte-source-foreground`, `--rte-source-border`, их можно перекрыть). На Листе `paper` светлый лист остаётся под тёмным полем: меняется содержимое листа, а не сам лист. Скругление — как у кода, шрифт — моноширинный 13 px, как у `CodeView`; отступы — как у области Документа. Высота при входе — не меньше высоты Документа, дальше поле растёт по содержимому и растягивается по вертикали вручную. С `maxHeight` или `resizable` поле живёт в той же области прокрутки, что и Документ (раздел «Высота поля»). Выделение текста в поле — тоже тёмной темы текущего акцента (`--selection-dark`) со светлым текстом поля: светлое выделение светлой темы на тёмном поле не читалось.
1127
1127
 
1128
1128
  **Применение — при выходе, уходе фокуса и чтении.** Текст поля проходит через Схему тем же разбором, что входящий `value` (всё, чего в Схеме нет, отбрасывается — `script`, обработчики `on*`, произвольные `style` и `class`, `javascript:`; разметка выключенных Инструментов снимается, текст остаётся), и заменяет Документ **одной правкой** — одна отмена `Ctrl/⌘+Z` в Документе возвращает его к состоянию до применения. Применение запускают:
1129
1129
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- sourceHash: 19857ad0f54d
2
+ sourceHash: 6f3e6f49e2eb
3
3
  ---
4
4
 
5
5
  # Setting up `@olddevs/ui`
@@ -132,10 +132,10 @@ peer dependencies are treated as external, and the metric is gzip:
132
132
  | `cn` and `tableStateToParams` | 0.6 kB |
133
133
  | one `Button` | 1.6 kB |
134
134
  | `DataTable` | 9.0 kB |
135
- | the whole kit (`import *`) | 71.1 kB |
135
+ | the whole kit (`import *`) | 71.4 kB |
136
136
  | `RichTextEditor` from `@olddevs/ui/editor`, without the engine, "start" | 70.4 kB |
137
137
  | `RichTextEditor` with the Tiptap engine, "start" (static graph, `basic`) | 295.1 kB |
138
- | `RichTextEditor` with the Tiptap engine, "full" (with all lazy chunks, `full`) | 338.9 kB |
138
+ | `RichTextEditor` with the Tiptap engine, "full" (with all lazy chunks, `full`) | 339.1 kB |
139
139
  | `RichTextView` from `@olddevs/ui/editor`, Tiptap engine included in the measurement | 12.0 kB |
140
140
  | `sanitizeRichText` and `richTextSanitizeConfig` from `@olddevs/ui/editor/schema`, Tiptap engine included | 3.6 kB |
141
141
  | `highlightCodeBlocks` from `@olddevs/ui/editor/highlight`, with lowlight | 22.6 kB |
@@ -129,10 +129,10 @@ peer-зависимости внешние, метрика gzip:
129
129
  | `cn` и `tableStateToParams` | 0,6 кБ |
130
130
  | одна `Button` | 1,6 кБ |
131
131
  | `DataTable` | 9,0 кБ |
132
- | весь кит целиком (`import *`) | 71,1 кБ |
132
+ | весь кит целиком (`import *`) | 71,4 кБ |
133
133
  | `RichTextEditor` из `@olddevs/ui/editor`, без движка, «старт» | 70,4 кБ |
134
134
  | `RichTextEditor` вместе с движком Tiptap, «старт» (статический граф, `basic`) | 295,1 кБ |
135
- | `RichTextEditor` вместе с движком Tiptap, «полный» (со всеми ленивыми чанками, `full`) | 338,9 кБ |
135
+ | `RichTextEditor` вместе с движком Tiptap, «полный» (со всеми ленивыми чанками, `full`) | 339,1 кБ |
136
136
  | `RichTextView` из `@olddevs/ui/editor`, движок Tiptap в замер включён | 12,0 кБ |
137
137
  | `sanitizeRichText` и `richTextSanitizeConfig` из `@olddevs/ui/editor/schema`, движок Tiptap в замер включён | 3,6 кБ |
138
138
  | `highlightCodeBlocks` из `@olddevs/ui/editor/highlight`, вместе с lowlight | 22,6 кБ |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olddevs/ui",
3
- "version": "4.0.3",
3
+ "version": "4.1.1",
4
4
  "description": "Olddevs UI — design system: tokens, themes and accessible UI primitives",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -194,10 +194,17 @@ export const Toggle = forwardRef<HTMLButtonElement, ToggleProps>(function Toggle
194
194
  оно обёрнуто в свой `<label for>`.
195
195
  */
196
196
  return (
197
- <div className={cn("flex items-center justify-between gap-4", className)}>
197
+ <div className={cn("group/toggle flex items-center justify-between gap-4", className)}>
198
198
  {/* Приглушается только текст: дорожка приглушает себя сама (`disabled:opacity-50`),
199
- и общая прозрачность строки дала бы ей 25 % вместо 50 % — иначе, чем без подписи. */}
200
- <div className={cn("flex min-w-0 flex-col", disabled && "opacity-50")}>
199
+ и общая прозрачность строки дала бы ей 25 % вместо 50 % — иначе, чем без подписи.
200
+ `group-has-[[role=switch]:disabled]/toggle:` — для кнопки, выключенной предком-`fieldset`:
201
+ свойство `disabled` у неё при этом ложно, и одно оно текст не погасило бы. */}
202
+ <div
203
+ className={cn(
204
+ "flex min-w-0 flex-col group-has-[[role=switch]:disabled]/toggle:opacity-50",
205
+ disabled && "opacity-50"
206
+ )}
207
+ >
201
208
  <label
202
209
  id={labelId}
203
210
  htmlFor={switchId}
@@ -68,10 +68,12 @@ export function Truncate({ children, lines = 1, side = "top", className, ...rest
68
68
  // клавиатурой не нужен и мешал бы.
69
69
  tabIndex={truncated ? 0 : undefined}
70
70
  className={cn(
71
- "focus-visible:outline-accent-strong block min-w-0 focus-visible:outline-2 focus-visible:outline-offset-2",
71
+ "focus-visible:outline-accent-strong min-w-0 focus-visible:outline-2 focus-visible:outline-offset-2",
72
72
  lines === 1
73
- ? "truncate"
74
- : // Многострочная обрезка: `line-clamp` считает строки сам, и число
73
+ ? "block truncate"
74
+ : // `block` сюда нельзя: в CSS он стоит позже `[display:-webkit-box]`,
75
+ // перебивает его, и `line-clamp` молча не работает.
76
+ // Многострочная обрезка: `line-clamp` считает строки сам, и число
75
77
  // приходит переменной — иначе класс собирался бы конкатенацией, а
76
78
  // такого текста в исходниках нет и Tailwind его не увидит.
77
79
  "[display:-webkit-box] overflow-hidden [-webkit-box-orient:vertical]",
@@ -263,7 +263,15 @@ export const MobileTabBarItem = forwardRef<HTMLAnchorElement, MobileTabBarItemPr
263
263
  {custom ? (
264
264
  <span className="absolute top-[-2px] -right-1.5 flex">{custom}</span>
265
265
  ) : shown ? (
266
- <Badge overlay dot={dot} className="top-[-2px] -right-1.5">
266
+ // Значок держится ЛЕВОЙ кромкой у правого верхнего угла иконки и растёт
267
+ // вправо: привязка правой кромкой (`-right-1.5`) растила широкий «99+» или
268
+ // «Beta» влево, на иконку. Сдвиг `-ml-*` повторяет прежнее положение точки
269
+ // (8px) и короткого числа (16px): они выглядят как раньше.
270
+ <Badge
271
+ overlay
272
+ dot={dot}
273
+ className={cn("top-[-2px] right-auto left-full", dot ? "-ml-0.5" : "-ml-2.5")}
274
+ >
267
275
  {count ?? text}
268
276
  </Badge>
269
277
  ) : null}
@@ -68,7 +68,12 @@ export interface NavItemProps extends Omit<ComponentPropsWithoutRef<"a">, "child
68
68
  const BASE =
69
69
  "focus-visible:outline-accent-strong flex items-center gap-3 rounded-control border " +
70
70
  "px-3 py-[9px] text-[13px] leading-5 no-underline transition-colors " +
71
- "focus-visible:outline-2 focus-visible:outline-offset-2";
71
+ "focus-visible:outline-2 focus-visible:outline-offset-2 " +
72
+ // Недоступный пункт красится по `aria-disabled`, как у `Button`: у ссылки нет
73
+ // родного `disabled`, и без этого правила пункт, выключенный атрибутом ARIA,
74
+ // выглядел бы рабочим — с тем же цветом и курсором-указателем.
75
+ "aria-disabled:opacity-50 aria-disabled:cursor-default " +
76
+ "aria-disabled:hover:bg-transparent aria-disabled:hover:text-fg-1";
72
77
 
73
78
  const STATE = {
74
79
  active: "border-line bg-bg-3 text-fg-0 font-semibold",
@@ -74,6 +74,24 @@ export const NavGroup = forwardRef<HTMLDivElement, NavGroupProps>(function NavGr
74
74
  );
75
75
  });
76
76
 
77
+ /**
78
+ * Схлопнута ли навигация — для содержимого слотов `header` и `footer`.
79
+ *
80
+ * Пункты и разделы кита подстраиваются под иконочный режим сами, а слоты
81
+ * принадлежат потребителю, и кит не знает, что в них лежит. Блоку пользователя
82
+ * в подвале нужно знать ширину колонки: в иконочном режиме место есть только
83
+ * для аватара, а имя и роль, оставленные как есть, переносятся по словам и
84
+ * вылезают за край.
85
+ *
86
+ * Хук, а не render-проп у слота: слот остаётся обычным `ReactNode`, и тот же
87
+ * компонент пользователя работает и вне `SideNav`. Там хук возвращает `false`
88
+ * — развёрнутое состояние по умолчанию, как у `NavItem`. В ящике мобильного
89
+ * режима навигация всегда развёрнута, и хук это повторяет.
90
+ *
91
+ * Для подстройки чистым CSS у `<nav>` есть атрибут `data-collapsed`.
92
+ */
93
+ export const useNavCollapsed = (): boolean => useContext(NavCollapsedContext);
94
+
77
95
  export interface SideNavProps extends Omit<ComponentPropsWithoutRef<"nav">, "children"> {
78
96
  /** Содержимое колонки между шапкой и подвалом: `NavGroup` и `NavItem`; прокручивается, если не помещается. */
79
97
  children: ReactNode;
@@ -181,12 +199,19 @@ export const SideNav = forwardRef<HTMLElement, SideNavProps>(function SideNav(
181
199
  )}
182
200
  {...rest}
183
201
  >
184
- {header != null ? <div className="shrink-0">{header}</div> : null}
202
+ {/*
203
+ * Шапка и подвал обрезают лишнее по ширине: слот потребителя может не
204
+ * знать про иконочный режим, и без обрезки его текст лёг бы поверх
205
+ * содержимого страницы. Обрезка только по горизонтали (`clip`, а не
206
+ * `hidden`): по вертикали слот растёт сам, а `hidden` по одной оси
207
+ * сделал бы другую прокручиваемой.
208
+ */}
209
+ {header != null ? <div className="shrink-0 overflow-x-clip">{header}</div> : null}
185
210
 
186
211
  <div className="flex flex-1 flex-col gap-0.5 overflow-y-auto px-2.5 py-1">{children}</div>
187
212
 
188
213
  {footer != null || showToggle ? (
189
- <div className="border-line-soft shrink-0 border-t p-2.5">
214
+ <div className="border-line-soft shrink-0 overflow-x-clip border-t p-2.5">
190
215
  {footer}
191
216
  {showToggle ? (
192
217
  <div className={cn("flex", isCollapsed ? "justify-center" : "justify-end")}>
@@ -56,6 +56,9 @@ export function AvatarGroup({
56
56
  const members = names.map((entry) => (typeof entry === "string" ? { name: entry } : entry));
57
57
  const shown = members.slice(0, max);
58
58
  const rest = members.length - shown.length;
59
+ // Перекрытие по размеру круга: на 28px прежние 8px плюс кольцо срезали
60
+ // инициалы слева и плюс у «+N»; на 64px они не мешают.
61
+ const overlap = size === "lg" ? "-ml-2" : "-ml-1";
59
62
 
60
63
  return (
61
64
  <span
@@ -67,7 +70,7 @@ export function AvatarGroup({
67
70
  <span
68
71
  key={`${name}-${index}`}
69
72
  aria-hidden
70
- className={cn("ring-bg-1 relative rounded-full ring-2", index > 0 && "-ml-2")}
73
+ className={cn("ring-bg-1 relative rounded-full ring-2", index > 0 && overlap)}
71
74
  style={{ zIndex: shown.length - index }}
72
75
  >
73
76
  {/* `alt=""`: круг скрыт, имя объявляет группа — подпись у фото была бы вторым чтением. */}
@@ -80,7 +83,8 @@ export function AvatarGroup({
80
83
  className={cn(
81
84
  // `text-fg-1`, а не `fg-2`: на подложке `bg-3` вторая ступень не держит 4.5 —
82
85
  // поймал аудит axe. Остаток «+N» это содержимое, а не подсказка.
83
- "bg-bg-3 text-fg-1 ring-bg-1 relative -ml-2 flex items-center justify-center rounded-full font-semibold ring-2",
86
+ "bg-bg-3 text-fg-1 ring-bg-1 relative flex items-center justify-center rounded-full font-semibold ring-2",
87
+ overlap,
84
88
  size === "lg" ? "text-md h-16 w-16" : "text-caption h-7 w-7"
85
89
  )}
86
90
  >
@@ -298,6 +298,13 @@ export function Table<T extends { id: Key }>({
298
298
  /** Сколько ячеек в строке — столько же обязана занять детальная. */
299
299
  const span = columns.length + (selectable ? 1 : 0) + (expandable ? 1 : 0);
300
300
 
301
+ /*
302
+ Ширина служебных колонок (выбор, раскрытие) идёт тем же путём, что и
303
+ `column.width`: `width` действует только внутри `ResizableTableContainer`,
304
+ вне его колонка растягивается наравне с данными, поэтому нужен CSS-стиль.
305
+ */
306
+ const serviceWidth = resizable ? { width: 40 } : { style: { width: 40 } };
307
+
301
308
  const table = (
302
309
  <AriaTable
303
310
  {...rest}
@@ -339,7 +346,7 @@ export function Table<T extends { id: Key }>({
339
346
  {selectable ? (
340
347
  <Column
341
348
  className="border-line-soft bg-bg-1 text-fg-2 border-b px-3 py-2 text-left font-semibold"
342
- width={40}
349
+ {...serviceWidth}
343
350
  >
344
351
  {/*
345
352
  Флажок «выделить всё» — родной RAC (`slot="selection"`), а не наш
@@ -354,7 +361,7 @@ export function Table<T extends { id: Key }>({
354
361
  {expandable ? (
355
362
  <Column
356
363
  id={`${COLUMN_KEY}od-expander`}
357
- width={40}
364
+ {...serviceWidth}
358
365
  className="border-line-soft bg-bg-1 border-b px-3 py-2"
359
366
  >
360
367
  {/*
@@ -401,7 +408,12 @@ export function Table<T extends { id: Key }>({
401
408
  />
402
409
  ) : null}
403
410
  {resizable ? (
404
- <ColumnResizer className="bg-line-soft hover:bg-accent-strong ml-auto w-px cursor-col-resize" />
411
+ <ColumnResizer
412
+ className={cn(
413
+ "bg-line-soft hover:bg-accent-strong w-px cursor-col-resize",
414
+ column.align !== "end" && "ml-auto"
415
+ )}
416
+ />
405
417
  ) : null}
406
418
  </span>
407
419
  )}
@@ -509,7 +509,17 @@ export function VirtualGrid({
509
509
  className="focus-visible:outline-accent-strong rounded-xl focus-visible:outline-2 focus-visible:outline-offset-2"
510
510
  >
511
511
  {/* `gridcell` у react-aria — `display: contents`: высоту держит обёртка. */}
512
- <div className="h-full" style={{ minHeight: minItemHeight }}>
512
+ {/* Обёртка — сетка из одной строки, а не блок с `min-height`: процентная высота
513
+ вложенного узла (`h-full`) от `min-height` не считается и схлопывается до
514
+ содержимого, а от строки сетки — считается. `items-start` оставляет ячейку без
515
+ `h-full` её естественной высоты. */}
516
+ <div
517
+ className="grid h-full grid-cols-1 items-start"
518
+ style={{
519
+ minHeight: minItemHeight,
520
+ gridTemplateRows: `minmax(${minItemHeight}px, 1fr)`,
521
+ }}
522
+ >
513
523
  {item.contextMenu != null ? (
514
524
  <CellContextMenu
515
525
  menu={item.contextMenu}
@@ -10,6 +10,7 @@ import {
10
10
  } from "react";
11
11
  import { CheckboxButton, CheckboxField, Text } from "react-aria-components";
12
12
  import { CheckIcon, MinusIcon } from "../../icons.js";
13
+ import { DIMMED_IN_DISABLED_FIELDSET } from "../../lib/choice-card.js";
13
14
  import { cn } from "../../lib/cn.js";
14
15
  import { hasContent } from "../../lib/empty.js";
15
16
  import { STATE_BORDER, TONE_TOKENS, type Size, type Tone } from "../../lib/variants.js";
@@ -239,6 +240,10 @@ export const Checkbox = forwardRef<HTMLDivElement, CheckboxProps>(function Check
239
240
  className={
240
241
  "hover:bg-bg-2 relative flex w-fit cursor-pointer items-center gap-2 rounded-sm px-1 py-0.5 " +
241
242
  "transition-colors disabled:cursor-default disabled:opacity-50 " +
243
+ // `disabled:` ловит только собственный `isDisabled`; выключенный предок-
244
+ // `fieldset` — см. `DIMMED_IN_DISABLED_FIELDSET`.
245
+ DIMMED_IN_DISABLED_FIELDSET +
246
+ " " +
242
247
  "before:absolute before:inset-x-0 before:bottom-full before:h-0.5 before:content-[''] after:absolute after:inset-x-0 after:top-full after:h-0.5 after:content-['']"
243
248
  }
244
249
  >
@@ -6,6 +6,7 @@ import { CheckIcon, MinusIcon } from "../../icons.js";
6
6
  import { cn } from "../../lib/cn.js";
7
7
  import {
8
8
  CHOICE_CARD_BASE,
9
+ CHOICE_CARD_DIMMED_IN_DISABLED_FIELDSET,
9
10
  CHOICE_CARD_FOCUS_RING,
10
11
  CHOICE_CARD_IDLE_BORDER,
11
12
  CHOICE_CARD_INVALID_BORDER,
@@ -82,6 +83,7 @@ export const CheckboxCard: CheckboxCardComponent = forwardRef<
82
83
  ? CHOICE_CARD_SELECTED_BORDER[tone]
83
84
  : CHOICE_CARD_IDLE_BORDER,
84
85
  isDisabled ? "opacity-50" : "hover:bg-bg-3",
86
+ CHOICE_CARD_DIMMED_IN_DISABLED_FIELDSET,
85
87
  orientation === "horizontal" && CHOICE_CARD_ROW,
86
88
  className
87
89
  )
@@ -14,6 +14,8 @@ import { hasContent } from "../../lib/empty.js";
14
14
  import { ChoiceCardBody } from "./ChoiceCardBody.js";
15
15
  import {
16
16
  CHOICE_CARD_BASE,
17
+ DIMMED_IN_DISABLED_FIELDSET,
18
+ CHOICE_CARD_DIMMED_IN_DISABLED_FIELDSET,
17
19
  CHOICE_CARD_FOCUS_RING,
18
20
  CHOICE_CARD_IDLE_BORDER,
19
21
  CHOICE_CARD_INVALID_BORDER,
@@ -214,6 +216,7 @@ export const Radio = forwardRef<HTMLDivElement, RadioProps>(function Radio(
214
216
  ? CHOICE_CARD_SELECTED_BORDER[tone]
215
217
  : CHOICE_CARD_IDLE_BORDER,
216
218
  isDisabled ? "opacity-50" : "hover:bg-bg-3",
219
+ CHOICE_CARD_DIMMED_IN_DISABLED_FIELDSET,
217
220
  orientation === "horizontal" && CHOICE_CARD_ROW,
218
221
  className
219
222
  )
@@ -247,7 +250,13 @@ export const Radio = forwardRef<HTMLDivElement, RadioProps>(function Radio(
247
250
  isDisabled={disabled}
248
251
  className={cn("flex flex-col", className)}
249
252
  >
250
- <RadioButton className="hover:bg-bg-2 flex w-fit cursor-pointer items-center gap-2 rounded-sm px-1 py-0.5 transition-colors disabled:cursor-default disabled:opacity-50">
253
+ <RadioButton
254
+ className={
255
+ "hover:bg-bg-2 flex w-fit cursor-pointer items-center gap-2 rounded-sm px-1 py-0.5 transition-colors " +
256
+ "disabled:cursor-default disabled:opacity-50 " +
257
+ DIMMED_IN_DISABLED_FIELDSET
258
+ }
259
+ >
251
260
  {({ isSelected, isInvalid, isFocusVisible }) => (
252
261
  <>
253
262
  <RadioBox selected={isSelected} invalid={isInvalid} focusVisible={isFocusVisible} />