@qkix/strapi-plugin-chartkit 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/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/_chunks/ChartField-CKCDQc6g.js +1 -0
- package/dist/_chunks/ChartField-N1BDNZ3c.mjs +86 -0
- package/dist/_chunks/en-BcPniuRQ.js +1 -0
- package/dist/_chunks/en-DYxpcVy9.mjs +4 -0
- package/dist/admin/index.js +1 -0
- package/dist/admin/index.mjs +163 -0
- package/dist/admin/src/components/ChartField.d.ts +39 -0
- package/dist/admin/src/components/Initializer.d.ts +5 -0
- package/dist/admin/src/components/PluginIcon.d.ts +9 -0
- package/dist/admin/src/index.d.ts +19 -0
- package/dist/admin/src/pluginId.d.ts +9 -0
- package/dist/admin/src/value.d.ts +57 -0
- package/dist/server/index.js +1 -0
- package/dist/server/index.mjs +18 -0
- package/dist/server/src/index.d.ts +14 -0
- package/dist/server/src/register.d.ts +17 -0
- package/package.json +110 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 qkix
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# @qkix/strapi-plugin-chartkit
|
|
2
|
+
|
|
3
|
+
Charts for Strapi v5, as a custom field. The chart is rendered to **SVG on the
|
|
4
|
+
server**, so a page carrying twenty of them ships no charting library and runs
|
|
5
|
+
no JavaScript to draw them.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @qkix/strapi-plugin-chartkit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Add a **Chart** field to a content type, and the edit view gives the author a
|
|
12
|
+
data grid, spreadsheet paste, and a live preview.
|
|
13
|
+
|
|
14
|
+
## What it stores
|
|
15
|
+
|
|
16
|
+
A `ChartSpec` — the same object the renderers take. There is nothing in between
|
|
17
|
+
the field and the page:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { Chart } from '@qkix/chartkit-react-renderer';
|
|
21
|
+
|
|
22
|
+
const article = await fetch('/api/articles/1').then((r) => r.json());
|
|
23
|
+
|
|
24
|
+
<Chart spec={article.data.chart} locale="en-US" />;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Astro is the same shape:
|
|
28
|
+
|
|
29
|
+
```astro
|
|
30
|
+
---
|
|
31
|
+
import { Chart } from '@qkix/chartkit-astro-renderer';
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
<Chart spec={article.chart} locale="en-US" />
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Both render to markup at build or request time. Neither sends a runtime.
|
|
38
|
+
|
|
39
|
+
## Styling
|
|
40
|
+
|
|
41
|
+
The chart carries no colors of its own. Series are drawn as
|
|
42
|
+
`fill="var(--chart-series-N, currentColor)"` and text inherits the page's font,
|
|
43
|
+
so a chart takes the theme of wherever it lands — including class-based dark
|
|
44
|
+
mode, which is the reason for hand-written SVG rather than an off-the-shelf SSR
|
|
45
|
+
renderer that bakes hex values into the markup.
|
|
46
|
+
|
|
47
|
+
```css
|
|
48
|
+
:root {
|
|
49
|
+
--chart-series-1: #4945ff;
|
|
50
|
+
--chart-series-2: #66b7f1;
|
|
51
|
+
--chart-axis: #8e8ea9;
|
|
52
|
+
--chart-text: #32324a;
|
|
53
|
+
--chart-grid: #eaeaef;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The admin preview is deliberately drawn on white with dark text regardless of
|
|
58
|
+
the admin's theme, because it is showing what the chart will look like on a page
|
|
59
|
+
this plugin knows nothing about.
|
|
60
|
+
|
|
61
|
+
## Field options
|
|
62
|
+
|
|
63
|
+
| Option | Effect |
|
|
64
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
65
|
+
| **Number formatting locale** | How axis numbers are formatted **in the admin preview** — a BCP 47 tag such as `de-DE`. The front end passes its own at render. |
|
|
66
|
+
| **Required** | Standard Strapi validation. |
|
|
67
|
+
|
|
68
|
+
## Three ways to get data in
|
|
69
|
+
|
|
70
|
+
The grid is there to type into, but most charts already exist as numbers
|
|
71
|
+
somewhere:
|
|
72
|
+
|
|
73
|
+
- **Paste from a spreadsheet** — copy a range out of Sheets or Excel and paste.
|
|
74
|
+
Tab, comma and semicolon are all detected; `1.234,5` and `1,234.5` both read
|
|
75
|
+
as 1234.5; `n/a` and blanks become **gaps, not zeroes**.
|
|
76
|
+
- **From the Media Library** — pick a `.csv`, `.tsv` or `.txt` already uploaded.
|
|
77
|
+
- **By hand** — add categories and series in the grid.
|
|
78
|
+
|
|
79
|
+
Paste and import both show **what was parsed before replacing anything**. The
|
|
80
|
+
parser has to guess whether the first row is a header, and a wrong guess that
|
|
81
|
+
silently overwrites an author's data is far worse than one they can cancel.
|
|
82
|
+
|
|
83
|
+
## The editor opens in a dialog
|
|
84
|
+
|
|
85
|
+
Rather than sitting inline. The editing surface is a preview plus a grid plus a
|
|
86
|
+
row of settings — most of a screen, given to a field the author is usually
|
|
87
|
+
scrolling past on their way to the rest of the form. Clicking the preview or
|
|
88
|
+
**Edit chart** opens it; **Cancel** genuinely cancels, because the dialog edits a
|
|
89
|
+
draft and only writes on save.
|
|
90
|
+
|
|
91
|
+
A field nobody has filled in shows an empty state rather than writing a starter
|
|
92
|
+
chart into the document. Opening an entry must not be enough to mark it dirty.
|
|
93
|
+
|
|
94
|
+
## Old charts, and charts that are not charts
|
|
95
|
+
|
|
96
|
+
The field **migrates a spec on the way in**, so an author who opens a chart saved
|
|
97
|
+
against an older schema edits and saves the current shape. Pages stay correct in
|
|
98
|
+
the meantime on their own: `renderChart` migrates in memory too, so nothing has
|
|
99
|
+
to be resaved for a site to keep working.
|
|
100
|
+
|
|
101
|
+
If the field holds something that is not a readable chart — a value written by an
|
|
102
|
+
import script, or a field that used to be a different type — it says so and goes
|
|
103
|
+
**read-only**. Showing the usual "create a chart" prompt over data that exists
|
|
104
|
+
would be an offer to overwrite it.
|
|
105
|
+
|
|
106
|
+
## Also available as a block
|
|
107
|
+
|
|
108
|
+
Chartkit is two surfaces over one editor. If you use
|
|
109
|
+
[Better Blocks](https://www.npmjs.com/package/@qkix/strapi-plugin-better-blocks),
|
|
110
|
+
charts can live inside a rich-text document as a block instead of, or as well
|
|
111
|
+
as, in a field of their own — see `@qkix/chartkit-core`'s `chartBlock` and the
|
|
112
|
+
`registerBlock` API.
|
|
113
|
+
|
|
114
|
+
## Packages
|
|
115
|
+
|
|
116
|
+
| Package | For |
|
|
117
|
+
| ---------------------------------------------------------------------------------------------- | -------------------------------------------- |
|
|
118
|
+
| [`@qkix/strapi-plugin-chartkit`](https://www.npmjs.com/package/@qkix/strapi-plugin-chartkit) | this plugin — the field in the Strapi admin |
|
|
119
|
+
| [`@qkix/chartkit-core`](https://www.npmjs.com/package/@qkix/chartkit-core) | the spec, the validator and the SVG renderer |
|
|
120
|
+
| [`@qkix/chartkit-react-renderer`](https://www.npmjs.com/package/@qkix/chartkit-react-renderer) | rendering a stored chart in React / Next.js |
|
|
121
|
+
| [`@qkix/chartkit-astro-renderer`](https://www.npmjs.com/package/@qkix/chartkit-astro-renderer) | rendering a stored chart in Astro |
|
|
122
|
+
| [`@qkix/chartkit-editor`](https://www.npmjs.com/package/@qkix/chartkit-editor) | the editing surface, if you are embedding it |
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react/jsx-runtime"),t=require("@strapi/design-system"),g=require("@qkix/chartkit-editor"),S=require("react"),u=require("../admin/index.js");function F(r){if(r&&r.__esModule)return r;const i=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(r){for(const a in r)if(a!=="default"){const o=Object.getOwnPropertyDescriptor(r,a);Object.defineProperty(i,a,o.get?o:{enumerable:!0,get:()=>r[a]})}}return i.default=r,Object.freeze(i)}const c=F(S),x=c.forwardRef((r,i)=>{const{name:a,value:o,onChange:j,disabled:s,error:p,hint:y,label:l,required:f}=r,[b,d]=c.useState(!1),n=c.useMemo(()=>u.readValue(o),[o]),h=r.attribute?.options?.previewLocale||void 0,m=k=>{j({target:{name:a,type:"json",value:u.writeValue(k)}})},v=c.useMemo(()=>u.starterSpec(),[]),C=n.status==="ok"?n.spec:v;return e.jsxs(e.Fragment,{children:[e.jsx(t.Field.Root,{id:a,name:a,required:f,error:p,hint:y,children:e.jsxs(t.Flex,{direction:"column",alignItems:"stretch",gap:1,ref:i,children:[e.jsx(t.Field.Label,{children:l}),n.status==="ok"&&e.jsx(g.ChartPreview,{spec:n.spec,locale:h,idPrefix:`chartkit-${a}`,onClick:s?void 0:()=>d(!0)}),n.status==="empty"&&e.jsx(E,{disabled:s,hasError:!!p,onCreate:()=>d(!0)}),n.status==="unreadable"&&e.jsxs(t.Box,{padding:4,background:"danger100",hasRadius:!0,children:[e.jsx(t.Typography,{variant:"pi",fontWeight:"bold",textColor:"danger600",tag:"p",children:n.reason}),e.jsx(t.Box,{paddingTop:2,children:e.jsx(t.Typography,{variant:"pi",textColor:"danger600",children:"Editing here would replace it, so the field is read-only until the value is fixed or cleared."})})]}),n.status==="ok"&&e.jsx(t.Flex,{paddingTop:2,gap:2,children:e.jsx(t.Button,{variant:"secondary",size:"S",disabled:s,onClick:()=>d(!0),children:"Edit chart"})}),e.jsx(t.Field.Hint,{}),e.jsx(t.Field.Error,{})]})}),n.status!=="unreadable"&&e.jsx(g.ChartEditorDialog,{spec:C,open:b,onOpenChange:d,onSave:m,locale:h,disabled:s,title:l?`Edit ${l}`:"Edit chart"})]})});x.displayName="ChartField";function E({disabled:r,hasError:i,onCreate:a}){return e.jsxs(t.Flex,{direction:"column",gap:2,padding:6,hasRadius:!0,justifyContent:"center",background:"neutral100",style:{border:`1px dashed ${i?"#d02b20":"#dcdce4"}`},children:[e.jsx(t.Typography,{variant:"pi",textColor:"neutral600",children:"No chart yet."}),e.jsx(t.Button,{variant:"secondary",size:"S",disabled:r,onClick:a,children:"Create chart"})]})}exports.default=x;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { jsxs as s, Fragment as F, jsx as e } from "react/jsx-runtime";
|
|
2
|
+
import { Field as o, Flex as u, Box as f, Typography as h, Button as C } from "@strapi/design-system";
|
|
3
|
+
import { ChartPreview as R, ChartEditorDialog as j } from "@qkix/chartkit-editor";
|
|
4
|
+
import * as d from "react";
|
|
5
|
+
import { readValue as B, starterSpec as T, writeValue as $ } from "../admin/index.mjs";
|
|
6
|
+
const z = d.forwardRef((a, l) => {
|
|
7
|
+
const { name: r, value: p, onChange: v, disabled: i, error: g, hint: y, label: c, required: b } = a, [x, n] = d.useState(!1), t = d.useMemo(() => B(p), [p]), m = a.attribute?.options?.previewLocale || void 0, k = (w) => {
|
|
8
|
+
v({ target: { name: r, type: "json", value: $(w) } });
|
|
9
|
+
}, E = d.useMemo(() => T(), []), S = t.status === "ok" ? t.spec : E;
|
|
10
|
+
return /* @__PURE__ */ s(F, { children: [
|
|
11
|
+
/* @__PURE__ */ e(o.Root, { id: r, name: r, required: b, error: g, hint: y, children: /* @__PURE__ */ s(u, { direction: "column", alignItems: "stretch", gap: 1, ref: l, children: [
|
|
12
|
+
/* @__PURE__ */ e(o.Label, { children: c }),
|
|
13
|
+
t.status === "ok" && /* @__PURE__ */ e(
|
|
14
|
+
R,
|
|
15
|
+
{
|
|
16
|
+
spec: t.spec,
|
|
17
|
+
locale: m,
|
|
18
|
+
idPrefix: `chartkit-${r}`,
|
|
19
|
+
onClick: i ? void 0 : () => n(!0)
|
|
20
|
+
}
|
|
21
|
+
),
|
|
22
|
+
t.status === "empty" && /* @__PURE__ */ e(
|
|
23
|
+
L,
|
|
24
|
+
{
|
|
25
|
+
disabled: i,
|
|
26
|
+
hasError: !!g,
|
|
27
|
+
onCreate: () => n(!0)
|
|
28
|
+
}
|
|
29
|
+
),
|
|
30
|
+
t.status === "unreadable" && /* @__PURE__ */ s(f, { padding: 4, background: "danger100", hasRadius: !0, children: [
|
|
31
|
+
/* @__PURE__ */ e(h, { variant: "pi", fontWeight: "bold", textColor: "danger600", tag: "p", children: t.reason }),
|
|
32
|
+
/* @__PURE__ */ e(f, { paddingTop: 2, children: /* @__PURE__ */ e(h, { variant: "pi", textColor: "danger600", children: "Editing here would replace it, so the field is read-only until the value is fixed or cleared." }) })
|
|
33
|
+
] }),
|
|
34
|
+
t.status === "ok" && /* @__PURE__ */ e(u, { paddingTop: 2, gap: 2, children: /* @__PURE__ */ e(
|
|
35
|
+
C,
|
|
36
|
+
{
|
|
37
|
+
variant: "secondary",
|
|
38
|
+
size: "S",
|
|
39
|
+
disabled: i,
|
|
40
|
+
onClick: () => n(!0),
|
|
41
|
+
children: "Edit chart"
|
|
42
|
+
}
|
|
43
|
+
) }),
|
|
44
|
+
/* @__PURE__ */ e(o.Hint, {}),
|
|
45
|
+
/* @__PURE__ */ e(o.Error, {})
|
|
46
|
+
] }) }),
|
|
47
|
+
t.status !== "unreadable" && /* @__PURE__ */ e(
|
|
48
|
+
j,
|
|
49
|
+
{
|
|
50
|
+
spec: S,
|
|
51
|
+
open: x,
|
|
52
|
+
onOpenChange: n,
|
|
53
|
+
onSave: k,
|
|
54
|
+
locale: m,
|
|
55
|
+
disabled: i,
|
|
56
|
+
title: c ? `Edit ${c}` : "Edit chart"
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
] });
|
|
60
|
+
});
|
|
61
|
+
z.displayName = "ChartField";
|
|
62
|
+
function L({
|
|
63
|
+
disabled: a,
|
|
64
|
+
hasError: l,
|
|
65
|
+
onCreate: r
|
|
66
|
+
}) {
|
|
67
|
+
return /* @__PURE__ */ s(
|
|
68
|
+
u,
|
|
69
|
+
{
|
|
70
|
+
direction: "column",
|
|
71
|
+
gap: 2,
|
|
72
|
+
padding: 6,
|
|
73
|
+
hasRadius: !0,
|
|
74
|
+
justifyContent: "center",
|
|
75
|
+
background: "neutral100",
|
|
76
|
+
style: { border: `1px dashed ${l ? "#d02b20" : "#dcdce4"}` },
|
|
77
|
+
children: [
|
|
78
|
+
/* @__PURE__ */ e(h, { variant: "pi", textColor: "neutral600", children: "No chart yet." }),
|
|
79
|
+
/* @__PURE__ */ e(C, { variant: "secondary", size: "S", disabled: a, onClick: r, children: "Create chart" })
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
export {
|
|
85
|
+
z as default
|
|
86
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e={};exports.default=e;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const a=require("react"),n=require("react/jsx-runtime"),o=require("@qkix/chartkit-core"),c=(e,t,i)=>{const s=e[t];return s?typeof s=="function"?s():Promise.resolve(s):new Promise((y,u)=>{(typeof queueMicrotask=="function"?queueMicrotask:setTimeout)(u.bind(null,new Error("Unknown variable dynamic import: "+t+(t.split("/").length!==i?". Note that variables only represent file names one level deep.":""))))})},r="chartkit",l="chart",d=({setPlugin:e})=>{const t=a.useRef(e);return a.useEffect(()=>{t.current(r)},[]),null},f=()=>n.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"none","aria-hidden":"true",children:[n.jsx("path",{d:"M2 14V2",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round"}),n.jsx("path",{d:"M2 14h12",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round"}),n.jsx("rect",{x:"4",y:"7",width:"2.5",height:"5",fill:"currentColor"}),n.jsx("rect",{x:"8",y:"4",width:"2.5",height:"8",fill:"currentColor"}),n.jsx("rect",{x:"12",y:"9",width:"2.5",height:"3",fill:"currentColor"})]});function p(e){if(e==null||e==="")return{status:"empty"};let t=e;if(typeof e=="string"){try{t=JSON.parse(e)}catch{return{status:"unreadable",reason:"This field contains text that is not JSON."}}if(t===null||t==="")return{status:"empty"}}if(typeof t!="object"||t===null||Array.isArray(t))return{status:"unreadable",reason:"This field contains JSON that is not a chart."};const i=o.migrateChartSpec(t);return i.status==="skipped"?{status:"unreadable",reason:`Chartkit cannot read what is in this field: ${i.reason}.`}:{status:"ok",spec:i.spec}}function h(e){return JSON.stringify(e)}function m(){return{version:o.CHART_SPEC_VERSION,type:"bar",data:{source:"inline",labels:["Q1","Q2","Q3","Q4"],series:[{name:"Series 1",values:[null,null,null,null]}]}}}const g={register(e){e.registerPlugin({id:r,name:r,initializer:d,isReady:!1}),e.customFields.register({name:l,pluginId:r,type:"json",icon:f,intlLabel:{id:`${r}.field.label`,defaultMessage:"Chart"},intlDescription:{id:`${r}.field.description`,defaultMessage:"A chart, rendered as SVG with no client-side JavaScript"},components:{Input:async()=>({default:(await Promise.resolve().then(()=>require("../_chunks/ChartField-CKCDQc6g.js"))).default})},options:{base:[{sectionTitle:{id:`${r}.section.preview`,defaultMessage:"Preview"},items:[{name:"options.previewLocale",type:"text",placeholder:{id:`${r}.previewLocale.placeholder`,defaultMessage:"en-US"},intlLabel:{id:`${r}.previewLocale`,defaultMessage:"Number formatting locale"},description:{id:`${r}.previewLocale.description`,defaultMessage:"How axis numbers are formatted in the admin preview — a BCP 47 tag such as de-DE or pl-PL. Affects the preview only; the front end passes its own locale when it renders. Left empty, the browser's is used."}}]}],advanced:[{sectionTitle:null,items:[{name:"required",type:"checkbox",intlLabel:{id:`${r}.required`,defaultMessage:"Required field"},description:{id:`${r}.required.description`,defaultMessage:"You won't be able to create an entry if this field is empty"}}]}]}})},async registerTrads({locales:e}){return Promise.all(e.map(async t=>{try{const{default:i}=await c(Object.assign({"./translations/en.json":()=>Promise.resolve().then(()=>require("../_chunks/en-BcPniuRQ.js"))}),`./translations/${t}.json`,3);return{data:i,locale:t}}catch{return{data:{},locale:t}}}))}};exports.FIELD_NAME=l;exports.PLUGIN_ID=r;exports.default=g;exports.readValue=p;exports.starterSpec=m;exports.writeValue=h;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useRef as o, useEffect as l } from "react";
|
|
2
|
+
import { jsxs as d, jsx as n } from "react/jsx-runtime";
|
|
3
|
+
import { migrateChartSpec as u, CHART_SPEC_VERSION as c } from "@qkix/chartkit-core";
|
|
4
|
+
const f = (e, t, i) => {
|
|
5
|
+
const s = e[t];
|
|
6
|
+
return s ? typeof s == "function" ? s() : Promise.resolve(s) : new Promise((y, a) => {
|
|
7
|
+
(typeof queueMicrotask == "function" ? queueMicrotask : setTimeout)(
|
|
8
|
+
a.bind(
|
|
9
|
+
null,
|
|
10
|
+
new Error(
|
|
11
|
+
"Unknown variable dynamic import: " + t + (t.split("/").length !== i ? ". Note that variables only represent file names one level deep." : "")
|
|
12
|
+
)
|
|
13
|
+
)
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
}, r = "chartkit", p = "chart", h = ({ setPlugin: e }) => {
|
|
17
|
+
const t = o(e);
|
|
18
|
+
return l(() => {
|
|
19
|
+
t.current(r);
|
|
20
|
+
}, []), null;
|
|
21
|
+
}, m = () => /* @__PURE__ */ d("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [
|
|
22
|
+
/* @__PURE__ */ n("path", { d: "M2 14V2", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }),
|
|
23
|
+
/* @__PURE__ */ n("path", { d: "M2 14h12", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }),
|
|
24
|
+
/* @__PURE__ */ n("rect", { x: "4", y: "7", width: "2.5", height: "5", fill: "currentColor" }),
|
|
25
|
+
/* @__PURE__ */ n("rect", { x: "8", y: "4", width: "2.5", height: "8", fill: "currentColor" }),
|
|
26
|
+
/* @__PURE__ */ n("rect", { x: "12", y: "9", width: "2.5", height: "3", fill: "currentColor" })
|
|
27
|
+
] });
|
|
28
|
+
function v(e) {
|
|
29
|
+
if (e == null || e === "") return { status: "empty" };
|
|
30
|
+
let t = e;
|
|
31
|
+
if (typeof e == "string") {
|
|
32
|
+
try {
|
|
33
|
+
t = JSON.parse(e);
|
|
34
|
+
} catch {
|
|
35
|
+
return {
|
|
36
|
+
status: "unreadable",
|
|
37
|
+
reason: "This field contains text that is not JSON."
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (t === null || t === "") return { status: "empty" };
|
|
41
|
+
}
|
|
42
|
+
if (typeof t != "object" || t === null || Array.isArray(t))
|
|
43
|
+
return {
|
|
44
|
+
status: "unreadable",
|
|
45
|
+
reason: "This field contains JSON that is not a chart."
|
|
46
|
+
};
|
|
47
|
+
const i = u(t);
|
|
48
|
+
return i.status === "skipped" ? {
|
|
49
|
+
status: "unreadable",
|
|
50
|
+
reason: `Chartkit cannot read what is in this field: ${i.reason}.`
|
|
51
|
+
} : { status: "ok", spec: i.spec };
|
|
52
|
+
}
|
|
53
|
+
function k(e) {
|
|
54
|
+
return JSON.stringify(e);
|
|
55
|
+
}
|
|
56
|
+
function L() {
|
|
57
|
+
return {
|
|
58
|
+
version: c,
|
|
59
|
+
type: "bar",
|
|
60
|
+
data: {
|
|
61
|
+
source: "inline",
|
|
62
|
+
labels: ["Q1", "Q2", "Q3", "Q4"],
|
|
63
|
+
series: [{ name: "Series 1", values: [null, null, null, null] }]
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const M = {
|
|
68
|
+
register(e) {
|
|
69
|
+
e.registerPlugin({
|
|
70
|
+
id: r,
|
|
71
|
+
name: r,
|
|
72
|
+
initializer: h,
|
|
73
|
+
isReady: !1
|
|
74
|
+
}), e.customFields.register({
|
|
75
|
+
name: p,
|
|
76
|
+
pluginId: r,
|
|
77
|
+
type: "json",
|
|
78
|
+
icon: m,
|
|
79
|
+
intlLabel: {
|
|
80
|
+
id: `${r}.field.label`,
|
|
81
|
+
defaultMessage: "Chart"
|
|
82
|
+
},
|
|
83
|
+
intlDescription: {
|
|
84
|
+
id: `${r}.field.description`,
|
|
85
|
+
defaultMessage: "A chart, rendered as SVG with no client-side JavaScript"
|
|
86
|
+
},
|
|
87
|
+
components: {
|
|
88
|
+
// Lazy, so the editor and its data grid are not in the bundle of every
|
|
89
|
+
// admin page — only of the ones that actually show a chart field.
|
|
90
|
+
Input: async () => ({
|
|
91
|
+
default: (await import("../_chunks/ChartField-N1BDNZ3c.mjs")).default
|
|
92
|
+
})
|
|
93
|
+
},
|
|
94
|
+
options: {
|
|
95
|
+
base: [
|
|
96
|
+
{
|
|
97
|
+
sectionTitle: {
|
|
98
|
+
id: `${r}.section.preview`,
|
|
99
|
+
defaultMessage: "Preview"
|
|
100
|
+
},
|
|
101
|
+
items: [
|
|
102
|
+
{
|
|
103
|
+
name: "options.previewLocale",
|
|
104
|
+
type: "text",
|
|
105
|
+
placeholder: {
|
|
106
|
+
id: `${r}.previewLocale.placeholder`,
|
|
107
|
+
defaultMessage: "en-US"
|
|
108
|
+
},
|
|
109
|
+
intlLabel: {
|
|
110
|
+
id: `${r}.previewLocale`,
|
|
111
|
+
defaultMessage: "Number formatting locale"
|
|
112
|
+
},
|
|
113
|
+
description: {
|
|
114
|
+
id: `${r}.previewLocale.description`,
|
|
115
|
+
defaultMessage: "How axis numbers are formatted in the admin preview — a BCP 47 tag such as de-DE or pl-PL. Affects the preview only; the front end passes its own locale when it renders. Left empty, the browser's is used."
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
advanced: [
|
|
122
|
+
{
|
|
123
|
+
sectionTitle: null,
|
|
124
|
+
items: [
|
|
125
|
+
{
|
|
126
|
+
name: "required",
|
|
127
|
+
type: "checkbox",
|
|
128
|
+
intlLabel: {
|
|
129
|
+
id: `${r}.required`,
|
|
130
|
+
defaultMessage: "Required field"
|
|
131
|
+
},
|
|
132
|
+
description: {
|
|
133
|
+
id: `${r}.required.description`,
|
|
134
|
+
defaultMessage: "You won't be able to create an entry if this field is empty"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
async registerTrads({ locales: e }) {
|
|
144
|
+
return Promise.all(
|
|
145
|
+
e.map(async (t) => {
|
|
146
|
+
try {
|
|
147
|
+
const { default: i } = await f(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("../_chunks/en-DYxpcVy9.mjs") }), `./translations/${t}.json`, 3);
|
|
148
|
+
return { data: i, locale: t };
|
|
149
|
+
} catch {
|
|
150
|
+
return { data: {}, locale: t };
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
export {
|
|
157
|
+
p as FIELD_NAME,
|
|
158
|
+
r as PLUGIN_ID,
|
|
159
|
+
M as default,
|
|
160
|
+
v as readValue,
|
|
161
|
+
L as starterSpec,
|
|
162
|
+
k as writeValue
|
|
163
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chart custom field.
|
|
3
|
+
*
|
|
4
|
+
* A preview, and a button that opens the editor. The editor itself is
|
|
5
|
+
* `@qkix/chartkit-editor` — the same component the chart block inside Better
|
|
6
|
+
* Blocks mounts, which is why it is a package rather than living in one of the
|
|
7
|
+
* two plugins that need it.
|
|
8
|
+
*
|
|
9
|
+
* The dialog rather than an inline editor for the same reason the block uses
|
|
10
|
+
* one: the editor is a preview plus a grid plus a row of settings, which is
|
|
11
|
+
* most of a screen given to a field the author usually only glances at on their
|
|
12
|
+
* way to the rest of the form.
|
|
13
|
+
*/
|
|
14
|
+
import * as React from 'react';
|
|
15
|
+
type ChartFieldProps = {
|
|
16
|
+
name: string;
|
|
17
|
+
value?: string | null;
|
|
18
|
+
onChange: (event: {
|
|
19
|
+
target: {
|
|
20
|
+
name: string;
|
|
21
|
+
type: string;
|
|
22
|
+
value: string;
|
|
23
|
+
};
|
|
24
|
+
}) => void;
|
|
25
|
+
attribute?: {
|
|
26
|
+
type: string;
|
|
27
|
+
customField: string;
|
|
28
|
+
options?: {
|
|
29
|
+
previewLocale?: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
label?: string;
|
|
33
|
+
hint?: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
required?: boolean;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
};
|
|
38
|
+
declare const ChartField: React.ForwardRefExoticComponent<ChartFieldProps & React.RefAttributes<HTMLDivElement>>;
|
|
39
|
+
export default ChartField;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
/**
|
|
3
|
+
* The icon shown against the field in the content-type builder.
|
|
4
|
+
*
|
|
5
|
+
* Drawn here rather than taken from `@strapi/icons`, which has no chart glyph
|
|
6
|
+
* that reads as one at 16px.
|
|
7
|
+
*/
|
|
8
|
+
declare const PluginIcon: () => import("react").JSX.Element;
|
|
9
|
+
export { PluginIcon };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
register(app: any): void;
|
|
3
|
+
registerTrads({ locales }: {
|
|
4
|
+
locales: string[];
|
|
5
|
+
}): Promise<{
|
|
6
|
+
data: any;
|
|
7
|
+
locale: string;
|
|
8
|
+
}[]>;
|
|
9
|
+
};
|
|
10
|
+
export default _default;
|
|
11
|
+
/**
|
|
12
|
+
* Re-exported so an app that stores a chart in this field and renders it
|
|
13
|
+
* elsewhere — a preview route, a custom admin page — does not need a second
|
|
14
|
+
* install to get at the spec's type or to draw one.
|
|
15
|
+
*/
|
|
16
|
+
export { readValue, writeValue, starterSpec } from './value';
|
|
17
|
+
export type { ReadValue } from './value';
|
|
18
|
+
export { FIELD_NAME, PLUGIN_ID } from './pluginId';
|
|
19
|
+
export type { ChartSpec, ChartType, Series, ChartOptions } from '@qkix/chartkit-core';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const PLUGIN_ID = "chartkit";
|
|
2
|
+
/**
|
|
3
|
+
* The custom field's name, which is not the plugin's.
|
|
4
|
+
*
|
|
5
|
+
* A field is referred to as `plugin::chartkit.chart` in a content-type schema,
|
|
6
|
+
* so the plugin is the namespace and this is the field inside it. Naming both
|
|
7
|
+
* `chartkit` would read as `plugin::chartkit.chartkit`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const FIELD_NAME = "chart";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Getting a chart in and out of a Strapi field value.
|
|
3
|
+
*
|
|
4
|
+
* Small, and separated out anyway, because it is where the field can lose
|
|
5
|
+
* someone's work. Strapi hands a custom field whatever is in the document: a
|
|
6
|
+
* JSON string on one code path and an already-parsed object on another, `null`
|
|
7
|
+
* for a new entry, and — for a field that used to be something else, or a
|
|
8
|
+
* document written by an import script — text that is not a chart at all.
|
|
9
|
+
*
|
|
10
|
+
* None of that may throw. A field that throws while rendering takes the whole
|
|
11
|
+
* edit view down, and the author loses every other field on the page along with
|
|
12
|
+
* this one.
|
|
13
|
+
*/
|
|
14
|
+
import { type ChartSpec } from '@qkix/chartkit-core';
|
|
15
|
+
/**
|
|
16
|
+
* What a stored value turned out to be.
|
|
17
|
+
*
|
|
18
|
+
* `empty` and `unreadable` are deliberately different. Empty is a field nobody
|
|
19
|
+
* has filled in yet and the answer is an invitation to make a chart. Unreadable
|
|
20
|
+
* is data that exists and is not a chart, where quietly showing that same
|
|
21
|
+
* invitation would be an offer to overwrite it.
|
|
22
|
+
*/
|
|
23
|
+
export type ReadValue = {
|
|
24
|
+
status: 'empty';
|
|
25
|
+
} | {
|
|
26
|
+
status: 'ok';
|
|
27
|
+
spec: ChartSpec;
|
|
28
|
+
} | {
|
|
29
|
+
status: 'unreadable';
|
|
30
|
+
reason: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Reads the stored value into a spec.
|
|
34
|
+
*
|
|
35
|
+
* Migrates on the way in, so an author opening a chart saved against an older
|
|
36
|
+
* schema edits the current shape and saves the current shape. `renderChart`
|
|
37
|
+
* migrates in memory too, which is what keeps the published page correct in the
|
|
38
|
+
* meantime — but a document only stops being old once someone writes it back.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readValue(value: unknown): ReadValue;
|
|
41
|
+
/**
|
|
42
|
+
* The value to store.
|
|
43
|
+
*
|
|
44
|
+
* A string, because Strapi's `json` custom fields are read back as strings and
|
|
45
|
+
* writing an object here makes the field's own round trip asymmetric — it would
|
|
46
|
+
* work until the page is reloaded.
|
|
47
|
+
*/
|
|
48
|
+
export declare function writeValue(spec: ChartSpec): string;
|
|
49
|
+
/**
|
|
50
|
+
* The chart a new field starts from.
|
|
51
|
+
*
|
|
52
|
+
* Not empty. An empty chart renders as an error panel, which is a poor first
|
|
53
|
+
* impression of a field someone just added, and a grid with no rows gives
|
|
54
|
+
* nothing to paste over or type into. Four quarters and one series is the
|
|
55
|
+
* smallest thing that is recognisably a chart.
|
|
56
|
+
*/
|
|
57
|
+
export declare function starterSpec(): ChartSpec;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const t=({strapi:e})=>{e.customFields.register({name:"chart",plugin:"chartkit",type:"json",inputSize:{default:12,isResizable:!1}})},s={register:t};module.exports=s;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const t = ({ strapi: e }) => {
|
|
2
|
+
e.customFields.register({
|
|
3
|
+
name: "chart",
|
|
4
|
+
plugin: "chartkit",
|
|
5
|
+
type: "json",
|
|
6
|
+
inputSize: {
|
|
7
|
+
// Full width. The field is a chart preview above a data grid, and half a
|
|
8
|
+
// row is not enough for either.
|
|
9
|
+
default: 12,
|
|
10
|
+
isResizable: !1
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
}, s = {
|
|
14
|
+
register: t
|
|
15
|
+
};
|
|
16
|
+
export {
|
|
17
|
+
s as default
|
|
18
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server half of the plugin.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately just a registration. Charts render from their spec, so there is
|
|
5
|
+
* no chart data to store beyond the field itself, no routes to serve it over
|
|
6
|
+
* and no service to compute it — a Chartkit content type would be a second
|
|
7
|
+
* place for the same numbers to live and drift.
|
|
8
|
+
*/
|
|
9
|
+
declare const _default: {
|
|
10
|
+
register: ({ strapi }: {
|
|
11
|
+
strapi: import("@strapi/types/dist/core").Strapi;
|
|
12
|
+
}) => void;
|
|
13
|
+
};
|
|
14
|
+
export default _default;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Core } from '@strapi/strapi';
|
|
2
|
+
/**
|
|
3
|
+
* Registers the chart custom field.
|
|
4
|
+
*
|
|
5
|
+
* `json`, because a chart is a spec — a type, some labels, some series — and
|
|
6
|
+
* the alternatives are worse. A relation would need a content type nobody
|
|
7
|
+
* asked for; text would need every consumer to parse it and guess at what
|
|
8
|
+
* failure means.
|
|
9
|
+
*
|
|
10
|
+
* The stored value is a `ChartSpec` from `@qkix/chartkit-core`, which is also
|
|
11
|
+
* exactly what the renderers take. A front end reads the field and passes it
|
|
12
|
+
* straight to `<Chart spec={...} />` with nothing in between.
|
|
13
|
+
*/
|
|
14
|
+
declare const register: ({ strapi }: {
|
|
15
|
+
strapi: Core.Strapi;
|
|
16
|
+
}) => void;
|
|
17
|
+
export default register;
|
package/package.json
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qkix/strapi-plugin-chartkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Charts for Strapi v5, rendered as server-side SVG with no client-side JavaScript",
|
|
5
|
+
"strapi": {
|
|
6
|
+
"kind": "plugin",
|
|
7
|
+
"name": "chartkit",
|
|
8
|
+
"displayName": "Chartkit",
|
|
9
|
+
"description": "Charts for Strapi v5, rendered as server-side SVG with no client-side JavaScript"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"strapi",
|
|
13
|
+
"strapi-plugin",
|
|
14
|
+
"strapi-v5",
|
|
15
|
+
"chart",
|
|
16
|
+
"charts",
|
|
17
|
+
"chartkit",
|
|
18
|
+
"svg",
|
|
19
|
+
"custom-field",
|
|
20
|
+
"cms",
|
|
21
|
+
"headless-cms",
|
|
22
|
+
"data-visualization"
|
|
23
|
+
],
|
|
24
|
+
"type": "commonjs",
|
|
25
|
+
"exports": {
|
|
26
|
+
"./package.json": "./package.json",
|
|
27
|
+
"./strapi-admin": {
|
|
28
|
+
"types": "./dist/admin/src/index.d.ts",
|
|
29
|
+
"source": "./admin/src/index.ts",
|
|
30
|
+
"import": "./dist/admin/index.mjs",
|
|
31
|
+
"require": "./dist/admin/index.js",
|
|
32
|
+
"default": "./dist/admin/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./strapi-server": {
|
|
35
|
+
"types": "./dist/server/src/index.d.ts",
|
|
36
|
+
"source": "./server/src/index.ts",
|
|
37
|
+
"import": "./dist/server/index.mjs",
|
|
38
|
+
"require": "./dist/server/index.js",
|
|
39
|
+
"default": "./dist/server/index.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@qkix/chartkit-core": "^0.2.0",
|
|
47
|
+
"@qkix/chartkit-editor": "^0.1.1"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@strapi/sdk-plugin": "^5.3.2",
|
|
51
|
+
"@strapi/strapi": "^5.12.5",
|
|
52
|
+
"@strapi/typescript-utils": "^5.12.5",
|
|
53
|
+
"@types/react": "^18.3.8",
|
|
54
|
+
"@types/react-dom": "^18.3.0",
|
|
55
|
+
"eslint": "^10.8.1",
|
|
56
|
+
"prettier": "^3.5.3",
|
|
57
|
+
"react": "^18.3.1",
|
|
58
|
+
"react-dom": "^18.3.1",
|
|
59
|
+
"styled-components": "^6.1.17",
|
|
60
|
+
"typescript": "^5.8.3",
|
|
61
|
+
"vitest": "^3.2.4",
|
|
62
|
+
"@qkix/tsconfig": "0.0.0",
|
|
63
|
+
"@qkix/eslint-config": "0.0.0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"@strapi/admin": "^5.0.0",
|
|
67
|
+
"@strapi/design-system": "^2.0.0",
|
|
68
|
+
"@strapi/icons": "^2.0.0",
|
|
69
|
+
"@strapi/strapi": "^5.0.0",
|
|
70
|
+
"react": "^17.0.0 || ^18.0.0",
|
|
71
|
+
"react-dom": "^17.0.0 || ^18.0.0",
|
|
72
|
+
"react-intl": "^6.0.0 || ^7.0.0",
|
|
73
|
+
"styled-components": "^6.0.0"
|
|
74
|
+
},
|
|
75
|
+
"engines": {
|
|
76
|
+
"node": ">=20.0.0 <=24.x.x",
|
|
77
|
+
"npm": ">=6.0.0"
|
|
78
|
+
},
|
|
79
|
+
"license": "MIT",
|
|
80
|
+
"author": "qkix",
|
|
81
|
+
"repository": {
|
|
82
|
+
"type": "git",
|
|
83
|
+
"url": "git+https://github.com/qkix/strapi-plugins.git",
|
|
84
|
+
"directory": "packages/strapi-plugin-chartkit"
|
|
85
|
+
},
|
|
86
|
+
"bugs": {
|
|
87
|
+
"url": "https://github.com/qkix/strapi-plugins/issues"
|
|
88
|
+
},
|
|
89
|
+
"homepage": "https://github.com/qkix/strapi-plugins#readme",
|
|
90
|
+
"nx": {
|
|
91
|
+
"tags": [
|
|
92
|
+
"scope:plugin"
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
"publishConfig": {
|
|
96
|
+
"access": "public"
|
|
97
|
+
},
|
|
98
|
+
"scripts": {
|
|
99
|
+
"build": "rm -rf dist && strapi-plugin build --minify",
|
|
100
|
+
"clean": "rm -rf dist",
|
|
101
|
+
"lint": "eslint . && prettier --check .",
|
|
102
|
+
"format": "prettier --write .",
|
|
103
|
+
"test": "vitest run",
|
|
104
|
+
"test:watch": "vitest",
|
|
105
|
+
"verify": "strapi-plugin verify",
|
|
106
|
+
"watch": "strapi-plugin watch",
|
|
107
|
+
"watch:link": "strapi-plugin watch:link",
|
|
108
|
+
"typecheck": "tsc -p admin/tsconfig.json && tsc -p server/tsconfig.json"
|
|
109
|
+
}
|
|
110
|
+
}
|