tembro 6.1.0 → 6.1.1

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 (146) hide show
  1. package/dist/components/actions/action-menu.d.ts +11 -1
  2. package/dist/components/display/index.d.ts +0 -2
  3. package/dist/components/filters/index.d.ts +0 -1
  4. package/dist/components/inputs/time-picker.d.ts +11 -3
  5. package/dist/components/modern/file-manager.d.ts +1 -1
  6. package/dist/components/modern/index.d.ts +3 -1
  7. package/dist/components/modern/map.cjs +1 -0
  8. package/dist/components/modern/map.d.ts +29 -0
  9. package/dist/components/modern/map.js +1 -0
  10. package/dist/components/modern/media-player.cjs +1 -0
  11. package/dist/components/modern/media-player.d.ts +21 -0
  12. package/dist/components/modern/media-player.js +1 -0
  13. package/dist/components/modern/spreadsheet.cjs +1 -0
  14. package/dist/components/modern/spreadsheet.d.ts +35 -0
  15. package/dist/components/modern/spreadsheet.js +1 -0
  16. package/dist/components/overlay/drawer.d.ts +3 -1
  17. package/dist/index.d.ts +0 -2
  18. package/dist/src/components/actions/action-menu.cjs +1 -1
  19. package/dist/src/components/actions/action-menu.js +47 -39
  20. package/dist/src/components/actions/copy-button.cjs +1 -1
  21. package/dist/src/components/actions/copy-button.js +4 -1
  22. package/dist/src/components/calendar/date-picker.cjs +1 -1
  23. package/dist/src/components/calendar/date-picker.js +5 -8
  24. package/dist/src/components/calendar/date-range-picker.cjs +1 -1
  25. package/dist/src/components/calendar/date-range-picker.js +12 -27
  26. package/dist/src/components/data-table/data-table.cjs +1 -1
  27. package/dist/src/components/data-table/data-table.js +172 -171
  28. package/dist/src/components/display/index.cjs +1 -1
  29. package/dist/src/components/display/index.js +9 -11
  30. package/dist/src/components/filters/index.cjs +1 -1
  31. package/dist/src/components/filters/index.js +2 -3
  32. package/dist/src/components/inputs/color-picker.cjs +1 -1
  33. package/dist/src/components/inputs/color-picker.js +63 -46
  34. package/dist/src/components/inputs/slider.cjs +1 -1
  35. package/dist/src/components/inputs/slider.js +3 -3
  36. package/dist/src/components/inputs/tag-input.cjs +1 -1
  37. package/dist/src/components/inputs/tag-input.js +7 -4
  38. package/dist/src/components/inputs/time-picker.cjs +1 -1
  39. package/dist/src/components/inputs/time-picker.js +90 -34
  40. package/dist/src/components/modern/index.cjs +1 -1
  41. package/dist/src/components/modern/index.js +6 -4
  42. package/dist/src/components/modern/map.cjs +1 -0
  43. package/dist/src/components/modern/map.js +158 -0
  44. package/dist/src/components/modern/media-player.cjs +1 -0
  45. package/dist/src/components/modern/media-player.js +176 -0
  46. package/dist/src/components/modern/resizable-panel.cjs +1 -1
  47. package/dist/src/components/modern/resizable-panel.js +39 -32
  48. package/dist/src/components/modern/spreadsheet.cjs +1 -0
  49. package/dist/src/components/modern/spreadsheet.js +97 -0
  50. package/dist/src/components/navigation/pagination.cjs +1 -1
  51. package/dist/src/components/navigation/pagination.js +5 -5
  52. package/dist/src/components/notifications/toast.cjs +1 -1
  53. package/dist/src/components/notifications/toast.js +68 -63
  54. package/dist/src/components/overlay/alert-dialog.cjs +1 -1
  55. package/dist/src/components/overlay/alert-dialog.js +8 -6
  56. package/dist/src/components/overlay/drawer.cjs +1 -1
  57. package/dist/src/components/overlay/drawer.js +38 -20
  58. package/dist/src/components/ui/checkbox/index.cjs +1 -1
  59. package/dist/src/components/ui/checkbox/index.js +2 -1
  60. package/dist/src/components/ui/dialog/index.cjs +1 -1
  61. package/dist/src/components/ui/dialog/index.js +8 -8
  62. package/dist/src/components/ui/dropdown-menu/index.cjs +1 -1
  63. package/dist/src/components/ui/dropdown-menu/index.js +21 -19
  64. package/dist/src/components/ui/popover/index.cjs +1 -1
  65. package/dist/src/components/ui/popover/index.js +10 -10
  66. package/dist/src/components/ui/tabs/index.cjs +1 -1
  67. package/dist/src/components/ui/tabs/index.js +6 -6
  68. package/dist/src/components/ui/toggle-group/index.cjs +1 -1
  69. package/dist/src/components/ui/toggle-group/index.js +2 -2
  70. package/dist/src/index.cjs +1 -1
  71. package/dist/src/index.js +80 -83
  72. package/dist/src/public-component-surface.cjs +1 -1
  73. package/dist/src/public-component-surface.js +15 -20
  74. package/package.json +1 -2
  75. package/packages/cli/dist/index.cjs +58 -178
  76. package/packages/cli/vendor/src/components/actions/action-menu.tsx +47 -19
  77. package/packages/cli/vendor/src/components/actions/copy-button.tsx +2 -2
  78. package/packages/cli/vendor/src/components/calendar/date-picker.tsx +2 -5
  79. package/packages/cli/vendor/src/components/calendar/date-range-picker.tsx +5 -12
  80. package/packages/cli/vendor/src/components/data-table/data-table.tsx +45 -34
  81. package/packages/cli/vendor/src/components/display/index.ts +3 -5
  82. package/packages/cli/vendor/src/components/filters/index.ts +0 -1
  83. package/packages/cli/vendor/src/components/inputs/color-picker.tsx +15 -1
  84. package/packages/cli/vendor/src/components/inputs/slider.tsx +3 -3
  85. package/packages/cli/vendor/src/components/inputs/tag-input.tsx +4 -4
  86. package/packages/cli/vendor/src/components/inputs/time-picker.tsx +91 -38
  87. package/packages/cli/vendor/src/components/modern/file-manager.tsx +1 -1
  88. package/packages/cli/vendor/src/components/modern/index.ts +3 -1
  89. package/packages/cli/vendor/src/components/modern/map.tsx +146 -0
  90. package/packages/cli/vendor/src/components/modern/media-player.tsx +182 -0
  91. package/packages/cli/vendor/src/components/modern/resizable-panel.tsx +15 -5
  92. package/packages/cli/vendor/src/components/modern/spreadsheet.tsx +155 -0
  93. package/packages/cli/vendor/src/components/navigation/pagination.tsx +9 -5
  94. package/packages/cli/vendor/src/components/notifications/toast.tsx +20 -10
  95. package/packages/cli/vendor/src/components/overlay/alert-dialog.tsx +3 -5
  96. package/packages/cli/vendor/src/components/overlay/drawer.tsx +22 -11
  97. package/packages/cli/vendor/src/components/ui/checkbox/index.tsx +3 -1
  98. package/packages/cli/vendor/src/components/ui/dialog/index.tsx +4 -4
  99. package/packages/cli/vendor/src/components/ui/dropdown-menu/index.tsx +18 -22
  100. package/packages/cli/vendor/src/components/ui/popover/index.tsx +4 -4
  101. package/packages/cli/vendor/src/components/ui/tabs/index.tsx +8 -3
  102. package/packages/cli/vendor/src/components/ui/toggle-group/index.tsx +6 -6
  103. package/packages/cli/vendor/src/index.ts +0 -2
  104. package/packages/cli/vendor/src/public-component-surface.ts +3 -4
  105. package/packages/cli/vendor/templates/showcase/src/showcase/data/registry.ts +6 -6
  106. package/packages/cli/vendor/templates/showcase/src/showcase/layout/HeroSection.tsx +23 -8
  107. package/packages/cli/vendor/templates/showcase/src/showcase/layout/WorkbenchSidebar.tsx +1 -1
  108. package/packages/cli/vendor/templates/showcase/src/showcase/sections/PatternsSection.tsx +1 -1
  109. package/packages/cli/vendor/templates/styles/globals.css +0 -120
  110. package/registry.json +74 -141
  111. package/dist/components/display/info-card.cjs +0 -1
  112. package/dist/components/display/info-card.d.ts +0 -51
  113. package/dist/components/display/info-card.js +0 -1
  114. package/dist/components/display/statistic.cjs +0 -1
  115. package/dist/components/display/statistic.d.ts +0 -25
  116. package/dist/components/display/statistic.js +0 -1
  117. package/dist/components/filters/filter-bar.cjs +0 -1
  118. package/dist/components/filters/filter-bar.d.ts +0 -28
  119. package/dist/components/filters/filter-bar.js +0 -1
  120. package/dist/components/modern/image-cropper.cjs +0 -1
  121. package/dist/components/modern/image-cropper.d.ts +0 -39
  122. package/dist/components/modern/image-cropper.js +0 -1
  123. package/dist/components/ui/number-field/index.cjs +0 -1
  124. package/dist/components/ui/number-field/index.d.ts +0 -18
  125. package/dist/components/ui/number-field/index.js +0 -1
  126. package/dist/components/ui/toolbar/index.cjs +0 -1
  127. package/dist/components/ui/toolbar/index.d.ts +0 -14
  128. package/dist/components/ui/toolbar/index.js +0 -1
  129. package/dist/src/components/display/info-card.cjs +0 -1
  130. package/dist/src/components/display/info-card.js +0 -124
  131. package/dist/src/components/display/statistic.cjs +0 -1
  132. package/dist/src/components/display/statistic.js +0 -104
  133. package/dist/src/components/filters/filter-bar.cjs +0 -1
  134. package/dist/src/components/filters/filter-bar.js +0 -103
  135. package/dist/src/components/modern/image-cropper.cjs +0 -1
  136. package/dist/src/components/modern/image-cropper.js +0 -143
  137. package/dist/src/components/ui/number-field/index.cjs +0 -1
  138. package/dist/src/components/ui/number-field/index.js +0 -73
  139. package/dist/src/components/ui/toolbar/index.cjs +0 -1
  140. package/dist/src/components/ui/toolbar/index.js +0 -50
  141. package/packages/cli/vendor/src/components/display/info-card.tsx +0 -195
  142. package/packages/cli/vendor/src/components/display/statistic.tsx +0 -114
  143. package/packages/cli/vendor/src/components/filters/filter-bar.tsx +0 -163
  144. package/packages/cli/vendor/src/components/modern/image-cropper.tsx +0 -226
  145. package/packages/cli/vendor/src/components/ui/number-field/index.tsx +0 -89
  146. package/packages/cli/vendor/src/components/ui/toolbar/index.tsx +0 -67
