@jbpark/ui-kit 2.9.7 → 2.10.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.
@@ -4,6 +4,7 @@ import { jsx } from "react/jsx-runtime";
4
4
  //#region src/components/organisms/list/item.tsx
5
5
  const Item = ({ children, className, ...props }) => {
6
6
  return /* @__PURE__ */ jsx("div", {
7
+ role: "listitem",
7
8
  className: cn(className),
8
9
  ...props,
9
10
  children
@@ -1 +1 @@
1
- {"version":3,"file":"item.mjs","names":[],"sources":["../../../../src/components/organisms/list/item.tsx"],"sourcesContent":["import { cn } from '@repo/ui/utils';\n\nconst Item = ({\n children,\n className,\n ...props\n}: React.ComponentPropsWithoutRef<'div'>) => {\n return (\n <div className={cn(className)} {...props}>\n {children}\n </div>\n );\n};\n\nexport default Item;\n"],"mappings":";;;;AAEA,MAAM,QAAQ,EACZ,UACA,WACA,GAAG,YACwC;AAC3C,QACE,oBAAC;EAAI,WAAW,GAAG,UAAU;EAAE,GAAI;EAChC;GACG"}
1
+ {"version":3,"file":"item.mjs","names":[],"sources":["../../../../src/components/organisms/list/item.tsx"],"sourcesContent":["import { cn } from '@repo/ui/utils';\n\nconst Item = ({\n children,\n className,\n ...props\n}: React.ComponentPropsWithoutRef<'div'>) => {\n return (\n <div role=\"listitem\" className={cn(className)} {...props}>\n {children}\n </div>\n );\n};\n\nexport default Item;\n"],"mappings":";;;;AAEA,MAAM,QAAQ,EACZ,UACA,WACA,GAAG,YACwC;AAC3C,QACE,oBAAC;EAAI,MAAK;EAAW,WAAW,GAAG,UAAU;EAAE,GAAI;EAChD;GACG"}
@@ -8,6 +8,7 @@ interface Props<T> extends Omit<React.ComponentPropsWithoutRef<'div'>, 'title'>
8
8
  loaderProps?: React.ComponentProps<typeof Skeleton>;
9
9
  loader?: React.ReactNode;
10
10
  title?: React.ReactNode;
11
+ titleLevel?: 1 | 2 | 3 | 4 | 5 | 6;
11
12
  header?: React.ReactNode;
12
13
  empty?: React.ReactNode;
13
14
  classNames?: {
@@ -24,6 +25,7 @@ interface Props<T> extends Omit<React.ComponentPropsWithoutRef<'div'>, 'title'>
24
25
  };
25
26
  data?: T[];
26
27
  renderItem?: (item: T, index: number) => React.ReactNode;
28
+ itemKey?: (item: T, index: number) => React.Key;
27
29
  }
28
30
  declare const List: <T>({
29
31
  loading,
@@ -31,12 +33,14 @@ declare const List: <T>({
31
33
  loader,
32
34
  header,
33
35
  title,
36
+ titleLevel,
34
37
  empty,
35
38
  scroll,
36
39
  data,
37
40
  className,
38
41
  classNames,
39
42
  renderItem,
43
+ itemKey,
40
44
  ...props
41
45
  }: Props<T>) => string | number | bigint | true | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | react_jsx_runtime0.JSX.Element;
42
46
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"list.d.mts","names":[],"sources":["../../../../src/components/organisms/list/list.tsx"],"mappings":";;;;;UAUU,KAAA,YAAiB,IAAA,CACzB,KAAA,CAAM,wBAAA;EAGN,OAAA;EACA,WAAA,GAAc,KAAA,CAAM,cAAA,QAAsB,QAAA;EAC1C,MAAA,GAAS,KAAA,CAAM,SAAA;EACf,KAAA,GAAQ,KAAA,CAAM,SAAA;EACd,MAAA,GAAS,KAAA,CAAM,SAAA;EACf,KAAA,GAAQ,KAAA,CAAM,SAAA;EACd,UAAA;IACE,KAAA;IACA,MAAA;IACA,IAAA;EAAA;EAEF,MAAA;IACE,OAAA;IACA,OAAA;IACA,MAAA,GAAS,KAAA,CAAM,SAAA;IACf,OAAA,GAAU,wBAAA;IACV,IAAA;EAAA;EAEF,IAAA,GAAO,CAAA;EACP,UAAA,IAAc,IAAA,EAAM,CAAA,EAAG,KAAA,aAAkB,KAAA,CAAM,SAAA;AAAA;AAAA,cAG3C,IAAA;EAAY,OAAA;EAAA,WAAA;EAAA,MAAA;EAAA,MAAA;EAAA,KAAA;EAAA,KAAA;EAAA,MAAA;EAAA,IAAA;EAAA,SAAA;EAAA,UAAA;EAAA,UAAA;EAAA,GAAA;AAAA,GAaf,KAAA,CAAM,CAAA,wCAAE,QAAA,CAAA,KAAA,CAAA,SAAA,IAAA,OAAA,sCAAA,KAAA,CAAA,WAAA,GAAA,KAAA,CAAA,YAAA,mBAAA,KAAA,CAAA,qBAAA,SAAA,QAAA,CAAA,KAAA,CAAA,SAAA,wBAAA,kBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"list.d.mts","names":[],"sources":["../../../../src/components/organisms/list/list.tsx"],"mappings":";;;;;UAWU,KAAA,YAAiB,IAAA,CACzB,KAAA,CAAM,wBAAA;EAGN,OAAA;EACA,WAAA,GAAc,KAAA,CAAM,cAAA,QAAsB,QAAA;EAC1C,MAAA,GAAS,KAAA,CAAM,SAAA;EACf,KAAA,GAAQ,KAAA,CAAM,SAAA;EACd,UAAA;EACA,MAAA,GAAS,KAAA,CAAM,SAAA;EACf,KAAA,GAAQ,KAAA,CAAM,SAAA;EACd,UAAA;IACE,KAAA;IACA,MAAA;IACA,IAAA;EAAA;EAEF,MAAA;IACE,OAAA;IACA,OAAA;IACA,MAAA,GAAS,KAAA,CAAM,SAAA;IACf,OAAA,GAAU,wBAAA;IACV,IAAA;EAAA;EAEF,IAAA,GAAO,CAAA;EACP,UAAA,IAAc,IAAA,EAAM,CAAA,EAAG,KAAA,aAAkB,KAAA,CAAM,SAAA;EAC/C,OAAA,IAAW,IAAA,EAAM,CAAA,EAAG,KAAA,aAAkB,KAAA,CAAM,GAAA;AAAA;AAAA,cAGxC,IAAA;EAAY,OAAA;EAAA,WAAA;EAAA,MAAA;EAAA,MAAA;EAAA,KAAA;EAAA,UAAA;EAAA,KAAA;EAAA,MAAA;EAAA,IAAA;EAAA,SAAA;EAAA,UAAA;EAAA,UAAA;EAAA,OAAA;EAAA,GAAA;AAAA,GAef,KAAA,CAAM,CAAA,wCAAE,QAAA,CAAA,KAAA,CAAA,SAAA,IAAA,OAAA,sCAAA,KAAA,CAAA,WAAA,GAAA,KAAA,CAAA,YAAA,mBAAA,KAAA,CAAA,qBAAA,SAAA,QAAA,CAAA,KAAA,CAAA,SAAA,wBAAA,kBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -1,22 +1,32 @@
1
1
  'use client';
2
2
 
3
3
  import { cn, renderConditional } from "../../../utils.mjs";
4
+ import Title from "../../atoms/typography/title.mjs";
5
+ import "../../../Typography.mjs";
4
6
  import Skeleton from "../../atoms/skeleton/index.mjs";
5
- import React, { useEffect } from "react";
7
+ import React, { useEffect, useRef } from "react";
6
8
  import { jsx, jsxs } from "react/jsx-runtime";
7
9
  import { useIntersectionObserver } from "@uidotdev/usehooks";
8
10
 
9
11
  //#region src/components/organisms/list/list.tsx
10
- const List = ({ loading, loaderProps, loader, header, title, empty, scroll, data, className, classNames, renderItem = () => null, ...props }) => {
12
+ const List = ({ loading, loaderProps, loader, header, title, titleLevel = 2, empty, scroll, data, className, classNames, renderItem = () => null, itemKey, ...props }) => {
11
13
  const [loaderRef, entry] = useIntersectionObserver({
12
14
  threshold: 0,
13
15
  root: null,
14
16
  rootMargin: "0px",
15
17
  ...scroll?.options
16
18
  });
19
+ const fetchingRef = useRef(false);
17
20
  useEffect(() => {
18
21
  if (!scroll) return;
19
- if (entry?.isIntersecting && scroll.hasMore && !scroll.loading) scroll.next();
22
+ if (scroll.loading) {
23
+ fetchingRef.current = false;
24
+ return;
25
+ }
26
+ if (entry?.isIntersecting && scroll.hasMore && !fetchingRef.current) {
27
+ fetchingRef.current = true;
28
+ scroll.next();
29
+ }
20
30
  }, [entry, scroll]);
21
31
  if (loading) return loader || /* @__PURE__ */ jsxs("div", {
22
32
  className: cn(className),
@@ -40,12 +50,17 @@ const List = ({ loading, loaderProps, loader, header, title, empty, scroll, data
40
50
  })
41
51
  ]
42
52
  });
43
- if (!data?.length && empty) return empty;
53
+ if (!data?.length && empty) return /* @__PURE__ */ jsx("div", {
54
+ className: cn(className),
55
+ ...props,
56
+ children: empty
57
+ });
44
58
  return /* @__PURE__ */ jsxs("div", {
45
59
  className: cn(className),
46
60
  ...props,
47
61
  children: [
48
- renderConditional(title, (v) => /* @__PURE__ */ jsx("h2", {
62
+ renderConditional(title, (v) => /* @__PURE__ */ jsx(Title, {
63
+ level: titleLevel,
49
64
  className: cn("px-1.5 py-4", "text-black-90 text-lg leading-5.75 font-bold"),
50
65
  children: v
51
66
  })),
@@ -54,15 +69,16 @@ const List = ({ loading, loaderProps, loader, header, title, empty, scroll, data
54
69
  children: header
55
70
  }),
56
71
  /* @__PURE__ */ jsxs("div", {
72
+ role: "list",
57
73
  className: cn("space-y-2", classNames?.body),
58
74
  children: [
59
- data?.map((item, i) => renderItem(item, i)),
75
+ data?.map((item, i) => itemKey ? /* @__PURE__ */ jsx(React.Fragment, { children: renderItem(item, i) }, itemKey(item, i)) : renderItem(item, i)),
60
76
  scroll?.loading && (scroll?.loader ?? /* @__PURE__ */ jsx(Skeleton.Node, {
61
77
  count: 10,
62
78
  gap: 10,
63
79
  ...loaderProps
64
80
  })),
65
- /* @__PURE__ */ jsx("div", { ref: loaderRef })
81
+ scroll && /* @__PURE__ */ jsx("div", { ref: loaderRef })
66
82
  ]
67
83
  })
68
84
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"list.mjs","names":[],"sources":["../../../../src/components/organisms/list/list.tsx"],"sourcesContent":["'use client';\n\nimport React, { useEffect } from 'react';\n\nimport { useIntersectionObserver } from '@uidotdev/usehooks';\n\nimport { cn, renderConditional } from '@repo/ui/utils';\n\nimport Skeleton from '../../atoms/skeleton';\n\ninterface Props<T> extends Omit<\n React.ComponentPropsWithoutRef<'div'>,\n 'title'\n> {\n loading?: boolean;\n loaderProps?: React.ComponentProps<typeof Skeleton>;\n loader?: React.ReactNode;\n title?: React.ReactNode;\n header?: React.ReactNode;\n empty?: React.ReactNode;\n classNames?: {\n title?: string;\n header?: string;\n body?: string;\n };\n scroll?: {\n hasMore: boolean;\n loading: boolean;\n loader?: React.ReactNode;\n options?: IntersectionObserverInit;\n next: () => void;\n };\n data?: T[];\n renderItem?: (item: T, index: number) => React.ReactNode;\n}\n\nconst List = <T,>({\n loading,\n loaderProps,\n loader,\n header,\n title,\n empty,\n scroll,\n data,\n className,\n classNames,\n renderItem = () => null,\n ...props\n}: Props<T>) => {\n const [loaderRef, entry] = useIntersectionObserver({\n threshold: 0,\n root: null,\n rootMargin: '0px',\n ...scroll?.options,\n });\n\n useEffect(() => {\n if (!scroll) {\n return;\n }\n\n if (entry?.isIntersecting && scroll.hasMore && !scroll.loading) {\n scroll.next();\n }\n }, [entry, scroll]);\n\n if (loading) {\n return (\n loader || (\n <div className={cn(className)} {...props}>\n <div className={cn(classNames?.title)}>{title && <Skeleton />}</div>\n <div className={cn(classNames?.header)}>\n {header && <Skeleton size=\"small\" />}\n </div>\n <div className={cn('space-y-2', classNames?.body)}>\n <Skeleton.Node count={10} gap={10} {...loaderProps} />\n </div>\n </div>\n )\n );\n }\n\n if (!data?.length && empty) {\n return empty;\n }\n\n return (\n <div\n className={cn(\n className,\n //\n )}\n {...props}\n >\n {renderConditional(title, v => (\n <h2\n className={cn(\n 'px-1.5 py-4',\n 'text-black-90 text-lg leading-5.75 font-bold',\n //\n )}\n >\n {v}\n </h2>\n ))}\n <div className={cn(classNames?.header)}>{header}</div>\n <div className={cn('space-y-2', classNames?.body)}>\n {data?.map((item, i) => renderItem(item, i))}\n {scroll?.loading &&\n (scroll?.loader ?? (\n <Skeleton.Node count={10} gap={10} {...loaderProps} />\n ))}\n <div ref={loaderRef} />\n </div>\n </div>\n );\n};\n\nexport default List;\n"],"mappings":";;;;;;;;;AAoCA,MAAM,QAAY,EAChB,SACA,aACA,QACA,QACA,OACA,OACA,QACA,MACA,WACA,YACA,mBAAmB,MACnB,GAAG,YACW;CACd,MAAM,CAAC,WAAW,SAAS,wBAAwB;EACjD,WAAW;EACX,MAAM;EACN,YAAY;EACZ,GAAG,QAAQ;EACZ,CAAC;AAEF,iBAAgB;AACd,MAAI,CAAC,OACH;AAGF,MAAI,OAAO,kBAAkB,OAAO,WAAW,CAAC,OAAO,QACrD,QAAO,MAAM;IAEd,CAAC,OAAO,OAAO,CAAC;AAEnB,KAAI,QACF,QACE,UACE,qBAAC;EAAI,WAAW,GAAG,UAAU;EAAE,GAAI;;GACjC,oBAAC;IAAI,WAAW,GAAG,YAAY,MAAM;cAAG,SAAS,oBAAC,aAAW;KAAO;GACpE,oBAAC;IAAI,WAAW,GAAG,YAAY,OAAO;cACnC,UAAU,oBAAC,YAAS,MAAK,UAAU;KAChC;GACN,oBAAC;IAAI,WAAW,GAAG,aAAa,YAAY,KAAK;cAC/C,oBAAC,SAAS;KAAK,OAAO;KAAI,KAAK;KAAI,GAAI;MAAe;KAClD;;GACF;AAKZ,KAAI,CAAC,MAAM,UAAU,MACnB,QAAO;AAGT,QACE,qBAAC;EACC,WAAW,GACT,UAED;EACD,GAAI;;GAEH,kBAAkB,QAAO,MACxB,oBAAC;IACC,WAAW,GACT,eACA,+CAED;cAEA;KACE,CACL;GACF,oBAAC;IAAI,WAAW,GAAG,YAAY,OAAO;cAAG;KAAa;GACtD,qBAAC;IAAI,WAAW,GAAG,aAAa,YAAY,KAAK;;KAC9C,MAAM,KAAK,MAAM,MAAM,WAAW,MAAM,EAAE,CAAC;KAC3C,QAAQ,YACN,QAAQ,UACP,oBAAC,SAAS;MAAK,OAAO;MAAI,KAAK;MAAI,GAAI;OAAe;KAE1D,oBAAC,SAAI,KAAK,YAAa;;KACnB;;GACF"}
1
+ {"version":3,"file":"list.mjs","names":[],"sources":["../../../../src/components/organisms/list/list.tsx"],"sourcesContent":["'use client';\n\nimport React, { useEffect, useRef } from 'react';\n\nimport { useIntersectionObserver } from '@uidotdev/usehooks';\n\nimport { cn, renderConditional } from '@repo/ui/utils';\n\nimport Skeleton from '../../atoms/skeleton';\nimport { Title } from '../../atoms/typography';\n\ninterface Props<T> extends Omit<\n React.ComponentPropsWithoutRef<'div'>,\n 'title'\n> {\n loading?: boolean;\n loaderProps?: React.ComponentProps<typeof Skeleton>;\n loader?: React.ReactNode;\n title?: React.ReactNode;\n titleLevel?: 1 | 2 | 3 | 4 | 5 | 6;\n header?: React.ReactNode;\n empty?: React.ReactNode;\n classNames?: {\n title?: string;\n header?: string;\n body?: string;\n };\n scroll?: {\n hasMore: boolean;\n loading: boolean;\n loader?: React.ReactNode;\n options?: IntersectionObserverInit;\n next: () => void;\n };\n data?: T[];\n renderItem?: (item: T, index: number) => React.ReactNode;\n itemKey?: (item: T, index: number) => React.Key;\n}\n\nconst List = <T,>({\n loading,\n loaderProps,\n loader,\n header,\n title,\n titleLevel = 2,\n empty,\n scroll,\n data,\n className,\n classNames,\n renderItem = () => null,\n itemKey,\n ...props\n}: Props<T>) => {\n const [loaderRef, entry] = useIntersectionObserver({\n threshold: 0,\n root: null,\n rootMargin: '0px',\n ...scroll?.options,\n });\n\n // `scroll` is typically a new object identity every render, so this\n // effect re-runs often. Guard with a ref (updated synchronously, unlike\n // the `loading` prop which only reflects the caller's next state update)\n // to avoid firing `next()` again for the same in-flight request before\n // the caller's `loading` has had a chance to become true.\n const fetchingRef = useRef(false);\n\n useEffect(() => {\n if (!scroll) {\n return;\n }\n\n if (scroll.loading) {\n fetchingRef.current = false;\n return;\n }\n\n if (entry?.isIntersecting && scroll.hasMore && !fetchingRef.current) {\n fetchingRef.current = true;\n scroll.next();\n }\n }, [entry, scroll]);\n\n if (loading) {\n return (\n loader || (\n <div className={cn(className)} {...props}>\n <div className={cn(classNames?.title)}>{title && <Skeleton />}</div>\n <div className={cn(classNames?.header)}>\n {header && <Skeleton size=\"small\" />}\n </div>\n <div className={cn('space-y-2', classNames?.body)}>\n <Skeleton.Node count={10} gap={10} {...loaderProps} />\n </div>\n </div>\n )\n );\n }\n\n if (!data?.length && empty) {\n return (\n <div className={cn(className)} {...props}>\n {empty}\n </div>\n );\n }\n\n return (\n <div\n className={cn(\n className,\n //\n )}\n {...props}\n >\n {renderConditional(title, v => (\n <Title\n level={titleLevel}\n className={cn(\n 'px-1.5 py-4',\n 'text-black-90 text-lg leading-5.75 font-bold',\n //\n )}\n >\n {v}\n </Title>\n ))}\n <div className={cn(classNames?.header)}>{header}</div>\n <div role=\"list\" className={cn('space-y-2', classNames?.body)}>\n {data?.map((item, i) =>\n itemKey ? (\n <React.Fragment key={itemKey(item, i)}>\n {renderItem(item, i)}\n </React.Fragment>\n ) : (\n renderItem(item, i)\n ),\n )}\n {scroll?.loading &&\n (scroll?.loader ?? (\n <Skeleton.Node count={10} gap={10} {...loaderProps} />\n ))}\n {scroll && <div ref={loaderRef} />}\n </div>\n </div>\n );\n};\n\nexport default List;\n"],"mappings":";;;;;;;;;;;AAuCA,MAAM,QAAY,EAChB,SACA,aACA,QACA,QACA,OACA,aAAa,GACb,OACA,QACA,MACA,WACA,YACA,mBAAmB,MACnB,SACA,GAAG,YACW;CACd,MAAM,CAAC,WAAW,SAAS,wBAAwB;EACjD,WAAW;EACX,MAAM;EACN,YAAY;EACZ,GAAG,QAAQ;EACZ,CAAC;CAOF,MAAM,cAAc,OAAO,MAAM;AAEjC,iBAAgB;AACd,MAAI,CAAC,OACH;AAGF,MAAI,OAAO,SAAS;AAClB,eAAY,UAAU;AACtB;;AAGF,MAAI,OAAO,kBAAkB,OAAO,WAAW,CAAC,YAAY,SAAS;AACnE,eAAY,UAAU;AACtB,UAAO,MAAM;;IAEd,CAAC,OAAO,OAAO,CAAC;AAEnB,KAAI,QACF,QACE,UACE,qBAAC;EAAI,WAAW,GAAG,UAAU;EAAE,GAAI;;GACjC,oBAAC;IAAI,WAAW,GAAG,YAAY,MAAM;cAAG,SAAS,oBAAC,aAAW;KAAO;GACpE,oBAAC;IAAI,WAAW,GAAG,YAAY,OAAO;cACnC,UAAU,oBAAC,YAAS,MAAK,UAAU;KAChC;GACN,oBAAC;IAAI,WAAW,GAAG,aAAa,YAAY,KAAK;cAC/C,oBAAC,SAAS;KAAK,OAAO;KAAI,KAAK;KAAI,GAAI;MAAe;KAClD;;GACF;AAKZ,KAAI,CAAC,MAAM,UAAU,MACnB,QACE,oBAAC;EAAI,WAAW,GAAG,UAAU;EAAE,GAAI;YAChC;GACG;AAIV,QACE,qBAAC;EACC,WAAW,GACT,UAED;EACD,GAAI;;GAEH,kBAAkB,QAAO,MACxB,oBAAC;IACC,OAAO;IACP,WAAW,GACT,eACA,+CAED;cAEA;KACK,CACR;GACF,oBAAC;IAAI,WAAW,GAAG,YAAY,OAAO;cAAG;KAAa;GACtD,qBAAC;IAAI,MAAK;IAAO,WAAW,GAAG,aAAa,YAAY,KAAK;;KAC1D,MAAM,KAAK,MAAM,MAChB,UACE,oBAAC,MAAM,sBACJ,WAAW,MAAM,EAAE,IADD,QAAQ,MAAM,EAAE,CAEpB,GAEjB,WAAW,MAAM,EAAE,CAEtB;KACA,QAAQ,YACN,QAAQ,UACP,oBAAC,SAAS;MAAK,OAAO;MAAI,KAAK;MAAI,GAAI;OAAe;KAEzD,UAAU,oBAAC,SAAI,KAAK,YAAa;;KAC9B;;GACF"}
@@ -9,7 +9,7 @@ declare namespace button_d_exports {
9
9
  }
10
10
  declare const buttonVariants: (props?: ({
11
11
  variant?: "link" | "default" | "secondary" | "destructive" | "outline" | "ghost" | null | undefined;
12
- size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
12
+ size?: "default" | "icon" | "xs" | "sm" | "lg" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
13
13
  } & class_variance_authority_types0.ClassProp) | undefined) => string;
14
14
  type Props = React$1.ComponentProps<'button'> & VariantProps<typeof buttonVariants> & {
15
15
  asChild?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbpark/ui-kit",
3
- "version": "2.9.7",
3
+ "version": "2.10.0",
4
4
  "description": "Modern React UI component library built with TypeScript, Tailwind CSS, and Radix UI. Featuring atoms, molecules, organisms and layout templates for building beautiful interfaces.",
5
5
  "keywords": [
6
6
  "react",