@umodeapporg/ui 0.1.0 → 0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umodeapporg/ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.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,11 +19,52 @@
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",
25
26
  "import": "./dist/locales/index.js"
26
27
  },
28
+ "./components/Form/ControlledForm": {
29
+ "types": "./dist/components/Form/ControlledForm/index.d.ts",
30
+ "import": "./dist/components/Form/ControlledForm/index.js"
31
+ },
32
+ "./components/Form/ControlledImage": {
33
+ "types": "./dist/components/Form/ControlledImage/index.d.ts",
34
+ "import": "./dist/components/Form/ControlledImage/index.js"
35
+ },
36
+ "./components/Search": {
37
+ "types": "./dist/components/Search/index.d.ts",
38
+ "import": "./dist/components/Search/index.js"
39
+ },
40
+ "./components/Search/filters": {
41
+ "types": "./dist/components/Search/filters/index.d.ts",
42
+ "import": "./dist/components/Search/filters/index.js"
43
+ },
44
+ "./components/UI/AlertInfo": {
45
+ "types": "./dist/components/UI/AlertInfo/index.d.ts",
46
+ "import": "./dist/components/UI/AlertInfo/index.js"
47
+ },
48
+ "./components/UI/DataTable": {
49
+ "types": "./dist/components/UI/DataTable/index.d.ts",
50
+ "import": "./dist/components/UI/DataTable/index.js"
51
+ },
52
+ "./components/UI/Kanban": {
53
+ "types": "./dist/components/UI/Kanban/index.d.ts",
54
+ "import": "./dist/components/UI/Kanban/index.js"
55
+ },
56
+ "./components/UI/MaterialSymbol": {
57
+ "types": "./dist/components/UI/MaterialSymbol/index.d.ts",
58
+ "import": "./dist/components/UI/MaterialSymbol/index.js"
59
+ },
60
+ "./components/UI/NoDataFound": {
61
+ "types": "./dist/components/UI/NoDataFound/index.d.ts",
62
+ "import": "./dist/components/UI/NoDataFound/index.js"
63
+ },
64
+ "./motion": {
65
+ "types": "./dist/motion/index.d.ts",
66
+ "import": "./dist/motion/index.js"
67
+ },
27
68
  "./package.json": "./package.json",
28
69
  "./*": {
29
70
  "types": "./dist/*.d.ts",
@@ -41,9 +82,7 @@
41
82
  "format": "prettier --write .",
42
83
  "lint:fix": "npm run format && npm run lint -- --fix",
43
84
  "audit:theme": "node scripts/theme-audit.mjs",
44
- "changeset": "changeset",
45
- "version": "changeset version",
46
- "release": "npm run build && changeset publish"
85
+ "release": "semantic-release"
47
86
  },
48
87
  "dependencies": {
49
88
  "@floating-ui/dom": "^1.8.0",
@@ -127,12 +166,14 @@
127
166
  }
128
167
  },
129
168
  "devDependencies": {
130
- "@changesets/cli": "^2.27.9",
131
169
  "@dnd-kit/core": "^6.3.1",
132
170
  "@dnd-kit/sortable": "^10.0.0",
133
171
  "@dnd-kit/utilities": "^3.2.2",
134
172
  "@eslint/js": "^9.34.0",
173
+ "@semantic-release/changelog": "^6.0.3",
174
+ "@semantic-release/git": "^10.0.1",
135
175
  "@tanstack/react-table": "^8.21.3",
176
+ "@types/node": "^24.13.3",
136
177
  "@types/react": "^18.3.12",
137
178
  "@types/react-dom": "^18.3.1",
138
179
  "@types/react-window": "^1.8.8",
@@ -141,6 +182,7 @@
141
182
  "@vitejs/plugin-react-swc": "^3.5.0",
142
183
  "apexcharts": "3.52.0",
143
184
  "autoprefixer": "^10.4.19",
185
+ "conventional-changelog-conventionalcommits": "^8.0.0",
144
186
  "date-fns": "^4.0.0",
145
187
  "eslint": "^9.34.0",
146
188
  "eslint-config-prettier": "^10.1.8",
@@ -166,9 +208,14 @@
166
208
  "react-toastify": "^10.0.6",
167
209
  "react-window": "^1.8.11",
168
210
  "remark-gfm": "^4.0.0",
211
+ "semantic-release": "^24.2.0",
169
212
  "tailwindcss": "^3.4.7",
170
213
  "typescript": "^5.2.2",
171
214
  "vite": "^6.3.2",
172
215
  "vite-plugin-dts": "^4.5.4"
216
+ },
217
+ "repository": {
218
+ "type": "git",
219
+ "url": "git+https://github.com/UmodeApp/umode-ui.git"
173
220
  }
