@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 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,4 @@
1
+ const e = {};
2
+ export {
3
+ e as default
4
+ };
@@ -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,5 @@
1
+ type InitializerProps = {
2
+ setPlugin: (id: string) => void;
3
+ };
4
+ declare const Initializer: ({ setPlugin }: InitializerProps) => null;
5
+ export { Initializer };
@@ -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
+ }