endfield-vision 2.1.0 → 2.2.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 (60) hide show
  1. package/README.md +31 -9
  2. package/README.ru.md +31 -9
  3. package/dist/components/CommandPalette.d.ts +24 -1
  4. package/dist/components/CommandPalette.d.ts.map +1 -1
  5. package/dist/components/CommandPalette.js +22 -10
  6. package/dist/components/CommandPalette.js.map +1 -1
  7. package/dist/components/DataTable.d.ts +38 -1
  8. package/dist/components/DataTable.d.ts.map +1 -1
  9. package/dist/components/DataTable.js +135 -25
  10. package/dist/components/DataTable.js.map +1 -1
  11. package/dist/components/Modal.d.ts +26 -0
  12. package/dist/components/Modal.d.ts.map +1 -1
  13. package/dist/components/Modal.js +92 -0
  14. package/dist/components/Modal.js.map +1 -1
  15. package/dist/components/Toast.d.ts +3 -0
  16. package/dist/components/Toast.d.ts.map +1 -1
  17. package/dist/components/Toast.js +5 -1
  18. package/dist/components/Toast.js.map +1 -1
  19. package/dist/index.d.ts +4 -3
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/lib/accent-preset.d.ts +40 -0
  24. package/dist/lib/accent-preset.d.ts.map +1 -0
  25. package/dist/lib/accent-preset.js +71 -0
  26. package/dist/lib/accent-preset.js.map +1 -0
  27. package/dist/lib/i18n-messages.d.ts +7 -0
  28. package/dist/lib/i18n-messages.d.ts.map +1 -1
  29. package/dist/lib/i18n-messages.js +10 -2
  30. package/dist/lib/i18n-messages.js.map +1 -1
  31. package/dist/lib/theme-script.d.ts +10 -4
  32. package/dist/lib/theme-script.d.ts.map +1 -1
  33. package/dist/lib/theme-script.js +8 -3
  34. package/dist/lib/theme-script.js.map +1 -1
  35. package/dist/lib/theme.d.ts +10 -6
  36. package/dist/lib/theme.d.ts.map +1 -1
  37. package/dist/lib/theme.js +1 -1
  38. package/dist/lib/theme.js.map +1 -1
  39. package/dist/styles/accents.css +6 -6
  40. package/dist/styles/components/data.css +120 -0
  41. package/dist/styles/components/navigation.css +6 -0
  42. package/dist/styles/components/overlay.css +43 -0
  43. package/dist/styles/shell.css +2 -0
  44. package/dist/styles/tokens.css +27 -27
  45. package/package.json +1 -1
  46. package/src/components/CommandPalette.tsx +68 -10
  47. package/src/components/DataTable.tsx +258 -41
  48. package/src/components/Modal.tsx +148 -1
  49. package/src/components/Toast.tsx +21 -1
  50. package/src/index.ts +4 -0
  51. package/src/lib/accent-preset.tsx +109 -0
  52. package/src/lib/i18n-messages.ts +17 -3
  53. package/src/lib/theme-script.tsx +19 -5
  54. package/src/lib/theme.ts +13 -8
  55. package/src/styles/accents.css +6 -6
  56. package/src/styles/components/data.css +120 -0
  57. package/src/styles/components/navigation.css +6 -0
  58. package/src/styles/components/overlay.css +43 -0
  59. package/src/styles/shell.css +2 -0
  60. package/src/styles/tokens.css +27 -27
@@ -12,6 +12,8 @@ import { Button } from './Button'
12
12
  * четырёх карточек, остальные сворачиваются в строку «и ещё N».
13
13
  * Повторный показ с тем же id заменяет карточку - без дублей.
14
14
  * Действия: одно (action) - текстовая кнопка, несколько (actions) - строка кнопок.
15
+ * onClick - действие по клику на саму карточку (открыть диалог): заголовок
16
+ * становится кнопкой, растянутой на карточку, - с клавиатуры тоже доступно.
15
17
  */
16
18
 
17
19
  export type ToastTone = 'info' | 'success' | 'warning' | 'error'
