@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
@@ -15,7 +15,7 @@ import { LISTBOX_ITEM_CLASS } from "../../lib/menu.js";
15
15
  import { FIELD_CLEAR_CLASS } from "../../lib/field.js";
16
16
  import { useMessages } from "../../i18n/useMessages.js";
17
17
  import { CheckIcon, CloseIcon, SelectIcon } from "../../icons.js";
18
- import { Popover, PopoverContent, PopoverTrigger } from "../overlays/Popover.js";
18
+ import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../overlays/Popover.js";
19
19
  import { VisuallyHidden } from "../content/VisuallyHidden.js";
20
20
 
21
21
  /** Минимум, который список требует от элемента. Остальные поля — ваши. */
@@ -502,41 +502,47 @@ export function Select<T extends SelectItem>(props: SelectProps<T>) {
502
502
  Кольцо фокуса рисуется на псевдоэлементе, а не на кнопке: обводить надо
503
503
  пилюлю целиком, иначе она у`же собственного контура.
504
504
  */}
505
- <span
506
- className={cn(
507
- "relative inline-flex items-center gap-2 rounded-full border px-3 py-[7px]",
508
- // `leading-5` задаёт высоту, а не наследует её: строчный бокс в 20px
509
- // плюс отступы дают ровно 36 — рост `Button` размера `md`, рядом с
510
- // которым пилюля и стоит в панели фильтров. С отступами формы
511
- // (`py-2.5` от потребителя) те же 20 дают 42 — рост `TextControl`
512
- // и `NumberInput` (см. `lib/field.ts`).
513
- //
514
- // Картинка выбранного значения тоже 20, и это условие, а не совпадение:
515
- // содержимое выше строчного бокса растягивало бы триггер. Пока она
516
- // была 22, пилюля с картинкой вырастала до 38, а триггер формы — до 44,
517
- // и контрол расходился и с соседним полем, и сам с собой в пустом
518
- // состоянии.
519
- // `transition-colors`, а не `transition-all`: на этом боксе меняются
520
- // только рамка, фон и цвет подписи — по ветке `hasSelection`. `all`
521
- // обещал переход всему, чего на пилюле нет, и заодно тому, что
522
- // потребитель пришлёт через `triggerClassName`. Заменено в волне 27
523
- // при ревизии движения: класс выглядел перемещающим, а перемещать
524
- // здесь нечего.
525
- "text-[13px] leading-5 font-semibold transition-colors",
526
- hasSelection && highlightSelected
527
- ? "border-accent-strong bg-accent-soft text-fg-0"
528
- : "border-line-soft text-fg-1 hover:border-line hover:bg-bg-2 bg-transparent",
529
- // Выбранное значение читается тёмным даже без акцентной подсветки:
530
- // подпись — это содержимое контрола, а не подсказка.
531
- hasSelection && !highlightSelected && "text-fg-0",
532
- // Гаснет ПИЛЮЛЯ, а не кнопка внутри неё: рамка и фон нарисованы здесь,
533
- // и приглушённая подпись рядом с яркой рамкой выглядела бы сбоем
534
- // отрисовки, а не выключенным полем.
535
- disabled && "cursor-not-allowed opacity-50",
536
- triggerClassName
537
- )}
538
- >
539
- {/*
505
+ {/*
506
+ Якорь панели — вся пилюля, а не кнопка-триггер внутри неё: кнопка отступает
507
+ от нижней кромки на рамку и `py-[7px]`, и панель с `sideOffset` в 4px
508
+ заходила на пилюлю вместо зазора.
509
+ */}
510
+ <PopoverAnchor asChild>
511
+ <span
512
+ className={cn(
513
+ "relative inline-flex items-center gap-2 rounded-full border px-3 py-[7px]",
514
+ // `leading-5` задаёт высоту, а не наследует её: строчный бокс в 20px
515
+ // плюс отступы дают ровно 36 — рост `Button` размера `md`, рядом с
516
+ // которым пилюля и стоит в панели фильтров. С отступами формы
517
+ // (`py-2.5` от потребителя) те же 20 дают 42 — рост `TextControl`
518
+ // и `NumberInput` (см. `lib/field.ts`).
519
+ //
520
+ // Картинка выбранного значения тоже 20, и это условие, а не совпадение:
521
+ // содержимое выше строчного бокса растягивало бы триггер. Пока она
522
+ // была 22, пилюля с картинкой вырастала до 38, а триггер формы — до 44,
523
+ // и контрол расходился и с соседним полем, и сам с собой в пустом
524
+ // состоянии.
525
+ // `transition-colors`, а не `transition-all`: на этом боксе меняются
526
+ // только рамка, фон и цвет подписи — по ветке `hasSelection`. `all`
527
+ // обещал переход всему, чего на пилюле нет, и заодно тому, что
528
+ // потребитель пришлёт через `triggerClassName`. Заменено в волне 27
529
+ // при ревизии движения: класс выглядел перемещающим, а перемещать
530
+ // здесь нечего.
531
+ "text-[13px] leading-5 font-semibold transition-colors",
532
+ hasSelection && highlightSelected
533
+ ? "border-accent-strong bg-accent-soft text-fg-0"
534
+ : "border-line-soft text-fg-1 hover:border-line hover:bg-bg-2 bg-transparent",
535
+ // Выбранное значение читается тёмным даже без акцентной подсветки:
536
+ // подпись — это содержимое контрола, а не подсказка.
537
+ hasSelection && !highlightSelected && "text-fg-0",
538
+ // Гаснет ПИЛЮЛЯ, а не кнопка внутри неё: рамка и фон нарисованы здесь,
539
+ // и приглушённая подпись рядом с яркой рамкой выглядела бы сбоем
540
+ // отрисовки, а не выключенным полем.
541
+ disabled && "cursor-not-allowed opacity-50",
542
+ triggerClassName
543
+ )}
544
+ >
545
+ {/*
540
546
  Подпись видна только скринридеру и стоит ВНЕ кнопки: внутри неё она
541
547
  попала бы в содержимое, то есть прочиталась бы дважды — один раз как
542
548
  имя, второй как значение.
@@ -544,46 +550,46 @@ export function Select<T extends SelectItem>(props: SelectProps<T>) {
544
550
  `elementType="span"`: по умолчанию react-aria рисует `<div>`, а здесь
545
551
  он оказался бы внутри строчной пилюли.
546
552
  */}
547
- {ariaLabel && !ariaLabelledBy ? (
548
- <VisuallyHidden elementType="span" id={labelId}>
549
- {ariaLabel}
550
- </VisuallyHidden>
551
- ) : null}
552
-
553
- <PopoverTrigger asChild>
554
- <button
555
- type="button"
556
- disabled={disabled}
557
- aria-haspopup="listbox"
558
- aria-expanded={open && !disabled}
559
- aria-labelledby={labelledBy ? `${labelledBy} ${valueId}` : undefined}
560
- aria-describedby={describedBy}
561
- className={cn(
562
- // `grow` + `justify-between`: когда потребитель растягивает пилюлю
563
- // (`w-full`, `min-w`), кнопка занимает всю ширину, и его
564
- // `justify-between` разводит иконку и подпись — как было до выноса.
565
- // На пилюле по размеру содержимого свободного места нет, и оба
566
- // класса ничего не меняют.
567
- "inline-flex min-w-0 grow cursor-pointer items-center justify-between gap-2 text-left",
568
- "disabled:cursor-not-allowed",
569
- "after:absolute after:inset-0 after:rounded-[inherit]",
570
- "focus-visible:after:outline-accent-strong focus-visible:after:outline-2 focus-visible:after:outline-offset-2",
571
- // Место под крестик — на кнопке, а не на пилюле: пилюля уже несёт
572
- // отступы потребителя, и добавка к ним сместила бы рамку.
573
- hasSelection && clearable && !disabled && "pr-6"
574
- )}
575
- >
576
- {/*
553
+ {ariaLabel && !ariaLabelledBy ? (
554
+ <VisuallyHidden elementType="span" id={labelId}>
555
+ {ariaLabel}
556
+ </VisuallyHidden>
557
+ ) : null}
558
+
559
+ <PopoverTrigger asChild>
560
+ <button
561
+ type="button"
562
+ disabled={disabled}
563
+ aria-haspopup="listbox"
564
+ aria-expanded={open && !disabled}
565
+ aria-labelledby={labelledBy ? `${labelledBy} ${valueId}` : undefined}
566
+ aria-describedby={describedBy}
567
+ className={cn(
568
+ // `grow` + `justify-between`: когда потребитель растягивает пилюлю
569
+ // (`w-full`, `min-w`), кнопка занимает всю ширину, и его
570
+ // `justify-between` разводит иконку и подпись — как было до выноса.
571
+ // На пилюле по размеру содержимого свободного места нет, и оба
572
+ // класса ничего не меняют.
573
+ "inline-flex min-w-0 grow cursor-pointer items-center justify-between gap-2 text-left",
574
+ "disabled:cursor-not-allowed",
575
+ "after:absolute after:inset-0 after:rounded-[inherit]",
576
+ "focus-visible:after:outline-accent-strong focus-visible:after:outline-2 focus-visible:after:outline-offset-2",
577
+ // Место под крестик — на кнопке, а не на пилюле: пилюля уже несёт
578
+ // отступы потребителя, и добавка к ним сместила бы рамку.
579
+ hasSelection && clearable && !disabled && "pr-6"
580
+ )}
581
+ >
582
+ {/*
577
583
  Содержимое собрано в одну группу, а не выложено соседями кнопки.
578
584
  Иначе `justify-between` разводит по краям именно их: в пилюле по
579
585
  ширине содержимого это незаметно, а в широком триггере (`w-full`
580
586
  в форме) картинка прилипает к левому краю, подпись — к правому,
581
587
  и между ними дыра во всю строку.
582
588
  */}
583
- {selected ? (
584
- <span id={valueId} className="inline-flex min-w-0 items-center gap-2">
585
- {renderMedia?.(selected, 20)}
586
- {/*
589
+ {selected ? (
590
+ <span id={valueId} className="inline-flex min-w-0 items-center gap-2">
591
+ {renderMedia?.(selected, 20)}
592
+ {/*
587
593
  Ширина подписи ограничена переменной, а не жёстким числом.
588
594
  В пилюле фильтра предел нужен: она растёт по содержимому, и
589
595
  «Абстрактные градиенты» растолкали бы соседние фильтры. В
@@ -592,10 +598,10 @@ export function Select<T extends SelectItem>(props: SelectProps<T>) {
592
598
  Снимается это классом на триггере:
593
599
  `triggerClassName="[--select-label-max:none]"`.
594
600
  */}
595
- <span className={LABEL_CLASS}>{selected.label}</span>
596
- </span>
597
- ) : hasSelection && props.multiple ? (
598
- /*
601
+ <span className={LABEL_CLASS}>{selected.label}</span>
602
+ </span>
603
+ ) : hasSelection && props.multiple ? (
604
+ /*
599
605
  Счётчик — СТРОГО за множественным выбором.
600
606
 
601
607
  Ветка ловила и одиночный `Select`, у которого значение есть, а
@@ -605,13 +611,13 @@ export function Select<T extends SelectItem>(props: SelectProps<T>) {
605
611
  нечего. Одиночный случай уходит теперь в ветку ниже и
606
612
  показывает сам ключ, как это делает `captionOf` в `TreeSelect`.
607
613
  */
608
- <span id={valueId} className={LABEL_CLASS}>
609
- {countLabel
610
- ? countLabel(selectedIds.length)
611
- : `${placeholder}: ${selectedIds.length}`}
612
- </span>
613
- ) : hasSelection ? (
614
- /*
614
+ <span id={valueId} className={LABEL_CLASS}>
615
+ {countLabel
616
+ ? countLabel(selectedIds.length)
617
+ : `${placeholder}: ${selectedIds.length}`}
618
+ </span>
619
+ ) : hasSelection ? (
620
+ /*
615
621
  Одиночный выбор, которому не нашлось элемента.
616
622
 
617
623
  Ключ ЧЕСТНЕЕ плейсхолдера, когда он человекочитаем
@@ -622,36 +628,37 @@ export function Select<T extends SelectItem>(props: SelectProps<T>) {
622
628
  умолчанию показывается ключ, как это делает `captionOf` в
623
629
  `TreeSelect`.
624
630
  */
625
- <span id={valueId} className={LABEL_CLASS}>
626
- {missingLabel ? missingLabel(selectedIds[0]!) : selectedIds[0]}
627
- </span>
628
- ) : (
629
- /*
631
+ <span id={valueId} className={LABEL_CLASS}>
632
+ {missingLabel ? missingLabel(selectedIds[0]!) : selectedIds[0]}
633
+ </span>
634
+ ) : (
635
+ /*
630
636
  `id` значения стоит на КОРНЕ КАЖДОЙ ветки, а не на общей обёртке
631
637
  вокруг них: обёртка — лишний бокс во флексе триггера, и она
632
638
  сместила бы содержимое там, где менять его никто не просил.
633
639
  */
634
- <span id={valueId} className="inline-flex min-w-0 items-center gap-2">
635
- <span aria-hidden className="inline-flex shrink-0 items-center opacity-60">
636
- {icon ?? <SelectIcon size={14} />}
640
+ <span id={valueId} className="inline-flex min-w-0 items-center gap-2">
641
+ <span aria-hidden className="inline-flex shrink-0 items-center opacity-60">
642
+ {icon ?? <SelectIcon size={14} />}
643
+ </span>
644
+ <span className="truncate">{placeholder}</span>
637
645
  </span>
638
- <span className="truncate">{placeholder}</span>
639
- </span>
640
- )}
641
- </button>
642
- </PopoverTrigger>
643
-
644
- {hasSelection && clearable && !disabled ? (
645
- <button
646
- type="button"
647
- aria-label={clearLabel ?? messages.common.clear}
648
- onClick={clear}
649
- className={FIELD_CLEAR_CLASS}
650
- >
651
- <CloseIcon size={10} aria-hidden />
652
- </button>
653
- ) : null}
654
- </span>
646
+ )}
647
+ </button>
648
+ </PopoverTrigger>
649
+
650
+ {hasSelection && clearable && !disabled ? (
651
+ <button
652
+ type="button"
653
+ aria-label={clearLabel ?? messages.common.clear}
654
+ onClick={clear}
655
+ className={FIELD_CLEAR_CLASS}
656
+ >
657
+ <CloseIcon size={10} aria-hidden />
658
+ </button>
659
+ ) : null}
660
+ </span>
661
+ </PopoverAnchor>
655
662
 
656
663
  <PopoverContent
657
664
  className={cn("w-[260px]", contentClassName)}
@@ -64,15 +64,19 @@ export type RangeSliderProps = SliderLookProps &
64
64
  `State=disabled` в Figma. `disabled:` здесь срабатывает на `data-disabled`,
65
65
  который react-aria ставит на корень. Раньше вида не было вовсе —
66
66
  отличался только курсор над бегунком.
67
+
68
+ `has-[input:disabled]:` — для ползунка внутри `<fieldset disabled>` (так `Form` с
69
+ `pending` гасит поля): react-aria о таком предке не знает и `data-disabled`
70
+ на корне не ставит, а нативное состояние видит только скрытый `input` бегунка.
67
71
  */
68
- const DIMMED_WHEN_DISABLED = "disabled:opacity-50";
72
+ const DIMMED_WHEN_DISABLED = "disabled:opacity-50 has-[input:disabled]:opacity-50";
69
73
  const ROOT_CLASS = `flex w-full flex-col gap-2 ${DIMMED_WHEN_DISABLED}`;
70
74
  const TRACK_CLASS = "bg-bg-3 relative h-1.5 w-full rounded-full";
71
75
  const FILL_CLASS = "bg-accent absolute h-full rounded-full";
72
76
  const THUMB_BASE_CLASS =
73
77
  "border-accent bg-bg-1 size-4 cursor-grab rounded-full border-2 shadow-[var(--shadow-1)] transition-shadow " +
74
78
  "dragging:cursor-grabbing focus-visible:outline-accent-strong focus-visible:outline-2 focus-visible:outline-offset-2 " +
75
- "disabled:cursor-not-allowed";
79
+ "disabled:cursor-not-allowed has-[input:disabled]:cursor-not-allowed";
76
80
  // Вдоль оси бегунок ставит react-aria (`left` или `top`), поперёк — мы.
77
81
  const THUMB_CLASS = `${THUMB_BASE_CLASS} top-1/2`;
78
82
 
@@ -91,7 +91,10 @@ export function AccordionItem({ id, title, disabled, className, children }: Acco
91
91
  />
92
92
  </AriaButton>
93
93
  </AriaHeading>
94
- <DisclosurePanel className="text-fg-1 pb-3 text-sm">{children}</DisclosurePanel>
94
+ {/* Отступ только у раскрытой панели: свёрнутая остаётся в потоке, и её `pb-3` добавлял 12px. */}
95
+ <DisclosurePanel className={cn("text-fg-1 text-sm", isExpanded && "pb-3")}>
96
+ {children}
97
+ </DisclosurePanel>
95
98
  </>
96
99
  )}
97
100
  </Disclosure>
@@ -165,57 +165,78 @@ export function TreeView({
165
165
  className="outline-none [--tree-indent:1.25rem]"
166
166
  >
167
167
  <TreeItemContent>
168
- {({ isExpanded, hasChildItems }) => (
169
- <div
170
- ref={(content) => markCurrent(content, current)}
171
- className={cn(
172
- "text-fg-1 flex items-center gap-2 rounded-xs py-1.5 pr-2 text-[13px] leading-5",
173
- "hover:bg-bg-2 selected:bg-accent-soft selected:text-fg-0",
174
- // Текущий узел — подложка и вес шрифта, как у `NavItem` с `active`.
175
- current && "bg-bg-3 text-fg-0 font-semibold",
176
- "focus-visible:outline-accent-strong focus-visible:outline-2 focus-visible:-outline-offset-2",
177
- "disabled:opacity-50"
178
- )}
179
- // Уровень вложенности берётся у react-aria: она кладёт его в
180
- // `--tree-item-level` на строке, и считать глубину самим не нужно
181
- // — иначе у нас была бы вторая копия того же числа.
182
- style={{
183
- paddingLeft: "calc((var(--tree-item-level) - 1) * var(--tree-indent) + 0.25rem)",
184
- }}
185
- >
186
- {hasChildItems ? (
187
- <Button
188
- slot="chevron"
189
- aria-label={isExpanded ? messages.treeView.collapse : messages.treeView.expand}
190
- className={cn(
191
- "text-fg-3 hover:text-fg-0 flex cursor-pointer items-center rounded-xs",
192
- "transition-colors",
193
- "focus-visible:outline-accent-strong focus-visible:outline-2 focus-visible:outline-offset-2"
194
- )}
195
- >
196
- <ChevronRightIcon
197
- size={14}
198
- aria-hidden
168
+ {({ isExpanded, hasChildItems, isSelected, isDisabled }) => {
169
+ // Гасятся шеврон, иконка и подпись по отдельности, а не весь `div`:
170
+ // флажок выбора получает блокировку от строки сам и гасится собственным
171
+ // `disabled:opacity-50`, так что приглушение всего ряда дало бы ему
172
+ // двойное — 25% вместо 50%.
173
+ const dim = isDisabled && "opacity-50";
174
+ return (
175
+ <div
176
+ ref={(content) => markCurrent(content, current)}
177
+ className={cn(
178
+ "text-fg-1 flex items-center gap-2 rounded-xs py-1.5 pr-2 text-[13px] leading-5",
179
+ // Выбор и блокировка берутся из рендер-пропа, а не вариантами
180
+ // `selected:` и `disabled:`: `data-selected` и `data-disabled`
181
+ // react-aria ставит на строку (`row`), а красится её содержимое —
182
+ // вариант на нём не срабатывал, и ни выбранный узел не
183
+ // подсвечивался, ни заблокированный не гас. Тот же случай, что
184
+ // шеврон в `Accordion`.
185
+ //
186
+ // Приглушение — не на всём `div`, а на частях (см. `dim` ниже).
187
+ //
188
+ // Текущий узел — подложка и вес шрифта, как у `NavItem` с `active`.
189
+ // Стоит ДО выбора: `cn` оставляет последнюю подложку, и у узла,
190
+ // который и текущий, и выбран, видна подложка выбора — как было,
191
+ // пока порядок решал каскад вариантов.
192
+ current && "bg-bg-3 text-fg-0 font-semibold",
193
+ isSelected ? "bg-accent-soft text-fg-0" : !isDisabled && "hover:bg-bg-2",
194
+ "focus-visible:outline-accent-strong focus-visible:outline-2 focus-visible:-outline-offset-2"
195
+ )}
196
+ // Уровень вложенности берётся у react-aria: она кладёт его в
197
+ // `--tree-item-level` на строке, и считать глубину самим не нужно
198
+ // — иначе у нас была бы вторая копия того же числа.
199
+ style={{
200
+ paddingLeft: "calc((var(--tree-item-level) - 1) * var(--tree-indent) + 0.25rem)",
201
+ }}
202
+ >
203
+ {hasChildItems ? (
204
+ <Button
205
+ slot="chevron"
206
+ aria-label={isExpanded ? messages.treeView.collapse : messages.treeView.expand}
199
207
  className={cn(
200
- "transition-transform motion-reduce:transition-none",
201
- isExpanded && "rotate-90"
208
+ "text-fg-3 hover:text-fg-0 flex cursor-pointer items-center rounded-xs",
209
+ "transition-colors",
210
+ dim,
211
+ "focus-visible:outline-accent-strong focus-visible:outline-2 focus-visible:outline-offset-2"
202
212
  )}
203
- />
204
- </Button>
205
- ) : (
206
- // Место раскрывателя занято всегда: без него подписи листьев
207
- // разъезжаются с подписями узлов на том же уровне.
208
- <span aria-hidden className="w-[14px] shrink-0" />
209
- )}
210
- {selectionMode ? (
211
- <Checkbox slot="selection" label={messages.treeView.select} />
212
- ) : null}
213
- {node.icon != null && (
214
- <span className="text-fg-3 flex shrink-0 items-center">{node.icon}</span>
215
- )}
216
- <span className="min-w-0 truncate">{node.label}</span>
217
- </div>
218
- )}
213
+ >
214
+ <ChevronRightIcon
215
+ size={14}
216
+ aria-hidden
217
+ className={cn(
218
+ "transition-transform motion-reduce:transition-none",
219
+ isExpanded && "rotate-90"
220
+ )}
221
+ />
222
+ </Button>
223
+ ) : (
224
+ // Место раскрывателя занято всегда: без него подписи листьев
225
+ // разъезжаются с подписями узлов на том же уровне.
226
+ <span aria-hidden className="w-[14px] shrink-0" />
227
+ )}
228
+ {selectionMode ? (
229
+ <Checkbox slot="selection" label={messages.treeView.select} />
230
+ ) : null}
231
+ {node.icon != null && (
232
+ <span className={cn("text-fg-3 flex shrink-0 items-center", dim)}>
233
+ {node.icon}
234
+ </span>
235
+ )}
236
+ <span className={cn("min-w-0 truncate", dim)}>{node.label}</span>
237
+ </div>
238
+ );
239
+ }}
219
240
  </TreeItemContent>
220
241
  {node.children != null && node.children.length > 0 ? (
221
242
  <Collection items={node.children}>{renderNode}</Collection>
@@ -67,7 +67,7 @@ import {
67
67
  } from "./core/jsonDocument.js";
68
68
  import { markdownOf } from "./core/atomText.js";
69
69
  import { countCharacters, EXTERNAL_CHANGE } from "./core/maxLength.js";
70
- import { proseClass } from "./styles/prose.js";
70
+ import { PAPER_SHEET_CLASS, proseClass } from "./styles/prose.js";
71
71
  import { SELECTION_BUBBLE_KEY, SelectionBubble } from "./ui/SelectionBubble.js";
72
72
  import { useSlashMenu } from "./ui/SlashMenu.js";
73
73
  import { useTriggerMenu } from "./ui/TriggerMenu.js";
@@ -1543,8 +1543,11 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
1543
1543
  {...surfaceAttributes(surface)}
1544
1544
  className={cn(
1545
1545
  "grid",
1546
- surface === "paper" ? EDITOR_PAPER_TOKENS_CLASS : EDITOR_TOKENS_CLASS,
1547
- surface === "paper" ? "my-3" : "py-2.5"
1546
+ // На листе светлый лист остаётся под тёмным полем: смена режима
1547
+ // меняет содержимое листа, а не сам лист.
1548
+ surface === "paper"
1549
+ ? cn(EDITOR_PAPER_TOKENS_CLASS, PAPER_SHEET_CLASS, "my-3")
1550
+ : cn(EDITOR_TOKENS_CLASS, "py-2.5")
1548
1551
  )}
1549
1552
  style={{ minHeight: source.minHeight }}
1550
1553
  >
@@ -415,7 +415,7 @@ export const PROSE_CLASS = EDITOR_TOKENS_CLASS + " " + PROSE_RULES;
415
415
  * видны на тёмной подложке так же, как на светлой. `scheme-light` — родной
416
416
  * флажок чек-листа и полосы прокрутки рисуются светлыми.
417
417
  */
418
- const PAPER_SHEET_CLASS =
418
+ export const PAPER_SHEET_CLASS =
419
419
  "rounded-(--r-sm) border border-(--line) bg-(--rte-background) px-4 py-3 " +
420
420
  "shadow-[var(--shadow-1)] scheme-light";
421
421
 
@@ -13,6 +13,7 @@ import {
13
13
  } from "../../components/overlays/Menu.js";
14
14
  import { Tooltip } from "../../components/overlays/Tooltip.js";
15
15
  import type { UiMessages } from "../../i18n/messages.js";
16
+ import { cn } from "../../lib/cn.js";
16
17
  import { useMessages, useUiLocale } from "../../i18n/useMessages.js";
17
18
  import {
18
19
  AlignCenterIcon,
@@ -227,7 +228,21 @@ export function formatMenuSections(
227
228
  icon={ItemIcon ? <ItemIcon size={14} aria-hidden /> : undefined}
228
229
  shortcut={item.keys ? shortcut(item.keys) : undefined}
229
230
  >
230
- {item.swatch ? <span className={item.swatch}>{item.label}</span> : item.label}
231
+ {item.swatch ? (
232
+ /*
233
+ Образец не должен брать вид выбранной строки: её голубой текст
234
+ и подложка перекрашивали цвета тона. Текст задан образцу
235
+ самому, а непрозрачная основа под ним не даёт полупрозрачной
236
+ подложке тона смешаться с подложкой строки. Основа — только в
237
+ отмеченной строке: у остальных образец лежит на панели меню,
238
+ как обёртка в документе, и выглядит так же.
239
+ */
240
+ <span className="in-[[aria-checked=true]]:bg-surface-elevated rounded-xs">
241
+ <span className={cn(item.swatch, "text-fg-0")}>{item.label}</span>
242
+ </span>
243
+ ) : (
244
+ item.label
245
+ )}
231
246
  </MenuItem>
232
247
  );
233
248
  if (!item.submenu || item.disabled) return entry;
package/src/index.ts CHANGED
@@ -208,7 +208,7 @@ export type {
208
208
  MobileTabBarProps,
209
209
  } from "./components/app-shell/MobileTabBar.js";
210
210
 
211
- export { NavGroup, SideNav } from "./components/app-shell/SideNav.js";
211
+ export { NavGroup, SideNav, useNavCollapsed } from "./components/app-shell/SideNav.js";
212
212
  export type { NavGroupProps, SideNavProps } from "./components/app-shell/SideNav.js";
213
213
 
214
214
  export { DataTable } from "./components/table/DataTable.js";
@@ -50,6 +50,23 @@ export const CHOICE_CARD_INVALID_BORDER = "border-danger-strong";
50
50
  */
51
51
  export const CHOICE_CARD_BASE = "flex flex-col rounded-lg border bg-bg-2 transition-colors";
52
52
 
53
+ /**
54
+ * Выключенность от `<fieldset disabled>` (так `Form` с `pending` гасит поля).
55
+ *
56
+ * React-aria о таком предке не знает: `isDisabled` ложен, `data-disabled` не
57
+ * ставится, и `disabled:` не срабатывает. Нативное состояние видит только
58
+ * скрытый `input` контрола — поэтому `has-[input:disabled]`, а не голый
59
+ * `has-disabled`: тот ловил бы любой выключенный узел внутри, например кнопку в
60
+ * описании карточки, и гасил бы рабочий контрол. Hover гасится вместе с видом:
61
+ * выключенный контрол не должен отзываться на указатель.
62
+ */
63
+ export const DIMMED_IN_DISABLED_FIELDSET =
64
+ "has-[input:disabled]:cursor-default has-[input:disabled]:opacity-50 has-[input:disabled]:hover:bg-transparent";
65
+
66
+ /** То же для карточки: её подложка `bg-bg-2` при наведении не меняется. */
67
+ export const CHOICE_CARD_DIMMED_IN_DISABLED_FIELDSET =
68
+ "has-[input:disabled]:opacity-50 has-[input:disabled]:hover:bg-bg-2";
69
+
53
70
  /** Раскладка карточки в строке с переносом: растёт, но не уезжает в полосу. */
54
71
  export const CHOICE_CARD_ROW = "max-w-xs flex-1 basis-48";
55
72