@@ -33,7 +33,11 @@ export type ActionMenuItem = {
33
33
 
34
34
  export type ActionMenuProps = {
35
35
  actions: ActionMenuItem[]
36
+ open?: boolean
37
+ defaultOpen?: boolean
38
+ onOpenChange?: (open: boolean) => void
36
39
  label?: React.ReactNode
40
+ triggerLabel?: string
37
41
  trigger?: React.ReactElement
38
42
  align?: "start" | "center" | "end"
39
43
  side?: "top" | "right" | "bottom" | "left"
@@ -50,12 +54,19 @@ export type ActionMenuProps = {
50
54
  menuWidth?: number | string
51
55
  loadingLabel?: React.ReactNode
52
56
  persistIconSpace?: boolean
57
+ itemRoleDescription?: string
58
+ renderItem?: (action: ActionMenuItem, state: { loading: boolean; close: () => void }) => React.ReactNode
59
+ onActionError?: (error: unknown, action: ActionMenuItem) => void
53
60
  }
54
-
61
+
55
62
  function ActionMenu({
56
- actions,
57
- label,
58
- trigger,
63
+ actions,
64
+ open: openProp,
65
+ defaultOpen = false,
66
+ onOpenChange,
67
+ label,
68
+ triggerLabel = "Open actions",
69
+ trigger,
59
70
  align = "end",
60
71
  side = "bottom",
61
72
  sideOffset = 4,
@@ -71,12 +82,21 @@ function ActionMenu({
71
82
  menuWidth,
72
83
  loadingLabel = "Working...",
73
84
  persistIconSpace = true,
85
+ itemRoleDescription,
86
+ renderItem,
87
+ onActionError,
74
88
  }: ActionMenuProps) {
75
89
  const visibleActions = actions.filter((action) => !action.hidden)
76
90
  const [loadingKey, setLoadingKey] = React.useState<string | null>(null)
77
- const [open, setOpen] = React.useState(false)
91
+ const [internalOpen, setInternalOpen] = React.useState(defaultOpen)
92
+ const open = openProp ?? internalOpen
78
93
  const lastInvokedActionKeyRef = React.useRef<string | null>(null)
79
94
 
95
+ const setOpen = React.useCallback((nextOpen: boolean) => {
96
+ if (openProp === undefined) setInternalOpen(nextOpen)
97
+ onOpenChange?.(nextOpen)
98
+ }, [onOpenChange, openProp])
99
+
80
100
  const handleSelect = async (action: ActionMenuItem) => {
81
101
  if (action.disabled || action.loading || loadingKey) return
82
102
 
@@ -84,6 +104,9 @@ function ActionMenu({
84
104
  setLoadingKey(action.key)
85
105
  await action.onSelect?.()
86
106
  if (closeOnSelect && !action.keepOpen) setOpen(false)
107
+ } catch (error) {
108
+ onActionError?.(error, action)
109
+ if (!onActionError) throw error
87
110
  } finally {
88
111
  setLoadingKey(null)
89
112
  }
@@ -122,7 +145,7 @@ function ActionMenu({
122
145
  >
123
146
  {!trigger && <MoreHorizontalIcon />}
124
147
  {!trigger && showChevron && <span className="text-[10px] font-medium uppercase tracking-[0.18em]">Menu</span>}
125
- <span className="sr-only">Open actions</span>
148
+ <span className="sr-only">{triggerLabel}</span>
126
149
  </DropdownMenuTrigger>
127
150
  <DropdownMenuContent
128
151
  align={align}
@@ -155,6 +178,7 @@ function ActionMenu({
155
178
  disabled={action.disabled || isLoading}
156
179
  variant={action.destructive ? "destructive" : "default"}
157
180
  closeOnSelect={closeOnSelect && !action.keepOpen}
181
+ aria-roledescription={itemRoleDescription}
158
182
  className={cn(
159
183
  "min-h-11 items-start gap-3 rounded-[calc(var(--radius-md)+1px)] border border-transparent py-2.5 transition-[background-color,color,border-color,box-shadow] data-[highlighted]:shadow-[inset_0_0_0_1px_color-mix(in_oklch,var(--primary),transparent_76%)] data-[disabled]:opacity-45",
160
184
  itemClassName
@@ -173,19 +197,23 @@ function ActionMenu({
173
197
  onMouseDown={stopInteractivePropagation}
174
198
  onDoubleClick={stopInteractivePropagation}
175
199
  >
176
- <span className={cn("mt-0.5 flex size-5 shrink-0 items-center justify-center text-muted-foreground", !persistIconSpace && !isLoading && !action.icon && "hidden")}>
177
- {isLoading ? <Loader2Icon className="size-4 animate-spin" /> : action.icon}
178
- </span>
179
- <span className="min-w-0 flex-1">
180
- <span className="block truncate">{action.label}</span>
181
- {action.description ? (
182
- <DropdownMenuItemDescription className="mt-0.5 truncate">
183
- {isLoading ? loadingLabel : action.description}
184
- </DropdownMenuItemDescription>
185
- ) : null}
186
- </span>
187
- {action.shortcut && (
188
- <DropdownMenuShortcut>{action.shortcut}</DropdownMenuShortcut>
200
+ {renderItem?.(action, { loading: isLoading, close: () => setOpen(false) }) ?? (
201
+ <>
202
+ <span className={cn("mt-0.5 flex size-5 shrink-0 items-center justify-center text-muted-foreground", !persistIconSpace && !isLoading && !action.icon && "hidden")}>
203
+ {isLoading ? <Loader2Icon className="size-4 animate-spin" /> : action.icon}
204
+ </span>
205
+ <span className="min-w-0 flex-1">
206
+ <span className="block truncate">{action.label}</span>
207
+ {action.description ? (
208
+ <DropdownMenuItemDescription className="mt-0.5 truncate">
209
+ {isLoading ? loadingLabel : action.description}
210
+ </DropdownMenuItemDescription>
211
+ ) : null}
212
+ </span>
213
+ {action.shortcut && (
214
+ <DropdownMenuShortcut>{action.shortcut}</DropdownMenuShortcut>
215
+ )}
216
+ </>
189
217
  )}
190
218
  </DropdownMenuItem>
191
219
  </React.Fragment>
@@ -97,8 +97,8 @@ function CopyButton({
97
97
  aria-live="polite"
98
98
  {...props}
99
99
  >
100
- {showIcon && (copied ? <CheckIcon data-icon="inline-start" /> : <CopyIcon data-icon="inline-start" />)}
101
- {children ?? (copied ? copiedLabel : copyLabel)}
100
+ {showIcon && (copied ? <CheckIcon data-icon="inline-start" className="size-3.5" /> : <CopyIcon data-icon="inline-start" className="size-3.5" />)}
101
+ {children ?? (copied ? copiedLabel : copyLabel)}
102
102
  </Button>
103
103
  )
104
104
  }
@@ -86,11 +86,8 @@ function DatePicker({
86
86
  />
87
87
  }
88
88
  >
89
- <CalendarIcon data-icon="inline-start" className={cn(hasValue && "text-primary")} />
90
- <span className={cn("grid min-w-0 flex-1", triggerVariant === "default" && "gap-0.5")}>
91
- {triggerVariant === "default" ? <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
92
- {labels?.selected ?? "Date"}
93
- </span> : null}
89
+ <CalendarIcon data-icon="inline-start" className={cn("size-4", hasValue && "text-primary")} />
90
+ <span className="grid min-w-0 flex-1">
94
91
  <span className={cn("truncate text-sm", hasValue && "font-semibold text-foreground")}>
95
92
  {hasValue ? formatValue(String(value)) : placeholder ?? labels?.placeholder ?? "Select date"}
96
93
  </span>
@@ -176,22 +176,15 @@ function DateRangePicker({
176
176
  />
177
177
  }
178
178
  >
179
- <CalendarIcon data-icon="inline-start" className={cn(hasValue && "text-primary")} />
179
+ <CalendarIcon data-icon="inline-start" className={cn("size-4", hasValue && "text-primary")} />
180
180
  {hasValue ? (
181
- <span className="grid min-w-0 flex-1 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-2">
182
- <span className="grid min-w-0 gap-0.5">
183
- <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">{labels?.start ?? "Start"}</span>
184
- <span className="truncate text-sm font-semibold text-foreground">{from ? formatValue(from) : "..."}</span>
185
- </span>
181
+ <span className="flex min-w-0 flex-1 items-center gap-2">
182
+ <span className="min-w-0 truncate text-sm font-semibold text-foreground">{from ? formatValue(from) : "..."}</span>
186
183
  <span className="text-muted-foreground">-</span>
187
- <span className="grid min-w-0 gap-0.5">
188
- <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">{labels?.end ?? "End"}</span>
189
- <span className="truncate text-sm font-semibold text-foreground">{to ? formatValue(to) : "..."}</span>
190
- </span>
184
+ <span className="min-w-0 truncate text-sm font-semibold text-foreground">{to ? formatValue(to) : "..."}</span>
191
185
  </span>
192
186
  ) : (
193
- <span className="grid min-w-0 flex-1 gap-0.5">
194
- <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">Range</span>
187
+ <span className="grid min-w-0 flex-1">
195
188
  <span className="truncate text-sm">{label}</span>
196
189
  </span>
197
190
  )}
@@ -4,8 +4,9 @@ import * as React from "react"
4
4
  import {
5
5
  flexRender,
6
6
  getCoreRowModel,
7
- getPaginationRowModel,
8
- useReactTable,
7
+ getPaginationRowModel,
8
+ getSortedRowModel,
9
+ useReactTable,
9
10
  type Cell,
10
11
  type ColumnDef,
11
12
  type Header,
@@ -288,40 +289,50 @@ function DataTable<TData, TValue = unknown>({
288
289
  ]
289
290
  }, [columns, features?.rowActions, rowActions])
290
291
 
291
- const paginationConfig = pagination === false ? undefined : pagination
292
- const controlledPagination = paginationConfig
293
- ? {
294
- pageIndex: paginationConfig.pageIndex,
295
- pageSize: paginationConfig.pageSize,
296
- }
297
- : undefined
298
- const manualPagination = Boolean(paginationConfig && paginationConfig.manual !== false)
299
- const resolvedRowSelection = rowSelection ?? {}
300
-
301
- // TanStack Table returns imperative helpers that React Compiler flags by design.
302
- // eslint-disable-next-line react-hooks/incompatible-library
303
- const table = useReactTable({
292
+ const paginationConfig = pagination === false ? undefined : pagination
293
+ const [internalSorting, setInternalSorting] = React.useState<SortingState>([])
294
+ const [internalColumnVisibility, setInternalColumnVisibility] = React.useState<VisibilityState>({})
295
+ const [internalRowSelection, setInternalRowSelection] = React.useState<RowSelectionState>({})
296
+ const [internalExpanded, setInternalExpanded] = React.useState<ExpandedState>({})
297
+ const [internalColumnPinning, setInternalColumnPinning] = React.useState<ColumnPinningState>({})
298
+ const resolvedSorting = sorting ?? internalSorting
299
+ const resolvedColumnVisibility = columnVisibility ?? internalColumnVisibility
300
+ const resolvedRowSelection = rowSelection ?? internalRowSelection
301
+ const resolvedExpanded = expanded ?? internalExpanded
302
+ const resolvedColumnPinning = columnPinning ?? internalColumnPinning
303
+ const controlledPagination = paginationConfig
304
+ ? {
305
+ pageIndex: paginationConfig.pageIndex,
306
+ pageSize: paginationConfig.pageSize,
307
+ }
308
+ : undefined
309
+ const manualPagination = Boolean(paginationConfig && paginationConfig.manual !== false)
310
+
311
+ // TanStack Table returns imperative helpers that React Compiler flags by design.
312
+ // eslint-disable-next-line react-hooks/incompatible-library
313
+ const table = useReactTable({
304
314
  data,
305
315
  columns: resolvedColumns,
306
- getRowId,
307
- getCoreRowModel: getCoreRowModel(),
308
- getPaginationRowModel: paginationConfig && !manualPagination ? getPaginationRowModel() : undefined,
309
- manualPagination,
310
- pageCount: paginationConfig?.pageCount,
311
- state: {
312
- sorting,
313
- columnVisibility,
314
- rowSelection: resolvedRowSelection,
315
- pagination: controlledPagination,
316
- expanded,
317
- ...(columnPinning ? { columnPinning } : {}),
318
- },
319
- onSortingChange,
320
- onColumnVisibilityChange,
321
- onRowSelectionChange,
322
- onExpandedChange,
323
- onColumnPinningChange,
324
- enableRowSelection,
316
+ getRowId,
317
+ getCoreRowModel: getCoreRowModel(),
318
+ getSortedRowModel: getSortedRowModel(),
319
+ getPaginationRowModel: paginationConfig && !manualPagination ? getPaginationRowModel() : undefined,
320
+ manualPagination,
321
+ pageCount: paginationConfig?.pageCount,
322
+ state: {
323
+ sorting: resolvedSorting,
324
+ columnVisibility: resolvedColumnVisibility,
325
+ rowSelection: resolvedRowSelection,
326
+ pagination: controlledPagination,
327
+ expanded: resolvedExpanded,
328
+ columnPinning: resolvedColumnPinning,
329
+ },
330
+ onSortingChange: onSortingChange ?? setInternalSorting,
331
+ onColumnVisibilityChange: onColumnVisibilityChange ?? setInternalColumnVisibility,
332
+ onRowSelectionChange: onRowSelectionChange ?? setInternalRowSelection,
333
+ onExpandedChange: onExpandedChange ?? setInternalExpanded,
334
+ onColumnPinningChange: onColumnPinningChange ?? setInternalColumnPinning,
335
+ enableRowSelection,
325
336
  getRowCanExpand,
326
337
  getExpandedRowModel: getExpandedRowModel(),
327
338
  })
@@ -3,15 +3,13 @@ export * from "./activity-feed"
3
3
  export * from "./avatar"
4
4
  export * from "./kanban"
5
5
  export * from "./code-block"
6
- export * from "./data-state"
7
- export * from "./list"
8
- export * from "./progress"
9
- export * from "./statistic"
6
+ export * from "./data-state"
7
+ export * from "./list"
8
+ export * from "./progress"
10
9
  export * from "./status-dot"
11
10
  export * from "./status-legend"
12
11
  export * from "./timeline"
13
12
  export * from "./tree-view"
14
- export * from "./info-card"
15
13
  export * from "./virtual-list"
16
14
  export * from "./qr-code"
17
15
  export * from "./carousel"
@@ -1,2 +1 @@
1
- export * from "./filter-bar"
2
1
  export * from "./saved-filter-select"
@@ -31,6 +31,14 @@ function withAlpha(value: string, alpha: number, includeAlpha: boolean) {
31
31
  return `${opaque}${channel}`
32
32
  }
33
33
 
34
+ function getAlphaTrackStyle(value: string): React.CSSProperties {
35
+ const opaque = getOpaqueColor(value)
36
+
37
+ return {
38
+ background: `linear-gradient(90deg, ${opaque}00 0%, ${opaque}ff 100%)`,
39
+ }
40
+ }
41
+
34
42
  export type ColorPickerProps = Omit<React.ComponentProps<"div">, "defaultValue" | "onChange"> & {
35
43
  value?: string
36
44
  defaultValue?: string
@@ -83,6 +91,11 @@ function ColorPicker({
83
91
  {label ? <div className="text-sm font-medium text-foreground">{label}</div> : null}
84
92
  <div className="flex min-w-0 items-center gap-2">
85
93
  <label className="relative size-10 shrink-0 overflow-hidden rounded-md border bg-background shadow-sm focus-within:ring-2 focus-within:ring-ring">
94
+ <span
95
+ aria-hidden="true"
96
+ className="absolute inset-0 bg-[linear-gradient(45deg,#e2e8f0_25%,transparent_25%),linear-gradient(-45deg,#e2e8f0_25%,transparent_25%),linear-gradient(45deg,transparent_75%,#e2e8f0_75%),linear-gradient(-45deg,transparent_75%,#e2e8f0_75%)] bg-[length:10px_10px] bg-[position:0_0,0_5px,5px_-5px,-5px_0]"
97
+ />
98
+ <span aria-hidden="true" className="absolute inset-0" style={{ backgroundColor: currentValue }} />
86
99
  <span className="sr-only">{labels?.color ?? "Choose color"}</span>
87
100
  <input
88
101
  type="color"
@@ -120,7 +133,8 @@ function ColorPicker({
120
133
  value={getAlphaPercent(currentValue)}
121
134
  disabled={disabled}
122
135
  aria-label={labels?.alpha ?? "Opacity"}
123
- className="w-full accent-foreground disabled:cursor-not-allowed"
136
+ style={getAlphaTrackStyle(currentValue)}
137
+ className="h-2 w-full cursor-pointer appearance-none rounded-full border border-border/70 outline-none focus-visible:ring-4 focus-visible:ring-ring/20 disabled:cursor-not-allowed [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-background [&::-webkit-slider-thumb]:bg-foreground [&::-webkit-slider-thumb]:shadow-sm"
124
138
  onChange={(event) => updateValue(withAlpha(currentValue, Number(event.target.value), true))}
125
139
  />
126
140
  <span className="w-10 text-right tabular-nums">{getAlphaPercent(currentValue)}%</span>
@@ -83,7 +83,7 @@ function Slider({
83
83
  step={step}
84
84
  disabled={disabled}
85
85
  style={getSliderTrackStyle(currentValue, min, max)}
86
- className="h-3 w-full cursor-pointer appearance-none rounded-full border border-[color:var(--aui-surface-border)] bg-[color:var(--aui-control-bg)] shadow-[inset_0_1px_2px_rgba(15,23,42,0.08)] outline-none transition hover:brightness-[1.02] disabled:cursor-not-allowed disabled:opacity-50 [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-[3px] [&::-moz-range-thumb]:border-[color:var(--aui-page-bg)] [&::-moz-range-thumb]:bg-[color:var(--aui-brand-strong)] [&::-moz-range-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-moz-range-thumb]:transition [&::-moz-range-track]:h-3 [&::-moz-range-track]:rounded-full [&::-webkit-slider-runnable-track]:h-3 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-thumb]:mt-[-5px] [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-[3px] [&::-webkit-slider-thumb]:border-[color:var(--aui-page-bg)] [&::-webkit-slider-thumb]:bg-[color:var(--aui-brand-strong)] [&::-webkit-slider-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)]"
86
+ className="h-3 w-full cursor-pointer appearance-none rounded-full border border-[color:var(--aui-surface-border)] bg-[color:var(--aui-control-bg)] shadow-[inset_0_1px_2px_rgba(15,23,42,0.08)] outline-none transition hover:brightness-[1.02] focus-visible:ring-4 focus-visible:ring-[color:color-mix(in_oklch,var(--aui-brand-strong),transparent_78%)] disabled:cursor-not-allowed disabled:opacity-50 [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-[3px] [&::-moz-range-thumb]:border-[color:var(--aui-page-bg)] [&::-moz-range-thumb]:bg-[color:var(--aui-brand-strong)] [&::-moz-range-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-moz-range-thumb]:transition [&::-moz-range-track]:h-3 [&::-moz-range-track]:rounded-full [&::-webkit-slider-runnable-track]:h-3 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-thumb]:mt-[-5px] [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-[3px] [&::-webkit-slider-thumb]:border-[color:var(--aui-page-bg)] [&::-webkit-slider-thumb]:bg-[color:var(--aui-brand-strong)] [&::-webkit-slider-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-webkit-slider-thumb]:transition-transform hover:[&::-webkit-slider-thumb]:scale-105"
87
87
  onChange={(event) => updateValue(event.currentTarget.valueAsNumber)}
88
88
  {...props}
89
89
  />
@@ -170,7 +170,7 @@ function RangeSlider({
170
170
  max={max}
171
171
  step={step}
172
172
  disabled={disabled}
173
- className="pointer-events-none absolute inset-0 h-6 w-full cursor-pointer appearance-none bg-transparent outline-none disabled:cursor-not-allowed disabled:opacity-50 [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-[3px] [&::-moz-range-thumb]:border-[color:var(--aui-page-bg)] [&::-moz-range-thumb]:bg-[color:var(--aui-brand-strong)] [&::-moz-range-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-moz-range-track]:h-3 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-transparent [&::-webkit-slider-runnable-track]:h-3 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:mt-[-5px] [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-[3px] [&::-webkit-slider-thumb]:border-[color:var(--aui-page-bg)] [&::-webkit-slider-thumb]:bg-[color:var(--aui-brand-strong)] [&::-webkit-slider-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)]"
173
+ className="pointer-events-none absolute inset-0 h-6 w-full cursor-pointer appearance-none bg-transparent outline-none focus-visible:ring-4 focus-visible:ring-[color:color-mix(in_oklch,var(--aui-brand-strong),transparent_78%)] disabled:cursor-not-allowed disabled:opacity-50 [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-[3px] [&::-moz-range-thumb]:border-[color:var(--aui-page-bg)] [&::-moz-range-thumb]:bg-[color:var(--aui-brand-strong)] [&::-moz-range-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-moz-range-track]:h-3 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-transparent [&::-webkit-slider-runnable-track]:h-3 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:mt-[-5px] [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-[3px] [&::-webkit-slider-thumb]:border-[color:var(--aui-page-bg)] [&::-webkit-slider-thumb]:bg-[color:var(--aui-brand-strong)] [&::-webkit-slider-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-webkit-slider-thumb]:transition-transform hover:[&::-webkit-slider-thumb]:scale-105"
174
174
  onChange={(event) => updateValue(0, event.currentTarget.valueAsNumber)}
175
175
  {...props}
176
176
  />
@@ -181,7 +181,7 @@ function RangeSlider({
181
181
  max={max}
182
182
  step={step}
183
183
  disabled={disabled}
184
- className="pointer-events-none absolute inset-0 h-6 w-full cursor-pointer appearance-none bg-transparent outline-none disabled:cursor-not-allowed disabled:opacity-50 [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-[3px] [&::-moz-range-thumb]:border-[color:var(--aui-page-bg)] [&::-moz-range-thumb]:bg-[color:var(--aui-brand-strong)] [&::-moz-range-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-moz-range-track]:h-3 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-transparent [&::-webkit-slider-runnable-track]:h-3 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:mt-[-5px] [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-[3px] [&::-webkit-slider-thumb]:border-[color:var(--aui-page-bg)] [&::-webkit-slider-thumb]:bg-[color:var(--aui-brand-strong)] [&::-webkit-slider-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)]"
184
+ className="pointer-events-none absolute inset-0 h-6 w-full cursor-pointer appearance-none bg-transparent outline-none focus-visible:ring-4 focus-visible:ring-[color:color-mix(in_oklch,var(--aui-brand-strong),transparent_78%)] disabled:cursor-not-allowed disabled:opacity-50 [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-[3px] [&::-moz-range-thumb]:border-[color:var(--aui-page-bg)] [&::-moz-range-thumb]:bg-[color:var(--aui-brand-strong)] [&::-moz-range-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-moz-range-track]:h-3 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-transparent [&::-webkit-slider-runnable-track]:h-3 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:mt-[-5px] [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-[3px] [&::-webkit-slider-thumb]:border-[color:var(--aui-page-bg)] [&::-webkit-slider-thumb]:bg-[color:var(--aui-brand-strong)] [&::-webkit-slider-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-webkit-slider-thumb]:transition-transform hover:[&::-webkit-slider-thumb]:scale-105"
185
185
  onChange={(event) => updateValue(1, event.currentTarget.valueAsNumber)}
186
186
  {...props}
187
187
  />
@@ -80,13 +80,13 @@ function TagInput({
80
80
  <Badge
81
81
  key={tag}
82
82
  variant="secondary"
83
- className="h-8 gap-1 rounded-full border border-[color:var(--aui-surface-border)] bg-[color:var(--aui-page-bg)] px-3 pr-1 text-[13px] font-medium shadow-sm"
83
+ className="h-8 gap-1.5 rounded-full border border-[color:var(--aui-surface-border)] bg-[color:var(--aui-page-bg)] px-3 pr-1.5 text-[13px] font-medium leading-none shadow-sm"
84
84
  >
85
- {tag}
85
+ <span className="leading-none">{tag}</span>
86
86
  {!readOnly && !disabled && (
87
87
  <button
88
88
  type="button"
89
- className="grid size-5 shrink-0 place-items-center rounded-full p-0 leading-none text-muted-foreground transition hover:bg-[color:var(--aui-control-bg)] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
89
+ className="inline-flex size-5 shrink-0 items-center justify-center rounded-full p-0 leading-none text-muted-foreground transition hover:bg-[color:var(--aui-control-bg)] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
90
90
  aria-label={`Remove ${tag}`}
91
91
  onClick={(event) => {
92
92
  stopInteractivePropagation(event)
@@ -95,7 +95,7 @@ function TagInput({
95
95
  onMouseDown={stopInteractivePropagation}
96
96
  onDoubleClick={stopInteractivePropagation}
97
97
  >
98
- <XIcon className="size-3" strokeWidth={2} />
98
+ <XIcon className="size-3" strokeWidth={2.25} />
99
99
  </button>
100
100
  )}
101
101
  </Badge>
@@ -1,22 +1,31 @@
1
- import * as React from "react"
2
-
3
- import { cn } from "@/lib/utils"
4
-
5
- export type TimePickerProps = Omit<React.ComponentProps<"input">, "type"> & {
6
- label?: React.ReactNode
7
- }
8
-
9
- export type DateTimePickerProps = Omit<React.ComponentProps<"input">, "type"> & {
10
- label?: React.ReactNode
11
- }
1
+ import * as React from "react"
2
+ import { CalendarClockIcon, ClockIcon, XIcon } from "lucide-react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ export type TimePickerProps = Omit<React.ComponentProps<"input">, "type"> & {
7
+ label?: React.ReactNode
8
+ clearable?: boolean
9
+ onClear?: () => void
10
+ }
11
+
12
+ export type DateTimePickerProps = Omit<React.ComponentProps<"input">, "type"> & {
13
+ label?: React.ReactNode
14
+ clearable?: boolean
15
+ onClear?: () => void
16
+ }
12
17
 
13
18
  export type TimeRangePickerProps = React.ComponentProps<"div"> & {
14
19
  from?: string
15
20
  to?: string
16
21
  onFromChange?: (value: string) => void
17
- onToChange?: (value: string) => void
18
- disabled?: boolean
19
- }
22
+ onToChange?: (value: string) => void
23
+ disabled?: boolean
24
+ labels?: {
25
+ from?: React.ReactNode
26
+ to?: React.ReactNode
27
+ }
28
+ }
20
29
 
21
30
  function FieldShell({ label, className, children }: { label?: React.ReactNode; className?: string; children: React.ReactNode }) {
22
31
  return (
@@ -27,29 +36,73 @@ function FieldShell({ label, className, children }: { label?: React.ReactNode; c
27
36
  )
28
37
  }
29
38
 
30
- function TimePicker({ label, className, ...props }: TimePickerProps) {
31
- return (
32
- <FieldShell label={label}>
33
- <input type="time" className={cn("h-10 rounded-md border bg-background px-3 text-sm outline-none ring-offset-background focus-visible:ring-2 focus-visible:ring-ring", className)} {...props} />
34
- </FieldShell>
35
- )
36
- }
37
-
38
- function DateTimePicker({ label, className, ...props }: DateTimePickerProps) {
39
- return (
40
- <FieldShell label={label}>
41
- <input type="datetime-local" className={cn("h-10 rounded-md border bg-background px-3 text-sm outline-none ring-offset-background focus-visible:ring-2 focus-visible:ring-ring", className)} {...props} />
42
- </FieldShell>
43
- )
44
- }
45
-
46
- function TimeRangePicker({ from, to, onFromChange, onToChange, disabled, className, ...props }: TimeRangePickerProps) {
47
- return (
48
- <div data-slot="time-range-picker" className={cn("grid gap-2 sm:grid-cols-2", className)} {...props}>
49
- <TimePicker value={from} disabled={disabled} onChange={(event) => onFromChange?.(event.target.value)} />
50
- <TimePicker value={to} disabled={disabled} onChange={(event) => onToChange?.(event.target.value)} />
51
- </div>
52
- )
53
- }
39
+ function clearInput(event: React.MouseEvent<HTMLButtonElement>, onChange: React.ChangeEventHandler<HTMLInputElement> | undefined, onClear: (() => void) | undefined) {
40
+ event.preventDefault()
41
+ event.stopPropagation()
42
+ onChange?.({ target: { value: "" }, currentTarget: { value: "" } } as React.ChangeEvent<HTMLInputElement>)
43
+ onClear?.()
44
+ }
45
+
46
+ function TimePicker({ label, className, clearable = false, onClear, value, defaultValue, disabled, onChange, ...props }: TimePickerProps) {
47
+ const hasValue = value !== undefined ? String(value).length > 0 : defaultValue !== undefined && String(defaultValue).length > 0
48
+
49
+ return (
50
+ <FieldShell label={label}>
51
+ <span className="relative grid">
52
+ <ClockIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
53
+ <input
54
+ type="time"
55
+ value={value}
56
+ defaultValue={defaultValue}
57
+ disabled={disabled}
58
+ onChange={onChange}
59
+ className={cn("h-11 rounded-[var(--aui-control-radius,var(--radius-md))] border border-input/82 bg-background px-10 text-sm tabular-nums outline-none transition-[border-color,box-shadow] hover:border-ring/30 focus-visible:border-ring focus-visible:ring-4 focus-visible:ring-ring/16 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground", className)}
60
+ {...props}
61
+ />
62
+ {clearable && hasValue && !disabled ? (
63
+ <button type="button" aria-label="Clear time" className="absolute right-2 top-1/2 grid size-7 -translate-y-1/2 place-items-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" onClick={(event) => clearInput(event, onChange, onClear)}>
64
+ <XIcon className="size-3.5" />
65
+ </button>
66
+ ) : null}
67
+ </span>
68
+ </FieldShell>
69
+ )
70
+ }
71
+
72
+ function DateTimePicker({ label, className, clearable = false, onClear, value, defaultValue, disabled, onChange, ...props }: DateTimePickerProps) {
73
+ const hasValue = value !== undefined ? String(value).length > 0 : defaultValue !== undefined && String(defaultValue).length > 0
74
+
75
+ return (
76
+ <FieldShell label={label}>
77
+ <span className="relative grid">
78
+ <CalendarClockIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
79
+ <input
80
+ type="datetime-local"
81
+ value={value}
82
+ defaultValue={defaultValue}
83
+ disabled={disabled}
84
+ onChange={onChange}
85
+ className={cn("h-11 rounded-[var(--aui-control-radius,var(--radius-md))] border border-input/82 bg-background px-10 text-sm tabular-nums outline-none transition-[border-color,box-shadow] hover:border-ring/30 focus-visible:border-ring focus-visible:ring-4 focus-visible:ring-ring/16 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground", className)}
86
+ {...props}
87
+ />
88
+ {clearable && hasValue && !disabled ? (
89
+ <button type="button" aria-label="Clear date and time" className="absolute right-2 top-1/2 grid size-7 -translate-y-1/2 place-items-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" onClick={(event) => clearInput(event, onChange, onClear)}>
90
+ <XIcon className="size-3.5" />
91
+ </button>
92
+ ) : null}
93
+ </span>
94
+ </FieldShell>
95
+ )
96
+ }
97
+
98
+ function TimeRangePicker({ from, to, onFromChange, onToChange, disabled, labels, className, ...props }: TimeRangePickerProps) {
99
+ return (
100
+ <div data-slot="time-range-picker" className={cn("grid gap-2 sm:grid-cols-[1fr_auto_1fr] sm:items-end", className)} {...props}>
101
+ <TimePicker label={labels?.from} value={from} clearable disabled={disabled} onChange={(event) => onFromChange?.(event.target.value)} onClear={() => onFromChange?.("")} />
102
+ <span className="hidden h-11 items-center text-sm text-muted-foreground sm:inline-flex">to</span>
103
+ <TimePicker label={labels?.to} value={to} clearable disabled={disabled} onChange={(event) => onToChange?.(event.target.value)} onClear={() => onToChange?.("")} />
104
+ </div>
105
+ )
106
+ }
54
107
 
55
108
  export { DateTimePicker, TimePicker, TimeRangePicker }
@@ -18,7 +18,7 @@ export type FileManagerItem = {
18
18
  status?: React.ReactNode
19
19
  }
20
20
 
21
- export type FileManagerProps = React.ComponentProps<"div"> & {
21
+ export type FileManagerProps = Omit<React.ComponentProps<"div">, "onSelect"> & {
22
22
  items: FileManagerItem[]
23
23
  selectedId?: string
24
24
  onSelect?: (item: FileManagerItem) => void
@@ -2,5 +2,7 @@ export * from "./calendar-scheduler"
2
2
  export * from "./dual-list-picker"
3
3
  export * from "./resizable-panel"
4
4
  export * from "./rich-text-editor"
5
- export * from "./image-cropper"
6
5
  export * from "./file-manager"
6
+ export * from "./map"
7
+ export * from "./media-player"
8
+ export * from "./spreadsheet"