@stoker-platform/web-app 0.5.184 → 0.5.186

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/CHANGELOG.md CHANGED
@@ -1,10 +1,22 @@
1
1
  # @stoker-platform/web-app
2
2
 
3
+ ## 0.5.186
4
+
5
+ ### Patch Changes
6
+
7
+ - feat: improve light mode metric colours
8
+
9
+ ## 0.5.185
10
+
11
+ ### Patch Changes
12
+
13
+ - feat: add temporary year picker
14
+
3
15
  ## 0.5.184
4
16
 
5
17
  ### Patch Changes
6
18
 
7
- - fix: perform bulk write in batches
19
+ - fix: perform bulk writes in batches
8
20
 
9
21
  ## 0.5.183
10
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stoker-platform/web-app",
3
- "version": "0.5.184",
3
+ "version": "0.5.186",
4
4
  "type": "module",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "scripts": {
@@ -176,7 +176,7 @@ export const DashboardMetric = ({ metric, title, collection }: DashboardMetricPr
176
176
 
177
177
  return (
178
178
  <div className="grid gap-3 h-full items-center bg-background rounded-xl">
179
- <Card className="p-4 pt-6 pb-6 h-full flex flex-col justify-center items-center bg-blue-500 dark:bg-blue-500/50 text-primary-foreground dark:text-primary border-transparent dark:border-background">
179
+ <Card className="p-4 pt-6 pb-6 h-full flex flex-col justify-center items-center text-blue-500 dark:bg-blue-500/50 dark:text-primary border-transparent dark:border-background">
180
180
  <div className="text-[16px] font-[600] line-clamp-1 text-center">{metricTitle}</div>
181
181
  {connectionStatus === "online" || isPreloadCacheEnabled ? (
182
182
  isLoading || (isPreloadCacheEnabled && isCacheLoading) ? (
@@ -196,6 +196,8 @@ export const DashboardMetric = ({ metric, title, collection }: DashboardMetricPr
196
196
  "leading-none",
197
197
  "overflow-hidden",
198
198
  "break-words",
199
+ "text-primary/80",
200
+ "dark:text-primary",
199
201
  metric.textSize || "text-3xl",
200
202
  )}
201
203
  >
package/src/List.tsx CHANGED
@@ -1556,7 +1556,7 @@ export function List({
1556
1556
  key={`metric-${index}`}
1557
1557
  className={cn("grid gap-3 place-content-center", hideThirdMetric)}
1558
1558
  >
1559
- <Card className="p-4 pt-6 pb-6 h-[175px] min-w-[175px] flex flex-col place-content-center items-center bg-blue-500 dark:bg-blue-500/50 text-primary-foreground dark:text-primary">
1559
+ <Card className="p-4 pt-6 pb-6 h-[175px] min-w-[175px] flex flex-col place-content-center items-center text-blue-500 dark:bg-blue-500/50 dark:text-primary">
1560
1560
  <div className="relative bottom-2">
1561
1561
  <div className="font-semibold line-clamp-1 text-center">
1562
1562
  {metricTitle}
@@ -1573,6 +1573,8 @@ export function List({
1573
1573
  "leading-none",
1574
1574
  "overflow-hidden",
1575
1575
  "break-words",
1576
+ "text-primary/80",
1577
+ "dark:text-primary",
1576
1578
  metric.textSize || "text-3xl",
1577
1579
  )}
1578
1580
  >
@@ -1,17 +1,67 @@
1
1
  import * as React from "react"
2
- import { DayPicker } from "react-day-picker"
2
+ import { format } from "date-fns"
3
+ import { DayPicker, type CaptionLabelProps } from "react-day-picker"
3
4
 
4
5
  import { cn } from "@/lib/utils"
5
6
  import { buttonVariants } from "@/components/ui/button"
6
7
  import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
8
+ import YearPicker from "@/components/ui/year-picker"
7
9
 
8
10
  /* eslint-disable react/prop-types */
9
11
 
10
12
  export type CalendarProps = React.ComponentProps<typeof DayPicker>
11
13
 
12
- function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
14
+ function CalendarCaptionLabel({ displayMonth, id, onYearClick }: CaptionLabelProps & { onYearClick: () => void }) {
15
+ return (
16
+ <div className="text-sm font-medium" aria-live="polite" role="presentation" id={id}>
17
+ <button
18
+ type="button"
19
+ onClick={onYearClick}
20
+ className="rounded-sm p-2 hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
21
+ >
22
+ {format(displayMonth, "MMMM ")}
23
+ {format(displayMonth, "yyyy")}
24
+ </button>
25
+ </div>
26
+ )
27
+ }
28
+
29
+ function Calendar({
30
+ className,
31
+ classNames,
32
+ showOutsideDays = true,
33
+ month: monthProp,
34
+ onMonthChange: onMonthChangeProp,
35
+ defaultMonth,
36
+ fromYear,
37
+ toYear,
38
+ components,
39
+ ...props
40
+ }: CalendarProps) {
41
+ const [showYearPicker, setShowYearPicker] = React.useState(false)
42
+ const [internalMonth, setInternalMonth] = React.useState<Date>(defaultMonth ?? new Date())
43
+
44
+ const month = monthProp ?? internalMonth
45
+ const onMonthChange = onMonthChangeProp ?? setInternalMonth
46
+
47
+ if (showYearPicker) {
48
+ return (
49
+ <YearPicker
50
+ currentMonth={month}
51
+ onYearChange={(newMonth) => {
52
+ onMonthChange(newMonth)
53
+ setShowYearPicker(false)
54
+ }}
55
+ fromYear={fromYear}
56
+ toYear={toYear}
57
+ />
58
+ )
59
+ }
60
+
13
61
  return (
14
62
  <DayPicker
63
+ month={month}
64
+ onMonthChange={onMonthChange}
15
65
  showOutsideDays={showOutsideDays}
16
66
  className={cn("p-3", className)}
17
67
  classNames={{
@@ -52,7 +102,13 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: C
52
102
  components={{
53
103
  IconLeft: () => <ChevronLeftIcon className="h-4 w-4" />,
54
104
  IconRight: () => <ChevronRightIcon className="h-4 w-4" />,
105
+ ...components,
106
+ CaptionLabel: (captionProps) => (
107
+ <CalendarCaptionLabel {...captionProps} onYearClick={() => setShowYearPicker(true)} />
108
+ ),
55
109
  }}
110
+ fromYear={fromYear}
111
+ toYear={toYear}
56
112
  {...props}
57
113
  />
58
114
  )
@@ -0,0 +1,248 @@
1
+ import { format, getYear, isSameYear, setYear, startOfToday } from "date-fns"
2
+ import { ChevronLeft, ChevronRight } from "lucide-react"
3
+ import * as React from "react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+ import { buttonVariants } from "./button"
7
+
8
+ const YEARS_PER_PAGE = 12
9
+ const GRID_COLUMNS = 3
10
+
11
+ interface YearPickerProps {
12
+ currentMonth: Date
13
+ onYearChange: (newMonth: Date) => void
14
+ disabled?: boolean
15
+ fromYear?: number
16
+ toYear?: number
17
+ }
18
+
19
+ export default function YearPicker({
20
+ currentMonth,
21
+ onYearChange,
22
+ disabled,
23
+ fromYear = 1900,
24
+ toYear = 2100,
25
+ }: YearPickerProps) {
26
+ const currentYear = getYear(currentMonth)
27
+ const [startYear, setStartYear] = React.useState(() => Math.floor(currentYear / YEARS_PER_PAGE) * YEARS_PER_PAGE)
28
+ const yearButtonRefs = React.useRef<(HTMLButtonElement | null)[]>([])
29
+
30
+ const years = Array.from({ length: YEARS_PER_PAGE }, (_, index) => startYear + index)
31
+
32
+ const isYearDisabled = React.useCallback(
33
+ (year: number) => disabled || year < fromYear || year > toYear,
34
+ [disabled, fromYear, toYear],
35
+ )
36
+
37
+ const getFirstFocusableIndex = React.useCallback(() => {
38
+ const selectedIndex = years.indexOf(currentYear)
39
+ // eslint-disable-next-line security/detect-object-injection
40
+ if (selectedIndex >= 0 && !isYearDisabled(years[selectedIndex])) {
41
+ return selectedIndex
42
+ }
43
+ return years.findIndex((year) => !isYearDisabled(year))
44
+ }, [years, currentYear, isYearDisabled])
45
+
46
+ const [focusedIndex, setFocusedIndex] = React.useState(0)
47
+
48
+ React.useEffect(() => {
49
+ setStartYear(Math.floor(getYear(currentMonth) / YEARS_PER_PAGE) * YEARS_PER_PAGE)
50
+ }, [currentMonth])
51
+
52
+ React.useEffect(() => {
53
+ setFocusedIndex(getFirstFocusableIndex())
54
+ }, [startYear, getFirstFocusableIndex])
55
+
56
+ React.useEffect(() => {
57
+ // eslint-disable-next-line security/detect-object-injection
58
+ yearButtonRefs.current[focusedIndex]?.focus()
59
+ }, [focusedIndex])
60
+
61
+ const today = startOfToday()
62
+
63
+ function previousPage() {
64
+ setStartYear((year) => Math.max(fromYear, year - YEARS_PER_PAGE))
65
+ }
66
+
67
+ function nextPage() {
68
+ setStartYear((year) => Math.min(toYear - YEARS_PER_PAGE + 1, year + YEARS_PER_PAGE))
69
+ }
70
+
71
+ function getNextFocusableIndex(index: number, step: number) {
72
+ let nextIndex = index + step
73
+ while (nextIndex >= 0 && nextIndex < years.length) {
74
+ // eslint-disable-next-line security/detect-object-injection
75
+ if (!isYearDisabled(years[nextIndex])) {
76
+ return nextIndex
77
+ }
78
+ nextIndex += step
79
+ }
80
+ return index
81
+ }
82
+
83
+ function handleYearKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, index: number, year: number) {
84
+ const yearDate = setYear(currentMonth, year)
85
+
86
+ switch (event.key) {
87
+ case "ArrowRight":
88
+ event.preventDefault()
89
+ setFocusedIndex(getNextFocusableIndex(index, 1))
90
+ break
91
+ case "ArrowLeft":
92
+ event.preventDefault()
93
+ setFocusedIndex(getNextFocusableIndex(index, -1))
94
+ break
95
+ case "ArrowDown": {
96
+ event.preventDefault()
97
+ const column = index % GRID_COLUMNS
98
+ let nextRow = Math.floor(index / GRID_COLUMNS) + 1
99
+ const totalRows = Math.ceil(years.length / GRID_COLUMNS)
100
+ while (nextRow < totalRows) {
101
+ const nextIndex = nextRow * GRID_COLUMNS + column
102
+ // eslint-disable-next-line security/detect-object-injection
103
+ if (nextIndex < years.length && !isYearDisabled(years[nextIndex])) {
104
+ setFocusedIndex(nextIndex)
105
+ return
106
+ }
107
+ nextRow++
108
+ }
109
+ break
110
+ }
111
+ case "ArrowUp": {
112
+ event.preventDefault()
113
+ const column = index % GRID_COLUMNS
114
+ let nextIndex = index - GRID_COLUMNS
115
+ while (nextIndex >= 0) {
116
+ const alignedIndex = Math.floor(nextIndex / GRID_COLUMNS) * GRID_COLUMNS + column
117
+ // eslint-disable-next-line security/detect-object-injection
118
+ if (!isYearDisabled(years[alignedIndex])) {
119
+ setFocusedIndex(alignedIndex)
120
+ return
121
+ }
122
+ nextIndex -= GRID_COLUMNS
123
+ }
124
+ break
125
+ }
126
+ case "Home":
127
+ event.preventDefault()
128
+ setFocusedIndex(getFirstFocusableIndex())
129
+ break
130
+ case "End": {
131
+ event.preventDefault()
132
+ for (let endIndex = years.length - 1; endIndex >= 0; endIndex--) {
133
+ // eslint-disable-next-line security/detect-object-injection
134
+ if (!isYearDisabled(years[endIndex])) {
135
+ setFocusedIndex(endIndex)
136
+ break
137
+ }
138
+ }
139
+ break
140
+ }
141
+ case "PageUp":
142
+ event.preventDefault()
143
+ previousPage()
144
+ break
145
+ case "PageDown":
146
+ event.preventDefault()
147
+ nextPage()
148
+ break
149
+ case "Enter":
150
+ case " ":
151
+ event.preventDefault()
152
+ if (!isYearDisabled(year)) {
153
+ onYearChange(yearDate)
154
+ }
155
+ break
156
+ }
157
+ }
158
+
159
+ return (
160
+ <div className="w-fit p-3">
161
+ <div className="flex w-fit flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0">
162
+ <div className="space-y-4">
163
+ <div className="relative flex w-56 items-center justify-center pt-1">
164
+ <div className="text-sm font-medium" aria-live="polite" role="presentation" id="year-picker">
165
+ {startYear} – {startYear + YEARS_PER_PAGE - 1}
166
+ </div>
167
+ <div className="flex items-center space-x-1">
168
+ <button
169
+ name="previous-years"
170
+ aria-label="Go to previous years"
171
+ className={cn(
172
+ buttonVariants({ variant: "outline" }),
173
+ "absolute left-1 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
174
+ )}
175
+ type="button"
176
+ onClick={previousPage}
177
+ disabled={disabled || startYear <= fromYear}
178
+ >
179
+ <ChevronLeft className="h-4 w-4" />
180
+ </button>
181
+ <button
182
+ name="next-years"
183
+ aria-label="Go to next years"
184
+ className={cn(
185
+ buttonVariants({ variant: "outline" }),
186
+ "absolute right-1 h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
187
+ )}
188
+ type="button"
189
+ onClick={nextPage}
190
+ disabled={disabled || startYear + YEARS_PER_PAGE - 1 >= toYear}
191
+ >
192
+ <ChevronRight className="h-4 w-4" />
193
+ </button>
194
+ </div>
195
+ </div>
196
+ <div className="grid w-56 grid-cols-3 gap-2" role="grid" aria-labelledby="year-picker">
197
+ {years.map((year, index) => {
198
+ const yearDate = setYear(currentMonth, year)
199
+ const isSelected = isSameYear(yearDate, currentMonth)
200
+ const isCurrentYear = isSameYear(yearDate, today)
201
+ const isDisabled = isYearDisabled(year)
202
+
203
+ return (
204
+ <div
205
+ key={year}
206
+ className="relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent rounded-md"
207
+ role="presentation"
208
+ >
209
+ <button
210
+ ref={(element) => {
211
+ // eslint-disable-next-line security/detect-object-injection
212
+ yearButtonRefs.current[index] = element
213
+ }}
214
+ name="year"
215
+ className={cn(
216
+ buttonVariants({ variant: "ghost" }),
217
+ "inline-flex h-9 w-full items-center justify-center p-0 text-sm font-normal aria-selected:opacity-100",
218
+ isSelected &&
219
+ "bg-blue-500 text-white hover:bg-blue-600 hover:text-white focus:bg-blue-600 focus:text-white",
220
+ !isSelected && isCurrentYear && "bg-accent text-accent-foreground",
221
+ )}
222
+ disabled={isDisabled}
223
+ role="gridcell"
224
+ tabIndex={index === focusedIndex ? 0 : -1}
225
+ type="button"
226
+ aria-selected={isSelected}
227
+ aria-label={
228
+ isSelected
229
+ ? `${year}, selected`
230
+ : isCurrentYear
231
+ ? `${year}, current year`
232
+ : String(year)
233
+ }
234
+ onClick={() => onYearChange(yearDate)}
235
+ onFocus={() => setFocusedIndex(index)}
236
+ onKeyDown={(event) => handleYearKeyDown(event, index, year)}
237
+ >
238
+ <time dateTime={format(yearDate, "yyyy-MM-dd")}>{year}</time>
239
+ </button>
240
+ </div>
241
+ )
242
+ })}
243
+ </div>
244
+ </div>
245
+ </div>
246
+ </div>
247
+ )
248
+ }