@motor-hero/ui-kit 0.45.0 → 0.46.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.
package/dist/vite.cjs ADDED
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/vite.ts
21
+ var vite_exports = {};
22
+ __export(vite_exports, {
23
+ versionFile: () => versionFile
24
+ });
25
+ module.exports = __toCommonJS(vite_exports);
26
+ var import_node_fs = require("fs");
27
+ var import_node_path = require("path");
28
+ function versionFile(options = {}) {
29
+ const fileName = options.fileName ?? "version.json";
30
+ const buildTime = (/* @__PURE__ */ new Date()).toISOString();
31
+ let outDir = "dist";
32
+ return {
33
+ name: "motor-hero:version-file",
34
+ config: () => ({
35
+ define: { "import.meta.env.VITE_BUILD_TIME": JSON.stringify(buildTime) }
36
+ }),
37
+ configResolved(config) {
38
+ outDir = config.build.outDir;
39
+ },
40
+ closeBundle() {
41
+ (0, import_node_fs.mkdirSync)(outDir, { recursive: true });
42
+ (0, import_node_fs.writeFileSync)(
43
+ (0, import_node_path.join)(outDir, fileName),
44
+ JSON.stringify({ buildTime, timestamp: Date.parse(buildTime) }, null, 2)
45
+ );
46
+ }
47
+ };
48
+ }
49
+ // Annotate the CommonJS export names for ESM import in node:
50
+ 0 && (module.exports = {
51
+ versionFile
52
+ });
53
+ //# sourceMappingURL=vite.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/vite.ts"],"sourcesContent":["import { mkdirSync, writeFileSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport type { Plugin } from \"vite\"\n\n/**\n * One build id in two places: baked into the bundle as\n * `import.meta.env.VITE_BUILD_TIME` and written to `dist/version.json`.\n * `UpdateBanner` / `useVersionCheck` compare the two at runtime, so the\n * running app knows its own version instead of guessing it from a fetch.\n *\n * // vite.config.ts\n * import { versionFile } from \"@motor-hero/ui-kit/vite\"\n * export default defineConfig({ plugins: [react(), versionFile()] })\n */\nexport function versionFile(options: { fileName?: string } = {}): Plugin {\n const fileName = options.fileName ?? \"version.json\"\n const buildTime = new Date().toISOString()\n let outDir = \"dist\"\n return {\n name: \"motor-hero:version-file\",\n config: () => ({\n define: { \"import.meta.env.VITE_BUILD_TIME\": JSON.stringify(buildTime) },\n }),\n configResolved(config) {\n outDir = config.build.outDir\n },\n closeBundle() {\n mkdirSync(outDir, { recursive: true })\n writeFileSync(\n join(outDir, fileName),\n JSON.stringify({ buildTime, timestamp: Date.parse(buildTime) }, null, 2),\n )\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAyC;AACzC,uBAAqB;AAad,SAAS,YAAY,UAAiC,CAAC,GAAW;AACvE,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AACzC,MAAI,SAAS;AACb,SAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ,OAAO;AAAA,MACb,QAAQ,EAAE,mCAAmC,KAAK,UAAU,SAAS,EAAE;AAAA,IACzE;AAAA,IACA,eAAe,QAAQ;AACrB,eAAS,OAAO,MAAM;AAAA,IACxB;AAAA,IACA,cAAc;AACZ,oCAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;AACrC;AAAA,YACE,uBAAK,QAAQ,QAAQ;AAAA,QACrB,KAAK,UAAU,EAAE,WAAW,WAAW,KAAK,MAAM,SAAS,EAAE,GAAG,MAAM,CAAC;AAAA,MACzE;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
package/dist/vite.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { Plugin } from "vite";
2
+ /**
3
+ * One build id in two places: baked into the bundle as
4
+ * `import.meta.env.VITE_BUILD_TIME` and written to `dist/version.json`.
5
+ * `UpdateBanner` / `useVersionCheck` compare the two at runtime, so the
6
+ * running app knows its own version instead of guessing it from a fetch.
7
+ *
8
+ * // vite.config.ts
9
+ * import { versionFile } from "@motor-hero/ui-kit/vite"
10
+ * export default defineConfig({ plugins: [react(), versionFile()] })
11
+ */
12
+ export declare function versionFile(options?: {
13
+ fileName?: string;
14
+ }): Plugin;
15
+ //# sourceMappingURL=vite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAElC;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,MAAM,CAoBvE"}
package/dist/vite.js ADDED
@@ -0,0 +1,28 @@
1
+ // src/vite.ts
2
+ import { mkdirSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ function versionFile(options = {}) {
5
+ const fileName = options.fileName ?? "version.json";
6
+ const buildTime = (/* @__PURE__ */ new Date()).toISOString();
7
+ let outDir = "dist";
8
+ return {
9
+ name: "motor-hero:version-file",
10
+ config: () => ({
11
+ define: { "import.meta.env.VITE_BUILD_TIME": JSON.stringify(buildTime) }
12
+ }),
13
+ configResolved(config) {
14
+ outDir = config.build.outDir;
15
+ },
16
+ closeBundle() {
17
+ mkdirSync(outDir, { recursive: true });
18
+ writeFileSync(
19
+ join(outDir, fileName),
20
+ JSON.stringify({ buildTime, timestamp: Date.parse(buildTime) }, null, 2)
21
+ );
22
+ }
23
+ };
24
+ }
25
+ export {
26
+ versionFile
27
+ };
28
+ //# sourceMappingURL=vite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/vite.ts"],"sourcesContent":["import { mkdirSync, writeFileSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport type { Plugin } from \"vite\"\n\n/**\n * One build id in two places: baked into the bundle as\n * `import.meta.env.VITE_BUILD_TIME` and written to `dist/version.json`.\n * `UpdateBanner` / `useVersionCheck` compare the two at runtime, so the\n * running app knows its own version instead of guessing it from a fetch.\n *\n * // vite.config.ts\n * import { versionFile } from \"@motor-hero/ui-kit/vite\"\n * export default defineConfig({ plugins: [react(), versionFile()] })\n */\nexport function versionFile(options: { fileName?: string } = {}): Plugin {\n const fileName = options.fileName ?? \"version.json\"\n const buildTime = new Date().toISOString()\n let outDir = \"dist\"\n return {\n name: \"motor-hero:version-file\",\n config: () => ({\n define: { \"import.meta.env.VITE_BUILD_TIME\": JSON.stringify(buildTime) },\n }),\n configResolved(config) {\n outDir = config.build.outDir\n },\n closeBundle() {\n mkdirSync(outDir, { recursive: true })\n writeFileSync(\n join(outDir, fileName),\n JSON.stringify({ buildTime, timestamp: Date.parse(buildTime) }, null, 2),\n )\n },\n }\n}\n"],"mappings":";AAAA,SAAS,WAAW,qBAAqB;AACzC,SAAS,YAAY;AAad,SAAS,YAAY,UAAiC,CAAC,GAAW;AACvE,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AACzC,MAAI,SAAS;AACb,SAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ,OAAO;AAAA,MACb,QAAQ,EAAE,mCAAmC,KAAK,UAAU,SAAS,EAAE;AAAA,IACzE;AAAA,IACA,eAAe,QAAQ;AACrB,eAAS,OAAO,MAAM;AAAA,IACxB;AAAA,IACA,cAAc;AACZ,gBAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;AACrC;AAAA,QACE,KAAK,QAAQ,QAAQ;AAAA,QACrB,KAAK,UAAU,EAAE,WAAW,WAAW,KAAK,MAAM,SAAS,EAAE,GAAG,MAAM,CAAC;AAAA,MACzE;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motor-hero/ui-kit",
3
- "version": "0.45.0",
3
+ "version": "0.46.1",
4
4
  "description": "Componentes React reutilizáveis para o ecossistema MotorHero — shadcn/ui + Tailwind CSS v4",
5
5
  "author": "MotorHero",
6
6
  "license": "UNLICENSED",
@@ -23,7 +23,12 @@
23
23
  "import": "./dist/index.js",
24
24
  "require": "./dist/index.cjs"
25
25
  },
