@forthtilliath/ts-kit 0.9.0 → 0.10.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 CHANGED
@@ -36,6 +36,10 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
36
36
  ### `array`
37
37
 
38
38
  - `chunk(array, size)` — splits an array into chunks of a given size.
39
+ - `createColumnStorage(defaults)` — `{ serialize, deserialize }` pair to
40
+ persist a user-customized column list (order + visibility, e.g. in
41
+ `localStorage`). Only `key`/`visible` are stored; on read, unknown keys are
42
+ dropped, new columns appended, and invalid input falls back to `defaults`.
39
43
  - `flattenDeep(array)` — recursively flattens nested arrays.
40
44
  - `getMostRecentIds(rows, limit?)` — most recently occurring distinct ids
41
45
  (`{ id, occurredAt }[]`), most recent first, deduplicated, limited
@@ -114,6 +118,8 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
114
118
  via a `data:` URI.
115
119
  - `downloadTextBlob(filename, text)` — same, but via a `Blob` + object URL
116
120
  (better for larger content).
121
+ - `sanitizeFileName(name)` — replaces characters forbidden in file names
122
+ (`\ / : * ? " < > |`) with hyphens, collapses whitespace and trims.
117
123
 
118
124
  ### `id`
119
125
 
@@ -148,6 +154,8 @@ import { chunk } from "@forthtilliath/ts-kit/array/chunk";
148
154
  - `formatDuration(ms)` — human-readable duration ("1d 1h 1m 1s").
149
155
  - `formatCsvNumber(value, decimals?)` — formats a number with a comma
150
156
  decimal separator (French-locale spreadsheets) instead of JS's dot.
157
+ - `padNumber(n, total, minWidth?)` — zero-pads `n` to the width of `total`
158
+ (at least `minWidth`, default 2) so numbered file names sort correctly.
151
159
 
152
160
  ### `object`
153
161
 
@@ -0,0 +1,30 @@
1
+ /** Minimal shape of a toggleable, reorderable column. */
2
+ export interface StorableColumn {
3
+ key: string;
4
+ visible: boolean;
5
+ }
6
+ export interface ColumnStorage<T extends StorableColumn> {
7
+ serialize: (columns: readonly T[]) => string;
8
+ deserialize: (raw: string) => T[];
9
+ }
10
+ /**
11
+ * Creates a (de)serializer for a user-customized list of columns (order and
12
+ * visibility), e.g. to persist a table layout in `localStorage`. Only `key`
13
+ * and `visible` are stored; other properties (labels…) always come from
14
+ * `defaults`. On read, unknown keys are dropped, columns missing from the
15
+ * stored value are appended in their default order, and invalid JSON falls
16
+ * back to `defaults`.
17
+ *
18
+ * @param defaults - The default columns, in their default order.
19
+ * @returns A `{ serialize, deserialize }` pair.
20
+ * @example
21
+ * const storage = createColumnStorage([
22
+ * { key: "name", label: "Name", visible: true },
23
+ * { key: "email", label: "Email", visible: true },
24
+ * ]);
25
+ * storage.deserialize('[{"key":"email","visible":false}]');
26
+ * // => [{ key: "email", label: "Email", visible: false },
27
+ * // { key: "name", label: "Name", visible: true }]
28
+ */
29
+ export declare function createColumnStorage<T extends StorableColumn>(defaults: readonly T[]): ColumnStorage<T>;
30
+ //# sourceMappingURL=createColumnStorage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createColumnStorage.d.ts","sourceRoot":"","sources":["../../src/array/createColumnStorage.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,cAAc;IACrD,SAAS,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,KAAK,MAAM,CAAC;IAC7C,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC,EAAE,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,cAAc,EAC1D,QAAQ,EAAE,SAAS,CAAC,EAAE,GACrB,aAAa,CAAC,CAAC,CAAC,CAgClB"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Creates a (de)serializer for a user-customized list of columns (order and
3
+ * visibility), e.g. to persist a table layout in `localStorage`. Only `key`
4
+ * and `visible` are stored; other properties (labels…) always come from
5
+ * `defaults`. On read, unknown keys are dropped, columns missing from the
6
+ * stored value are appended in their default order, and invalid JSON falls
7
+ * back to `defaults`.
8
+ *
9
+ * @param defaults - The default columns, in their default order.
10
+ * @returns A `{ serialize, deserialize }` pair.
11
+ * @example
12
+ * const storage = createColumnStorage([
13
+ * { key: "name", label: "Name", visible: true },
14
+ * { key: "email", label: "Email", visible: true },
15
+ * ]);
16
+ * storage.deserialize('[{"key":"email","visible":false}]');
17
+ * // => [{ key: "email", label: "Email", visible: false },
18
+ * // { key: "name", label: "Name", visible: true }]
19
+ */
20
+ export function createColumnStorage(defaults) {
21
+ function serialize(columns) {
22
+ return JSON.stringify(columns.map(({ key, visible }) => ({ key, visible })));
23
+ }
24
+ function deserialize(raw) {
25
+ try {
26
+ const stored = JSON.parse(raw);
27
+ if (!Array.isArray(stored))
28
+ return [...defaults];
29
+ const result = [];
30
+ for (const entry of stored) {
31
+ const { key, visible } = (entry ?? {});
32
+ const column = defaults.find((c) => c.key === key);
33
+ if (!column || result.some((c) => c.key === column.key))
34
+ continue;
35
+ result.push({
36
+ ...column,
37
+ visible: typeof visible === "boolean" ? visible : column.visible,
38
+ });
39
+ }
40
+ for (const column of defaults) {
41
+ if (!result.some((c) => c.key === column.key))
42
+ result.push(column);
43
+ }
44
+ return result;
45
+ }
46
+ catch {
47
+ return [...defaults];
48
+ }
49
+ }
50
+ return { serialize, deserialize };
51
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./chunk.js";
2
+ export * from "./createColumnStorage.js";
2
3
  export * from "./flattenDeep.js";
3
4
  export * from "./getMostRecentIds.js";
4
5
  export * from "./nextInCycle.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/array/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/array/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,gBAAgB,CAAC"}
