pretrunc 0.0.0-stage → 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.
Files changed (52) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/DESIGN.md +235 -0
  3. package/LICENSE +21 -0
  4. package/README.md +200 -2
  5. package/dist/cut-D7qi2yZl.js +356 -0
  6. package/dist/cut-D7qi2yZl.js.map +1 -0
  7. package/dist/cut.d.ts +75 -0
  8. package/dist/dom-CPL0-qc3.js +52 -0
  9. package/dist/dom-CPL0-qc3.js.map +1 -0
  10. package/dist/dom.d.ts +22 -0
  11. package/dist/dom.js +1 -0
  12. package/dist/face-pICHAjvk.js +68 -0
  13. package/dist/face-pICHAjvk.js.map +1 -0
  14. package/dist/face.d.ts +46 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +3 -0
  17. package/dist/lines-DOj2DQLl.js +334 -0
  18. package/dist/lines-DOj2DQLl.js.map +1 -0
  19. package/dist/lines.d.ts +32 -0
  20. package/dist/lines.js +1 -0
  21. package/dist/metrics-C2jv4JVL.js +49 -0
  22. package/dist/metrics-C2jv4JVL.js.map +1 -0
  23. package/dist/metrics-cli.js +19 -0
  24. package/dist/metrics-cli.js.map +1 -0
  25. package/dist/metrics.d.ts +36 -0
  26. package/dist/metrics.js +73 -0
  27. package/dist/metrics.js.map +1 -0
  28. package/dist/react-CnQfjWyS.js +156 -0
  29. package/dist/react-CnQfjWyS.js.map +1 -0
  30. package/dist/react-lines.d.ts +27 -0
  31. package/dist/react-lines.js +74 -0
  32. package/dist/react-lines.js.map +1 -0
  33. package/dist/react.d.ts +81 -0
  34. package/dist/react.js +1 -0
  35. package/dist/solid-BWC32VMg.js +155 -0
  36. package/dist/solid-BWC32VMg.js.map +1 -0
  37. package/dist/solid-lines.d.ts +27 -0
  38. package/dist/solid-lines.js +90 -0
  39. package/dist/solid-lines.js.map +1 -0
  40. package/dist/solid.d.ts +77 -0
  41. package/dist/solid.js +1 -0
  42. package/package.json +145 -3
  43. package/src/cut.ts +596 -0
  44. package/src/dom.ts +59 -0
  45. package/src/face.ts +112 -0
  46. package/src/index.ts +15 -0
  47. package/src/lines.ts +585 -0
  48. package/src/metrics.ts +106 -0
  49. package/src/react-lines.ts +99 -0
  50. package/src/react.ts +251 -0
  51. package/src/solid-lines.tsx +89 -0
  52. package/src/solid.tsx +225 -0
