@ideare/kit 0.0.0-stage → 0.1.0-next.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 +148 -2
- package/THIRD_PARTY.md +52 -0
- package/dist/brand/schema.d.ts +265 -0
- package/dist/brand/schema.js +271 -0
- package/dist/check/copy.d.ts +27 -0
- package/dist/check/copy.js +139 -0
- package/dist/check/index.d.ts +54 -0
- package/dist/check/index.js +492 -0
- package/dist/color/contrast.d.ts +41 -0
- package/dist/color/contrast.js +84 -0
- package/dist/color/palette.d.ts +27 -0
- package/dist/color/palette.js +114 -0
- package/dist/color/wcag.d.ts +22 -0
- package/dist/color/wcag.js +50 -0
- package/dist/draft/draft.d.ts +61 -0
- package/dist/draft/draft.js +383 -0
- package/dist/editor/history.d.ts +78 -0
- package/dist/editor/history.js +221 -0
- package/dist/editor/operations.d.ts +112 -0
- package/dist/editor/operations.js +621 -0
- package/dist/files/docs.d.ts +49 -0
- package/dist/files/docs.js +392 -0
- package/dist/files/generate.d.ts +47 -0
- package/dist/files/generate.js +254 -0
- package/dist/files/png.d.ts +40 -0
- package/dist/files/png.js +168 -0
- package/dist/files/raster.d.ts +14 -0
- package/dist/files/raster.js +0 -0
- package/dist/files/templates.d.ts +162 -0
- package/dist/files/templates.js +390 -0
- package/dist/files/tokens.d.ts +22 -0
- package/dist/files/tokens.js +204 -0
- package/dist/fonts/catalog.d.ts +3 -0
- package/dist/fonts/catalog.js +345 -0
- package/dist/fonts/ofl.d.ts +5 -0
- package/dist/fonts/ofl.js +5 -0
- package/dist/fonts/pick.d.ts +33 -0
- package/dist/fonts/pick.js +73 -0
- package/dist/imports/folder.d.ts +80 -0
- package/dist/imports/folder.js +124 -0
- package/dist/imports/unzip.d.ts +32 -0
- package/dist/imports/unzip.js +128 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +34 -0
- package/dist/marks/draw.d.ts +48 -0
- package/dist/marks/draw.js +149 -0
- package/dist/marks/icons.data.d.ts +5 -0
- package/dist/marks/icons.data.js +600 -0
- package/dist/marks/svg.d.ts +46 -0
- package/dist/marks/svg.js +341 -0
- package/dist/node/index.d.ts +13 -0
- package/dist/node/index.js +41 -0
- package/dist/posts/pix.d.ts +19 -0
- package/dist/posts/pix.js +49 -0
- package/dist/posts/templates.d.ts +45 -0
- package/dist/posts/templates.js +868 -0
- package/dist/resvg/index.d.ts +2 -0
- package/dist/resvg/index.js +22 -0
- package/dist/scene/brand-art.d.ts +49 -0
- package/dist/scene/brand-art.js +154 -0
- package/dist/scene/frame.d.ts +89 -0
- package/dist/scene/frame.js +213 -0
- package/dist/scene/grain.d.ts +7 -0
- package/dist/scene/grain.js +7 -0
- package/dist/scene/qr.d.ts +10 -0
- package/dist/scene/qr.js +359 -0
- package/dist/scene/render.d.ts +76 -0
- package/dist/scene/render.js +557 -0
- package/dist/scene/schema.d.ts +271 -0
- package/dist/scene/schema.js +257 -0
- package/dist/sign.d.ts +47 -0
- package/dist/sign.js +88 -0
- package/dist/text/engine.d.ts +128 -0
- package/dist/text/engine.js +245 -0
- package/dist/text/fonts.d.ts +56 -0
- package/dist/text/fonts.js +65 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/wasm/index.d.ts +15 -0
- package/dist/wasm/index.js +46 -0
- package/package.json +78 -5
- package/schema/brand.v1.json +442 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ImportEntry, type ImportMime, type ImportSkipReason } from "./folder.ts";
|
|
2
|
+
export type UnzippedFile = {
|
|
3
|
+
path: string;
|
|
4
|
+
bytes: Uint8Array<ArrayBuffer>;
|
|
5
|
+
mime: ImportMime;
|
|
6
|
+
};
|
|
7
|
+
export type UnzipResult = {
|
|
8
|
+
files: UnzippedFile[];
|
|
9
|
+
skipped: (ImportEntry & {
|
|
10
|
+
reason: ImportSkipReason;
|
|
11
|
+
ext?: string;
|
|
12
|
+
})[];
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A file's name as its owner typed it. fflate reads a name as Latin-1 unless the zip flags it as
|
|
16
|
+
* UTF-8, but macOS's zip and many others write UTF-8 without the flag, so "Café" would come out
|
|
17
|
+
* as "Café". When the bytes are valid UTF-8, they are UTF-8.
|
|
18
|
+
*/
|
|
19
|
+
export declare function zipName(name: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* The files of a zip that ideare reads, inflated as a stream with the limits held on the bytes
|
|
22
|
+
* that come out, whatever sizes the zip claims: one file stops at IMPORT_LIMITS.fileBytes, all of
|
|
23
|
+
* them at IMPORT_LIMITS.totalBytes, and at most IMPORT_LIMITS.files are kept, so a zip bomb costs
|
|
24
|
+
* the limits and no more. Nothing is written to disk, so a name like `../../x` reaches nowhere.
|
|
25
|
+
* Every file left out comes back in `skipped` with its reason, like planImport's. Throws when the
|
|
26
|
+
* bytes are not a zip, or a zip with no files.
|
|
27
|
+
*
|
|
28
|
+
* ponytail: files are kept in the zip's own order, so a zip with more than 200 readable files
|
|
29
|
+
* keeps its first 200 (planImport puts the guide and fonts first for a folder). Upgrade path: read
|
|
30
|
+
* the central directory first and sort it the same way.
|
|
31
|
+
*/
|
|
32
|
+
export declare function unzipImport(source: Uint8Array | AsyncIterable<Uint8Array>): Promise<UnzipResult>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Unzip, UnzipInflate } from "fflate";
|
|
2
|
+
import { IMPORT_LIMITS, checkImportEntry, importPath, } from "./folder.js";
|
|
3
|
+
/** Handed to the stream in slices this big, with a pause between them, so a page drawing a
|
|
4
|
+
* progress bar keeps answering while a big zip opens. */
|
|
5
|
+
const SLICE = 1024 * 1024;
|
|
6
|
+
async function* slices(source) {
|
|
7
|
+
if (!(source instanceof Uint8Array)) {
|
|
8
|
+
yield* source;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
for (let at = 0; at < source.length; at += SLICE)
|
|
12
|
+
yield source.subarray(at, at + SLICE);
|
|
13
|
+
}
|
|
14
|
+
const pause = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
15
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
16
|
+
/**
|
|
17
|
+
* A file's name as its owner typed it. fflate reads a name as Latin-1 unless the zip flags it as
|
|
18
|
+
* UTF-8, but macOS's zip and many others write UTF-8 without the flag, so "Café" would come out
|
|
19
|
+
* as "Café". When the bytes are valid UTF-8, they are UTF-8.
|
|
20
|
+
*/
|
|
21
|
+
export function zipName(name) {
|
|
22
|
+
const codes = Array.from(name, (char) => char.charCodeAt(0));
|
|
23
|
+
if (codes.every((code) => code < 0x80) || codes.some((code) => code > 0xff))
|
|
24
|
+
return name;
|
|
25
|
+
try {
|
|
26
|
+
return utf8.decode(Uint8Array.from(codes));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return name;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The files of a zip that ideare reads, inflated as a stream with the limits held on the bytes
|
|
34
|
+
* that come out, whatever sizes the zip claims: one file stops at IMPORT_LIMITS.fileBytes, all of
|
|
35
|
+
* them at IMPORT_LIMITS.totalBytes, and at most IMPORT_LIMITS.files are kept, so a zip bomb costs
|
|
36
|
+
* the limits and no more. Nothing is written to disk, so a name like `../../x` reaches nowhere.
|
|
37
|
+
* Every file left out comes back in `skipped` with its reason, like planImport's. Throws when the
|
|
38
|
+
* bytes are not a zip, or a zip with no files.
|
|
39
|
+
*
|
|
40
|
+
* ponytail: files are kept in the zip's own order, so a zip with more than 200 readable files
|
|
41
|
+
* keeps its first 200 (planImport puts the guide and fonts first for a folder). Upgrade path: read
|
|
42
|
+
* the central directory first and sort it the same way.
|
|
43
|
+
*/
|
|
44
|
+
export async function unzipImport(source) {
|
|
45
|
+
const result = { files: [], skipped: [] };
|
|
46
|
+
let total = 0;
|
|
47
|
+
let open = 0;
|
|
48
|
+
let failure = null;
|
|
49
|
+
let seen = 0;
|
|
50
|
+
const unzip = new Unzip((file) => {
|
|
51
|
+
seen++;
|
|
52
|
+
const path = importPath(zipName(file.name));
|
|
53
|
+
if (path.endsWith("/"))
|
|
54
|
+
return; // a folder entry: its files come on their own
|
|
55
|
+
const declared = file.originalSize ?? 1;
|
|
56
|
+
const check = checkImportEntry(path, declared);
|
|
57
|
+
const skip = (reason, size = declared, ext) => result.skipped.push({ path, size, reason, ...(ext ? { ext } : {}) });
|
|
58
|
+
// Unstarted files can still be fed: never let one reach fflate's default handler.
|
|
59
|
+
file.ondata = () => { };
|
|
60
|
+
if (!check.ok)
|
|
61
|
+
return skip(check.reason, declared, check.ext);
|
|
62
|
+
if (result.files.length + open >= IMPORT_LIMITS.files)
|
|
63
|
+
return skip("tooMany");
|
|
64
|
+
const chunks = [];
|
|
65
|
+
let size = 0;
|
|
66
|
+
let stopped = false;
|
|
67
|
+
open++;
|
|
68
|
+
const stop = (reason) => {
|
|
69
|
+
stopped = true;
|
|
70
|
+
open--;
|
|
71
|
+
file.terminate();
|
|
72
|
+
total -= size;
|
|
73
|
+
skip(reason, Math.max(size, declared));
|
|
74
|
+
};
|
|
75
|
+
file.ondata = (error, chunk, final) => {
|
|
76
|
+
if (stopped)
|
|
77
|
+
return;
|
|
78
|
+
if (error)
|
|
79
|
+
return stop("unreadable");
|
|
80
|
+
size += chunk.length;
|
|
81
|
+
total += chunk.length;
|
|
82
|
+
if (size > IMPORT_LIMITS.fileBytes)
|
|
83
|
+
return stop("tooBig");
|
|
84
|
+
if (total > IMPORT_LIMITS.totalBytes)
|
|
85
|
+
return stop("tooMuch");
|
|
86
|
+
chunks.push(chunk);
|
|
87
|
+
if (!final)
|
|
88
|
+
return;
|
|
89
|
+
open--;
|
|
90
|
+
if (size === 0)
|
|
91
|
+
return skip("empty", 0);
|
|
92
|
+
const bytes = new Uint8Array(size);
|
|
93
|
+
let at = 0;
|
|
94
|
+
for (const part of chunks) {
|
|
95
|
+
bytes.set(part, at);
|
|
96
|
+
at += part.length;
|
|
97
|
+
}
|
|
98
|
+
result.files.push({ path, bytes, mime: check.mime });
|
|
99
|
+
};
|
|
100
|
+
try {
|
|
101
|
+
file.start();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// A compression fflate can't read (LZMA, a password): that file only.
|
|
105
|
+
stopped = true;
|
|
106
|
+
open--;
|
|
107
|
+
skip("unreadable");
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
unzip.register(UnzipInflate);
|
|
111
|
+
try {
|
|
112
|
+
let previous = null;
|
|
113
|
+
for await (const chunk of slices(source)) {
|
|
114
|
+
if (previous)
|
|
115
|
+
unzip.push(previous);
|
|
116
|
+
previous = chunk;
|
|
117
|
+
await pause();
|
|
118
|
+
}
|
|
119
|
+
unzip.push(previous ?? new Uint8Array(0), true);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
failure = error instanceof Error ? error : new Error(String(error));
|
|
123
|
+
}
|
|
124
|
+
// A zip that breaks halfway still gives the files before the break.
|
|
125
|
+
if (seen === 0)
|
|
126
|
+
throw new Error(`This is not a zip, or it holds no files${failure ? ` (${failure.message})` : ""}.`);
|
|
127
|
+
return result;
|
|
128
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export { VERSION } from "./version.ts";
|
|
2
|
+
export { BRAND_SCHEMA_URL, BRAND_SCHEMA_VERSION, COLOR_ROLES, CONTACT_LINK_KINDS, FONT_ROLES, LINK_KINDS, LOCALES, brandJsonSchema, brandSchema, parseBrand, } from "./brand/schema.ts";
|
|
3
|
+
export type { Brand, BrandCopy, BrandFont, BrandInput, BrandIssue, ColorRole, FontRole, LinkKind, Locale, Palette, ParseBrandResult, } from "./brand/schema.ts";
|
|
4
|
+
export { contrast, formatRatio, luminance, normalizeHex, over, parseHex, toHex, } from "./color/wcag.ts";
|
|
5
|
+
export { ensureContrast, fromOklch, isLight, onColor, toOklch } from "./color/contrast.ts";
|
|
6
|
+
export type { ContrastFix, Oklch } from "./color/contrast.ts";
|
|
7
|
+
export { CONTRAST_PAIRS, MOODS, generatePalette } from "./color/palette.ts";
|
|
8
|
+
export type { Mood, PaletteSeed } from "./color/palette.ts";
|
|
9
|
+
export { safeEqual, sign, verify, LIVE_IMAGE_PURPOSE, canonicalImageMessage, signImageUrl, verifyImageUrl, } from "./sign.ts";
|
|
10
|
+
export { FONTSOURCE_CDN, FontLoadError, fetchFontResolver, fontFileName, fontsourceUrl, ideareFontUrl, } from "./text/fonts.ts";
|
|
11
|
+
export type { FontFormat, FontKey, FontResolver, FontStyle } from "./text/fonts.ts";
|
|
12
|
+
export { TextEngine, loadHarfbuzz } from "./text/engine.ts";
|
|
13
|
+
export type { LoadedFont, Run, TextAlign, TextBlock, TextLayout, TextOverflow, TextVAlign, } from "./text/engine.ts";
|
|
14
|
+
export { FONT_CATALOG } from "./fonts/catalog.ts";
|
|
15
|
+
export { FONT_TAGS, catalogFont, catalogVersions, nearestWeight, pickFonts } from "./fonts/pick.ts";
|
|
16
|
+
export type { CatalogFont, FontTag } from "./fonts/pick.ts";
|
|
17
|
+
export { OFL_TEXT } from "./fonts/ofl.ts";
|
|
18
|
+
export { aiProvenanceXmp, base64, isPng, itxtChunk, packIco, pngSize, readIco, sha256Hex, textChunk, withChunks, } from "./files/png.ts";
|
|
19
|
+
export { BADGES, iconBody, iconMark, iconNames, initials, matchIcons, monogramMark, pathBounds, } from "./marks/draw.ts";
|
|
20
|
+
export type { Badge } from "./marks/draw.ts";
|
|
21
|
+
export { cleanMarkSvg, markBody, markSvg, parseMark } from "./marks/svg.ts";
|
|
22
|
+
export type { MarkElement, MarkPaint, ParsedMark } from "./marks/svg.ts";
|
|
23
|
+
export { FORMATS, SCENE_VERSION, layerSchema, parseScene, sceneSchema } from "./scene/schema.ts";
|
|
24
|
+
export type * from "./scene/schema.ts";
|
|
25
|
+
export { FILTER_BUDGET_PX, RENDERER_VERSION, createRenderCache, engineFor, renderScene, sceneRevision, } from "./scene/render.ts";
|
|
26
|
+
export type { AssetResolver, LayerBox, RenderCache, RenderContext, RenderResult, SceneCheck, } from "./scene/render.ts";
|
|
27
|
+
export { MIN_SIZE, SNAP_PX, boundsOf, centerOf, cornersOf, frameTransform, handlePoint, isCorner, localFrame, normalizeAngle, pageFrame, resizeFrame, rotatePoint, snapBox, snapGuides, turnAngle, unionOf, withHeight, } from "./scene/frame.ts";
|
|
28
|
+
export type { Box, Guides, Handle, Point } from "./scene/frame.ts";
|
|
29
|
+
export { DUPLICATE_OFFSET, MAX_PAGES, addLayer, addPage, alignLayers, distributeLayers, duplicateLayers, duplicatePage, findLayer, groupLayers, lockLayers, movePage, moveLayers, removeLayers, removePage, reorderLayers, resizeLayers, rotateLayers, scalePath, selectionFrame, setTextHeight, snapTargets, ungroupLayers, } from "./editor/operations.ts";
|
|
30
|
+
export type { AlignMode, Edit, ZOrder } from "./editor/operations.ts";
|
|
31
|
+
export { HISTORY_LIMIT, applyPatches, diffScenes, emptyHistory, endEditGroup, mergeScenes, rebaseHistory, recordEdit, redoEdit, undoEdit, } from "./editor/history.ts";
|
|
32
|
+
export type { EditHistory, SceneMerge, ScenePatch } from "./editor/history.ts";
|
|
33
|
+
export { POST_FORMATS, POST_TEMPLATES, POST_TEMPLATE_INFO, postTemplate, } from "./posts/templates.ts";
|
|
34
|
+
export type { PostFormat, PostTemplateId, PostTemplateOptions } from "./posts/templates.ts";
|
|
35
|
+
export { pixCode } from "./posts/pix.ts";
|
|
36
|
+
export type { PixDetails } from "./posts/pix.ts";
|
|
37
|
+
export { brandArt, fontFor, logoFragment } from "./scene/brand-art.ts";
|
|
38
|
+
export type { BrandArt, LogoFragment, LogoTone, LogoVariant } from "./scene/brand-art.ts";
|
|
39
|
+
export { qrMatrix, qrPath } from "./scene/qr.ts";
|
|
40
|
+
export type { QrEcc } from "./scene/qr.ts";
|
|
41
|
+
export type { Raster, Rasterizer } from "./files/raster.ts";
|
|
42
|
+
export { GenerateRefusal, TARGETS, generate } from "./files/generate.ts";
|
|
43
|
+
export type { CardPage, DerivedFile, GenerateOptions, TargetId } from "./files/generate.ts";
|
|
44
|
+
export { brandMd, copyFor, headTags, ogAlt, promptMd, publicBrand, signatureHtml, slugOf, webManifest, } from "./files/docs.ts";
|
|
45
|
+
export type { HeadOptions } from "./files/docs.ts";
|
|
46
|
+
export { dtcgTokens, fontStack, fontsCss, registryTheme, tailwindCss, tokensCss, } from "./files/tokens.ts";
|
|
47
|
+
export * as templates from "./files/templates.ts";
|
|
48
|
+
export { check, summarize } from "./check/index.ts";
|
|
49
|
+
export type { CheckContext, CheckFix, CheckLevel, CheckResult } from "./check/index.ts";
|
|
50
|
+
export { DRAFT_FONT_PAIRS, DRAFT_LIMITS, draftBrand, draftFontFiles, draftParams, newDraftSeed, readDraftParams, } from "./draft/draft.ts";
|
|
51
|
+
export type { DraftInput, DraftPicks } from "./draft/draft.ts";
|
|
52
|
+
export { BANNED_WORDS, COPY_LIMITS, checkCopy } from "./check/copy.ts";
|
|
53
|
+
export type { CopyField, CopyIssue } from "./check/copy.ts";
|
|
54
|
+
export { oklabDistance } from "./color/contrast.ts";
|
|
55
|
+
export { dpiChunk, pngChunk } from "./files/png.ts";
|
|
56
|
+
export { IMPORT_LIMITS, IMPORT_MIMES, checkImportEntry, importPath, planImport, } from "./imports/folder.ts";
|
|
57
|
+
export type { ImportCheck, ImportEntry, ImportMime, ImportPlan, ImportSkipReason, } from "./imports/folder.ts";
|
|
58
|
+
export { unzipImport } from "./imports/unzip.ts";
|
|
59
|
+
export type { UnzipResult, UnzippedFile } from "./imports/unzip.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export { VERSION } from "./version.js";
|
|
2
|
+
export { BRAND_SCHEMA_URL, BRAND_SCHEMA_VERSION, COLOR_ROLES, CONTACT_LINK_KINDS, FONT_ROLES, LINK_KINDS, LOCALES, brandJsonSchema, brandSchema, parseBrand, } from "./brand/schema.js";
|
|
3
|
+
export { contrast, formatRatio, luminance, normalizeHex, over, parseHex, toHex, } from "./color/wcag.js";
|
|
4
|
+
export { ensureContrast, fromOklch, isLight, onColor, toOklch } from "./color/contrast.js";
|
|
5
|
+
export { CONTRAST_PAIRS, MOODS, generatePalette } from "./color/palette.js";
|
|
6
|
+
export { safeEqual, sign, verify, LIVE_IMAGE_PURPOSE, canonicalImageMessage, signImageUrl, verifyImageUrl, } from "./sign.js";
|
|
7
|
+
export { FONTSOURCE_CDN, FontLoadError, fetchFontResolver, fontFileName, fontsourceUrl, ideareFontUrl, } from "./text/fonts.js";
|
|
8
|
+
export { TextEngine, loadHarfbuzz } from "./text/engine.js";
|
|
9
|
+
export { FONT_CATALOG } from "./fonts/catalog.js";
|
|
10
|
+
export { FONT_TAGS, catalogFont, catalogVersions, nearestWeight, pickFonts } from "./fonts/pick.js";
|
|
11
|
+
export { OFL_TEXT } from "./fonts/ofl.js";
|
|
12
|
+
export { aiProvenanceXmp, base64, isPng, itxtChunk, packIco, pngSize, readIco, sha256Hex, textChunk, withChunks, } from "./files/png.js";
|
|
13
|
+
export { BADGES, iconBody, iconMark, iconNames, initials, matchIcons, monogramMark, pathBounds, } from "./marks/draw.js";
|
|
14
|
+
export { cleanMarkSvg, markBody, markSvg, parseMark } from "./marks/svg.js";
|
|
15
|
+
export { FORMATS, SCENE_VERSION, layerSchema, parseScene, sceneSchema } from "./scene/schema.js";
|
|
16
|
+
export { FILTER_BUDGET_PX, RENDERER_VERSION, createRenderCache, engineFor, renderScene, sceneRevision, } from "./scene/render.js";
|
|
17
|
+
export { MIN_SIZE, SNAP_PX, boundsOf, centerOf, cornersOf, frameTransform, handlePoint, isCorner, localFrame, normalizeAngle, pageFrame, resizeFrame, rotatePoint, snapBox, snapGuides, turnAngle, unionOf, withHeight, } from "./scene/frame.js";
|
|
18
|
+
export { DUPLICATE_OFFSET, MAX_PAGES, addLayer, addPage, alignLayers, distributeLayers, duplicateLayers, duplicatePage, findLayer, groupLayers, lockLayers, movePage, moveLayers, removeLayers, removePage, reorderLayers, resizeLayers, rotateLayers, scalePath, selectionFrame, setTextHeight, snapTargets, ungroupLayers, } from "./editor/operations.js";
|
|
19
|
+
export { HISTORY_LIMIT, applyPatches, diffScenes, emptyHistory, endEditGroup, mergeScenes, rebaseHistory, recordEdit, redoEdit, undoEdit, } from "./editor/history.js";
|
|
20
|
+
export { POST_FORMATS, POST_TEMPLATES, POST_TEMPLATE_INFO, postTemplate, } from "./posts/templates.js";
|
|
21
|
+
export { pixCode } from "./posts/pix.js";
|
|
22
|
+
export { brandArt, fontFor, logoFragment } from "./scene/brand-art.js";
|
|
23
|
+
export { qrMatrix, qrPath } from "./scene/qr.js";
|
|
24
|
+
export { GenerateRefusal, TARGETS, generate } from "./files/generate.js";
|
|
25
|
+
export { brandMd, copyFor, headTags, ogAlt, promptMd, publicBrand, signatureHtml, slugOf, webManifest, } from "./files/docs.js";
|
|
26
|
+
export { dtcgTokens, fontStack, fontsCss, registryTheme, tailwindCss, tokensCss, } from "./files/tokens.js";
|
|
27
|
+
export * as templates from "./files/templates.js";
|
|
28
|
+
export { check, summarize } from "./check/index.js";
|
|
29
|
+
export { DRAFT_FONT_PAIRS, DRAFT_LIMITS, draftBrand, draftFontFiles, draftParams, newDraftSeed, readDraftParams, } from "./draft/draft.js";
|
|
30
|
+
export { BANNED_WORDS, COPY_LIMITS, checkCopy } from "./check/copy.js";
|
|
31
|
+
export { oklabDistance } from "./color/contrast.js";
|
|
32
|
+
export { dpiChunk, pngChunk } from "./files/png.js";
|
|
33
|
+
export { IMPORT_LIMITS, IMPORT_MIMES, checkImportEntry, importPath, planImport, } from "./imports/folder.js";
|
|
34
|
+
export { unzipImport } from "./imports/unzip.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { LoadedFont, TextEngine } from "../text/engine.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Marks drawn by code (plan D7): a monogram in a shape, or an icon from Lucide matched by the
|
|
4
|
+
* business's words. Both cost nothing and never misspell the name. Icon marks come from an open
|
|
5
|
+
* set, so the studio labels them "not exclusive" (`source.kind: "icon"`).
|
|
6
|
+
*/
|
|
7
|
+
export declare const BADGES: readonly ["circle", "rounded", "square", "hexagon", "none"];
|
|
8
|
+
export type Badge = (typeof BADGES)[number];
|
|
9
|
+
/** The bounding box of absolute path data (control points included, so slightly generous). */
|
|
10
|
+
export declare function pathBounds(d: string): {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
w: number;
|
|
14
|
+
h: number;
|
|
15
|
+
};
|
|
16
|
+
/** Up to two initials: the first letters of the first two words that are not "de", "and" and the like. */
|
|
17
|
+
export declare function initials(name: string, count?: number): string;
|
|
18
|
+
/**
|
|
19
|
+
* A monogram: the initials outlined in the display font, centered on their ink (not their
|
|
20
|
+
* advance), inside a badge. With `badge: "none"` the letters take the fill color on their own.
|
|
21
|
+
*/
|
|
22
|
+
export declare function monogramMark(engine: TextEngine, font: LoadedFont, input: {
|
|
23
|
+
letters: string;
|
|
24
|
+
badge: Badge;
|
|
25
|
+
fill: string;
|
|
26
|
+
ink: string;
|
|
27
|
+
tracking?: number;
|
|
28
|
+
}): string;
|
|
29
|
+
export declare function iconNames(): string[];
|
|
30
|
+
/** The inner markup of a 24 px Lucide icon, or undefined for a name outside the curated set. */
|
|
31
|
+
export declare function iconBody(name: string): string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* An icon mark: the Lucide icon stroked in `ink` inside a badge filled with `fill`. With
|
|
34
|
+
* `badge: "none"` the icon is stroked in the fill color on its own.
|
|
35
|
+
*/
|
|
36
|
+
export declare function iconMark(name: string, input: {
|
|
37
|
+
badge: Badge;
|
|
38
|
+
fill: string;
|
|
39
|
+
ink: string;
|
|
40
|
+
}): string;
|
|
41
|
+
/**
|
|
42
|
+
* Icons ranked for a business, from its name, description and tagline in any of its languages.
|
|
43
|
+
* Longer keywords count twice, since they say more. When nothing matches, a few friendly general icons come back, so there is always something to try.
|
|
44
|
+
*/
|
|
45
|
+
export declare function matchIcons(text: string, limit?: number): {
|
|
46
|
+
name: string;
|
|
47
|
+
score: number;
|
|
48
|
+
}[];
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { fmt } from "../text/engine.js";
|
|
2
|
+
import { LUCIDE_ICONS } from "./icons.data.js";
|
|
3
|
+
/**
|
|
4
|
+
* Marks drawn by code (plan D7): a monogram in a shape, or an icon from Lucide matched by the
|
|
5
|
+
* business's words. Both cost nothing and never misspell the name. Icon marks come from an open
|
|
6
|
+
* set, so the studio labels them "not exclusive" (`source.kind: "icon"`).
|
|
7
|
+
*/
|
|
8
|
+
export const BADGES = ["circle", "rounded", "square", "hexagon", "none"];
|
|
9
|
+
/** The bounding box of absolute path data (control points included, so slightly generous). */
|
|
10
|
+
export function pathBounds(d) {
|
|
11
|
+
const numbers = d.match(/-?(?:\d+\.?\d*|\.\d+)(?:e-?\d+)?/gi)?.map(Number) ?? [];
|
|
12
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
13
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
14
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
15
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
16
|
+
for (let i = 0; i + 1 < numbers.length; i += 2) {
|
|
17
|
+
const x = numbers[i] ?? 0;
|
|
18
|
+
const y = numbers[i + 1] ?? 0;
|
|
19
|
+
minX = Math.min(minX, x);
|
|
20
|
+
maxX = Math.max(maxX, x);
|
|
21
|
+
minY = Math.min(minY, y);
|
|
22
|
+
maxY = Math.max(maxY, y);
|
|
23
|
+
}
|
|
24
|
+
if (minX > maxX)
|
|
25
|
+
return { x: 0, y: 0, w: 0, h: 0 };
|
|
26
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
27
|
+
}
|
|
28
|
+
const STOP_WORDS = new Set([
|
|
29
|
+
"de",
|
|
30
|
+
"da",
|
|
31
|
+
"do",
|
|
32
|
+
"das",
|
|
33
|
+
"dos",
|
|
34
|
+
"e",
|
|
35
|
+
"y",
|
|
36
|
+
"the",
|
|
37
|
+
"and",
|
|
38
|
+
"of",
|
|
39
|
+
"la",
|
|
40
|
+
"el",
|
|
41
|
+
"los",
|
|
42
|
+
"las",
|
|
43
|
+
"&",
|
|
44
|
+
"a",
|
|
45
|
+
"o",
|
|
46
|
+
]);
|
|
47
|
+
/** Up to two initials: the first letters of the first two words that are not "de", "and" and the like. */
|
|
48
|
+
export function initials(name, count = 2) {
|
|
49
|
+
const words = name
|
|
50
|
+
.split(/[\s\-_/.]+/)
|
|
51
|
+
.map((word) => word.replace(/[^\p{L}\p{N}]/gu, ""))
|
|
52
|
+
.filter((word) => word && !STOP_WORDS.has(word.toLowerCase()));
|
|
53
|
+
const letters = words.slice(0, count).map((word) => [...word][0] ?? "");
|
|
54
|
+
return (letters.join("") || [...name.trim()][0] || "?").toLocaleUpperCase();
|
|
55
|
+
}
|
|
56
|
+
function badgeShape(shape, fill) {
|
|
57
|
+
switch (shape) {
|
|
58
|
+
case "circle":
|
|
59
|
+
return `<circle cx="50" cy="50" r="50" fill="${fill}"/>`;
|
|
60
|
+
case "rounded":
|
|
61
|
+
return `<rect width="100" height="100" rx="22" fill="${fill}"/>`;
|
|
62
|
+
case "square":
|
|
63
|
+
return `<rect width="100" height="100" fill="${fill}"/>`;
|
|
64
|
+
case "hexagon":
|
|
65
|
+
return `<path d="M50 0L93.3 25V75L50 100L6.7 75V25Z" fill="${fill}"/>`;
|
|
66
|
+
case "none":
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const SVG_OPEN = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">';
|
|
71
|
+
/**
|
|
72
|
+
* A monogram: the initials outlined in the display font, centered on their ink (not their
|
|
73
|
+
* advance), inside a badge. With `badge: "none"` the letters take the fill color on their own.
|
|
74
|
+
*/
|
|
75
|
+
export function monogramMark(engine, font, input) {
|
|
76
|
+
const letters = input.letters.slice(0, 3);
|
|
77
|
+
const room = input.badge === "none" ? 92 : input.badge === "circle" ? 58 : 64;
|
|
78
|
+
// Measure at 100 px, then scale the ink box into the room.
|
|
79
|
+
const run = engine.shape(font, letters, 100, input.tracking ?? 0);
|
|
80
|
+
const raw = engine.runPath(font, run, 100, 0, 0);
|
|
81
|
+
const box = pathBounds(raw);
|
|
82
|
+
if (box.w === 0)
|
|
83
|
+
throw new TypeError(`The font has no glyphs for "${letters}"`);
|
|
84
|
+
const scale = Math.min(room / box.w, (room * 0.8) / box.h);
|
|
85
|
+
const size = 100 * scale;
|
|
86
|
+
const drawn = engine.runPath(font, engine.shape(font, letters, size, input.tracking ?? 0), size, 0, 0);
|
|
87
|
+
const ink = pathBounds(drawn);
|
|
88
|
+
const dx = 50 - (ink.x + ink.w / 2);
|
|
89
|
+
const dy = 50 - (ink.y + ink.h / 2);
|
|
90
|
+
const color = input.badge === "none" ? input.fill : input.ink;
|
|
91
|
+
return `${SVG_OPEN}${badgeShape(input.badge, input.fill)}<path transform="translate(${fmt(dx)} ${fmt(dy)})" d="${drawn}" fill="${color}"/></svg>`;
|
|
92
|
+
}
|
|
93
|
+
export function iconNames() {
|
|
94
|
+
return Object.keys(LUCIDE_ICONS);
|
|
95
|
+
}
|
|
96
|
+
/** The inner markup of a 24 px Lucide icon, or undefined for a name outside the curated set. */
|
|
97
|
+
export function iconBody(name) {
|
|
98
|
+
return LUCIDE_ICONS[name.replace(/^lucide:/, "")]?.body;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* An icon mark: the Lucide icon stroked in `ink` inside a badge filled with `fill`. With
|
|
102
|
+
* `badge: "none"` the icon is stroked in the fill color on its own.
|
|
103
|
+
*/
|
|
104
|
+
export function iconMark(name, input) {
|
|
105
|
+
const body = iconBody(name);
|
|
106
|
+
if (!body)
|
|
107
|
+
throw new TypeError(`"${name}" is not in the kit's icon set`);
|
|
108
|
+
const color = input.badge === "none" ? input.fill : input.ink;
|
|
109
|
+
const size = input.badge === "none" ? 88 : input.badge === "circle" ? 52 : 58;
|
|
110
|
+
const offset = (100 - size) / 2;
|
|
111
|
+
const scale = size / 24;
|
|
112
|
+
return `${SVG_OPEN}${badgeShape(input.badge, input.fill)}<g transform="translate(${fmt(offset)} ${fmt(offset)}) scale(${fmt(scale)})" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${body}</g></svg>`;
|
|
113
|
+
}
|
|
114
|
+
/** Lowercase, without accents, so "Padaria Pão" matches "padar" and "pao". */
|
|
115
|
+
function plain(text) {
|
|
116
|
+
return ` ${text
|
|
117
|
+
.normalize("NFD")
|
|
118
|
+
.replace(/\p{M}/gu, "")
|
|
119
|
+
.toLowerCase()
|
|
120
|
+
.replace(/[^a-z0-9\s-]/g, " ")
|
|
121
|
+
.replace(/\s+/g, " ")} `;
|
|
122
|
+
}
|
|
123
|
+
const FALLBACK_ICONS = ["sparkles", "star", "heart", "store", "leaf", "sun"];
|
|
124
|
+
/**
|
|
125
|
+
* Icons ranked for a business, from its name, description and tagline in any of its languages.
|
|
126
|
+
* Longer keywords count twice, since they say more. When nothing matches, a few friendly general icons come back, so there is always something to try.
|
|
127
|
+
*/
|
|
128
|
+
export function matchIcons(text, limit = 6) {
|
|
129
|
+
const haystack = plain(text);
|
|
130
|
+
const ranked = Object.entries(LUCIDE_ICONS)
|
|
131
|
+
.map(([name, { words }]) => {
|
|
132
|
+
let score = 0;
|
|
133
|
+
// A keyword matches the start of a word; one with a trailing space, the whole word.
|
|
134
|
+
for (const word of words)
|
|
135
|
+
if (haystack.includes(` ${word}`))
|
|
136
|
+
score += word.length >= 5 ? 2 : 1;
|
|
137
|
+
return { name, score };
|
|
138
|
+
})
|
|
139
|
+
.filter((entry) => entry.score > 0)
|
|
140
|
+
.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
|
|
141
|
+
.slice(0, limit);
|
|
142
|
+
for (const name of FALLBACK_ICONS) {
|
|
143
|
+
if (ranked.length >= limit)
|
|
144
|
+
break;
|
|
145
|
+
if (!ranked.some((entry) => entry.name === name))
|
|
146
|
+
ranked.push({ name, score: 0 });
|
|
147
|
+
}
|
|
148
|
+
return ranked;
|
|
149
|
+
}
|