@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-styleguide",
3
- "version": "0.13.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-theme": "^0.194.0",
24
- "@kolkrabbi/kol-component": "^0.307.0",
25
- "@kolkrabbi/kol-framework": "^0.59.0",
26
- "@kolkrabbi/kol-icons": "^0.49.0"
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",
@@ -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,