@opentagcloud/core 0.3.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/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @opentagcloud/core
2
+
3
+ Framework-agnostic core of [openTagCloud](https://github.com/hkoren/openTagCloud)
4
+ — a dependency-free, self-packing, SSR-friendly tag cloud. This package is the
5
+ layout engine plus a vanilla-JS renderer; framework adapters
6
+ (`opentagcloud` for Svelte, `@opentagcloud/react`, `@opentagcloud/vue`,
7
+ `@opentagcloud/angular`, `@opentagcloud/solid`, `@opentagcloud/next`) are thin
8
+ wrappers over it.
9
+
10
+ ## Vanilla usage
11
+
12
+ ```js
13
+ import { renderTagCloud } from '@opentagcloud/core';
14
+
15
+ const cloud = renderTagCloud(document.querySelector('#cloud'), [
16
+ { label: 'JavaScript', weight: 95, href: '/tags/javascript' },
17
+ { label: 'TypeScript', weight: 88 },
18
+ { label: 'Rust', weight: 60, color: 'tomato' },
19
+ ]);
20
+
21
+ cloud.update(newItems); // replace items and re-pack
22
+ cloud.destroy(); // stop observing, remove tags
23
+ ```
24
+
25
+ Give the container a sized parent; the cloud packs to its width and grows its
26
+ `min-height` to fit. Styles are injected at runtime — no CSS setup needed. For
27
+ server-rendered pages, also ship `@opentagcloud/core/styles.css` so the no-JS
28
+ fallback (justified inline flow) is styled before hydration.
29
+
30
+ ## Script tag / custom element (no build step)
31
+
32
+ The package also ships a browser global (`dist/opentagcloud.vanilla.js`, the
33
+ `unpkg`/`jsdelivr` entry) that registers a light-DOM `<otc-tag-cloud>` custom
34
+ element and exposes `openTagCloud.mount(container, items, options)`:
35
+
36
+ ```html
37
+ <script src="https://unpkg.com/@opentagcloud/core/dist/opentagcloud.vanilla.js"></script>
38
+
39
+ <otc-tag-cloud
40
+ style="height: 320px"
41
+ items='[{ "label": "Rust", "weight": 60 }]'
42
+ >
43
+ </otc-tag-cloud>
44
+ ```
45
+
46
+ `mount()` and `defineElement()` are also exported from the package root and the
47
+ `./vanilla` subpath for ES-module consumers (call `defineElement()` to register
48
+ the element). Both are thin wrappers over `renderTagCloud` — same engine, same
49
+ markup contract. Try the
50
+ [live vanilla example](https://hkoren.github.io/openTagCloud/) or view the
51
+ [`example source`](examples/vanilla.html).
52
+
53
+ ## SSR height estimation
54
+
55
+ `estimateCloudHeight(items, width, options?)` is pure and DOM-free: emit it as
56
+ the container's `min-height` during SSR to reserve space and avoid layout
57
+ shift (roughly ±25% of the real packed height).
58
+
59
+ ## Building your own adapter
60
+
61
+ - `prepareTags(items, { minPx, maxPx })` — pure and DOM-free: returns each
62
+ tag's font size, opacity, key, display text, and inline style. Call it in
63
+ your template layer (works during SSR).
64
+ - Render one element per prepared tag with its `className`, `style`, `title`,
65
+ and `data-fs` / `data-weight` / `data-key` attributes, inside a container
66
+ with class `otc-cloud`. Render its `parts` as the content — `nowrap` parts go
67
+ in a `<span class="otc-nb">` so lines never break at a hyphen while the DOM
68
+ text stays byte-identical to the label.
69
+ - `new TagCloudLayout(container, { fill }).attach()` on mount; `refresh()`
70
+ after the tags change; `destroy()` on teardown. All methods are no-ops
71
+ without a DOM.
72
+
73
+ See the [project README](https://github.com/hkoren/openTagCloud#readme) for
74
+ props, theming, and how the packer works.
75
+
76
+ ## License
77
+
78
+ MIT © Henry Koren
@@ -0,0 +1,19 @@
1
+ import type { TagCloudItem } from './types.js';
2
+ import { type PrepareOptions } from './prepare.js';
3
+ /**
4
+ * Estimate the packed height of a cloud for a given container width — without
5
+ * a DOM, so it can run during SSR. Emit it as the container's `min-height` to
6
+ * reserve space before hydration and avoid layout shift (CLS):
7
+ *
8
+ * ```svelte
9
+ * <div style="min-height: {estimateCloudHeight(items, 720)}px">
10
+ * <TagCloud {items} />
11
+ * </div>
12
+ * ```
13
+ *
14
+ * It mirrors the packer's own area-based box-height formula, with text
15
+ * measured by a character-width heuristic instead of the real font metrics,
16
+ * so expect roughly ±25% — enough to absorb the fallback→packed reflow. A
17
+ * fixed-height container is still the zero-shift option.
18
+ */
19
+ export declare function estimateCloudHeight(items: TagCloudItem[], width: number, options?: PrepareOptions): number;
@@ -0,0 +1,48 @@
1
+ import { prepareTags } from './prepare.js';
2
+ import { PAD, LOOSEN, widthFactor } from './layout.js';
3
+ /**
4
+ * Estimate the packed height of a cloud for a given container width — without
5
+ * a DOM, so it can run during SSR. Emit it as the container's `min-height` to
6
+ * reserve space before hydration and avoid layout shift (CLS):
7
+ *
8
+ * ```svelte
9
+ * <div style="min-height: {estimateCloudHeight(items, 720)}px">
10
+ * <TagCloud {items} />
11
+ * </div>
12
+ * ```
13
+ *
14
+ * It mirrors the packer's own area-based box-height formula, with text
15
+ * measured by a character-width heuristic instead of the real font metrics,
16
+ * so expect roughly ±25% — enough to absorb the fallback→packed reflow. A
17
+ * fixed-height container is still the zero-shift option.
18
+ */
19
+ export function estimateCloudHeight(items, width, options = {}) {
20
+ if (!items.length || width < 2)
21
+ return 0;
22
+ const wf = widthFactor(width);
23
+ const wide = width >= 380;
24
+ // average advance width of a bold system-font glyph, in em
25
+ const GLYPH = 0.58;
26
+ let area = 0;
27
+ let tallest = 0;
28
+ for (const p of prepareTags(items, options)) {
29
+ const fs = Math.max(8, p.fontPx * wf);
30
+ const textW = p.text.length * fs * GLYPH;
31
+ let w;
32
+ let lines = 1;
33
+ if (wide) {
34
+ // nowrap; the packer shrinks anything wider than the container
35
+ w = Math.min(textW, width);
36
+ }
37
+ else {
38
+ const maxW = Math.min(6.5 * fs, width);
39
+ lines = Math.max(1, Math.ceil(textW / maxW));
40
+ w = Math.min(textW, maxW);
41
+ }
42
+ const h = lines * fs * 0.95; // line-height 0.95
43
+ area += (w + PAD) * (h + PAD);
44
+ tallest = Math.max(tallest, h);
45
+ }
46
+ return Math.ceil(Math.max((area * LOOSEN) / width, tallest));
47
+ }
48
+ //# sourceMappingURL=estimate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estimate.js","sourceRoot":"","sources":["../src/estimate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAuB,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEvD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAqB,EACrB,KAAa,EACb,UAA0B,EAAE;IAE5B,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IACzC,MAAM,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAC9B,MAAM,IAAI,GAAG,KAAK,IAAI,GAAG,CAAC;IAC1B,2DAA2D;IAC3D,MAAM,KAAK,GAAG,IAAI,CAAC;IACnB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;QAC5C,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;QACtC,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,KAAK,CAAC;QACzC,IAAI,CAAS,CAAC;QACd,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,IAAI,EAAE,CAAC;YACT,+DAA+D;YAC/D,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC7B,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,EAAE,KAAK,CAAC,CAAC;YACvC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC;YAC7C,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAC5B,CAAC;QACD,MAAM,CAAC,GAAG,KAAK,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,mBAAmB;QAChD,IAAI,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;QAC9B,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,MAAM,CAAC,GAAG,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;AAC/D,CAAC"}
@@ -0,0 +1,12 @@
1
+ export type { TagCloudItem, Fill } from './types.js';
2
+ export { prepareTags, keyOf } from './prepare.js';
3
+ export type { PreparedTag, PrepareOptions, LabelPart } from './prepare.js';
4
+ export { TagCloudLayout } from './layout.js';
5
+ export type { TagCloudLayoutOptions } from './layout.js';
6
+ export { renderTagCloud, createTagElement } from './render.js';
7
+ export type { RenderOptions, TagCloudHandle } from './render.js';
8
+ export { injectStyles, TAG_CLOUD_CSS } from './styles.js';
9
+ export { makeRng } from './rng.js';
10
+ export { mount, defineElement } from './vanilla.js';
11
+ export type { MountOptions, CloudHandle } from './vanilla.js';
12
+ export { estimateCloudHeight } from './estimate.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { prepareTags, keyOf } from './prepare.js';
2
+ export { TagCloudLayout } from './layout.js';
3
+ export { renderTagCloud, createTagElement } from './render.js';
4
+ export { injectStyles, TAG_CLOUD_CSS } from './styles.js';
5
+ export { makeRng } from './rng.js';
6
+ export { mount, defineElement } from './vanilla.js';
7
+ export { estimateCloudHeight } from './estimate.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/D,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,59 @@
1
+ import type { Fill } from './types.js';
2
+ export interface TagCloudLayoutOptions {
3
+ /** Also spread terms vertically to fill the container's height. */
4
+ fill?: Fill;
5
+ /**
6
+ * Add the component stylesheet to the document on `attach()` (default true).
7
+ * Set false if you serve `@opentagcloud/core/styles.css` yourself.
8
+ */
9
+ injectStyles?: boolean;
10
+ /**
11
+ * Keep unchanged tags in place when `refresh()` runs after an item update:
12
+ * tags whose measured box still fits at its old position stay put; only
13
+ * new, resized, or displaced tags are re-placed (seeded from their old
14
+ * position, so they move minimally). Falls back to a full re-pack when the
15
+ * width changed or more than 40% of tags would move. Off by default — a
16
+ * full re-pack always produces the canonical deterministic layout.
17
+ */
18
+ incremental?: boolean;
19
+ }
20
+ /** @internal Gap between tag boxes; shared with estimateCloudHeight(). */
21
+ export declare const PAD = 5;
22
+ /** @internal Area slack factor used to derive the packing box height. */
23
+ export declare const LOOSEN = 1.4;
24
+ /** @internal container width → font: shrink on narrow (mobile), grow modestly when wide. */
25
+ export declare const widthFactor: (w: number) => number;
26
+ /**
27
+ * The self-packing layout engine. Framework-free: it operates on a container
28
+ * element whose children carry class `otc-tag` plus three data attributes —
29
+ * `data-fs` (base font px), `data-weight`, and `data-key` (scatter seed) — all
30
+ * emitted by `prepareTags()`. How those elements got into the DOM (a framework
31
+ * template, `renderTagCloud()`, or server-rendered HTML) is irrelevant.
32
+ *
33
+ * Lifecycle: construct with the container, call `attach()` once it's in the
34
+ * document, `refresh()` after the tag elements change, `destroy()` on teardown.
35
+ * All methods are no-ops without a DOM, so adapters can call them under SSR.
36
+ *
37
+ * Re-packs and re-distributions animate: moved tags FLIP from their previous
38
+ * visual position, new tags scale in. Duration/easing come from the
39
+ * `--otc-move-transition` custom property (default `250ms` ease-out); set it
40
+ * to `0s` or `none` to disable, and `prefers-reduced-motion: reduce` disables
41
+ * it automatically. The initial pack never animates.
42
+ */
43
+ export declare class TagCloudLayout {
44
+ #private;
45
+ constructor(root: HTMLElement, options?: TagCloudLayoutOptions);
46
+ /** Start observing the container and pack the initial layout. */
47
+ attach(): void;
48
+ /** Re-pack after the tag elements changed (items added/removed/re-weighted). */
49
+ refresh(): void;
50
+ /** Change the fill mode; only term positions move, never the container height. */
51
+ setFill(fill: Fill | undefined): void;
52
+ /** Stop observing. The current positions are left in place. */
53
+ destroy(): void;
54
+ pack(): void;
55
+ distribute(flipFrom?: Map<string, {
56
+ x: number;
57
+ y: number;
58
+ }> | null): void;
59
+ }