@swiftbrowser/web 0.2.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/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(Type),
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': outline(User),
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': outline(Image),
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': outline(Package),
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': outline(Trophy),
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.findIndex(([tag, attrs]) => {
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:#007aff;color:#fff;font-weight:600;' : 'background:rgba(120,120,128,0.2);color:inherit;');
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);