@swiftbrowser/web 0.3.0 → 0.3.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/package.json +1 -1
- package/src/layout/engine.ts +84 -10
- package/src/styles.css +17 -0
package/package.json
CHANGED
package/src/layout/engine.ts
CHANGED
|
@@ -151,6 +151,10 @@ const LIST_CELL_PADDING = 16;
|
|
|
151
151
|
// is a sentence-case headline-sized line that starts 9 into the 35 gap
|
|
152
152
|
// between cards and ends 10 above its card.
|
|
153
153
|
const LIST_ROW_VPAD = 13;
|
|
154
|
+
// A row holding just a toggle or a stepper has 10 above and below it (so
|
|
155
|
+
// both come out 52 tall); a segmented picker has 14 (60 tall).
|
|
156
|
+
const LIST_ROW_CONTROL_VPAD = 10;
|
|
157
|
+
const LIST_ROW_SEGMENTED_VPAD = 14;
|
|
154
158
|
const LIST_ROW_MIN_HEIGHT = 52;
|
|
155
159
|
const LIST_TOP = 18;
|
|
156
160
|
const LIST_BOTTOM = 20;
|
|
@@ -158,6 +162,8 @@ const LIST_HEADER_LIFT = 9;
|
|
|
158
162
|
const LIST_HEADER_GAP = 10;
|
|
159
163
|
const LIST_FOOTER_GAP = 6;
|
|
160
164
|
const LIST_SECTION_GAP = 35;
|
|
165
|
+
// Below a section's footer the gap to the next section is 12 shorter.
|
|
166
|
+
const LIST_SECTION_GAP_AFTER_FOOTER = 23;
|
|
161
167
|
const LIST_CHEVRON_WIDTH = 20;
|
|
162
168
|
// Phase 4.
|
|
163
169
|
// iOS 26: the tab bar is a 62-tall glass capsule floating 21 from the sides
|
|
@@ -460,6 +466,19 @@ function cellStyle(node: LayoutNode, key: string): unknown {
|
|
|
460
466
|
return undefined;
|
|
461
467
|
}
|
|
462
468
|
|
|
469
|
+
/** The default vertical padding above and below a list row's content. */
|
|
470
|
+
function listRowVPad(row: LayoutNode): number {
|
|
471
|
+
switch (row.kind) {
|
|
472
|
+
case 'picker':
|
|
473
|
+
return str(row.props['style']) === 'segmented' ? LIST_ROW_SEGMENTED_VPAD : 0;
|
|
474
|
+
case 'toggle':
|
|
475
|
+
case 'stepper':
|
|
476
|
+
return LIST_ROW_CONTROL_VPAD;
|
|
477
|
+
default:
|
|
478
|
+
return LIST_ROW_VPAD;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
463
482
|
/** `gridCellColumns` of a grid cell (at least 1). */
|
|
464
483
|
function gridSpanOf(node: LayoutNode): number {
|
|
465
484
|
const span = num(cellStyle(node, 'gridCellColumns'));
|
|
@@ -472,6 +491,20 @@ function gridAnchorOf(node: LayoutNode): [number, number] | null {
|
|
|
472
491
|
return point ? [point.x, point.y] : null;
|
|
473
492
|
}
|
|
474
493
|
|
|
494
|
+
/** `gridColumnAlignment` of a grid cell as a horizontal unit anchor. */
|
|
495
|
+
function gridColumnAnchorOf(node: LayoutNode): number | null {
|
|
496
|
+
switch (str(cellStyle(node, 'gridColumnAlignment'))) {
|
|
497
|
+
case 'leading':
|
|
498
|
+
return 0;
|
|
499
|
+
case 'center':
|
|
500
|
+
return 0.5;
|
|
501
|
+
case 'trailing':
|
|
502
|
+
return 1;
|
|
503
|
+
default:
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
475
508
|
/** Vertical anchor of a `gridrow` alignment (null → the grid's). */
|
|
476
509
|
function rowAnchor(alignment: string | null, gridAlignment: string): number {
|
|
477
510
|
switch (alignment) {
|
|
@@ -892,6 +925,8 @@ interface GridPlan {
|
|
|
892
925
|
size: Size;
|
|
893
926
|
columns: number[];
|
|
894
927
|
columnX: number[];
|
|
928
|
+
/** Horizontal unit anchor per column from a cell's `gridColumnAlignment` (null → the grid's). */
|
|
929
|
+
columnAnchors: (number | null)[];
|
|
895
930
|
rows: GridRowPlan[];
|
|
896
931
|
}
|
|
897
932
|
|
|
@@ -1310,8 +1345,15 @@ export class LayoutPass {
|
|
|
1310
1345
|
height: style === 'plain' ? TEXTFIELD_HEIGHT_PLAIN : TEXTFIELD_HEIGHT_ROUNDED,
|
|
1311
1346
|
};
|
|
1312
1347
|
}
|
|
1313
|
-
case 'color':
|
|
1314
1348
|
case 'shape':
|
|
1349
|
+
if (node.props['shape'] === 'circle') {
|
|
1350
|
+
// SwiftUI's `Circle` is the largest square in the proposal (so in a
|
|
1351
|
+
// stack it gives the leftover width to its siblings).
|
|
1352
|
+
// An open axis counts as SHAPE_DEFAULT, as for the other shapes.
|
|
1353
|
+
return square(Math.min(p.width ?? SHAPE_DEFAULT, p.height ?? SHAPE_DEFAULT));
|
|
1354
|
+
}
|
|
1355
|
+
return { width: p.width ?? SHAPE_DEFAULT, height: p.height ?? SHAPE_DEFAULT };
|
|
1356
|
+
case 'color':
|
|
1315
1357
|
case 'canvas':
|
|
1316
1358
|
return { width: p.width ?? SHAPE_DEFAULT, height: p.height ?? SHAPE_DEFAULT };
|
|
1317
1359
|
case 'video':
|
|
@@ -2178,7 +2220,11 @@ export class LayoutPass {
|
|
|
2178
2220
|
};
|
|
2179
2221
|
for (let g = 0; g < groupsSource.length; g++) {
|
|
2180
2222
|
const source = groupsSource[g] as GroupSource;
|
|
2181
|
-
|
|
2223
|
+
const previous = groups[g - 1];
|
|
2224
|
+
if (previous) {
|
|
2225
|
+
const footed = previous.footer !== null || (previous.footerView !== null && previous.footerView.rect.height > 0);
|
|
2226
|
+
y += plain ? sectionGap : footed ? LIST_SECTION_GAP_AFTER_FOOTER : sectionGap;
|
|
2227
|
+
}
|
|
2182
2228
|
const top = y;
|
|
2183
2229
|
let header: ListGroupPlan['header'] = null;
|
|
2184
2230
|
let footer: ListGroupPlan['footer'] = null;
|
|
@@ -2205,13 +2251,12 @@ export class LayoutPass {
|
|
|
2205
2251
|
continue;
|
|
2206
2252
|
}
|
|
2207
2253
|
// A menu or inline picker already reports its row height (44 for
|
|
2208
|
-
// a menu, 44 per option inline);
|
|
2209
|
-
//
|
|
2254
|
+
// a menu, 44 per option inline); a toggle, stepper or segmented
|
|
2255
|
+
// picker has its own vertical padding, other rows 13. Phase 6
|
|
2210
2256
|
// `listRowInsets` replaces all four insets; a `badge` reserves its
|
|
2211
2257
|
// width (plus a gap) at the trailing edge.
|
|
2212
2258
|
const hints = rowHintsOf(rowNode);
|
|
2213
|
-
const
|
|
2214
|
-
const defaultVPad = unwrappedRow.kind === 'picker' && str(unwrappedRow.props['style']) !== 'segmented' ? 0 : LIST_ROW_VPAD;
|
|
2259
|
+
const defaultVPad = listRowVPad(unwrapStyled(rowNode));
|
|
2215
2260
|
const insets: EdgeInsets = hints.insets ?? {
|
|
2216
2261
|
top: defaultVPad,
|
|
2217
2262
|
leading: LIST_CELL_PADDING,
|
|
@@ -3613,8 +3658,12 @@ export class LayoutPass {
|
|
|
3613
3658
|
* whose non-spanning cells are all flexible (or that has none) is flexible;
|
|
3614
3659
|
* the others take the largest ideal width of their non-spanning cells. The
|
|
3615
3660
|
* flexible columns share what is left of the proposal width equally (their
|
|
3616
|
-
* ideal width with a nil proposal).
|
|
3617
|
-
* cell
|
|
3661
|
+
* ideal width with a nil proposal). A non-`gridrow` child is one full-span
|
|
3662
|
+
* cell; one wider than the columns (a `Divider` takes the whole proposal)
|
|
3663
|
+
* widens the grid, every column by the same amount (unless it has
|
|
3664
|
+
* `gridCellUnsizedAxes(.horizontal)`). Row heights come from
|
|
3665
|
+
* measuring every cell at its cell width. A cell's `gridColumnAlignment`
|
|
3666
|
+
* aligns its whole column.
|
|
3618
3667
|
*/
|
|
3619
3668
|
private gridPlan(node: LayoutNode, ctx: Context, p: Proposal): GridPlan {
|
|
3620
3669
|
return this.plan(node, p, () => {
|
|
@@ -3677,6 +3726,30 @@ export class LayoutPass {
|
|
|
3677
3726
|
if (flexible[j]) columns[j] = flexibleCount > 0 ? remaining / flexibleCount : 0;
|
|
3678
3727
|
}
|
|
3679
3728
|
}
|
|
3729
|
+
// Full-span children wider than the columns widen every column alike.
|
|
3730
|
+
const fullProposal: Proposal = { width: finite(p.width) ? p.width : null, height: null };
|
|
3731
|
+
let fullWidth = 0;
|
|
3732
|
+
for (const source of sources) {
|
|
3733
|
+
if (!source.full) continue;
|
|
3734
|
+
const cctx = this.gridCellContext(node, null, ctx);
|
|
3735
|
+
for (const cell of source.cells) {
|
|
3736
|
+
if (cellStyle(cell.node, 'gridCellUnsizedAxes') === true) continue;
|
|
3737
|
+
fullWidth = Math.max(fullWidth, this.measure(cell.node, cctx, fullProposal).width);
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
const columnsWidth = columns.reduce((sum, w) => sum + w, 0) + hSpacing * (columnCount - 1);
|
|
3741
|
+
if (fullWidth > columnsWidth + EPSILON) {
|
|
3742
|
+
const extra = (fullWidth - columnsWidth) / columnCount;
|
|
3743
|
+
for (let j = 0; j < columnCount; j++) columns[j] = (columns[j] as number) + extra;
|
|
3744
|
+
}
|
|
3745
|
+
const columnAnchors = new Array<number | null>(columnCount).fill(null);
|
|
3746
|
+
for (const source of sources) {
|
|
3747
|
+
if (source.full) continue;
|
|
3748
|
+
for (const cell of source.cells) {
|
|
3749
|
+
const anchor = cell.span === 1 ? gridColumnAnchorOf(cell.node) : null;
|
|
3750
|
+
if (anchor !== null && columnAnchors[cell.column] === null) columnAnchors[cell.column] = anchor;
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3680
3753
|
const columnX = new Array<number>(columnCount).fill(0);
|
|
3681
3754
|
for (let j = 1; j < columnCount; j++) {
|
|
3682
3755
|
columnX[j] = (columnX[j - 1] as number) + (columns[j - 1] as number) + hSpacing;
|
|
@@ -3713,7 +3786,7 @@ export class LayoutPass {
|
|
|
3713
3786
|
y += height + vSpacing;
|
|
3714
3787
|
}
|
|
3715
3788
|
const gridHeight = rows.length > 0 ? y - vSpacing : 0;
|
|
3716
|
-
return { size: { width: gridWidth, height: gridHeight }, columns, columnX, rows };
|
|
3789
|
+
return { size: { width: gridWidth, height: gridHeight }, columns, columnX, columnAnchors, rows };
|
|
3717
3790
|
});
|
|
3718
3791
|
}
|
|
3719
3792
|
|
|
@@ -3727,7 +3800,8 @@ export class LayoutPass {
|
|
|
3727
3800
|
const rowY = rowAnchor(row.alignment, gridAlignment);
|
|
3728
3801
|
for (const cell of row.cells) {
|
|
3729
3802
|
const box: Size = { width: cell.width, height: row.height };
|
|
3730
|
-
const
|
|
3803
|
+
const columnX = cell.full ? null : (plan.columnAnchors[cell.column] ?? null);
|
|
3804
|
+
const [ax, ay] = gridAnchorOf(cell.node) ?? [columnX ?? gridX, rowY];
|
|
3731
3805
|
const dx = safeOffset(box.width, cell.size.width, ax);
|
|
3732
3806
|
const dy = safeOffset(box.height, cell.size.height, ay);
|
|
3733
3807
|
const cellX = cell.full ? 0 : (plan.columnX[cell.column] ?? 0);
|
package/src/styles.css
CHANGED
|
@@ -1026,6 +1026,23 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
1026
1026
|
pointer-events: none;
|
|
1027
1027
|
}
|
|
1028
1028
|
|
|
1029
|
+
/* A text field row is the <input> (or <textarea>) itself, which has no
|
|
1030
|
+
::after: its separator is a 1px background image in the same place. */
|
|
1031
|
+
.sb-section-rows > :is(input, textarea):not(:last-child),
|
|
1032
|
+
.sb-list-content > :is(input, textarea):not(:last-child):not(:has(+ .sb-section)) {
|
|
1033
|
+
background-image: linear-gradient(var(--sb-color-row-separator), var(--sb-color-row-separator));
|
|
1034
|
+
background-repeat: no-repeat;
|
|
1035
|
+
background-position: var(--sb-separator-leading, 16px) 100%;
|
|
1036
|
+
background-size: calc(100% - var(--sb-separator-leading, 16px) - 16px) 1px;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
.sb-section-rows > :is(input, textarea)[data-sb-separator-bottom="hidden"],
|
|
1040
|
+
.sb-list-content > :is(input, textarea)[data-sb-separator-bottom="hidden"],
|
|
1041
|
+
.sb-section-rows > :is(input, textarea):has(+ [data-sb-separator-top="hidden"]),
|
|
1042
|
+
.sb-list-content > :is(input, textarea):has(+ [data-sb-separator-top="hidden"]) {
|
|
1043
|
+
background-image: none;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1029
1046
|
.sb-section-rows > .sb-button,
|
|
1030
1047
|
.sb-list-content > .sb-button {
|
|
1031
1048
|
border-radius: 0;
|