@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: !
|
|
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 (!
|
|
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"
|
|
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.
|
|
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": {
|