@prokodo/ui 0.0.55 β†’ 0.1.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
@@ -8,17 +8,32 @@
8
8
 
9
9
  **Modern, customizable UI components built with React and TypeScript β€” developed by [prokodo](https://www.prokodo.com) for high-performance web interfaces.**
10
10
 
11
- > πŸ‡ΊπŸ‡Έ Need help shipping a production **Next.js + Headless CMS** in 4–6 weeks?
12
- > **prokodo β€” Next.js CMS agency** β†’ [click here](https://www.prokodo.com/en/solution/next-js-cms?utm_source=github&utm_medium=readme_top)
11
+ > πŸ‡ΊπŸ‡Έ Need help shipping production **Next.js (App Router)** fast?
12
+ > **prokodo β€” Next.js Agency** β†’ [click here](https://www.prokodo.com/en/next-js-agency/?utm_source=github&utm_medium=readme_top&utm_campaign=ui)
13
13
  >
14
- > πŸ‡©πŸ‡ͺ Sie suchen eine **Next.js Agentur** (Strapi/Contentful/WP)?
15
- > **prokodo β€” Next.js CMS Agentur** β†’ [hier klicken](https://www.prokodo.com/de/loesung/next-js-cms?utm_source=github&utm_medium=readme_top)
14
+ > πŸ‡©πŸ‡ͺ Sie suchen eine **Next.js Agentur** (App Router, SEO, Performance)?
15
+ > **prokodo β€” Next.js Agentur** β†’ [hier klicken](https://www.prokodo.com/de/next-js-agentur/?utm_source=github&utm_medium=readme_top&utm_campaign=ui)
16
+
17
+ <details>
18
+ <summary><b>Further reading: Next.js guides</b> (SEO Β· Performance Β· Migration)</summary>
19
+
20
+ - SEO (Metadata API, hreflang):
21
+ https://www.prokodo.com/en/guide/next-js/next-js-seo/?utm_source=github&utm_medium=readme_examples&utm_campaign=ui&utm_content=seo_en
22
+
23
+ - Performance (LCP/INP/CLS, Streaming SSR):
24
+ https://www.prokodo.com/en/guide/next-js/next-js-performance/?utm_source=github&utm_medium=readme_examples&utm_campaign=ui&utm_content=perf_en
25
+
26
+ - Migration Playbook (RACI, Canary, Rollback):
27
+ https://www.prokodo.com/en/guide/next-js/next-js-migration/?utm_source=github&utm_medium=readme_examples&utm_campaign=ui&utm_content=migration_en
28
+ </details>
16
29
 
17
30
  [![npm](https://img.shields.io/npm/v/@prokodo/ui?style=flat&color=3178c6&label=npm)](https://www.npmjs.com/package/@prokodo/ui)
18
31
  [![CI](https://github.com/prokodo-agency/ui/actions/workflows/release.yml/badge.svg)](https://github.com/prokodo-agency/ui/actions/workflows/release.yml)
19
32
  [![License: BUSL-1.1](https://img.shields.io/badge/license-BUSL--1.1-blue.svg)](LICENSE)
20
33
  [![Storybook](https://img.shields.io/badge/storybook-ui.prokodo.com-ff4785?logo=storybook&logoColor=white)](https://ui.prokodo.com)
21
34
  [![bundlephobia](https://img.shields.io/bundlephobia/minzip/@prokodo/ui?label=bundle%20size&style=flat&color=blue)](https://bundlephobia.com/result?p=@prokodo/ui)
35
+ [![Next.js](https://img.shields.io/badge/Next.js-13–16-black)](#)
36
+ [![Turbopack](https://img.shields.io/badge/Works%20with-Turbopack-000)](#)
22
37
 
23
38
  ---
24
39
 
@@ -31,7 +46,9 @@
31
46
  - πŸ§ͺ **Reliable**: Fully tested with Jest and Testing Library
32
47
  - πŸ“š **Storybook**: Explore the components at [ui.prokodo.com](https://ui.prokodo.com)
33
48
  - πŸ“¦ **Ready-to-install**: Distributed via npm for non-production use under the BUSL-1.1 license
34
- - 🧱 **Optimized for SSR**: Works great with Next.js and React Server Components
49
+ - πŸš€ **Optimized for Next.js 13–16 out of the box** (App Router, React Server Components)
50
+ - ⚑ **Turbopack compatible** (no config required)
51
+ - πŸ”— **Framework adapters** via `UIRuntimeProvider` for `next/link` & `next/image`
35
52
 
36
53
  ## ⚑ Lightweight by Design
37
54
 
@@ -160,7 +177,7 @@ export default function GalleryPage() {
160
177
  | Grid/GridRow | βœ… | – |
161
178
  | Headline | βœ… | - |
162
179
  | Icon | βœ… | – |
163
- | Image | βœ… | – |
180
+ | Image | βœ… | βœ… |
164
181
  | ImageText | βœ… | - |
165
182
  | Input | βœ… | βœ… |
166
183
  | Label | βœ… | – |
@@ -186,6 +203,80 @@ export default function GalleryPage() {
186
203
  | Table | βœ… | – |
187
204
  | Teaser | βœ… | - |
188
205
 
206
+ ## Since Next.js 16
207
+
208
+ - Link/Image runtime provider (required for <Link>/<Image> adapters)
209
+ - For Next.js apps, provide your framework components (next/link, next/image) via a small client provider.
210
+ - Do not pass linkComponent / imageComponent props from pagesβ€”use the provider instead.
211
+
212
+ ### 1. Create a client provider
213
+
214
+ ```tsx
215
+ // app/providers/ProkodoUiNextProvider.tsx
216
+ "use client"
217
+
218
+ import NextLink from "next/link"
219
+ import NextImage from "next/image"
220
+ import { UIRuntimeProvider } from "@prokodo/ui/runtime"
221
+
222
+ export function ProkodoUiNextProvider({
223
+ children,
224
+ }: {
225
+ children: React.ReactNode
226
+ }) {
227
+ return (
228
+ <UIRuntimeProvider
229
+ value={{ linkComponent: NextLink, imageComponent: NextImage }}
230
+ >
231
+ {children}
232
+ </UIRuntimeProvider>
233
+ )
234
+ }
235
+ ```
236
+
237
+ ### 2. Wrap your root layout
238
+
239
+ ```tsx
240
+ // app/layout.tsx (server component)
241
+ import { ProkodoUiNextProvider } from "./providers/ProkodoUiNextProvider"
242
+
243
+ export default function RootLayout({
244
+ children,
245
+ }: {
246
+ children: React.ReactNode
247
+ }) {
248
+ return (
249
+ <html lang="en">
250
+ <body>
251
+ <ProkodoUiNextProvider>{children}</ProkodoUiNextProvider>
252
+ </body>
253
+ </html>
254
+ )
255
+ }
256
+ ```
257
+
258
+ ### 3. Use components normally (no extra props needed)
259
+
260
+ ```tsx
261
+ import { Link } from "@prokodo/ui/link"
262
+ import { Image } from "@prokodo/ui/image"
263
+
264
+ export default function Page() {
265
+ return (
266
+ <>
267
+ <Link href="/about">About</Link>
268
+ <Image src="/hero.jpg" alt="Hero" />
269
+ </>
270
+ )
271
+ }
272
+ ```
273
+
274
+ **Notes**:
275
+
276
+ - The provider file must be a "use client" module.
277
+ - Remove any linkComponent / imageComponent props you previously passed from server code.
278
+ - Plain React (non-Next) apps don’t need this; just use <a> / <img> or pass your own adapters inside client components.
279
+
189
280
  ## How to create my own Island Component?
190
281
 
191
282
  ### 1. Create your island component (Navbar.tsx):
@@ -243,11 +334,29 @@ export default createLazyWrapper<NavbarProps>({
243
334
 
244
335
  ## Examples (Next.js + Headless CMS)
245
336
 
337
+ Real-world setups we ship:
338
+
246
339
  - Next.js + **Strapi** content models
247
340
  - Next.js + **Contentful** entries & preview
248
341
  - Migration from **Headless WordPress** to Next.js
249
342
 
250
- Compare CMS options β†’ [Strapi vs Contentful vs Headless WP](https://www.prokodo.com/de/loesung/next-js-cms?utm_source=github&utm_medium=readme_examples)
343
+ **Need help or a quick scoping?**
344
+
345
+ - πŸ‡ΊπŸ‡Έ **Next.js Agency (EN)** β†’ https://www.prokodo.com/en/next-js-agency/?utm_source=github&utm_medium=readme_examples&utm_campaign=ui&utm_content=examples_cta_en
346
+ - πŸ‡©πŸ‡ͺ **Next.js Agentur (DE)** β†’ https://www.prokodo.com/de/next-js-agentur/?utm_source=github&utm_medium=readme_examples&utm_campaign=ui&utm_content=examples_cta_de
347
+
348
+ <details>
349
+ <summary><b>Further reading: Next.js guides</b> (SEO Β· Performance Β· Migration)</summary>
350
+
351
+ - SEO (Metadata API, hreflang):
352
+ https://www.prokodo.com/en/guide/next-js/next-js-seo/?utm_source=github&utm_medium=readme_examples&utm_campaign=ui&utm_content=seo_en
353
+
354
+ - Performance (LCP/INP/CLS, Streaming SSR):
355
+ https://www.prokodo.com/en/guide/next-js/next-js-performance/?utm_source=github&utm_medium=readme_examples&utm_campaign=ui&utm_content=perf_en
356
+
357
+ - Migration Playbook (RACI, Canary, Rollback):
358
+ https://www.prokodo.com/en/guide/next-js/next-js-migration/?utm_source=github&utm_medium=readme_examples&utm_campaign=ui&utm_content=migration_en
359
+ </details>
251
360
 
252
361
  ## πŸ“˜ Documentation
253
362
 
@@ -0,0 +1,39 @@
1
+ "use client";
2
+ var __defProp = Object.defineProperty;
3
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+ import { jsxs, jsx } from "react/jsx-runtime";
5
+ import { create } from "../../helpers/bem.js";
6
+ import { useUIRuntime } from "../../helpers/runtime.client.js";
7
+ import { isString } from "../../helpers/validations.js";
8
+ import styles from "./Image.module.scss.js";
9
+ const bem = create(styles, "Image");
10
+ const ImageClient = /* @__PURE__ */ __name(({
11
+ alt,
12
+ caption,
13
+ containerClassName,
14
+ captionClassName,
15
+ className,
16
+ imageComponent,
17
+ ...props
18
+ }) => {
19
+ const { imageComponent: ctxImage } = useUIRuntime();
20
+ const CustomImage = imageComponent ?? ctxImage ?? "img";
21
+ const renderImage = /* @__PURE__ */ __name(() => /* @__PURE__ */ jsx(
22
+ CustomImage,
23
+ {
24
+ alt: alt ?? "",
25
+ className: bem("image", void 0, className),
26
+ ...props
27
+ }
28
+ ), "renderImage");
29
+ if (isString(caption)) {
30
+ return /* @__PURE__ */ jsxs("figure", { className: bem(void 0, void 0, containerClassName), children: [
31
+ renderImage(),
32
+ /* @__PURE__ */ jsx("figcaption", { className: bem("caption", void 0, captionClassName), children: caption })
33
+ ] });
34
+ }
35
+ return renderImage();
36
+ }, "ImageClient");
37
+ export {
38
+ ImageClient as default
39
+ };
@@ -1,35 +1,13 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
- import { jsxs, jsx } from "react/jsx-runtime";
4
- import { create } from "../../helpers/bem.js";
5
- import { isString } from "../../helpers/validations.js";
6
- import styles from "./Image.module.scss.js";
7
- const bem = create(styles, "Image");
8
- const Image = /* @__PURE__ */ __name(({
9
- alt,
10
- caption,
11
- containerClassName,
12
- captionClassName,
13
- className,
14
- imageComponent: CustomImage = "img",
15
- ...props
16
- }) => {
17
- const renderImage = /* @__PURE__ */ __name(() => /* @__PURE__ */ jsx(
18
- CustomImage,
19
- {
20
- alt: alt ?? "",
21
- className: bem("image", void 0, className),
22
- ...props
23
- }
24
- ), "renderImage");
25
- if (isString(caption)) {
26
- return /* @__PURE__ */ jsxs("figure", { className: bem(void 0, void 0, containerClassName), children: [
27
- renderImage(),
28
- /* @__PURE__ */ jsx("figcaption", { className: bem("caption", void 0, captionClassName), children: caption })
29
- ] });
30
- }
31
- return renderImage();
32
- }, "Image");
3
+ import { createIsland } from "../../helpers/createIsland.js";
4
+ import ImageServer from "./Image.server.js";
5
+ const Image = createIsland({
6
+ name: "Image",
7
+ Server: ImageServer,
8
+ loadLazy: /* @__PURE__ */ __name(() => import("./Image.lazy.js"), "loadLazy"),
9
+ isInteractive: /* @__PURE__ */ __name((p) => typeof p.imageComponent === "function", "isInteractive")
10
+ });
33
11
  export {
34
12
  Image
35
13
  };
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ var __defProp = Object.defineProperty;
3
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+ import { createLazyWrapper } from "../../helpers/createLazyWrapper.js";
5
+ import ImageClient from "./Image.client.js";
6
+ import ImageServer from "./Image.server.js";
7
+ const Image_lazy = createLazyWrapper({
8
+ name: "Image",
9
+ Client: ImageClient,
10
+ Server: ImageServer,
11
+ // treat as interactive if a custom image component is a function (e.g. NextImage)
12
+ isInteractive: /* @__PURE__ */ __name((p) => typeof p.imageComponent === "function", "isInteractive")
13
+ });
14
+ export {
15
+ Image_lazy as default
16
+ };
@@ -0,0 +1,88 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+ import { jsxs, jsx } from "react/jsx-runtime";
4
+ import { create } from "../../helpers/bem.js";
5
+ import { isString } from "../../helpers/validations.js";
6
+ import styles from "./Image.module.scss.js";
7
+ const bem = create(styles, "Image");
8
+ function toImgOnlyProps(p) {
9
+ const {
10
+ // Strip next/image-only props so they don't leak onto <img>
11
+ _fill,
12
+ // intentionally unused (name the same to show intent)
13
+ _loader,
14
+ _placeholder,
15
+ _blurDataURL,
16
+ _priority,
17
+ _quality,
18
+ // Note: sizes *is* valid on <img> when it's a string, so we won't strip it
19
+ _onLoadingComplete,
20
+ // our runtime wiring we never want on <img>
21
+ _imageComponent,
22
+ ...rest
23
+ } = p;
24
+ let src;
25
+ const rawSrc = rest == null ? void 0 : rest.src;
26
+ if (typeof rawSrc === "string") {
27
+ src = rawSrc;
28
+ } else if (typeof rawSrc === "object" && rawSrc !== null && // explicit property check to satisfy strict-boolean-expressions
29
+ Object.prototype.hasOwnProperty.call(
30
+ rawSrc,
31
+ "src"
32
+ ) && typeof rawSrc.src === "string") {
33
+ const rs = rawSrc;
34
+ ({ src } = rs);
35
+ } else {
36
+ src = void 0;
37
+ }
38
+ const { width, height } = rest;
39
+ const imgProps = {
40
+ ...rest,
41
+ src,
42
+ width,
43
+ height
44
+ };
45
+ return imgProps;
46
+ }
47
+ __name(toImgOnlyProps, "toImgOnlyProps");
48
+ const ImageServer = /* @__PURE__ */ __name(({
49
+ alt,
50
+ caption,
51
+ containerClassName,
52
+ captionClassName,
53
+ className,
54
+ ...rawProps
55
+ }) => {
56
+ if (process.env.NODE_ENV !== "production" && typeof rawProps.imageComponent === "function") {
57
+ console.error(
58
+ "[UI] Do not pass function props (imageComponent) to <Image> on the server. Use UIRuntimeProvider in the parent app instead.",
59
+ rawProps
60
+ );
61
+ }
62
+ const {
63
+ imageComponent: _dropImageComponent,
64
+ onClick: _dropClick,
65
+ onKeyDown: _dropKey,
66
+ ...rest
67
+ } = rawProps;
68
+ const CustomImage = "img";
69
+ const imgProps = toImgOnlyProps(rest);
70
+ const renderImage = /* @__PURE__ */ __name(() => /* @__PURE__ */ jsx(
71
+ CustomImage,
72
+ {
73
+ alt: alt ?? "",
74
+ className: bem("image", void 0, className),
75
+ ...imgProps
76
+ }
77
+ ), "renderImage");
78
+ if (isString(caption)) {
79
+ return /* @__PURE__ */ jsxs("figure", { className: bem(void 0, void 0, containerClassName), children: [
80
+ renderImage(),
81
+ /* @__PURE__ */ jsx("figcaption", { className: bem("caption", void 0, captionClassName), children: caption })
82
+ ] });
83
+ }
84
+ return renderImage();
85
+ }, "ImageServer");
86
+ export {
87
+ ImageServer as default
88
+ };
@@ -1,10 +1,12 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import { memo } from "react";
4
+ import { useUIRuntime } from "../../helpers/runtime.client.js";
4
5
  import BaseLinkServer from "../base-link/BaseLink.server.js";
5
6
  import { LinkView } from "./Link.view.js";
6
7
  const LinkClient = memo((props) => {
7
8
  const { href, onClick } = props;
9
+ const { linkComponent: ctxLink } = useUIRuntime();
8
10
  const linkTag = onClick && !href ? "span" : "a";
9
11
  const hasHandlers = Boolean(onClick) || Boolean(props.onKeyDown);
10
12
  return /* @__PURE__ */ jsx(
@@ -13,7 +15,8 @@ const LinkClient = memo((props) => {
13
15
  ...props,
14
16
  BaseLinkComponent: BaseLinkServer,
15
17
  hasHandlers,
16
- LinkTag: linkTag
18
+ LinkTag: linkTag,
19
+ ...ctxLink ? { linkComponent: ctxLink } : null
17
20
  }
18
21
  );
19
22
  });
@@ -3,9 +3,21 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  import BaseLinkServer from "../base-link/BaseLink.server.js";
5
5
  import { LinkView } from "./Link.view.js";
6
- function LinkServer(props) {
6
+ function LinkServer(rawProps) {
7
+ if (process.env.NODE_ENV !== "production" && typeof (rawProps == null ? void 0 : rawProps.linkComponent) === "function") {
8
+ console.error(
9
+ "[UI] Do not pass function props (linkComponent) to <Link> on the server. Use the UIRuntimeProvider in the parent app instead.",
10
+ rawProps
11
+ );
12
+ }
13
+ const {
14
+ onClick,
15
+ onKeyDown: _onKeyDown,
16
+ linkComponent: _drop,
17
+ ...props
18
+ } = rawProps;
7
19
  const hasHandlers = false;
8
- const linkTag = props.onClick && !props.href ? "span" : "a";
20
+ const linkTag = onClick && !(props == null ? void 0 : props.href) ? "span" : "a";
9
21
  return /* @__PURE__ */ jsx(
10
22
  LinkView,
11
23
  {
@@ -1,4 +1,4 @@
1
- const PROKODO_UI_VERSION = "0.0.55";
1
+ const PROKODO_UI_VERSION = "0.1.0";
2
2
  export {
3
3
  PROKODO_UI_VERSION
4
4
  };
@@ -2,6 +2,12 @@ var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  import { lazy, Suspense, cloneElement } from "react";
5
+ function stripFnProps(p) {
6
+ return Object.fromEntries(
7
+ Object.entries(p).filter(([, v]) => typeof v !== "function")
8
+ );
9
+ }
10
+ __name(stripFnProps, "stripFnProps");
5
11
  function createIsland({
6
12
  name,
7
13
  Server,
@@ -13,40 +19,25 @@ function createIsland({
13
19
  void loadLazy();
14
20
  }
15
21
  function withIslandAttr(el, priority) {
16
- var _a;
17
22
  const islandName = name.toLowerCase();
18
- if (typeof process !== "undefined" && typeof ((_a = process == null ? void 0 : process.env) == null ? void 0 : _a.PK_ENABLE_DEBUG_LOGS) === "string") {
19
- console.debug(
20
- `[hydrate] createIsland β€œ${name}” rendering as interactive=${Boolean(
21
- priority
22
- )}`
23
- );
24
- }
25
23
  const extra = {
26
24
  "data-island": islandName
27
25
  };
28
- if (Boolean(priority)) {
29
- extra.priority = priority;
30
- }
26
+ if (Boolean(priority)) extra.priority = true;
31
27
  return cloneElement(el, extra);
32
28
  }
33
29
  __name(withIslandAttr, "withIslandAttr");
34
- const Island = /* @__PURE__ */ __name(({
35
- priority = false,
36
- ...raw
37
- }) => {
30
+ const Island = /* @__PURE__ */ __name(({ ...raw }) => {
38
31
  const props = raw;
39
32
  const autoInteractive = Object.entries(props).some(
40
33
  ([k, v]) => k.startsWith("on") && typeof v === "function"
41
34
  ) || props.redirect !== void 0;
42
35
  const interactive = customInteractive ? customInteractive(props) || autoInteractive : autoInteractive;
43
- if (interactive && priority) {
44
- return withIslandAttr(/* @__PURE__ */ jsx(LazyComp, { ...props }));
45
- }
36
+ const serverSafe = stripFnProps(props);
46
37
  if (!interactive) {
47
- return withIslandAttr(/* @__PURE__ */ jsx(Server, { ...props }));
38
+ return withIslandAttr(/* @__PURE__ */ jsx(Server, { ...serverSafe }));
48
39
  }
49
- const fallback = withIslandAttr(/* @__PURE__ */ jsx(Server, { ...props }));
40
+ const fallback = withIslandAttr(/* @__PURE__ */ jsx(Server, { ...serverSafe }));
50
41
  return /* @__PURE__ */ jsx(Suspense, { fallback, children: withIslandAttr(/* @__PURE__ */ jsx(LazyComp, { ...props })) });
51
42
  }, "Island");
52
43
  Island.displayName = `${name}Island`;
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ var __defProp = Object.defineProperty;
3
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+ import { jsx } from "react/jsx-runtime";
5
+ import { useContext, createContext } from "react";
6
+ const UIRuntimeContext = createContext({});
7
+ const UIRuntimeProvider = /* @__PURE__ */ __name(({
8
+ value,
9
+ children
10
+ }) => /* @__PURE__ */ jsx(UIRuntimeContext.Provider, { value, children }), "UIRuntimeProvider");
11
+ const useUIRuntime = /* @__PURE__ */ __name(() => useContext(UIRuntimeContext), "useUIRuntime");
12
+ export {
13
+ UIRuntimeProvider,
14
+ useUIRuntime
15
+ };
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { UIRuntimeProvider, useUIRuntime } from "./helpers/runtime.client.js";
1
2
  import { Accordion } from "./components/accordion/Accordion.js";
2
3
  import { Animated } from "./components/animated/Animated.js";
3
4
  import { AnimatedText } from "./components/animatedText/AnimatedText.js";
@@ -90,5 +91,7 @@ export {
90
91
  Stepper,
91
92
  Switch,
92
93
  Table,
93
- Teaser
94
+ Teaser,
95
+ UIRuntimeProvider,
96
+ useUIRuntime
94
97
  };