@postxl/generators 1.0.9 → 1.0.11

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 (34) hide show
  1. package/dist/frontend-admin/generators/admin-sidebar.generator.js +7 -12
  2. package/dist/frontend-admin/generators/admin-sidebar.generator.js.map +1 -1
  3. package/dist/frontend-admin/generators/audit-log-sidebar.generator.js +36 -53
  4. package/dist/frontend-admin/generators/audit-log-sidebar.generator.js.map +1 -1
  5. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/checkbox-cell.tsx +3 -3
  6. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/date-cell.tsx +1 -2
  7. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/long-text-cell.tsx +1 -0
  8. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/multi-select-cell.tsx +5 -9
  9. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/number-cell.tsx +5 -3
  10. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/react-node-cell.tsx +2 -1
  11. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/select-cell.tsx +4 -4
  12. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/short-text-cell.tsx +4 -3
  13. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/utils/gantt-timeline.tsx +6 -5
  14. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-cell-wrapper.tsx +2 -2
  15. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-cell.tsx +7 -6
  16. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-column-header.tsx +13 -20
  17. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-row.tsx +2 -2
  18. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-search.tsx +2 -2
  19. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-utils.ts +3 -14
  20. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-view-menu.tsx +3 -3
  21. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid.tsx +6 -6
  22. package/dist/frontend-core/template/src/components/ui/data-grid/styles.css +3 -0
  23. package/dist/frontend-core/template/src/components/ui/data-table/data-table.tsx +11 -12
  24. package/dist/frontend-core/template/src/context-providers/theme-context-provider.tsx +52 -67
  25. package/dist/frontend-core/template/src/main.tsx +1 -1
  26. package/dist/frontend-core/template/src/pages/dashboard/dashboard.page.tsx +4 -4
  27. package/dist/frontend-core/template/src/styles/styles.css +102 -2
  28. package/dist/frontend-core/template/src/styles/theme-default.css +108 -178
  29. package/dist/frontend-trpc-client/generators/model-hook.generator.js +3 -11
  30. package/dist/frontend-trpc-client/generators/model-hook.generator.js.map +1 -1
  31. package/dist/frontend-trpc-client/trpc-client.generator.d.ts +1 -7
  32. package/dist/frontend-trpc-client/trpc-client.generator.js +1 -6
  33. package/dist/frontend-trpc-client/trpc-client.generator.js.map +1 -1
  34. package/package.json +1 -1
@@ -28,6 +28,7 @@ import {
28
28
  } from '@components/ui/dropdown-menu/dropdown-menu'
29
29
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@components/ui/tooltip/tooltip'
30
30
  import { cn } from '@lib/utils'
31
+
31
32
  import { GanttTimeline } from './cell-variants/utils/gantt-timeline'
32
33
 
