@stevederico/skateboard-ui 5.1.0 → 5.2.3

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,3 +1,22 @@
1
+ 5.2.3
2
+
3
+ No border on phones
4
+
5
+ 5.2.2
6
+
7
+ Fix CI install
8
+
9
+ 5.2.1
10
+
11
+ Publish from CI
12
+
13
+ 5.2.0
14
+
15
+ Add live list option
16
+ Reload stale builds
17
+ Raise dark contrast
18
+ Bolder page titles
19
+
1
20
  5.1.0
2
21
 
3
22
  Add loadLegal option
package/dist/App.js CHANGED
@@ -112,6 +112,22 @@ export function createSkateboardApp({ constants, appRoutes, defaultRoute = appRo
112
112
  validateConstants(constants);
113
113
  // Initialize utilities with constants
114
114
  initializeUtilities(constants);
115
+ // A deploy replaces the hashed chunk files. A page opened before it asks
116
+ // for chunks that are gone ("Importing a module script failed"); reload
117
+ // once to pick up the new build instead of showing a broken route.
118
+ window.addEventListener('vite:preloadError', (event) => {
119
+ event.preventDefault();
120
+ try {
121
+ const last = Number(sessionStorage.getItem('skateboard-chunk-reload') || 0);
122
+ if (Date.now() - last < 10_000)
123
+ return;
124
+ sessionStorage.setItem('skateboard-chunk-reload', String(Date.now()));
125
+ }
126
+ catch {
127
+ // Storage can be unavailable (private mode, some web views); reload anyway
128
+ }
129
+ window.location.reload();
130
+ });
115
131
  // Prevent theme flash by setting dark class before React hydrates
116
132
  // This runs synchronously before render, reading from localStorage or system preference
117
133
  const storageKey = 'theme';
@@ -360,13 +360,24 @@ export declare function apiRequestWithParams<T = any>(endpoint: string, params?:
360
360
  * @throws {Error} - If required fields are missing or invalid
361
361
  */
362
362
  export declare function validateConstants(constants: SkateboardConstants): SkateboardConstants;
363
+ /** Options for {@link useListData}. */
364
+ export interface ListDataOptions {
365
+ /**
366
+ * Keep the list fresh: refetch quietly every this many milliseconds while
367
+ * the page is visible, and again when the user returns to it. `true`
368
+ * means 60 seconds. Quiet refetches skip the loading state and keep the
369
+ * last good rows on error, so new items just appear.
370
+ */
371
+ live?: boolean | number;
372
+ }
363
373
  /**
364
- * Standard list data fetcher with optional sorting
374
+ * Standard list data fetcher with optional sorting and live refresh
365
375
  * @param {string} endpoint - API endpoint to fetch from
366
376
  * @param {function} sortFn - Optional sort function for results
377
+ * @param {ListDataOptions} options - `live` turns on quiet periodic refetch
367
378
  * @returns {object} - { data, loading, error, refetch }
368
379
  */
369
- export declare function useListData<T = any>(endpoint: string, sortFn?: ((a: T, b: T) => number) | null): {
380
+ export declare function useListData<T = any>(endpoint: string, sortFn?: ((a: T, b: T) => number) | null, options?: ListDataOptions): {
370
381
  data: T[];
371
382
  loading: boolean;
372
383
  error: string | null;
@@ -838,22 +838,24 @@ export function validateConstants(constants) {
838
838
  }
839
839
  return constants;
840
840
  }
841
- // ===== REACT HOOKS =====
842
841
  /**
843
- * Standard list data fetcher with optional sorting
842
+ * Standard list data fetcher with optional sorting and live refresh
844
843
  * @param {string} endpoint - API endpoint to fetch from
845
844
  * @param {function} sortFn - Optional sort function for results
845
+ * @param {ListDataOptions} options - `live` turns on quiet periodic refetch
846
846
  * @returns {object} - { data, loading, error, refetch }
847
847
  */
848
- export function useListData(endpoint, sortFn = null) {
848
+ export function useListData(endpoint, sortFn = null, options = {}) {
849
849
  const [data, setData] = useState([]);
850
850
  const [loading, setLoading] = useState(true);
851
851
  const [error, setError] = useState(null);
852
852
  // Keep sortFn in a ref so an inline sort function doesn't retrigger the fetch effect every render
853
853
  const sortFnRef = useRef(sortFn);
854
854
  sortFnRef.current = sortFn;
855
- const fetchData = async (signal) => {
856
- setLoading(true);
855
+ const liveMs = options.live === true ? 60_000 : typeof options.live === 'number' ? options.live : 0;
856
+ const fetchData = async (signal, quiet = false) => {
857
+ if (!quiet)
858
+ setLoading(true);
857
859
  try {
858
860
  const result = await apiRequest(endpoint, { signal });
859
861
  const sort = sortFnRef.current;
@@ -865,17 +867,32 @@ export function useListData(endpoint, sortFn = null) {
865
867
  // Ignore abort errors (DOMException in the browser, not an Error)
866
868
  if ((err instanceof DOMException || err instanceof Error) && err.name === 'AbortError')
867
869
  return;
868
- setError(err instanceof Error ? err.message : String(err));
870
+ // A failed quiet refresh keeps showing the last good rows
871
+ if (!quiet)
872
+ setError(err instanceof Error ? err.message : String(err));
869
873
  }
870
874
  finally {
871
- setLoading(false);
875
+ if (!quiet)
876
+ setLoading(false);
872
877
  }
873
878
  };
874
879
  useEffect(() => {
875
880
  const controller = new AbortController();
876
881
  fetchData(controller.signal);
877
- return () => controller.abort();
878
- }, [endpoint]);
882
+ if (liveMs <= 0)
883
+ return () => controller.abort();
884
+ const tick = () => {
885
+ if (document.visibilityState === 'visible')
886
+ fetchData(controller.signal, true);
887
+ };
888
+ const timer = setInterval(tick, liveMs);
889
+ document.addEventListener('visibilitychange', tick);
890
+ return () => {
891
+ controller.abort();
892
+ clearInterval(timer);
893
+ document.removeEventListener('visibilitychange', tick);
894
+ };
895
+ }, [endpoint, liveMs]);
879
896
  return { data, loading, error, refetch: () => fetchData() };
880
897
  }
881
898
  // ===== UI VISIBILITY CONTROLS =====
@@ -4,6 +4,6 @@ import { Button } from "../../ui/button.js";
4
4
  import { Badge } from "../../ui/badge.js";
5
5
  import { cn } from "../../shadcn/lib/utils.js";
6
6
  function Header({ title, buttonTitle, onButtonTitleClick, buttonClass, className, children, ...props }) {
7
- return (_jsxs(_Fragment, { children: [_jsx("header", { className: cn("flex h-(--header-height) shrink-0 items-center gap-2", className), ...props, children: _jsxs("div", { className: "flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6", children: [_jsx("h1", { className: "text-lg font-medium", children: title }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [typeof buttonTitle !== "undefined" && (_jsx(Button, { variant: "ghost", size: "sm", className: buttonClass || "", onClick: onButtonTitleClick, children: buttonTitle })), children] })] }) }), _jsx(Separator, {})] }));
7
+ return (_jsxs(_Fragment, { children: [_jsx("header", { className: cn("flex h-(--header-height) shrink-0 items-center gap-2", className), ...props, children: _jsxs("div", { className: "flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6", children: [_jsx("h1", { className: "text-[1.625rem] leading-8 font-extrabold tracking-tight", children: title }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [typeof buttonTitle !== "undefined" && (_jsx(Button, { variant: "ghost", size: "sm", className: buttonClass || "", onClick: onButtonTitleClick, children: buttonTitle })), children] })] }) }), _jsx(Separator, {})] }));
8
8
  }
9
9
  export default Header;
@@ -14,5 +14,5 @@ export default function Layout({ children }) {
14
14
  return (_jsxs("div", { className: "min-h-screen flex flex-col pt-[env(safe-area-inset-top)] pb-[calc(5rem+env(safe-area-inset-bottom))] md:pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]", children: [_jsx("a", { href: "#main", className: "sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-background focus:text-foreground focus:rounded-md focus:ring-2 focus:ring-ring", children: "Skip to content" }), _jsxs(SidebarProvider, { defaultOpen: !constants.sidebarCollapsed, style: {
15
15
  '--sidebar-width': '12rem',
16
16
  '--header-height': '3.5rem',
17
- }, children: [showSidebar && _jsx(Sidebar, { variant: "inset" }), _jsx(SidebarInset, { id: "main", className: `border border-border/50 ${constants.hideSidebarInsetRounding ? "md:peer-data-[variant=inset]:rounded-none" : ""}`, children: _jsx(Outlet, {}) })] }), showTabBar && _jsx(TabBar, { className: "md:hidden" })] }));
17
+ }, children: [showSidebar && _jsx(Sidebar, { variant: "inset" }), _jsx(SidebarInset, { id: "main", className: `md:border md:border-border/50 ${constants.hideSidebarInsetRounding ? "md:peer-data-[variant=inset]:rounded-none" : ""}`, children: _jsx(Outlet, {}) })] }), showTabBar && _jsx(TabBar, { className: "md:hidden" })] }));
18
18
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stevederico/skateboard-ui",
3
3
  "private": false,
4
- "version": "5.1.0",
4
+ "version": "5.2.3",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
package/styles.css CHANGED
@@ -170,22 +170,22 @@
170
170
  .dark {
171
171
  --background: oklch(0.05 0 0);
172
172
  --foreground: oklch(0.985 0 0);
173
- --card: oklch(0.12 0 0);
173
+ --card: oklch(0.19 0 0);
174
174
  --card-foreground: oklch(0.985 0 0);
175
- --popover: oklch(0.12 0 0);
175
+ --popover: oklch(0.19 0 0);
176
176
  --popover-foreground: oklch(0.985 0 0);
177
177
  --primary: oklch(0.985 0 0);
178
178
  --primary-foreground: oklch(0.05 0 0);
179
- --secondary: oklch(0.18 0 0);
179
+ --secondary: oklch(0.28 0 0);
180
180
  --secondary-foreground: oklch(0.985 0 0);
181
- --muted: oklch(0.18 0 0);
182
- --muted-foreground: oklch(0.708 0 0);
183
- --accent: oklch(0.2 0 0);
181
+ --muted: oklch(0.28 0 0);
182
+ --muted-foreground: oklch(0.8 0 0);
183
+ --accent: oklch(0.28 0 0);
184
184
  --accent-foreground: oklch(0.985 0 0);
185
185
  --destructive: oklch(0.396 0.141 25.723);
186
186
  --destructive-foreground: oklch(0.637 0.237 25.331);
187
- --border: oklch(0.32 0 0);
188
- --input: oklch(0.30 0 0);
187
+ --border: oklch(0.5 0 0);
188
+ --input: oklch(0.5 0 0);
189
189
  --ring: oklch(0.439 0 0);
190
190
  --chart-1: oklch(0.488 0.243 264.376);
191
191
  --chart-2: oklch(0.696 0.17 162.48);
@@ -207,22 +207,22 @@
207
207
  :root:not(.light) {
208
208
  --background: oklch(0.05 0 0);
209
209
  --foreground: oklch(0.985 0 0);
210
- --card: oklch(0.12 0 0);
210
+ --card: oklch(0.19 0 0);
211
211
  --card-foreground: oklch(0.985 0 0);
212
- --popover: oklch(0.12 0 0);
212
+ --popover: oklch(0.19 0 0);
213
213
  --popover-foreground: oklch(0.985 0 0);
214
214
  --primary: oklch(0.985 0 0);
215
215
  --primary-foreground: oklch(0.05 0 0);
216
- --secondary: oklch(0.18 0 0);
216
+ --secondary: oklch(0.28 0 0);
217
217
  --secondary-foreground: oklch(0.985 0 0);
218
- --muted: oklch(0.18 0 0);
219
- --muted-foreground: oklch(0.708 0 0);
220
- --accent: oklch(0.2 0 0);
218
+ --muted: oklch(0.28 0 0);
219
+ --muted-foreground: oklch(0.8 0 0);
220
+ --accent: oklch(0.28 0 0);
221
221
  --accent-foreground: oklch(0.985 0 0);
222
222
  --destructive: oklch(0.396 0.141 25.723);
223
223
  --destructive-foreground: oklch(0.637 0.237 25.331);
224
- --border: oklch(0.32 0 0);
225
- --input: oklch(0.30 0 0);
224
+ --border: oklch(0.5 0 0);
225
+ --input: oklch(0.5 0 0);
226
226
  --ring: oklch(0.439 0 0);
227
227
  --chart-1: oklch(0.488 0.243 264.376);
228
228
  --chart-2: oklch(0.696 0.17 162.48);