@@ -0,0 +1,73 @@
1
+ import { r as face } from "./face-pICHAjvk.js";
2
+ //#region src/metrics.ts
3
+ var UNKNOWN = 1.5;
4
+ var decoded = /* @__PURE__ */ new WeakMap();
5
+ var decode = (metrics) => {
6
+ let d = decoded.get(metrics);
7
+ if (!d) {
8
+ const codes = Array.from(metrics.chars, (c) => c.codePointAt(0));
9
+ const advance = /* @__PURE__ */ new Map();
10
+ for (let i = 0; i < codes.length; i++) advance.set(codes[i], metrics.advances[i]);
11
+ const kern = /* @__PURE__ */ new Map();
12
+ const k = metrics.kerning;
13
+ for (let i = 0; i < k.length; i += 3) kern.set(codes[k[i]] * 1114112 + codes[k[i + 1]], k[i + 2]);
14
+ decoded.set(metrics, d = {
15
+ advance,
16
+ kern
17
+ });
18
+ }
19
+ return d;
20
+ };
21
+ var up = (units, scale) => Math.ceil(units * scale * 60) / 60;
22
+ /** Measures text in `metrics` at `px` px: each character's advance plus each pair's kerning. */
23
+ var measure = (metrics, px, text) => {
24
+ const { advance, kern } = decode(metrics);
25
+ const em = metrics.unitsPerEm;
26
+ const scale = px / em;
27
+ let width = 0;
28
+ let glyph = 0;
29
+ let previous = -1;
30
+ for (const char of text) {
31
+ const code = char.codePointAt(0);
32
+ if (previous >= 0) width += up(glyph + (kern.get(previous * 1114112 + code) ?? 0), scale);
33
+ glyph = advance.get(code) ?? UNKNOWN * em;
34
+ previous = code;
35
+ }
36
+ return previous < 0 ? 0 : width + up(glyph, scale);
37
+ };
38
+ /** A face that measures with `metrics` at `px` px, for `prepare` with no canvas. */
39
+ var tableFace = (metrics, px) => face((text) => measure(metrics, px, text));
40
+ /**
41
+ * A stand-in for `OffscreenCanvas` that measures with tables instead of a font engine,
42
+ * for pretext and `pretrunc/lines` where there is no canvas: install it as
43
+ * `globalThis.OffscreenCanvas` before the first `prepareLines`. The context reads the
44
+ * size and the first family with a table from its `font`, and falls back to the first.
45
+ */
46
+ var tableCanvas = (tables) => class {
47
+ getContext() {
48
+ let px = 16;
49
+ let metrics = tables[0];
50
+ return {
51
+ get font() {
52
+ return `${px}px ${metrics.family}`;
53
+ },
54
+ set font(font) {
55
+ const parts = /([\d.]+)px(?:\/\S+)?\s+(.*)$/.exec(font);
56
+ px = Number.parseFloat(parts?.[1] ?? "16");
57
+ metrics = tables[0];
58
+ search: for (const family of (parts?.[2] ?? "").split(",")) {
59
+ const name = family.trim().replace(/^["']|["']$/g, "");
60
+ for (const table of tables) if (table.family === name) {
61
+ metrics = table;
62
+ break search;
63
+ }
64
+ }
65
+ },
66
+ measureText: (text) => ({ width: measure(metrics, px, text) })
67
+ };
68
+ }
69
+ };
70
+ //#endregion
71
+ export { measure, tableCanvas, tableFace };
72
+
73
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","names":[],"sources":["../src/metrics.ts"],"sourcesContent":["import { type Face, face } from \"./face.ts\";\n\n/**\n * A font's widths, written by `scripts/metrics.ts` from the font file: what a face needs\n * to measure text with no canvas, on a server, in a worker, or before the font loads.\n */\nexport interface Metrics {\n /** The family it was read from, as CSS names it. */\n readonly family: string;\n readonly unitsPerEm: number;\n /** The characters covered, in order. */\n readonly chars: string;\n /** Each covered character's advance, in font units, in the order of `chars`. */\n readonly advances: ReadonlyArray<number>;\n /** Kerned pairs as triples: first's index in `chars`, second's, adjustment in font units. */\n readonly kerning: ReadonlyArray<number>;\n}\n\n// A character the table doesn't cover comes from some fallback font, so it is counted\n// wider than any fallback glyph a cut is likely to meet, CJK and emoji included.\nconst UNKNOWN = 1.5;\n\ninterface Decoded {\n readonly advance: Map<number, number>;\n readonly kern: Map<number, number>;\n}\nconst decoded = new WeakMap<Metrics, Decoded>();\nconst decode = (metrics: Metrics): Decoded => {\n let d = decoded.get(metrics);\n if (!d) {\n const codes = Array.from(metrics.chars, (c) => c.codePointAt(0)!);\n const advance = new Map<number, number>();\n for (let i = 0; i < codes.length; i++) advance.set(codes[i]!, metrics.advances[i]!);\n const kern = new Map<number, number>();\n const k = metrics.kerning;\n for (let i = 0; i < k.length; i += 3) {\n kern.set(codes[k[i]!]! * 0x110000 + codes[k[i + 1]!]!, k[i + 2]!);\n }\n decoded.set(metrics, (d = { advance, kern }));\n }\n return d;\n};\n\n// Firefox rounds each glyph's advance, kerning to the next glyph included, to the nearest\n// 1/60 px, its layout unit, so a run of glyphs that round up lays out wider than their\n// sum: 0.4 px over 352 characters of Inter at 15 px. Each glyph is rounded up instead,\n// never less than Firefox and never less than the exact widths Chrome and WebKit use.\nconst up = (units: number, scale: number) => Math.ceil(units * scale * 60) / 60;\n\n/** Measures text in `metrics` at `px` px: each character's advance plus each pair's kerning. */\nexport const measure = (metrics: Metrics, px: number, text: string): number => {\n const { advance, kern } = decode(metrics);\n const em = metrics.unitsPerEm;\n const scale = px / em;\n let width = 0;\n // The previous glyph's advance in font units, waiting for its kerning to this one.\n let glyph = 0;\n let previous = -1;\n for (const char of text) {\n const code = char.codePointAt(0)!;\n if (previous >= 0) width += up(glyph + (kern.get(previous * 0x110000 + code) ?? 0), scale);\n glyph = advance.get(code) ?? UNKNOWN * em;\n previous = code;\n }\n return previous < 0 ? 0 : width + up(glyph, scale);\n};\n\n/** A face that measures with `metrics` at `px` px, for `prepare` with no canvas. */\nexport const tableFace = (metrics: Metrics, px: number): Face =>\n face((text) => measure(metrics, px, text));\n\n/**\n * A stand-in for `OffscreenCanvas` that measures with tables instead of a font engine,\n * for pretext and `pretrunc/lines` where there is no canvas: install it as\n * `globalThis.OffscreenCanvas` before the first `prepareLines`. The context reads the\n * size and the first family with a table from its `font`, and falls back to the first.\n */\nexport const tableCanvas = (tables: ReadonlyArray<Metrics>) =>\n class {\n getContext() {\n let px = 16;\n let metrics = tables[0]!;\n return {\n get font() {\n return `${px}px ${metrics.family}`;\n },\n set font(font: string) {\n // `[style] [weight] 15px[/line-height] \"Family\", fallback`\n const parts = /([\\d.]+)px(?:\\/\\S+)?\\s+(.*)$/.exec(font);\n px = Number.parseFloat(parts?.[1] ?? \"16\");\n metrics = tables[0]!;\n // The first family named that has a table.\n search: for (const family of (parts?.[2] ?? \"\").split(\",\")) {\n const name = family.trim().replace(/^[\"']|[\"']$/g, \"\");\n for (const table of tables) {\n if (table.family === name) {\n metrics = table;\n break search;\n }\n }\n }\n },\n measureText: (text: string) => ({ width: measure(metrics, px, text) }),\n };\n }\n };\n"],"mappings":";;AAoBA,IAAM,UAAU;AAMhB,IAAM,0BAAU,IAAI,QAA0B;AAC9C,IAAM,UAAU,YAA8B;CAC5C,IAAI,IAAI,QAAQ,IAAI,OAAO;CAC3B,IAAI,CAAC,GAAG;EACN,MAAM,QAAQ,MAAM,KAAK,QAAQ,QAAQ,MAAM,EAAE,YAAY,CAAC,CAAE;EAChE,MAAM,0BAAU,IAAI,IAAoB;EACxC,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,QAAQ,IAAI,MAAM,IAAK,QAAQ,SAAS,EAAG;EAClF,MAAM,uBAAO,IAAI,IAAoB;EACrC,MAAM,IAAI,QAAQ;EAClB,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,GACjC,KAAK,IAAI,MAAM,EAAE,MAAQ,UAAW,MAAM,EAAE,IAAI,KAAO,EAAE,IAAI,EAAG;EAElE,QAAQ,IAAI,SAAU,IAAI;GAAE;GAAS;EAAK,CAAE;CAC9C;CACA,OAAO;AACT;AAMA,IAAM,MAAM,OAAe,UAAkB,KAAK,KAAK,QAAQ,QAAQ,EAAE,IAAI;;AAG7E,IAAa,WAAW,SAAkB,IAAY,SAAyB;CAC7E,MAAM,EAAE,SAAS,SAAS,OAAO,OAAO;CACxC,MAAM,KAAK,QAAQ;CACnB,MAAM,QAAQ,KAAK;CACnB,IAAI,QAAQ;CAEZ,IAAI,QAAQ;CACZ,IAAI,WAAW;CACf,KAAK,MAAM,QAAQ,MAAM;EACvB,MAAM,OAAO,KAAK,YAAY,CAAC;EAC/B,IAAI,YAAY,GAAG,SAAS,GAAG,SAAS,KAAK,IAAI,WAAW,UAAW,IAAI,KAAK,IAAI,KAAK;EACzF,QAAQ,QAAQ,IAAI,IAAI,KAAK,UAAU;EACvC,WAAW;CACb;CACA,OAAO,WAAW,IAAI,IAAI,QAAQ,GAAG,OAAO,KAAK;AACnD;;AAGA,IAAa,aAAa,SAAkB,OAC1C,MAAM,SAAS,QAAQ,SAAS,IAAI,IAAI,CAAC;;;;;;;AAQ3C,IAAa,eAAe,WAC1B,MAAM;CACJ,aAAa;EACX,IAAI,KAAK;EACT,IAAI,UAAU,OAAO;EACrB,OAAO;GACL,IAAI,OAAO;IACT,OAAO,GAAG,GAAG,KAAK,QAAQ;GAC5B;GACA,IAAI,KAAK,MAAc;IAErB,MAAM,QAAQ,+BAA+B,KAAK,IAAI;IACtD,KAAK,OAAO,WAAW,QAAQ,MAAM,IAAI;IACzC,UAAU,OAAO;IAEjB,QAAQ,KAAK,MAAM,WAAW,QAAQ,MAAM,GAAA,CAAI,MAAM,GAAG,GAAG;KAC1D,MAAM,OAAO,OAAO,KAAK,CAAC,CAAC,QAAQ,gBAAgB,EAAE;KACrD,KAAK,MAAM,SAAS,QAClB,IAAI,MAAM,WAAW,MAAM;MACzB,UAAU;MACV,MAAM;KACR;IAEJ;GACF;GACA,cAAc,UAAkB,EAAE,OAAO,QAAQ,SAAS,IAAI,IAAI,EAAE;EACtE;CACF;AACF"}
@@ -0,0 +1,156 @@
1
+ import { c as widthOf, i as faceOf } from "./face-pICHAjvk.js";
2
+ import { a as prepare, l as truncated, r as head, s as tail, t as cut } from "./cut-D7qi2yZl.js";
3
+ import { i as spacingOf, n as observe, r as onFonts, t as fontOf } from "./dom-CPL0-qc3.js";
4
+ import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
+ //#region src/react.ts
6
+ var Fonts = createContext({
7
+ font: void 0,
8
+ letterSpacing: void 0
9
+ });
10
+ /**
11
+ * Gives every `Truncate`, `Clamp` and `useBox` inside the font and letter spacing their
12
+ * text renders in, once for a list. Each would otherwise read its element's computed
13
+ * style when it mounts: 5,000 rows take 4 to 5 ms of style reads. Cuts follow what's
14
+ * given, so give what the rows really render in.
15
+ */
16
+ function FontProvider(props) {
17
+ const { font, letterSpacing } = props;
18
+ const value = useMemo(() => ({
19
+ font,
20
+ letterSpacing
21
+ }), [font, letterSpacing]);
22
+ return createElement(Fonts, { value }, props.children);
23
+ }
24
+ /**
25
+ * The width and font text is cut to: the ones given, then a `FontProvider`'s, or an
26
+ * element's. The width is a store, not state, so a component can read what it derives
27
+ * from it with `useSyncExternalStore` and render only when that changes.
28
+ */
29
+ var useBox = (props) => {
30
+ const provided = useContext(Fonts);
31
+ const given = props.font ?? provided.font;
32
+ const givenSpacing = props.letterSpacing ?? provided.letterSpacing;
33
+ const [element, setElement] = useState(null);
34
+ const [computed, setComputed] = useState();
35
+ const [fonts, setFonts] = useState(0);
36
+ const observed = useRef(void 0);
37
+ useEffect(() => onFonts(() => setFonts((n) => n + 1)), []);
38
+ const readFont = given === void 0;
39
+ const readSpacing = givenSpacing === void 0;
40
+ const ref = useCallback((e) => {
41
+ setElement(e);
42
+ if (!e || !readFont && !readSpacing) return;
43
+ const style = getComputedStyle(e);
44
+ setComputed({
45
+ font: fontOf(e, style),
46
+ spacing: spacingOf(e, style)
47
+ });
48
+ }, [readFont, readSpacing]);
49
+ const observing = props.width === void 0;
50
+ const subscribe = useCallback((change) => {
51
+ if (!element || !observing) return () => {};
52
+ return observe(element, (width) => {
53
+ observed.current = width;
54
+ change();
55
+ });
56
+ }, [element, observing]);
57
+ const width = props.width;
58
+ return {
59
+ ref,
60
+ font: given ?? computed?.font,
61
+ spacing: givenSpacing ?? computed?.spacing ?? 0,
62
+ fonts,
63
+ subscribe,
64
+ width: () => width ?? observed.current
65
+ };
66
+ };
67
+ /**
68
+ * `protect` kept by what it says, not by identity: an inline literal is new on every
69
+ * render, and this returns one value while its patterns, weights and wholes stay the same.
70
+ */
71
+ var useProtect = (protect) => {
72
+ let key = "";
73
+ if (protect) for (const given of "length" in protect ? protect : [protect]) {
74
+ const { pattern, weight = 1, whole = false } = given instanceof RegExp ? { pattern: given } : given;
75
+ key += `${pattern}${weight}${whole} `;
76
+ }
77
+ const [kept, setKept] = useState({
78
+ key,
79
+ protect
80
+ });
81
+ if (kept.key !== key) setKept({
82
+ key,
83
+ protect
84
+ });
85
+ return kept.key === key ? kept.protect : protect;
86
+ };
87
+ var useCutWith = (props, apart) => {
88
+ const box = useBox(props);
89
+ const { font, spacing, fonts } = box;
90
+ const { text } = props;
91
+ const protect = useProtect(props.protect);
92
+ const prepared = useMemo(() => font === void 0 ? void 0 : prepare(text, font, {
93
+ ...protect ? { protect } : {},
94
+ letterSpacing: spacing
95
+ }), [
96
+ text,
97
+ font,
98
+ spacing,
99
+ protect,
100
+ fonts
101
+ ]);
102
+ const snapshot = () => {
103
+ const width = box.width();
104
+ if (prepared === void 0 || width === void 0 || font === void 0) return void 0;
105
+ const ellipsis = props.ellipsis ?? "…";
106
+ return apart && typeof ellipsis === "string" ? cut(prepared, width, {
107
+ ...props,
108
+ ellipsis: widthOf(faceOf(font), ellipsis) + spacing * ellipsis.length
109
+ }) : cut(prepared, width, props);
110
+ };
111
+ const result = useSyncExternalStore(box.subscribe, snapshot, () => void 0);
112
+ return {
113
+ ref: box.ref,
114
+ prepared,
115
+ cut: result
116
+ };
117
+ };
118
+ /** The cut of `props.text` for an element's width and font, or for the ones given. */
119
+ var useCut = (props) => useCutWith(props, props.separateEllipsis === true);
120
+ var line = {
121
+ display: "block",
122
+ overflow: "hidden",
123
+ whiteSpace: "nowrap",
124
+ textOverflow: "ellipsis"
125
+ };
126
+ var collapsed = { fontSize: 0 };
127
+ var unselectable = {
128
+ userSelect: "none",
129
+ WebkitUserSelect: "none"
130
+ };
131
+ /** One line of text, cut to fit its box. */
132
+ function Truncate(props) {
133
+ const full = props.render === "full";
134
+ const handle = useCutWith(props, full);
135
+ const { prepared: p, cut: c } = handle;
136
+ const ellipsis = props.ellipsis ?? "…";
137
+ const cutNow = p !== void 0 && c !== void 0 && truncated(c);
138
+ const text = p?.text ?? props.text;
139
+ const kept = cutNow ? text.slice(0, head(c)) : text;
140
+ const rest = cutNow ? text.slice(tail(c)) : "";
141
+ const own = {
142
+ ref: handle.ref,
143
+ className: props.className,
144
+ style: line,
145
+ title: !full && cutNow ? text : void 0
146
+ };
147
+ if (!full) return createElement("span", own, kept + (cutNow ? ellipsis + rest : ""));
148
+ return createElement("span", own, kept, createElement("span", { style: collapsed }, cutNow ? text.slice(head(c), tail(c)) : ""), cutNow ? createElement("span", {
149
+ "aria-hidden": true,
150
+ style: unselectable
151
+ }, ellipsis) : null, rest);
152
+ }
153
+ //#endregion
154
+ export { useProtect as a, useCut as i, Truncate as n, useBox as r, FontProvider as t };
155
+
156
+ //# sourceMappingURL=react-CnQfjWyS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-CnQfjWyS.js","names":[],"sources":["../src/react.ts"],"sourcesContent":["// Written with createElement, not JSX: every .tsx file here compiles as Solid, so plain\n// .ts keeps the React adapter clear of the Solid compiler without a second JSX setup.\nimport {\n createContext,\n createElement,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport {\n cut,\n type Cut,\n type CutOptions,\n head,\n prepare,\n type Prepared,\n type Protect,\n tail,\n truncated,\n} from \"./cut.ts\";\nimport { fontOf, observe, onFonts, spacingOf } from \"./dom.ts\";\nimport { faceOf, widthOf } from \"./face.ts\";\n\nexport interface BoxProps {\n /** The room in px. Left out, it is observed from the element. */\n readonly width?: number;\n /** A CSS font shorthand. Left out, it is read from the element's computed style. */\n readonly font?: string;\n /** CSS `letter-spacing` in px. Left out, it is read from the element's computed style. */\n readonly letterSpacing?: number;\n}\n\nexport interface Box {\n /** Give it the element whose width and font the text follows. */\n readonly ref: (element: Element | null) => void;\n /** The font given, or the element's; undefined until it mounts. */\n readonly font: string | undefined;\n /** The letter spacing given, or the element's, in px. */\n readonly spacing: number;\n /** Changes whenever web fonts load: a dependency wherever text is prepared. */\n readonly fonts: number;\n /** Subscribes to the width, for `useSyncExternalStore`. */\n readonly subscribe: (change: () => void) => () => void;\n /** The width given, or the element's content width; undefined until observed. */\n readonly width: () => number | undefined;\n}\n\nexport interface FontProviderProps {\n /** A CSS font shorthand that every cut inside is in, unless it's given its own. */\n readonly font?: string;\n /** CSS `letter-spacing` in px for every cut inside, unless it's given its own. */\n readonly letterSpacing?: number;\n readonly children?: ReactNode;\n}\n\ninterface Provided {\n readonly font: string | undefined;\n readonly letterSpacing: number | undefined;\n}\n\nconst Fonts = createContext<Provided>({ font: undefined, letterSpacing: undefined });\n\n/**\n * Gives every `Truncate`, `Clamp` and `useBox` inside the font and letter spacing their\n * text renders in, once for a list. Each would otherwise read its element's computed\n * style when it mounts: 5,000 rows take 4 to 5 ms of style reads. Cuts follow what's\n * given, so give what the rows really render in.\n */\nexport function FontProvider(props: FontProviderProps): ReactNode {\n const { font, letterSpacing } = props;\n const value = useMemo(() => ({ font, letterSpacing }), [font, letterSpacing]);\n return createElement(Fonts, { value }, props.children);\n}\n\n/**\n * The width and font text is cut to: the ones given, then a `FontProvider`'s, or an\n * element's. The width is a store, not state, so a component can read what it derives\n * from it with `useSyncExternalStore` and render only when that changes.\n */\nexport const useBox = (props: BoxProps): Box => {\n const provided = useContext(Fonts);\n const given = props.font ?? provided.font;\n const givenSpacing = props.letterSpacing ?? provided.letterSpacing;\n const [element, setElement] = useState<Element | null>(null);\n const [computed, setComputed] = useState<{ font: string; spacing: number }>();\n const [fonts, setFonts] = useState(0);\n const observed = useRef<number | undefined>(undefined);\n useEffect(() => onFonts(() => setFonts((n) => n + 1)), []);\n const readFont = given === undefined;\n const readSpacing = givenSpacing === undefined;\n // The style is read once per element as it mounts, before it paints, and only for what\n // wasn't given.\n const ref = useCallback(\n (e: Element | null) => {\n setElement(e);\n if (!e || (!readFont && !readSpacing)) return;\n const style = getComputedStyle(e);\n setComputed({ font: fontOf(e, style), spacing: spacingOf(e, style) });\n },\n [readFont, readSpacing],\n );\n const observing = props.width === undefined;\n const subscribe = useCallback(\n (change: () => void) => {\n if (!element || !observing) return () => {};\n return observe(element, (width) => {\n observed.current = width;\n change();\n });\n },\n [element, observing],\n );\n const width = props.width;\n return {\n ref,\n font: given ?? computed?.font,\n spacing: givenSpacing ?? computed?.spacing ?? 0,\n fonts,\n subscribe,\n width: () => width ?? observed.current,\n };\n};\n\nexport interface CutProps extends CutOptions, BoxProps {\n readonly text: string;\n readonly protect?: Protect;\n}\n\n/**\n * `protect` kept by what it says, not by identity: an inline literal is new on every\n * render, and this returns one value while its patterns, weights and wholes stay the same.\n */\nexport const useProtect = (protect: Protect | undefined): Protect | undefined => {\n let key = \"\";\n if (protect) {\n for (const given of \"length\" in protect ? protect : [protect]) {\n const {\n pattern,\n weight = 1,\n whole = false,\n } = given instanceof RegExp ? { pattern: given } : given;\n key += `${pattern}${weight}${whole} `;\n }\n }\n // The value from the render that first saw this key, as React's derived state is kept.\n const [kept, setKept] = useState({ key, protect });\n if (kept.key !== key) setKept({ key, protect });\n return kept.key === key ? kept.protect : protect;\n};\n\nexport interface UseCutProps extends CutProps {\n /**\n * The ellipsis is painted in an element of its own, so it is measured as a run of its\n * own and no kerning across it counts.\n */\n readonly separateEllipsis?: boolean;\n}\n\nexport interface CutHandle {\n /** Give it the element whose width and font the cut follows. */\n readonly ref: (element: Element | null) => void;\n readonly prepared: Prepared | undefined;\n /** Undefined until the width and font are known. */\n readonly cut: Cut | undefined;\n}\n\nconst useCutWith = (props: CutProps, apart: boolean): CutHandle => {\n const box = useBox(props);\n const { font, spacing, fonts } = box;\n const { text } = props;\n const protect = useProtect(props.protect);\n const prepared = useMemo(\n () =>\n font === undefined\n ? undefined\n : prepare(text, font, { ...(protect ? { protect } : {}), letterSpacing: spacing }),\n // oxlint-disable-next-line react-hooks/exhaustive-deps -- fonts changes when a web font loads, after forget() has dropped the widths the last prepare used\n [text, font, spacing, protect, fonts],\n );\n // The cut is the snapshot: a number, so a resize that doesn't move it renders nothing.\n const snapshot = (): Cut | undefined => {\n const width = box.width();\n if (prepared === undefined || width === undefined || font === undefined) return undefined;\n const ellipsis = props.ellipsis ?? \"…\";\n return apart && typeof ellipsis === \"string\"\n ? cut(prepared, width, {\n ...props,\n ellipsis: widthOf(faceOf(font), ellipsis) + spacing * ellipsis.length,\n })\n : cut(prepared, width, props);\n };\n const result = useSyncExternalStore(box.subscribe, snapshot, () => undefined);\n return { ref: box.ref, prepared, cut: result };\n};\n\n/** The cut of `props.text` for an element's width and font, or for the ones given. */\nexport const useCut = (props: UseCutProps): CutHandle =>\n useCutWith(props, props.separateEllipsis === true);\n\nexport interface TruncateProps extends CutProps {\n readonly ellipsis?: string;\n /**\n * \"kept\" (default) paints the kept text, with the whole text in `title` once cut.\n * \"full\" paints all of it with the cut part collapsed to no width, so copy,\n * find-in-page and screen readers get the whole text.\n */\n readonly render?: \"kept\" | \"full\";\n readonly className?: string;\n}\n\n// Until the first cut, the browser's own end ellipsis stands in.\nconst line = {\n display: \"block\",\n overflow: \"hidden\",\n whiteSpace: \"nowrap\",\n textOverflow: \"ellipsis\",\n} as const;\nconst collapsed = { fontSize: 0 } as const;\nconst unselectable = { userSelect: \"none\", WebkitUserSelect: \"none\" } as const;\n\n/** One line of text, cut to fit its box. */\nexport function Truncate(props: TruncateProps): ReactNode {\n const full = props.render === \"full\";\n const handle = useCutWith(props, full);\n const { prepared: p, cut: c } = handle;\n const ellipsis = props.ellipsis ?? \"…\";\n const cutNow = p !== undefined && c !== undefined && truncated(c);\n const text = p?.text ?? props.text;\n const kept = cutNow ? text.slice(0, head(c)) : text;\n const rest = cutNow ? text.slice(tail(c)) : \"\";\n const own = {\n ref: handle.ref,\n className: props.className,\n style: line,\n title: !full && cutNow ? text : undefined,\n };\n if (!full) return createElement(\"span\", own, kept + (cutNow ? ellipsis + rest : \"\"));\n return createElement(\n \"span\",\n own,\n kept,\n createElement(\"span\", { style: collapsed }, cutNow ? text.slice(head(c), tail(c)) : \"\"),\n cutNow ? createElement(\"span\", { \"aria-hidden\": true, style: unselectable }, ellipsis) : null,\n rest,\n );\n}\n"],"mappings":";;;;;AAiEA,IAAM,QAAQ,cAAwB;CAAE,MAAM,KAAA;CAAW,eAAe,KAAA;AAAU,CAAC;;;;;;;AAQnF,SAAgB,aAAa,OAAqC;CAChE,MAAM,EAAE,MAAM,kBAAkB;CAChC,MAAM,QAAQ,eAAe;EAAE;EAAM;CAAc,IAAI,CAAC,MAAM,aAAa,CAAC;CAC5E,OAAO,cAAc,OAAO,EAAE,MAAM,GAAG,MAAM,QAAQ;AACvD;;;;;;AAOA,IAAa,UAAU,UAAyB;CAC9C,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,QAAQ,MAAM,QAAQ,SAAS;CACrC,MAAM,eAAe,MAAM,iBAAiB,SAAS;CACrD,MAAM,CAAC,SAAS,cAAc,SAAyB,IAAI;CAC3D,MAAM,CAAC,UAAU,eAAe,SAA4C;CAC5E,MAAM,CAAC,OAAO,YAAY,SAAS,CAAC;CACpC,MAAM,WAAW,OAA2B,KAAA,CAAS;CACrD,gBAAgB,cAAc,UAAU,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;CACzD,MAAM,WAAW,UAAU,KAAA;CAC3B,MAAM,cAAc,iBAAiB,KAAA;CAGrC,MAAM,MAAM,aACT,MAAsB;EACrB,WAAW,CAAC;EACZ,IAAI,CAAC,KAAM,CAAC,YAAY,CAAC,aAAc;EACvC,MAAM,QAAQ,iBAAiB,CAAC;EAChC,YAAY;GAAE,MAAM,OAAO,GAAG,KAAK;GAAG,SAAS,UAAU,GAAG,KAAK;EAAE,CAAC;CACtE,GACA,CAAC,UAAU,WAAW,CACxB;CACA,MAAM,YAAY,MAAM,UAAU,KAAA;CAClC,MAAM,YAAY,aACf,WAAuB;EACtB,IAAI,CAAC,WAAW,CAAC,WAAW,aAAa,CAAC;EAC1C,OAAO,QAAQ,UAAU,UAAU;GACjC,SAAS,UAAU;GACnB,OAAO;EACT,CAAC;CACH,GACA,CAAC,SAAS,SAAS,CACrB;CACA,MAAM,QAAQ,MAAM;CACpB,OAAO;EACL;EACA,MAAM,SAAS,UAAU;EACzB,SAAS,gBAAgB,UAAU,WAAW;EAC9C;EACA;EACA,aAAa,SAAS,SAAS;CACjC;AACF;;;;;AAWA,IAAa,cAAc,YAAsD;CAC/E,IAAI,MAAM;CACV,IAAI,SACF,KAAK,MAAM,SAAS,YAAY,UAAU,UAAU,CAAC,OAAO,GAAG;EAC7D,MAAM,EACJ,SACA,SAAS,GACT,QAAQ,UACN,iBAAiB,SAAS,EAAE,SAAS,MAAM,IAAI;EACnD,OAAO,GAAG,UAAU,SAAS,MAAM;CACrC;CAGF,MAAM,CAAC,MAAM,WAAW,SAAS;EAAE;EAAK;CAAQ,CAAC;CACjD,IAAI,KAAK,QAAQ,KAAK,QAAQ;EAAE;EAAK;CAAQ,CAAC;CAC9C,OAAO,KAAK,QAAQ,MAAM,KAAK,UAAU;AAC3C;AAkBA,IAAM,cAAc,OAAiB,UAA8B;CACjE,MAAM,MAAM,OAAO,KAAK;CACxB,MAAM,EAAE,MAAM,SAAS,UAAU;CACjC,MAAM,EAAE,SAAS;CACjB,MAAM,UAAU,WAAW,MAAM,OAAO;CACxC,MAAM,WAAW,cAEb,SAAS,KAAA,IACL,KAAA,IACA,QAAQ,MAAM,MAAM;EAAE,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;EAAI,eAAe;CAAQ,CAAC,GAErF;EAAC;EAAM;EAAM;EAAS;EAAS;CAAK,CACtC;CAEA,MAAM,iBAAkC;EACtC,MAAM,QAAQ,IAAI,MAAM;EACxB,IAAI,aAAa,KAAA,KAAa,UAAU,KAAA,KAAa,SAAS,KAAA,GAAW,OAAO,KAAA;EAChF,MAAM,WAAW,MAAM,YAAY;EACnC,OAAO,SAAS,OAAO,aAAa,WAChC,IAAI,UAAU,OAAO;GACnB,GAAG;GACH,UAAU,QAAQ,OAAO,IAAI,GAAG,QAAQ,IAAI,UAAU,SAAS;EACjE,CAAC,IACD,IAAI,UAAU,OAAO,KAAK;CAChC;CACA,MAAM,SAAS,qBAAqB,IAAI,WAAW,gBAAgB,KAAA,CAAS;CAC5E,OAAO;EAAE,KAAK,IAAI;EAAK;EAAU,KAAK;CAAO;AAC/C;;AAGA,IAAa,UAAU,UACrB,WAAW,OAAO,MAAM,qBAAqB,IAAI;AAcnD,IAAM,OAAO;CACX,SAAS;CACT,UAAU;CACV,YAAY;CACZ,cAAc;AAChB;AACA,IAAM,YAAY,EAAE,UAAU,EAAE;AAChC,IAAM,eAAe;CAAE,YAAY;CAAQ,kBAAkB;AAAO;;AAGpE,SAAgB,SAAS,OAAiC;CACxD,MAAM,OAAO,MAAM,WAAW;CAC9B,MAAM,SAAS,WAAW,OAAO,IAAI;CACrC,MAAM,EAAE,UAAU,GAAG,KAAK,MAAM;CAChC,MAAM,WAAW,MAAM,YAAY;CACnC,MAAM,SAAS,MAAM,KAAA,KAAa,MAAM,KAAA,KAAa,UAAU,CAAC;CAChE,MAAM,OAAO,GAAG,QAAQ,MAAM;CAC9B,MAAM,OAAO,SAAS,KAAK,MAAM,GAAG,KAAK,CAAC,CAAC,IAAI;CAC/C,MAAM,OAAO,SAAS,KAAK,MAAM,KAAK,CAAC,CAAC,IAAI;CAC5C,MAAM,MAAM;EACV,KAAK,OAAO;EACZ,WAAW,MAAM;EACjB,OAAO;EACP,OAAO,CAAC,QAAQ,SAAS,OAAO,KAAA;CAClC;CACA,IAAI,CAAC,MAAM,OAAO,cAAc,QAAQ,KAAK,QAAQ,SAAS,WAAW,OAAO,GAAG;CACnF,OAAO,cACL,QACA,KACA,MACA,cAAc,QAAQ,EAAE,OAAO,UAAU,GAAG,SAAS,KAAK,MAAM,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,EAAE,GACtF,SAAS,cAAc,QAAQ;EAAE,eAAe;EAAM,OAAO;CAAa,GAAG,QAAQ,IAAI,MACzF,IACF;AACF"}
@@ -0,0 +1,27 @@
1
+ import { type ReactNode } from "react";
2
+ import { type CutOptions, type Protect } from "./cut.js";
3
+ export interface ClampProps extends Omit<CutOptions, "tailGraphemes"> {
4
+ readonly text: string;
5
+ /** How many lines to keep. */
6
+ readonly lines: number;
7
+ readonly ellipsis?: string;
8
+ /**
9
+ * Painted after the text whenever it is cut or `expanded`, such as a "more" or
10
+ * "less" button. Its width is observed and kept free on the last line.
11
+ */
12
+ readonly after?: ReactNode;
13
+ /** Show every line, with `after` following the last one, on its own if it doesn't fit. */
14
+ readonly expanded?: boolean;
15
+ /** The room in px. Left out, it is observed from the element. */
16
+ readonly width?: number;
17
+ /** A CSS font shorthand. Left out, it is read from the element's computed style. */
18
+ readonly font?: string;
19
+ /** CSS `letter-spacing` in px. Left out, it is read from the element's computed style. */
20
+ readonly letterSpacing?: number;
21
+ readonly protect?: Protect;
22
+ readonly whiteSpace?: "normal" | "pre-wrap";
23
+ readonly wordBreak?: "normal" | "keep-all";
24
+ readonly className?: string;
25
+ }
26
+ /** A paragraph cut to a number of lines at its end, start or middle. */
27
+ export declare function Clamp(props: ClampProps): ReactNode;
@@ -0,0 +1,74 @@
1
+ import { l as truncated, r as head, s as tail } from "./cut-D7qi2yZl.js";
2
+ import { n as observe } from "./dom-CPL0-qc3.js";
3
+ import { n as prepareLines, t as clamp } from "./lines-DOj2DQLl.js";
4
+ import { a as useProtect, r as useBox } from "./react-CnQfjWyS.js";
5
+ import { createElement, useEffect, useMemo, useState, useSyncExternalStore } from "react";
6
+ //#region src/react-lines.ts
7
+ var box = { display: "inline-block" };
8
+ /** A paragraph cut to a number of lines at its end, start or middle. */
9
+ function Clamp(props) {
10
+ const b = useBox(props);
11
+ const { font, spacing, fonts } = b;
12
+ const { text, lines, at, ratio, words, trim, ellipsis = "…", whiteSpace, wordBreak } = props;
13
+ const hasAfter = props.after !== void 0;
14
+ const [after, setAfter] = useState(null);
15
+ const [afterWidth, setAfterWidth] = useState(0);
16
+ useEffect(() => after ? observe(after, setAfterWidth) : void 0, [after]);
17
+ const protect = useProtect(props.protect);
18
+ const prepared = useMemo(() => font === void 0 ? void 0 : prepareLines(text, font, {
19
+ ...protect ? { protect } : {},
20
+ ...whiteSpace ? { whiteSpace } : {},
21
+ ...wordBreak ? { wordBreak } : {},
22
+ letterSpacing: spacing
23
+ }), [
24
+ text,
25
+ font,
26
+ spacing,
27
+ protect,
28
+ whiteSpace,
29
+ wordBreak,
30
+ fonts
31
+ ]);
32
+ const width = useSyncExternalStore(b.subscribe, b.width, () => void 0);
33
+ const within = typeof words === "object" ? words.within : words;
34
+ const c = useMemo(() => {
35
+ if (prepared === void 0 || width === void 0) return void 0;
36
+ return clamp(prepared, width, {
37
+ lines,
38
+ ellipsis,
39
+ after: hasAfter ? afterWidth : 0,
40
+ ...at === void 0 ? {} : { at },
41
+ ...ratio === void 0 ? {} : { ratio },
42
+ ...within === void 0 ? {} : { words: typeof within === "number" ? { within } : within },
43
+ ...trim === void 0 ? {} : { trim }
44
+ });
45
+ }, [
46
+ prepared,
47
+ width,
48
+ lines,
49
+ ellipsis,
50
+ hasAfter,
51
+ afterWidth,
52
+ at,
53
+ ratio,
54
+ within,
55
+ trim
56
+ ]);
57
+ const cutNow = c !== void 0 && truncated(c);
58
+ const shown = props.expanded || prepared === void 0 || !cutNow ? text : prepared.text.slice(0, head(c)) + ellipsis + prepared.text.slice(tail(c));
59
+ return createElement("div", {
60
+ ref: b.ref,
61
+ className: props.className,
62
+ style: {
63
+ overflowWrap: "break-word",
64
+ whiteSpace: whiteSpace ?? "normal"
65
+ }
66
+ }, shown, hasAfter && (props.expanded || cutNow) ? createElement("span", {
67
+ ref: setAfter,
68
+ style: box
69
+ }, props.after) : null);
70
+ }
71
+ //#endregion
72
+ export { Clamp };
73
+
74
+ //# sourceMappingURL=react-lines.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-lines.js","names":[],"sources":["../src/react-lines.ts"],"sourcesContent":["// Written with createElement, not JSX, as react.ts is.\nimport {\n createElement,\n type ReactNode,\n useEffect,\n useMemo,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport { type CutOptions, head, type Protect, tail, truncated } from \"./cut.ts\";\nimport { observe } from \"./dom.ts\";\nimport { clamp, prepareLines } from \"./lines.ts\";\nimport { useBox, useProtect } from \"./react.ts\";\n\nexport interface ClampProps extends Omit<CutOptions, \"tailGraphemes\"> {\n readonly text: string;\n /** How many lines to keep. */\n readonly lines: number;\n readonly ellipsis?: string;\n /**\n * Painted after the text whenever it is cut or `expanded`, such as a \"more\" or\n * \"less\" button. Its width is observed and kept free on the last line.\n */\n readonly after?: ReactNode;\n /** Show every line, with `after` following the last one, on its own if it doesn't fit. */\n readonly expanded?: boolean;\n /** The room in px. Left out, it is observed from the element. */\n readonly width?: number;\n /** A CSS font shorthand. Left out, it is read from the element's computed style. */\n readonly font?: string;\n /** CSS `letter-spacing` in px. Left out, it is read from the element's computed style. */\n readonly letterSpacing?: number;\n readonly protect?: Protect;\n readonly whiteSpace?: \"normal\" | \"pre-wrap\";\n readonly wordBreak?: \"normal\" | \"keep-all\";\n readonly className?: string;\n}\n\nconst box = { display: \"inline-block\" } as const;\n\n/** A paragraph cut to a number of lines at its end, start or middle. */\nexport function Clamp(props: ClampProps): ReactNode {\n const b = useBox(props);\n const { font, spacing, fonts } = b;\n const { text, lines, at, ratio, words, trim, ellipsis = \"…\", whiteSpace, wordBreak } = props;\n const hasAfter = props.after !== undefined;\n // `after` mounts only once the text is cut, so it is observed from its own ref.\n const [after, setAfter] = useState<Element | null>(null);\n const [afterWidth, setAfterWidth] = useState(0);\n useEffect(() => (after ? observe(after, setAfterWidth) : undefined), [after]);\n const protect = useProtect(props.protect);\n const prepared = useMemo(\n () =>\n font === undefined\n ? undefined\n : prepareLines(text, font, {\n ...(protect ? { protect } : {}),\n ...(whiteSpace ? { whiteSpace } : {}),\n ...(wordBreak ? { wordBreak } : {}),\n letterSpacing: spacing,\n }),\n // oxlint-disable-next-line react-hooks/exhaustive-deps -- fonts changes when a web font loads, after forget() has dropped the widths the last prepare used\n [text, font, spacing, protect, whiteSpace, wordBreak, fonts],\n );\n // Clamps are few beside a table's rows, so each renders on a width change and clamps\n // only when something it reads has changed.\n const width = useSyncExternalStore(b.subscribe, b.width, () => undefined);\n const within = typeof words === \"object\" ? words.within : words;\n const c = useMemo(() => {\n if (prepared === undefined || width === undefined) return undefined;\n return clamp(prepared, width, {\n lines,\n ellipsis,\n // `after` is an element here and a width to `clamp`, so it's always given as one.\n after: hasAfter ? afterWidth : 0,\n ...(at === undefined ? {} : { at }),\n ...(ratio === undefined ? {} : { ratio }),\n ...(within === undefined ? {} : { words: typeof within === \"number\" ? { within } : within }),\n ...(trim === undefined ? {} : { trim }),\n });\n }, [prepared, width, lines, ellipsis, hasAfter, afterWidth, at, ratio, within, trim]);\n const cutNow = c !== undefined && truncated(c);\n const shown =\n props.expanded || prepared === undefined || !cutNow\n ? text\n : prepared.text.slice(0, head(c)) + ellipsis + prepared.text.slice(tail(c));\n return createElement(\n \"div\",\n {\n ref: b.ref,\n className: props.className,\n style: { overflowWrap: \"break-word\", whiteSpace: whiteSpace ?? \"normal\" },\n },\n shown,\n hasAfter && (props.expanded || cutNow)\n ? createElement(\"span\", { ref: setAfter, style: box }, props.after)\n : null,\n );\n}\n"],"mappings":";;;;;;AAsCA,IAAM,MAAM,EAAE,SAAS,eAAe;;AAGtC,SAAgB,MAAM,OAA8B;CAClD,MAAM,IAAI,OAAO,KAAK;CACtB,MAAM,EAAE,MAAM,SAAS,UAAU;CACjC,MAAM,EAAE,MAAM,OAAO,IAAI,OAAO,OAAO,MAAM,WAAW,KAAK,YAAY,cAAc;CACvF,MAAM,WAAW,MAAM,UAAU,KAAA;CAEjC,MAAM,CAAC,OAAO,YAAY,SAAyB,IAAI;CACvD,MAAM,CAAC,YAAY,iBAAiB,SAAS,CAAC;CAC9C,gBAAiB,QAAQ,QAAQ,OAAO,aAAa,IAAI,KAAA,GAAY,CAAC,KAAK,CAAC;CAC5E,MAAM,UAAU,WAAW,MAAM,OAAO;CACxC,MAAM,WAAW,cAEb,SAAS,KAAA,IACL,KAAA,IACA,aAAa,MAAM,MAAM;EACvB,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;EAC7B,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;EACnC,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;EACjC,eAAe;CACjB,CAAC,GAEP;EAAC;EAAM;EAAM;EAAS;EAAS;EAAY;EAAW;CAAK,CAC7D;CAGA,MAAM,QAAQ,qBAAqB,EAAE,WAAW,EAAE,aAAa,KAAA,CAAS;CACxE,MAAM,SAAS,OAAO,UAAU,WAAW,MAAM,SAAS;CAC1D,MAAM,IAAI,cAAc;EACtB,IAAI,aAAa,KAAA,KAAa,UAAU,KAAA,GAAW,OAAO,KAAA;EAC1D,OAAO,MAAM,UAAU,OAAO;GAC5B;GACA;GAEA,OAAO,WAAW,aAAa;GAC/B,GAAI,OAAO,KAAA,IAAY,CAAC,IAAI,EAAE,GAAG;GACjC,GAAI,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM;GACvC,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,OAAO,WAAW,WAAW,EAAE,OAAO,IAAI,OAAO;GAC1F,GAAI,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK;EACvC,CAAC;CACH,GAAG;EAAC;EAAU;EAAO;EAAO;EAAU;EAAU;EAAY;EAAI;EAAO;EAAQ;CAAI,CAAC;CACpF,MAAM,SAAS,MAAM,KAAA,KAAa,UAAU,CAAC;CAC7C,MAAM,QACJ,MAAM,YAAY,aAAa,KAAA,KAAa,CAAC,SACzC,OACA,SAAS,KAAK,MAAM,GAAG,KAAK,CAAC,CAAC,IAAI,WAAW,SAAS,KAAK,MAAM,KAAK,CAAC,CAAC;CAC9E,OAAO,cACL,OACA;EACE,KAAK,EAAE;EACP,WAAW,MAAM;EACjB,OAAO;GAAE,cAAc;GAAc,YAAY,cAAc;EAAS;CAC1E,GACA,OACA,aAAa,MAAM,YAAY,UAC3B,cAAc,QAAQ;EAAE,KAAK;EAAU,OAAO;CAAI,GAAG,MAAM,KAAK,IAChE,IACN;AACF"}
@@ -0,0 +1,81 @@
1
+ import { type ReactNode } from "react";
2
+ import { type Cut, type CutOptions, type Prepared, type Protect } from "./cut.js";
3
+ export interface BoxProps {
4
+ /** The room in px. Left out, it is observed from the element. */
5
+ readonly width?: number;
6
+ /** A CSS font shorthand. Left out, it is read from the element's computed style. */
7
+ readonly font?: string;
8
+ /** CSS `letter-spacing` in px. Left out, it is read from the element's computed style. */
9
+ readonly letterSpacing?: number;
10
+ }
11
+ export interface Box {
12
+ /** Give it the element whose width and font the text follows. */
13
+ readonly ref: (element: Element | null) => void;
14
+ /** The font given, or the element's; undefined until it mounts. */
15
+ readonly font: string | undefined;
16
+ /** The letter spacing given, or the element's, in px. */
17
+ readonly spacing: number;
18
+ /** Changes whenever web fonts load: a dependency wherever text is prepared. */
19
+ readonly fonts: number;
20
+ /** Subscribes to the width, for `useSyncExternalStore`. */
21
+ readonly subscribe: (change: () => void) => () => void;
22
+ /** The width given, or the element's content width; undefined until observed. */
23
+ readonly width: () => number | undefined;
24
+ }
25
+ export interface FontProviderProps {
26
+ /** A CSS font shorthand that every cut inside is in, unless it's given its own. */
27
+ readonly font?: string;
28
+ /** CSS `letter-spacing` in px for every cut inside, unless it's given its own. */
29
+ readonly letterSpacing?: number;
30
+ readonly children?: ReactNode;
31
+ }
32
+ /**
33
+ * Gives every `Truncate`, `Clamp` and `useBox` inside the font and letter spacing their
34
+ * text renders in, once for a list. Each would otherwise read its element's computed
35
+ * style when it mounts: 5,000 rows take 4 to 5 ms of style reads. Cuts follow what's
36
+ * given, so give what the rows really render in.
37
+ */
38
+ export declare function FontProvider(props: FontProviderProps): ReactNode;
39
+ /**
40
+ * The width and font text is cut to: the ones given, then a `FontProvider`'s, or an
41
+ * element's. The width is a store, not state, so a component can read what it derives
42
+ * from it with `useSyncExternalStore` and render only when that changes.
43
+ */
44
+ export declare const useBox: (props: BoxProps) => Box;
45
+ export interface CutProps extends CutOptions, BoxProps {
46
+ readonly text: string;
47
+ readonly protect?: Protect;
48
+ }
49
+ /**
50
+ * `protect` kept by what it says, not by identity: an inline literal is new on every
51
+ * render, and this returns one value while its patterns, weights and wholes stay the same.
52
+ */
53
+ export declare const useProtect: (protect: Protect | undefined) => Protect | undefined;
54
+ export interface UseCutProps extends CutProps {
55
+ /**
56
+ * The ellipsis is painted in an element of its own, so it is measured as a run of its
57
+ * own and no kerning across it counts.
58
+ */
59
+ readonly separateEllipsis?: boolean;
60
+ }
61
+ export interface CutHandle {
62
+ /** Give it the element whose width and font the cut follows. */
63
+ readonly ref: (element: Element | null) => void;
64
+ readonly prepared: Prepared | undefined;
65
+ /** Undefined until the width and font are known. */
66
+ readonly cut: Cut | undefined;
67
+ }
68
+ /** The cut of `props.text` for an element's width and font, or for the ones given. */
69
+ export declare const useCut: (props: UseCutProps) => CutHandle;
70
+ export interface TruncateProps extends CutProps {
71
+ readonly ellipsis?: string;
72
+ /**
73
+ * "kept" (default) paints the kept text, with the whole text in `title` once cut.
74
+ * "full" paints all of it with the cut part collapsed to no width, so copy,
75
+ * find-in-page and screen readers get the whole text.
76
+ */
77
+ readonly render?: "kept" | "full";
78
+ readonly className?: string;
79
+ }
80
+ /** One line of text, cut to fit its box. */
81
+ export declare function Truncate(props: TruncateProps): ReactNode;
package/dist/react.js ADDED
@@ -0,0 +1 @@
1
+ export { t as FontProvider, n as Truncate, r as useBox, i as useCut, a as useProtect } from "./react-CnQfjWyS.js";
@@ -0,0 +1,155 @@
1
+ import { c as widthOf, i as faceOf } from "./face-pICHAjvk.js";
2
+ import { a as prepare, l as truncated, r as head, s as tail, t as cut } from "./cut-D7qi2yZl.js";
3
+ import { i as spacingOf, n as observe, r as onFonts, t as fontOf } from "./dom-CPL0-qc3.js";
4
+ import { className, createComponent, effect, insert, memo, readShallow, ref, setAttribute, template } from "@solidjs/web";
5
+ import { Show, createContext, createMemo, createSignal, onSettled, untrack, useContext } from "solid-js";
6
+ //#region src/solid.tsx
7
+ var _tmpl$ = /* @__PURE__ */ template(`<span style=display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis>`);
8
+ var _tmpl$2 = /* @__PURE__ */ template(`<span style=font-size:0>`);
9
+ var _tmpl$3 = /* @__PURE__ */ template(`<span aria-hidden=true style=user-select:none;-webkit-user-select:none>`);
10
+ var Fonts = createContext({});
11
+ /**
12
+ * Gives every `Truncate`, `Clamp` and `createBox` inside the font and letter spacing
13
+ * their text renders in, once for a list. Each would otherwise read its element's
14
+ * computed style when it mounts: 5,000 rows take 4 to 5 ms of style reads. Cuts follow
15
+ * what's given, so give what the rows really render in.
16
+ */
17
+ function FontProvider(props) {
18
+ return createComponent(Fonts, {
19
+ value: props,
20
+ get children() {
21
+ return props.children;
22
+ }
23
+ });
24
+ }
25
+ /**
26
+ * The width and font text is cut to: the ones given, then a `FontProvider`'s, or an
27
+ * element's, observed. Whether to observe the width and read the font is decided once,
28
+ * when it settles.
29
+ */
30
+ var createBox = (props) => {
31
+ const provided = useContext(Fonts);
32
+ const given = () => props.font ?? provided.font;
33
+ const spacing = () => props.letterSpacing ?? provided.letterSpacing;
34
+ let element;
35
+ const [observed, setObserved] = createSignal();
36
+ const [computed, setComputed] = createSignal();
37
+ const [spaced, setSpaced] = createSignal(0);
38
+ const [fonts, setFonts] = createSignal(0);
39
+ onSettled(() => {
40
+ const stops = [onFonts(() => setFonts((n) => n + 1))];
41
+ const readFont = untrack(given) === void 0;
42
+ const readSpacing = untrack(spacing) === void 0;
43
+ if (element && (readFont || readSpacing)) {
44
+ const style = getComputedStyle(element);
45
+ if (readFont) setComputed(fontOf(element, style));
46
+ if (readSpacing) setSpaced(spacingOf(element, style));
47
+ }
48
+ if (element && untrack(() => props.width) === void 0) stops.push(observe(element, setObserved));
49
+ return () => {
50
+ for (const stop of stops) stop();
51
+ };
52
+ });
53
+ return {
54
+ ref: (e) => element = e,
55
+ width: () => props.width ?? observed(),
56
+ font: () => given() ?? computed(),
57
+ spacing: () => spacing() ?? spaced(),
58
+ fonts
59
+ };
60
+ };
61
+ /** The cut of `props.text` for an element's width and font, or for the ones given. */
62
+ var createCut = (props) => cutOf(props, untrack(() => props.separateEllipsis));
63
+ var cutOf = (props, apart) => {
64
+ const box = createBox(props);
65
+ const font = box.font;
66
+ const prepared = createMemo(() => {
67
+ box.fonts();
68
+ const f = font();
69
+ if (f === void 0) return void 0;
70
+ return prepare(props.text, f, {
71
+ ...props.protect ? { protect: props.protect } : {},
72
+ letterSpacing: box.spacing()
73
+ });
74
+ });
75
+ const result = createMemo(() => {
76
+ const p = prepared();
77
+ const width = box.width();
78
+ const f = font();
79
+ if (p === void 0 || width === void 0 || f === void 0) return void 0;
80
+ const ellipsis = props.ellipsis ?? "…";
81
+ return apart && typeof ellipsis === "string" ? cut(p, width, {
82
+ ...props,
83
+ ellipsis: widthOf(faceOf(f), ellipsis) + box.spacing() * ellipsis.length
84
+ }) : cut(p, width, props);
85
+ });
86
+ return {
87
+ ref: box.ref,
88
+ prepared,
89
+ cut: result
90
+ };
91
+ };
92
+ /** One line of text, cut to fit its box. */
93
+ function Truncate(props) {
94
+ const full = untrack(() => props.render === "full");
95
+ const handle = cutOf(props, full);
96
+ const ellipsis = () => props.ellipsis ?? "…";
97
+ const parts = createMemo(() => {
98
+ const p = handle.prepared();
99
+ const c = handle.cut();
100
+ if (p === void 0 || c === void 0) return void 0;
101
+ const text = p.text;
102
+ return [
103
+ text.slice(0, head(c)),
104
+ text.slice(head(c), tail(c)),
105
+ text.slice(tail(c)),
106
+ truncated(c) ? text : void 0
107
+ ];
108
+ });
109
+ var _el$ = _tmpl$();
110
+ var _ref$ = handle.ref;
111
+ typeof _ref$ === "function" || Array.isArray(_ref$) ? ref(() => {
112
+ return _ref$;
113
+ }, _el$) : handle.ref = _el$;
114
+ insert(_el$, full ? [
115
+ memo(() => {
116
+ return parts()?.[0] ?? props.text;
117
+ }),
118
+ (() => {
119
+ var _el$2 = _tmpl$2();
120
+ insert(_el$2, () => {
121
+ return parts()?.[1];
122
+ });
123
+ return _el$2;
124
+ })(),
125
+ createComponent(Show, {
126
+ get when() {
127
+ return parts()?.[3];
128
+ },
129
+ get children() {
130
+ var _el$3 = _tmpl$3();
131
+ insert(_el$3, ellipsis);
132
+ return _el$3;
133
+ }
134
+ }),
135
+ memo(() => {
136
+ return parts()?.[2];
137
+ })
138
+ ] : memo(() => {
139
+ return (parts()?.[0] ?? props.text) + (parts()?.[3] ? ellipsis() + parts()[2] : "");
140
+ }));
141
+ effect(() => {
142
+ return {
143
+ e: readShallow(props.class),
144
+ t: full ? void 0 : parts()?.[3]
145
+ };
146
+ }, ({ e, t }, _p$) => {
147
+ className(_el$, e, _p$?.e);
148
+ t !== _p$?.t && setAttribute(_el$, "title", t);
149
+ });
150
+ return _el$;
151
+ }
152
+ //#endregion
153
+ export { createCut as i, Truncate as n, createBox as r, FontProvider as t };
154
+
155
+ //# sourceMappingURL=solid-BWC32VMg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solid-BWC32VMg.js","names":["<Fonts value={props}>{props.children}</Fonts>","value={props}","ref={handle.ref}","{parts()?.[0] ?? props.text}","<Show when={parts()?.[3]}>\n <span\n aria-hidden=\"true\"\n style={{ \"user-select\": \"none\", \"-webkit-user-select\": \"none\" }}\n >\n {ellipsis()}\n </span>\n </Show>","<span\n aria-hidden=\"true\"\n style={{ \"user-select\": \"none\", \"-webkit-user-select\": \"none\" }}\n >\n {ellipsis()}\n </span>","{parts()?.[2]}","{(parts()?.[0] ?? props.text) + (parts()?.[3] ? ellipsis() + parts()![2] : \"\")}","title={full ? undefined : parts()?.[3]}","<span\n ref={handle.ref}\n class={props.class}\n style={{\n display: \"block\",\n overflow: \"hidden\",\n \"white-space\": \"nowrap\",\n \"text-overflow\": \"ellipsis\",\n }}\n title={full ? undefined : parts()?.[3]}\n >\n {full ? (\n <>\n {parts()?.[0] ?? props.text}\n <span style={{ \"font-size\": \"0\" }}>{parts()?.[1]}</span>\n <Show when={parts()?.[3]}>\n <span\n aria-hidden=\"true\"\n style={{ \"user-select\": \"none\", \"-webkit-user-select\": \"none\" }}\n >\n {ellipsis()}\n </span>\n </Show>\n {parts()?.[2]}\n </>\n ) : (\n <>{(parts()?.[0] ?? props.text) + (parts()?.[3] ? ellipsis() + parts()![2] : \"\")}</>\n )}\n </span>","_$createComponent","_$ref","_$memo","_$readShallow","_$setAttribute"],"sources":["../src/solid.tsx"],"sourcesContent":["import type { JSX } from \"@solidjs/web\";\nimport {\n type Accessor,\n createContext,\n createMemo,\n createSignal,\n onSettled,\n Show,\n untrack,\n useContext,\n} from \"solid-js\";\nimport {\n cut,\n type Cut,\n type CutOptions,\n head,\n prepare,\n type Prepared,\n type Protect,\n tail,\n truncated,\n} from \"./cut.ts\";\nimport { fontOf, observe, onFonts, spacingOf } from \"./dom.ts\";\nimport { faceOf, widthOf } from \"./face.ts\";\n\nexport interface CutProps extends CutOptions {\n readonly text: string;\n /** The room in px. Left out, it is observed from the element. */\n readonly width?: number;\n /** A CSS font shorthand. Left out, it is read from the element's computed style. */\n readonly font?: string;\n /** CSS `letter-spacing` in px. Left out, it is read from the element's computed style. */\n readonly letterSpacing?: number;\n readonly protect?: Protect;\n}\n\nexport interface CutHandle {\n /** Give it the element whose width and font the cut follows. */\n readonly ref: (element: Element) => void;\n readonly prepared: Accessor<Prepared | undefined>;\n /** Undefined until the width and font are known. */\n readonly cut: Accessor<Cut | undefined>;\n}\n\nexport interface Box {\n /** Give it the element whose width and font the text follows. */\n readonly ref: (element: Element) => void;\n /** The width given, or the element's content width; undefined until observed. */\n readonly width: Accessor<number | undefined>;\n /** The font given, or the element's; undefined until it mounts. */\n readonly font: Accessor<string | undefined>;\n /** The letter spacing given, or the element's, in px. */\n readonly spacing: Accessor<number>;\n /** Changes whenever web fonts load: read it where text is prepared. */\n readonly fonts: Accessor<number>;\n}\n\nexport interface FontProviderProps {\n /** A CSS font shorthand that every cut inside is in, unless it's given its own. */\n readonly font?: string;\n /** CSS `letter-spacing` in px for every cut inside, unless it's given its own. */\n readonly letterSpacing?: number;\n readonly children?: JSX.Element;\n}\n\nconst Fonts = createContext<FontProviderProps>({});\n\n/**\n * Gives every `Truncate`, `Clamp` and `createBox` inside the font and letter spacing\n * their text renders in, once for a list. Each would otherwise read its element's\n * computed style when it mounts: 5,000 rows take 4 to 5 ms of style reads. Cuts follow\n * what's given, so give what the rows really render in.\n */\nexport function FontProvider(props: FontProviderProps): JSX.Element {\n return <Fonts value={props}>{props.children}</Fonts>;\n}\n\n/**\n * The width and font text is cut to: the ones given, then a `FontProvider`'s, or an\n * element's, observed. Whether to observe the width and read the font is decided once,\n * when it settles.\n */\nexport const createBox = (props: {\n readonly width?: number;\n readonly font?: string;\n readonly letterSpacing?: number;\n}): Box => {\n const provided = useContext(Fonts);\n const given = () => props.font ?? provided.font;\n const spacing = () => props.letterSpacing ?? provided.letterSpacing;\n let element: Element | undefined;\n const [observed, setObserved] = createSignal<number>();\n const [computed, setComputed] = createSignal<string>();\n const [spaced, setSpaced] = createSignal(0);\n const [fonts, setFonts] = createSignal(0);\n onSettled(() => {\n const stops = [onFonts(() => setFonts((n) => n + 1))];\n const readFont = untrack(given) === undefined;\n const readSpacing = untrack(spacing) === undefined;\n if (element && (readFont || readSpacing)) {\n const style = getComputedStyle(element);\n if (readFont) setComputed(fontOf(element, style));\n if (readSpacing) setSpaced(spacingOf(element, style));\n }\n if (element && untrack(() => props.width) === undefined) {\n stops.push(observe(element, setObserved));\n }\n return () => {\n for (const stop of stops) stop();\n };\n });\n return {\n ref: (e) => (element = e),\n width: () => props.width ?? observed(),\n font: () => given() ?? computed(),\n spacing: () => spacing() ?? spaced(),\n fonts,\n };\n};\n\nexport interface CreateCutProps extends CutProps {\n /**\n * The ellipsis is painted in an element of its own, so it is measured as a run of its\n * own and no kerning across it counts. Read once.\n */\n readonly separateEllipsis?: boolean;\n}\n\n/** The cut of `props.text` for an element's width and font, or for the ones given. */\nexport const createCut = (props: CreateCutProps): CutHandle =>\n cutOf(\n props,\n untrack(() => props.separateEllipsis),\n );\n\nconst cutOf = (props: CutProps, apart: boolean | undefined): CutHandle => {\n const box = createBox(props);\n const font = box.font;\n const prepared = createMemo(() => {\n box.fonts();\n const f = font();\n if (f === undefined) return undefined;\n return prepare(props.text, f, {\n ...(props.protect ? { protect: props.protect } : {}),\n letterSpacing: box.spacing(),\n });\n });\n const result = createMemo(() => {\n const p = prepared();\n const width = box.width();\n const f = font();\n if (p === undefined || width === undefined || f === undefined) return undefined;\n const ellipsis = props.ellipsis ?? \"…\";\n return apart && typeof ellipsis === \"string\"\n ? cut(p, width, {\n ...props,\n ellipsis: widthOf(faceOf(f), ellipsis) + box.spacing() * ellipsis.length,\n })\n : cut(p, width, props);\n });\n return { ref: box.ref, prepared, cut: result };\n};\n\nexport interface TruncateProps extends CutProps {\n readonly ellipsis?: string;\n /**\n * \"kept\" (default) paints the kept text, with the whole text in `title` once cut.\n * \"full\" paints all of it with the cut part collapsed to no width, so copy,\n * find-in-page and screen readers get the whole text. Read once.\n */\n readonly render?: \"kept\" | \"full\";\n readonly class?: string;\n}\n\n/** One line of text, cut to fit its box. */\nexport function Truncate(props: TruncateProps): JSX.Element {\n const full = untrack(() => props.render === \"full\");\n const handle = cutOf(props, full);\n const ellipsis = () => props.ellipsis ?? \"…\";\n // Head, cut part, tail and, once cut, the whole text. Undefined until the first cut.\n const parts = createMemo(() => {\n const p = handle.prepared();\n const c = handle.cut();\n if (p === undefined || c === undefined) return undefined;\n const text = p.text;\n return [\n text.slice(0, head(c)),\n text.slice(head(c), tail(c)),\n text.slice(tail(c)),\n truncated(c) ? text : undefined,\n ] as const;\n });\n // Until the first cut, the browser's own end ellipsis stands in.\n return (\n <span\n ref={handle.ref}\n class={props.class}\n style={{\n display: \"block\",\n overflow: \"hidden\",\n \"white-space\": \"nowrap\",\n \"text-overflow\": \"ellipsis\",\n }}\n title={full ? undefined : parts()?.[3]}\n >\n {full ? (\n <>\n {parts()?.[0] ?? props.text}\n <span style={{ \"font-size\": \"0\" }}>{parts()?.[1]}</span>\n <Show when={parts()?.[3]}>\n <span\n aria-hidden=\"true\"\n style={{ \"user-select\": \"none\", \"-webkit-user-select\": \"none\" }}\n >\n {ellipsis()}\n </span>\n </Show>\n {parts()?.[2]}\n </>\n ) : (\n <>{(parts()?.[0] ?? props.text) + (parts()?.[3] ? ellipsis() + parts()![2] : \"\")}</>\n )}\n </span>\n );\n}\n"],"mappings":"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"}