33
34
  function getColumnVariant(variant?: Cell['variant']): {
@@ -139,7 +140,7 @@ export function DataGridColumnHeader<TData, TValue>({
139
140
  <DropdownMenu>
140
141
  <DropdownMenuTrigger
141
142
  className={cn(
142
- 'flex size-full items-center justify-between gap-2 p-2 text-sm bg-sidebar-accent/80 font-bold hover:bg-neutral-100/40 data-[state=open]:bg-neutral-100/40 [&_svg]:size-4 dark:hover:bg-neutral-800/40 dark:data-[state=open]:bg-neutral-800/40',
143
+ 'flex size-full items-center justify-between gap-2 p-2 text-sm bg-sidebar-accent/80 font-bold hover:bg-secondary/40 data-[state=open]:bg-secondary/40 [&_svg]:size-4',
143
144
  isAnyColumnResizing && 'pointer-events-none',
144
145
  className,
145
146
  )}
@@ -151,7 +152,7 @@ export function DataGridColumnHeader<TData, TValue>({
151
152
  <TooltipProvider>
152
153
  <Tooltip delayDuration={100}>
153
154
  <TooltipTrigger asChild>
154
- <columnVariant.icon className="size-3.5 shrink-0 text-neutral-500 dark:text-neutral-400" />
155
+ <columnVariant.icon className="size-3.5 shrink-0 text-muted-foreground" />
155
156
  </TooltipTrigger>
156
157
  <TooltipContent side="top">
157
158
  <p>{columnVariant.label}</p>
@@ -161,13 +162,13 @@ export function DataGridColumnHeader<TData, TValue>({
161
162
  )}
162
163
  <span className="truncate">{label}</span>
163
164
  </div>
164
- <ChevronDownIcon className="shrink-0 text-neutral-500 dark:text-neutral-400" />
165
+ <ChevronDownIcon className="shrink-0 text-muted-foreground" />
165
166
  </DropdownMenuTrigger>
166
167
  <DropdownMenuContent align="start" sideOffset={0} className="w-60">
167
168
  {column.getCanSort() && (
168
169
  <>
169
170
  <DropdownMenuCheckboxItem
170
- className="relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-neutral-500 dark:[&_svg]:text-neutral-400"
171
+ className="relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground"
171
172
  checked={column.getIsSorted() === 'asc'}
172
173
  onClick={() => onSortingChange('asc')}
173
174
  >
@@ -175,7 +176,7 @@ export function DataGridColumnHeader<TData, TValue>({
175
176
  Sort asc
176
177
  </DropdownMenuCheckboxItem>
177
178
  <DropdownMenuCheckboxItem
178
- className="relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-neutral-500 dark:[&_svg]:text-neutral-400"
179
+ className="relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground"
179
180
  checked={column.getIsSorted() === 'desc'}
180
181
  onClick={() => onSortingChange('desc')}
181
182
  >
@@ -195,29 +196,23 @@ export function DataGridColumnHeader<TData, TValue>({
195
196
  {column.getCanSort() && <DropdownMenuSeparator />}
196
197
 
197
198
  {isPinnedLeft ? (
198
- <DropdownMenuItem className="[&_svg]:text-neutral-500 dark:[&_svg]:text-neutral-400" onClick={onUnpin}>
199
+ <DropdownMenuItem className="[&_svg]:text-muted-foreground" onClick={onUnpin}>
199
200
  <PinOffIcon />
200
201
  Unpin from left
201
202
  </DropdownMenuItem>
202
203
  ) : (
203
- <DropdownMenuItem
204
- className="[&_svg]:text-neutral-500 dark:[&_svg]:text-neutral-400"
205
- onClick={onLeftPin}
206
- >
204
+ <DropdownMenuItem className="[&_svg]:text-muted-foreground" onClick={onLeftPin}>
207
205
  <PinIcon />
208
206
  Pin to left
209
207
  </DropdownMenuItem>
210
208
  )}
211
209
  {isPinnedRight ? (
212
- <DropdownMenuItem className="[&_svg]:text-neutral-500 dark:[&_svg]:text-neutral-400" onClick={onUnpin}>
210
+ <DropdownMenuItem className="[&_svg]:text-muted-foreground" onClick={onUnpin}>
213
211
  <PinOffIcon />
214
212
  Unpin from right
215
213
  </DropdownMenuItem>
216
214
  ) : (
217
- <DropdownMenuItem
218
- className="[&_svg]:text-neutral-500 dark:[&_svg]:text-neutral-400"
219
- onClick={onRightPin}
220
- >
215
+ <DropdownMenuItem className="[&_svg]:text-muted-foreground" onClick={onRightPin}>
221
216
  <PinIcon />
222
217
  Pin to right
223
218
  </DropdownMenuItem>
@@ -228,7 +223,7 @@ export function DataGridColumnHeader<TData, TValue>({
228
223
  <>
229
224
  <DropdownMenuSeparator />
230
225
  <DropdownMenuCheckboxItem
231
- className="relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-neutral-500 dark:[&_svg]:text-neutral-400"
226
+ className="relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground"
232
227
  checked={!column.getIsVisible()}
233
228
  onClick={() => column.toggleVisibility(false)}
234
229
  >
@@ -292,12 +287,10 @@ function DataGridColumnResizerImpl<TData, TValue>({ header, table, label }: Data
292
287
  className={cn(
293
288
  'right-0 absolute top-0 z-50 h-full w-0 touch-none select-none cursor-ew-resize focus:outline-none',
294
289
  // visible thin line (use right positioning and a stable 1px width)
295
- "before:content-[''] before:absolute before:inset-y-0 before:right-0 before:translate-x-1/2 before:w-px before:h-full before:bg-neutral-700 dark:before:bg-neutral-400 before:opacity-0 before:transition-opacity before:duration-150",
290
+ "before:content-[''] before:absolute before:inset-y-0 before:right-0 before:translate-x-1/2 before:w-px before:h-full before:bg-accent-foreground/70 before:opacity-0 before:transition-opacity before:duration-150",
296
291
  // large invisible hit area
297
292
  "after:content-[''] after:absolute after:inset-y-0 after:right-0 after:translate-x-1/2 after:w-[18px] after:h-full after:bg-transparent",
298
- header.column.getIsResizing()
299
- ? 'before:opacity-100 before:bg-neutral-900 dark:before:bg-neutral-50'
300
- : 'hover:before:opacity-100',
293
+ header.column.getIsResizing() ? 'before:opacity-100 before:bg-accent-foreground' : 'hover:before:opacity-100',
301
294
  )}
302
295
  style={{ willChange: 'transform, opacity', transform: 'translateZ(0)' }}
303
296
  onDoubleClick={onDoubleClick}
@@ -79,7 +79,7 @@ function DataGridRowImpl<TData>({
79
79
  ref={rowRef}
80
80
  tabIndex={-1}
81
81
  className={cn(
82
- 'absolute flex w-full border-b h-[calc(var(--line-height-sm)*(var(--line-count))+12px)]',
82
+ 'absolute flex w-full border-b h-[calc(var(--data-grid-line-height)*(var(--line-count))+12px)]',
83
83
  className,
84
84
  )}
85
85
  style={{ '--line-count': `${getLineCount(rowHeight)}` } as React.CSSProperties}
@@ -107,7 +107,7 @@ function DataGridRowImpl<TData>({
107
107
  {typeof cell.column.columnDef.header === 'function' ? (
108
108
  <div
109
109
  className={cn('size-full px-3 py-1.5', {
110
- 'bg-neutral-900/10 dark:bg-neutral-50/10': isRowSelected,
110
+ 'bg-accent-foreground/10': isRowSelected,
111
111
  })}
112
112
  >
113
113
  {flexRender(cell.column.columnDef.cell, cell.getContext())}
@@ -152,7 +152,7 @@ function DataGridSearchImpl({
152
152
  <div
153
153
  role="search"
154
154
  data-slot="grid-search"
155
- className="fade-in-0 slide-in-from-top-2 absolute top-4 right-4 z-50 flex animate-in flex-col gap-2 rounded-lg border border-neutral-200 bg-white p-2 shadow-lg dark:border-neutral-800 dark:bg-neutral-950"
155
+ className="fade-in-0 slide-in-from-top-2 absolute top-4 right-4 z-50 flex animate-in flex-col gap-2 rounded-lg border border-border bg-background p-2 shadow-lg"
156
156
  >
157
157
  <div className="flex items-center gap-2">
158
158
  <Input
@@ -195,7 +195,7 @@ function DataGridSearchImpl({
195
195
  </Button>
196
196
  </div>
197
197
  </div>
198
- <div className="flex items-center gap-1 whitespace-nowrap text-neutral-500 text-xs dark:text-neutral-400">
198
+ <div className="flex items-center gap-1 whitespace-nowrap text-muted-foreground">
199
199
  {searchMatches.length > 0 ? (
200
200
  <span>
201
201
  {matchIndex + 1} of {searchMatches.length}
@@ -20,21 +20,10 @@ export function parseCellKey(cellKey: string): Required<CellPosition> {
20
20
  }
21
21
 
22
22
  export function getRowHeightValue(rowHeight: RowHeightValue): number {
23
- // TODO: find a way to use the css variable directly (currently the style might not be loaded yet which would lead to wrong numbers)
24
- const lineHeight = /* Number.parseFloat(
25
- getComputedStyle(document.documentElement)
26
- .getPropertyValue('--line-height-sm')
27
- .trim()
28
- .replaceAll(/[^\d.-]/g, ''),
29
- ) */ 16
30
- /* const rowHeightMap: Record<RowHeightValue, number> = {
31
- short: lineHeight * getLineCount(rowHeight) + 12,
32
- medium: lineHeight * getLineCount(rowHeight) + 12,
33
- tall: lineHeight * getLineCount(rowHeight) + 12,
34
- 'extra-tall': lineHeight * getLineCount(rowHeight) + 12,
35
- } */
23
+ // TODO: Make lineHeight configurable / use css variable: --data-grid-line-height
24
+ const lineHeight = 16
36
25
 
37
- return /* rowHeightMap[rowHeight] */ lineHeight * getLineCount(rowHeight) + 12
26
+ return lineHeight * getLineCount(rowHeight) + 12
38
27
  }
39
28
 
40
29
  export function getLineCount(rowHeight: RowHeightValue): number {
@@ -251,7 +251,7 @@ export function DataGridViewMenu<TData>({ table, ...props }: DataGridViewMenuPro
251
251
  size="sm"
252
252
  className="ml-auto hidden h-8 font-normal lg:flex"
253
253
  >
254
- <MixerHorizontalIcon className="text-neutral-500 dark:text-neutral-400" />
254
+ <MixerHorizontalIcon className="text-muted-foreground" />
255
255
  View
256
256
  </Button>
257
257
  </PopoverTrigger>
@@ -310,7 +310,7 @@ export function DataGridViewMenu<TData>({ table, ...props }: DataGridViewMenuPro
310
310
  startPointerDrag(e, column.id)
311
311
  }}
312
312
  className={cn(
313
- 'size-4 text-neutral-400',
313
+ 'size-4 text-muted-foreground',
314
314
  searchQuery.trim() !== '' && 'opacity-10',
315
315
  searchQuery.trim() === '' && (isDragging ? 'cursor-grabbing' : 'cursor-grab'),
316
316
  )}
@@ -326,7 +326,7 @@ export function DataGridViewMenu<TData>({ table, ...props }: DataGridViewMenuPro
326
326
  ? column.getIsVisible()
327
327
  ? 'opacity-100'
328
328
  : 'opacity-0'
329
- : 'opacity-50 text-neutral-400',
329
+ : 'opacity-50 text-muted-foreground',
330
330
  )}
331
331
  />
332
332
  </CommandItem>
@@ -82,7 +82,7 @@ export function DataGrid<TData>({
82
82
  data-slot="grid"
83
83
  tabIndex={0}
84
84
  ref={dataGridRef}
85
- className="relative grid select-none overflow-auto rounded-md border border-neutral-200 focus:outline-none dark:border-neutral-800"
85
+ className="relative grid select-none overflow-auto rounded-md border border-border focus:outline-none"
86
86
  style={{
87
87
  ...columnSizeVars,
88
88
  // dynamically include measured header/footer heights (falling back to previous magic numbers)
@@ -98,7 +98,7 @@ export function DataGrid<TData>({
98
98
  role="rowgroup"
99
99
  data-slot="grid-header"
100
100
  ref={headerRef}
101
- className="sticky top-0 z-10 grid border-b bg-white dark:bg-neutral-950"
101
+ className="sticky top-0 z-10 grid border-b bg-background"
102
102
  >
103
103
  {table.getHeaderGroups().map((headerGroup, rowIndex) => (
104
104
  <div
@@ -139,7 +139,7 @@ export function DataGrid<TData>({
139
139
  }}
140
140
  >
141
141
  {header.isPlaceholder ? null : typeof header.column.columnDef.header === 'function' ? (
142
- <div className="size-full px-3 py-1.5 bg-sidebar-accent/80 hover:bg-neutral-100/40 dark:hover:bg-neutral-800/40">
142
+ <div className="size-full px-3 py-1.5 bg-secondary/80 hover:bg-secondary/40">
143
143
  {flexRender(header.column.columnDef.header, header.getContext())}
144
144
  </div>
145
145
  ) : (
@@ -183,7 +183,7 @@ export function DataGrid<TData>({
183
183
  role="rowgroup"
184
184
  data-slot="grid-footer"
185
185
  ref={footerRef}
186
- className="sticky bottom-0 z-10 grid border-t bg-white dark:bg-neutral-950"
186
+ className="sticky bottom-0 z-10 grid border-t bg-background"
187
187
  >
188
188
  <div
189
189
  role="row"
@@ -195,7 +195,7 @@ export function DataGrid<TData>({
195
195
  <div
196
196
  role="gridcell"
197
197
  tabIndex={0}
198
- className="relative flex h-9 grow items-center bg-neutral-100/30 transition-colors hover:bg-neutral-100/50 focus:bg-neutral-100/50 focus:outline-none dark:bg-neutral-800/30 dark:hover:bg-neutral-800/50 dark:focus:bg-neutral-800/50"
198
+ className="relative flex h-9 grow items-center bg-secondary/30 transition-colors hover:bg-secondary/50 focus:bg-secondary/50 focus:outline-none"
199
199
  style={{
200
200
  width: table.getTotalSize(),
201
201
  minWidth: table.getTotalSize(),
@@ -203,7 +203,7 @@ export function DataGrid<TData>({
203
203
  onClick={onRowAdd}
204
204
  onKeyDown={onAddRowKeyDown}
205
205
  >
206
- <div className="sticky left-0 flex items-center gap-2 px-3 text-neutral-500 dark:text-neutral-400">
206
+ <div className="sticky left-0 flex items-center gap-2 px-3 text-muted-foreground">
207
207
  <PlusIcon className="size-3.5" />
208
208
  <span className="text-sm">Add row</span>
209
209
  </div>
@@ -0,0 +1,3 @@
1
+ @theme inline {
2
+ --data-grid-line-height: 16px;
3
+ }
@@ -1,6 +1,6 @@
1
1
  import '@glideapps/glide-data-grid/dist/index.css'
2
2
 
3
- import { useTheme } from '@context-providers/theme-context-provider'
3
+ import { type Theme as ContextTheme, useTheme } from '@context-providers/theme-context-provider'
4
4
  import {
5
5
  type CellClickedEventArgs,
6
6
  DataEditor,
@@ -125,20 +125,19 @@ export function DataTable<Row extends object, ColumnId extends string = string>(
125
125
  }: DataTableProps<Row, ColumnId>) {
126
126
  const sortOrder = currentSort?.order === 'asc' ? 'sortAscending' : 'sortDescending'
127
127
  const [managedColumns, setManagedColumns] = useState<ManagedDataTableColumn<Row, ColumnId>[]>([])
128
- const { theme, isThemeReady } = useTheme()
128
+ const { brand, colorMode } = useTheme()
129
+ const theme: ContextTheme = `${brand} ${colorMode}`
129
130
  const [tableTheme, setTableTheme] = useState<Partial<Theme> | undefined>()
130
131
 
131
- const getTheme = useCallback(() => getTableTheme(theme), [theme])
132
-
133
132
  useEffect(() => {
134
- // we need this timeout to make sure 'getTableTheme' returns the correct css variables
135
- if (isThemeReady) {
136
- setTimeout(() => {
137
- const newTheme = getTheme()
138
- setTableTheme(newTheme)
139
- }, 0)
140
- }
141
- }, [getTheme, isThemeReady, theme])
133
+ const observer = new MutationObserver(() => {
134
+ setTableTheme(getTableTheme(theme))
135
+ })
136
+
137
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['style', 'class'] })
138
+
139
+ return () => observer.disconnect()
140
+ }, [theme])
142
141
 
143
142
  useEffect(() => {
144
143
  setManagedColumns(
@@ -1,100 +1,85 @@
1
- import { createContext, useEffect, useMemo, useState } from 'react'
1
+ import { createContext, useEffect, useMemo } from 'react'
2
2
  import useLocalStorageState from 'use-local-storage-state'
3
3
 
4
4
  import { createUseContext } from '@lib/react'
5
5
 
6
- export type Theme = 'dark' | 'light' | 'system'
7
- type ThemeLightDark = 'dark' | 'light'
6
+ const ALLOWED_BRANDS = ['client', 'default'] as const
7
+
8
+ export type ColorMode = 'dark' | 'light' | 'system'
9
+ export type Brand = (typeof ALLOWED_BRANDS)[number]
10
+ export type Theme = `${Brand} ${ColorMode}`
11
+ type ResolvedColorMode = 'dark' | 'light'
8
12
 
9
13
  type ThemeProviderProps = {
10
14
  children: React.ReactNode
11
- defaultTheme?: Theme
15
+ defaultBrand?: Brand
16
+ defaultColorMode?: ColorMode
12
17
  storageKey?: string
13
18
  }
14
19
 
15
20
  type ThemeProviderState = {
16
- theme: Theme
17
- setTheme: (theme: Theme) => void
18
- isThemeReady: boolean
19
- }
20
-
21
- const initialState: ThemeProviderState = {
22
- theme: 'system',
23
- setTheme: () => null,
24
- isThemeReady: false,
21
+ brand: Brand
22
+ colorMode: ColorMode
23
+ setColorMode: (colorMode: ColorMode) => void
24
+ setBrand: (brand: Brand) => void
25
25
  }
26
26
 
27
- const ThemeContext = createContext<ThemeProviderState>(initialState)
28
- const STORAGE_KEY = 'usp-theme-mode'
29
-
30
- function nextFrame(): Promise<void> {
31
- return new Promise((r) => requestAnimationFrame(() => r()))
27
+ type ThemeStorage = {
28
+ brand: Brand
29
+ colorMode: ColorMode
32
30
  }
33
31
 
34
- /**
35
- * here we wait until the css var '--theme-ready-flag' is set, so we know the css is loaded
36
- * we need this 'hack' because we set the values of the DataTable with js and read the values from the css file.
37
- * to make sure these values are not undefined, we need to wait until the css is loaded.
38
- */
39
- async function waitForThemeApplied(expected: Theme, timeoutMs = 500): Promise<boolean> {
40
- const start = performance.now()
41
- // Give the browser a paint opportunity
42
- await nextFrame()
43
-
44
- while (performance.now() - start < timeoutMs) {
45
- const value = getComputedStyle(document.documentElement)
46
- .getPropertyValue('--theme-ready-flag')
47
- .replaceAll(/['"\s]/g, '')
48
- if (value === expected) {
49
- return true
50
- }
51
- await nextFrame()
52
- }
53
- return false
32
+ const initialState: ThemeProviderState = {
33
+ brand: 'default',
34
+ colorMode: 'system',
35
+ setColorMode: () => null,
36
+ setBrand: () => null,
54
37
  }
55
38
 
56
- export function ThemeProvider({ children, defaultTheme = 'system', ...props }: Readonly<ThemeProviderProps>) {
57
- const [theme, setTheme] = useLocalStorageState(STORAGE_KEY, {
58
- defaultValue: defaultTheme,
39
+ const ThemeContext = createContext<ThemeProviderState>(initialState)
40
+ const STORAGE_KEY = 'PXL-theme-mode'
41
+
42
+ export function ThemeProvider({
43
+ children,
44
+ defaultBrand = 'default',
45
+ defaultColorMode = 'system',
46
+ ...props
47
+ }: Readonly<ThemeProviderProps>) {
48
+ const [themeStorage, setThemeStorage] = useLocalStorageState<ThemeStorage>(STORAGE_KEY, {
49
+ defaultValue: { brand: defaultBrand, colorMode: defaultColorMode },
59
50
  })
60
- const [isThemeReady, setIsThemeReady] = useState(false)
51
+ const { brand, colorMode } = themeStorage
61
52
 
62
53
  useEffect(() => {
63
54
  const root = globalThis.document.documentElement
64
55
 
65
- root.classList.remove('light', 'dark')
56
+ root.classList.remove('light', 'dark', ...ALLOWED_BRANDS)
57
+ root.classList.add(brand)
66
58
 
67
- let applied: ThemeLightDark
68
- if (theme === 'system') {
69
- applied = globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
70
- root.classList.add(applied)
59
+ let resolvedColorMode: ResolvedColorMode
60
+ if (colorMode === 'system') {
61
+ resolvedColorMode = globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
62
+ setThemeStorage({ brand, colorMode: resolvedColorMode })
63
+ root.classList.add(resolvedColorMode)
71
64
  return
72
65
  }
73
- applied = theme
74
- root.classList.add(theme)
75
66
 
76
- let cancelled = false
77
-
78
- void (async () => {
79
- // Option A: wait for computed variable
80
- const ok = await waitForThemeApplied(applied)
81
- if (!cancelled) {
82
- setIsThemeReady(ok)
83
- }
84
- })()
85
-
86
- return () => {
87
- cancelled = true
88
- }
89
- }, [theme])
67
+ resolvedColorMode = colorMode
68
+ root.classList.add(resolvedColorMode)
69
+ }, [brand, colorMode, setThemeStorage])
90
70
 
91
71
  const value = useMemo(
92
72
  () => ({
93
- theme,
94
- setTheme,
95
- isThemeReady,
73
+ brand,
74
+ colorMode,
75
+ setColorMode: (newColorMode: ColorMode) => {
76
+ setThemeStorage({ brand, colorMode: newColorMode })
77
+ },
78
+ setBrand: (newBrand: Brand) => {
79
+ setThemeStorage({ brand: newBrand, colorMode })
80
+ },
96
81
  }),
97
- [isThemeReady, setTheme, theme],
82
+ [brand, colorMode, setThemeStorage],
98
83
  )
99
84
 
100
85
  return (
@@ -32,7 +32,7 @@ function App() {
32
32
  <QueryClientProvider client={queryClient}>
33
33
  <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
34
34
  <AuthProvider>
35
- <ThemeProvider defaultTheme="light">
35
+ <ThemeProvider>
36
36
  <AppRouter />
37
37
  </ThemeProvider>
38
38
  </AuthProvider>
@@ -17,14 +17,14 @@ import type { Model } from '@components/ui/spreadsheet/types'
17
17
 
18
18
  export const DashboardPage = () => {
19
19
  const { logout, user } = useAuth()
20
- const { theme, setTheme } = useTheme()
20
+ const { colorMode, setColorMode } = useTheme()
21
21
 
22
22
  const [selectedPost, setSelectedPost] = useState<Post | null>(null)
23
23
  const [isUpdateModalOpen, setIsUpdateModalOpen] = useState(false)
24
24
 
25
25
  const onToggleTheme = () => {
26
- setTheme(theme === 'light' ? 'dark' : 'light')
27
- toast.info("As you may see now it's " + (theme === 'light' ? 'dark' : 'light') + ' theme')
26
+ setColorMode(colorMode === 'light' ? 'dark' : 'light')
27
+ toast.info("As you may see now it's " + (colorMode === 'light' ? 'dark' : 'light') + ' color mode')
28
28
  }
29
29
  const { list } = usePosts()
30
30
 
@@ -43,7 +43,7 @@ export const DashboardPage = () => {
43
43
  title={APP_CONFIG.AUTH ? 'Dashboard (authorized route)' : 'Dashboard'}
44
44
  controls={[
45
45
  <Button key="btn-theme" onClick={onToggleTheme} __e2e_test_id__="button-toggle-theme">
46
- {theme === 'light' ? <MoonIcon /> : <SunIcon />}
46
+ {colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
47
47
  </Button>,
48
48
  APP_CONFIG.AUTH && (
49
49
  <Button key="btn-logout" onClick={logout}>
@@ -3,6 +3,8 @@
3
3
 
4
4
  @import './theme-default.css';
5
5
 
6
+ @import '../components/ui/data-grid/styles.css';
7
+
6
8
  /* important to override the tailwind variant to use our class instead of the preferred color theme (https://tailwindcss.com/docs/dark-mode) */
7
9
  @custom-variant dark (&:where(.dark, .dark *)); /* NOSONAR */
8
10
 
@@ -15,12 +17,110 @@
15
17
  }
16
18
  body {
17
19
  @apply bg-background text-foreground text-base font-normal;
20
+ }
21
+ }
22
+
23
+ @layer components {
24
+ h1 {
25
+ font-size: var(--text-2xl);
26
+ line-height: var(--text-2xl-line-height);
27
+ font-weight: var(--font-weight-semibold);
28
+ }
29
+
30
+ h2 {
31
+ font-size: var(--text-xl);
32
+ line-height: var(--text-xl-line-height);
33
+ font-weight: var(--font-weight-semibold);
34
+ }
18
35
 
19
- /* line height for text-sm (used to determine row height in data-grid) */
20
- --line-height-sm: 16px;
36
+ h3 {
37
+ font-size: var(--text-lg);
38
+ line-height: var(--text-lg-line-height);
39
+ font-weight: var(--font-weight-semibold);
21
40
  }
22
41
  }
23
42
 
43
+ /**
44
+ * the theme will be parsed by the tailwindcss plugin and creates utility classes for each variable
45
+ * @see: https://tailwindcss.com/docs/theme#theme-variable-namespaces
46
+ * @see: defaults: https://tailwindcss.com/docs/theme#default-theme-variable-reference
47
+ * @see: usage of utility classes: https://tailwindcss.com/docs/styling-with-utility-classes
48
+ */
49
+ @theme inline {
50
+ --font-sans: Helvetica, sans-serif;
51
+
52
+ /* table content and other space relevant text */
53
+ --text-sm: 11px;
54
+ --text-sm-line-height: 16px;
55
+ /* 12px (header buttons, dropdowns, menus) */
56
+ --text-base: 12px;
57
+ --text-base-line-height: 16px;
58
+ /* 14px (H3, tab nav menu, news copy) */
59
+ --text-lg: 14px;
60
+ --text-lg-line-height: 20px;
61
+ /* 16px (H2) */
62
+ --text-xl: 16px;
63
+ --text-xl-line-height: 24px;
64
+ /* 24px (H1, large value texts) */
65
+ --text-2xl: 24px;
66
+ --text-2xl-line-height: 32px;
67
+
68
+ /* Colors */
69
+ --color-background: var(--background);
70
+ --color-foreground: var(--foreground);
71
+ --color-card: var(--card);
72
+ --color-card-foreground: var(--card-foreground);
73
+ --color-popover: var(--popover);
74
+ --color-popover-foreground: var(--popover-foreground);
75
+ --color-primary: var(--primary);
76
+ --color-primary-foreground: var(--primary-foreground);
77
+ --color-secondary: var(--secondary);
78
+ --color-secondary-foreground: var(--secondary-foreground);
79
+ --color-muted: var(--muted);
80
+ --color-muted-foreground: var(--muted-foreground);
81
+ --color-accent: var(--accent);
82
+ --color-accent-foreground: var(--accent-foreground);
83
+ --color-destructive: var(--destructive);
84
+ --color-destructive-foreground: var(--destructive-foreground);
85
+ --color-border: var(--border);
86
+ --color-input: var(--input);
87
+ --color-ring: var(--ring);
88
+ --color-chart-1: var(--chart-1);
89
+ --color-chart-2: var(--chart-2);
90
+ --color-chart-3: var(--chart-3);
91
+ --color-chart-4: var(--chart-4);
92
+ --color-chart-5: var(--chart-5);
93
+ --color-sidebar: var(--sidebar);
94
+ --color-sidebar-foreground: var(--sidebar-foreground);
95
+ --color-sidebar-background: light-dark(var(--color-gray-50), var(--color-gray-500));
96
+ --color-sidebar-primary: var(--sidebar-primary);
97
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
98
+ --color-sidebar-accent: var(--sidebar-accent);
99
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
100
+ --color-sidebar-border: var(--sidebar-border);
101
+ --color-sidebar-ring: var(--sidebar-ring);
102
+
103
+ /* Border radius */
104
+ --radius-xs: calc(var(--radius) * 0.25);
105
+ --radius-sm: calc(var(--radius) * 0.5);
106
+ --radius-md: var(--radius);
107
+ --radius-lg: calc(var(--radius) * 1.5);
108
+ --radius-xl: calc(var(--radius) * 2);
109
+ --radius-2xl: calc(var(--radius) * 2.5);
110
+ --radius-3xl: calc(var(--radius) * 3.5);
111
+ --radius-4xl: calc(var(--radius) * 5);
112
+
113
+ /* Shadows */
114
+ --shadow-2xs: var(--shadow-2xs);
115
+ --shadow-xs: var(--shadow-xs);
116
+ --shadow-sm: var(--shadow-sm);
117
+ --shadow: var(--shadow);
118
+ --shadow-md: var(--shadow-md);
119
+ --shadow-lg: var(--shadow-lg);
120
+ --shadow-xl: var(--shadow-xl);
121
+ --shadow-2xl: var(--shadow-2xl);
122
+ }
123
+
24
124
  /**
25
125
  * Tailwind Breakpoints
26
126
  *