@tristan2828/ui-foundation 3.1.1 → 3.1.2

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.
@@ -162,7 +162,28 @@ When the useful question is "how soon?" rather than "which day?".
162
162
  rebuilt as UTC midnight** (`Date.UTC(y, m, d)`). Subtracting `Date.now()`
163
163
  shifts the answer by a day for anyone west of UTC.
164
164
  - Relative text **goes stale**, so a fixture pinning a date drifts. Freeze
165
- the clock (Playwright's `page.clock`) in any test asserting one.
165
+ the clock (Playwright's `page.clock`) in any test asserting one, and
166
+ assert one past and one future value:
167
+
168
+ ```ts
169
+ // West of UTC, where subtracting Date.now() would be off by a day.
170
+ test.use({ timezoneId: 'America/Los_Angeles' })
171
+
172
+ test('release dates read relative to today', async ({ page }) => {
173
+ // Before goto, so the first render already sees it. Noon UTC is the same
174
+ // calendar day in every timezone from UTC-11 to UTC+11.
175
+ await page.clock.setFixedTime(new Date('2026-06-01T12:00:00Z'))
176
+ await page.goto('/games')
177
+ // Mock fixtures: one released 2026-04-01, one due 2026-06-22.
178
+ await expect(page.getByRole('cell', { name: '2 months ago' })).toBeVisible()
179
+ await expect(page.getByRole('cell', { name: 'in 3 weeks' })).toBeVisible()
180
+ })
181
+ ```
182
+
183
+ `setFixedTime` pins `Date.now()` and `new Date()` and leaves the real
184
+ timers alone, so debounces and TanStack Query behave as usual. Reach for
185
+ `page.clock.install()` only when a test has to move time forward.
186
+
166
187
  - `title` is mouse-only. If the exact date matters, use pattern 4 instead.
167
188
  - **Cost:** "2 years ago" is vague, and the column doesn't look sorted even
168
189
  though server-side sorting still works.
@@ -1,13 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { LogOutIcon, MoonIcon, SunIcon } from 'lucide-react';
3
3
  import { useTheme } from 'next-themes';
4
- import { useState } from 'react';
4
+ import { useEffect, useState } from 'react';
5
5
  import { NavLink, Navigate, Outlet, useLocation } from 'react-router';
6
6
  import { useAuth } from '../../auth/use-auth.js';
7
7
  import { ErrorState } from './error-state.js';
8
8
  import { Button } from '../ui/button.js';
9
9
  import { Skeleton } from '../ui/skeleton.js';
10
- import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger, } from '../ui/sidebar.js';
10
+ import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger, useSidebar, } from '../ui/sidebar.js';
11
11
  import { Toaster } from '../ui/sonner.js';
12
12
  import { TooltipProvider } from '../ui/tooltip.js';
13
13
  // Must match SIDEBAR_COOKIE_NAME in ../ui/sidebar.tsx, which writes the
@@ -17,6 +17,18 @@ function storedSidebarOpen(fallback) {
17
17
  const match = document.cookie.match(new RegExp(`(?:^|; )${SIDEBAR_COOKIE_NAME}=([^;]*)`));
18
18
  return match ? match[1] === 'true' : fallback;
19
19
  }
