@regardio/react 1.2.0 → 1.3.0

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.
@@ -1,127 +0,0 @@
1
- import { t as lowerCaseSzett } from "../text-BFTwzxPw.mjs";
2
- import { createContext, useCallback, useContext } from "react";
3
- import { tv } from "@regardio/tailwind/utils";
4
- import { Fragment as Fragment$1, jsx } from "react/jsx-runtime";
5
- import { NavLink } from "react-router";
6
- //#region src/link/link.tsx
7
- const PathResolverContext = createContext(null);
8
- const PathResolverProvider = PathResolverContext.Provider;
9
- function usePathResolver() {
10
- return useContext(PathResolverContext);
11
- }
12
- const LinkBase = ({ className, to, routeKey, children, onClick, viewTransition = true, ...props }) => {
13
- const pathResolver = usePathResolver();
14
- let path;
15
- if (routeKey && pathResolver) path = pathResolver(routeKey);
16
- else if (typeof to === "string") path = to;
17
- else {
18
- path = to?.pathname ?? "";
19
- if (to?.search) path += to.search;
20
- if (to?.hash) path += to.hash;
21
- }
22
- const isExternal = path.startsWith("tel:") || path.startsWith("mailto:") || path.startsWith("#") || path.startsWith("http");
23
- const handleClick = useCallback((event) => {
24
- onClick?.(event);
25
- if (event.defaultPrevented) return;
26
- if (path.startsWith("tel:") || path.startsWith("mailto:")) return;
27
- if (path.startsWith("#")) {
28
- event.preventDefault();
29
- const element = document.getElementById(path.substring(1));
30
- if (element) element.scrollIntoView({ behavior: "smooth" });
31
- return;
32
- }
33
- if (path.startsWith("http")) {
34
- event.preventDefault();
35
- window.open(path, "_blank", "noopener,noreferrer");
36
- return;
37
- }
38
- }, [onClick, path]);
39
- if (!path) return /* @__PURE__ */ jsx(Fragment$1, { children: typeof children === "function" ? null : children });
40
- if (isExternal) {
41
- const externalState = {
42
- isActive: false,
43
- isPending: false,
44
- isTransitioning: false
45
- };
46
- const resolvedClassName = typeof className === "function" ? className(externalState) : className;
47
- const resolvedStyle = typeof props.style === "function" ? props.style(externalState) : props.style;
48
- return /* @__PURE__ */ jsx("a", {
49
- className: resolvedClassName,
50
- href: path,
51
- onClick: handleClick,
52
- style: resolvedStyle,
53
- children: typeof children === "function" ? children(externalState) : children
54
- });
55
- }
56
- return /* @__PURE__ */ jsx(NavLink, {
57
- ...props,
58
- className,
59
- onClick: handleClick,
60
- to: path,
61
- viewTransition,
62
- children
63
- });
64
- };
65
- const link = tv({
66
- base: [],
67
- defaultVariants: { variant: "primary" },
68
- variants: {
69
- arrow: {
70
- darr: "darr",
71
- larr: "larr",
72
- rarr: "rarr",
73
- uarr: "uarr"
74
- },
75
- variant: {
76
- button: [
77
- "block",
78
- "button",
79
- "mt-s",
80
- "relative",
81
- "rarr",
82
- "text-right",
83
- "text-sm",
84
- "tracking-wider",
85
- "uppercase"
86
- ],
87
- code: ["font-monospace"],
88
- link: [
89
- "rarr",
90
- "!bg-transparent",
91
- "uppercase",
92
- "!tracking-wider"
93
- ],
94
- navtitle: [
95
- "block",
96
- "uppercase",
97
- "tracking-wider"
98
- ],
99
- primary: [],
100
- subtitle: ["text-lg"]
101
- }
102
- }
103
- });
104
- const Link = ({ arrow, children, className, routeKey, to, variant, viewTransition, ...props }) => {
105
- return /* @__PURE__ */ jsx(LinkBase, {
106
- ...props,
107
- className: link({
108
- arrow,
109
- className: typeof className === "string" ? className : void 0,
110
- variant
111
- }),
112
- routeKey,
113
- to,
114
- viewTransition,
115
- children: lowerCaseSzett(children)
116
- });
117
- };
118
- const MarkdownLink = ({ children, href, ...props }) => {
119
- if (href) return /* @__PURE__ */ jsx(Link, {
120
- to: href,
121
- ...props,
122
- children
123
- });
124
- return null;
125
- };
126
- //#endregion
127
- export { Link, LinkBase, MarkdownLink, PathResolverProvider, usePathResolver };
@@ -1,45 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { GenericError, getErrorDescriptor } from './generic-error';
3
-
4
- const meta: Meta<typeof GenericError> = {
5
- component: GenericError,
6
- parameters: {
7
- layout: 'fullscreen',
8
- },
9
- tags: ['autodocs'],
10
- title: 'Components/GenericError',
11
- };
12
-
13
- export default meta;
14
- type Story = StoryObj<typeof GenericError>;
15
-
16
- const ErrorWrapper = ({ status }: { status: number }) => {
17
- const error = new Response('Not Found', { status, statusText: 'Not Found' });
18
-
19
- return <GenericError error={error} />;
20
- };
21
-
22
- export const Error404: Story = {
23
- render: () => <ErrorWrapper status={404} />,
24
- };
25
-
26
- export const Error500: Story = {
27
- render: () => <ErrorWrapper status={500} />,
28
- };
29
-
30
- export const GetErrorDescriptorDemo: Story = {
31
- render: () => {
32
- const httpError = getErrorDescriptor(new Response('Not Found', { status: 404 }));
33
- const runtimeError = getErrorDescriptor(new Error('Something went wrong'));
34
- const unknownError = getErrorDescriptor('unknown');
35
-
36
- return (
37
- <div style={{ padding: '24px' }}>
38
- <h3>Error Descriptor Examples</h3>
39
- <pre style={{ background: '#f5f5f5', padding: '16px' }}>
40
- {JSON.stringify({ httpError, runtimeError, unknownError }, null, 2)}
41
- </pre>
42
- </div>
43
- );
44
- },
45
- };
@@ -1,105 +0,0 @@
1
- import { isRouteErrorResponse } from 'react-router';
2
-
3
- /**
4
- * Descriptor returned from getErrorDescriptor to help apps localize messages.
5
- */
6
- export type ErrorDescriptor =
7
- | {
8
- type: 'http';
9
- status: number;
10
- statusText: string;
11
- defaultId: string; // e.g. "errors.http"
12
- defaultMessage: string; // e.g. "Error {status}"
13
- }
14
- | {
15
- type: 'error';
16
- defaultId: string; // e.g. "errors.runtime"
17
- defaultMessage: string; // e.g. "An unexpected error occurred."
18
- message?: string;
19
- stack?: string;
20
- }
21
- | {
22
- type: 'unknown';
23
- defaultId: string; // e.g. "errors.unknown"
24
- defaultMessage: string; // e.g. "An unexpected error occurred."
25
- };
26
-
27
- /**
28
- * Compute a normalized error descriptor from a React Router error.
29
- * Apps can use this to map to i18n keys.
30
- */
31
- export function getErrorDescriptor(error: unknown): ErrorDescriptor {
32
- if (isRouteErrorResponse(error)) {
33
- const status = error.status;
34
- const statusText = error.statusText || 'Error';
35
- return {
36
- defaultId: status === 404 ? 'errors.404' : 'errors.http',
37
- defaultMessage: status === 404 ? 'Not found' : `Error ${status}`,
38
- status,
39
- statusText,
40
- type: 'http',
41
- };
42
- }
43
- if (error instanceof Error) {
44
- return {
45
- defaultId: 'errors.runtime',
46
- defaultMessage: 'An unexpected error occurred.',
47
- message: error.message,
48
- type: 'error',
49
- ...(error.stack ? { stack: error.stack } : {}),
50
- };
51
- }
52
- return {
53
- defaultId: 'errors.unknown',
54
- defaultMessage: 'An unexpected error occurred.',
55
- type: 'unknown',
56
- };
57
- }
58
-
59
- /**
60
- * GenericError
61
- *
62
- * A reusable error boundary component for React Router apps.
63
- * - Displays status-based messages for route responses
64
- * - Shows stack traces in development for non-response errors
65
- * - SSR-safe: accepts optional error prop for server-side rendering
66
- *
67
- * For localization, apps may either:
68
- * - Wrap this component and use `getErrorDescriptor(error)` to map to i18n keys
69
- * - Or provide a custom `renderMessage` to override the displayed details
70
- */
71
- export function GenericError({
72
- error,
73
- renderMessage,
74
- }: {
75
- error: unknown;
76
- renderMessage?: (descriptor: ErrorDescriptor) => React.JSX.Element;
77
- }): React.JSX.Element {
78
- const descriptor = getErrorDescriptor(error);
79
-
80
- const title = descriptor.type === 'http' ? `Error ${descriptor.status}` : 'Error';
81
-
82
- return (
83
- <main className="pt-2xl p-sm container mx-auto">
84
- <h1>{title}</h1>
85
- {renderMessage ? (
86
- renderMessage(descriptor)
87
- ) : (
88
- <>
89
- <p>
90
- {descriptor.type === 'http'
91
- ? descriptor.defaultMessage
92
- : descriptor.type === 'error'
93
- ? descriptor.message || descriptor.defaultMessage
94
- : descriptor.defaultMessage}
95
- </p>
96
- {import.meta.env.DEV && descriptor.type === 'error' && descriptor.stack && (
97
- <pre className="w-full p-sm overflow-x-auto">
98
- <code>{descriptor.stack}</code>
99
- </pre>
100
- )}
101
- </>
102
- )}
103
- </main>
104
- );
105
- }
@@ -1,2 +0,0 @@
1
- export type { ErrorDescriptor } from './generic-error';
2
- export { GenericError, getErrorDescriptor } from './generic-error';
@@ -1,20 +0,0 @@
1
- 'use client';
2
-
3
- import { useMemo } from 'react';
4
- import { useLocation, useMatches } from 'react-router';
5
-
6
- /**
7
- * This base hook is used to access data related to the current route
8
- * @returns {JSON|undefined} The router data or undefined if not found
9
- */
10
- export function useCurrentRouteData<HeaderData>(): HeaderData | undefined {
11
- const location = useLocation();
12
- const matchingRoutes = useMatches();
13
- const route = useMemo(() => {
14
- return matchingRoutes.find((route) => {
15
- return route.pathname === location.pathname;
16
- });
17
- }, [matchingRoutes, location]);
18
-
19
- return (route?.loaderData as HeaderData) || undefined;
20
- }
@@ -1,21 +0,0 @@
1
- 'use client';
2
-
3
- import { useMemo } from 'react';
4
- import { useMatches } from 'react-router';
5
-
6
- /**
7
- * This base hook is used in other hooks to quickly search for specific data
8
- * across all loader data using useMatches.
9
- * @param {string} id The route id
10
- * @returns {JSON|undefined} The router data or undefined if not found
11
- */
12
- export function useMatchesData<T>(id: string): T | undefined {
13
- const matchingRoutes = useMatches();
14
- const route = useMemo(() => {
15
- return matchingRoutes.find((route) => {
16
- return route.id === id;
17
- });
18
- }, [matchingRoutes, id]);
19
-
20
- return (route?.data as T) || undefined;
21
- }
@@ -1,73 +0,0 @@
1
- import type { User } from '@supabase/supabase-js';
2
- import { createContext, type ReactNode, useContext } from 'react';
3
-
4
- /**
5
- * Context for storing and accessing the current authenticated user
6
- */
7
- export interface UserContextType {
8
- /**
9
- * Whether the user data is currently loading
10
- */
11
- isLoading: boolean;
12
- /**
13
- * The current authenticated user, or null if not authenticated
14
- */
15
- user: User | null;
16
- }
17
-
18
- /**
19
- * Default context value when no provider is present
20
- */
21
- const defaultContextValue: UserContextType = {
22
- isLoading: false,
23
- user: null,
24
- };
25
-
26
- /**
27
- * Context for storing and accessing the current authenticated user
28
- */
29
- export const UserContext: React.Context<UserContextType> =
30
- createContext<UserContextType>(defaultContextValue);
31
-
32
- /**
33
- * Props for the UserContextProvider component
34
- */
35
- export interface UserContextProviderProps {
36
- /**
37
- * Child components that will have access to the user context
38
- */
39
- children: ReactNode;
40
- /**
41
- * Whether the user data is currently loading
42
- */
43
- isLoading?: boolean;
44
- /**
45
- * The current authenticated user, or null if not authenticated
46
- */
47
- user: User | null;
48
- }
49
-
50
- /**
51
- * Provider component for the UserContext
52
- */
53
- export function UserContextProvider({
54
- user,
55
- isLoading = false,
56
- children,
57
- }: UserContextProviderProps): React.JSX.Element {
58
- return <UserContext.Provider value={{ isLoading, user }}>{children}</UserContext.Provider>;
59
- }
60
-
61
- /**
62
- * Hook to access the current authenticated user from the UserContext
63
- * @returns The current user context containing the user object and loading state
64
- */
65
- export function useUser(): UserContextType {
66
- const context = useContext(UserContext);
67
-
68
- if (context === undefined) {
69
- throw new Error('useUser must be used within a UserContextProvider');
70
- }
71
-
72
- return context;
73
- }
package/src/link/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export type { LinkBaseProps, LinkProps, MarkdownLinkProps, PathResolver } from './link';
2
- export { Link, LinkBase, MarkdownLink, PathResolverProvider, usePathResolver } from './link';
@@ -1,109 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { MemoryRouter } from 'react-router';
3
- import { Link } from './link';
4
-
5
- const meta: Meta<typeof Link> = {
6
- component: Link,
7
- decorators: [
8
- (Story) => (
9
- <MemoryRouter>
10
- <Story />
11
- </MemoryRouter>
12
- ),
13
- ],
14
- parameters: {
15
- layout: 'padded',
16
- },
17
- tags: ['autodocs'],
18
- title: 'Components/Link',
19
- };
20
-
21
- export default meta;
22
- type Story = StoryObj<typeof Link>;
23
-
24
- export const Default: Story = {
25
- args: {
26
- children: 'Default Link',
27
- to: '/example',
28
- },
29
- };
30
-
31
- export const External: Story = {
32
- args: {
33
- children: 'External Link',
34
- to: 'https://example.com',
35
- },
36
- };
37
-
38
- export const WithGermanText: Story = {
39
- args: {
40
- children: 'Größenübersicht',
41
- to: '/sizes',
42
- },
43
- };
44
-
45
- export const AllVariants: Story = {
46
- render: () => (
47
- <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
48
- <Link to="/internal">Internal Link</Link>
49
- <Link to="https://example.com">External Link</Link>
50
- <Link
51
- className="text-blue-600 underline"
52
- to="/styled"
53
- >
54
- Styled Link
55
- </Link>
56
- </div>
57
- ),
58
- };
59
-
60
- export const TelephoneLink: Story = {
61
- args: {
62
- children: 'Call Us',
63
- to: 'tel:+1234567890',
64
- },
65
- };
66
-
67
- export const MailtoLink: Story = {
68
- args: {
69
- children: 'Email Us',
70
- to: 'mailto:hello@example.com',
71
- },
72
- };
73
-
74
- export const HashLink: Story = {
75
- args: {
76
- children: 'Jump to Section',
77
- to: '#section-id',
78
- },
79
- };
80
-
81
- export const WithSearchAndHash: Story = {
82
- args: {
83
- children: 'Link with Query',
84
- to: { hash: '#results', pathname: '/search', search: '?q=test' },
85
- },
86
- };
87
-
88
- export const EmptyPath: Story = {
89
- args: {
90
- children: 'No destination',
91
- to: '',
92
- },
93
- };
94
-
95
- export const WithArrow: Story = {
96
- args: {
97
- arrow: 'rarr',
98
- children: 'Link with Arrow',
99
- to: '/arrow',
100
- },
101
- };
102
-
103
- export const ButtonVariant: Story = {
104
- args: {
105
- children: 'Button Style Link',
106
- to: '/button',
107
- variant: 'button',
108
- },
109
- };
@@ -1,169 +0,0 @@
1
- import { cleanup, fireEvent, render, screen } from '@testing-library/react';
2
- import { MemoryRouter } from 'react-router';
3
- import { afterEach, describe, expect, it, vi } from 'vitest';
4
-
5
- import { Link, LinkBase, MarkdownLink, PathResolverProvider } from './link';
6
-
7
- afterEach(() => {
8
- cleanup();
9
- });
10
-
11
- const renderWithRouter = (ui: React.ReactNode) => {
12
- return render(<MemoryRouter>{ui}</MemoryRouter>);
13
- };
14
-
15
- describe('LinkBase', () => {
16
- it('renders internal link with NavLink', () => {
17
- renderWithRouter(<LinkBase to="/about">About</LinkBase>);
18
- expect(screen.getByText('About')).toBeDefined();
19
- });
20
-
21
- it('renders external http link as anchor', () => {
22
- renderWithRouter(<LinkBase to="https://example.com">External</LinkBase>);
23
- const link = screen.getByText('External');
24
- expect(link.tagName).toBe('A');
25
- expect(link.getAttribute('href')).toBe('https://example.com');
26
- });
27
-
28
- it('renders mailto link as anchor', () => {
29
- renderWithRouter(<LinkBase to="mailto:test@example.com">Email</LinkBase>);
30
- const link = screen.getByText('Email');
31
- expect(link.tagName).toBe('A');
32
- expect(link.getAttribute('href')).toBe('mailto:test@example.com');
33
- });
34
-
35
- it('renders tel link as anchor', () => {
36
- renderWithRouter(<LinkBase to="tel:+1234567890">Call</LinkBase>);
37
- const link = screen.getByText('Call');
38
- expect(link.tagName).toBe('A');
39
- expect(link.getAttribute('href')).toBe('tel:+1234567890');
40
- });
41
-
42
- it('uses pathResolver when routeKey is provided', () => {
43
- const resolver = vi.fn().mockReturnValue('/resolved-path');
44
- renderWithRouter(
45
- <PathResolverProvider value={resolver}>
46
- <LinkBase routeKey="home">Home</LinkBase>
47
- </PathResolverProvider>,
48
- );
49
- expect(resolver).toHaveBeenCalledWith('home');
50
- expect(screen.getByText('Home')).toBeDefined();
51
- });
52
-
53
- it('handles object to prop with pathname, search, and hash', () => {
54
- renderWithRouter(
55
- <LinkBase to={{ hash: '#section', pathname: '/page', search: '?q=test' }}>Complex</LinkBase>,
56
- );
57
- expect(screen.getByText('Complex')).toBeDefined();
58
- });
59
-
60
- it('renders children only when path is empty', () => {
61
- renderWithRouter(<LinkBase>No Link</LinkBase>);
62
- expect(screen.getByText('No Link')).toBeDefined();
63
- });
64
-
65
- it('calls onClick handler', () => {
66
- const handleClick = vi.fn();
67
- renderWithRouter(
68
- <LinkBase
69
- onClick={handleClick}
70
- to="/test"
71
- >
72
- Click Me
73
- </LinkBase>,
74
- );
75
- fireEvent.click(screen.getByText('Click Me'));
76
- expect(handleClick).toHaveBeenCalled();
77
- });
78
-
79
- it('opens external http link in new window', () => {
80
- const windowOpen = vi.spyOn(window, 'open').mockImplementation(() => null);
81
- renderWithRouter(<LinkBase to="https://example.com">External</LinkBase>);
82
- fireEvent.click(screen.getByText('External'));
83
- expect(windowOpen).toHaveBeenCalledWith('https://example.com', '_blank', 'noopener,noreferrer');
84
- windowOpen.mockRestore();
85
- });
86
-
87
- it('scrolls to element for hash links', () => {
88
- const scrollIntoView = vi.fn();
89
- const element = document.createElement('div');
90
- element.id = 'section';
91
- element.scrollIntoView = scrollIntoView;
92
- document.body.appendChild(element);
93
-
94
- renderWithRouter(<LinkBase to="#section">Jump</LinkBase>);
95
- fireEvent.click(screen.getByText('Jump'));
96
- expect(scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth' });
97
-
98
- document.body.removeChild(element);
99
- });
100
-
101
- it('handles hash link when element not found', () => {
102
- renderWithRouter(<LinkBase to="#nonexistent">Jump</LinkBase>);
103
- fireEvent.click(screen.getByText('Jump'));
104
- });
105
-
106
- it('does not prevent default for tel links', () => {
107
- renderWithRouter(<LinkBase to="tel:+1234567890">Call</LinkBase>);
108
- const link = screen.getByText('Call');
109
- const event = fireEvent.click(link);
110
- expect(event).toBe(true);
111
- });
112
-
113
- it('respects defaultPrevented in onClick', () => {
114
- const handleClick = vi.fn((e: React.MouseEvent) => e.preventDefault());
115
- const windowOpen = vi.spyOn(window, 'open').mockImplementation(() => null);
116
-
117
- renderWithRouter(
118
- <LinkBase
119
- onClick={handleClick}
120
- to="https://example.com"
121
- >
122
- External
123
- </LinkBase>,
124
- );
125
- fireEvent.click(screen.getByText('External'));
126
- expect(handleClick).toHaveBeenCalled();
127
- expect(windowOpen).not.toHaveBeenCalled();
128
-
129
- windowOpen.mockRestore();
130
- });
131
- });
132
-
133
- describe('Link', () => {
134
- it('renders with variant', () => {
135
- renderWithRouter(
136
- <Link
137
- to="/test"
138
- variant="button"
139
- >
140
- Button Link
141
- </Link>,
142
- );
143
- expect(screen.getByText('Button Link')).toBeDefined();
144
- });
145
-
146
- it('renders with arrow', () => {
147
- renderWithRouter(
148
- <Link
149
- arrow="rarr"
150
- to="/test"
151
- >
152
- Arrow Link
153
- </Link>,
154
- );
155
- expect(screen.getByText('Arrow Link')).toBeDefined();
156
- });
157
- });
158
-
159
- describe('MarkdownLink', () => {
160
- it('renders Link when href is provided', () => {
161
- renderWithRouter(<MarkdownLink href="/page">Markdown</MarkdownLink>);
162
- expect(screen.getByText('Markdown')).toBeDefined();
163
- });
164
-
165
- it('returns null when href is not provided', () => {
166
- const { container } = renderWithRouter(<MarkdownLink>No Link</MarkdownLink>);
167
- expect(container.innerHTML).toBe('');
168
- });
169
- });