Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23f53d46a7 | ||
|
|
f693edda20 | ||
|
|
71454da082 | ||
|
|
263e173604 |
+454
-164
File diff suppressed because it is too large
Load Diff
+14
-14
@@ -11,14 +11,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/geist": "^5.3.0",
|
||||
"@scalar/api-reference-react": "^0.9.63",
|
||||
"@tanstack/react-router": "^1.170.28",
|
||||
"@tanstack/react-start": "^1.168.45",
|
||||
"@unom/app-ui": "^0.2.1",
|
||||
"@scalar/api-reference-react": "^0.9.65",
|
||||
"@tanstack/react-router": "^1.170.32",
|
||||
"@tanstack/react-start": "^1.168.49",
|
||||
"@unom/app-ui": "^0.3.0",
|
||||
"@unom/style": "^0.4.4",
|
||||
"@unom/ui": "^0.9.2",
|
||||
"fumadocs-core": "^16.14.4",
|
||||
"fumadocs-ui": "^16.14.4",
|
||||
"@unom/ui": "^0.10.0",
|
||||
"fumadocs-core": "^16.15.4",
|
||||
"fumadocs-ui": "^16.15.4",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8"
|
||||
},
|
||||
@@ -26,14 +26,14 @@
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@tanstack/nitro-v2-vite-plugin": "^1.155.0",
|
||||
"@types/mdx": "^2.0.14",
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/node": "^26",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
"fumadocs-mdx": "^15.2.3",
|
||||
"@types/react-dom": "^19.2.5",
|
||||
"@vitejs/plugin-react": "^6",
|
||||
"fumadocs-mdx": "^15.4.0",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.6",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
"typescript": "^7",
|
||||
"vite": "^8",
|
||||
"vite-tsconfig-paths": "^6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
import { createRouter as createTanStackRouter, Link } from '@tanstack/react-router'
|
||||
import { reloadOnStaleChunk } from '@unom/ui/preload-reload'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
|
||||
// Hydration can build a second router and discard the first. `reloadOnStaleChunk`
|
||||
// registers once, so its callback has to read whichever router is live at event
|
||||
// time rather than closing over the one that happened to exist at startup.
|
||||
let liveRouter: { latestLocation: { href: string } } | undefined
|
||||
|
||||
export function getRouter() {
|
||||
return createTanStackRouter({
|
||||
const router = createTanStackRouter({
|
||||
routeTree,
|
||||
defaultPreload: 'intent',
|
||||
scrollRestoration: true,
|
||||
defaultNotFoundComponent: NotFound,
|
||||
})
|
||||
|
||||
liveRouter = router
|
||||
|
||||
// A deploy replaces every hashed chunk, so a tab opened before it asks for files the
|
||||
// new server has never heard of and the next navigation dies — with `defaultPreload:
|
||||
// 'intent'`, a hover is enough to trip it. Handing back where the router was heading
|
||||
// means the click that tripped it still lands on the page the reader asked for; during
|
||||
// a hover-preload that is the current URL, which degrades to a plain reload.
|
||||
reloadOnStaleChunk(10_000, () => liveRouter?.latestLocation.href)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
function NotFound() {
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.5.1/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.5.11/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": ["**"]
|
||||
"includes": ["**", "!public", "!storybook-static"]
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
|
||||
+328
-240
File diff suppressed because it is too large
Load Diff
+652
-492
File diff suppressed because it is too large
Load Diff
+23
-23
@@ -23,42 +23,42 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/geist": "^5.3.0",
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"@tanstack/react-router": "^1.170.18",
|
||||
"@tanstack/react-start": "^1.168.32",
|
||||
"@tanstack/react-query": "^5.102.8",
|
||||
"@tanstack/react-router": "^1.170.32",
|
||||
"@tanstack/react-start": "^1.168.49",
|
||||
"@unom/style": "^0.4.4",
|
||||
"@unom/ui": "^0.9.2",
|
||||
"@unom/ui": "^0.10.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.469.0",
|
||||
"motion": "^12.42.2",
|
||||
"radix-ui": "^1.6.4",
|
||||
"lucide-react": "^1",
|
||||
"motion": "^13.1.1",
|
||||
"radix-ui": "^1.6.7",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"recharts": "^3.10.0",
|
||||
"tailwind-merge": "^2.6.1",
|
||||
"recharts": "^3.10.1",
|
||||
"tailwind-merge": "^3",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.5.5",
|
||||
"@inlang/paraglide-js": "^2.22.0",
|
||||
"@inlang/plugin-message-format": "^4.4.0",
|
||||
"@storybook/react-vite": "^10.5.3",
|
||||
"@biomejs/biome": "^2.5.11",
|
||||
"@inlang/paraglide-js": "^2.25.0",
|
||||
"@inlang/plugin-message-format": "^4.4.3",
|
||||
"@storybook/react-vite": "^10.5.10",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@tanstack/nitro-v2-vite-plugin": "^1.155.0",
|
||||
"@types/node": "^22.20.1",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
"@types/node": "^26",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.5",
|
||||
"@vitejs/plugin-react": "^6",
|
||||
"bun2nix": "2.1.2",
|
||||
"orval": "^8.22.0",
|
||||
"playwright": "^1.61.1",
|
||||
"storybook": "^10.5.3",
|
||||
"orval": "^8.26.0",
|
||||
"playwright": "^1.62.1",
|
||||
"storybook": "^10.5.10",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.6",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
"typescript": "^7",
|
||||
"vite": "^8",
|
||||
"vite-tsconfig-paths": "^6"
|
||||
},
|
||||
"overrides": {
|
||||
"tar": "^7.5.21",
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
//
|
||||
// Wins over the `/api/**` catch-all by h3 route specificity — the catch-all injects the admin
|
||||
// bearer unconditionally, so without this file the host-side check is inert for this caller.
|
||||
import { defineEventHandler, getRequestURL, getRouterParam, readBody } from "h3";
|
||||
import {
|
||||
defineEventHandler,
|
||||
getRequestURL,
|
||||
getRouterParam,
|
||||
readBody,
|
||||
} from "h3";
|
||||
import { forwardJson } from "../../../../../util/forward";
|
||||
import { confirmIfCommandExecution } from "../../../../../util/libraryConfirm";
|
||||
|
||||
|
||||
@@ -27,7 +27,9 @@ interface EntryLike {
|
||||
}
|
||||
|
||||
/** Does this entry carry a field the host will hand to a shell? */
|
||||
export function carriesCommandExecution(entry: EntryLike | null | undefined): boolean {
|
||||
export function carriesCommandExecution(
|
||||
entry: EntryLike | null | undefined,
|
||||
): boolean {
|
||||
if (!entry || typeof entry !== "object") return false;
|
||||
if (Array.isArray(entry.prep) && entry.prep.length > 0) return true;
|
||||
return entry.launch?.kind === "command";
|
||||
|
||||
@@ -82,11 +82,11 @@ const resolve = (os?: string): { viewBox: string; d: string } | undefined => {
|
||||
};
|
||||
|
||||
/** The host's OS mark; a generic monitor when the chain is absent or entirely unknown. */
|
||||
export const OsIcon: FC<{ os?: string; className?: string; label?: string }> = ({
|
||||
os,
|
||||
className,
|
||||
label,
|
||||
}) => {
|
||||
export const OsIcon: FC<{
|
||||
os?: string;
|
||||
className?: string;
|
||||
label?: string;
|
||||
}> = ({ os, className, label }) => {
|
||||
const icon = resolve(os);
|
||||
if (!icon) return <Monitor className={className} aria-hidden />;
|
||||
return (
|
||||
|
||||
+17
-1
@@ -1,5 +1,6 @@
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
||||
import { reloadOnStaleChunk } from "@unom/ui/preload-reload";
|
||||
import { ApiError } from "./api/fetcher";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
|
||||
@@ -38,6 +39,10 @@ function createQueryClient() {
|
||||
*/
|
||||
let browserQueryClient: QueryClient | undefined;
|
||||
|
||||
// Same rebuild, same reason: `reloadOnStaleChunk` registers once, so its callback reads
|
||||
// whichever router is live at event time instead of the one discarded by hydration.
|
||||
let liveRouter: { latestLocation: { href: string } } | undefined;
|
||||
|
||||
export function getRouter() {
|
||||
let queryClient: QueryClient;
|
||||
if (typeof window === "undefined") {
|
||||
@@ -47,7 +52,7 @@ export function getRouter() {
|
||||
queryClient = browserQueryClient;
|
||||
}
|
||||
|
||||
return createTanStackRouter({
|
||||
const router = createTanStackRouter({
|
||||
routeTree,
|
||||
context: { queryClient },
|
||||
defaultPreload: "intent",
|
||||
@@ -56,6 +61,17 @@ export function getRouter() {
|
||||
<QueryProvider client={queryClient}>{children}</QueryProvider>
|
||||
),
|
||||
});
|
||||
|
||||
liveRouter = router;
|
||||
|
||||
// A deploy replaces every hashed chunk, so a console left open across one asks for files
|
||||
// the new server has never heard of and the next navigation dies — with `defaultPreload:
|
||||
// "intent"`, a hover is enough to trip it. Handing back where the router was heading means
|
||||
// the click that tripped it still lands on the page the operator asked for; during a
|
||||
// hover-preload that is the current URL, which degrades to a plain reload.
|
||||
reloadOnStaleChunk(10_000, () => liveRouter?.latestLocation.href);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
// Local import kept below the function so the module reads top-down.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { SectionDisplays } from "@/sections/Displays";
|
||||
|
||||
export const Route = createFileRoute("/displays")({ component: SectionDisplays });
|
||||
export const Route = createFileRoute("/displays")({
|
||||
component: SectionDisplays,
|
||||
});
|
||||
|
||||
@@ -93,7 +93,9 @@ const GameRow: FC<{
|
||||
})
|
||||
: untracked
|
||||
? m.games_untracked_note()
|
||||
: [game.client, planeLabel(game.plane)].filter(Boolean).join(" · ")}
|
||||
: [game.client, planeLabel(game.plane)]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "@unom/ui/toast";
|
||||
import { type FC, useEffect, useMemo } from "react";
|
||||
import { type FC, useMemo } from "react";
|
||||
import {
|
||||
getGetLibraryQueryKey,
|
||||
useDeleteCustomGame,
|
||||
@@ -27,16 +27,11 @@ export const LibraryGridSection: FC<{
|
||||
onEdit: (entry: OperatorGameEntry) => void;
|
||||
/** Show only entries owned by this provider, or everything when null. */
|
||||
providerFilter?: string | null;
|
||||
/** Reports the full (unfiltered) list up, so the providers card can count owners. */
|
||||
onEntries?: (entries: OperatorGameEntry[]) => void;
|
||||
}> = ({ onEdit, providerFilter, onEntries }) => {
|
||||
}> = ({ onEdit, providerFilter }) => {
|
||||
const qc = useQueryClient();
|
||||
const { confirm } = useDialogs();
|
||||
const library = useGetLibrary();
|
||||
const all = library.data;
|
||||
useEffect(() => {
|
||||
if (all) onEntries?.(all);
|
||||
}, [all, onEntries]);
|
||||
// Filtering CLIENT-side: `GET /library?provider=` exists, but the page already holds the whole
|
||||
// list for the grid, and a second parameterised query would just be a second cache entry of the
|
||||
// same data going stale independently.
|
||||
|
||||
@@ -156,8 +156,10 @@ interface JsonSchemaDoc {
|
||||
* a form that only reads the top level silently loses every title and default on those fields.
|
||||
*/
|
||||
const flatten = (node: JsonSchemaNode): JsonSchemaNode =>
|
||||
// Object.assign rather than a spread: the accumulator starts as a fresh copy of `node`, so
|
||||
// mutating it is contained here, and it stays O(n) instead of rebuilding the object per branch.
|
||||
(node.allOf ?? []).reduce<JsonSchemaNode>(
|
||||
(acc, branch) => ({ ...acc, ...branch }),
|
||||
(acc, branch) => Object.assign(acc, branch),
|
||||
{ ...node },
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Section from "@unom/ui/section";
|
||||
import { Plus } from "lucide-react";
|
||||
import { type FC, useState } from "react";
|
||||
import type { GameEntry } from "@/api/gen/model/gameEntry";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useLocale } from "@/lib/i18n";
|
||||
import { m } from "@/paraglide/messages";
|
||||
@@ -17,9 +16,7 @@ export const SectionLibrary: FC = () => {
|
||||
// null = form hidden; "new" = adding; a GameEntry = editing that custom entry. Keying the form
|
||||
// by the target re-seeds its fields when switching add → edit (or between entries).
|
||||
const [target, setTarget] = useState<FormTarget | null>(null);
|
||||
// The full list, lifted from the grid so the providers card can count owners without a second
|
||||
// copy of the same query, plus which provider (if any) the grid is filtered to.
|
||||
const [entries, setEntries] = useState<GameEntry[]>([]);
|
||||
// Which provider (if any) the grid is filtered to.
|
||||
const [providerFilter, setProviderFilter] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
@@ -51,7 +48,6 @@ export const SectionLibrary: FC = () => {
|
||||
<LibraryGridSection
|
||||
onEdit={(entry) => setTarget(entry)}
|
||||
providerFilter={providerFilter}
|
||||
onEntries={setEntries}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
@@ -153,6 +153,7 @@ export const LogsCard: FC<{
|
||||
// offers to remove it. Do NOT take that fix — it is a TRIGGER, the signal that new lines arrived.
|
||||
// Removing it reinstates the bug this replaced: the effect stops re-running and follow-mode
|
||||
// quietly stops following. The same warning was here before, on `visible.length`.
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: newestVisible is a deliberate trigger, not a read — see the note above; removing it stops follow-mode from following.
|
||||
useEffect(() => {
|
||||
if (!follow) return;
|
||||
const el = listRef.current;
|
||||
|
||||
@@ -15,8 +15,8 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { m } from "@/paraglide/messages";
|
||||
import {
|
||||
type AccessDraft,
|
||||
AccessControls,
|
||||
type AccessDraft,
|
||||
draftExpirySecs,
|
||||
draftFromStored,
|
||||
GRANT_ALL,
|
||||
@@ -124,7 +124,9 @@ export const ApproveDialog: FC<{
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="approve-name">{m.pairing_pending_name_prompt()}</Label>
|
||||
<Label htmlFor="approve-name">
|
||||
{m.pairing_pending_name_prompt()}
|
||||
</Label>
|
||||
<Input
|
||||
id="approve-name"
|
||||
autoFocus
|
||||
@@ -140,7 +142,11 @@ export const ApproveDialog: FC<{
|
||||
</p>
|
||||
)}
|
||||
|
||||
<AccessControls value={draft} onChange={setDraft} idPrefix="approve" />
|
||||
<AccessControls
|
||||
value={draft}
|
||||
onChange={setDraft}
|
||||
idPrefix="approve"
|
||||
/>
|
||||
|
||||
{/* Approving pairs the device outright — no PIN — so it re-confirms the console
|
||||
password, which the BFF verifies and strips (util/confirm.ts). Both approve
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { m } from "@/paraglide/messages";
|
||||
import {
|
||||
type AccessDraft,
|
||||
AccessControls,
|
||||
type AccessDraft,
|
||||
draftExpirySecs,
|
||||
GRANT_ALL,
|
||||
} from "./access";
|
||||
@@ -45,7 +45,15 @@ export const EditAccessSheet: FC<{
|
||||
/** Hands off to the existing unpair confirmation. */
|
||||
onRemove: (fingerprint: string) => void;
|
||||
isPending: boolean;
|
||||
}> = ({ target, nowUnix, onCancel, onSave, onExpireNow, onRemove, isPending }) => {
|
||||
}> = ({
|
||||
target,
|
||||
nowUnix,
|
||||
onCancel,
|
||||
onSave,
|
||||
onExpireNow,
|
||||
onRemove,
|
||||
isPending,
|
||||
}) => {
|
||||
const [draft, setDraft] = useState<AccessDraft>({
|
||||
grants: GRANT_ALL,
|
||||
expiry: "keep",
|
||||
@@ -63,8 +71,7 @@ export const EditAccessSheet: FC<{
|
||||
});
|
||||
}, [target]);
|
||||
|
||||
const expired =
|
||||
target?.expiresUnix != null && target.expiresUnix <= nowUnix;
|
||||
const expired = target?.expiresUnix != null && target.expiresUnix <= nowUnix;
|
||||
|
||||
const save = () => {
|
||||
if (!target) return;
|
||||
@@ -93,7 +100,9 @@ export const EditAccessSheet: FC<{
|
||||
<DialogHeader>
|
||||
<DialogTitle>{m.access_edit_title()}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{m.access_edit_desc({ name: target.name || target.fingerprint.slice(0, 16) })}
|
||||
{m.access_edit_desc({
|
||||
name: target.name || target.fingerprint.slice(0, 16),
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ import { Label } from "@/components/ui/label";
|
||||
import type { Loadable } from "@/lib/query";
|
||||
import { m } from "@/paraglide/messages";
|
||||
import {
|
||||
type AccessDraft,
|
||||
AccessControls,
|
||||
type AccessDraft,
|
||||
draftExpirySecs,
|
||||
GRANT_ALL,
|
||||
} from "./access";
|
||||
|
||||
@@ -421,8 +421,7 @@ export const PairedDevices: FC<{
|
||||
size="icon"
|
||||
aria-label={m.action_unpair()}
|
||||
disabled={
|
||||
isUnpairingAll ||
|
||||
pendingFingerprint === r.fingerprint
|
||||
isUnpairingAll || pendingFingerprint === r.fingerprint
|
||||
}
|
||||
onClick={() => onUnpair(r.protocol, r.fingerprint)}
|
||||
>
|
||||
|
||||
@@ -131,10 +131,7 @@ export const draftFromStored = (
|
||||
export const useNowUnix = (stepMs = 30_000): number => {
|
||||
const [now, setNow] = useState(() => Math.floor(Date.now() / 1000));
|
||||
useEffect(() => {
|
||||
const t = setInterval(
|
||||
() => setNow(Math.floor(Date.now() / 1000)),
|
||||
stepMs,
|
||||
);
|
||||
const t = setInterval(() => setNow(Math.floor(Date.now() / 1000)), stepMs);
|
||||
return () => clearInterval(t);
|
||||
}, [stepMs]);
|
||||
return now;
|
||||
@@ -142,7 +139,8 @@ export const useNowUnix = (stepMs = 30_000): number => {
|
||||
|
||||
/** Remaining seconds → a short "left" label ("2 h left"); the caller has ruled out ≤ 0. */
|
||||
export const fmtRemaining = (secs: number): string => {
|
||||
if (secs >= 48 * 3600) return m.access_left_days({ d: Math.round(secs / 86400) });
|
||||
if (secs >= 48 * 3600)
|
||||
return m.access_left_days({ d: Math.round(secs / 86400) });
|
||||
if (secs >= 3600) return m.access_left_hours({ h: Math.round(secs / 3600) });
|
||||
if (secs >= 60) return m.access_left_minutes({ min: Math.ceil(secs / 60) });
|
||||
return m.access_left_under_minute();
|
||||
@@ -270,7 +268,9 @@ export const AccessControls: FC<{
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`${idPrefix}-expires`}>{m.access_expires_label()}</Label>
|
||||
<Label htmlFor={`${idPrefix}-expires`}>
|
||||
{m.access_expires_label()}
|
||||
</Label>
|
||||
<Select
|
||||
value={value.expiry}
|
||||
onValueChange={(expiry) =>
|
||||
@@ -284,7 +284,9 @@ export const AccessControls: FC<{
|
||||
{allowKeepExpiry && (
|
||||
<SelectItem value="keep">{m.access_expires_keep()}</SelectItem>
|
||||
)}
|
||||
<SelectItem value="forever">{m.access_expires_forever()}</SelectItem>
|
||||
<SelectItem value="forever">
|
||||
{m.access_expires_forever()}
|
||||
</SelectItem>
|
||||
<SelectItem value="1h">{m.access_expires_1h()}</SelectItem>
|
||||
<SelectItem value="4h">{m.access_expires_4h()}</SelectItem>
|
||||
<SelectItem value="8h">{m.access_expires_8h()}</SelectItem>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { MoonlightPairing } from "@/sections/Pairing/MoonlightPairingCard";
|
||||
import { NativePairingCard } from "@/sections/Pairing/NativePairingCard";
|
||||
import { PairedDevices, type PairedRow } from "@/sections/Pairing/PairedDevices";
|
||||
import {
|
||||
PairedDevices,
|
||||
type PairedRow,
|
||||
} from "@/sections/Pairing/PairedDevices";
|
||||
import { PendingDevices } from "@/sections/Pairing/PendingDevices";
|
||||
import { PairingView } from "@/sections/Pairing/view";
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { ApproveDialog } from "@/sections/Pairing/ApproveDialog";
|
||||
import { EditAccessSheet } from "@/sections/Pairing/EditAccessSheet";
|
||||
import { PairedDevices, type PairedRow } from "@/sections/Pairing/PairedDevices";
|
||||
import {
|
||||
PairedDevices,
|
||||
type PairedRow,
|
||||
} from "@/sections/Pairing/PairedDevices";
|
||||
import {
|
||||
accessNowUnix,
|
||||
nativeClients,
|
||||
|
||||
Reference in New Issue
Block a user