@dazl/component-gallery 1.0.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/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/example/example.d.ts +7 -0
- package/dist/example/example.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/route/component-gallery.d.ts +2 -0
- package/dist/route/component-gallery.d.ts.map +1 -0
- package/dist/route/example-card.d.ts +8 -0
- package/dist/route/example-card.d.ts.map +1 -0
- package/dist/route/index.d.ts +3 -0
- package/dist/route/index.d.ts.map +1 -0
- package/dist/route/index.js +115 -0
- package/dist/route/types.d.ts +18 -0
- package/dist/route/types.d.ts.map +1 -0
- package/dist/route/use-component-gallery-params.d.ts +3 -0
- package/dist/route/use-component-gallery-params.d.ts.map +1 -0
- package/dist/route/use-link-behavior-override.d.ts +7 -0
- package/dist/route/use-link-behavior-override.d.ts.map +1 -0
- package/dist/route/use-scroll-to-example-card.d.ts +7 -0
- package/dist/route/use-scroll-to-example-card.d.ts.map +1 -0
- package/dist/styles.css +1 -0
- package/package.json +42 -0
- package/src/example/example.module.css +30 -0
- package/src/example/example.tsx +18 -0
- package/src/index.ts +1 -0
- package/src/route/component-gallery.module.css +10 -0
- package/src/route/component-gallery.tsx +18 -0
- package/src/route/example-card.module.css +65 -0
- package/src/route/example-card.tsx +92 -0
- package/src/route/index.ts +2 -0
- package/src/route/types.ts +16 -0
- package/src/route/use-component-gallery-params.ts +27 -0
- package/src/route/use-link-behavior-override.ts +35 -0
- package/src/route/use-scroll-to-example-card.ts +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License Copyright (c) 2026 Dazl
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of
|
|
4
|
+
charge, to any person obtaining a copy of this software and associated
|
|
5
|
+
documentation files (the "Software"), to deal in the Software without
|
|
6
|
+
restriction, including without limitation the rights to use, copy, modify, merge,
|
|
7
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice
|
|
12
|
+
(including the next paragraph) shall be included in all copies or substantial
|
|
13
|
+
portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
16
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
18
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
19
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @dazl/component-gallery
|
|
2
|
+
|
|
3
|
+
A [Dazl](https://dazl.dev) integration that lets you browse and preview your project's React components.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Within your React Router project:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @dazl/component-gallery
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
### 1. Add the Gallery Route
|
|
16
|
+
|
|
17
|
+
Create a development-only route in your React Router project:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
// app/dev/components.tsx
|
|
21
|
+
import '@dazl/component-gallery/styles.css';
|
|
22
|
+
export { default } from '@dazl/component-gallery/route';
|
|
23
|
+
export function clientLoader() {}
|
|
24
|
+
export function HydrateFallback() {}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Register the route:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// app/routes.ts
|
|
31
|
+
import { route, type RouteConfig } from '@react-router/dev/routes';
|
|
32
|
+
|
|
33
|
+
export default [
|
|
34
|
+
// ... your other routes
|
|
35
|
+
route('dev/components', 'dev/components.tsx'),
|
|
36
|
+
] satisfies RouteConfig;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Create Component Examples
|
|
40
|
+
|
|
41
|
+
Define component examples as `.example.tsx` files that export a React component as the default export:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
// app/components/button/button.example.tsx
|
|
45
|
+
import { Button } from './button';
|
|
46
|
+
|
|
47
|
+
export default function ButtonExample() {
|
|
48
|
+
return <Button>Click me</Button>;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
When you open the project in Dazl, it will automatically discover these files and provide a menu that lists all examples, allowing you to preview them.
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
### Organizing Examples with Sections
|
|
57
|
+
|
|
58
|
+
Use the `Section` component to group related examples within a single file:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// app/components/button/button.example.tsx
|
|
62
|
+
import { Section } from '@dazl/component-gallery';
|
|
63
|
+
import { Button } from './button';
|
|
64
|
+
|
|
65
|
+
export default function ButtonExample() {
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<Section title="Sizes" layout="row">
|
|
69
|
+
<Button size="sm">Small</Button>
|
|
70
|
+
<Button size="md">Medium</Button>
|
|
71
|
+
<Button size="lg">Large</Button>
|
|
72
|
+
</Section>
|
|
73
|
+
|
|
74
|
+
<Section title="States" layout="column">
|
|
75
|
+
<Button disabled>Disabled</Button>
|
|
76
|
+
<Button loading>Loading</Button>
|
|
77
|
+
</Section>
|
|
78
|
+
</>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface SectionProps {
|
|
2
|
+
title?: string;
|
|
3
|
+
layout?: 'row' | 'column';
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
export declare const Section: ({ title, layout, children }: SectionProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=example.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"example.d.ts","sourceRoot":"","sources":["../../src/example/example.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC1B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC7B;AAED,eAAO,MAAM,OAAO,GAAI,6BAAwC,YAAY,4CAS3E,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsxs as c, jsx as t } from "react/jsx-runtime";
|
|
2
|
+
const i = "_section_omose_1", l = "_sectionTitle_omose_9", m = "_sectionContentColumn_omose_18", C = "_sectionContentRow_omose_24", o = {
|
|
3
|
+
section: i,
|
|
4
|
+
sectionTitle: l,
|
|
5
|
+
sectionContentColumn: m,
|
|
6
|
+
sectionContentRow: C
|
|
7
|
+
}, u = ({ title: n, layout: e = "column", children: s }) => /* @__PURE__ */ c("section", { className: o.section, children: [
|
|
8
|
+
n ? /* @__PURE__ */ t("div", { className: o.sectionTitle, children: n }) : null,
|
|
9
|
+
/* @__PURE__ */ t("div", { className: e === "column" ? o.sectionContentColumn : o.sectionContentRow, children: s })
|
|
10
|
+
] });
|
|
11
|
+
export {
|
|
12
|
+
u as Section
|
|
13
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component-gallery.d.ts","sourceRoot":"","sources":["../../src/route/component-gallery.tsx"],"names":[],"mappings":"AAMA,MAAM,CAAC,OAAO,UAAU,gBAAgB,4CAWvC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ComponentExampleInfo } from './types';
|
|
2
|
+
interface ExampleCardProps {
|
|
3
|
+
example: ComponentExampleInfo;
|
|
4
|
+
}
|
|
5
|
+
export declare const ExampleCard: import("react").NamedExoticComponent<ExampleCardProps>;
|
|
6
|
+
export declare const scrollExampleCardIntoView: (id: string) => void;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=example-card.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"example-card.d.ts","sourceRoot":"","sources":["../../src/route/example-card.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAIpD,UAAU,gBAAgB;IACtB,OAAO,EAAE,oBAAoB,CAAC;CACjC;AAED,eAAO,MAAM,WAAW,wDAgBtB,CAAC;AA8CH,eAAO,MAAM,yBAAyB,GAAI,IAAI,MAAM,SAoBnD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/route/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { jsxs as u, jsx as n } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback as f, memo as _, useState as h, lazy as b, Suspense as g, Component as v, useMemo as w, useEffect as p, useRef as E } from "react";
|
|
3
|
+
import { z as s } from "zod";
|
|
4
|
+
const S = "_root_segk4_1", k = {
|
|
5
|
+
root: S
|
|
6
|
+
}, y = () => f((e) => {
|
|
7
|
+
if (!e) return;
|
|
8
|
+
const r = (t) => {
|
|
9
|
+
if (!(t.target instanceof Element)) return;
|
|
10
|
+
const a = t.target.closest("a");
|
|
11
|
+
if (!a) return;
|
|
12
|
+
const o = a.getAttribute("href");
|
|
13
|
+
o && (t.preventDefault(), C(o) && window.open(o, "_blank", "noopener,noreferrer"));
|
|
14
|
+
};
|
|
15
|
+
return e.addEventListener("click", r), () => {
|
|
16
|
+
e.removeEventListener("click", r);
|
|
17
|
+
};
|
|
18
|
+
}, []), C = (e) => /^[a-z][a-z0-9+.-]*:/i.test(e), x = "_card_krcb2_1", N = "_cardHeader_krcb2_10", I = "_cardContent_krcb2_19", L = "_loadingState_krcb2_23", D = "_spinner_krcb2_30", H = "_errorState_krcb2_45", P = "_errorIcon_krcb2_56", z = "_errorMessage_krcb2_60", c = {
|
|
19
|
+
card: x,
|
|
20
|
+
cardHeader: N,
|
|
21
|
+
cardContent: I,
|
|
22
|
+
loadingState: L,
|
|
23
|
+
spinner: D,
|
|
24
|
+
errorState: H,
|
|
25
|
+
errorIcon: P,
|
|
26
|
+
errorMessage: z
|
|
27
|
+
}, M = _(function({ example: r }) {
|
|
28
|
+
const [t] = h(() => b(() => import(
|
|
29
|
+
/* @vite-ignore */
|
|
30
|
+
`/${r.relativePath}`
|
|
31
|
+
))), a = y();
|
|
32
|
+
return /* @__PURE__ */ u("div", { id: r.relativePath, className: c.card, children: [
|
|
33
|
+
/* @__PURE__ */ n("h3", { className: c.cardHeader, children: r.displayName }),
|
|
34
|
+
/* @__PURE__ */ n("div", { ref: a, className: c.cardContent, children: /* @__PURE__ */ n(R, { fallback: (o) => /* @__PURE__ */ n(F, { error: o }), children: /* @__PURE__ */ n(g, { fallback: /* @__PURE__ */ n(A, {}), children: /* @__PURE__ */ n(t, {}) }) }) })
|
|
35
|
+
] });
|
|
36
|
+
}), A = () => /* @__PURE__ */ n("div", { className: c.loadingState, children: /* @__PURE__ */ n("div", { className: c.spinner }) }), F = ({ error: e }) => /* @__PURE__ */ u("div", { className: c.errorState, children: [
|
|
37
|
+
/* @__PURE__ */ n("span", { className: c.errorIcon, children: "⚠" }),
|
|
38
|
+
/* @__PURE__ */ n("span", { className: c.errorMessage, children: e.message })
|
|
39
|
+
] });
|
|
40
|
+
class R extends v {
|
|
41
|
+
constructor(r) {
|
|
42
|
+
super(r), this.state = { error: null };
|
|
43
|
+
}
|
|
44
|
+
static getDerivedStateFromError(r) {
|
|
45
|
+
return { error: r };
|
|
46
|
+
}
|
|
47
|
+
render() {
|
|
48
|
+
return this.state.error ? this.props.fallback(this.state.error) : this.props.children;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const j = (e) => {
|
|
52
|
+
const r = document.getElementById(e);
|
|
53
|
+
r && (r.scrollIntoView({
|
|
54
|
+
behavior: "instant",
|
|
55
|
+
block: "nearest"
|
|
56
|
+
}), r.animate(
|
|
57
|
+
[
|
|
58
|
+
{ boxShadow: "0 0 0 0 #3E63DD" },
|
|
59
|
+
{ boxShadow: "0 0 0 4px #3E63DD", offset: 0.2 },
|
|
60
|
+
{ boxShadow: "0 0 0 0 #3E63DD" }
|
|
61
|
+
],
|
|
62
|
+
{
|
|
63
|
+
duration: 900,
|
|
64
|
+
easing: "ease-out"
|
|
65
|
+
}
|
|
66
|
+
));
|
|
67
|
+
}, O = s.object({
|
|
68
|
+
displayName: s.string(),
|
|
69
|
+
relativePath: s.string()
|
|
70
|
+
}), B = s.object({
|
|
71
|
+
columns: s.number().default(2),
|
|
72
|
+
examples: s.array(O).default([]),
|
|
73
|
+
selectedExamplePath: s.string().optional(),
|
|
74
|
+
random: s.number().optional()
|
|
75
|
+
}), G = () => {
|
|
76
|
+
const [e, r] = h(() => typeof window < "u" ? window.location.hash : "");
|
|
77
|
+
return p(() => {
|
|
78
|
+
const t = () => r(window.location.hash);
|
|
79
|
+
return window.addEventListener("hashchange", t), () => window.removeEventListener("hashchange", t);
|
|
80
|
+
}, []), e;
|
|
81
|
+
}, U = () => {
|
|
82
|
+
const e = G();
|
|
83
|
+
return w(() => {
|
|
84
|
+
try {
|
|
85
|
+
const r = JSON.parse(decodeURIComponent(e.slice(1)));
|
|
86
|
+
return B.parse(r);
|
|
87
|
+
} catch {
|
|
88
|
+
return { columns: 1, examples: [] };
|
|
89
|
+
}
|
|
90
|
+
}, [e]);
|
|
91
|
+
}, V = (e, r) => {
|
|
92
|
+
const t = E(null);
|
|
93
|
+
return p(() => {
|
|
94
|
+
const a = t.current;
|
|
95
|
+
if (!e || !a) return;
|
|
96
|
+
let o = !1, l = !1, i = 0;
|
|
97
|
+
const d = () => {
|
|
98
|
+
l || (o = !0);
|
|
99
|
+
}, m = new ResizeObserver(() => {
|
|
100
|
+
o || (l = !0, j(e), cancelAnimationFrame(i), i = requestAnimationFrame(() => {
|
|
101
|
+
l = !1;
|
|
102
|
+
}));
|
|
103
|
+
});
|
|
104
|
+
return window.addEventListener("scroll", d), m.observe(a), () => {
|
|
105
|
+
window.removeEventListener("scroll", d), m.disconnect(), cancelAnimationFrame(i);
|
|
106
|
+
};
|
|
107
|
+
}, [e, r]), t;
|
|
108
|
+
};
|
|
109
|
+
function T() {
|
|
110
|
+
const { columns: e, examples: r, selectedExamplePath: t, random: a } = U(), o = V(t, a);
|
|
111
|
+
return /* @__PURE__ */ n("div", { ref: o, className: k.root, style: { "--columns": e }, children: r.map((l) => /* @__PURE__ */ n(M, { example: l }, l.relativePath)) });
|
|
112
|
+
}
|
|
113
|
+
export {
|
|
114
|
+
T as default
|
|
115
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const componentExampleInfoSchema: z.ZodObject<{
|
|
3
|
+
displayName: z.ZodString;
|
|
4
|
+
relativePath: z.ZodString;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export declare const componentGalleryParamsSchema: z.ZodObject<{
|
|
7
|
+
columns: z.ZodDefault<z.ZodNumber>;
|
|
8
|
+
examples: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
9
|
+
displayName: z.ZodString;
|
|
10
|
+
relativePath: z.ZodString;
|
|
11
|
+
}, z.core.$strip>>>;
|
|
12
|
+
selectedExamplePath: z.ZodOptional<z.ZodString>;
|
|
13
|
+
random: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
export type ComponentExampleInfo = z.infer<typeof componentExampleInfoSchema>;
|
|
16
|
+
export type ComponentGalleryParams = z.infer<typeof componentGalleryParamsSchema>;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/route/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,QAAA,MAAM,0BAA0B;;;iBAG9B,CAAC;AAEH,eAAO,MAAM,4BAA4B;;;;;;;;iBAKvC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAC9E,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-component-gallery-params.d.ts","sourceRoot":"","sources":["../../src/route/use-component-gallery-params.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,sBAAsB,EAAgC,MAAM,SAAS,CAAC;AAcpF,eAAO,MAAM,yBAAyB,QAAO,sBAW5C,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type RefCallback } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Always open absolute URLs in a new tab and prevent navigation for relative
|
|
4
|
+
* URLs.
|
|
5
|
+
*/
|
|
6
|
+
export declare const useLinkBehaviorOverride: () => RefCallback<HTMLElement>;
|
|
7
|
+
//# sourceMappingURL=use-link-behavior-override.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-link-behavior-override.d.ts","sourceRoot":"","sources":["../../src/route/use-link-behavior-override.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAe,MAAM,OAAO,CAAC;AAEtD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAAO,WAAW,CAAC,WAAW,CA0BjE,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scrolls to an example card whenever the container resizes (e.g., as lazy
|
|
3
|
+
* components load). Auto-scroll stops once the user scrolls manually, and
|
|
4
|
+
* resets on dependency changes.
|
|
5
|
+
*/
|
|
6
|
+
export declare const useScrollToExampleCard: (relativePath: string | undefined, counter: number | undefined) => import("react").RefObject<HTMLDivElement | null>;
|
|
7
|
+
//# sourceMappingURL=use-scroll-to-example-card.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-scroll-to-example-card.d.ts","sourceRoot":"","sources":["../../src/route/use-scroll-to-example-card.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GAAI,cAAc,MAAM,GAAG,SAAS,EAAE,SAAS,MAAM,GAAG,SAAS,qDAqCnG,CAAC"}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
._section_omose_1{margin-bottom:32px}._section_omose_1:last-child{margin-bottom:0}._sectionTitle_omose_9{margin-bottom:16px;font-weight:700;font-size:12px;color:light-dark(#666,#999);text-transform:uppercase;letter-spacing:.05em}._sectionContentColumn_omose_18{display:flex;flex-direction:column;gap:16px}._sectionContentRow_omose_24{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:16px}._root_segk4_1{display:grid;gap:16px;padding:16px;grid-template-columns:repeat(var(--columns, 1),minmax(0,1fr))}._root_segk4_1>*{scroll-margin:16px}._card_krcb2_1{border-radius:8px;padding:16px;display:flex;flex-direction:column;background-color:light-dark(#fff,#222);overflow:auto}._cardHeader_krcb2_10{margin-bottom:16px;font-weight:700;font-size:12px;color:light-dark(#666,#999);text-transform:uppercase;letter-spacing:.05em}._cardContent_krcb2_19{flex:1}._loadingState_krcb2_23{display:flex;align-items:center;justify-content:center;padding:0 32px 32px}._spinner_krcb2_30{width:24px;height:24px;border:2px solid light-dark(#eee,#444);border-top-color:light-dark(#999,#888);border-radius:50%;animation:_spin_krcb2_30 1s linear infinite}@keyframes _spin_krcb2_30{to{transform:rotate(360deg)}}._errorState_krcb2_45{padding:32px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;text-align:center;color:light-dark(#ce2c31,#ec5d5e)}._errorIcon_krcb2_56{font-size:32px}._errorMessage_krcb2_60{border-radius:4px;overflow-wrap:break-word;font-weight:400;font-size:14px}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dazl/component-gallery",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "View your components in a gallery inside Dazl",
|
|
5
|
+
"author": "Dazl",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./route": {
|
|
14
|
+
"import": "./dist/route/index.js",
|
|
15
|
+
"types": "./dist/route/index.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./styles.css": "./dist/styles.css",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"dist",
|
|
23
|
+
"!dist/tsconfig.tsbuildinfo"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"package": "vite build"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/dazl-dev/dazl-libs.git",
|
|
34
|
+
"directory": "packages/component-gallery"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"zod": ">=4.0.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": ">=19.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
.section {
|
|
2
|
+
margin-bottom: 32px;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.section:last-child {
|
|
6
|
+
margin-bottom: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.sectionTitle {
|
|
10
|
+
margin-bottom: 16px;
|
|
11
|
+
font-weight: bold;
|
|
12
|
+
font-size: 12px;
|
|
13
|
+
color: light-dark(#666, #999);
|
|
14
|
+
text-transform: uppercase;
|
|
15
|
+
letter-spacing: 0.05em;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.sectionContentColumn {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: 16px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.sectionContentRow {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: row;
|
|
27
|
+
flex-wrap: wrap;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: 16px;
|
|
30
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import styles from './example.module.css';
|
|
2
|
+
|
|
3
|
+
export interface SectionProps {
|
|
4
|
+
title?: string;
|
|
5
|
+
layout?: 'row' | 'column';
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Section = ({ title, layout = 'column', children }: SectionProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<section className={styles.section}>
|
|
12
|
+
{title ? <div className={styles.sectionTitle}>{title}</div> : null}
|
|
13
|
+
<div className={layout === 'column' ? styles.sectionContentColumn : styles.sectionContentRow}>
|
|
14
|
+
{children}
|
|
15
|
+
</div>
|
|
16
|
+
</section>
|
|
17
|
+
);
|
|
18
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './example/example';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
import styles from './component-gallery.module.css';
|
|
3
|
+
import { ExampleCard } from './example-card';
|
|
4
|
+
import { useComponentGalleryParams } from './use-component-gallery-params';
|
|
5
|
+
import { useScrollToExampleCard } from './use-scroll-to-example-card';
|
|
6
|
+
|
|
7
|
+
export default function ComponentGallery() {
|
|
8
|
+
const { columns, examples, selectedExamplePath, random } = useComponentGalleryParams();
|
|
9
|
+
const ref = useScrollToExampleCard(selectedExamplePath, random);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div ref={ref} className={styles.root} style={{ '--columns': columns } as CSSProperties}>
|
|
13
|
+
{examples.map((example) => (
|
|
14
|
+
<ExampleCard key={example.relativePath} example={example} />
|
|
15
|
+
))}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
.card {
|
|
2
|
+
border-radius: 8px;
|
|
3
|
+
padding: 16px;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
background-color: light-dark(#fff, #222);
|
|
7
|
+
overflow: auto;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.cardHeader {
|
|
11
|
+
margin-bottom: 16px;
|
|
12
|
+
font-weight: bold;
|
|
13
|
+
font-size: 12px;
|
|
14
|
+
color: light-dark(#666, #999);
|
|
15
|
+
text-transform: uppercase;
|
|
16
|
+
letter-spacing: 0.05em;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.cardContent {
|
|
20
|
+
flex: 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.loadingState {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
padding: 0 32px 32px 32px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.spinner {
|
|
31
|
+
width: 24px;
|
|
32
|
+
height: 24px;
|
|
33
|
+
border: 2px solid light-dark(#eee, #444);
|
|
34
|
+
border-top-color: light-dark(#999, #888);
|
|
35
|
+
border-radius: 50%;
|
|
36
|
+
animation: spin 1s linear infinite;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@keyframes spin {
|
|
40
|
+
to {
|
|
41
|
+
transform: rotate(360deg);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.errorState {
|
|
46
|
+
padding: 32px;
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
gap: 8px;
|
|
52
|
+
text-align: center;
|
|
53
|
+
color: light-dark(#ce2c31, #ec5d5e);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.errorIcon {
|
|
57
|
+
font-size: 32px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.errorMessage {
|
|
61
|
+
border-radius: 4px;
|
|
62
|
+
overflow-wrap: break-word;
|
|
63
|
+
font-weight: 400;
|
|
64
|
+
font-size: 14px;
|
|
65
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { lazy, Component, Suspense, useState, memo } from 'react';
|
|
2
|
+
import type { ComponentExampleInfo } from './types';
|
|
3
|
+
import { useLinkBehaviorOverride } from './use-link-behavior-override';
|
|
4
|
+
import styles from './example-card.module.css';
|
|
5
|
+
|
|
6
|
+
interface ExampleCardProps {
|
|
7
|
+
example: ComponentExampleInfo;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ExampleCard = memo(function ExampleCard({ example }: ExampleCardProps) {
|
|
11
|
+
const [Component] = useState(() => lazy(() => import(/* @vite-ignore */ `/${example.relativePath}`)));
|
|
12
|
+
const contentRef = useLinkBehaviorOverride();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div id={example.relativePath} className={styles.card}>
|
|
16
|
+
<h3 className={styles.cardHeader}>{example.displayName}</h3>
|
|
17
|
+
<div ref={contentRef} className={styles.cardContent}>
|
|
18
|
+
<ErrorBoundary fallback={(error) => <ErrorState error={error} />}>
|
|
19
|
+
<Suspense fallback={<LoadingState />}>
|
|
20
|
+
<Component />
|
|
21
|
+
</Suspense>
|
|
22
|
+
</ErrorBoundary>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const LoadingState = () => {
|
|
29
|
+
return (
|
|
30
|
+
<div className={styles.loadingState}>
|
|
31
|
+
<div className={styles.spinner} />
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const ErrorState = ({ error }: { error: Error }) => {
|
|
37
|
+
return (
|
|
38
|
+
<div className={styles.errorState}>
|
|
39
|
+
<span className={styles.errorIcon}>⚠</span>
|
|
40
|
+
<span className={styles.errorMessage}>{error.message}</span>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
interface ErrorBoundaryProps {
|
|
46
|
+
children: React.ReactNode;
|
|
47
|
+
fallback: (error: Error) => React.ReactNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ErrorBoundaryState {
|
|
51
|
+
error: Error | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
55
|
+
constructor(props: ErrorBoundaryProps) {
|
|
56
|
+
super(props);
|
|
57
|
+
this.state = { error: null };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
61
|
+
return { error };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
render() {
|
|
65
|
+
if (this.state.error) {
|
|
66
|
+
return this.props.fallback(this.state.error);
|
|
67
|
+
}
|
|
68
|
+
return this.props.children;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const scrollExampleCardIntoView = (id: string) => {
|
|
73
|
+
const element = document.getElementById(id);
|
|
74
|
+
if (!element) return;
|
|
75
|
+
|
|
76
|
+
element.scrollIntoView({
|
|
77
|
+
behavior: 'instant',
|
|
78
|
+
block: 'nearest',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
element.animate(
|
|
82
|
+
[
|
|
83
|
+
{ boxShadow: '0 0 0 0 #3E63DD' },
|
|
84
|
+
{ boxShadow: '0 0 0 4px #3E63DD', offset: 0.2 },
|
|
85
|
+
{ boxShadow: '0 0 0 0 #3E63DD' },
|
|
86
|
+
],
|
|
87
|
+
{
|
|
88
|
+
duration: 900,
|
|
89
|
+
easing: 'ease-out',
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const componentExampleInfoSchema = z.object({
|
|
4
|
+
displayName: z.string(),
|
|
5
|
+
relativePath: z.string(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const componentGalleryParamsSchema = z.object({
|
|
9
|
+
columns: z.number().default(2),
|
|
10
|
+
examples: z.array(componentExampleInfoSchema).default([]),
|
|
11
|
+
selectedExamplePath: z.string().optional(),
|
|
12
|
+
random: z.number().optional(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type ComponentExampleInfo = z.infer<typeof componentExampleInfoSchema>;
|
|
16
|
+
export type ComponentGalleryParams = z.infer<typeof componentGalleryParamsSchema>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { type ComponentGalleryParams, componentGalleryParamsSchema } from './types';
|
|
3
|
+
|
|
4
|
+
const useLocationHash = (): string => {
|
|
5
|
+
const [hash, setHash] = useState(() => (typeof window !== 'undefined' ? window.location.hash : ''));
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const handleHashChange = () => setHash(window.location.hash);
|
|
9
|
+
window.addEventListener('hashchange', handleHashChange);
|
|
10
|
+
return () => window.removeEventListener('hashchange', handleHashChange);
|
|
11
|
+
}, []);
|
|
12
|
+
|
|
13
|
+
return hash;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const useComponentGalleryParams = (): ComponentGalleryParams => {
|
|
17
|
+
const hash = useLocationHash();
|
|
18
|
+
|
|
19
|
+
return useMemo(() => {
|
|
20
|
+
try {
|
|
21
|
+
const parsed: unknown = JSON.parse(decodeURIComponent(hash.slice(1)));
|
|
22
|
+
return componentGalleryParamsSchema.parse(parsed);
|
|
23
|
+
} catch {
|
|
24
|
+
return { columns: 1, examples: [] };
|
|
25
|
+
}
|
|
26
|
+
}, [hash]);
|
|
27
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type RefCallback, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Always open absolute URLs in a new tab and prevent navigation for relative
|
|
5
|
+
* URLs.
|
|
6
|
+
*/
|
|
7
|
+
export const useLinkBehaviorOverride = (): RefCallback<HTMLElement> => {
|
|
8
|
+
return useCallback((container: HTMLElement | null) => {
|
|
9
|
+
if (!container) return;
|
|
10
|
+
|
|
11
|
+
const handleClick = (event: MouseEvent) => {
|
|
12
|
+
if (!(event.target instanceof Element)) return;
|
|
13
|
+
|
|
14
|
+
const link = event.target.closest('a');
|
|
15
|
+
if (!link) return;
|
|
16
|
+
|
|
17
|
+
const href = link.getAttribute('href');
|
|
18
|
+
if (!href) return;
|
|
19
|
+
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
|
|
22
|
+
if (isAbsoluteUrl(href)) {
|
|
23
|
+
window.open(href, '_blank', 'noopener,noreferrer');
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
container.addEventListener('click', handleClick);
|
|
28
|
+
|
|
29
|
+
return () => {
|
|
30
|
+
container.removeEventListener('click', handleClick);
|
|
31
|
+
};
|
|
32
|
+
}, []);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const isAbsoluteUrl = (url: string): boolean => /^[a-z][a-z0-9+.-]*:/i.test(url);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { scrollExampleCardIntoView } from './example-card';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Scrolls to an example card whenever the container resizes (e.g., as lazy
|
|
6
|
+
* components load). Auto-scroll stops once the user scrolls manually, and
|
|
7
|
+
* resets on dependency changes.
|
|
8
|
+
*/
|
|
9
|
+
export const useScrollToExampleCard = (relativePath: string | undefined, counter: number | undefined) => {
|
|
10
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const container = containerRef.current;
|
|
14
|
+
if (!relativePath || !container) return;
|
|
15
|
+
|
|
16
|
+
let scrolledManually = false;
|
|
17
|
+
let scrollingProgrammatically = false;
|
|
18
|
+
let animationFrameId = 0;
|
|
19
|
+
|
|
20
|
+
const handleScroll = () => {
|
|
21
|
+
if (scrollingProgrammatically) return;
|
|
22
|
+
scrolledManually = true;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
26
|
+
if (scrolledManually) return;
|
|
27
|
+
scrollingProgrammatically = true;
|
|
28
|
+
scrollExampleCardIntoView(relativePath);
|
|
29
|
+
cancelAnimationFrame(animationFrameId);
|
|
30
|
+
animationFrameId = requestAnimationFrame(() => {
|
|
31
|
+
scrollingProgrammatically = false;
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
window.addEventListener('scroll', handleScroll);
|
|
36
|
+
resizeObserver.observe(container);
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
window.removeEventListener('scroll', handleScroll);
|
|
40
|
+
resizeObserver.disconnect();
|
|
41
|
+
cancelAnimationFrame(animationFrameId);
|
|
42
|
+
};
|
|
43
|
+
}, [relativePath, counter]);
|
|
44
|
+
|
|
45
|
+
return containerRef;
|
|
46
|
+
};
|