@remit/web-client 0.0.10 → 0.0.11
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
|
|
6
6
|
"exports": {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
configOperationsGetConfigQueryKey,
|
|
7
7
|
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
8
8
|
import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
|
|
9
|
-
import { Button, Input, Select, securityToApi } from "@remit/ui";
|
|
9
|
+
import { Button, Input, Select, SlidePanel, securityToApi } from "@remit/ui";
|
|
10
10
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
11
11
|
import { Check, Loader2, X } from "lucide-react";
|
|
12
12
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
type ProviderPreset,
|
|
20
20
|
} from "../../lib/provider-presets.js";
|
|
21
21
|
import { cn } from "../../lib/utils";
|
|
22
|
-
import { SlidePanel } from "../ui/SlidePanel";
|
|
23
22
|
import {
|
|
24
23
|
appendAppPasswordHint,
|
|
25
24
|
computeSmtpAutoFill,
|
package/src/index.css
CHANGED
|
@@ -4,9 +4,18 @@
|
|
|
4
4
|
the .dark ancestor scope used everywhere in this app. */
|
|
5
5
|
@import "@remit/ui/tokens.css";
|
|
6
6
|
|
|
7
|
-
/* Tailwind v4 content scanning:
|
|
8
|
-
|
|
7
|
+
/* Tailwind v4 content scanning: name every source Tailwind must inspect for
|
|
8
|
+
utility class usage. Automatic detection is rooted at the Vite root, which
|
|
9
|
+
for the distributor harness build is a throwaway directory holding only the
|
|
10
|
+
entry — so nothing here may rely on it. Both this package's own components
|
|
11
|
+
and the linked workspace UI package are declared explicitly.
|
|
12
|
+
|
|
13
|
+
Tests are excluded: they assert on class strings, and a scanned assertion
|
|
14
|
+
emits the utility it names into the shipped stylesheet. */
|
|
15
|
+
@source "./**/*.{ts,tsx}";
|
|
9
16
|
@source "../../ui/src/**/*.{ts,tsx}";
|
|
17
|
+
@source not "./**/*.test.{ts,tsx}";
|
|
18
|
+
@source not "../../ui/src/**/*.test.{ts,tsx}";
|
|
10
19
|
|
|
11
20
|
/* Hide scrollbars while maintaining scroll functionality */
|
|
12
21
|
.overflow-y-auto {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tailwind v4 roots automatic source detection at the bundler's root. The
|
|
9
|
+
* distributor harness builds from a throwaway root holding only the entry, so
|
|
10
|
+
* nothing this app renders is discovered automatically: every directory whose
|
|
11
|
+
* components carry utility classes has to be named by an `@source` rule. When
|
|
12
|
+
* one is missing the build still succeeds — it just ships a stylesheet with
|
|
13
|
+
* those utilities absent, and the screens using them lay out as unstyled boxes
|
|
14
|
+
* (#57: the settings slide-over covered the whole viewport).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const cssPath = resolve(here, "index.css");
|
|
19
|
+
const css = readFileSync(cssPath, "utf8");
|
|
20
|
+
|
|
21
|
+
/** Globs Tailwind scans, and globs it is told to skip. */
|
|
22
|
+
const globs = (kind: "include" | "exclude"): string[] =>
|
|
23
|
+
[...css.matchAll(/@source\s+(not\s+)?"([^"]+)"/g)]
|
|
24
|
+
.filter(([, negated]) => (kind === "exclude") === Boolean(negated))
|
|
25
|
+
.map(([, , glob]) => glob);
|
|
26
|
+
|
|
27
|
+
/** The directory an `@source` glob starts scanning from. */
|
|
28
|
+
const sourceRoots = (): string[] =>
|
|
29
|
+
globs("include").map((glob) => {
|
|
30
|
+
const literal = glob.split("*")[0] ?? glob;
|
|
31
|
+
return resolve(dirname(cssPath), literal);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** Every directory below `root` that holds a component file. */
|
|
35
|
+
const componentDirs = (root: string): string[] => {
|
|
36
|
+
const found: string[] = [];
|
|
37
|
+
const walk = (dir: string): void => {
|
|
38
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
39
|
+
if (entries.some((e) => e.isFile() && e.name.endsWith(".tsx"))) {
|
|
40
|
+
found.push(dir);
|
|
41
|
+
}
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
44
|
+
walk(resolve(dir, entry.name));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
walk(root);
|
|
49
|
+
return found;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const isCovered = (dir: string, roots: string[]): boolean =>
|
|
53
|
+
roots.some((root) => {
|
|
54
|
+
const rel = relative(root, dir);
|
|
55
|
+
return rel === "" || (!rel.startsWith("..") && !rel.startsWith(sep));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("tailwind source coverage (#57)", () => {
|
|
59
|
+
const roots = sourceRoots();
|
|
60
|
+
|
|
61
|
+
it("declares at least one source", () => {
|
|
62
|
+
assert.ok(roots.length > 0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("covers every directory in this package that renders components", () => {
|
|
66
|
+
const uncovered = componentDirs(here).filter((d) => !isCovered(d, roots));
|
|
67
|
+
assert.deepEqual(
|
|
68
|
+
uncovered.map((d) => relative(here, d)),
|
|
69
|
+
[],
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("covers the linked design system package", () => {
|
|
74
|
+
const ui = resolve(here, "../../ui/src");
|
|
75
|
+
const uncovered = componentDirs(ui).filter((d) => !isCovered(d, roots));
|
|
76
|
+
assert.deepEqual(
|
|
77
|
+
uncovered.map((d) => relative(ui, d)),
|
|
78
|
+
[],
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A test asserting on a class string is a class string in a scanned file, so
|
|
84
|
+
* without this every utility a test names is emitted into the shipped CSS.
|
|
85
|
+
*/
|
|
86
|
+
it("excludes test files from every source it scans", () => {
|
|
87
|
+
const uncovered = globs("include").filter(
|
|
88
|
+
(glob) =>
|
|
89
|
+
!globs("exclude").some(
|
|
90
|
+
(excluded) =>
|
|
91
|
+
excluded ===
|
|
92
|
+
glob.replace(/\/\*\*\/\*\.\{ts,tsx\}$/, "/**/*.test.{ts,tsx}"),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
assert.deepEqual(uncovered, []);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { X } from "lucide-react";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
3
|
-
import { cn } from "../../lib/utils";
|
|
4
|
-
|
|
5
|
-
interface SlidePanelProps {
|
|
6
|
-
isOpen: boolean;
|
|
7
|
-
onClose: () => void;
|
|
8
|
-
title: string;
|
|
9
|
-
children: ReactNode;
|
|
10
|
-
footer?: ReactNode;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const SlidePanel = ({
|
|
14
|
-
isOpen,
|
|
15
|
-
onClose,
|
|
16
|
-
title,
|
|
17
|
-
children,
|
|
18
|
-
footer,
|
|
19
|
-
}: SlidePanelProps) => (
|
|
20
|
-
<>
|
|
21
|
-
{/* Backdrop overlay */}
|
|
22
|
-
{/* biome-ignore lint/a11y/useSemanticElements: full-screen backdrop overlay; <button> default styles would break fixed inset positioning */}
|
|
23
|
-
<div
|
|
24
|
-
className={cn(
|
|
25
|
-
"fixed inset-0 bg-black/30 z-40 transition-opacity",
|
|
26
|
-
isOpen ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
27
|
-
)}
|
|
28
|
-
onClick={onClose}
|
|
29
|
-
onKeyDown={(e) => e.key === "Escape" && onClose()}
|
|
30
|
-
role="button"
|
|
31
|
-
tabIndex={0}
|
|
32
|
-
aria-label="Close panel"
|
|
33
|
-
/>
|
|
34
|
-
|
|
35
|
-
{/* Slide-in panel */}
|
|
36
|
-
<div
|
|
37
|
-
className={cn(
|
|
38
|
-
"fixed top-0 right-0 h-full w-full sm:w-[400px] sm:max-w-[90vw] bg-canvas border-l border-line shadow-xl z-50",
|
|
39
|
-
"transform transition-transform duration-200 ease-out",
|
|
40
|
-
isOpen ? "translate-x-0" : "translate-x-full",
|
|
41
|
-
)}
|
|
42
|
-
role="dialog"
|
|
43
|
-
aria-modal="true"
|
|
44
|
-
aria-labelledby="slide-panel-title"
|
|
45
|
-
>
|
|
46
|
-
{/* Panel header */}
|
|
47
|
-
<div className="flex items-center justify-between px-4 h-14 border-b border-line">
|
|
48
|
-
<h2 id="slide-panel-title" className="font-semibold">
|
|
49
|
-
{title}
|
|
50
|
-
</h2>
|
|
51
|
-
<button
|
|
52
|
-
type="button"
|
|
53
|
-
onClick={onClose}
|
|
54
|
-
className="p-1.5 rounded-md hover:bg-surface-raised transition-colors"
|
|
55
|
-
aria-label="Close"
|
|
56
|
-
>
|
|
57
|
-
<X className="size-5" />
|
|
58
|
-
</button>
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
{/* Panel content */}
|
|
62
|
-
<div className="h-[calc(100%-3.5rem)] flex flex-col">
|
|
63
|
-
<div className="flex-1 overflow-auto p-4">{children}</div>
|
|
64
|
-
{footer && (
|
|
65
|
-
<div className="flex justify-end gap-3 p-4 border-t border-line bg-canvas">
|
|
66
|
-
{footer}
|
|
67
|
-
</div>
|
|
68
|
-
)}
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
</>
|
|
72
|
-
);
|