174
221
  }
@@ -0,0 +1,490 @@
1
+ @import './theme.css';
2
+
3
+
4
+ @tailwind base;
5
+ @tailwind components;
6
+ @tailwind utilities;
7
+
8
+ /* Fluid interface density. Tailwind utilities are rem-based, so scaling the root
9
+ font-size scales typography, spacing, control heights and icons together as one
10
+ system. Full size (1rem = the user's default, 16px) on viewports >= 1368px (the
11
+ `desktop` breakpoint), so fullscreen notebooks stay pixel-identical; shrinks
12
+ linearly below that down to 94% at <= 1152px (compact windows, split screens).
13
+ The rem-based clamp keeps user font-size preferences and browser zoom working,
14
+ and breakpoints stay px-based so layout switches do not feed back into this
15
+ scale. */
16
+ html {
17
+ font-size: clamp(0.94rem, 0.62rem + 0.4444vw, 1rem);
18
+ background-color: rgb(var(--color-canvas));
19
+ }
20
+
21
+ body {
22
+ background-color: rgb(var(--color-canvas));
23
+ color: rgb(var(--color-foreground));
24
+ }
25
+
26
+ /* ---------------------------------------------------------------------------
27
+ Theme switch.
28
+
29
+ The swap itself is instant: every colour resolves from a CSS variable, so one
30
+ attribute write repaints the whole UI already correct.
31
+
32
+ This class exists because instant was NOT instant. Around 350 components carry
33
+ their own `transition-colors`, so on a theme change each of them animated on its
34
+ own clock (140ms here, 200ms there, 280ms on a collapse) while untransitioned
35
+ elements changed at once. The page arrived in waves and the borders landed last.
36
+ Cancelling those transitions for the duration of the swap is what makes it read
37
+ as ONE change.
38
+
39
+ Measured on a 5.4k-node page carrying the app's real transition classes:
40
+ instant, components still transitioning ...... 406ms of blocked main thread
41
+ instant + this suppression ................... 49ms
42
+ one uniform transition on `*` ................ 627ms
43
+ tokens typed with @property and morphed ...... 13fps, main thread starved
44
+
45
+ The last two are why there is no animation here. Any smooth transition between
46
+ two full-page themes is either a BLEND of both (illegible at the midpoint: dark
47
+ text on light mixed with light text on dark is grey on grey) or a repaint of
48
+ every element on every frame. Neither is acceptable, so the swap is atomic and
49
+ the only motion left is the toggle's own knob.
50
+ --------------------------------------------------------------------------- */
51
+ .theme-switching,
52
+ .theme-switching *,
53
+ .theme-switching *::before,
54
+ .theme-switching *::after {
55
+ transition: none !important;
56
+ }
57
+
58
+ /* ---------------------------------------------------------------------------
59
+ Motion.
60
+
61
+ One vocabulary so a dropdown, a drawer and a card hover do not each invent a
62
+ duration. Entering uses a decelerating curve (arrives and settles), leaving
63
+ uses an accelerating one (gets out of the way). Everything animates only
64
+ `opacity` and `transform`, which the compositor handles without layout.
65
+
66
+ Durations are deliberately short: this is a work tool, and animation that
67
+ delays a click is a tax, not a delight.
68
+ --------------------------------------------------------------------------- */
69
+ @keyframes surface-in {
70
+ from {
71
+ opacity: 0;
72
+ transform: translate3d(0, -0.375rem, 0) scale(0.98);
73
+ }
74
+ to {
75
+ opacity: 1;
76
+ transform: translate3d(0, 0, 0) scale(1);
77
+ }
78
+ }
79
+
80
+ @keyframes scrim-in {
81
+ from {
82
+ opacity: 0;
83
+ }
84
+ to {
85
+ opacity: 1;
86
+ }
87
+ }
88
+
89
+ /* Floating surfaces that Material Tailwind mounts on open. Targeting the roles
90
+ rather than the components means a new menu inherits the motion for free. */
91
+ [role='menu'],
92
+ [role='tooltip'],
93
+ [data-popover='true'],
94
+ .glass,
95
+ .glass-elevated {
96
+ animation: surface-in 160ms cubic-bezier(0.16, 1, 0.3, 1);
97
+ }
98
+
99
+ .glass-scrim {
100
+ animation: scrim-in 160ms cubic-bezier(0.4, 0, 0.2, 1);
101
+ }
102
+
103
+ /* The interaction contract for anything clickable: colour and elevation move on
104
+ the same clock, so hover feels like one system instead of per-component taste. */
105
+ .interactive {
106
+ transition-property: background-color, border-color, color, box-shadow, opacity, transform;
107
+ transition-duration: 140ms;
108
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
109
+ }
110
+
111
+ .interactive:active {
112
+ transform: scale(0.985);
113
+ }
114
+
115
+ /* Reduced motion: movement goes, feedback stays. Opacity-only transitions are
116
+ kept because removing them entirely makes state changes feel broken, and a
117
+ crossfade is not the motion this setting protects against. */
118
+ @media (prefers-reduced-motion: reduce) {
119
+ [role='menu'],
120
+ [role='tooltip'],
121
+ [data-popover='true'],
122
+ .glass,
123
+ .glass-elevated,
124
+ .glass-scrim {
125
+ animation-name: scrim-in;
126
+ animation-duration: 100ms;
127
+ }
128
+
129
+ .interactive:active {
130
+ transform: none;
131
+ }
132
+ }
133
+
134
+ /* ---------------------------------------------------------------------------
135
+ Forced theme islands.
136
+
137
+ `data-theme` re-declares the whole token set, so any subtree can pin itself to a
138
+ theme. This is how print previews stay truthful: what the user sees on screen is
139
+ what comes out of the printer, regardless of the app theme.
140
+ --------------------------------------------------------------------------- */
141
+ [data-theme='light'],
142
+ [data-theme='dark'] {
143
+ background-color: rgb(var(--color-canvas));
144
+ color: rgb(var(--color-foreground));
145
+ }
146
+
147
+ @media print {
148
+ :root {
149
+ color-scheme: light;
150
+ }
151
+ }
152
+
153
+ /* ---------------------------------------------------------------------------
154
+ Focus visibility (WCAG 2.4.7 / 1.4.11).
155
+
156
+ One ring for the whole app, driven by the `ring` token, so it has 3:1 against
157
+ both canvas and surface in either theme. `:focus-visible` (not `:focus`) keeps
158
+ it off mouse clicks. Components that draw their own ring opt out by setting
159
+ `outline: none` plus their own `focus-visible:` classes.
160
+ --------------------------------------------------------------------------- */
161
+ :focus-visible {
162
+ outline: 2px solid rgb(var(--color-ring));
163
+ outline-offset: 2px;
164
+ }
165
+
166
+ /* The MT primitives ship `focus:outline-none`, which would swallow the ring above;
167
+ these re-assert it for keyboard users only. */
168
+ button:focus-visible,
169
+ a:focus-visible,
170
+ [role='button']:focus-visible,
171
+ [tabindex]:focus-visible {
172
+ outline: 2px solid rgb(var(--color-ring));
173
+ outline-offset: 2px;
174
+ }
175
+
176
+ /* Editable text fields always match :focus-visible in Chromium, and this block sits
177
+ after the utilities layer, so their `outline-none` class loses the cascade. They
178
+ draw their own focus chrome (RichTextEditor border/ring, or the host's
179
+ focus-within ring around the bare variant), so the global rectangle would double
180
+ the indicator and ignore the rounded container. */
181
+ [contenteditable]:focus-visible {
182
+ outline: none;
183
+ }
184
+
185
+ ::-webkit-scrollbar {
186
+ width: 7px;
187
+ height: 7px;
188
+ }
189
+
190
+ .scrollbar-hide {
191
+ -ms-overflow-style: none;
192
+ scrollbar-width: none;
193
+ }
194
+
195
+ .scrollbar-hide::-webkit-scrollbar {
196
+ display: none;
197
+ }
198
+
199
+ ::-webkit-scrollbar-thumb {
200
+ background-color: rgb(var(--color-border-strong) / 0.55);
201
+ border-radius: 10px;
202
+ }
203
+
204
+ ::-webkit-scrollbar-thumb:hover {
205
+ background-color: rgb(var(--color-border-strong) / 0.8);
206
+ }
207
+
208
+ ::-webkit-scrollbar-track {
209
+ background-color: transparent;
210
+ }
211
+
212
+ * {
213
+ padding: 0;
214
+ margin: 0;
215
+ box-sizing: border-box;
216
+ font-family: 'Inter', sans-serif;
217
+ }
218
+
219
+ input:focus {
220
+ box-shadow: none !important;
221
+ }
222
+
223
+ /* Chrome paints its own autofill background, which ignores every theme token. The
224
+ giant inset shadow is the only way to repaint it, and it has to follow the theme
225
+ or autofilled fields turn into white blocks in dark mode. */
226
+ input:-webkit-autofill,
227
+ input:-webkit-autofill:hover,
228
+ input:-webkit-autofill:focus,
229
+ input:-webkit-autofill:active {
230
+ -webkit-box-shadow: 0 0 0 1000px rgb(var(--color-surface)) inset !important;
231
+ box-shadow: 0 0 0 1000px rgb(var(--color-surface)) inset !important;
232
+ -webkit-text-fill-color: rgb(var(--color-foreground)) !important;
233
+ caret-color: rgb(var(--color-foreground));
234
+ transition: background-color 5000s ease-in-out 0s !important;
235
+ }
236
+
237
+ /* @tailwindcss/forms paints every form control from `colors.gray.500` and `colors.blue.600`,
238
+ and withMT replaced those with the Material palette. The result is a white fill, a 2.6:1
239
+ border and a blue focus ring frozen into the base layer of every input, select and
240
+ textarea, so a control that does not name its own background renders white in dark mode.
241
+ The selectors below mirror the plugin's at the same specificity, so a utility class still
242
+ wins and only the frozen colors change.
243
+
244
+ The control names its own `field` tokens rather than borrowing `surface` + `border-strong`,
245
+ because the two themes identify a control differently. In LIGHT nothing changed: a white
246
+ field with a 3.28:1 outline. In DARK the control is a FILLED field with a soft rim, which
247
+ is the trade recorded below. */
248
+ input:where(
249
+ [type='text'],
250
+ :not([type]),
251
+ [type='email'],
252
+ [type='url'],
253
+ [type='password'],
254
+ [type='number'],
255
+ [type='date'],
256
+ [type='datetime-local'],
257
+ [type='month'],
258
+ [type='search'],
259
+ [type='tel'],
260
+ [type='time'],
261
+ [type='week']
262
+ ),
263
+ select,
264
+ textarea {
265
+ background-color: rgb(var(--color-field));
266
+ border-color: rgb(var(--color-field-border));
267
+ }
268
+
269
+ input:where(
270
+ [type='text'],
271
+ :not([type]),
272
+ [type='email'],
273
+ [type='url'],
274
+ [type='password'],
275
+ [type='number'],
276
+ [type='date'],
277
+ [type='datetime-local'],
278
+ [type='month'],
279
+ [type='search'],
280
+ [type='tel'],
281
+ [type='time'],
282
+ [type='week']
283
+ ):focus,
284
+ select:focus,
285
+ textarea:focus {
286
+ border-color: rgb(var(--color-ring));
287
+ --tw-ring-color: rgb(var(--color-ring));
288
+ }
289
+
290
+ /* Checkbox and radio keep `border-strong`: they are tiny, they have no fill to be
291
+ identified by, and their whole shape IS the outline. */
292
+ input:where([type='checkbox'], [type='radio']) {
293
+ color: rgb(var(--color-primary));
294
+ background-color: rgb(var(--color-field));
295
+ border-color: rgb(var(--color-border-strong));
296
+ }
297
+
298
+ input:where([type='checkbox'], [type='radio']):focus {
299
+ --tw-ring-color: rgb(var(--color-ring));
300
+ }
301
+
302
+ ::placeholder {
303
+ color: rgb(var(--color-foreground-faint));
304
+ opacity: 1;
305
+ }
306
+
307
+ /* The plugin's own `input::placeholder` outranks the bare `::placeholder` above. */
308
+ input::placeholder,
309
+ textarea::placeholder {
310
+ color: rgb(var(--color-foreground-faint));
311
+ opacity: 1;
312
+ }
313
+
314
+ /* ---------------------------------------------------------------------------
315
+ Text selection.
316
+
317
+ The band is an OPAQUE token, not the brand colour at low alpha. Alpha over a
318
+ dark canvas is what made the selection invisible: `primary / 0.24` composited
319
+ to 1.21:1 against the page in the dark theme, so a selected paragraph looked
320
+ identical to an unselected one. Opaque values land at 2.07:1 (light) and
321
+ 2.16:1 (dark) against every surface, with the ink above 6:1 on the band.
322
+
323
+ Both channels are tokens, so a `data-theme` island (the print preview) selects
324
+ with its own theme instead of the app's.
325
+ --------------------------------------------------------------------------- */
326
+ ::selection {
327
+ background-color: rgb(var(--color-selection));
328
+ color: rgb(var(--color-selection-fg));
329
+ }
330
+
331
+ /* ---------------------------------------------------------------------------
332
+ Disabled and read-only.
333
+
334
+ Two different facts, and they must not share a visual: disabled means "you
335
+ cannot use this", read-only means "this is real content you may read and copy
336
+ but not edit". Neither had any rule here, so a disabled field kept the enabled
337
+ fill and a read-only field was indistinguishable from an editable one.
338
+
339
+ The ink is `foreground-faint`, which is the token's documented job (the 3:1
340
+ non-textual tier), NOT an opacity. Opacity multiplies against the backdrop:
341
+ the same icon at `opacity-40` measures 1.82:1 in light and 2.03:1 in dark,
342
+ while the token holds 4.06:1 / 4.19:1. The fill drops to `muted-active` and
343
+ the border to `border-emphasis` so the control keeps its shape and still reads
344
+ as switched off.
345
+ --------------------------------------------------------------------------- */
346
+ input:where(:disabled),
347
+ select:where(:disabled),
348
+ textarea:where(:disabled) {
349
+ background-color: rgb(var(--color-field-disabled));
350
+ border-color: rgb(var(--color-border-emphasis));
351
+ color: rgb(var(--color-foreground-subtle));
352
+ cursor: not-allowed;
353
+ }
354
+
355
+ input:where(:disabled)::placeholder,
356
+ textarea:where(:disabled)::placeholder {
357
+ color: rgb(var(--color-foreground-faint));
358
+ }
359
+
360
+ input:where([type='checkbox'], [type='radio']):where(:disabled) {
361
+ background-color: rgb(var(--color-field-disabled));
362
+ border-color: rgb(var(--color-border-emphasis));
363
+ color: rgb(var(--color-foreground-faint));
364
+ cursor: not-allowed;
365
+ }
366
+
367
+ /* There is deliberately NO `[readonly]` rule here. In this codebase the attribute
368
+ does not mean "locked content": four of its six uses are combobox triggers
369
+ (`ControlledSelect`, `DatePicker`, `DateRangePicker`), where it only blocks
370
+ typing. Styling it as a distinct state would repaint every select in the app. */
371
+
372
+ button:disabled,
373
+ [aria-disabled='true'] {
374
+ cursor: not-allowed;
375
+ }
376
+
377
+ .material-symbols {
378
+ user-select: none;
379
+ font-weight: normal;
380
+ font-style: normal;
381
+ font-size: inherit;
382
+ display: block;
383
+ line-height: 1;
384
+ text-transform: none;
385
+ letter-spacing: normal;
386
+ word-wrap: normal;
387
+ white-space: nowrap;
388
+ direction: ltr;
389
+ }
390
+
391
+ .wf-loading,
392
+ .wf-materialicons-n4-inactive {
393
+ .material-icons {
394
+ display: none;
395
+ }
396
+ }
397
+
398
+ textarea:focus {
399
+ outline: none;
400
+ box-shadow: none;
401
+ }
402
+
403
+ .Toastify__toast-container {
404
+ width: min(40rem, calc(100vw - 2rem)) !important;
405
+ z-index: 2147483647 !important;
406
+ }
407
+
408
+ [role='tooltip'] {
409
+ z-index: 9999 !important;
410
+ cursor: help !important;
411
+ }
412
+
413
+ [role='menu'] {
414
+ overflow: hidden !important;
415
+ }
416
+
417
+ .inter-font {
418
+ font-family: 'Inter', sans-serif !important;
419
+ }
420
+
421
+ .zIndexMax {
422
+ z-index: 9999 !important;
423
+ }
424
+
425
+ /* Rich text authored in RichTextEditor. Colour is stored as a role, never as a literal value,
426
+ so the same message stays legible after a theme switch. Both the editor and every surface
427
+ that renders the sanitized body share these rules. */
428
+ .rt-color-brand {
429
+ color: rgb(var(--color-brand-emphasis));
430
+ }
431
+
432
+ .rt-color-secondary {
433
+ color: rgb(var(--color-secondary-emphasis));
434
+ }
435
+
436
+ .rt-color-accent {
437
+ color: rgb(var(--color-accent-emphasis));
438
+ }
439
+
440
+ .rt-color-success {
441
+ color: rgb(var(--color-success-emphasis));
442
+ }
443
+
444
+ .rt-color-warning {
445
+ color: rgb(var(--color-warning-emphasis));
446
+ }
447
+
448
+ .rt-color-danger {
449
+ color: rgb(var(--color-danger-emphasis));
450
+ }
451
+
452
+ .rt-color-info {
453
+ color: rgb(var(--color-info-emphasis));
454
+ }
455
+
456
+ .rt-color-muted {
457
+ color: rgb(var(--color-foreground-muted));
458
+ }
459
+
460
+ .rt-file {
461
+ display: inline-flex;
462
+ align-items: center;
463
+ gap: 0.25rem;
464
+ max-width: 100%;
465
+ padding: 0.125rem 0.5rem 0.125rem 0.375rem;
466
+ border: 1px solid rgb(var(--color-border));
467
+ border-radius: 0.5rem;
468
+ background-color: rgb(var(--color-muted));
469
+ color: rgb(var(--color-foreground-muted));
470
+ font-size: 0.75rem;
471
+ line-height: 1.25rem;
472
+ text-decoration: none;
473
+ vertical-align: baseline;
474
+ }
475
+
476
+ .rt-file:hover {
477
+ background-color: rgb(var(--color-muted-hover));
478
+ color: rgb(var(--color-foreground));
479
+ }
480
+
481
+ .rt-file::before {
482
+ content: 'attach_file';
483
+ font-family: 'Material Symbols Outlined', 'Material Icons', sans-serif;
484
+ font-size: 1rem;
485
+ line-height: 1;
486
+ font-weight: normal;
487
+ letter-spacing: normal;
488
+ -webkit-font-feature-settings: 'liga';
489
+ font-feature-settings: 'liga';
490
+ }
@@ -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';