@astratra/native-ui 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.
- package/LICENSE +21 -0
- package/README.md +370 -0
- package/package.json +64 -0
- package/src/components/Chevron.js +31 -0
- package/src/components/CollapsibleHeader.js +253 -0
- package/src/components/FloatingPagination.js +135 -0
- package/src/components/GlassButton.js +117 -0
- package/src/components/GlassSurface.js +61 -0
- package/src/components/MarkdownView.js +322 -0
- package/src/components/PaleCard.js +83 -0
- package/src/components/TabBar.js +346 -0
- package/src/components/runtime.js +88 -0
- package/src/components/useCollapsingBar.js +55 -0
- package/src/index.d.ts +229 -0
- package/src/index.js +20 -0
- package/src/logic/anchorQuestion.js +41 -0
- package/src/logic/collapse.js +77 -0
- package/src/logic/collapsibleHeader.js +103 -0
- package/src/logic/color.js +68 -0
- package/src/logic/glass.js +151 -0
- package/src/logic/index.d.ts +203 -0
- package/src/logic/index.js +16 -0
- package/src/logic/markdown.js +229 -0
- package/src/logic/paleCard.js +66 -0
- package/src/logic/tabBar.js +104 -0
- package/src/logic/tableColumns.js +149 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column widths for a table in an AI answer — the rule, dry.
|
|
3
|
+
*
|
|
4
|
+
* THE DEFECT. The table was rendered in a horizontal scroll, but each cell was
|
|
5
|
+
* free text in a row, WITHOUT a column width. React Native then shares the
|
|
6
|
+
* room according to the content of EACH ROW: the header and its rows do not
|
|
7
|
+
* line up, "Subscription" breaks, values float. The table is no longer a
|
|
8
|
+
* table — it is three lists side by side that don't talk to each other.
|
|
9
|
+
*
|
|
10
|
+
* THE RULE. A column has ONE width, computed once for the whole table from its
|
|
11
|
+
* longest content — header included, since the header is what was breaking.
|
|
12
|
+
* The measure is in characters: with no access to the text engine, a
|
|
13
|
+
* character's width is approximated by the font's average width at the size
|
|
14
|
+
* used. An assumed approximation, because the error it makes (a column a bit
|
|
15
|
+
* wide) is invisible, while the error before (misaligned columns) jumped out.
|
|
16
|
+
*
|
|
17
|
+
* WHY A FLOOR ON THE LONGEST WORD. A column narrower than its longest word
|
|
18
|
+
* cuts that word in the middle — "Subscrip- / tion". The floor prevents it: a
|
|
19
|
+
* whole word always fits, and wrapping only happens between two words.
|
|
20
|
+
*
|
|
21
|
+
* WHY A CEILING. Without one, a whole sentence in a cell pushes the column to
|
|
22
|
+
* 600 points and the horizontal scroll never ends. Above the ceiling the text
|
|
23
|
+
* wraps in a taller cell — the row grows, the alignment holds.
|
|
24
|
+
*
|
|
25
|
+
* WHY THE ROUNDING IS INJECTED. A width must land on the screen's pixel grid,
|
|
26
|
+
* or the separating hairline renders as two blurry greys. `PixelRatio` only
|
|
27
|
+
* exists in the app; here the rounding function is received, and a test
|
|
28
|
+
* swaps in the one of a chosen density.
|
|
29
|
+
*
|
|
30
|
+
* MEASURE THE VISIBLE TEXT. Cells go through the inline Markdown renderer
|
|
31
|
+
* ("**Starter**" is shown bold, without its stars), so the caller measures
|
|
32
|
+
* `inlinesToText(parseInline(cell))`, not the raw source.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const DEFAULTS = Object.freeze({
|
|
36
|
+
/* MEASURED TOO NARROW. These were 6.4 and 6.9: at the real size of the cells
|
|
37
|
+
(13.5) and headers (11, uppercase, letter-spaced), a digit takes more.
|
|
38
|
+
"17/09/2026" fell into a column too short and broke into "17/09/202"
|
|
39
|
+
then "6". Measure wide: a column one point too wide doesn't show, a word
|
|
40
|
+
cut in two does. */
|
|
41
|
+
charWidth: 7.4,
|
|
42
|
+
/* THE HEADER IS SHOWN IN SPACED CAPITALS. Measured like the body, "STATUS"
|
|
43
|
+
fell into a column too short and broke into "STATU / S": a capital is
|
|
44
|
+
wider than an average lowercase letter, and letter-spacing adds to it. */
|
|
45
|
+
headerCharWidth: 8.6,
|
|
46
|
+
/* THE REAL PADDING, NOT AN OLD ONE. This margin was 20 — EXACTLY the cell
|
|
47
|
+
padding of the time (10 left, 10 right). Not one pixel was left for
|
|
48
|
+
rounding, and "Number" broke into "Numbe / r". A floor exact to the pixel
|
|
49
|
+
is not a floor. It follows the current padding (12 + 12). */
|
|
50
|
+
padding: 24,
|
|
51
|
+
minWidth: 58,
|
|
52
|
+
maxWidth: 176,
|
|
53
|
+
targetWidth: 0
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/* A number, an amount, a percentage, a mark out of twenty: what reads
|
|
57
|
+
right-aligned, because the eye then compares units with units. Thousands
|
|
58
|
+
separators and decimal commas are allowed, and any currency symbol (\p{Sc})
|
|
59
|
+
before or after. */
|
|
60
|
+
const NUMERIC_VALUE = /^[\p{Sc}+\-−]?\s?\d[\d\s.,/]*\s?(?:%|\p{Sc}|\/\s?\d+)?$/u;
|
|
61
|
+
|
|
62
|
+
function isNumeric(value) {
|
|
63
|
+
const text = String(value ?? '').trim();
|
|
64
|
+
if (!text) return false;
|
|
65
|
+
return NUMERIC_VALUE.test(text);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The longest word of a cell, in characters (code points, not UTF-16 units). */
|
|
69
|
+
function longestWord(text) {
|
|
70
|
+
return text
|
|
71
|
+
.trim()
|
|
72
|
+
.split(/\s+/)
|
|
73
|
+
.reduce((longest, word) => Math.max(longest, [...word].length), 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function lengthOf(text) {
|
|
77
|
+
return [...text.trim()].length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string[]} header
|
|
82
|
+
* @param {string[][]} rows
|
|
83
|
+
* @param {object} [options] charWidth, headerCharWidth, padding, minWidth,
|
|
84
|
+
* maxWidth, targetWidth, fontScale, round.
|
|
85
|
+
* @returns {{widths: number[], numeric: boolean[], totalWidth: number}}
|
|
86
|
+
*/
|
|
87
|
+
function measureColumns(header, rows, options = {}) {
|
|
88
|
+
const o = { ...DEFAULTS, ...options };
|
|
89
|
+
const round = typeof options.round === 'function' ? options.round : (value) => value;
|
|
90
|
+
/* THE PHONE'S TEXT SIZE. Text grows with the size chosen in the phone's
|
|
91
|
+
settings; the columns must grow too, or a phone set to large text cuts
|
|
92
|
+
every header. Character widths follow it, the padding does not. A
|
|
93
|
+
smaller setting never shrinks below the normal measure. */
|
|
94
|
+
const scale = Math.max(1, options.fontScale ?? 1);
|
|
95
|
+
const charWidth = o.charWidth * scale;
|
|
96
|
+
const headerCharWidth = o.headerCharWidth * scale;
|
|
97
|
+
|
|
98
|
+
/* The column count comes from the fullest row, not just the header: a
|
|
99
|
+
model sometimes writes a row longer than its header, and a cell with no
|
|
100
|
+
column disappeared silently. */
|
|
101
|
+
const columnCount = Math.max(header.length, ...rows.map((row) => row.length), 0);
|
|
102
|
+
|
|
103
|
+
const widths = [];
|
|
104
|
+
const numeric = [];
|
|
105
|
+
|
|
106
|
+
for (let column = 0; column < columnCount; column += 1) {
|
|
107
|
+
const title = header[column] ?? '';
|
|
108
|
+
const cells = rows.map((row) => row[column] ?? '');
|
|
109
|
+
|
|
110
|
+
/* Wanted width: the longest content, header included. */
|
|
111
|
+
const wantedBody = Math.max(0, ...cells.map(lengthOf)) * charWidth;
|
|
112
|
+
const wantedHeader = lengthOf(title) * headerCharWidth;
|
|
113
|
+
const wanted = Math.max(wantedBody, wantedHeader) + o.padding;
|
|
114
|
+
|
|
115
|
+
/* Floor: the longest word must fit whole, even past the ceiling. */
|
|
116
|
+
const wordFloor =
|
|
117
|
+
Math.max(longestWord(title) * headerCharWidth, ...cells.map((cell) => longestWord(cell) * charWidth), 0) +
|
|
118
|
+
o.padding;
|
|
119
|
+
|
|
120
|
+
const bounded = Math.min(Math.max(wanted, o.minWidth), o.maxWidth);
|
|
121
|
+
widths.push(round(Math.max(bounded, wordFloor)));
|
|
122
|
+
|
|
123
|
+
/* Numeric only if it has values and ALL are numbers. One text value —
|
|
124
|
+
"unpaid" among amounts — and the column is text again: mixed alignment
|
|
125
|
+
reads worse than a uniform left alignment. */
|
|
126
|
+
const filled = cells.filter((cell) => cell.trim().length > 0);
|
|
127
|
+
numeric.push(filled.length > 0 && filled.every(isNumeric));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Stretch to the target width, proportionally: every column grows by the
|
|
131
|
+
same percentage, so the width ratios — and the reading — don't change.
|
|
132
|
+
The old `minWidth: 300` on the frame widened the border but not the
|
|
133
|
+
columns, leaving a gap on the right. The rounding remainder goes to the
|
|
134
|
+
widest column, the only one where one more point doesn't show. */
|
|
135
|
+
const total = widths.reduce((sum, width) => sum + width, 0);
|
|
136
|
+
if (o.targetWidth > total && total > 0) {
|
|
137
|
+
const factor = o.targetWidth / total;
|
|
138
|
+
for (let i = 0; i < widths.length; i += 1) widths[i] = round(widths[i] * factor);
|
|
139
|
+
const missing = o.targetWidth - widths.reduce((sum, width) => sum + width, 0);
|
|
140
|
+
if (missing > 0) {
|
|
141
|
+
const widest = widths.indexOf(Math.max(...widths));
|
|
142
|
+
widths[widest] = round(widths[widest] + missing);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { widths, numeric, totalWidth: widths.reduce((sum, width) => sum + width, 0) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = { TABLE_COLUMN_DEFAULTS: DEFAULTS, isNumeric, measureColumns };
|