@pylonsync/client 0.3.267

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.
@@ -0,0 +1,106 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ export interface RouteSpec {
6
+ /** Path pattern. `/users/:id`, `/posts/:slug?`, `/*` for catch-all. */
7
+ path: string;
8
+ /** Component rendered when matched. */
9
+ component?: () => ReactNode;
10
+ /** Explicit JSX alternative when you don't want a factory. */
11
+ element?: ReactNode;
12
+ /**
13
+ * Require an authenticated session. `true` = signed in. `"admin"` =
14
+ * signed in AND isAdmin. Custom predicates: render your own gate
15
+ * inside the component instead.
16
+ */
17
+ requireAuth?: boolean | "admin";
18
+ /** Where to send unauthenticated visitors. Default: `/sign-in`. */
19
+ signInUrl?: string;
20
+ /** Nested routes — parent renders `<Outlet />` where these appear. */
21
+ children?: RouteSpec[];
22
+ }
23
+
24
+ export interface MatchedRoute {
25
+ route: RouteSpec;
26
+ params: Record<string, string>;
27
+ /** Tail of the path that wasn't consumed by this route — used to
28
+ * match children. Empty string when the parent consumed the whole
29
+ * path. */
30
+ tail: string;
31
+ }
32
+
33
+ /**
34
+ * Walk the routes tree depth-first, picking the first leaf that
35
+ * fully consumes the pathname. Returns the full match chain
36
+ * (root → ... → leaf) so layouts can render via `<Outlet />`.
37
+ */
38
+ export function matchRoutes(
39
+ routes: RouteSpec[],
40
+ pathname: string,
41
+ ): MatchedRoute[] | null {
42
+ const normalized = normalize(pathname);
43
+ for (const route of routes) {
44
+ const here = matchOne(route, normalized);
45
+ if (!here) continue;
46
+ if (route.children && route.children.length > 0) {
47
+ const childChain = matchRoutes(route.children, here.tail);
48
+ if (childChain) return [here, ...childChain];
49
+ // A parent with children must match a child too; otherwise
50
+ // we treat the parent as non-matching for this URL. This
51
+ // avoids rendering a layout with no page inside it.
52
+ continue;
53
+ }
54
+ // Leaf — require the tail to be empty so we don't half-match.
55
+ if (here.tail === "" || here.tail === "/") return [here];
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function matchOne(route: RouteSpec, pathname: string): MatchedRoute | null {
61
+ const pattern = normalize(route.path);
62
+ const isCatchAll = pattern.endsWith("/*");
63
+ const patternSegments = pattern
64
+ .replace(/\/\*$/, "")
65
+ .split("/")
66
+ .filter(Boolean);
67
+ const pathSegments = pathname.split("/").filter(Boolean);
68
+
69
+ const params: Record<string, string> = {};
70
+ let i = 0;
71
+ for (; i < patternSegments.length; i++) {
72
+ const p = patternSegments[i]!;
73
+ const seg = pathSegments[i];
74
+ if (p.startsWith(":")) {
75
+ const optional = p.endsWith("?");
76
+ const name = p.slice(1).replace(/\?$/, "");
77
+ if (seg === undefined) {
78
+ if (optional) continue;
79
+ return null;
80
+ }
81
+ params[name] = decodeURIComponent(seg);
82
+ } else {
83
+ if (seg === undefined || seg !== p) return null;
84
+ }
85
+ }
86
+
87
+ if (isCatchAll) {
88
+ const rest = pathSegments.slice(i).join("/");
89
+ params["*"] = rest;
90
+ return { route, params, tail: "" };
91
+ }
92
+
93
+ const tail = "/" + pathSegments.slice(i).join("/");
94
+ // When children are declared we leave the tail for them; otherwise
95
+ // the leaf check above requires tail === "".
96
+ return { route, params, tail: tail === "/" ? "" : tail };
97
+ }
98
+
99
+ function normalize(p: string): string {
100
+ if (!p) return "/";
101
+ const trimmed = p.replace(/\/+$/, "");
102
+ if (!trimmed.startsWith("/")) return `/${trimmed}`;
103
+ return trimmed === "" ? "/" : trimmed;
104
+ }
105
+
106
+ export { normalize };
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { useContext } from "react";
4
+ import { RouterContext, type RouterContextValue } from "./context";
5
+
6
+ /**
7
+ * Access the router from any descendant of `<Router />`. Returns a
8
+ * stable snapshot of the current URL plus `push` / `replace` / `back`
9
+ * helpers. Throws when used outside a `<Router />` — that's almost
10
+ * always a bug worth surfacing immediately.
11
+ *
12
+ * ```tsx
13
+ * const { pathname, params, push } = useRouter();
14
+ * useEffect(() => { if (!user) push("/sign-in"); }, [user]);
15
+ * ```
16
+ */
17
+ export function useRouter(): RouterContextValue {
18
+ const ctx = useContext(RouterContext);
19
+ if (!ctx) {
20
+ throw new Error(
21
+ "[@pylonsync/client] useRouter() must be called inside a <Router /> tree",
22
+ );
23
+ }
24
+ return ctx;
25
+ }
26
+
27
+ /** Shorthand for `useRouter().params`. */
28
+ export function useParams<T extends Record<string, string>>(): T {
29
+ return useRouter().params as T;
30
+ }
31
+
32
+ /** Shorthand for `useRouter().query`. */
33
+ export function useSearchParams(): Record<string, string> {
34
+ return useRouter().query;
35
+ }
36
+
37
+ /** Shorthand for `useRouter().pathname`. */
38
+ export function usePathname(): string {
39
+ return useRouter().pathname;
40
+ }
package/src/theme.css ADDED
@@ -0,0 +1,30 @@
1
+ /* @pylonsync/client — default theme tokens.
2
+ Override any of these CSS vars at the document root (or any ancestor
3
+ of the auth components) to restyle the entire suite. Tailwind classes
4
+ in the components reference these via var(--pylon-*) fallbacks so the
5
+ package works zero-config without Tailwind too. */
6
+ :root {
7
+ --pylon-ink: #0a0a0a;
8
+ --pylon-ink-2: #52525b;
9
+ --pylon-ink-3: #71717a;
10
+ --pylon-paper: #ffffff;
11
+ --pylon-paper-2: #f4f4f5;
12
+ --pylon-rule: #e5e7eb;
13
+ --pylon-error-ink: #b91c1c;
14
+ --pylon-error-bg: #fef2f2;
15
+ --pylon-error-rule: #fecaca;
16
+ }
17
+
18
+ @media (prefers-color-scheme: dark) {
19
+ :root {
20
+ --pylon-ink: #fafafa;
21
+ --pylon-ink-2: #a1a1aa;
22
+ --pylon-ink-3: #71717a;
23
+ --pylon-paper: #0a0a0a;
24
+ --pylon-paper-2: #18181b;
25
+ --pylon-rule: #27272a;
26
+ --pylon-error-ink: #fca5a5;
27
+ --pylon-error-bg: #450a0a;
28
+ --pylon-error-rule: #7f1d1d;
29
+ }
30
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }