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.
- package/README.md +31 -9
- package/README.ru.md +31 -9
- package/dist/components/CommandPalette.d.ts +24 -1
- package/dist/components/CommandPalette.d.ts.map +1 -1
- package/dist/components/CommandPalette.js +22 -10
- package/dist/components/CommandPalette.js.map +1 -1
- package/dist/components/DataTable.d.ts +38 -1
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/DataTable.js +135 -25
- package/dist/components/DataTable.js.map +1 -1
- package/dist/components/Modal.d.ts +26 -0
- package/dist/components/Modal.d.ts.map +1 -1
- package/dist/components/Modal.js +92 -0
- package/dist/components/Modal.js.map +1 -1
- package/dist/components/Toast.d.ts +3 -0
- package/dist/components/Toast.d.ts.map +1 -1
- package/dist/components/Toast.js +5 -1
- package/dist/components/Toast.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/accent-preset.d.ts +40 -0
- package/dist/lib/accent-preset.d.ts.map +1 -0
- package/dist/lib/accent-preset.js +71 -0
- package/dist/lib/accent-preset.js.map +1 -0
- package/dist/lib/i18n-messages.d.ts +7 -0
- package/dist/lib/i18n-messages.d.ts.map +1 -1
- package/dist/lib/i18n-messages.js +10 -2
- package/dist/lib/i18n-messages.js.map +1 -1
- package/dist/lib/theme-script.d.ts +10 -4
- package/dist/lib/theme-script.d.ts.map +1 -1
- package/dist/lib/theme-script.js +8 -3
- package/dist/lib/theme-script.js.map +1 -1
- package/dist/lib/theme.d.ts +10 -6
- package/dist/lib/theme.d.ts.map +1 -1
- package/dist/lib/theme.js +1 -1
- package/dist/lib/theme.js.map +1 -1
- package/dist/styles/accents.css +6 -6
- package/dist/styles/components/data.css +120 -0
- package/dist/styles/components/navigation.css +6 -0
- package/dist/styles/components/overlay.css +43 -0
- package/dist/styles/shell.css +2 -0
- package/dist/styles/tokens.css +27 -27
- package/package.json +1 -1
- package/src/components/CommandPalette.tsx +68 -10
- package/src/components/DataTable.tsx +258 -41
- package/src/components/Modal.tsx +148 -1
- package/src/components/Toast.tsx +21 -1
- package/src/index.ts +4 -0
- package/src/lib/accent-preset.tsx +109 -0
- package/src/lib/i18n-messages.ts +17 -3
- package/src/lib/theme-script.tsx +19 -5
- package/src/lib/theme.ts +13 -8
- package/src/styles/accents.css +6 -6
- package/src/styles/components/data.css +120 -0
- package/src/styles/components/navigation.css +6 -0
- package/src/styles/components/overlay.css +43 -0
- package/src/styles/shell.css +2 -0
- package/src/styles/tokens.css +27 -27
package/src/components/Toast.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/lib/i18n-messages.ts
CHANGED
|
@@ -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',
|
package/src/lib/theme-script.tsx
CHANGED
|
@@ -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
|
-
/** Акцент без сохранённого
|
|
25
|
-
defaultAccent?:
|
|
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
|
-
|
|
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():
|
|
53
|
-
return
|
|
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
|
-
/**
|
|
70
|
-
export function setAccent(accent:
|
|
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
|
-
|
|
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:
|
|
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
|
|
107
|
+
const accent = useSyncExternalStore(subscribe, readAccent, () => 'indigo') as A
|
|
103
108
|
const style = useSyncExternalStore(subscribe, readStyle, () => 'default' as Style)
|
|
104
109
|
|
|
105
110
|
// Режим 'system': следим за сменой темы ОС.
|
package/src/styles/accents.css
CHANGED
|
@@ -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,
|
|
171
|
-
--ev-accent-text: #
|
|
172
|
-
--ev-accent-strong: #
|
|
173
|
-
--ev-accent-strong-hover: #
|
|
174
|
-
--ev-text-on-accent: #
|
|
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;
|
|
@@ -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);
|