@gridkitjs/core 0.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Blagoja Blazhevski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @gridkitjs/core
2
+
3
+ Framework-agnostic data-grid logic — column resolution, alignment and sizing.
4
+ Plain TypeScript, no DOM required, so it is reusable from any framework
5
+ adapter.
6
+
7
+ > **Early development.** The API is still moving and may break between minor
8
+ > versions.
9
+
10
+ ```bash
11
+ pnpm add @gridkitjs/core
12
+ ```
13
+
14
+ ```ts
15
+ import { defineColumnsFromRows, resolveColumnWidths } from "@gridkitjs/core";
16
+
17
+ const columns = defineColumnsFromRows(rows);
18
+ const resolved = resolveColumnWidths(columns, sizing, {
19
+ sizes: { width: 160 },
20
+ resizable: true,
21
+ });
22
+ ```
23
+
24
+ Each entry in `resolved` pairs a column with everything needed to render it —
25
+ `width`, `label`, `resizable` and `alignment` — so an adapter renders what it
26
+ is handed rather than re-deriving it.
27
+
28
+ For React, see [`@gridkitjs/react`](https://www.npmjs.com/package/@gridkitjs/react).
29
+
30
+ Full documentation: [github.com/blagojablazhevski/gridkit](https://github.com/blagojablazhevski/gridkit#readme)
31
+
32
+ ## License
33
+
34
+ MIT © Blagoja Blazhevski
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Values a cell shows as-is instead of being drilled into. Arrays and `Date`s
3
+ * are cell values rather than groups of columns, and `null` cannot be
4
+ * introspected, so all three end a path.
5
+ */
6
+ type LeafValue = bigint | boolean | number | string | symbol | null | undefined | Date | readonly unknown[] | ((...args: never[]) => unknown);
7
+ /** Keys of `T` holding a leaf value — the only ones a path may end on. */
8
+ type LeafKey<T> = {
9
+ [K in keyof T & string]: NonNullable<T[K]> extends LeafValue ? K : never;
10
+ }[keyof T & string];
11
+ /**
12
+ * Every addressable field of `Row`, one level deep: `"Id"` for a flat field,
13
+ * `"Application.Id"` for a leaf inside a nested object. A nested object
14
+ * contributes only its leaf children, never itself and never anything deeper.
15
+ */
16
+ type FieldPath<Row> = {
17
+ [K in keyof Row & string]: NonNullable<Row[K]> extends LeafValue ? K : `${K}.${LeafKey<NonNullable<Row[K]>>}`;
18
+ }[keyof Row & string];
19
+ /**
20
+ * The value type of a column's cells. This package is framework-agnostic so it
21
+ * defaults to `string`;
22
+ */
23
+ type ColumnType = "dateTime" | "date" | "time" | "number" | "string" | "boolean" | "decimal" | "currency" | "percent";
24
+ type ColumnAlignment = "left" | "center" | "right";
25
+ /**
26
+ * @typeParam Header - What a header renders to. This package is
27
+ * framework-agnostic so it defaults to `string`; `@gridkitjs/react` binds it to
28
+ * `ReactNode` so a header callback can return JSX.
29
+ */
30
+ interface ColumnDefinition<Row, Header = string> {
31
+ /**
32
+ * Path to this cell's value: a key of `Row`, or `"Parent.Child"` for a leaf
33
+ * one level inside a nested object. Any string still compiles — the union
34
+ * exists to drive autocomplete, not to lock the grid down.
35
+ */
36
+ field: FieldPath<Row> | (string & {});
37
+ /**
38
+ * Stable identity for state keyed by this column. Defaults to `field`, which
39
+ * is unique in the common case but need not be — two columns may render the
40
+ * same field differently.
41
+ */
42
+ id?: string;
43
+ /** Header content, or a function returning it at render time. */
44
+ header?: Header | (() => Header) | undefined;
45
+ /**
46
+ * The value type of this column's cells. This package is framework-agnostic so it
47
+ * defaults to `string`;
48
+ */
49
+ type?: ColumnType;
50
+ /**
51
+ * Alignment of this column's cells. This package is framework-agnostic so it
52
+ * defaults to `left` (`right` for numbers);
53
+ */
54
+ alignment?: ColumnAlignment;
55
+ /**
56
+ * Width in px this column starts at. A user resize overrides it for as long
57
+ * as that resize lives in the sizing state.
58
+ */
59
+ width?: number;
60
+ /** Lower bound a resize may not drag below. */
61
+ minWidth?: number;
62
+ /** Upper bound a resize may not drag above. */
63
+ maxWidth?: number;
64
+ /** Whether this column can be resized, overriding the grid-level default. */
65
+ resizable?: boolean;
66
+ }
67
+ /**
68
+ * Widths the user has changed, keyed by column id. Holding only what changed —
69
+ * rather than every width — means a `width` edited in the column definition
70
+ * still takes effect, and resetting is discarding the state.
71
+ */
72
+ type ColumnSizingState = Readonly<Record<string, number>>;
73
+ /** Sizes used for a column that does not specify its own. */
74
+ interface ColumnSizeDefaults {
75
+ width: number;
76
+ minWidth: number;
77
+ maxWidth: number;
78
+ }
79
+ /** The bounds a column's width is held between, resolved once from both. */
80
+ interface ColumnConstraints {
81
+ minWidth: number;
82
+ maxWidth: number;
83
+ }
84
+ /**
85
+ * Grid-level defaults a column falls back to when it does not set its own.
86
+ * Sizes are grouped so the rest of the grid's defaults have somewhere to go.
87
+ */
88
+ interface ColumnResolveOptions {
89
+ sizes?: Partial<ColumnSizeDefaults> | undefined;
90
+ /** Whether columns are resizable, unless a column says otherwise. */
91
+ resizable?: boolean | undefined;
92
+ }
93
+ /**
94
+ * A column paired with everything it takes to render it. Each field is a
95
+ * decision made once here rather than in each adapter, so a second framework
96
+ * binding renders identically without repeating the logic.
97
+ */
98
+ interface ResolvedColumn<Row, Header = string> {
99
+ column: ColumnDefinition<Row, Header>;
100
+ id: string;
101
+ width: number;
102
+ /**
103
+ * Whether `width` came from the sizing state rather than the column
104
+ * definition — that is, whether the user set it. Auto-fit leaves these
105
+ * columns alone so that resizing one does not undo itself.
106
+ */
107
+ sized: boolean;
108
+ /**
109
+ * What the header shows: the column's own `header`, or a label read off the
110
+ * field path. `string` is in the union because that fallback is text
111
+ * whatever `Header` is bound to.
112
+ */
113
+ label: Header | string;
114
+ /** Whether this column can be resized, after the grid-level default. */
115
+ resizable: boolean;
116
+ /** How this column's cells align, after falling back to its type. */
117
+ alignment: ColumnAlignment;
118
+ }
119
+ /**
120
+ * A resize in progress. It captures its constraints up front so that applying
121
+ * a pointer position is arithmetic on numbers alone — no column, no DOM.
122
+ */
123
+ interface ColumnResizeSession {
124
+ readonly columnId: string;
125
+ readonly startWidth: number;
126
+ readonly startPosition: number;
127
+ readonly constraints: ColumnConstraints;
128
+ }
129
+ /**
130
+ * Reports a user resize. `phase` separates the two audiences: `"move"` fires
131
+ * continuously for live feedback, `"end"` once on release — the one to persist.
132
+ */
133
+ interface ColumnResizeEvent {
134
+ readonly columnId: string;
135
+ readonly width: number;
136
+ readonly sizing: ColumnSizingState;
137
+ readonly phase: "move" | "end";
138
+ }
139
+
140
+ /**
141
+ * How a column of `type` aligns by default. Numeric types align right so that
142
+ * their digits line up by place value; everything else reads from the left.
143
+ */
144
+ declare function alignmentForType(type: ColumnType): ColumnAlignment;
145
+ /**
146
+ * Derives one column per field across `rows`, in the order each field is first
147
+ * seen — so a key that only appears in a later row still gets a column,
148
+ * appended at the end. Nested objects contribute their non-object properties
149
+ * as `"Parent.Child"`; anything deeper is skipped, as is the nested object
150
+ * itself.
151
+ *
152
+ * Order follows `Object.entries`, which lists integer-like keys first
153
+ * regardless of the order they were written in.
154
+ */
155
+ declare function defineColumnsFromRows<Row>(rows: readonly Row[]): readonly ColumnDefinition<Row>[];
156
+ /**
157
+ * The key state about a column is stored under, and the key it renders with.
158
+ * Every such lookup goes through here so the two can never disagree.
159
+ */
160
+ declare function getColumnId<Row>(column: ColumnDefinition<Row, unknown>): string;
161
+ /**
162
+ * What a column's header shows: its own `header`, called first if it is a
163
+ * function, or else a label read off the field path — `"Application.Id"`
164
+ * reading as `"Application Id"`.
165
+ */
166
+ declare function resolveColumnLabel<Row, Header>(column: ColumnDefinition<Row, Header>): Header | string;
167
+ /**
168
+ * Accesses a value at a dotted path, e.g. `foo.bar.baz`.
169
+ *
170
+ * @param obj The object to access
171
+ * @param path Path to the value, e.g. `"foo.bar"`
172
+ * @returns The value at `path`, or `undefined` if it doesn't exist
173
+ */
174
+ declare function accessDotted(obj: unknown, path: string): unknown;
175
+
176
+ /**
177
+ * Applied to any column that does not size itself. The minimum is a width a
178
+ * header stays legible at rather than a measured value — a column dragged
179
+ * narrower than this reads as a rendering fault.
180
+ */
181
+ declare const DEFAULT_COLUMN_SIZES: ColumnSizeDefaults;
182
+ /** How far one arrow key press moves a column edge. */
183
+ declare const KEYBOARD_STEP = 10;
184
+ /**
185
+ * The bounds `column` may be sized between. Resolved in one place so that a
186
+ * drag, a clamp and an auto-fit cannot disagree about a column's limits.
187
+ */
188
+ declare function resolveColumnConstraints<Row>(column: ColumnDefinition<Row, unknown>, defaults?: Partial<ColumnSizeDefaults>): ColumnConstraints;
189
+ /** Holds `width` within `constraints`. */
190
+ declare function clampColumnWidth(width: number, constraints: ColumnConstraints): number;
191
+ /**
192
+ * Pairs each column with everything needed to render it — the width it sits
193
+ * at, the label its header shows, whether it may be resized and how its cells
194
+ * align. Widths take the first of the sizing state, the column's own `width`,
195
+ * and the default, then clamp.
196
+ *
197
+ * The sizing state winning means a user's drag outlives a re-render; a column
198
+ * absent from it still tracks a `width` edited in the definition.
199
+ */
200
+ declare function resolveColumnWidths<Row, Header>(columns: readonly ColumnDefinition<Row, Header>[], sizing: ColumnSizingState, options?: ColumnResolveOptions): readonly ResolvedColumn<Row, Header>[];
201
+ declare function totalColumnWidth<Row, Header>(resolved: readonly ResolvedColumn<Row, Header>[]): number;
202
+ /**
203
+ * Opens a resize. The session captures its constraints and starting point up
204
+ * front, so applying a pointer position later is arithmetic on numbers alone —
205
+ * which is what keeps the drag testable without a DOM.
206
+ *
207
+ * @param startPosition Where the pointer went down, on the axis being dragged.
208
+ */
209
+ declare function beginColumnResize<Row>(column: ColumnDefinition<Row, unknown>, startWidth: number, startPosition: number, defaults?: Partial<ColumnSizeDefaults>): ColumnResizeSession;
210
+ /**
211
+ * The width the dragged column takes with the pointer at `position`. The delta
212
+ * is signed, so a right-to-left adapter negates it and nothing else changes.
213
+ */
214
+ declare function applyColumnResize(session: ColumnResizeSession, position: number): number;
215
+ /**
216
+ * Grows columns proportionally until they fill `availableWidth`.
217
+ *
218
+ * Growth only: columns that already meet or exceed the width are returned
219
+ * untouched, so a container too narrow for them scrolls rather than crushing
220
+ * them. Columns the user has sized are left at that width and the rest share
221
+ * what is left, so dragging one column does not rubber-band it back.
222
+ */
223
+ declare function fitColumnsToWidth<Row, Header>(resolved: readonly ResolvedColumn<Row, Header>[], availableWidth: number, defaults?: Partial<ColumnSizeDefaults>): readonly ResolvedColumn<Row, Header>[];
224
+ /**
225
+ * The width a column needs to show `measuredWidth` of content.
226
+ *
227
+ * `padding` is an allowance on top of the measurement, since a column sized to
228
+ * exactly its content can still round down into an ellipsis.
229
+ */
230
+ declare function sizeColumnToContent(measuredWidth: number, constraints: ColumnConstraints, padding?: number): number;
231
+
232
+ export { type ColumnAlignment, type ColumnConstraints, type ColumnDefinition, type ColumnResizeEvent, type ColumnResizeSession, type ColumnResolveOptions, type ColumnSizeDefaults, type ColumnSizingState, type ColumnType, DEFAULT_COLUMN_SIZES, type FieldPath, KEYBOARD_STEP, type ResolvedColumn, accessDotted, alignmentForType, applyColumnResize, beginColumnResize, clampColumnWidth, defineColumnsFromRows, fitColumnsToWidth, getColumnId, resolveColumnConstraints, resolveColumnLabel, resolveColumnWidths, sizeColumnToContent, totalColumnWidth };
package/dist/index.js ADDED
@@ -0,0 +1,195 @@
1
+ // src/util/grid.ts
2
+ function isNested(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
4
+ }
5
+ function getType(value) {
6
+ if (value instanceof Date) return "dateTime";
7
+ if (typeof value === "string") return "string";
8
+ if (typeof value === "number") return "number";
9
+ if (typeof value === "boolean") return "boolean";
10
+ if (typeof value === "bigint") return "number";
11
+ if (typeof value === "symbol") return "string";
12
+ return "string";
13
+ }
14
+ function alignmentForType(type) {
15
+ switch (type) {
16
+ case "number":
17
+ case "decimal":
18
+ case "currency":
19
+ case "percent":
20
+ return "right";
21
+ default:
22
+ return "left";
23
+ }
24
+ }
25
+ function getAlignment(value) {
26
+ return alignmentForType(getType(value));
27
+ }
28
+ function defineColumnsFromRows(rows) {
29
+ const columns = [];
30
+ const seen = /* @__PURE__ */ new Set();
31
+ function add(field, value) {
32
+ if (seen.has(field)) {
33
+ return;
34
+ }
35
+ seen.add(field);
36
+ columns.push({
37
+ field,
38
+ type: getType(value),
39
+ alignment: getAlignment(value)
40
+ });
41
+ }
42
+ for (const row of rows) {
43
+ if (!isNested(row)) {
44
+ continue;
45
+ }
46
+ for (const [key, value] of Object.entries(row)) {
47
+ if (!isNested(value)) {
48
+ add(key, value);
49
+ continue;
50
+ }
51
+ for (const [nestedKey, nestedValue] of Object.entries(value)) {
52
+ if (!isNested(nestedValue)) {
53
+ add(`${key}.${nestedKey}`, nestedValue);
54
+ }
55
+ }
56
+ }
57
+ }
58
+ return columns;
59
+ }
60
+ function getColumnId(column) {
61
+ return column.id ?? column.field;
62
+ }
63
+ function resolveColumnLabel(column) {
64
+ const { header } = column;
65
+ if (header === void 0) {
66
+ return column.field.split(".").join(" ");
67
+ }
68
+ return typeof header === "function" ? header() : header;
69
+ }
70
+ function accessDotted(obj, path) {
71
+ return path.split(".").reduce((value, key) => isNested(value) ? value[key] : void 0, obj);
72
+ }
73
+
74
+ // src/util/sizing.ts
75
+ var DEFAULT_COLUMN_SIZES = {
76
+ width: 150,
77
+ minWidth: 40,
78
+ maxWidth: Number.POSITIVE_INFINITY
79
+ };
80
+ var KEYBOARD_STEP = 10;
81
+ function withDefaults(defaults) {
82
+ return { ...DEFAULT_COLUMN_SIZES, ...defaults };
83
+ }
84
+ function resolveColumnConstraints(column, defaults) {
85
+ const resolved = withDefaults(defaults);
86
+ return {
87
+ minWidth: column.minWidth ?? resolved.minWidth,
88
+ maxWidth: column.maxWidth ?? resolved.maxWidth
89
+ };
90
+ }
91
+ function clampColumnWidth(width, constraints) {
92
+ return Math.min(Math.max(width, constraints.minWidth), constraints.maxWidth);
93
+ }
94
+ function resolveColumnWidths(columns, sizing, options) {
95
+ const resolved = withDefaults(options?.sizes);
96
+ return columns.map((column) => {
97
+ const id = getColumnId(column);
98
+ const stored = sizing[id];
99
+ return {
100
+ column,
101
+ id,
102
+ sized: stored !== void 0,
103
+ width: clampColumnWidth(
104
+ stored ?? column.width ?? resolved.width,
105
+ resolveColumnConstraints(column, resolved)
106
+ ),
107
+ label: resolveColumnLabel(column),
108
+ resizable: column.resizable ?? options?.resizable ?? false,
109
+ alignment: column.alignment ?? alignmentForType(column.type ?? "string")
110
+ };
111
+ });
112
+ }
113
+ function totalColumnWidth(resolved) {
114
+ return resolved.reduce((total, entry) => total + entry.width, 0);
115
+ }
116
+ function beginColumnResize(column, startWidth, startPosition, defaults) {
117
+ return {
118
+ columnId: getColumnId(column),
119
+ startWidth,
120
+ startPosition,
121
+ constraints: resolveColumnConstraints(column, defaults)
122
+ };
123
+ }
124
+ function applyColumnResize(session, position) {
125
+ return clampColumnWidth(
126
+ session.startWidth + (position - session.startPosition),
127
+ session.constraints
128
+ );
129
+ }
130
+ function fitColumnsToWidth(resolved, availableWidth, defaults) {
131
+ const target = Math.floor(availableWidth);
132
+ if (resolved.length === 0 || totalColumnWidth(resolved) >= target) {
133
+ return resolved;
134
+ }
135
+ const entries = resolved.map((entry) => ({
136
+ entry,
137
+ width: entry.width,
138
+ maxWidth: resolveColumnConstraints(entry.column, defaults).maxWidth,
139
+ growable: !entry.sized
140
+ }));
141
+ const sum = () => entries.reduce((total, item) => total + item.width, 0);
142
+ for (; ; ) {
143
+ const growing = entries.filter(
144
+ (item) => item.growable && item.width < item.maxWidth
145
+ );
146
+ const surplus = target - sum();
147
+ if (surplus <= 0 || growing.length === 0) {
148
+ break;
149
+ }
150
+ const base = growing.reduce((total, item) => total + item.width, 0);
151
+ let clamped = false;
152
+ for (const item of growing) {
153
+ const share = base === 0 ? surplus / growing.length : surplus * item.width / base;
154
+ if (item.width + share >= item.maxWidth) {
155
+ item.width = item.maxWidth;
156
+ clamped = true;
157
+ } else {
158
+ item.width += share;
159
+ }
160
+ }
161
+ if (!clamped) {
162
+ break;
163
+ }
164
+ }
165
+ let remainder = target - entries.reduce((t, i) => t + Math.floor(i.width), 0);
166
+ return entries.map((item) => {
167
+ let width = Math.floor(item.width);
168
+ if (remainder > 0 && item.growable && width < item.maxWidth) {
169
+ width += 1;
170
+ remainder -= 1;
171
+ }
172
+ return { ...item.entry, width };
173
+ });
174
+ }
175
+ function sizeColumnToContent(measuredWidth, constraints, padding = 2) {
176
+ return clampColumnWidth(Math.ceil(measuredWidth + padding), constraints);
177
+ }
178
+ export {
179
+ DEFAULT_COLUMN_SIZES,
180
+ KEYBOARD_STEP,
181
+ accessDotted,
182
+ alignmentForType,
183
+ applyColumnResize,
184
+ beginColumnResize,
185
+ clampColumnWidth,
186
+ defineColumnsFromRows,
187
+ fitColumnsToWidth,
188
+ getColumnId,
189
+ resolveColumnConstraints,
190
+ resolveColumnLabel,
191
+ resolveColumnWidths,
192
+ sizeColumnToContent,
193
+ totalColumnWidth
194
+ };
195
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util/grid.ts","../src/util/sizing.ts"],"sourcesContent":["import type { ColumnAlignment, ColumnDefinition, ColumnType } from \"../types\";\n\n/**\n * Narrows to a value worth drilling into. Mirrors the `LeafValue` union in\n * `../types`: arrays and `Date`s are cell values rather than groups of\n * columns, and `null` cannot be introspected. Keep the two in sync.\n */\nfunction isNested(value: unknown): value is Record<string, unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date)\n );\n}\n\nfunction getType(value: unknown): ColumnType {\n if (value instanceof Date) return \"dateTime\";\n if (typeof value === \"string\") return \"string\";\n if (typeof value === \"number\") return \"number\";\n if (typeof value === \"boolean\") return \"boolean\";\n if (typeof value === \"bigint\") return \"number\";\n if (typeof value === \"symbol\") return \"string\";\n return \"string\";\n}\n\n/**\n * How a column of `type` aligns by default. Numeric types align right so that\n * their digits line up by place value; everything else reads from the left.\n */\nexport function alignmentForType(type: ColumnType): ColumnAlignment {\n switch (type) {\n case \"number\":\n case \"decimal\":\n case \"currency\":\n case \"percent\":\n return \"right\";\n default:\n return \"left\";\n }\n}\n\nfunction getAlignment(value: unknown): ColumnAlignment {\n return alignmentForType(getType(value));\n}\n\n/**\n * Derives one column per field across `rows`, in the order each field is first\n * seen — so a key that only appears in a later row still gets a column,\n * appended at the end. Nested objects contribute their non-object properties\n * as `\"Parent.Child\"`; anything deeper is skipped, as is the nested object\n * itself.\n *\n * Order follows `Object.entries`, which lists integer-like keys first\n * regardless of the order they were written in.\n */\nexport function defineColumnsFromRows<Row>(\n rows: readonly Row[],\n): readonly ColumnDefinition<Row>[] {\n const columns: ColumnDefinition<Row>[] = [];\n const seen = new Set<string>();\n\n function add(field: string, value: unknown): void {\n if (seen.has(field)) {\n return;\n }\n seen.add(field);\n columns.push({\n field,\n type: getType(value),\n alignment: getAlignment(value),\n });\n }\n\n for (const row of rows) {\n if (!isNested(row)) {\n continue;\n }\n for (const [key, value] of Object.entries(row)) {\n if (!isNested(value)) {\n add(key, value);\n continue;\n }\n for (const [nestedKey, nestedValue] of Object.entries(value)) {\n if (!isNested(nestedValue)) {\n add(`${key}.${nestedKey}`, nestedValue);\n }\n }\n }\n }\n\n return columns;\n}\n\n/**\n * The key state about a column is stored under, and the key it renders with.\n * Every such lookup goes through here so the two can never disagree.\n */\nexport function getColumnId<Row>(\n column: ColumnDefinition<Row, unknown>,\n): string {\n return column.id ?? column.field;\n}\n\n/**\n * What a column's header shows: its own `header`, called first if it is a\n * function, or else a label read off the field path — `\"Application.Id\"`\n * reading as `\"Application Id\"`.\n */\nexport function resolveColumnLabel<Row, Header>(\n column: ColumnDefinition<Row, Header>,\n): Header | string {\n const { header } = column;\n if (header === undefined) {\n return column.field.split(\".\").join(\" \");\n }\n // `Header` is open, so it could itself be a function type and the `typeof`\n // check cannot narrow on its own. A header that is callable is the lazy\n // form by definition — no adapter binds `Header` to a function.\n return typeof header === \"function\" ? (header as () => Header)() : header;\n}\n\n/**\n * Accesses a value at a dotted path, e.g. `foo.bar.baz`.\n *\n * @param obj The object to access\n * @param path Path to the value, e.g. `\"foo.bar\"`\n * @returns The value at `path`, or `undefined` if it doesn't exist\n */\nexport function accessDotted(obj: unknown, path: string): unknown {\n return path\n .split(\".\")\n .reduce((value, key) => (isNested(value) ? value[key] : undefined), obj);\n}\n","import type {\n ColumnConstraints,\n ColumnDefinition,\n ColumnResizeSession,\n ColumnResolveOptions,\n ColumnSizeDefaults,\n ColumnSizingState,\n ResolvedColumn,\n} from \"../types\";\nimport { alignmentForType, getColumnId, resolveColumnLabel } from \"./grid\";\n\n/**\n * Applied to any column that does not size itself. The minimum is a width a\n * header stays legible at rather than a measured value — a column dragged\n * narrower than this reads as a rendering fault.\n */\nexport const DEFAULT_COLUMN_SIZES: ColumnSizeDefaults = {\n width: 150,\n minWidth: 40,\n maxWidth: Number.POSITIVE_INFINITY,\n};\n\n/** How far one arrow key press moves a column edge. */\nexport const KEYBOARD_STEP = 10;\n\nfunction withDefaults(\n defaults?: Partial<ColumnSizeDefaults>,\n): ColumnSizeDefaults {\n return { ...DEFAULT_COLUMN_SIZES, ...defaults };\n}\n\n/**\n * The bounds `column` may be sized between. Resolved in one place so that a\n * drag, a clamp and an auto-fit cannot disagree about a column's limits.\n */\nexport function resolveColumnConstraints<Row>(\n column: ColumnDefinition<Row, unknown>,\n defaults?: Partial<ColumnSizeDefaults>,\n): ColumnConstraints {\n const resolved = withDefaults(defaults);\n return {\n minWidth: column.minWidth ?? resolved.minWidth,\n maxWidth: column.maxWidth ?? resolved.maxWidth,\n };\n}\n\n/** Holds `width` within `constraints`. */\nexport function clampColumnWidth(\n width: number,\n constraints: ColumnConstraints,\n): number {\n return Math.min(Math.max(width, constraints.minWidth), constraints.maxWidth);\n}\n\n/**\n * Pairs each column with everything needed to render it — the width it sits\n * at, the label its header shows, whether it may be resized and how its cells\n * align. Widths take the first of the sizing state, the column's own `width`,\n * and the default, then clamp.\n *\n * The sizing state winning means a user's drag outlives a re-render; a column\n * absent from it still tracks a `width` edited in the definition.\n */\nexport function resolveColumnWidths<Row, Header>(\n columns: readonly ColumnDefinition<Row, Header>[],\n sizing: ColumnSizingState,\n options?: ColumnResolveOptions,\n): readonly ResolvedColumn<Row, Header>[] {\n const resolved = withDefaults(options?.sizes);\n\n return columns.map((column) => {\n const id = getColumnId(column);\n const stored = sizing[id];\n\n return {\n column,\n id,\n sized: stored !== undefined,\n width: clampColumnWidth(\n stored ?? column.width ?? resolved.width,\n resolveColumnConstraints(column, resolved),\n ),\n label: resolveColumnLabel(column),\n resizable: column.resizable ?? options?.resizable ?? false,\n alignment: column.alignment ?? alignmentForType(column.type ?? \"string\"),\n };\n });\n}\n\nexport function totalColumnWidth<Row, Header>(\n resolved: readonly ResolvedColumn<Row, Header>[],\n): number {\n return resolved.reduce((total, entry) => total + entry.width, 0);\n}\n\n/**\n * Opens a resize. The session captures its constraints and starting point up\n * front, so applying a pointer position later is arithmetic on numbers alone —\n * which is what keeps the drag testable without a DOM.\n *\n * @param startPosition Where the pointer went down, on the axis being dragged.\n */\nexport function beginColumnResize<Row>(\n column: ColumnDefinition<Row, unknown>,\n startWidth: number,\n startPosition: number,\n defaults?: Partial<ColumnSizeDefaults>,\n): ColumnResizeSession {\n return {\n columnId: getColumnId(column),\n startWidth,\n startPosition,\n constraints: resolveColumnConstraints(column, defaults),\n };\n}\n\n/**\n * The width the dragged column takes with the pointer at `position`. The delta\n * is signed, so a right-to-left adapter negates it and nothing else changes.\n */\nexport function applyColumnResize(\n session: ColumnResizeSession,\n position: number,\n): number {\n return clampColumnWidth(\n session.startWidth + (position - session.startPosition),\n session.constraints,\n );\n}\n\n/**\n * Grows columns proportionally until they fill `availableWidth`.\n *\n * Growth only: columns that already meet or exceed the width are returned\n * untouched, so a container too narrow for them scrolls rather than crushing\n * them. Columns the user has sized are left at that width and the rest share\n * what is left, so dragging one column does not rubber-band it back.\n */\nexport function fitColumnsToWidth<Row, Header>(\n resolved: readonly ResolvedColumn<Row, Header>[],\n availableWidth: number,\n defaults?: Partial<ColumnSizeDefaults>,\n): readonly ResolvedColumn<Row, Header>[] {\n const target = Math.floor(availableWidth);\n if (resolved.length === 0 || totalColumnWidth(resolved) >= target) {\n return resolved;\n }\n\n const entries = resolved.map((entry) => ({\n entry,\n width: entry.width,\n maxWidth: resolveColumnConstraints(entry.column, defaults).maxWidth,\n growable: !entry.sized,\n }));\n const sum = () => entries.reduce((total, item) => total + item.width, 0);\n\n // Clamping a column at its maximum frees surplus the others have to absorb,\n // so a pass that clamped anything is followed by another.\n for (;;) {\n const growing = entries.filter(\n (item) => item.growable && item.width < item.maxWidth,\n );\n const surplus = target - sum();\n if (surplus <= 0 || growing.length === 0) {\n break;\n }\n\n const base = growing.reduce((total, item) => total + item.width, 0);\n let clamped = false;\n\n for (const item of growing) {\n const share =\n base === 0 ? surplus / growing.length : (surplus * item.width) / base;\n\n if (item.width + share >= item.maxWidth) {\n item.width = item.maxWidth;\n clamped = true;\n } else {\n item.width += share;\n }\n }\n\n if (!clamped) {\n break;\n }\n }\n\n // Fractional widths would leave the table a sub-pixel short of its container\n // and show a scrollbar that never goes away, so floor every column and hand\n // the lost pixels back out one at a time.\n let remainder = target - entries.reduce((t, i) => t + Math.floor(i.width), 0);\n\n return entries.map((item) => {\n let width = Math.floor(item.width);\n if (remainder > 0 && item.growable && width < item.maxWidth) {\n width += 1;\n remainder -= 1;\n }\n return { ...item.entry, width };\n });\n}\n\n/**\n * The width a column needs to show `measuredWidth` of content.\n *\n * `padding` is an allowance on top of the measurement, since a column sized to\n * exactly its content can still round down into an ellipsis.\n */\nexport function sizeColumnToContent(\n measuredWidth: number,\n constraints: ColumnConstraints,\n padding = 2,\n): number {\n return clampColumnWidth(Math.ceil(measuredWidth + padding), constraints);\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@gridkitjs/core",
3
+ "version": "0.0.1",
4
+ "description": "Framework-agnostic data-grid logic: column resolution, alignment and sizing.",
5
+ "license": "MIT",
6
+ "author": "Blagoja Blazhevski",
7
+ "homepage": "https://github.com/blagojablazhevski/gridkit#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/blagojablazhevski/gridkit.git",
11
+ "directory": "packages/core"
12
+ },
13
+ "keywords": [
14
+ "datagrid",
15
+ "grid",
16
+ "table",
17
+ "headless",
18
+ "typescript",
19
+ "framework-agnostic"
20
+ ],
21
+ "type": "module",
22
+ "engines": {
23
+ "node": ">=22"
24
+ },
25
+ "main": "./src/index.ts",
26
+ "types": "./src/index.ts",
27
+ "exports": {
28
+ ".": "./src/index.ts"
29
+ },
30
+ "publishConfig": {
31
+ "main": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ }
38
+ }
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "LICENSE",
43
+ "README.md"
44
+ ],
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "typecheck": "tsc --noEmit",
48
+ "lint": "eslint src",
49
+ "test": "vitest run"
50
+ },
51
+ "devDependencies": {
52
+ "tsup": "^8.5.1",
53
+ "typescript": "^6.0.3",
54
+ "vitest": "^4.1.10"
55
+ }
56
+ }