@@ -35,6 +37,8 @@ export interface ToastOptions {
35
37
  action?: { label: string; onClick: () => void }
36
38
  /** Несколько действий - строка небольших кнопок («Открыть», «Отменить»). Вместе с action оно идёт первым. */
37
39
  actions?: ToastAction[]
40
+ /** Клик по карточке (не по её кнопкам): открыть диалог, запись. После него уведомление закрывается. */
41
+ onClick?: () => void
38
42
  }
39
43
 
40
44
  /** Карточка в хранилище. Экспортируется для тестов, в публичный API не входит. */
@@ -44,6 +48,7 @@ export interface ToastItem extends Required<Pick<ToastOptions, 'id' | 'tone'>> {
44
48
  duration: number | null
45
49
  action?: ToastOptions['action']
46
50
  actions?: ToastAction[]
51
+ onClick?: () => void
47
52
  createdAt: number
48
53
  }
49
54
 
@@ -74,6 +79,7 @@ function show(opts: ToastOptions): string {
74
79
  duration: opts.duration === undefined ? DEFAULT_DURATION[tone] : opts.duration,
75
80
  action: opts.action,
76
81
  actions: opts.actions && opts.actions.length > 0 ? opts.actions : undefined,
82
+ onClick: opts.onClick,
77
83
  createdAt: Date.now(),
78
84
  }
79
85
  const exists = items.some((t) => t.id === id)
