@kolkrabbi/kol-styleguide 0.13.0 → 0.14.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/package.json +7 -5
- package/src/A4Pages.jsx +100 -0
- package/src/PrintStudio.jsx +115 -0
- package/src/index.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-styleguide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Brand guide components and the brand tool",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,15 +15,17 @@
|
|
|
15
15
|
"@kolkrabbi/kol-component": ">=0.188.0",
|
|
16
16
|
"@kolkrabbi/kol-framework": ">=0.39.0",
|
|
17
17
|
"@kolkrabbi/kol-icons": ">=0.18.0",
|
|
18
|
+
"@kolkrabbi/kol-print": ">=0.1.0",
|
|
18
19
|
"@kolkrabbi/kol-theme": ">=0.53.0",
|
|
19
20
|
"react": "^18.3.0 || ^19.0.0",
|
|
20
21
|
"react-dom": "^18.3.0 || ^19.0.0"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
|
-
"@kolkrabbi/kol-
|
|
24
|
-
"@kolkrabbi/kol-component": "^0.
|
|
25
|
-
"@kolkrabbi/kol-
|
|
26
|
-
"@kolkrabbi/kol-
|
|
24
|
+
"@kolkrabbi/kol-print": "^0.1.0",
|
|
25
|
+
"@kolkrabbi/kol-component": "^0.311.0",
|
|
26
|
+
"@kolkrabbi/kol-theme": "^0.196.0",
|
|
27
|
+
"@kolkrabbi/kol-framework": "^0.61.0",
|
|
28
|
+
"@kolkrabbi/kol-icons": "^0.52.0"
|
|
27
29
|
},
|
|
28
30
|
"files": [
|
|
29
31
|
"src",
|
package/src/A4Pages.jsx
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useRef, useState, useLayoutEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
/* A4 portrait @96dpi, with print-style page margins. */
|
|
4
|
+
const A4_W = 794
|
|
5
|
+
const A4_H = 1123
|
|
6
|
+
const MARGIN = 48
|
|
7
|
+
/* A few px of headroom so a printed sheet stays just under a physical A4 page
|
|
8
|
+
(1123px ≈ 297.3mm > A4's 297mm) and never spills onto a blank trailing page. */
|
|
9
|
+
const PRINT_SAFETY = 8
|
|
10
|
+
const CONTENT_W = A4_W - MARGIN * 2
|
|
11
|
+
const CONTENT_H = A4_H - MARGIN * 2 - PRINT_SAFETY
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A4Pages — flows any content across as many real A4 sheets (1:√2) as it needs, at natural size. kol-website apps/record's
|
|
15
|
+
* `A4Page`, carried verbatim (PdfToolsFromStudio, 2026-10-28 — record's kit preview, now the DS's); the sheet's box is
|
|
16
|
+
* kol-theme's `.kol-a4-sheet` · `.kol-a4-clip` · `.kol-a4-measure`, and printing maps each sheet to one A4 page.
|
|
17
|
+
*
|
|
18
|
+
* A4 page wrapper — flows a kit section across as many real A4 sheets (1:√2) as
|
|
19
|
+
* it needs, at natural size. No scaling: the section is measured once off-screen,
|
|
20
|
+
* page breaks are snapped to element top-edges (so nothing is cut mid-element),
|
|
21
|
+
* and each page renders the section clipped to its slice via a translateY window.
|
|
22
|
+
*
|
|
23
|
+
* Re-measured on resize and as images (heroes) finish loading.
|
|
24
|
+
*/
|
|
25
|
+
export default function A4Pages({ children, label }) {
|
|
26
|
+
const measureRef = useRef(null)
|
|
27
|
+
// breaks[k]..breaks[k+1] is page k's vertical slice of the content.
|
|
28
|
+
const [breaks, setBreaks] = useState([0, CONTENT_H])
|
|
29
|
+
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
const el = measureRef.current
|
|
32
|
+
if (!el) return
|
|
33
|
+
|
|
34
|
+
const compute = () => {
|
|
35
|
+
const top = el.getBoundingClientRect().top
|
|
36
|
+
// Break candidates = the top edge of every block, so a page can only end
|
|
37
|
+
// *between* elements — never through one.
|
|
38
|
+
const ys = new Set([0])
|
|
39
|
+
el.querySelectorAll('*').forEach((node) => {
|
|
40
|
+
const y = Math.round(node.getBoundingClientRect().top - top)
|
|
41
|
+
if (y > 0) ys.add(y)
|
|
42
|
+
})
|
|
43
|
+
const total = el.scrollHeight
|
|
44
|
+
ys.add(total)
|
|
45
|
+
const sorted = [...ys].sort((a, b) => a - b)
|
|
46
|
+
|
|
47
|
+
const pts = [0]
|
|
48
|
+
let cursor = 0
|
|
49
|
+
while (cursor < total) {
|
|
50
|
+
const limit = cursor + CONTENT_H
|
|
51
|
+
let next = -1
|
|
52
|
+
for (const c of sorted) {
|
|
53
|
+
if (c > cursor && c <= limit) next = c
|
|
54
|
+
else if (c > limit) break
|
|
55
|
+
}
|
|
56
|
+
// An element taller than a page can't be snapped — hard-cut at the limit.
|
|
57
|
+
if (next === -1) next = Math.min(limit, total)
|
|
58
|
+
if (next <= cursor) break
|
|
59
|
+
pts.push(next)
|
|
60
|
+
cursor = next
|
|
61
|
+
}
|
|
62
|
+
setBreaks(pts)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
compute()
|
|
66
|
+
const ro = new ResizeObserver(compute)
|
|
67
|
+
ro.observe(el)
|
|
68
|
+
const imgs = el.querySelectorAll('img')
|
|
69
|
+
imgs.forEach((img) => { if (!img.complete) img.addEventListener('load', compute) })
|
|
70
|
+
return () => {
|
|
71
|
+
ro.disconnect()
|
|
72
|
+
imgs.forEach((img) => img.removeEventListener('load', compute))
|
|
73
|
+
}
|
|
74
|
+
}, [])
|
|
75
|
+
|
|
76
|
+
const pageCount = Math.max(1, breaks.length - 1)
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<>
|
|
80
|
+
{/* Off-screen measurer at exact page-content width. */}
|
|
81
|
+
<div ref={measureRef} className="kol-a4-measure" aria-hidden="true">
|
|
82
|
+
{children}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{Array.from({ length: pageCount }, (_, k) => {
|
|
86
|
+
const from = breaks[k] ?? 0
|
|
87
|
+
const to = breaks[k + 1] ?? from + CONTENT_H
|
|
88
|
+
return (
|
|
89
|
+
<section key={k} className="kol-a4-sheet" aria-label={`${label} — page ${k + 1} of ${pageCount}`}>
|
|
90
|
+
<div className="kol-a4-clip" style={{ height: to - from }}>
|
|
91
|
+
{/* Negative margin (not transform) shifts the slice — Chrome's
|
|
92
|
+
print engine drops images inside transformed containers. */}
|
|
93
|
+
<div style={{ marginTop: -from }}>{children}</div>
|
|
94
|
+
</div>
|
|
95
|
+
</section>
|
|
96
|
+
)
|
|
97
|
+
})}
|
|
98
|
+
</>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { Button, Dropdown, Input, Textarea } from '@kolkrabbi/kol-component'
|
|
3
|
+
import { flow, frontmatter, paginateInDocument, printHtml, footLine, toDocxBlob, toMarkdown } from '@kolkrabbi/kol-print'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* PrintStudio — markdown to print, in the page: pick a layout, write or paste the document, see the A4 sheets, and take it
|
|
7
|
+
* away as PDF, DOCX or MD (PdfToolsFromStudio, 2026-10-28 — user: *"we have a bunch of stationery, we would like to connect
|
|
8
|
+
* those together with the package"*). A tool: drop it into an app's Hub page or a site's page.
|
|
9
|
+
*
|
|
10
|
+
* The engine is `@kolkrabbi/kol-print` — the same flow and pagination the `kol-print` CLI runs in headless Chrome, here run
|
|
11
|
+
* in this browser, so a sheet on screen is the sheet the CLI prints. PDF is the browser's own print of the sheets (a hidden
|
|
12
|
+
* frame, A4, no margins); DOCX is built in the page (`docx`); MD is the source under the export frontmatter.
|
|
13
|
+
*
|
|
14
|
+
* Two kinds of layout share the picker: **Document** (kol-print's: the studio's header, the parties block, numbered
|
|
15
|
+
* sections, kv tables, footnotes, the ✋ 🅿️ 📍 marks — paginated from the markdown) and the app's **templates** — finished
|
|
16
|
+
* A4 boards (the stationery: letters, invoices, agreements), shown and printed as they are.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} studio `{ studio, designer, address, contacts }` — the sheet's header and the parties block (the
|
|
19
|
+
* caller's: the package ships no studio's details)
|
|
20
|
+
* @param {string} logoSvg the header's logo markup (the sheets)
|
|
21
|
+
* @param {string} logoPngUrl the header's logo as a PNG url (the DOCX)
|
|
22
|
+
* @param {Object} meta the project — `{ project, slug, client, contact?, site?, dates }`, editable on the page
|
|
23
|
+
* @param {string} markdown the starting document (kol-docs markdown, frontmatter `title`)
|
|
24
|
+
* @param {Array} templates `[{ id, label, sheets: [html] }]` — finished A4 boards (1190 × 1684), printed as they are
|
|
25
|
+
*/
|
|
26
|
+
const ZOOM = 0.42 // a 1190px sheet at ~500px on screen
|
|
27
|
+
/* a template sheet may be a whole HTML document (the stationery boards are): its styles and body, without the document */
|
|
28
|
+
const sheetBody = (h) => {
|
|
29
|
+
if (!/<body[\s>]/i.test(h)) return h
|
|
30
|
+
const styles = (h.match(/<style[\s\S]*?<\/style>/gi) ?? []).join('')
|
|
31
|
+
const body = (h.match(/<body[^>]*>([\s\S]*)<\/body>/i) ?? [, h])[1]
|
|
32
|
+
return styles + body
|
|
33
|
+
}
|
|
34
|
+
const download = (blob, name) => {
|
|
35
|
+
const a = document.createElement('a')
|
|
36
|
+
a.href = URL.createObjectURL(blob); a.download = name
|
|
37
|
+
document.body.appendChild(a); a.click(); a.remove()
|
|
38
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 1000)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function PrintStudio({ studio = {}, logoSvg = '', logoPngUrl, meta: metaProp = {}, markdown: mdProp = '', templates = [], className = '' }) {
|
|
42
|
+
const [layout, setLayout] = useState('document')
|
|
43
|
+
const [md, setMd] = useState(mdProp)
|
|
44
|
+
const [meta, setMeta] = useState({ project: '', slug: 'doc', client: '', dates: '', ...metaProp })
|
|
45
|
+
const [pages, setPages] = useState([])
|
|
46
|
+
const frameRef = useRef(null)
|
|
47
|
+
const template = templates.find((t) => t.id === layout)
|
|
48
|
+
|
|
49
|
+
/* the document: flow → pages, cut in this browser by the CLI's own script */
|
|
50
|
+
const { doc } = useMemo(() => (md.trim() ? flow(md, meta, studio) : { doc: '', html: '' }), [md, meta, studio])
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (template || !md.trim()) { setPages([]); return }
|
|
53
|
+
const { html } = flow(md, meta, studio)
|
|
54
|
+
setPages(paginateInDocument(html))
|
|
55
|
+
}, [md, meta, studio, template])
|
|
56
|
+
|
|
57
|
+
const foot = footLine(studio, meta, doc)
|
|
58
|
+
const printable = template
|
|
59
|
+
? `<!doctype html><meta charset="utf-8"><title>${template.label}</title><style>@page{size:A4;margin:0}html{zoom:0.6665}body{margin:0}.sheet{width:1190px;height:1684px;overflow:hidden;break-after:page}</style>${template.sheets.map((h) => `<section class="sheet">${sheetBody(h)}</section>`).join('')}`
|
|
60
|
+
: printHtml(pages, { title: doc, studio, logoSvg, foot })
|
|
61
|
+
/* on screen: the same document at a reading zoom, the sheets on a grey desk */
|
|
62
|
+
const preview = printable.replace('html{zoom:0.6665}body{margin:0}', `html{zoom:${ZOOM}}body{margin:0;padding:24px 0;background:#d4d4d8}.sheet{margin:0 auto 24px;box-shadow:0 4px 24px rgba(0,0,0,.25)}`)
|
|
63
|
+
const sheetCount = template ? template.sheets.length : pages.length
|
|
64
|
+
|
|
65
|
+
const printPdf = () => {
|
|
66
|
+
const f = frameRef.current
|
|
67
|
+
f.srcdoc = printable
|
|
68
|
+
f.onload = () => { f.contentWindow.focus(); f.contentWindow.print() }
|
|
69
|
+
}
|
|
70
|
+
const saveDocx = async () => {
|
|
71
|
+
const logoPng = logoPngUrl ? new Uint8Array(await (await fetch(logoPngUrl)).arrayBuffer()) : undefined
|
|
72
|
+
download(await toDocxBlob(pages, { foot, title: `${doc} ${meta.project}`, studio, logoPng }), `${meta.slug}-${(frontmatter(md).fm.title || 'document').toLowerCase().replace(/[^\w]+/g, '-')}.docx`)
|
|
73
|
+
}
|
|
74
|
+
const saveMd = () => download(new Blob([toMarkdown(md, { meta, studio, doc })], { type: 'text/markdown' }), `${meta.slug}-${(doc || 'document').toLowerCase().replace(/[^\w]+/g, '-')}.md`)
|
|
75
|
+
const field = (k, label) => (
|
|
76
|
+
<label className="flex flex-col gap-1">
|
|
77
|
+
<span className="kol-mono-10 text-meta uppercase">{label}</span>
|
|
78
|
+
<Input size="sm" value={meta[k] ?? ''} onChange={(e) => setMeta((m) => ({ ...m, [k]: e.target.value }))} aria-label={label} />
|
|
79
|
+
</label>
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className={`flex h-full min-h-0 flex-col md:flex-row ${className}`.trim()} data-print-studio>
|
|
84
|
+
<aside className="flex flex-col gap-4 p-4 md:w-[380px] md:shrink-0 md:overflow-y-auto">
|
|
85
|
+
<label className="flex flex-col gap-1">
|
|
86
|
+
<span className="kol-mono-10 text-meta uppercase">Layout</span>
|
|
87
|
+
<Dropdown size="sm" value={layout} onChange={setLayout} ariaLabel="Layout"
|
|
88
|
+
options={[{ value: 'document', label: 'Document' }, ...templates.map((t) => ({ value: t.id, label: t.label }))]} />
|
|
89
|
+
</label>
|
|
90
|
+
{!template && (
|
|
91
|
+
<>
|
|
92
|
+
<div className="grid grid-cols-2 gap-3">
|
|
93
|
+
{field('project', 'Project')}
|
|
94
|
+
{field('client', 'Client')}
|
|
95
|
+
{field('dates', 'Dates')}
|
|
96
|
+
{field('slug', 'File slug')}
|
|
97
|
+
</div>
|
|
98
|
+
<label className="flex flex-col gap-1">
|
|
99
|
+
<span className="kol-mono-10 text-meta uppercase">Markdown</span>
|
|
100
|
+
<Textarea rows={16} value={md} onChange={(e) => setMd(e.target.value)} aria-label="Markdown" className="kol-mono-12" />
|
|
101
|
+
</label>
|
|
102
|
+
</>
|
|
103
|
+
)}
|
|
104
|
+
<div className="flex flex-wrap gap-2">
|
|
105
|
+
<Button size="sm" tone="inverted" onClick={printPdf} disabled={!sheetCount}>PDF</Button>
|
|
106
|
+
<Button size="sm" onClick={saveDocx} disabled={!!template || !pages.length}>DOCX</Button>
|
|
107
|
+
<Button size="sm" onClick={saveMd} disabled={!!template || !md.trim()}>MD</Button>
|
|
108
|
+
</div>
|
|
109
|
+
<span className="kol-mono-10 text-meta">{sheetCount} {sheetCount === 1 ? 'sheet' : 'sheets'} · A4</span>
|
|
110
|
+
</aside>
|
|
111
|
+
<iframe title="Sheets" className="min-h-[60vh] flex-1 border-0" srcDoc={preview} data-print-preview />
|
|
112
|
+
<iframe ref={frameRef} title="Print" aria-hidden="true" tabIndex={-1} className="fixed h-0 w-0 border-0 opacity-0 pointer-events-none" />
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}
|
package/src/index.js
CHANGED
|
@@ -27,6 +27,9 @@ export { generatePalette, fgOn, hexToHsl, hslToHex, GENERATION_MODES } from './c
|
|
|
27
27
|
export { default as AssetCard } from './AssetCard.jsx'
|
|
28
28
|
/* a folder's pages as the brand book's contents cards (plan-2026-10-28-document W5.2) */
|
|
29
29
|
export { default as DocumentIndex } from './DocumentIndex.jsx'
|
|
30
|
+
/* print (PdfToolsFromStudio, 2026-10-28): A4 sheets for any content, and markdown to PDF · DOCX · MD on @kolkrabbi/kol-print */
|
|
31
|
+
export { default as A4Pages } from './A4Pages.jsx'
|
|
32
|
+
export { default as PrintStudio } from './PrintStudio.jsx'
|
|
30
33
|
export { default as Swatch } from './Swatch.jsx'
|
|
31
34
|
export {
|
|
32
35
|
PostPhoto, PostType, PostProduct, PostEditorial, StoryPhoto, StoryType, BrandAvatar, ProfileAvatar,
|