@swiftbrowser/web 0.2.0 → 0.3.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 +98 -57
- package/app/index.html +10 -10
- package/node/dist/node/examples.d.ts.map +1 -1
- package/node/dist/node/examples.js +1 -0
- package/node/dist/node/examples.js.map +1 -1
- package/node/dist/plugins/asset-catalogs.js +10 -10
- package/node/dist/plugins/asset-catalogs.js.map +1 -1
- package/package.json +1 -1
- package/plugins/asset-catalogs.ts +10 -10
- package/src/interaction.ts +50 -2
- package/src/largeTitle.ts +79 -0
- package/src/layout/engine.ts +411 -158
- package/src/layoutDom.ts +13 -3
- package/src/main.ts +4 -1
- package/src/mock.ts +2 -2
- package/src/protocol.ts +22 -2
- package/src/renderer.ts +19 -8
- package/src/scanner.ts +1 -1
- package/src/styles.css +313 -97
- package/src/styles.ts +30 -0
- package/src/symbols.ts +97 -10
- package/src/webAuth.ts +1 -1
package/src/styles.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { assetColorVar } from './assets';
|
|
17
17
|
import {
|
|
18
|
+
isAngularGradientStyle,
|
|
18
19
|
isAssetColor,
|
|
19
20
|
isColor,
|
|
20
21
|
isColorGradientStyle,
|
|
@@ -169,6 +170,10 @@ export function styleToCSS(style: ShapeStyle, role: PaintRole): PaintCSS {
|
|
|
169
170
|
const { stops, center, startRadius, endRadius } = style.radialGradient;
|
|
170
171
|
return gradient(radialGradientCSS(stops ?? [], center, startRadius, endRadius, opacity), stops, opacity);
|
|
171
172
|
}
|
|
173
|
+
if (isAngularGradientStyle(style)) {
|
|
174
|
+
const { stops, center, startAngle, endAngle } = style.angularGradient;
|
|
175
|
+
return gradient(angularGradientCSS(stops ?? [], center, startAngle, endAngle, opacity), stops, opacity);
|
|
176
|
+
}
|
|
172
177
|
if (isColorGradientStyle(style)) {
|
|
173
178
|
const base = colorToCSS(style.colorGradient);
|
|
174
179
|
const light = withOpacity(`color-mix(in srgb, ${base}, white 12%)`, opacity);
|
|
@@ -243,6 +248,27 @@ export function radialGradientCSS(
|
|
|
243
248
|
return `radial-gradient(circle ${size} at ${round(cx * 100)}% ${round(cy * 100)}%, ${list})`;
|
|
244
249
|
}
|
|
245
250
|
|
|
251
|
+
/**
|
|
252
|
+
* `conic-gradient(from <angle> at <cx>% <cy>%, ...)`. SwiftUI measures angles
|
|
253
|
+
* clockwise from 3 o'clock and CSS from 12 o'clock, so the CSS start is the
|
|
254
|
+
* start angle plus 90°; the stops span start...end as a share of the turn.
|
|
255
|
+
*/
|
|
256
|
+
export function angularGradientCSS(
|
|
257
|
+
stops: readonly GradientStop[],
|
|
258
|
+
center: UnitPoint | undefined,
|
|
259
|
+
startAngle: number | undefined,
|
|
260
|
+
endAngle: number | undefined,
|
|
261
|
+
opacity?: number,
|
|
262
|
+
): string {
|
|
263
|
+
const cx = unit(center?.x, 0.5);
|
|
264
|
+
const cy = unit(center?.y, 0.5);
|
|
265
|
+
const start = unit(startAngle, 0);
|
|
266
|
+
const end = unit(endAngle, start + 360);
|
|
267
|
+
const span = end === start ? 360 : end - start;
|
|
268
|
+
const list = stopsCSS(stops, (location) => (location * span) / 360, opacity);
|
|
269
|
+
return `conic-gradient(from ${round(start + 90)}deg at ${round(cx * 100)}% ${round(cy * 100)}%, ${list})`;
|
|
270
|
+
}
|
|
271
|
+
|
|
246
272
|
/**
|
|
247
273
|
* `color pct%, ...`, with `map` turning a stop location into a 0...1 position
|
|
248
274
|
* and `opacity` multiplying every stop. Degenerate lists are padded to two stops.
|
|
@@ -386,6 +412,10 @@ export function maskImageCSS(style: ShapeStyle): string {
|
|
|
386
412
|
const { stops, center, startRadius, endRadius } = style.radialGradient;
|
|
387
413
|
return radialGradientCSS(stops ?? [], center, startRadius, endRadius, style.opacity);
|
|
388
414
|
}
|
|
415
|
+
if (isAngularGradientStyle(style)) {
|
|
416
|
+
const { stops, center, startAngle, endAngle } = style.angularGradient;
|
|
417
|
+
return angularGradientCSS(stops ?? [], center, startAngle, endAngle, style.opacity);
|
|
418
|
+
}
|
|
389
419
|
if (isColorGradientStyle(style)) return styleToCSS(style, 'paint').value;
|
|
390
420
|
const alpha = isColor(style) ? colorAlpha(style) : clamp01(style.opacity ?? 1);
|
|
391
421
|
const solid = `rgba(0, 0, 0, ${round(alpha)})`;
|
package/src/symbols.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
Bitcoin,
|
|
59
59
|
Bluetooth,
|
|
60
60
|
Bold,
|
|
61
|
+
CaseSensitive,
|
|
61
62
|
Book,
|
|
62
63
|
Bookmark,
|
|
63
64
|
BookOpen,
|
|
@@ -310,6 +311,7 @@ import {
|
|
|
310
311
|
Send,
|
|
311
312
|
Server,
|
|
312
313
|
Settings,
|
|
314
|
+
Shapes,
|
|
313
315
|
Share,
|
|
314
316
|
Share2,
|
|
315
317
|
Shield,
|
|
@@ -426,6 +428,41 @@ export interface SymbolGlyph {
|
|
|
426
428
|
fill: GlyphFill;
|
|
427
429
|
}
|
|
428
430
|
|
|
431
|
+
/**
|
|
432
|
+
* The activity indicator (an indeterminate `ProgressView`), as iOS draws it:
|
|
433
|
+
* eight rounded spokes filling the glyph, the leading one (at the top) at full
|
|
434
|
+
* strength and the three behind it fading out to the rest's 0.31. styles.css
|
|
435
|
+
* steps that ramp around the spokes clockwise (the glyph itself stays still),
|
|
436
|
+
* so the fade trails the leading spoke.
|
|
437
|
+
*/
|
|
438
|
+
const ACTIVITY_INDICATOR: IconNode = [
|
|
439
|
+
['line', { x1: '12', y1: '6.21', x2: '12', y2: '1.52', 'stroke-width': '3.05', 'stroke-opacity': '1' }],
|
|
440
|
+
['line', { x1: '16.09', y1: '7.91', x2: '19.41', y2: '4.59', 'stroke-width': '3.05', 'stroke-opacity': '0.31' }],
|
|
441
|
+
['line', { x1: '17.79', y1: '12', x2: '22.48', y2: '12', 'stroke-width': '3.05', 'stroke-opacity': '0.31' }],
|
|
442
|
+
['line', { x1: '16.09', y1: '16.09', x2: '19.41', y2: '19.41', 'stroke-width': '3.05', 'stroke-opacity': '0.31' }],
|
|
443
|
+
['line', { x1: '12', y1: '17.79', x2: '12', y2: '22.48', 'stroke-width': '3.05', 'stroke-opacity': '0.31' }],
|
|
444
|
+
['line', { x1: '7.91', y1: '16.09', x2: '4.59', y2: '19.41', 'stroke-width': '3.05', 'stroke-opacity': '0.49' }],
|
|
445
|
+
['line', { x1: '6.21', y1: '12', x2: '1.52', y2: '12', 'stroke-width': '3.05', 'stroke-opacity': '0.66' }],
|
|
446
|
+
['line', { x1: '7.91', y1: '7.91', x2: '4.59', y2: '4.59', 'stroke-width': '3.05', 'stroke-opacity': '0.83' }],
|
|
447
|
+
];
|
|
448
|
+
|
|
449
|
+
/** `books.vertical.fill`: three upright books and a leaning one, as solid shapes. */
|
|
450
|
+
const BOOKS_VERTICAL_FILL: IconNode = [
|
|
451
|
+
['rect', { x: '1.5', y: '3', width: '5', height: '18', rx: '1.2', stroke: 'none' }],
|
|
452
|
+
['rect', { x: '7.5', y: '3', width: '5', height: '18', rx: '1.2', stroke: 'none' }],
|
|
453
|
+
['rect', { x: '13.5', y: '5', width: '4', height: '16', rx: '1.2', stroke: 'none' }],
|
|
454
|
+
['rect', { x: '17.8', y: '4.5', width: '4', height: '16', rx: '1.2', stroke: 'none', transform: 'rotate(-14 19.8 12.5)' }],
|
|
455
|
+
];
|
|
456
|
+
|
|
457
|
+
/** Color used for inner marks of `knockout` glyphs; styles.css sets the variable. */
|
|
458
|
+
const KNOCKOUT_COLOR = 'var(--sb-icon-knockout, var(--sb-color-system-background, #fff))';
|
|
459
|
+
|
|
460
|
+
/** `square.on.circle.fill`: a solid square over a solid circle, a gap between them. */
|
|
461
|
+
const SQUARE_ON_CIRCLE_FILL: IconNode = [
|
|
462
|
+
['circle', { cx: '9.5', cy: '9.5', r: '7.5' }],
|
|
463
|
+
['rect', { x: '9', y: '9', width: '13', height: '13', rx: '3', stroke: KNOCKOUT_COLOR, 'stroke-width': '2.4' }],
|
|
464
|
+
];
|
|
465
|
+
|
|
429
466
|
const outline = (icon: IconNode): SymbolGlyph => ({ icon, fill: 'outline' });
|
|
430
467
|
const knockout = (icon: IconNode): SymbolGlyph => ({ icon, fill: 'knockout' });
|
|
431
468
|
const solid = (icon: IconNode): SymbolGlyph => ({ icon, fill: 'solid' });
|
|
@@ -532,7 +569,7 @@ export const SF_SYMBOLS: Readonly<Record<string, SymbolGlyph>> = {
|
|
|
532
569
|
'flag.fill': solid(Flag),
|
|
533
570
|
tag: outline(Tag),
|
|
534
571
|
'tag.fill': solid(Tag),
|
|
535
|
-
textformat: outline(
|
|
572
|
+
textformat: outline(CaseSensitive),
|
|
536
573
|
'line.3.horizontal': outline(Menu),
|
|
537
574
|
'line.3.horizontal.decrease': outline(Funnel),
|
|
538
575
|
'line.3.horizontal.decrease.circle': outline(Funnel),
|
|
@@ -548,7 +585,7 @@ export const SF_SYMBOLS: Readonly<Record<string, SymbolGlyph>> = {
|
|
|
548
585
|
|
|
549
586
|
// People and places
|
|
550
587
|
person: outline(User),
|
|
551
|
-
'person.fill':
|
|
588
|
+
'person.fill': solid(User),
|
|
552
589
|
'person.2': outline(Users),
|
|
553
590
|
'person.circle': outline(CircleUser),
|
|
554
591
|
'person.circle.fill': knockout(CircleUser),
|
|
@@ -608,7 +645,7 @@ export const SF_SYMBOLS: Readonly<Record<string, SymbolGlyph>> = {
|
|
|
608
645
|
camera: outline(Camera),
|
|
609
646
|
'camera.fill': outline(Camera),
|
|
610
647
|
photo: outline(Image),
|
|
611
|
-
'photo.fill':
|
|
648
|
+
'photo.fill': knockout(Image),
|
|
612
649
|
'photo.on.rectangle': outline(Image),
|
|
613
650
|
play: outline(Play),
|
|
614
651
|
'play.fill': solid(Play),
|
|
@@ -668,7 +705,7 @@ export const SF_SYMBOLS: Readonly<Record<string, SymbolGlyph>> = {
|
|
|
668
705
|
'doc.text.fill': outline(FileText),
|
|
669
706
|
archivebox: outline(Archive),
|
|
670
707
|
'shippingbox': outline(Package),
|
|
671
|
-
'shippingbox.fill':
|
|
708
|
+
'shippingbox.fill': knockout(Package),
|
|
672
709
|
|
|
673
710
|
// Settings and security
|
|
674
711
|
gear: outline(Settings),
|
|
@@ -704,7 +741,7 @@ export const SF_SYMBOLS: Readonly<Record<string, SymbolGlyph>> = {
|
|
|
704
741
|
crown: outline(Crown),
|
|
705
742
|
'crown.fill': solid(Crown),
|
|
706
743
|
trophy: outline(Trophy),
|
|
707
|
-
'trophy.fill':
|
|
744
|
+
'trophy.fill': solid(Trophy),
|
|
708
745
|
|
|
709
746
|
// Nature and weather
|
|
710
747
|
'sun.max': outline(Sun),
|
|
@@ -727,6 +764,7 @@ export const SF_SYMBOLS: Readonly<Record<string, SymbolGlyph>> = {
|
|
|
727
764
|
'circle.grid.3x3': outline(Grid3x3),
|
|
728
765
|
'fork.knife': outline(Utensils),
|
|
729
766
|
'text.bubble': outline(MessageSquareText),
|
|
767
|
+
'text.bubble.fill': knockout(MessageSquareText),
|
|
730
768
|
'plus.bubble': outline(MessageSquarePlus),
|
|
731
769
|
'box.truck': outline(Truck),
|
|
732
770
|
'list.bullet.below.rectangle': outline(LayoutList),
|
|
@@ -758,6 +796,8 @@ export const SF_SYMBOLS: Readonly<Record<string, SymbolGlyph>> = {
|
|
|
758
796
|
|
|
759
797
|
// Activity
|
|
760
798
|
'rays': outline(LoaderCircle),
|
|
799
|
+
// An indeterminate ProgressView's spinner (eight spokes; styles.css animates their fade).
|
|
800
|
+
'progress.indicator': outline(ACTIVITY_INDICATOR),
|
|
761
801
|
'circle.dotted': outline(LoaderCircle),
|
|
762
802
|
|
|
763
803
|
// More common symbols: people, documents, arrows, text, shapes, money,
|
|
@@ -790,7 +830,6 @@ export const SF_SYMBOLS: Readonly<Record<string, SymbolGlyph>> = {
|
|
|
790
830
|
'calendar.badge.plus': outline(CalendarPlus),
|
|
791
831
|
'calendar.badge.clock': outline(CalendarClock),
|
|
792
832
|
'calendar.badge.exclamationmark': outline(CalendarX),
|
|
793
|
-
'calendar.circle': outline(Calendar),
|
|
794
833
|
'clock.arrow.circlepath': outline(History),
|
|
795
834
|
'clock.badge.checkmark': outline(ClockCheck),
|
|
796
835
|
'arrow.uturn.backward': outline(Undo2),
|
|
@@ -821,6 +860,8 @@ export const SF_SYMBOLS: Readonly<Record<string, SymbolGlyph>> = {
|
|
|
821
860
|
'arrowshape.up': outline(ArrowBigUp),
|
|
822
861
|
'arrowshape.down': outline(ArrowBigDown),
|
|
823
862
|
'square.on.square': outline(Copy),
|
|
863
|
+
'square.on.circle': outline(Shapes),
|
|
864
|
+
'square.on.circle.fill': solid(SQUARE_ON_CIRCLE_FILL),
|
|
824
865
|
'plus.square': outline(SquarePlus),
|
|
825
866
|
'plus.square.fill': knockout(SquarePlus),
|
|
826
867
|
'minus.square': outline(SquareMinus),
|
|
@@ -949,6 +990,8 @@ export const SF_SYMBOLS: Readonly<Record<string, SymbolGlyph>> = {
|
|
|
949
990
|
tortoise: outline(Turtle),
|
|
950
991
|
fish: outline(Fish),
|
|
951
992
|
bird: outline(Bird),
|
|
993
|
+
// The Swift logo: a bird in flight.
|
|
994
|
+
swift: outline(Bird),
|
|
952
995
|
cat: outline(Cat),
|
|
953
996
|
dog: outline(Dog),
|
|
954
997
|
carrot: outline(Carrot),
|
|
@@ -1003,6 +1046,7 @@ export const SF_SYMBOLS: Readonly<Record<string, SymbolGlyph>> = {
|
|
|
1003
1046
|
dice: outline(Dices),
|
|
1004
1047
|
newspaper: outline(Newspaper),
|
|
1005
1048
|
'books.vertical': outline(Library),
|
|
1049
|
+
'books.vertical.fill': solid(BOOKS_VERTICAL_FILL),
|
|
1006
1050
|
'note.text': outline(StickyNote),
|
|
1007
1051
|
note: outline(StickyNote),
|
|
1008
1052
|
'location.circle': outline(LocateFixed),
|
|
@@ -1068,6 +1112,21 @@ function resolveVariant(systemName: string): SymbolGlyph | undefined {
|
|
|
1068
1112
|
parts = parts.filter((part) => part !== 'fill');
|
|
1069
1113
|
const badge = parts.indexOf('badge');
|
|
1070
1114
|
if (badge > 0) parts = parts.slice(0, badge);
|
|
1115
|
+
// `trash.circle`, `arrow.down.circle.fill`, `heart.square.fill`: the base
|
|
1116
|
+
// glyph inside the enclosure (drawn whole, or filled with the glyph
|
|
1117
|
+
// knocked out), unless the table lists the full name's base form.
|
|
1118
|
+
const enclosure = parts.at(-1);
|
|
1119
|
+
if ((enclosure === 'circle' || enclosure === 'square') && parts.length > 1) {
|
|
1120
|
+
const listed = SF_SYMBOLS[parts.join('.')];
|
|
1121
|
+
if (listed) {
|
|
1122
|
+
// The listed outline form, its enclosure filled (`arrow.down.circle.fill`).
|
|
1123
|
+
if (fill && enclosingShapeIndex(listed.icon) >= 0) return { icon: listed.icon, fill: 'knockout' };
|
|
1124
|
+
} else {
|
|
1125
|
+
const base = parts.slice(0, -1).join('.');
|
|
1126
|
+
const glyph = GENERIC_SHAPES.has(base) ? undefined : lookupSymbol(base);
|
|
1127
|
+
if (glyph) return enclosed(glyph.icon, enclosure, fill);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1071
1130
|
const bases: string[] = [];
|
|
1072
1131
|
for (let length = parts.length; length > 0; length--) {
|
|
1073
1132
|
const base = parts.slice(0, length).join('.');
|
|
@@ -1084,6 +1143,31 @@ function resolveVariant(systemName: string): SymbolGlyph | undefined {
|
|
|
1084
1143
|
return undefined;
|
|
1085
1144
|
}
|
|
1086
1145
|
|
|
1146
|
+
/**
|
|
1147
|
+
* `icon` at 55% inside a circle or rounded square: outlined (`X.circle`), or
|
|
1148
|
+
* filled with the glyph knocked out of it (`X.circle.fill`), as SF Symbols
|
|
1149
|
+
* draw their enclosed forms.
|
|
1150
|
+
*/
|
|
1151
|
+
function enclosed(icon: IconNode, enclosure: 'circle' | 'square', fill: boolean): SymbolGlyph {
|
|
1152
|
+
const outer: IconNode[number] =
|
|
1153
|
+
enclosure === 'circle' ? ['circle', { cx: '12', cy: '12', r: '10' }] : ['rect', { x: '3', y: '3', width: '18', height: '18', rx: '4' }];
|
|
1154
|
+
// Scaled about the center; the stroke is widened to keep its weight.
|
|
1155
|
+
const inner = icon.map(([tag, attrs]) => [tag, { ...attrs, transform: 'translate(5.4 5.4) scale(0.55)', 'stroke-width': '3.3' }] as IconNode[number]);
|
|
1156
|
+
return { icon: [outer, ...inner], fill: fill ? 'knockout' : 'outline' };
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* The name of the filled form of `systemName` when the table can draw one
|
|
1161
|
+
* (`house` → `house.fill`), else the name itself: a tab bar shows its items'
|
|
1162
|
+
* symbols filled, as iOS does (`symbolVariants` `.fill`).
|
|
1163
|
+
*/
|
|
1164
|
+
export function filledSymbolName(systemName: string): string {
|
|
1165
|
+
if (systemName.split('.').includes('fill')) return systemName;
|
|
1166
|
+
const filled = `${systemName}.fill`;
|
|
1167
|
+
const glyph = lookupSymbol(filled);
|
|
1168
|
+
return glyph && glyph.fill !== 'outline' ? filled : systemName;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1087
1171
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
1088
1172
|
|
|
1089
1173
|
/**
|
|
@@ -1092,15 +1176,18 @@ const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
|
1092
1176
|
* with its triangle), else the first shape.
|
|
1093
1177
|
*/
|
|
1094
1178
|
export function outerShapeIndex(icon: IconNode): number {
|
|
1095
|
-
const index = icon
|
|
1179
|
+
const index = enclosingShapeIndex(icon);
|
|
1180
|
+
return index >= 0 ? index : 0;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/** The index of the circle or rectangle enclosing `icon`'s other marks, or -1. */
|
|
1184
|
+
function enclosingShapeIndex(icon: IconNode): number {
|
|
1185
|
+
return icon.findIndex(([tag, attrs]) => {
|
|
1096
1186
|
const n = (key: string): number => Number(attrs[key] ?? 0);
|
|
1097
1187
|
return (tag === 'circle' && n('r') >= 9) || (tag === 'rect' && n('width') >= 16 && n('height') >= 12);
|
|
1098
1188
|
});
|
|
1099
|
-
return index >= 0 ? index : 0;
|
|
1100
1189
|
}
|
|
1101
1190
|
|
|
1102
|
-
/** Color used for inner marks of `knockout` glyphs; styles.css sets the variable. */
|
|
1103
|
-
const KNOCKOUT_COLOR = 'var(--sb-icon-knockout, var(--sb-color-system-background, #fff))';
|
|
1104
1191
|
|
|
1105
1192
|
/**
|
|
1106
1193
|
* Build the `<svg>` for a symbol. Returns `null` for unknown names; the
|
package/src/webAuth.ts
CHANGED
|
@@ -215,7 +215,7 @@ function promptToOpen(url: string, open: (url: string) => PopupLike | null): Pro
|
|
|
215
215
|
element.dataset['action'] = action;
|
|
216
216
|
element.style.cssText =
|
|
217
217
|
'flex:1;padding:10px 14px;border-radius:10px;border:0;font:inherit;cursor:pointer;' +
|
|
218
|
-
(primary ? 'background:#
|
|
218
|
+
(primary ? 'background:#0088ff;color:#fff;font-weight:600;' : 'background:rgba(120,120,128,0.2);color:inherit;');
|
|
219
219
|
return element;
|
|
220
220
|
};
|
|
221
221
|
const cancel = button('Cancel', 'cancel', false);
|