@@ -1,4 +1,5 @@
1
1
  export * from "./chunk.js";
2
+ export * from "./createColumnStorage.js";
2
3
  export * from "./flattenDeep.js";
3
4
  export * from "./getMostRecentIds.js";
4
5
  export * from "./nextInCycle.js";
@@ -1,2 +1,3 @@
1
1
  export * from "./downloadText.js";
2
+ export * from "./sanitizeFileName.js";
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/files/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/files/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC"}
@@ -1 +1,2 @@
1
1
  export * from "./downloadText.js";
2
+ export * from "./sanitizeFileName.js";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Makes a string safe to use as a file name: characters forbidden on Windows
3
+ * (`\ / : * ? " < > |`) become hyphens, whitespace runs collapse into a single
4
+ * space and the result is trimmed. Also prevents a `/` from creating a
5
+ * sub-folder inside a ZIP archive.
6
+ *
7
+ * @param name - The raw file name.
8
+ * @returns The sanitized file name.
9
+ * @example
10
+ * sanitizeFileName("AC/DC: Live?"); // => "AC-DC- Live-"
11
+ * sanitizeFileName(" My file "); // => "My file"
12
+ */
13
+ export declare function sanitizeFileName(name: string): string;
14
+ //# sourceMappingURL=sanitizeFileName.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitizeFileName.d.ts","sourceRoot":"","sources":["../../src/files/sanitizeFileName.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKrD"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Makes a string safe to use as a file name: characters forbidden on Windows
3
+ * (`\ / : * ? " < > |`) become hyphens, whitespace runs collapse into a single
4
+ * space and the result is trimmed. Also prevents a `/` from creating a
5
+ * sub-folder inside a ZIP archive.
6
+ *
7
+ * @param name - The raw file name.
8
+ * @returns The sanitized file name.
9
+ * @example
10
+ * sanitizeFileName("AC/DC: Live?"); // => "AC-DC- Live-"
11
+ * sanitizeFileName(" My file "); // => "My file"
12
+ */
13
+ export function sanitizeFileName(name) {
14
+ return name
15
+ .replace(/[\\/:*?"<>|]/g, "-")
16
+ .replace(/\s+/g, " ")
17
+ .trim();
18
+ }
@@ -2,5 +2,6 @@ export * from "./clamp.js";
2
2
  export * from "./formatBytes.js";
3
3
  export * from "./formatCsvNumber.js";
4
4
  export * from "./formatDuration.js";
5
+ export * from "./padNumber.js";
5
6
  export * from "./round.js";
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/number/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/number/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
@@ -2,4 +2,5 @@ export * from "./clamp.js";
2
2
  export * from "./formatBytes.js";
3
3
  export * from "./formatCsvNumber.js";
4
4
  export * from "./formatDuration.js";
5
+ export * from "./padNumber.js";
5
6
  export * from "./round.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Left-pads a number with zeros so that file names sort correctly in a file
3
+ * explorer (`"01"`, `"02"`, …, `"10"`). The width adapts to `total` (3 digits
4
+ * from 100 items on) and never goes below `minWidth`.
5
+ *
6
+ * @param n - The number to pad.
7
+ * @param total - The total count, used to compute the width.
8
+ * @param minWidth - The minimum width (default: 2).
9
+ * @returns The zero-padded number.
10
+ * @example
11
+ * padNumber(1, 12); // => "01"
12
+ * padNumber(7, 120); // => "007"
13
+ */
14
+ export declare function padNumber(n: number, total: number, minWidth?: number): string;
15
+ //# sourceMappingURL=padNumber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"padNumber.d.ts","sourceRoot":"","sources":["../../src/number/padNumber.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAExE"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Left-pads a number with zeros so that file names sort correctly in a file
3
+ * explorer (`"01"`, `"02"`, …, `"10"`). The width adapts to `total` (3 digits
4
+ * from 100 items on) and never goes below `minWidth`.
5
+ *
6
+ * @param n - The number to pad.
7
+ * @param total - The total count, used to compute the width.
8
+ * @param minWidth - The minimum width (default: 2).
9
+ * @returns The zero-padded number.
10
+ * @example
11
+ * padNumber(1, 12); // => "01"
12
+ * padNumber(7, 120); // => "007"
13
+ */
14
+ export function padNumber(n, total, minWidth = 2) {
15
+ return String(n).padStart(Math.max(minWidth, String(total).length), "0");
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forthtilliath/ts-kit",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {