@umodeapporg/ui 0.1.1 → 0.3.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 +78 -0
- package/dist/components/Form/ControlledSelect.js +1 -1
- package/dist/components/Form/ControlledTabs.js +4 -4
- package/dist/components/Form/ControlledTabs.js.map +1 -1
- package/dist/components/Form/ControlledViewToggle.js +2 -2
- package/dist/components/Form/ControlledViewToggle.js.map +1 -1
- package/dist/components/Navigation/ControlledBreadcrumb.js +3 -2
- package/dist/components/Navigation/ControlledBreadcrumb.js.map +1 -1
- package/dist/components/Navigation/PageTitle.js +3 -6
- package/dist/components/Navigation/PageTitle.js.map +1 -1
- package/dist/components/Navigation/ReactRouterNavigationAdapter.d.ts +7 -0
- package/dist/components/Navigation/ReactRouterNavigationAdapter.js +26 -0
- package/dist/components/Navigation/ReactRouterNavigationAdapter.js.map +1 -0
- package/dist/components/Navigation/UiNavigationContext.d.ts +38 -0
- package/dist/components/Navigation/UiNavigationContext.js +43 -0
- package/dist/components/Navigation/UiNavigationContext.js.map +1 -0
- package/dist/components/Search/filters/useFilters.js +2 -2
- package/dist/components/Search/filters/useFilters.js.map +1 -1
- package/dist/components/UI/BaseDataCard.js +3 -2
- package/dist/components/UI/BaseDataCard.js.map +1 -1
- package/dist/components/UI/DataTable/DataTable.js +1 -1
- package/dist/components/UI/DataTable/DataTableBulkEditDialog.js +1 -1
- package/dist/components/UI/DataTable/DataTableFilters.js +1 -1
- package/dist/components/UI/DataTable/DataTableToolbar.js +1 -1
- package/dist/components/UI/DataTable/applyDataTableFilters.js +1 -1
- package/dist/components/UI/UserMenu.d.ts +48 -0
- package/dist/components/UI/UserMenu.js +196 -0
- package/dist/components/UI/UserMenu.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/locales/en/ui.json.d.ts +7 -0
- package/dist/locales/en/ui.json.js +3 -0
- package/dist/locales/en/ui.json.js.map +1 -1
- package/dist/locales/index.d.ts +14 -0
- package/dist/locales/pt/ui.json.d.ts +7 -0
- package/dist/locales/pt/ui.json.js +3 -0
- package/dist/locales/pt/ui.json.js.map +1 -1
- package/package.json +8 -4
- package/src/assets/styles/base.css +489 -0
- package/src/assets/styles/index.css +2 -0
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { default as default13 } from "./components/Navigation/ControlledBreadcru
|
|
|
14
14
|
import { default as default14 } from "./components/Navigation/ErrorPage.js";
|
|
15
15
|
import { default as default15 } from "./components/Navigation/PageTitle.js";
|
|
16
16
|
import { PageTitleNavigationProvider, usePageTitleNavigation } from "./components/Navigation/PageTitleNavigationContext.js";
|
|
17
|
+
import { UiNavigationProvider, useUiNavigation } from "./components/Navigation/UiNavigationContext.js";
|
|
17
18
|
import { default as default16 } from "./components/Search/UnifiedSearchBar.js";
|
|
18
19
|
import { UiAccessProvider, useUiAccess } from "./components/UI/access/UiAccessContext.js";
|
|
19
20
|
import { default as default17 } from "./components/UI/BaseDataCard.js";
|
|
@@ -34,6 +35,7 @@ import { default as default31 } from "./components/UI/LoadingSpinner.js";
|
|
|
34
35
|
import { MaterialSymbol, combineClasses } from "./components/UI/MaterialSymbol/index.js";
|
|
35
36
|
import { default as default32 } from "./components/UI/NoDataFound/index.js";
|
|
36
37
|
import { default as default33 } from "./components/UI/SkeletonLoading.js";
|
|
38
|
+
import { default as default34 } from "./components/UI/UserMenu.js";
|
|
37
39
|
import { composeRefs } from "./helpers/composeRefs.js";
|
|
38
40
|
import { isPressOnLayeredSurface } from "./helpers/layeredSurfaces.js";
|
|
39
41
|
import { readThemeToken, useThemeChartTokens } from "./helpers/themeTokens.js";
|
|
@@ -87,7 +89,9 @@ export {
|
|
|
87
89
|
PageTitleNavigationProvider,
|
|
88
90
|
default33 as SkeletonLoading,
|
|
89
91
|
UiAccessProvider,
|
|
92
|
+
UiNavigationProvider,
|
|
90
93
|
default16 as UnifiedSearchBar,
|
|
94
|
+
default34 as UserMenu,
|
|
91
95
|
collapseVariants,
|
|
92
96
|
combineClasses,
|
|
93
97
|
composeRefs,
|
|
@@ -120,6 +124,7 @@ export {
|
|
|
120
124
|
useResolvedTheme,
|
|
121
125
|
useThemeChartTokens,
|
|
122
126
|
useUiAccess,
|
|
127
|
+
useUiNavigation,
|
|
123
128
|
writeUiPreference
|
|
124
129
|
};
|
|
125
130
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -459,6 +459,13 @@ declare const _default: {
|
|
|
459
459
|
"urlLinkCell": {
|
|
460
460
|
"clickToAccess": "Click to access"
|
|
461
461
|
},
|
|
462
|
+
"userMenu": {
|
|
463
|
+
"loading": "Loading...",
|
|
464
|
+
"yourAccounts": "Your Accounts",
|
|
465
|
+
"searchAccount": "Search account",
|
|
466
|
+
"noAccounts": "No active accounts available",
|
|
467
|
+
"logout": "Sign out"
|
|
468
|
+
},
|
|
462
469
|
"voiceInput": {
|
|
463
470
|
"start": "Transcribe by voice",
|
|
464
471
|
"stop": "Stop transcription",
|
|
@@ -28,6 +28,7 @@ const toast = { "learnMore": "Learn more" };
|
|
|
28
28
|
const unifiedSearchBar = { "clearFilter": "Clear filter" };
|
|
29
29
|
const uploader = { "dragOr": "Drag or", "selectFile": "select a file", "orPaste": "or paste (Ctrl+V)", "allowedFormats": "Allowed formats:", "upTo": "up to", "megabytesSuffix": "MB", "sendFiles": "Send file(s)", "filesIgnoredMaxSize": "Some files were ignored because they exceeded the maximum size of {{size}}MB", "invalidFormat": "Please add files in the allowed formats", "allowedFormatsTooltip": "Allowed formats: {{formats}} up to {{size}}MB" };
|
|
30
30
|
const urlLinkCell = { "clickToAccess": "Click to access" };
|
|
31
|
+
const userMenu = { "loading": "Loading...", "yourAccounts": "Your Accounts", "searchAccount": "Search account", "noAccounts": "No active accounts available", "logout": "Sign out" };
|
|
31
32
|
const voiceInput = { "start": "Transcribe by voice", "stop": "Stop transcription", "commandsHint": "Voice commands: say {{commands}} to insert punctuation or delete the last word." };
|
|
32
33
|
const en = {
|
|
33
34
|
accordion,
|
|
@@ -60,6 +61,7 @@ const en = {
|
|
|
60
61
|
unifiedSearchBar,
|
|
61
62
|
uploader,
|
|
62
63
|
urlLinkCell,
|
|
64
|
+
userMenu,
|
|
63
65
|
voiceInput
|
|
64
66
|
};
|
|
65
67
|
export {
|
|
@@ -94,6 +96,7 @@ export {
|
|
|
94
96
|
unifiedSearchBar,
|
|
95
97
|
uploader,
|
|
96
98
|
urlLinkCell,
|
|
99
|
+
userMenu,
|
|
97
100
|
voiceInput
|
|
98
101
|
};
|
|
99
102
|
//# sourceMappingURL=ui.json.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ui.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/locales/index.d.ts
CHANGED
|
@@ -461,6 +461,13 @@ export declare const uiLocales: {
|
|
|
461
461
|
urlLinkCell: {
|
|
462
462
|
clickToAccess: string;
|
|
463
463
|
};
|
|
464
|
+
userMenu: {
|
|
465
|
+
loading: string;
|
|
466
|
+
yourAccounts: string;
|
|
467
|
+
searchAccount: string;
|
|
468
|
+
noAccounts: string;
|
|
469
|
+
logout: string;
|
|
470
|
+
};
|
|
464
471
|
voiceInput: {
|
|
465
472
|
start: string;
|
|
466
473
|
stop: string;
|
|
@@ -928,6 +935,13 @@ export declare const uiLocales: {
|
|
|
928
935
|
urlLinkCell: {
|
|
929
936
|
clickToAccess: string;
|
|
930
937
|
};
|
|
938
|
+
userMenu: {
|
|
939
|
+
loading: string;
|
|
940
|
+
yourAccounts: string;
|
|
941
|
+
searchAccount: string;
|
|
942
|
+
noAccounts: string;
|
|
943
|
+
logout: string;
|
|
944
|
+
};
|
|
931
945
|
voiceInput: {
|
|
932
946
|
start: string;
|
|
933
947
|
stop: string;
|
|
@@ -459,6 +459,13 @@ declare const _default: {
|
|
|
459
459
|
"urlLinkCell": {
|
|
460
460
|
"clickToAccess": "Clique para acessar"
|
|
461
461
|
},
|
|
462
|
+
"userMenu": {
|
|
463
|
+
"loading": "Carregando...",
|
|
464
|
+
"yourAccounts": "Suas Contas",
|
|
465
|
+
"searchAccount": "Buscar conta",
|
|
466
|
+
"noAccounts": "Nenhuma conta ativa disponível",
|
|
467
|
+
"logout": "Sair"
|
|
468
|
+
},
|
|
462
469
|
"voiceInput": {
|
|
463
470
|
"start": "Transcrever por voz",
|
|
464
471
|
"stop": "Parar transcrição",
|
|
@@ -28,6 +28,7 @@ const toast = { "learnMore": "Saiba Mais" };
|
|
|
28
28
|
const unifiedSearchBar = { "clearFilter": "Limpar Filtro" };
|
|
29
29
|
const uploader = { "dragOr": "Arraste ou", "selectFile": "selecione um arquivo", "orPaste": "ou cole (Ctrl+V)", "allowedFormats": "Formatos permitidos:", "upTo": "até", "megabytesSuffix": "MB", "sendFiles": "Enviar arquivo(s)", "filesIgnoredMaxSize": "Alguns arquivos foram ignorados porque excederam o tamanho máximo de {{size}}MB", "invalidFormat": "Por favor, insira arquivos nos formatos permitidos", "allowedFormatsTooltip": "Formatos permitidos: {{formats}} até {{size}}MB" };
|
|
30
30
|
const urlLinkCell = { "clickToAccess": "Clique para acessar" };
|
|
31
|
+
const userMenu = { "loading": "Carregando...", "yourAccounts": "Suas Contas", "searchAccount": "Buscar conta", "noAccounts": "Nenhuma conta ativa disponível", "logout": "Sair" };
|
|
31
32
|
const voiceInput = { "start": "Transcrever por voz", "stop": "Parar transcrição", "commandsHint": "Comandos de voz: diga {{commands}} para inserir pontuação ou apagar a última palavra." };
|
|
32
33
|
const pt = {
|
|
33
34
|
accordion,
|
|
@@ -60,6 +61,7 @@ const pt = {
|
|
|
60
61
|
unifiedSearchBar,
|
|
61
62
|
uploader,
|
|
62
63
|
urlLinkCell,
|
|
64
|
+
userMenu,
|
|
63
65
|
voiceInput
|
|
64
66
|
};
|
|
65
67
|
export {
|
|
@@ -94,6 +96,7 @@ export {
|
|
|
94
96
|
unifiedSearchBar,
|
|
95
97
|
uploader,
|
|
96
98
|
urlLinkCell,
|
|
99
|
+
userMenu,
|
|
97
100
|
voiceInput
|
|
98
101
|
};
|
|
99
102
|
//# sourceMappingURL=ui.json.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ui.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umodeapporg/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
],
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
-
"src/assets/styles
|
|
11
|
+
"src/assets/styles",
|
|
12
12
|
"tailwind.preset.js",
|
|
13
13
|
"tailwind-plugins"
|
|
14
14
|
],
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"./styles.css": "./dist/assets/umode-ui.css",
|
|
21
21
|
"./theme.css": "./src/assets/styles/theme.css",
|
|
22
|
+
"./base.css": "./src/assets/styles/base.css",
|
|
22
23
|
"./tailwind-preset": "./tailwind.preset.js",
|
|
23
24
|
"./locales": {
|
|
24
25
|
"types": "./dist/locales/index.d.ts",
|
|
@@ -103,10 +104,10 @@
|
|
|
103
104
|
"i18next": ">=23",
|
|
104
105
|
"konva": "^9.3.22",
|
|
105
106
|
"motion": "^13.1.0",
|
|
106
|
-
"react": "^18.3.1",
|
|
107
|
+
"react": "^18.3.1 || ^19.0.0",
|
|
107
108
|
"react-apexcharts": ">=1.4.1 <2",
|
|
108
109
|
"react-day-picker": "^9.3.0",
|
|
109
|
-
"react-dom": "^18.3.1",
|
|
110
|
+
"react-dom": "^18.3.1 || ^19.0.0",
|
|
110
111
|
"react-i18next": ">=14",
|
|
111
112
|
"react-infinite-scroll-component": "^6.1.0",
|
|
112
113
|
"react-konva": "^18.2.16",
|
|
@@ -162,6 +163,9 @@
|
|
|
162
163
|
},
|
|
163
164
|
"remark-gfm": {
|
|
164
165
|
"optional": true
|
|
166
|
+
},
|
|
167
|
+
"react-router-dom": {
|
|
168
|
+
"optional": true
|
|
165
169
|
}
|
|
166
170
|
},
|
|
167
171
|
"devDependencies": {
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
@import './theme.css';
|
|
2
|
+
|
|
3
|
+
@tailwind base;
|
|
4
|
+
@tailwind components;
|
|
5
|
+
@tailwind utilities;
|
|
6
|
+
|
|
7
|
+
/* Fluid interface density. Tailwind utilities are rem-based, so scaling the root
|
|
8
|
+
font-size scales typography, spacing, control heights and icons together as one
|
|
9
|
+
system. Full size (1rem = the user's default, 16px) on viewports >= 1368px (the
|
|
10
|
+
`desktop` breakpoint), so fullscreen notebooks stay pixel-identical; shrinks
|
|
11
|
+
linearly below that down to 94% at <= 1152px (compact windows, split screens).
|
|
12
|
+
The rem-based clamp keeps user font-size preferences and browser zoom working,
|
|
13
|
+
and breakpoints stay px-based so layout switches do not feed back into this
|
|
14
|
+
scale. */
|
|
15
|
+
html {
|
|
16
|
+
font-size: clamp(0.94rem, 0.62rem + 0.4444vw, 1rem);
|
|
17
|
+
background-color: rgb(var(--color-canvas));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
body {
|
|
21
|
+
background-color: rgb(var(--color-canvas));
|
|
22
|
+
color: rgb(var(--color-foreground));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* ---------------------------------------------------------------------------
|
|
26
|
+
Theme switch.
|
|
27
|
+
|
|
28
|
+
The swap itself is instant: every colour resolves from a CSS variable, so one
|
|
29
|
+
attribute write repaints the whole UI already correct.
|
|
30
|
+
|
|
31
|
+
This class exists because instant was NOT instant. Around 350 components carry
|
|
32
|
+
their own `transition-colors`, so on a theme change each of them animated on its
|
|
33
|
+
own clock (140ms here, 200ms there, 280ms on a collapse) while untransitioned
|
|
34
|
+
elements changed at once. The page arrived in waves and the borders landed last.
|
|
35
|
+
Cancelling those transitions for the duration of the swap is what makes it read
|
|
36
|
+
as ONE change.
|
|
37
|
+
|
|
38
|
+
Measured on a 5.4k-node page carrying the app's real transition classes:
|
|
39
|
+
instant, components still transitioning ...... 406ms of blocked main thread
|
|
40
|
+
instant + this suppression ................... 49ms
|
|
41
|
+
one uniform transition on `*` ................ 627ms
|
|
42
|
+
tokens typed with @property and morphed ...... 13fps, main thread starved
|
|
43
|
+
|
|
44
|
+
The last two are why there is no animation here. Any smooth transition between
|
|
45
|
+
two full-page themes is either a BLEND of both (illegible at the midpoint: dark
|
|
46
|
+
text on light mixed with light text on dark is grey on grey) or a repaint of
|
|
47
|
+
every element on every frame. Neither is acceptable, so the swap is atomic and
|
|
48
|
+
the only motion left is the toggle's own knob.
|
|
49
|
+
--------------------------------------------------------------------------- */
|
|
50
|
+
.theme-switching,
|
|
51
|
+
.theme-switching *,
|
|
52
|
+
.theme-switching *::before,
|
|
53
|
+
.theme-switching *::after {
|
|
54
|
+
transition: none !important;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ---------------------------------------------------------------------------
|
|
58
|
+
Motion.
|
|
59
|
+
|
|
60
|
+
One vocabulary so a dropdown, a drawer and a card hover do not each invent a
|
|
61
|
+
duration. Entering uses a decelerating curve (arrives and settles), leaving
|
|
62
|
+
uses an accelerating one (gets out of the way). Everything animates only
|
|
63
|
+
`opacity` and `transform`, which the compositor handles without layout.
|
|
64
|
+
|
|
65
|
+
Durations are deliberately short: this is a work tool, and animation that
|
|
66
|
+
delays a click is a tax, not a delight.
|
|
67
|
+
--------------------------------------------------------------------------- */
|
|
68
|
+
@keyframes surface-in {
|
|
69
|
+
from {
|
|
70
|
+
opacity: 0;
|
|
71
|
+
transform: translate3d(0, -0.375rem, 0) scale(0.98);
|
|
72
|
+
}
|
|
73
|
+
to {
|
|
74
|
+
opacity: 1;
|
|
75
|
+
transform: translate3d(0, 0, 0) scale(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@keyframes scrim-in {
|
|
80
|
+
from {
|
|
81
|
+
opacity: 0;
|
|
82
|
+
}
|
|
83
|
+
to {
|
|
84
|
+
opacity: 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Floating surfaces that Material Tailwind mounts on open. Targeting the roles
|
|
89
|
+
rather than the components means a new menu inherits the motion for free. */
|
|
90
|
+
[role='menu'],
|
|
91
|
+
[role='tooltip'],
|
|
92
|
+
[data-popover='true'],
|
|
93
|
+
.glass,
|
|
94
|
+
.glass-elevated {
|
|
95
|
+
animation: surface-in 160ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.glass-scrim {
|
|
99
|
+
animation: scrim-in 160ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* The interaction contract for anything clickable: colour and elevation move on
|
|
103
|
+
the same clock, so hover feels like one system instead of per-component taste. */
|
|
104
|
+
.interactive {
|
|
105
|
+
transition-property: background-color, border-color, color, box-shadow, opacity, transform;
|
|
106
|
+
transition-duration: 140ms;
|
|
107
|
+
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.interactive:active {
|
|
111
|
+
transform: scale(0.985);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Reduced motion: movement goes, feedback stays. Opacity-only transitions are
|
|
115
|
+
kept because removing them entirely makes state changes feel broken, and a
|
|
116
|
+
crossfade is not the motion this setting protects against. */
|
|
117
|
+
@media (prefers-reduced-motion: reduce) {
|
|
118
|
+
[role='menu'],
|
|
119
|
+
[role='tooltip'],
|
|
120
|
+
[data-popover='true'],
|
|
121
|
+
.glass,
|
|
122
|
+
.glass-elevated,
|
|
123
|
+
.glass-scrim {
|
|
124
|
+
animation-name: scrim-in;
|
|
125
|
+
animation-duration: 100ms;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.interactive:active {
|
|
129
|
+
transform: none;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ---------------------------------------------------------------------------
|
|
134
|
+
Forced theme islands.
|
|
135
|
+
|
|
136
|
+
`data-theme` re-declares the whole token set, so any subtree can pin itself to a
|
|
137
|
+
theme. This is how print previews stay truthful: what the user sees on screen is
|
|
138
|
+
what comes out of the printer, regardless of the app theme.
|
|
139
|
+
--------------------------------------------------------------------------- */
|
|
140
|
+
[data-theme='light'],
|
|
141
|
+
[data-theme='dark'] {
|
|
142
|
+
background-color: rgb(var(--color-canvas));
|
|
143
|
+
color: rgb(var(--color-foreground));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@media print {
|
|
147
|
+
:root {
|
|
148
|
+
color-scheme: light;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ---------------------------------------------------------------------------
|
|
153
|
+
Focus visibility (WCAG 2.4.7 / 1.4.11).
|
|
154
|
+
|
|
155
|
+
One ring for the whole app, driven by the `ring` token, so it has 3:1 against
|
|
156
|
+
both canvas and surface in either theme. `:focus-visible` (not `:focus`) keeps
|
|
157
|
+
it off mouse clicks. Components that draw their own ring opt out by setting
|
|
158
|
+
`outline: none` plus their own `focus-visible:` classes.
|
|
159
|
+
--------------------------------------------------------------------------- */
|
|
160
|
+
:focus-visible {
|
|
161
|
+
outline: 2px solid rgb(var(--color-ring));
|
|
162
|
+
outline-offset: 2px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* The MT primitives ship `focus:outline-none`, which would swallow the ring above;
|
|
166
|
+
these re-assert it for keyboard users only. */
|
|
167
|
+
button:focus-visible,
|
|
168
|
+
a:focus-visible,
|
|
169
|
+
[role='button']:focus-visible,
|
|
170
|
+
[tabindex]:focus-visible {
|
|
171
|
+
outline: 2px solid rgb(var(--color-ring));
|
|
172
|
+
outline-offset: 2px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* Editable text fields always match :focus-visible in Chromium, and this block sits
|
|
176
|
+
after the utilities layer, so their `outline-none` class loses the cascade. They
|
|
177
|
+
draw their own focus chrome (RichTextEditor border/ring, or the host's
|
|
178
|
+
focus-within ring around the bare variant), so the global rectangle would double
|
|
179
|
+
the indicator and ignore the rounded container. */
|
|
180
|
+
[contenteditable]:focus-visible {
|
|
181
|
+
outline: none;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
::-webkit-scrollbar {
|
|
185
|
+
width: 7px;
|
|
186
|
+
height: 7px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.scrollbar-hide {
|
|
190
|
+
-ms-overflow-style: none;
|
|
191
|
+
scrollbar-width: none;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.scrollbar-hide::-webkit-scrollbar {
|
|
195
|
+
display: none;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
::-webkit-scrollbar-thumb {
|
|
199
|
+
background-color: rgb(var(--color-border-strong) / 0.55);
|
|
200
|
+
border-radius: 10px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
::-webkit-scrollbar-thumb:hover {
|
|
204
|
+
background-color: rgb(var(--color-border-strong) / 0.8);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
::-webkit-scrollbar-track {
|
|
208
|
+
background-color: transparent;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
* {
|
|
212
|
+
padding: 0;
|
|
213
|
+
margin: 0;
|
|
214
|
+
box-sizing: border-box;
|
|
215
|
+
font-family: 'Inter', sans-serif;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
input:focus {
|
|
219
|
+
box-shadow: none !important;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* Chrome paints its own autofill background, which ignores every theme token. The
|
|
223
|
+
giant inset shadow is the only way to repaint it, and it has to follow the theme
|
|
224
|
+
or autofilled fields turn into white blocks in dark mode. */
|
|
225
|
+
input:-webkit-autofill,
|
|
226
|
+
input:-webkit-autofill:hover,
|
|
227
|
+
input:-webkit-autofill:focus,
|
|
228
|
+
input:-webkit-autofill:active {
|
|
229
|
+
-webkit-box-shadow: 0 0 0 1000px rgb(var(--color-surface)) inset !important;
|
|
230
|
+
box-shadow: 0 0 0 1000px rgb(var(--color-surface)) inset !important;
|
|
231
|
+
-webkit-text-fill-color: rgb(var(--color-foreground)) !important;
|
|
232
|
+
caret-color: rgb(var(--color-foreground));
|
|
233
|
+
transition: background-color 5000s ease-in-out 0s !important;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* @tailwindcss/forms paints every form control from `colors.gray.500` and `colors.blue.600`,
|
|
237
|
+
and withMT replaced those with the Material palette. The result is a white fill, a 2.6:1
|
|
238
|
+
border and a blue focus ring frozen into the base layer of every input, select and
|
|
239
|
+
textarea, so a control that does not name its own background renders white in dark mode.
|
|
240
|
+
The selectors below mirror the plugin's at the same specificity, so a utility class still
|
|
241
|
+
wins and only the frozen colors change.
|
|
242
|
+
|
|
243
|
+
The control names its own `field` tokens rather than borrowing `surface` + `border-strong`,
|
|
244
|
+
because the two themes identify a control differently. In LIGHT nothing changed: a white
|
|
245
|
+
field with a 3.28:1 outline. In DARK the control is a FILLED field with a soft rim, which
|
|
246
|
+
is the trade recorded below. */
|
|
247
|
+
input:where(
|
|
248
|
+
[type='text'],
|
|
249
|
+
:not([type]),
|
|
250
|
+
[type='email'],
|
|
251
|
+
[type='url'],
|
|
252
|
+
[type='password'],
|
|
253
|
+
[type='number'],
|
|
254
|
+
[type='date'],
|
|
255
|
+
[type='datetime-local'],
|
|
256
|
+
[type='month'],
|
|
257
|
+
[type='search'],
|
|
258
|
+
[type='tel'],
|
|
259
|
+
[type='time'],
|
|
260
|
+
[type='week']
|
|
261
|
+
),
|
|
262
|
+
select,
|
|
263
|
+
textarea {
|
|
264
|
+
background-color: rgb(var(--color-field));
|
|
265
|
+
border-color: rgb(var(--color-field-border));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
input:where(
|
|
269
|
+
[type='text'],
|
|
270
|
+
:not([type]),
|
|
271
|
+
[type='email'],
|
|
272
|
+
[type='url'],
|
|
273
|
+
[type='password'],
|
|
274
|
+
[type='number'],
|
|
275
|
+
[type='date'],
|
|
276
|
+
[type='datetime-local'],
|
|
277
|
+
[type='month'],
|
|
278
|
+
[type='search'],
|
|
279
|
+
[type='tel'],
|
|
280
|
+
[type='time'],
|
|
281
|
+
[type='week']
|
|
282
|
+
):focus,
|
|
283
|
+
select:focus,
|
|
284
|
+
textarea:focus {
|
|
285
|
+
border-color: rgb(var(--color-ring));
|
|
286
|
+
--tw-ring-color: rgb(var(--color-ring));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* Checkbox and radio keep `border-strong`: they are tiny, they have no fill to be
|
|
290
|
+
identified by, and their whole shape IS the outline. */
|
|
291
|
+
input:where([type='checkbox'], [type='radio']) {
|
|
292
|
+
color: rgb(var(--color-primary));
|
|
293
|
+
background-color: rgb(var(--color-field));
|
|
294
|
+
border-color: rgb(var(--color-border-strong));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
input:where([type='checkbox'], [type='radio']):focus {
|
|
298
|
+
--tw-ring-color: rgb(var(--color-ring));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
::placeholder {
|
|
302
|
+
color: rgb(var(--color-foreground-faint));
|
|
303
|
+
opacity: 1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* The plugin's own `input::placeholder` outranks the bare `::placeholder` above. */
|
|
307
|
+
input::placeholder,
|
|
308
|
+
textarea::placeholder {
|
|
309
|
+
color: rgb(var(--color-foreground-faint));
|
|
310
|
+
opacity: 1;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* ---------------------------------------------------------------------------
|
|
314
|
+
Text selection.
|
|
315
|
+
|
|
316
|
+
The band is an OPAQUE token, not the brand colour at low alpha. Alpha over a
|
|
317
|
+
dark canvas is what made the selection invisible: `primary / 0.24` composited
|
|
318
|
+
to 1.21:1 against the page in the dark theme, so a selected paragraph looked
|
|
319
|
+
identical to an unselected one. Opaque values land at 2.07:1 (light) and
|
|
320
|
+
2.16:1 (dark) against every surface, with the ink above 6:1 on the band.
|
|
321
|
+
|
|
322
|
+
Both channels are tokens, so a `data-theme` island (the print preview) selects
|
|
323
|
+
with its own theme instead of the app's.
|
|
324
|
+
--------------------------------------------------------------------------- */
|
|
325
|
+
::selection {
|
|
326
|
+
background-color: rgb(var(--color-selection));
|
|
327
|
+
color: rgb(var(--color-selection-fg));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* ---------------------------------------------------------------------------
|
|
331
|
+
Disabled and read-only.
|
|
332
|
+
|
|
333
|
+
Two different facts, and they must not share a visual: disabled means "you
|
|
334
|
+
cannot use this", read-only means "this is real content you may read and copy
|
|
335
|
+
but not edit". Neither had any rule here, so a disabled field kept the enabled
|
|
336
|
+
fill and a read-only field was indistinguishable from an editable one.
|
|
337
|
+
|
|
338
|
+
The ink is `foreground-faint`, which is the token's documented job (the 3:1
|
|
339
|
+
non-textual tier), NOT an opacity. Opacity multiplies against the backdrop:
|
|
340
|
+
the same icon at `opacity-40` measures 1.82:1 in light and 2.03:1 in dark,
|
|
341
|
+
while the token holds 4.06:1 / 4.19:1. The fill drops to `muted-active` and
|
|
342
|
+
the border to `border-emphasis` so the control keeps its shape and still reads
|
|
343
|
+
as switched off.
|
|
344
|
+
--------------------------------------------------------------------------- */
|
|
345
|
+
input:where(:disabled),
|
|
346
|
+
select:where(:disabled),
|
|
347
|
+
textarea:where(:disabled) {
|
|
348
|
+
background-color: rgb(var(--color-field-disabled));
|
|
349
|
+
border-color: rgb(var(--color-border-emphasis));
|
|
350
|
+
color: rgb(var(--color-foreground-subtle));
|
|
351
|
+
cursor: not-allowed;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
input:where(:disabled)::placeholder,
|
|
355
|
+
textarea:where(:disabled)::placeholder {
|
|
356
|
+
color: rgb(var(--color-foreground-faint));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
input:where([type='checkbox'], [type='radio']):where(:disabled) {
|
|
360
|
+
background-color: rgb(var(--color-field-disabled));
|
|
361
|
+
border-color: rgb(var(--color-border-emphasis));
|
|
362
|
+
color: rgb(var(--color-foreground-faint));
|
|
363
|
+
cursor: not-allowed;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* There is deliberately NO `[readonly]` rule here. In this codebase the attribute
|
|
367
|
+
does not mean "locked content": four of its six uses are combobox triggers
|
|
368
|
+
(`ControlledSelect`, `DatePicker`, `DateRangePicker`), where it only blocks
|
|
369
|
+
typing. Styling it as a distinct state would repaint every select in the app. */
|
|
370
|
+
|
|
371
|
+
button:disabled,
|
|
372
|
+
[aria-disabled='true'] {
|
|
373
|
+
cursor: not-allowed;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.material-symbols {
|
|
377
|
+
user-select: none;
|
|
378
|
+
font-weight: normal;
|
|
379
|
+
font-style: normal;
|
|
380
|
+
font-size: inherit;
|
|
381
|
+
display: block;
|
|
382
|
+
line-height: 1;
|
|
383
|
+
text-transform: none;
|
|
384
|
+
letter-spacing: normal;
|
|
385
|
+
word-wrap: normal;
|
|
386
|
+
white-space: nowrap;
|
|
387
|
+
direction: ltr;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.wf-loading,
|
|
391
|
+
.wf-materialicons-n4-inactive {
|
|
392
|
+
.material-icons {
|
|
393
|
+
display: none;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
textarea:focus {
|
|
398
|
+
outline: none;
|
|
399
|
+
box-shadow: none;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.Toastify__toast-container {
|
|
403
|
+
width: min(40rem, calc(100vw - 2rem)) !important;
|
|
404
|
+
z-index: 2147483647 !important;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
[role='tooltip'] {
|
|
408
|
+
z-index: 9999 !important;
|
|
409
|
+
cursor: help !important;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
[role='menu'] {
|
|
413
|
+
overflow: hidden !important;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.inter-font {
|
|
417
|
+
font-family: 'Inter', sans-serif !important;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.zIndexMax {
|
|
421
|
+
z-index: 9999 !important;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* Rich text authored in RichTextEditor. Colour is stored as a role, never as a literal value,
|
|
425
|
+
so the same message stays legible after a theme switch. Both the editor and every surface
|
|
426
|
+
that renders the sanitized body share these rules. */
|
|
427
|
+
.rt-color-brand {
|
|
428
|
+
color: rgb(var(--color-brand-emphasis));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.rt-color-secondary {
|
|
432
|
+
color: rgb(var(--color-secondary-emphasis));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.rt-color-accent {
|
|
436
|
+
color: rgb(var(--color-accent-emphasis));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.rt-color-success {
|
|
440
|
+
color: rgb(var(--color-success-emphasis));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.rt-color-warning {
|
|
444
|
+
color: rgb(var(--color-warning-emphasis));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.rt-color-danger {
|
|
448
|
+
color: rgb(var(--color-danger-emphasis));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.rt-color-info {
|
|
452
|
+
color: rgb(var(--color-info-emphasis));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.rt-color-muted {
|
|
456
|
+
color: rgb(var(--color-foreground-muted));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.rt-file {
|
|
460
|
+
display: inline-flex;
|
|
461
|
+
align-items: center;
|
|
462
|
+
gap: 0.25rem;
|
|
463
|
+
max-width: 100%;
|
|
464
|
+
padding: 0.125rem 0.5rem 0.125rem 0.375rem;
|
|
465
|
+
border: 1px solid rgb(var(--color-border));
|
|
466
|
+
border-radius: 0.5rem;
|
|
467
|
+
background-color: rgb(var(--color-muted));
|
|
468
|
+
color: rgb(var(--color-foreground-muted));
|
|
469
|
+
font-size: 0.75rem;
|
|
470
|
+
line-height: 1.25rem;
|
|
471
|
+
text-decoration: none;
|
|
472
|
+
vertical-align: baseline;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.rt-file:hover {
|
|
476
|
+
background-color: rgb(var(--color-muted-hover));
|
|
477
|
+
color: rgb(var(--color-foreground));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.rt-file::before {
|
|
481
|
+
content: 'attach_file';
|
|
482
|
+
font-family: 'Material Symbols Outlined', 'Material Icons', sans-serif;
|
|
483
|
+
font-size: 1rem;
|
|
484
|
+
line-height: 1;
|
|
485
|
+
font-weight: normal;
|
|
486
|
+
letter-spacing: normal;
|
|
487
|
+
-webkit-font-feature-settings: 'liga';
|
|
488
|
+
font-feature-settings: 'liga';
|
|
489
|
+
}
|