20
+ // Below the mobile breakpoint the sidebar is a modal sheet, and the
21
+ // primitive leaves it open when one of its links navigates, covering the
22
+ // page the user just asked for. Keyed on location.key, so following a link
23
+ // to the page already showing closes it too. Covers sidebarExtra's links.
24
+ function CloseMobileSidebarOnNavigate() {
25
+ const { setOpenMobile } = useSidebar();
26
+ const { key } = useLocation();
27
+ useEffect(() => {
28
+ setOpenMobile(false);
29
+ }, [key, setOpenMobile]);
30
+ return null;
31
+ }
20
32
  function ThemeToggle() {
21
33
  const { resolvedTheme, setTheme } = useTheme();
22
34
  const isDark = resolvedTheme === 'dark';
@@ -52,7 +64,7 @@ export function AppShell({ title, nav, sidebarExtra, defaultSidebarOpen = true }
52
64
  const from = `${location.pathname}${location.search}${location.hash}`;
53
65
  return _jsx(Navigate, { to: "/login", replace: true, state: isLoggingOut ? undefined : { from } });
54
66
  }
55
- return (_jsxs(TooltipProvider, { children: [_jsxs(SidebarProvider, { defaultOpen: storedSidebarOpen(defaultSidebarOpen), children: [_jsx("nav", { "aria-label": "Primary", children: _jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsx("span", { className: "px-2 text-sm font-semibold text-sidebar-foreground group-data-[collapsible=icon]:hidden", children: title }) }), _jsxs(SidebarContent, { children: [_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: "Navigation" }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: nav.map(({ to, label, icon: Icon, end }) => {
67
+ return (_jsxs(TooltipProvider, { children: [_jsxs(SidebarProvider, { defaultOpen: storedSidebarOpen(defaultSidebarOpen), children: [_jsx(CloseMobileSidebarOnNavigate, {}), _jsx("nav", { "aria-label": "Primary", children: _jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsx("span", { className: "px-2 text-sm font-semibold text-sidebar-foreground group-data-[collapsible=icon]:hidden", children: title }) }), _jsxs(SidebarContent, { children: [_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: "Navigation" }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: nav.map(({ to, label, icon: Icon, end }) => {
56
68
  const isActive = end
57
69
  ? location.pathname === to
58
70
  : location.pathname.startsWith(to);
@@ -182,12 +182,17 @@ const sidebarMenuButtonVariants = cva("peer/menu-button group/menu-button flex w
182
182
  });
183
183
  function SidebarMenuButton({ render, isActive = false, variant = "default", size = "default", tooltip, className, ...props }) {
184
184
  const { isMobile, state } = useSidebar();
185
+ // Patched (ui-foundation): upstream keeps the tooltip on mobile and only
186
+ // hides its content, so keyboard focus still opened an invisible tooltip
187
+ // and the first Escape closed that instead of the sidebar sheet. On
188
+ // mobile the label is always visible, so there is no tooltip at all.
189
+ const hasTooltip = !!tooltip && !isMobile;
185
190
  const comp = useRender({
186
191
  defaultTagName: "button",
187
192
  props: mergeProps({
188
193
  className: cn(sidebarMenuButtonVariants({ variant, size }), className),
189
194
  }, props),
190
- render: !tooltip ? render : _jsx(TooltipTrigger, { render: render }),
195
+ render: !hasTooltip ? render : _jsx(TooltipTrigger, { render: render }),
191
196
  state: {
192
197
  slot: "sidebar-menu-button",
193
198
  sidebar: "menu-button",
@@ -195,7 +200,7 @@ function SidebarMenuButton({ render, isActive = false, variant = "default", size
195
200
  active: isActive,
196
201
  },
197
202
  });
198
- if (!tooltip) {
203
+ if (!hasTooltip) {
199
204
  return comp;
200
205
  }
201
206
  if (typeof tooltip === "string") {
@@ -203,7 +208,7 @@ function SidebarMenuButton({ render, isActive = false, variant = "default", size
203
208
  children: tooltip,
204
209
  };
205
210
  }
206
- return (_jsxs(Tooltip, { children: [comp, _jsx(TooltipContent, { side: "right", align: "center", hidden: state !== "collapsed" || isMobile, ...tooltip })] }));
211
+ return (_jsxs(Tooltip, { children: [comp, _jsx(TooltipContent, { side: "right", align: "center", hidden: state !== "collapsed", ...tooltip })] }));
207
212
  }
208
213
  function SidebarMenuAction({ className, render, showOnHover = false, ...props }) {
209
214
  return useRender({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tristan2828/ui-foundation",
3
- "version": "3.1.1",
3
+ "version": "3.1.2",
4
4
  "description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
5
5
  "license": "MIT",
6
6
  "repository": {