@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.
Files changed (41) hide show
  1. package/README.md +78 -0
  2. package/dist/components/Form/ControlledSelect.js +1 -1
  3. package/dist/components/Form/ControlledTabs.js +4 -4
  4. package/dist/components/Form/ControlledTabs.js.map +1 -1
  5. package/dist/components/Form/ControlledViewToggle.js +2 -2
  6. package/dist/components/Form/ControlledViewToggle.js.map +1 -1
  7. package/dist/components/Navigation/ControlledBreadcrumb.js +3 -2
  8. package/dist/components/Navigation/ControlledBreadcrumb.js.map +1 -1
  9. package/dist/components/Navigation/PageTitle.js +3 -6
  10. package/dist/components/Navigation/PageTitle.js.map +1 -1
  11. package/dist/components/Navigation/ReactRouterNavigationAdapter.d.ts +7 -0
  12. package/dist/components/Navigation/ReactRouterNavigationAdapter.js +26 -0
  13. package/dist/components/Navigation/ReactRouterNavigationAdapter.js.map +1 -0
  14. package/dist/components/Navigation/UiNavigationContext.d.ts +38 -0
  15. package/dist/components/Navigation/UiNavigationContext.js +43 -0
  16. package/dist/components/Navigation/UiNavigationContext.js.map +1 -0
  17. package/dist/components/Search/filters/useFilters.js +2 -2
  18. package/dist/components/Search/filters/useFilters.js.map +1 -1
  19. package/dist/components/UI/BaseDataCard.js +3 -2
  20. package/dist/components/UI/BaseDataCard.js.map +1 -1
  21. package/dist/components/UI/DataTable/DataTable.js +1 -1
  22. package/dist/components/UI/DataTable/DataTableBulkEditDialog.js +1 -1
  23. package/dist/components/UI/DataTable/DataTableFilters.js +1 -1
  24. package/dist/components/UI/DataTable/DataTableToolbar.js +1 -1
  25. package/dist/components/UI/DataTable/applyDataTableFilters.js +1 -1
  26. package/dist/components/UI/UserMenu.d.ts +48 -0
  27. package/dist/components/UI/UserMenu.js +196 -0
  28. package/dist/components/UI/UserMenu.js.map +1 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.js +5 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/locales/en/ui.json.d.ts +7 -0
  33. package/dist/locales/en/ui.json.js +3 -0
  34. package/dist/locales/en/ui.json.js.map +1 -1
  35. package/dist/locales/index.d.ts +14 -0
  36. package/dist/locales/pt/ui.json.d.ts +7 -0
  37. package/dist/locales/pt/ui.json.js +3 -0
  38. package/dist/locales/pt/ui.json.js.map +1 -1
  39. package/package.json +8 -4
  40. package/src/assets/styles/base.css +489 -0
  41. 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -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.1.1",
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/theme.css",
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
+ }
@@ -0,0 +1,2 @@
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
2
+ @import './base.css';