26
- "./styles": "./dist/styles.css"
26
+ "./styles": "./dist/styles.css",
27
+ "./vite": {
28
+ "types": "./dist/vite.d.ts",
29
+ "import": "./dist/vite.js",
30
+ "require": "./dist/vite.cjs"
31
+ }
27
32
  },
28
33
  "files": [
29
34
  "dist",
@@ -20,6 +20,14 @@ export interface AppShellProps {
20
20
  headerActions?: ReactNode
21
21
  collapsible?: boolean
22
22
  defaultCollapsed?: boolean
23
+ /**
24
+ * Bottom of the desktop sidebar, above the collapse control — a product
25
+ * credit, a version, a support link. Receives the collapsed state so it
26
+ * can shrink with the sidebar.
27
+ */
28
+ sidebarFooter?: ReactNode | ((collapsed: boolean) => ReactNode)
29
+ /** Above the header, full width of the content column — `UpdateBanner` goes here. */
30
+ banner?: ReactNode
23
31
  children: ReactNode
24
32
  }
25
33
 
@@ -37,6 +45,8 @@ export function AppShell({
37
45
  headerActions,
38
46
  collapsible = true,
39
47
  defaultCollapsed = false,
48
+ sidebarFooter,
49
+ banner,
40
50
  children,
41
51
  }: AppShellProps) {
42
52
  const [collapsed, setCollapsed] = useState(defaultCollapsed)
@@ -67,6 +77,11 @@ export function AppShell({
67
77
  {collapsed ? (brandCollapsed ?? brand) : brand}
68
78
  </div>
69
79
  <div className="flex-1 overflow-y-auto px-2 py-4">{nav(collapsed)}</div>
80
+ {sidebarFooter && (
81
+ <div className="border-t px-3 py-2" data-slot="sidebar-footer">
82
+ {typeof sidebarFooter === "function" ? sidebarFooter(collapsed) : sidebarFooter}
83
+ </div>
84
+ )}
70
85
  {collapsible && (
71
86
  <div className="border-t p-2">
72
87
  <button
@@ -104,6 +119,7 @@ export function AppShell({
104
119
 
105
120
  {/* Main column */}
106
121
  <div className="flex flex-1 flex-col overflow-hidden">
122
+ {banner}
107
123
  <header className="flex h-14 shrink-0 items-center gap-2 border-b bg-background px-4">
108
124
  <button
109
125
  type="button"
@@ -58,10 +58,16 @@ export interface BaseDialogProps {
58
58
 
59
59
  /**
60
60
  * Standardized, responsive dialog shell:
61
- * - mobile: bottom sheet (full width, up to 92vh tall)
62
- * - desktop: centered dialog (up to 90vh tall)
61
+ * - mobile: bottom sheet (full width, up to 92dvh tall)
62
+ * - desktop: centered dialog (up to 92dvh tall)
63
63
  * Header and footer stay fixed; only the body scrolls.
64
64
  *
65
+ * Heights use `dvh`, not `vh`: on iOS `vh` is locked to the large viewport
66
+ * (toolbars collapsed), so `92vh` can exceed the visible area by 10-15%. With
67
+ * the sheet anchored to `bottom-0` the overflow leaves through the top, and
68
+ * since the body is the only scrollable child, what goes off-screen is the
69
+ * header - title and close button. Does not reproduce outside real iOS.
70
+ *
65
71
  * Also fixes the known Radix bug where `body { pointer-events: none }`
66
72
  * can persist after closing a dialog that contained a Select/Popover,
67
73
  * which would otherwise make the rest of the page unclickable.
@@ -153,9 +159,9 @@ export function BaseDialog({
153
159
  document.body.style.pointerEvents = ""
154
160
  }}
155
161
  className={cn(
156
- "fixed z-50 flex max-h-[92vh] flex-col bg-background shadow-lg outline-none",
162
+ "fixed z-50 flex max-h-[92dvh] flex-col bg-background shadow-lg outline-none",
157
163
  "inset-x-0 bottom-0 rounded-t-2xl",
158
- "sm:inset-x-auto sm:bottom-auto sm:left-1/2 sm:top-1/2 sm:max-h-[92vh] sm:w-full sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-lg",
164
+ "sm:inset-x-auto sm:bottom-auto sm:left-1/2 sm:top-1/2 sm:max-h-[92dvh] sm:w-full sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-lg",
159
165
  "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
160
166
  sizeClasses[size],
161
167
  className,
@@ -178,7 +178,7 @@ export function Combobox({
178
178
  </div>
179
179
  <CommandPrimitive.List
180
180
  ref={listRef}
181
- className="max-h-[50vh] overflow-y-auto overflow-x-hidden p-1 sm:max-h-60"
181
+ className="max-h-[50dvh] overflow-y-auto overflow-x-hidden p-1 sm:max-h-60"
182
182
  >
183
183
  {!isServer && !showCreate && (
184
184
  <CommandPrimitive.Empty className="py-6 text-center text-sm text-muted-foreground">
@@ -260,7 +260,7 @@ export function Combobox({
260
260
  <DrawerPrimitive.Trigger asChild>{trigger}</DrawerPrimitive.Trigger>
261
261
  <DrawerPrimitive.Portal>
262
262
  <DrawerPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80" />
263
- <DrawerPrimitive.Content className="fixed inset-x-0 bottom-0 z-50 mt-24 flex max-h-[85vh] flex-col rounded-t-2xl border bg-popover text-popover-foreground outline-none">
263
+ <DrawerPrimitive.Content className="fixed inset-x-0 bottom-0 z-50 mt-24 flex max-h-[85dvh] flex-col rounded-t-2xl border bg-popover text-popover-foreground outline-none">
264
264
  <DrawerPrimitive.Title className="sr-only">
265
265
  {placeholder}
266
266
  </DrawerPrimitive.Title>
@@ -351,7 +351,7 @@ function DatePickerImpl({
351
351
  onInteractOutside={(event) => {
352
352
  if (triggerRef.current?.contains(event.target as Node)) event.preventDefault()
353
353
  }}
354
- className="fixed inset-x-0 bottom-0 z-50 mt-24 flex max-h-[85vh] flex-col rounded-t-2xl border bg-popover text-popover-foreground outline-none"
354
+ className="fixed inset-x-0 bottom-0 z-50 mt-24 flex max-h-[85dvh] flex-col rounded-t-2xl border bg-popover text-popover-foreground outline-none"
355
355
  >
356
356
  <DrawerPrimitive.Title className="sr-only">
357
357
  {withTime ? "Selecione a data e o horário" : "Selecione a data"}
@@ -147,7 +147,7 @@ export function FilterPopover({
147
147
  <DrawerPrimitive.Trigger asChild>{trigger}</DrawerPrimitive.Trigger>
148
148
  <DrawerPrimitive.Portal>
149
149
  <DrawerPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80" />
150
- <DrawerPrimitive.Content className="fixed inset-x-0 bottom-0 z-50 mt-24 flex max-h-[85vh] flex-col rounded-t-2xl border bg-popover text-popover-foreground outline-none">
150
+ <DrawerPrimitive.Content className="fixed inset-x-0 bottom-0 z-50 mt-24 flex max-h-[85dvh] flex-col rounded-t-2xl border bg-popover text-popover-foreground outline-none">
151
151
  <DrawerPrimitive.Title className="sr-only">
152
152
  {title ?? label}
153
153
  </DrawerPrimitive.Title>
@@ -180,7 +180,7 @@ export function MultiCombobox({
180
180
  </div>
181
181
  <CommandPrimitive.List
182
182
  ref={listRef}
183
- className="max-h-[50vh] overflow-y-auto overflow-x-hidden p-1 sm:max-h-60"
183
+ className="max-h-[50dvh] overflow-y-auto overflow-x-hidden p-1 sm:max-h-60"
184
184
  >
185
185
  {!isServer && (
186
186
  <CommandPrimitive.Empty className="py-6 text-center text-sm text-muted-foreground">
@@ -248,7 +248,7 @@ export function MultiCombobox({
248
248
  </DrawerPrimitive.Trigger>
249
249
  <DrawerPrimitive.Portal>
250
250
  <DrawerPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80" />
251
- <DrawerPrimitive.Content className="fixed inset-x-0 bottom-0 z-50 mt-24 flex max-h-[85vh] flex-col rounded-t-2xl border bg-popover text-popover-foreground outline-none">
251
+ <DrawerPrimitive.Content className="fixed inset-x-0 bottom-0 z-50 mt-24 flex max-h-[85dvh] flex-col rounded-t-2xl border bg-popover text-popover-foreground outline-none">
252
252
  <DrawerPrimitive.Title className="sr-only">
253
253
  {placeholder}
254
254
  </DrawerPrimitive.Title>
@@ -0,0 +1,53 @@
1
+ import { RefreshCw } from "lucide-react"
2
+ import { cn } from "../lib/utils"
3
+ import { type UseVersionCheckOptions, useVersionCheck } from "../hooks/use-version-check"
4
+
5
+ export interface UpdateBannerProps extends UseVersionCheckOptions {
6
+ title?: string
7
+ description?: string
8
+ actionLabel?: string
9
+ className?: string
10
+ }
11
+
12
+ /**
13
+ * A bar that appears when a newer build is deployed and stays until the
14
+ * person reloads — not a toast: a notice that can be missed or dismissed
15
+ * leaves the old bundle running against a new API. Themed with the product
16
+ * tokens so it matches light and dark alike. Put it in `AppShell`'s
17
+ * `banner` slot (or at the top of any page).
18
+ */
19
+ export function UpdateBanner({
20
+ title = "Nova versão disponível",
21
+ description = "Atualize para continuar na versão mais recente.",
22
+ actionLabel = "Atualizar",
23
+ className,
24
+ ...options
25
+ }: UpdateBannerProps) {
26
+ const { updateAvailable, reload } = useVersionCheck(options)
27
+ if (!updateAvailable) return null
28
+
29
+ return (
30
+ <div
31
+ role="status"
32
+ aria-live="polite"
33
+ data-slot="update-banner"
34
+ className={cn(
35
+ "flex flex-wrap items-center justify-between gap-3 border-b border-primary/30 bg-primary px-4 py-2 text-sm text-primary-foreground",
36
+ className,
37
+ )}
38
+ >
39
+ <span className="flex min-w-0 items-center gap-2">
40
+ <RefreshCw className="h-4 w-4 shrink-0 animate-spin [animation-duration:3s]" aria-hidden />
41
+ <span className="font-medium">{title}</span>
42
+ <span className="hidden truncate opacity-80 sm:inline">{description}</span>
43
+ </span>
44
+ <button
45
+ type="button"
46
+ onClick={reload}
47
+ className="shrink-0 rounded-md bg-background px-3 py-1 text-xs font-medium text-foreground transition-colors hover:bg-background/90"
48
+ >
49
+ {actionLabel}
50
+ </button>
51
+ </div>
52
+ )
53
+ }
@@ -0,0 +1,72 @@
1
+ import { useCallback, useEffect, useState } from "react"
2
+
3
+ export interface UseVersionCheckOptions {
4
+ /** The build id baked into the running bundle (see `versionFile` in `@motor-hero/ui-kit/vite`). */
5
+ currentBuild: string | undefined
6
+ /** Where the deployed build id is published. */
7
+ url?: string
8
+ intervalMs?: number
9
+ }
10
+
11
+ export interface VersionCheckState {
12
+ /** The deployed build differs from the running one. */
13
+ updateAvailable: boolean
14
+ /** Grows on every poll that still sees a newer build. */
15
+ staleChecks: number
16
+ latestBuild: string | null
17
+ reload: () => void
18
+ }
19
+
20
+ /**
21
+ * Polls the deployed `version.json` and compares its `buildTime` with the
22
+ * build id baked into this bundle. Nothing is inferred from a first fetch —
23
+ * a page loaded during a deploy, or a poll that fails while the server
24
+ * restarts, cannot make the running code believe it is current. Polling
25
+ * pauses while the tab is hidden and runs at once when it comes back.
26
+ */
27
+ export function useVersionCheck({
28
+ currentBuild,
29
+ url = "/version.json",
30
+ intervalMs = 60_000,
31
+ }: UseVersionCheckOptions): VersionCheckState {
32
+ const [latestBuild, setLatestBuild] = useState<string | null>(null)
33
+ const [staleChecks, setStaleChecks] = useState(0)
34
+
35
+ useEffect(() => {
36
+ if (!currentBuild) return
37
+ let cancelled = false
38
+
39
+ const check = async () => {
40
+ if (document.visibilityState === "hidden") return
41
+ try {
42
+ const res = await fetch(url, { cache: "no-store" })
43
+ const contentType = res.headers.get("content-type") ?? ""
44
+ if (!res.ok || !contentType.includes("application/json")) return
45
+ const data: { buildTime?: unknown } = await res.json()
46
+ if (cancelled || typeof data.buildTime !== "string") return
47
+ if (data.buildTime !== currentBuild) {
48
+ setLatestBuild(data.buildTime)
49
+ setStaleChecks((n) => n + 1)
50
+ }
51
+ } catch (error) {
52
+ console.error("Version check failed", error)
53
+ }
54
+ }
55
+
56
+ check()
57
+ const interval = setInterval(check, intervalMs)
58
+ const onVisible = () => {
59
+ if (document.visibilityState === "visible") check()
60
+ }
61
+ document.addEventListener("visibilitychange", onVisible)
62
+ return () => {
63
+ cancelled = true
64
+ clearInterval(interval)
65
+ document.removeEventListener("visibilitychange", onVisible)
66
+ }
67
+ }, [currentBuild, url, intervalMs])
68
+
69
+ const reload = useCallback(() => window.location.reload(), [])
70
+
71
+ return { updateAvailable: latestBuild !== null, staleChecks, latestBuild, reload }
72
+ }
package/src/index.ts CHANGED
@@ -64,6 +64,8 @@ export type {
64
64
  } from "./components/theme-provider";
65
65
  export { ThemeProvider, useTheme } from "./components/theme-provider";
66
66
  export { Toaster } from "./components/toaster";
67
+ export type { UpdateBannerProps } from "./components/update-banner";
68
+ export { UpdateBanner } from "./components/update-banner";
67
69
  export type { RenderLink, RenderLinkProps } from "./components/types";
68
70
  export { defaultRenderLink } from "./components/types";
69
71
  export type {
@@ -83,6 +85,8 @@ export {
83
85
  // Hooks
84
86
  export { useDisclosure } from "./hooks/use-disclosure";
85
87
  export { useIsDesktop } from "./hooks/use-is-desktop";
88
+ export type { UseVersionCheckOptions, VersionCheckState } from "./hooks/use-version-check";
89
+ export { useVersionCheck } from "./hooks/use-version-check";
86
90
  export { toast, useCustomToast } from "./hooks/use-toast";
87
91
  export { extractApiError } from "./lib/api-error";
88
92
  export type { CompanyTheme, CompanyThemeTokens } from "./lib/company-theme";
package/src/vite.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import type { Plugin } from "vite"
4
+
5
+ /**
6
+ * One build id in two places: baked into the bundle as
7
+ * `import.meta.env.VITE_BUILD_TIME` and written to `dist/version.json`.
8
+ * `UpdateBanner` / `useVersionCheck` compare the two at runtime, so the
9
+ * running app knows its own version instead of guessing it from a fetch.
10
+ *
11
+ * // vite.config.ts
12
+ * import { versionFile } from "@motor-hero/ui-kit/vite"
13
+ * export default defineConfig({ plugins: [react(), versionFile()] })
14
+ */
15
+ export function versionFile(options: { fileName?: string } = {}): Plugin {
16
+ const fileName = options.fileName ?? "version.json"
17
+ const buildTime = new Date().toISOString()
18
+ let outDir = "dist"
19
+ return {
20
+ name: "motor-hero:version-file",
21
+ config: () => ({
22
+ define: { "import.meta.env.VITE_BUILD_TIME": JSON.stringify(buildTime) },
23
+ }),
24
+ configResolved(config) {
25
+ outDir = config.build.outDir
26
+ },
27
+ closeBundle() {
28
+ mkdirSync(outDir, { recursive: true })
29
+ writeFileSync(
30
+ join(outDir, fileName),
31
+ JSON.stringify({ buildTime, timestamp: Date.parse(buildTime) }, null, 2),
32
+ )
33
+ },
34
+ }
35
+ }