@@ -166,6 +172,7 @@ export function ToastCard({ item }: { item: ToastItem }) {
166
172
  className="ev-toast ev-corners"
167
173
  data-corners="diagonal"
168
174
  data-tone={item.tone}
175
+ data-clickable={item.onClick ? '' : undefined}
169
176
  role={item.tone === 'error' ? 'alert' : 'status'}
170
177
  onMouseEnter={stop}
171
178
  onMouseLeave={start}
@@ -174,7 +181,20 @@ export function ToastCard({ item }: { item: ToastItem }) {
174
181
  {ICONS[item.tone]}
175
182
  </span>
176
183
  <div className="ev-toast-body">
177
- <div className="ev-toast-title">{item.title}</div>
184
+ {item.onClick ? (
185
+ <button
186
+ type="button"
187
+ className="ev-toast-title ev-toast-open"
188
+ onClick={() => {
189
+ item.onClick?.()
190
+ dismiss(item.id)
191
+ }}
192
+ >
193
+ {item.title}
194
+ </button>
195
+ ) : (
196
+ <div className="ev-toast-title">{item.title}</div>
197
+ )}
178
198
  {item.description ? <div className="ev-toast-desc">{item.description}</div> : null}
179
199
  {item.actions ? (
180
200
  <div className="ev-toast-actions">
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ export {
25
25
  themeBootstrap,
26
26
  ThemeScript,
27
27
  type Accent,
28
+ type AccentName,
28
29
  type Style,
29
30
  type Theme,
30
31
  type ThemePreference,
@@ -32,6 +33,7 @@ export {
32
33
  type ThemeScriptProps,
33
34
  } from './lib/theme-script'
34
35
  export { setAccent, setStyle, setTheme, useTheme, type ThemeState } from './lib/theme'
36
+ export { AccentStyles, accentPresetCss, type AccentColors, type AccentPreset, type AccentStylesProps } from './lib/accent-preset'
35
37
  export { LocaleProvider, useLocale, useMessages, useNumberFormat, type LocaleProviderProps } from './lib/i18n'
36
38
  export { en, MESSAGES, resolveMessages, ru, type Locale, type Messages, type MessagesOverride } from './lib/i18n-messages'
37
39
  export { LinkProvider, UiLink, useLinkComponent, type LinkComponent } from './lib/link'
@@ -163,6 +165,7 @@ export {
163
165
  type DataTableProps,
164
166
  type FilterBarProps,
165
167
  type PaginationProps,
168
+ type RowAttributes,
166
169
  type SortDir,
167
170
  type SortState,
168
171
  } from './components/DataTable'
@@ -181,6 +184,7 @@ export {
181
184
  type ModalProps,
182
185
  type ModalsApi,
183
186
  type ModalSize,
187
+ type PromptOptions,
184
188
  } from './components/Modal'
185
189
  export { toast, Toaster, ToastProvider, useToast, type ToastAction, type ToastApi, type ToastOptions, type ToastTone } from './components/Toast'
186
190
  export { Timeline, type TimelineItem, type TimelineProps } from './components/Timeline'
@@ -0,0 +1,109 @@
1
+ /*
2
+ * Свой пресет акцента. Модуль без 'use client': CSS пресета собирается
3
+ * строкой и вставляется в <head> серверным layout (ThemeScript accents или
4
+ * AccentStyles) - акцент приложения есть с первой отрисовки, как встроенные.
5
+ *
6
+ * Пресет задаёт те же токены, что styles/accents.css, для тёмной и светлой
7
+ * темы и обоих стилей (Slim - свои значения или те же). После этого имя
8
+ * работает в data-accent, ThemeScript defaultAccent, setAccent и
9
+ * useTheme<Accent | 'brand'>().
10
+ *
11
+ * Контраст - как у встроенных: text к --ev-surface-1 >= 4.5:1, onStrong к
12
+ * strong >= 4.5:1 в каждой теме и стиле.
13
+ */
14
+
15
+ import { normalizeHexColor } from './color'
16
+
17
+ /** Цвета акцента для одной темы. Все значения - #RRGGBB (или #RGB). */
18
+ export interface AccentColors {
19
+ /** Основной цвет: ссылки, активные элементы, мягкие заливки и кольцо фокуса (--ev-accent-rgb). */
20
+ accent: string
21
+ /** Текст акцентом на поверхности (--ev-accent-text). */
22
+ text: string
23
+ /** Заливка основной кнопки (--ev-accent-strong). */
24
+ strong: string
25
+ /** Заливка основной кнопки при наведении (--ev-accent-strong-hover). */
26
+ strongHover: string
27
+ /** Текст на заливке (--ev-text-on-accent). */
28
+ onStrong: string
29
+ }
30
+
31
+ export interface AccentPreset {
32
+ /** Имя для data-accent: латиница в нижнем регистре, цифры, дефис. Не 'indigo' (это базовый акцент). */
33
+ name: string
34
+ /** Образец для переключателей (--ev-accent-swatch-<name>): цвет или градиент. По умолчанию - accent тёмной темы. */
35
+ swatch?: string
36
+ dark: AccentColors
37
+ light: AccentColors
38
+ /** Значения для стиля Slim; без них - те же, что в стандартном стиле. */
39
+ slim?: { dark?: AccentColors; light?: AccentColors }
40
+ }
41
+
42
+ const LAYER_ORDER = '@layer ev.reset, ev.tokens, ev.base, ev.components, ev.shell, ev.styles, ev.utilities;'
43
+ const NAME = /^[a-z][a-z0-9-]*$/
44
+
45
+ function fail(message: string): never {
46
+ throw new Error(`endfield-vision: ${message}`)
47
+ }
48
+
49
+ function hex(preset: string, field: string, value: string): string {
50
+ const v = normalizeHexColor(value)
51
+ if (!v) fail(`accent "${preset}": ${field} must be a #RRGGBB color, got "${value}"`)
52
+ return v
53
+ }
54
+
55
+ function rgb(value: string): string {
56
+ const n = parseInt(value.slice(1), 16)
57
+ return `${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}`
58
+ }
59
+
60
+ function block(selector: string, preset: string, c: AccentColors): string {
61
+ const at = (field: keyof AccentColors) => hex(preset, field, c[field])
62
+ return [
63
+ `${selector} {`,
64
+ ` --ev-accent-rgb: ${rgb(at('accent'))};`,
65
+ ` --ev-accent-text: ${at('text')};`,
66
+ ` --ev-accent-strong: ${at('strong')};`,
67
+ ` --ev-accent-strong-hover: ${at('strongHover')};`,
68
+ ` --ev-text-on-accent: ${at('onStrong')};`,
69
+ '}',
70
+ ].join('\n')
71
+ }
72
+
73
+ /**
74
+ * CSS пресетов акцента: токены в слое ev.tokens. Селекторы с удвоенным
75
+ * :root сильнее встроенных - пресет может и переопределить встроенный
76
+ * (например, свой emerald), и не зависит от порядка стилей в <head>.
77
+ */
78
+ export function accentPresetCss(presets: AccentPreset | readonly AccentPreset[]): string {
79
+ const list = Array.isArray(presets) ? presets : [presets as AccentPreset]
80
+ const rules: string[] = []
81
+ for (const p of list) {
82
+ if (!NAME.test(p.name)) fail(`accent name "${p.name}" must match ${NAME}`)
83
+ if (p.name === 'indigo') fail('accent "indigo" is the base accent; override its tokens on :root instead')
84
+ const swatch = p.swatch ?? hex(p.name, 'dark.accent', p.dark.accent)
85
+ if (/[<>{};]/.test(swatch)) fail(`accent "${p.name}": swatch must be a plain CSS color or gradient`)
86
+ const at = `[data-accent='${p.name}']`
87
+ rules.push(
88
+ `:root:root {\n --ev-accent-swatch-${p.name}: ${swatch};\n}`,
89
+ block(`:root:root${at}`, p.name, p.dark),
90
+ block(`:root:root[data-theme='light']${at}`, p.name, p.light),
91
+ block(`:root:root[data-style='slim']${at}`, p.name, p.slim?.dark ?? p.dark),
92
+ block(`:root:root[data-style='slim'][data-theme='light']${at}`, p.name, p.slim?.light ?? p.light),
93
+ )
94
+ }
95
+ // Порядок слоёв повторяется: стиль может оказаться в <head> раньше стилей библиотеки.
96
+ return `${LAYER_ORDER}\n@layer ev.tokens {\n${rules.join('\n')}\n}`
97
+ }
98
+
99
+ export interface AccentStylesProps {
100
+ accents: readonly AccentPreset[]
101
+ /** nonce для строгой CSP (style-src 'nonce-...'). */
102
+ nonce?: string
103
+ }
104
+
105
+ /** <style> с пресетами акцента для <head>. Подходит для серверных компонентов. */
106
+ export function AccentStyles({ accents, nonce }: AccentStylesProps) {
107
+ if (accents.length === 0) return null
108
+ return <style nonce={nonce} data-ev-accents="" dangerouslySetInnerHTML={{ __html: accentPresetCss(accents) }} />
109
+ }
@@ -81,6 +81,10 @@ export interface Messages {
81
81
  selectAllOnPage: string
82
82
  selectRow: string
83
83
  selectAllRows: string
84
+ expandRow: string
85
+ collapseRow: string
86
+ /** Заголовок итоговой карточки на узком экране. */
87
+ total: string
84
88
  }
85
89
  pagination: {
86
90
  noRecords: string
@@ -107,7 +111,7 @@ export interface Messages {
107
111
  /** Названия стран в списке кодов телефона. */
108
112
  countries: Record<PhoneCountry, string>
109
113
  }
110
- modal: { actionFailed: string }
114
+ modal: { actionFailed: string; required: string }
111
115
  select: {
112
116
  placeholder: string
113
117
  multiPlaceholder: string
@@ -163,6 +167,8 @@ export interface Messages {
163
167
  hintNavigate: string
164
168
  hintSelect: string
165
169
  hintClose: string
170
+ /** Идёт поиск (результаты с сервера). */
171
+ searching: string
166
172
  }
167
173
  steps: {
168
174
  label: string
@@ -423,6 +429,9 @@ export const ru: Messages = {
423
429
  selectAllOnPage: 'Выбрать все на странице',
424
430
  selectRow: 'Выбрать строку',
425
431
  selectAllRows: 'Выбрать все строки на странице',
432
+ expandRow: 'Раскрыть строку',
433
+ collapseRow: 'Свернуть строку',
434
+ total: 'Итого',
426
435
  },
427
436
  pagination: {
428
437
  noRecords: 'Нет записей',
@@ -471,7 +480,7 @@ export const ru: Messages = {
471
480
  CN: 'Китай',
472
481
  },
473
482
  },
474
- modal: { actionFailed: 'Не удалось выполнить действие' },
483
+ modal: { actionFailed: 'Не удалось выполнить действие', required: 'Заполните поле' },
475
484
  select: {
476
485
  placeholder: 'Выберите',
477
486
  multiPlaceholder: 'Не выбрано',
@@ -525,6 +534,7 @@ export const ru: Messages = {
525
534
  hintNavigate: 'выбор',
526
535
  hintSelect: 'открыть',
527
536
  hintClose: 'закрыть',
537
+ searching: 'Поиск...',
528
538
  },
529
539
  steps: {
530
540
  label: 'Шаги',
@@ -771,6 +781,9 @@ export const en: Messages = {
771
781
  selectAllOnPage: 'Select all on this page',
772
782
  selectRow: 'Select row',
773
783
  selectAllRows: 'Select all rows on this page',
784
+ expandRow: 'Expand row',
785
+ collapseRow: 'Collapse row',
786
+ total: 'Total',
774
787
  },
775
788
  pagination: {
776
789
  noRecords: 'No records',
@@ -819,7 +832,7 @@ export const en: Messages = {
819
832
  CN: 'China',
820
833
  },
821
834
  },
822
- modal: { actionFailed: 'The action failed' },
835
+ modal: { actionFailed: 'The action failed', required: 'This field is required' },
823
836
  select: {
824
837
  placeholder: 'Select',
825
838
  multiPlaceholder: 'None selected',
@@ -873,6 +886,7 @@ export const en: Messages = {
873
886
  hintNavigate: 'navigate',
874
887
  hintSelect: 'open',
875
888
  hintClose: 'close',
889
+ searching: 'Searching...',
876
890
  },
877
891
  steps: {
878
892
  label: 'Progress',
@@ -1,3 +1,5 @@
1
+ import { AccentStyles, type AccentPreset } from './accent-preset'
2
+
1
3
  /*
2
4
  * Скрипт темы для <head>. Модуль без 'use client': серверный layout
3
5
  * вставляет строку как есть, до первой отрисовки - без вспышки другой темы.
@@ -8,8 +10,11 @@
8
10
 
9
11
  export type Theme = 'dark' | 'light'
10
12
  export type ThemePreference = Theme | 'system'
13
+ /** Встроенные пресеты акцента (styles/accents.css). */
11
14
  export const ACCENTS = ['indigo', 'amber', 'emerald', 'cyan', 'rose', 'violet', 'ink'] as const
12
15
  export type Accent = (typeof ACCENTS)[number]
16
+ /** Имя акцента: встроенный пресет или свой (AccentPreset, ThemeScript accents). */
17
+ export type AccentName = Accent | (string & {})
13
18
  /** Стили оформления: те же размеры и раскладка, другая подача. */
14
19
  export const STYLES = ['default', 'slim'] as const
15
20
  export type Style = (typeof STYLES)[number]
@@ -21,8 +26,8 @@ export const STYLE_STORAGE_KEY = 'ev-style'
21
26
  export interface ThemeScriptOptions {
22
27
  /** Тема без сохранённого выбора. По умолчанию - 'dark'. */
23
28
  defaultTheme?: ThemePreference
24
- /** Акцент без сохранённого выбора. По умолчанию - 'indigo'. */
25
- defaultAccent?: Accent
29
+ /** Акцент без сохранённого выбора: встроенный или свой. По умолчанию - 'indigo'. */
30
+ defaultAccent?: AccentName
26
31
  /** Стиль без сохранённого выбора. По умолчанию - 'default'. */
27
32
  defaultStyle?: Style
28
33
  themeKey?: string
@@ -44,11 +49,20 @@ export function themeBootstrap({
44
49
  }
45
50
 
46
51
  export interface ThemeScriptProps extends ThemeScriptOptions {
47
- /** nonce для строгой CSP (script-src 'nonce-...'): тот же, что у остальных скриптов страницы. */
52
+ /** nonce для строгой CSP (script-src / style-src 'nonce-...'): тот же, что у остальных скриптов страницы. */
48
53
  nonce?: string
54
+ /** Свои пресеты акцента: их токены вставляются рядом со скриптом (<style>). */
55
+ accents?: readonly AccentPreset[]
49
56
  }
50
57
 
51
58
  /** Скрипт для <head> корневого layout. Не требует клиента: подходит для серверных компонентов. */
52
- export function ThemeScript({ nonce, ...options }: ThemeScriptProps) {
53
- return <script nonce={nonce} dangerouslySetInnerHTML={{ __html: themeBootstrap(options) }} />
59
+ export function ThemeScript({ nonce, accents, ...options }: ThemeScriptProps) {
60
+ const script = <script nonce={nonce} dangerouslySetInnerHTML={{ __html: themeBootstrap(options) }} />
61
+ if (!accents || accents.length === 0) return script
62
+ return (
63
+ <>
64
+ <AccentStyles accents={accents} nonce={nonce} />
65
+ {script}
66
+ </>
67
+ )
54
68
  }
package/src/lib/theme.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  STYLE_STORAGE_KEY,
7
7
  THEME_STORAGE_KEY,
8
8
  type Accent,
9
+ type AccentName,
9
10
  type Style,
10
11
  type Theme,
11
12
  type ThemePreference,
@@ -49,8 +50,8 @@ function readPreference(): ThemePreference {
49
50
  return p === 'system' || p === 'light' ? p : p === 'dark' ? 'dark' : readTheme()
50
51
  }
51
52
 
52
- function readAccent(): Accent {
53
- return (document.documentElement.dataset.accent as Accent | undefined) ?? 'indigo'
53
+ function readAccent(): string {
54
+ return document.documentElement.dataset.accent ?? 'indigo'
54
55
  }
55
56
 
56
57
  function readStyle(): Style {
@@ -66,8 +67,8 @@ export function setTheme(pref: ThemePreference, { persist = true, storageKey = T
66
67
  emit()
67
68
  }
68
69
 
69
- /** Акцент из пресетов (styles/accents.css). 'indigo' - базовый, атрибут снимается. */
70
- export function setAccent(accent: Accent, { persist = true, storageKey = ACCENT_STORAGE_KEY } = {}): void {
70
+ /** Акцент: встроенный пресет (styles/accents.css) или свой (AccentPreset). 'indigo' - базовый, атрибут снимается. */
71
+ export function setAccent(accent: AccentName, { persist = true, storageKey = ACCENT_STORAGE_KEY } = {}): void {
71
72
  const root = document.documentElement
72
73
  if (accent === 'indigo') delete root.dataset.accent
73
74
  else root.dataset.accent = accent
@@ -84,22 +85,26 @@ export function setStyle(style: Style, { persist = true, storageKey = STYLE_STOR
84
85
  emit()
85
86
  }
86
87
 
87
- export interface ThemeState {
88
+ /**
89
+ * Состояние темы. A - имена акцентов приложения: по умолчанию встроенные;
90
+ * со своими пресетами - useTheme<Accent | 'brand'>().
91
+ */
92
+ export interface ThemeState<A extends string = Accent> {
88
93
  /** Действующая тема. */
89
94
  theme: Theme
90
95
  /** Выбор пользователя (может быть 'system'). */
91
96
  preference: ThemePreference
92
- accent: Accent
97
+ accent: A
93
98
  style: Style
94
99
  setTheme: typeof setTheme
95
100
  setAccent: typeof setAccent
96
101
  setStyle: typeof setStyle
97
102
  }
98
103
 
99
- export function useTheme(): ThemeState {
104
+ export function useTheme<A extends string = Accent>(): ThemeState<A> {
100
105
  const theme = useSyncExternalStore(subscribe, readTheme, () => 'dark' as Theme)
101
106
  const preference = useSyncExternalStore(subscribe, readPreference, () => 'dark' as ThemePreference)
102
- const accent = useSyncExternalStore(subscribe, readAccent, () => 'indigo' as Accent)
107
+ const accent = useSyncExternalStore(subscribe, readAccent, () => 'indigo') as A
103
108
  const style = useSyncExternalStore(subscribe, readStyle, () => 'default' as Style)
104
109
 
105
110
  // Режим 'system': следим за сменой темы ОС.
@@ -115,7 +115,7 @@
115
115
  }
116
116
 
117
117
  /*
118
- * Стиль Slim: те же пресеты, приглушённые под тёплые нейтрали.
118
+ * Стиль Slim: те же пресеты, приглушённые под нейтрали стиля.
119
119
  * indigo - без атрибута data-accent.
120
120
  */
121
121
  :root[data-style='slim']:not([data-accent]) {
@@ -167,11 +167,11 @@
167
167
  }
168
168
 
169
169
  :root[data-style='slim'][data-accent='ink'] {
170
- --ev-accent-rgb: 200, 196, 186;
171
- --ev-accent-text: #e2dfd8;
172
- --ev-accent-strong: #e8e6df;
173
- --ev-accent-strong-hover: #f4f2ec;
174
- --ev-text-on-accent: #1c1b18;
170
+ --ev-accent-rgb: 200, 198, 192;
171
+ --ev-accent-text: #e2e0db;
172
+ --ev-accent-strong: #e9e8e3;
173
+ --ev-accent-strong-hover: #f5f4f0;
174
+ --ev-text-on-accent: #181817;
175
175
  }
176
176
 
177
177
  :root[data-style='slim'][data-theme='light']:not([data-accent]) {
@@ -131,6 +131,20 @@
131
131
  box-shadow: inset 0 1px 0 var(--ev-selected-edge), inset 0 -1px 0 var(--ev-selected-edge);
132
132
  }
133
133
 
134
+ /* Тон строки (rowTone): тонированный фон, наведение - тинт поверх него. */
135
+ .ev-table tbody tr[data-tone],
136
+ .ev-table-card[data-tone] {
137
+ --ev-tone-row: color-mix(in srgb, var(--ev-tone-soft) 65%, transparent);
138
+ }
139
+
140
+ .ev-table tbody tr[data-tone] > td {
141
+ background: var(--ev-tone-row);
142
+ }
143
+
144
+ .ev-table tbody tr[data-tone]:hover > td {
145
+ background: linear-gradient(var(--ev-hover), var(--ev-hover)), var(--ev-tone-row);
146
+ }
147
+
134
148
  .ev-table tbody tr[data-selected] td {
135
149
  background: var(--ev-selected-bg-subtle);
136
150
  }
@@ -139,6 +153,89 @@
139
153
  color: var(--ev-text-muted);
140
154
  }
141
155
 
156
+ /* --- Раскрывающиеся строки --- */
157
+ .ev-table .ev-table-expand {
158
+ width: 40px;
159
+ padding-right: 0;
160
+ }
161
+
162
+ .ev-table-expand-btn {
163
+ display: inline-flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ width: 24px;
167
+ height: 24px;
168
+ margin: 0 -4px;
169
+ border-radius: var(--ev-radius-xs);
170
+ color: var(--ev-text-muted);
171
+ vertical-align: middle;
172
+ }
173
+
174
+ .ev-table-expand-btn:hover {
175
+ background: var(--ev-hover);
176
+ color: var(--ev-text);
177
+ }
178
+
179
+ .ev-table-expand-btn svg {
180
+ transition: transform var(--ev-dur-fast) var(--ev-ease);
181
+ }
182
+
183
+ .ev-table-expand-btn[aria-expanded='true'] svg {
184
+ transform: rotate(90deg);
185
+ }
186
+
187
+ .ev-table tbody tr[data-expanded]:not([data-tone], [data-selected]) > td {
188
+ background: var(--ev-tint-1);
189
+ }
190
+
191
+ .ev-table tbody tr[data-expanded] > td {
192
+ border-bottom-color: transparent;
193
+ }
194
+
195
+ .ev-table .ev-table-detail-row > td {
196
+ height: auto;
197
+ padding: 0;
198
+ white-space: normal;
199
+ background: var(--ev-tint-1);
200
+ }
201
+
202
+ .ev-table-detail {
203
+ padding: var(--ev-space-2) var(--ev-space-5) var(--ev-space-5);
204
+ }
205
+
206
+ .ev-card-body-flush .ev-table-detail {
207
+ padding-inline: var(--ev-space-6);
208
+ }
209
+
210
+ /* --- Итоговая строка --- */
211
+ .ev-table tfoot td {
212
+ position: sticky;
213
+ bottom: 0;
214
+ z-index: var(--ev-z-sticky);
215
+ height: 40px;
216
+ background: var(--ev-surface-2);
217
+ border-top: 1px solid var(--ev-border);
218
+ border-bottom: 0;
219
+ font-weight: var(--ev-fw-medium);
220
+ }
221
+
222
+ :is(.ev-card, .ev-panel, .ev-modal, .ev-drawer) .ev-table tfoot td {
223
+ background: var(--ev-surface-3);
224
+ }
225
+
226
+ .ev-card-body-flush .ev-table tfoot td {
227
+ background: var(--ev-surface-2);
228
+ }
229
+
230
+ /* --- Шапка, липнущая к странице (stickyHeader): сдвиг считает DataTable --- */
231
+ .ev-table[data-sticky-page] thead th {
232
+ transform: translateY(var(--ev-thead-shift, 0px));
233
+ }
234
+
235
+ .ev-table[data-stuck] thead th {
236
+ box-shadow: var(--ev-shadow-sm);
237
+ }
238
+
142
239
  .ev-table-select {
143
240
  width: 44px;
144
241
  padding-right: 0 !important;
@@ -258,6 +355,29 @@
258
355
  opacity: 0.7;
259
356
  }
260
357
 
358
+ .ev-table-card[data-tone]:not([data-selected]) {
359
+ background-image: linear-gradient(var(--ev-tone-row), var(--ev-tone-row));
360
+ }
361
+
362
+ .ev-table-card[data-tone][data-clickable]:not([data-selected]):active {
363
+ background-image: linear-gradient(var(--ev-active), var(--ev-active)), linear-gradient(var(--ev-tone-row), var(--ev-tone-row));
364
+ }
365
+
366
+ .ev-table-card-head .ev-table-expand-btn {
367
+ flex: 0 0 auto;
368
+ margin: -2px -4px 0 0;
369
+ }
370
+
371
+ .ev-table-card-detail {
372
+ padding-top: var(--ev-space-3);
373
+ border-top: 1px solid var(--ev-border-soft);
374
+ min-width: 0;
375
+ }
376
+
377
+ .ev-table-card-total {
378
+ background: var(--ev-surface-2);
379
+ }
380
+
261
381
  .ev-table-card-head {
262
382
  display: flex;
263
383
  align-items: flex-start;
@@ -34,6 +34,12 @@
34
34
  color: var(--ev-text-muted);
35
35
  }
36
36
 
37
+ .ev-cmdk-spinner {
38
+ display: inline-flex;
39
+ width: 17px;
40
+ justify-content: center;
41
+ }
42
+
37
43
  .ev-cmdk-input {
38
44
  flex: 1 1 auto;
39
45
  min-width: 0;
@@ -276,6 +276,12 @@
276
276
  padding: 0;
277
277
  }
278
278
 
279
+ .ev-modal-prompt {
280
+ display: flex;
281
+ flex-direction: column;
282
+ gap: var(--ev-space-4);
283
+ }
284
+
279
285
  .ev-modal-message {
280
286
  color: var(--ev-text-secondary);
281
287
  line-height: 1.55;
@@ -462,6 +468,43 @@
462
468
  font-weight: var(--ev-fw-semibold);
463
469
  }
464
470
 
471
+ /* Карточка с onClick: заголовок - кнопка, растянутая на карточку; кнопки карточки - поверх неё. */
472
+ .ev-toast-open {
473
+ display: block;
474
+ padding: 0;
475
+ color: inherit;
476
+ text-align: left;
477
+ }
478
+
479
+ .ev-toast-open::after {
480
+ content: '';
481
+ position: absolute;
482
+ inset: 0;
483
+ border-radius: inherit;
484
+ }
485
+
486
+ .ev-toast-open:focus-visible {
487
+ outline: none;
488
+ box-shadow: none;
489
+ }
490
+
491
+ .ev-toast-open:focus-visible::after {
492
+ box-shadow: var(--ev-focus-ring);
493
+ }
494
+
495
+ .ev-toast[data-clickable] {
496
+ cursor: pointer;
497
+ }
498
+
499
+ .ev-toast[data-clickable]:hover {
500
+ background-image: linear-gradient(var(--ev-tint-2), var(--ev-tint-2));
501
+ }
502
+
503
+ .ev-toast[data-clickable] :is(.ev-toast-action, .ev-toast-actions, .ev-toast-close) {
504
+ position: relative;
505
+ z-index: 1;
506
+ }
507
+
465
508
  .ev-toast-desc {
466
509
  margin-top: 2px;
467
510
  font-size: var(--ev-fs-sm);
@@ -2,6 +2,8 @@
2
2
 
3
3
  @layer ev.shell {
4
4
  .ev-shell {
5
+ /* Шапка таблицы со stickyHeader липнет под шапкой приложения. */
6
+ --ev-table-sticky-top: var(--ev-topbar-h);
5
7
  display: flex;
6
8
  min-height: 100dvh;
7
9
  width: 100%;