@sjolystinnovation/app-kit 0.9.0 → 0.11.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.
package/README.md CHANGED
@@ -309,6 +309,45 @@ export const POST = createRevalidateRoute({
309
309
  });
310
310
  ```
311
311
 
312
+ ### `@sjolystinnovation/app-kit/next-config`
313
+
314
+ For `next.config.ts`. It imports nothing but Next's types, so Next loads it with no build step.
315
+
316
+ `defineAppConfig(options, extra?)` gives the config every app shares: the standalone build, app-kit
317
+ in `transpilePackages`, no `X-Powered-By`, `expireTime` of five minutes, image qualities 75 and 100,
318
+ the incremental cache in memory, and the security headers on every page. `extra` is merged over it,
319
+ with `transpilePackages`, `images`, `headers` and `rewrites` combined rather than replaced.
320
+
321
+ ```ts
322
+ // next.config.ts
323
+ import { defineAppConfig } from "@sjolystinnovation/app-kit/next-config";
324
+
325
+ export default defineAppConfig({
326
+ apiUrl: process.env.NEXT_PUBLIC_API_URL,
327
+ dev: process.env.NODE_ENV !== "production",
328
+ contentImages: true,
329
+ });
330
+ ```
331
+
332
+ | Option | Type | Description |
333
+ | --- | --- | --- |
334
+ | `apiUrl` | `string \| undefined` | The API URL. Its origin is allowed in `connect-src`. |
335
+ | `dev` | `boolean` | Development builds also allow `'unsafe-eval'`, which hot reload needs. |
336
+ | `websocket` | `boolean` | Also allow `ws:` and `wss:` to the API origin, for SignalR. |
337
+ | `imgFromApi` | `boolean` | Also allow images from the API origin. Not needed with `contentImages`. |
338
+ | `contentImages` | `boolean` | Proxy `/content-images/*` to the API's `/content-images/*`, so the browser never contacts the API host. |
339
+ | `connectSrc`, `imgSrc` | `string[]` | Extra sources for those directives. |
340
+ | `frameSrc`, `objectSrc` | `string[]` | Sources for those directives. Both default to `'none'`. |
341
+ | `pathHeaders` | `{ source, headers }[]` | Headers for particular paths, such as a file download. They come after the page headers and replace a page header of the same name. Every path keeps the rest. |
342
+ | `permissionsPolicy` | `string` | Replaces `camera=(), microphone=(), geolocation=()`. |
343
+ | `expireTime` | `number` | Seconds a cache may serve a stale page. Defaults to 300. Next's default is a year. |
344
+ | `isrFlushToDisk` | `boolean` | Write revalidated pages to disk. Off by default, for a read-only root filesystem. |
345
+
346
+ The pieces are exported too: `securityHeaders(options)`, `contentSecurityPolicy(options)`,
347
+ `contentImagesRewrite(apiUrl, { source?, upstreamPath? })`, `apiOrigin(apiUrl)` and
348
+ `apiBaseUrl(apiUrl)`. Scripts allow `'unsafe-inline'`, since a nonce would make every statically
349
+ rendered page dynamic.
350
+
312
351
  ### `@sjolystinnovation/app-kit/session`
313
352
 
314
353
  Decides when a user has to sign in again. Safe to import from server code, such as the next-auth
@@ -621,6 +660,70 @@ import { Alert } from "@sjolystinnovation/app-kit/ui";
621
660
  | `className` | `string` | | Classes for placing the alert, such as a margin. Restyle it through the theme variables. A clashing class wins or loses by the order Tailwind emits them in. |
622
661
  | `children` | `ReactNode` | | Content. |
623
662
 
663
+ ### `ContentImage`
664
+
665
+ From `@sjolystinnovation/app-kit/ui`. An uploaded image from the content API, as a plain `<img>`
666
+ with a `srcset` of the widths the API serves. It goes through the content images rewrite from
667
+ `defineAppConfig({ contentImages: true })`, so the browser never contacts the API host. It works in
668
+ a server component.
669
+
670
+ ```tsx
671
+ <ContentImage imageId={post.imageId} alt={post.title} sizes="(min-width: 768px) 50vw, 100vw" />
672
+ <ContentImage imageId={hero.imageId} fallbackSrc="/hero.jpg" alt="" sizes="100vw" priority aria-hidden />
673
+ ```
674
+
675
+ | Prop | Type | Default | Description |
676
+ | --- | --- | --- | --- |
677
+ | `imageId` | `string \| null` | | Uploaded image id. Null or empty shows `fallbackSrc` as it is, with no `srcset`. |
678
+ | `alt` | `string` | | Empty for a decorative image. |
679
+ | `sizes` | `string` | | Widths the image is shown at. Required, since a browser assumes `100vw` without it. |
680
+ | `fallbackSrc` | `string` | | A static path for when there is no uploaded image. With neither, nothing renders. |
681
+ | `priority` | `boolean` | `false` | For the one image visible without scrolling. It loads eagerly, at high priority. |
682
+ | `width`, `height` | `number` | | Intrinsic size, to reserve space where the layout fixes no ratio. |
683
+ | `base` | `string` | `"/content-images"` | Path the content images rewrite serves. |
684
+ | `widths` | `number[]` | `CONTENT_IMAGE_WIDTHS` | Widths in the `srcset`. |
685
+ | `className` | `string` | | Classes on the `<img>`. |
686
+
687
+ `contentImagePath(id, base?)` and `contentImageSrcSet(id, widths?, base?)` build the same URLs for
688
+ other uses, such as a CSS background or an Open Graph image.
689
+
690
+ ### `ErrorState`
691
+
692
+ From `@sjolystinnovation/app-kit/ui`. What an `error.tsx` shows. It is a client component, as an
693
+ error boundary has to be.
694
+
695
+ ```tsx
696
+ "use client";
697
+
698
+ export default function Error({ reset }: { error: Error; reset: () => void }) {
699
+ return <ErrorState reset={reset} homeHref="/" />;
700
+ }
701
+ ```
702
+
703
+ | Prop | Type | Description |
704
+ | --- | --- | --- |
705
+ | `reset` | `() => void` | The `reset` Next passes to `error.tsx`. The try again button calls it. |
706
+ | `homeHref` | `string` | Where the front page link goes, such as `/` or `/en`. |
707
+ | `strings` | `TextOverrides<ErrorStateStrings>` | `title`, `body`, `tryAgain` and `home`. |
708
+ | `className` | `string` | Classes for placing it, on the outer element. |
709
+
710
+ The error itself is never shown.
711
+
712
+ ### `NotFoundState`
713
+
714
+ From `@sjolystinnovation/app-kit/ui`. What a `not-found.tsx` shows: a large code, the message as
715
+ the page's heading, and a link home. It works in a server component.
716
+
717
+ ```tsx
718
+ <NotFoundState homeHref="/no" strings={{ body: dict.notFound, home: dict.toFrontPage }} />
719
+ ```
720
+
721
+ | Prop | Type | Description |
722
+ | --- | --- | --- |
723
+ | `homeHref` | `string` | Where the home link goes. |
724
+ | `strings` | `TextOverrides<NotFoundStrings>` | `code`, `body` and `home`. |
725
+ | `className` | `string` | Classes for placing it, on the outer element. |
726
+
624
727
  ### `PasswordInput`
625
728
 
626
729
  A labelled password field with a button that shows and hides the value. It also takes the other
@@ -728,6 +831,11 @@ selector such as `.dark` for a scoped theme:
728
831
  | `--color-dialog-bg` | white |
729
832
  | `--color-dialog-title` | gray 900 |
730
833
  | `--color-dialog-text` | gray 600 |
834
+ | `--color-state-heading` | gray 900, the heading of an `ErrorState` |
835
+ | `--color-state-text` | gray 600, the text of an `ErrorState` or `NotFoundState` |
836
+ | `--color-state-code` | gray 900, the code of a `NotFoundState` |
837
+ | `--font-state-code` | `inherit`, the font of that code |
838
+ | `--color-state-link`, `-hover` | gray 900, the home link |
731
839
 
732
840
  A default that points at another variable is read once for the whole page. The focus ring and the
733
841
  primary button point at `--color-primary` and `--color-primary-foreground`. A scoped theme that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjolystinnovation/app-kit",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Building blocks for Next.js apps that sign in against a JWT API through next-auth",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Sjølyst Innovation AS",
@@ -25,6 +25,7 @@
25
25
  "./ui": "./src/ui/index.ts",
26
26
  "./toast": "./src/toast/index.ts",
27
27
  "./validation": "./src/validation/index.ts",
28
+ "./next-config": "./src/next-config/index.ts",
28
29
  "./styles.css": "./src/styles.css"
29
30
  },
30
31
  "files": [
@@ -0,0 +1,154 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ /** A response header, as `next.config.ts` lists them. */
4
+ export interface Header {
5
+ key: string;
6
+ value: string;
7
+ }
8
+
9
+ /** The API's origin, such as `https://api.example.com`. Empty when the URL is missing or invalid. */
10
+ export function apiOrigin(apiUrl: string | undefined): string {
11
+ if (!apiUrl) return "";
12
+ try {
13
+ return new URL(apiUrl).origin;
14
+ } catch {
15
+ return "";
16
+ }
17
+ }
18
+
19
+ /** The API's base URL with its path, such as `https://api.example.com/api`, without a trailing slash. Empty when missing or invalid. */
20
+ export function apiBaseUrl(apiUrl: string | undefined): string {
21
+ if (!apiUrl) return "";
22
+ try {
23
+ return new URL(apiUrl).toString().replace(/\/$/, "");
24
+ } catch {
25
+ return "";
26
+ }
27
+ }
28
+
29
+ export interface CspOptions {
30
+ /** The app's API URL, usually `process.env.NEXT_PUBLIC_API_URL`. Its origin may be fetched from. */
31
+ apiUrl: string | undefined;
32
+ /** Development builds also allow `'unsafe-eval'`, which hot reload needs. */
33
+ dev: boolean;
34
+ /** Also allow `ws:` and `wss:` to the API, for a WebSocket such as SignalR. */
35
+ websocket?: boolean | undefined;
36
+ /** Also allow images from the API origin. Not needed when images go through the content images rewrite. */
37
+ imgFromApi?: boolean | undefined;
38
+ /** Extra sources for `connect-src`. */
39
+ connectSrc?: readonly string[] | undefined;
40
+ /** Extra sources for `img-src`. */
41
+ imgSrc?: readonly string[] | undefined;
42
+ /** Sources for `frame-src`. Defaults to `'none'`, since no page embeds a frame. */
43
+ frameSrc?: readonly string[] | undefined;
44
+ /** Sources for `object-src`. Defaults to `'none'`, since no page embeds an object. */
45
+ objectSrc?: readonly string[] | undefined;
46
+ }
47
+
48
+ const sources = (list: readonly (string | false | undefined)[]) => list.filter(Boolean).join(" ");
49
+
50
+ /**
51
+ * The Content-Security-Policy for an app's pages. Scripts allow `'unsafe-inline'`, because a nonce
52
+ * would make every statically rendered page dynamic.
53
+ */
54
+ export function contentSecurityPolicy(o: CspOptions): string {
55
+ const origin = apiOrigin(o.apiUrl);
56
+ const wsOrigin = o.websocket ? origin.replace(/^https:/, "wss:").replace(/^http:/, "ws:") : "";
57
+ return [
58
+ "default-src 'self'",
59
+ o.dev ? "script-src 'self' 'unsafe-eval' 'unsafe-inline'" : "script-src 'self' 'unsafe-inline'",
60
+ "style-src 'self' 'unsafe-inline'",
61
+ `img-src ${sources(["'self'", o.imgFromApi && origin, "data:", "blob:", ...(o.imgSrc ?? [])])}`,
62
+ `connect-src ${sources(["'self'", origin, wsOrigin, ...(o.connectSrc ?? [])])}`,
63
+ `frame-src ${sources(o.frameSrc ?? ["'none'"])}`,
64
+ `object-src ${sources(o.objectSrc ?? ["'none'"])}`,
65
+ "font-src 'self'",
66
+ "base-uri 'self'",
67
+ "form-action 'self'",
68
+ "frame-ancestors 'none'",
69
+ "upgrade-insecure-requests",
70
+ ].join("; ");
71
+ }
72
+
73
+ /** The security headers for an app's pages, including the Content-Security-Policy. */
74
+ export function securityHeaders(o: CspOptions & { permissionsPolicy?: string | undefined }): Header[] {
75
+ return [
76
+ { key: "X-Content-Type-Options", value: "nosniff" },
77
+ { key: "X-Frame-Options", value: "DENY" },
78
+ { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
79
+ { key: "X-DNS-Prefetch-Control", value: "on" },
80
+ { key: "Permissions-Policy", value: o.permissionsPolicy ?? "camera=(), microphone=(), geolocation=()" },
81
+ { key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" },
82
+ { key: "Content-Security-Policy", value: contentSecurityPolicy(o) },
83
+ ];
84
+ }
85
+
86
+ /**
87
+ * Content images, proxied so the browser never contacts the API host. A rewrite rather than a route
88
+ * handler, so the query string and the Accept header, which webp needs, pass through. No rewrite
89
+ * when the API URL is missing or invalid.
90
+ */
91
+ export function contentImagesRewrite(apiUrl: string | undefined, o: { source?: string | undefined; upstreamPath?: string | undefined } = {}) {
92
+ const base = apiBaseUrl(apiUrl);
93
+ if (!base) return [];
94
+ const source = o.source ?? "/content-images";
95
+ const upstream = o.upstreamPath ?? "/content-images";
96
+ return [{ source: `${source}/:path*`, destination: `${base}${upstream}/:path*` }];
97
+ }
98
+
99
+ export interface AppConfigOptions extends CspOptions {
100
+ /** Proxy `/content-images/*` to the API. See `contentImagesRewrite`. */
101
+ contentImages?: boolean | undefined;
102
+ /**
103
+ * Headers for particular paths. Every path gets the page headers, and these come after them, so
104
+ * they replace a page header of the same name.
105
+ */
106
+ pathHeaders?: readonly { source: string; headers: Header[] }[] | undefined;
107
+ /** Replaces the default Permissions-Policy. */
108
+ permissionsPolicy?: string | undefined;
109
+ /**
110
+ * Seconds a cache may go on serving a page after it goes stale. Next's default is a year, long
111
+ * enough for a browser to hand back an old page and fetch the current one behind it. Defaults to
112
+ * 300, matching the client router cache's stale time.
113
+ */
114
+ expireTime?: number | undefined;
115
+ /**
116
+ * Write revalidated pages to disk. Off by default: with a read-only root filesystem Next cannot
117
+ * write to `.next/server/app`, so the incremental cache stays in memory.
118
+ */
119
+ isrFlushToDisk?: boolean | undefined;
120
+ }
121
+
122
+ /**
123
+ * The shared Next config for an app: the standalone build, app-kit transpiled, no `X-Powered-By`,
124
+ * the security headers on every page, and optionally the content images rewrite. `extra` is merged
125
+ * over it, with `transpilePackages` and `images` combined.
126
+ */
127
+ export function defineAppConfig(o: AppConfigOptions, extra: NextConfig = {}): NextConfig {
128
+ const pageHeaders = securityHeaders(o);
129
+ const paths = o.pathHeaders ?? [];
130
+
131
+ const { transpilePackages, images, experimental, headers: extraHeaders, rewrites: extraRewrites, ...rest } = extra;
132
+ return {
133
+ output: "standalone",
134
+ poweredByHeader: false,
135
+ expireTime: o.expireTime ?? 300,
136
+ ...rest,
137
+ transpilePackages: [...new Set(["@sjolystinnovation/app-kit", ...(transpilePackages ?? [])])],
138
+ images: { qualities: [75, 100], ...images },
139
+ experimental: { isrFlushToDisk: o.isrFlushToDisk ?? false, ...experimental },
140
+ async headers() {
141
+ // Every path gets the page headers. Next lets a later rule replace a header of the same name,
142
+ // so a path's own headers come after them.
143
+ const own = [{ source: "/:path*", headers: pageHeaders }, ...paths.map(p => ({ source: p.source, headers: p.headers }))];
144
+ return [...own, ...((await extraHeaders?.()) ?? [])];
145
+ },
146
+ async rewrites() {
147
+ const own = o.contentImages ? contentImagesRewrite(o.apiUrl) : [];
148
+ const more = await extraRewrites?.();
149
+ if (!more) return own;
150
+ if (Array.isArray(more)) return [...own, ...more];
151
+ return { ...more, afterFiles: [...own, ...(more.afterFiles ?? [])] };
152
+ },
153
+ };
154
+ }
package/src/styles.css CHANGED
@@ -45,6 +45,13 @@
45
45
  --color-toggle-off: var(--color-gray-200);
46
46
  --color-toggle-thumb: var(--color-white);
47
47
 
48
+ --color-state-heading: var(--color-gray-900);
49
+ --color-state-text: var(--color-gray-600);
50
+ --color-state-code: var(--color-gray-900);
51
+ --font-state-code: inherit;
52
+ --color-state-link: var(--color-gray-900);
53
+ --color-state-link-hover: var(--color-gray-900);
54
+
48
55
  --radius-button: var(--radius-lg);
49
56
  --color-button-primary-bg: var(--color-primary);
50
57
  --color-button-primary-text: var(--color-primary-foreground);
@@ -0,0 +1,73 @@
1
+ /** The widths the API serves an uploaded image at, as webp. */
2
+ export const CONTENT_IMAGE_WIDTHS: readonly number[] = [384, 640, 768, 1024, 1366, 1600];
3
+
4
+ /** Where the browser fetches an uploaded image. Site-relative, through the app's content images rewrite. */
5
+ export function contentImagePath(id: string, base = "/content-images"): string {
6
+ return `${base}/${encodeURIComponent(id)}`;
7
+ }
8
+
9
+ /**
10
+ * The `srcset` for an uploaded image. The API serves the nearest webp at or above the requested
11
+ * width, or the original when the browser does not accept webp.
12
+ */
13
+ export function contentImageSrcSet(id: string, widths: readonly number[] = CONTENT_IMAGE_WIDTHS, base?: string): string {
14
+ return widths.map(w => `${contentImagePath(id, base)}?w=${w} ${w}w`).join(", ");
15
+ }
16
+
17
+ export interface ContentImageProps {
18
+ /** Uploaded image id. Null falls back to `fallbackSrc`, which is served as it is. */
19
+ imageId: string | null;
20
+ /** Empty for an image that carries no meaning of its own, such as a decorative backdrop. */
21
+ alt: string;
22
+ /** Widths the image is shown at. Required, since a browser assumes 100vw without it. */
23
+ sizes: string;
24
+ /** A static path for when there is no uploaded image. */
25
+ fallbackSrc?: string | undefined;
26
+ className?: string | undefined;
27
+ /** Intrinsic size, to reserve space. Only needed where the layout fixes no ratio. */
28
+ width?: number | undefined;
29
+ height?: number | undefined;
30
+ /** Set on the one image visible without scrolling. It loads eagerly, at high priority. */
31
+ priority?: boolean | undefined;
32
+ "aria-hidden"?: boolean | undefined;
33
+ /** Path the content images rewrite serves, `/content-images` by default. */
34
+ base?: string | undefined;
35
+ /** Widths in the `srcset`. Defaults to the widths the API serves. */
36
+ widths?: readonly number[] | undefined;
37
+ }
38
+
39
+ /** An image from the content API, served responsively from the `sizes` a call site declares. */
40
+ export function ContentImage({
41
+ imageId,
42
+ alt,
43
+ sizes,
44
+ fallbackSrc,
45
+ className,
46
+ width,
47
+ height,
48
+ priority = false,
49
+ "aria-hidden": ariaHidden,
50
+ base,
51
+ widths,
52
+ }: ContentImageProps) {
53
+ const uploaded = imageId != null && imageId !== "";
54
+ const src = uploaded ? contentImagePath(imageId, base) : fallbackSrc;
55
+ if (!src) return null;
56
+
57
+ return (
58
+ // A plain img, because the API already serves the sizes. next/image would resize them again.
59
+ <img
60
+ src={src}
61
+ srcSet={uploaded ? contentImageSrcSet(imageId, widths, base) : undefined}
62
+ sizes={uploaded ? sizes : undefined}
63
+ alt={alt}
64
+ className={className}
65
+ width={width}
66
+ height={height}
67
+ loading={priority ? "eager" : "lazy"}
68
+ fetchPriority={priority ? "high" : undefined}
69
+ decoding={priority ? "sync" : "async"}
70
+ aria-hidden={ariaHidden}
71
+ />
72
+ );
73
+ }
@@ -0,0 +1,52 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { cx } from "./cx";
5
+ import { withDefaults, type TextOverrides } from "./withDefaults";
6
+
7
+ export interface ErrorStateStrings {
8
+ title: string;
9
+ body: string;
10
+ tryAgain: string;
11
+ home: string;
12
+ }
13
+
14
+ export const defaultErrorStateStrings: ErrorStateStrings = {
15
+ title: "Temporarily unavailable",
16
+ body: "We can't reach the service right now. Please try again shortly.",
17
+ tryAgain: "Try again",
18
+ home: "Go to the front page",
19
+ };
20
+
21
+ export interface ErrorStateProps {
22
+ /** The `reset` Next passes to an `error.tsx`. It renders the page again. */
23
+ reset: () => void;
24
+ /** Where the front page link goes, such as `/` or `/en`. */
25
+ homeHref: string;
26
+ strings?: TextOverrides<ErrorStateStrings> | undefined;
27
+ /** Classes for placing it, on the outer element. */
28
+ className?: string | undefined;
29
+ }
30
+
31
+ /** What an `error.tsx` shows when a page cannot be rendered. The response is still a 500. */
32
+ export function ErrorState({ reset, homeHref, strings, className }: ErrorStateProps) {
33
+ const text = withDefaults(defaultErrorStateStrings, strings);
34
+ return (
35
+ <div className={cx("min-h-[60vh] flex flex-col items-center justify-center gap-4 px-4 text-center", className)}>
36
+ <h1 className="text-3xl font-black text-state-heading">{text.title}</h1>
37
+ <p className="text-lg text-state-text max-w-md">{text.body}</p>
38
+ <div className="mt-2 flex flex-wrap items-center justify-center gap-4">
39
+ <button
40
+ type="button"
41
+ onClick={reset}
42
+ className="rounded-button bg-button-primary-bg text-button-primary-text font-semibold px-6 py-3 hover:brightness-95 transition"
43
+ >
44
+ {text.tryAgain}
45
+ </button>
46
+ <Link href={homeHref} className="font-semibold text-state-link hover:text-state-link-hover underline">
47
+ {text.home}
48
+ </Link>
49
+ </div>
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,37 @@
1
+ import Link from "next/link";
2
+ import { cx } from "./cx";
3
+ import { withDefaults, type TextOverrides } from "./withDefaults";
4
+
5
+ export interface NotFoundStrings {
6
+ code: string;
7
+ body: string;
8
+ home: string;
9
+ }
10
+
11
+ export const defaultNotFoundStrings: NotFoundStrings = {
12
+ code: "404",
13
+ body: "This page does not exist.",
14
+ home: "Go home",
15
+ };
16
+
17
+ export interface NotFoundStateProps {
18
+ /** Where the home link goes, such as `/` or `/en`. */
19
+ homeHref: string;
20
+ strings?: TextOverrides<NotFoundStrings> | undefined;
21
+ /** Classes for placing it, on the outer element. */
22
+ className?: string | undefined;
23
+ }
24
+
25
+ /** What a `not-found.tsx` shows. The message is the page's heading. */
26
+ export function NotFoundState({ homeHref, strings, className }: NotFoundStateProps) {
27
+ const text = withDefaults(defaultNotFoundStrings, strings);
28
+ return (
29
+ <div className={cx("min-h-[60vh] flex flex-col items-center justify-center gap-4 px-4 text-center", className)}>
30
+ <span className="text-6xl font-black font-state-code text-state-code">{text.code}</span>
31
+ <h1 className="text-lg text-state-text">{text.body}</h1>
32
+ <Link href={homeHref} className="font-semibold text-state-link hover:text-state-link-hover underline">
33
+ {text.home}
34
+ </Link>
35
+ </div>
36
+ );
37
+ }
package/src/ui/index.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  export { Alert } from "./Alert";
2
2
  export type { AlertProps } from "./Alert";
3
+ export { ContentImage, CONTENT_IMAGE_WIDTHS, contentImagePath, contentImageSrcSet } from "./ContentImage";
4
+ export type { ContentImageProps } from "./ContentImage";
3
5
  export { CredentialsForm } from "./CredentialsForm";
4
6
  export type { CredentialsFormProps } from "./CredentialsForm";
5
7
  export { SignInRejected, defaultCredentialsFormStrings } from "./credentialsFormStrings";
6
8
  export type { CredentialsFormStrings } from "./credentialsFormStrings";
9
+ export { ErrorState, defaultErrorStateStrings } from "./ErrorState";
10
+ export type { ErrorStateProps, ErrorStateStrings } from "./ErrorState";
11
+ export { NotFoundState, defaultNotFoundStrings } from "./NotFoundState";
12
+ export type { NotFoundStateProps, NotFoundStrings } from "./NotFoundState";
7
13
  export { PasswordInput } from "./PasswordInput";
8
14
  export type { PasswordInputProps } from "./PasswordInput";
9
15
  export { TextArea } from "./TextArea";