@swiftbrowser/web 0.5.3 → 0.5.5
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/chartDom.ts +2 -2
- package/src/layout/engine.ts +19 -4
- package/src/protocol.ts +3 -2
- package/src/renderer.ts +6 -1
- package/src/styles.css +46 -16
- package/src/styles.ts +17 -2
- package/src/symbols.ts +7 -0
package/package.json
CHANGED
package/src/chartDom.ts
CHANGED
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
type GradientStop,
|
|
46
46
|
type ShapeStyle,
|
|
47
47
|
} from './protocol';
|
|
48
|
-
import {
|
|
48
|
+
import { colorToCSS, materialAlpha, styleToCSS } from './styles';
|
|
49
49
|
|
|
50
50
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
51
51
|
const LINE_WIDTH_DEFAULT = 2;
|
|
@@ -330,7 +330,7 @@ export function paintFor(doc: Document, defs: SVGDefsElement, style: ShapeStyle
|
|
|
330
330
|
if (isColor(style)) return colorToCSS(style);
|
|
331
331
|
if (isHierarchicalStyle(style)) return styleToCSS(style, 'paint').value;
|
|
332
332
|
if (isMaterialStyle(style)) {
|
|
333
|
-
const alpha = (
|
|
333
|
+
const alpha = materialAlpha(style.material) * clamp01(style.opacity ?? 1);
|
|
334
334
|
return `rgba(${MATERIAL_GRAY}, ${r2(alpha * 0.5)})`;
|
|
335
335
|
}
|
|
336
336
|
const opacity = clamp01(style.opacity ?? 1);
|
package/src/layout/engine.ts
CHANGED
|
@@ -252,6 +252,11 @@ const NAV_GLASS_SIZE = 44;
|
|
|
252
252
|
const NAV_GLASS_PAD_H = 12;
|
|
253
253
|
const NAV_GROUP_SPACING = 8;
|
|
254
254
|
|
|
255
|
+
/** Whether any text is laid out in an item (a bottom bar pads text items as text). */
|
|
256
|
+
function hasText(item: LayoutNode): boolean {
|
|
257
|
+
return item.kind === 'text' || item.children.some(hasText);
|
|
258
|
+
}
|
|
259
|
+
|
|
255
260
|
/** A bar item that is only a symbol (an `image`, or a button, menu or link around one). */
|
|
256
261
|
function isSymbolItem(item: LayoutNode): boolean {
|
|
257
262
|
const inner = unwrapStyled(item);
|
|
@@ -268,11 +273,16 @@ function navSlot(item: LayoutNode, width: number): number {
|
|
|
268
273
|
// bottom toolbar's items float on Liquid Glass in a 48 row 28 above the
|
|
269
274
|
// screen bottom (as the search field does) and 28 in from the sides.
|
|
270
275
|
// Consecutive items share a capsule: an icon takes a 50 slot, text its width
|
|
271
|
-
// and 18 either side, 6 between slots; a lone icon is a 48 circle.
|
|
276
|
+
// and 18 either side, 6 between slots; a lone icon is a 48 circle. An item
|
|
277
|
+
// with no text (symbols, a symbol and a chevron, a custom view) is an icon
|
|
278
|
+
// at any width: at least 50, else its width and 8 either side, so a little
|
|
279
|
+
// more width gives it a little more room (not text's 18 a side). Alone in its
|
|
280
|
+
// group it is a circle only if it fits an icon's slot; wider, a capsule.
|
|
272
281
|
const BOTTOM_GLASS_HEIGHT = 48;
|
|
273
282
|
const BOTTOM_GLASS_MARGIN_H = 28;
|
|
274
283
|
const BOTTOM_ICON_SLOT = 50;
|
|
275
284
|
const BOTTOM_TEXT_PAD_H = 18;
|
|
285
|
+
const BOTTOM_ICON_PAD_H = 8;
|
|
276
286
|
const BOTTOM_ITEM_GAP = 6;
|
|
277
287
|
/** A fixed `ToolbarSpacer` between two bottom groups. */
|
|
278
288
|
const BOTTOM_FIXED_SPACER = 8;
|
|
@@ -1715,7 +1725,7 @@ export class LayoutPass {
|
|
|
1715
1725
|
|
|
1716
1726
|
private buttonIsBordered(node: LayoutNode): boolean {
|
|
1717
1727
|
const style = str(node.props['style']);
|
|
1718
|
-
return style === 'bordered' || style === 'borderedProminent';
|
|
1728
|
+
return style === 'bordered' || style === 'borderedProminent' || style === 'glass';
|
|
1719
1729
|
}
|
|
1720
1730
|
|
|
1721
1731
|
private measureButton(node: LayoutNode, ctx: Context, p: Proposal): Size {
|
|
@@ -3419,7 +3429,8 @@ export class LayoutPass {
|
|
|
3419
3429
|
const close = (): void => {
|
|
3420
3430
|
if (open.length === 0) return;
|
|
3421
3431
|
const icon = (item: Item): boolean => item.size.width < NAV_GLASS_SIZE;
|
|
3422
|
-
|
|
3432
|
+
// a lone icon is a circle; a wider one (a symbol and a chevron) keeps its capsule
|
|
3433
|
+
const lone = open.length === 1 && icon(open[0] as Item) && (open[0] as Item).slot <= BOTTOM_ICON_SLOT;
|
|
3423
3434
|
if (lone) (open[0] as Item).slot = BOTTOM_GLASS_HEIGHT;
|
|
3424
3435
|
const slots = open.reduce((sum, item) => sum + item.slot, 0);
|
|
3425
3436
|
tokens.push({ kind: 'group', items: open, width: slots + (open.length - 1) * BOTTOM_ITEM_GAP });
|
|
@@ -3435,7 +3446,11 @@ export class LayoutPass {
|
|
|
3435
3446
|
const itemCtx = this.childContext(toolbar, cctx);
|
|
3436
3447
|
for (const node of toolbar.children) {
|
|
3437
3448
|
const size = this.measure(node, itemCtx, { width: null, height: null });
|
|
3438
|
-
const slot =
|
|
3449
|
+
const slot = !hasText(node)
|
|
3450
|
+
? Math.max(BOTTOM_ICON_SLOT, size.width + 2 * BOTTOM_ICON_PAD_H)
|
|
3451
|
+
: size.width < NAV_GLASS_SIZE
|
|
3452
|
+
? BOTTOM_ICON_SLOT
|
|
3453
|
+
: size.width + 2 * BOTTOM_TEXT_PAD_H;
|
|
3439
3454
|
open.push({ node, toolbar, size, slot });
|
|
3440
3455
|
}
|
|
3441
3456
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -156,7 +156,8 @@ export interface ColorGradientStyle extends StyleOpacity {
|
|
|
156
156
|
colorGradient: Color;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
/** `glass` / `glassClear`: iOS 26 Liquid Glass (`.glassEffect`), the bar buttons' fill with the glass rim. */
|
|
160
|
+
export type MaterialName = 'ultraThin' | 'thin' | 'regular' | 'thick' | 'ultraThick' | 'glass' | 'glassClear';
|
|
160
161
|
/** A translucent material: the system background at some opacity over a blurred backdrop. */
|
|
161
162
|
export interface MaterialStyle extends StyleOpacity {
|
|
162
163
|
material: MaterialName;
|
|
@@ -463,7 +464,7 @@ export interface HStackProps {
|
|
|
463
464
|
export interface SpacerProps {
|
|
464
465
|
minLength: number | null;
|
|
465
466
|
}
|
|
466
|
-
export type ButtonStyle = 'automatic' | 'plain' | 'bordered' | 'borderedProminent';
|
|
467
|
+
export type ButtonStyle = 'automatic' | 'plain' | 'bordered' | 'borderedProminent' | 'glass';
|
|
467
468
|
/** Phase 8: `cancel` is the role an `alert` draws apart (bold, or in its own card). */
|
|
468
469
|
export type ButtonRole = 'destructive' | 'cancel' | null;
|
|
469
470
|
/**
|
package/src/renderer.ts
CHANGED
|
@@ -583,7 +583,10 @@ export class Renderer {
|
|
|
583
583
|
const active = document.activeElement;
|
|
584
584
|
for (const input of this.root.querySelectorAll<HTMLElement>('.sb-textfield[data-sb-focus], .sb-texteditor[data-sb-focus]')) {
|
|
585
585
|
const wanted = input.dataset['sbFocus'] === 'true';
|
|
586
|
-
|
|
586
|
+
// `preventScroll`: a field in a sheet still sliding in sits below the
|
|
587
|
+
// screen, and the browser would scroll the whole screen to reveal it,
|
|
588
|
+
// blanking the page until something scrolls it back.
|
|
589
|
+
if (wanted && active !== input) input.focus({ preventScroll: true });
|
|
587
590
|
else if (!wanted && active === input) input.blur();
|
|
588
591
|
}
|
|
589
592
|
}
|
|
@@ -3223,6 +3226,8 @@ function applyPaint(el: HTMLElement, paint: PaintCSS): void {
|
|
|
3223
3226
|
s.setProperty('backdrop-filter', paint.backdropFilter);
|
|
3224
3227
|
s.setProperty('-webkit-backdrop-filter', paint.backdropFilter);
|
|
3225
3228
|
}
|
|
3229
|
+
// Liquid Glass gets the rim (styles.css `[data-sb-glass]`).
|
|
3230
|
+
el.toggleAttribute('data-sb-glass', paint.glass === true);
|
|
3226
3231
|
}
|
|
3227
3232
|
|
|
3228
3233
|
// Stacks, spacers and zstacks are pure layout: the engine reads their props
|
package/src/styles.css
CHANGED
|
@@ -108,7 +108,10 @@
|
|
|
108
108
|
--sb-glass-selected: rgba(120, 120, 128, 0.16);
|
|
109
109
|
--sb-color-segment-track: rgba(118, 118, 128, 0.12);
|
|
110
110
|
--sb-color-segment-selected: #ffffff;
|
|
111
|
-
|
|
111
|
+
/* Menus, floating sheets and alerts are Liquid Glass too: translucent
|
|
112
|
+
over a heavier backdrop blur, with the glass rim. */
|
|
113
|
+
--sb-color-menu-background: rgba(250, 250, 250, 0.82);
|
|
114
|
+
--sb-sheet-glass: rgba(255, 255, 255, 0.55);
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
.device[data-theme="dark"] {
|
|
@@ -167,7 +170,9 @@
|
|
|
167
170
|
--sb-color-tabbar-background: rgba(29, 29, 29, 0.94);
|
|
168
171
|
--sb-color-segment-track: rgba(118, 118, 128, 0.24);
|
|
169
172
|
--sb-color-segment-selected: #5a5a5f;
|
|
170
|
-
--sb-color-menu-background: rgba(37, 37, 37, 0.
|
|
173
|
+
--sb-color-menu-background: rgba(37, 37, 37, 0.82);
|
|
174
|
+
/* The iPhone's dark floating sheet reads 42 over black: dark gray glass. */
|
|
175
|
+
--sb-sheet-glass: rgba(60, 60, 60, 0.7);
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
/* ---------------------------------------------------------------------------
|
|
@@ -500,14 +505,30 @@ html[data-sb-mode="device"] .device-screen {
|
|
|
500
505
|
color: #ffffff;
|
|
501
506
|
}
|
|
502
507
|
|
|
508
|
+
/* `.buttonStyle(.glass)`: a Liquid Glass capsule (the bar buttons' fill,
|
|
509
|
+
blur and rim) in the tint. */
|
|
510
|
+
.sb-button[data-sb-style="glass"] {
|
|
511
|
+
border-radius: 9999px;
|
|
512
|
+
background: var(--sb-glass-background);
|
|
513
|
+
box-shadow: var(--sb-glass-shadow);
|
|
514
|
+
backdrop-filter: blur(12px) saturate(180%);
|
|
515
|
+
-webkit-backdrop-filter: blur(12px) saturate(180%);
|
|
516
|
+
color: var(--sb-button-color, var(--sb-tint, var(--sb-color-accent)));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/* `.glassEffect`: a shape painted with the `glass` material (renderer). */
|
|
520
|
+
[data-sb-glass] {
|
|
521
|
+
box-shadow: var(--sb-glass-shadow);
|
|
522
|
+
}
|
|
523
|
+
|
|
503
524
|
/* iOS 26 draws a disabled bordered button gray rather than faded
|
|
504
525
|
(`.disabled` marks the element around the button and dims it inline; the
|
|
505
526
|
renderer keeps the undimmed opacity in `--sb-undimmed-opacity`). */
|
|
506
|
-
[data-sb-disabled]:has(> .sb-button:is([data-sb-style="bordered"], [data-sb-style="borderedProminent"])) {
|
|
527
|
+
[data-sb-disabled]:has(> .sb-button:is([data-sb-style="bordered"], [data-sb-style="borderedProminent"], [data-sb-style="glass"])) {
|
|
507
528
|
opacity: var(--sb-undimmed-opacity, 1) !important;
|
|
508
529
|
}
|
|
509
530
|
|
|
510
|
-
[data-sb-disabled] > .sb-button:is([data-sb-style="bordered"], [data-sb-style="borderedProminent"]) {
|
|
531
|
+
[data-sb-disabled] > .sb-button:is([data-sb-style="bordered"], [data-sb-style="borderedProminent"], [data-sb-style="glass"]) {
|
|
511
532
|
background: var(--sb-color-secondary-fill);
|
|
512
533
|
color: var(--sb-color-tertiary-label);
|
|
513
534
|
}
|
|
@@ -1386,9 +1407,10 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
1386
1407
|
rounded all around, over a lighter dim. */
|
|
1387
1408
|
.sb-sheet[data-sb-floating] > .sb-sheet-card {
|
|
1388
1409
|
border-radius: 38px;
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1410
|
+
/* Liquid Glass: the content behind shows as soft blobs under a heavy blur. */
|
|
1411
|
+
background: var(--sb-sheet-glass);
|
|
1412
|
+
backdrop-filter: blur(40px) saturate(180%);
|
|
1413
|
+
-webkit-backdrop-filter: blur(40px) saturate(180%);
|
|
1392
1414
|
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.18);
|
|
1393
1415
|
}
|
|
1394
1416
|
|
|
@@ -1832,11 +1854,9 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
1832
1854
|
z-index: 6;
|
|
1833
1855
|
border-radius: 13px;
|
|
1834
1856
|
background: var(--sb-color-menu-background);
|
|
1835
|
-
backdrop-filter: blur(
|
|
1836
|
-
-webkit-backdrop-filter: blur(
|
|
1837
|
-
box-shadow:
|
|
1838
|
-
0 0 0 0.5px rgba(0, 0, 0, 0.08),
|
|
1839
|
-
0 10px 40px rgba(0, 0, 0, 0.22);
|
|
1857
|
+
backdrop-filter: blur(40px) saturate(180%);
|
|
1858
|
+
-webkit-backdrop-filter: blur(40px) saturate(180%);
|
|
1859
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.22);
|
|
1840
1860
|
overflow: hidden;
|
|
1841
1861
|
transform-origin: 50% 0;
|
|
1842
1862
|
animation: sb-menu-in 0.18s cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
|
@@ -2211,7 +2231,9 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2211
2231
|
dimmest at the top-right and bottom-left). A gradient-filled pseudo-element
|
|
2212
2232
|
the size of the surface, masked to its outer 1px (the content box cut out
|
|
2213
2233
|
of the padding box); it takes no input and sits above the surface's items,
|
|
2214
|
-
which never reach the edge.
|
|
2234
|
+
which never reach the edge. The surfaces: bar buttons and capsules, the
|
|
2235
|
+
tab bar, the search field, the sidebar toggle, menus, floating sheets,
|
|
2236
|
+
alert and dialog cards, `.glass` buttons and `.glassEffect` shapes. */
|
|
2215
2237
|
.sb-navbar-back::after,
|
|
2216
2238
|
.sb-navbar-more::after,
|
|
2217
2239
|
.sb-tabbar::after,
|
|
@@ -2219,7 +2241,14 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2219
2241
|
.sb-toolbar[data-sb-placement="trailing"]::after,
|
|
2220
2242
|
.sb-bottom-glass::after,
|
|
2221
2243
|
.sb-searchfield::after,
|
|
2222
|
-
.sb-navsplit-toggle::after
|
|
2244
|
+
.sb-navsplit-toggle::after,
|
|
2245
|
+
.sb-picker-menu::after,
|
|
2246
|
+
.sb-picker-menu-exit::after,
|
|
2247
|
+
.sb-sheet[data-sb-floating] > .sb-sheet-card::after,
|
|
2248
|
+
.sb-alert-card::after,
|
|
2249
|
+
.sb-alert-cancel-card::after,
|
|
2250
|
+
.sb-button[data-sb-style="glass"]::after,
|
|
2251
|
+
[data-sb-glass]::after {
|
|
2223
2252
|
content: "";
|
|
2224
2253
|
position: absolute;
|
|
2225
2254
|
inset: 0;
|
|
@@ -3139,8 +3168,9 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
3139
3168
|
box-sizing: border-box;
|
|
3140
3169
|
border-radius: 14px;
|
|
3141
3170
|
background: color-mix(in srgb, var(--sb-color-secondary-system-background) 85%, transparent);
|
|
3142
|
-
|
|
3143
|
-
backdrop-filter: blur(
|
|
3171
|
+
/* Liquid Glass: a heavy blur (what is behind an iPhone's alert is a formless glow). */
|
|
3172
|
+
-webkit-backdrop-filter: blur(60px) saturate(180%);
|
|
3173
|
+
backdrop-filter: blur(60px) saturate(180%);
|
|
3144
3174
|
overflow: hidden;
|
|
3145
3175
|
}
|
|
3146
3176
|
|
package/src/styles.ts
CHANGED
|
@@ -106,6 +106,8 @@ export type PaintRole = 'text' | 'paint';
|
|
|
106
106
|
export interface PaintCSS {
|
|
107
107
|
/** `color` for a flat style, `gradient` for a CSS `<image>`, `material` for a translucent fill. */
|
|
108
108
|
kind: 'color' | 'gradient' | 'material';
|
|
109
|
+
/** Liquid Glass: the element also gets the glass rim (`data-sb-glass`). */
|
|
110
|
+
glass?: boolean;
|
|
109
111
|
/**
|
|
110
112
|
* What to paint with: a `<color>` (`color`, `material`) or an `<image>`
|
|
111
113
|
* (`gradient`). Valid as a `background` shorthand value either way.
|
|
@@ -137,7 +139,7 @@ const HIERARCHICAL_PAINT: Record<1 | 2 | 3 | 4, string> = {
|
|
|
137
139
|
};
|
|
138
140
|
|
|
139
141
|
/** Opacity of the system background each material shows over the blurred backdrop. */
|
|
140
|
-
export const MATERIAL_ALPHA: Record<MaterialName, number> = {
|
|
142
|
+
export const MATERIAL_ALPHA: Record<Exclude<MaterialName, 'glass' | 'glassClear'>, number> = {
|
|
141
143
|
ultraThin: 0.55,
|
|
142
144
|
thin: 0.7,
|
|
143
145
|
regular: 0.82,
|
|
@@ -145,6 +147,15 @@ export const MATERIAL_ALPHA: Record<MaterialName, number> = {
|
|
|
145
147
|
ultraThick: 0.96,
|
|
146
148
|
};
|
|
147
149
|
export const MATERIAL_BLUR = 'blur(20px)';
|
|
150
|
+
/** Liquid Glass (`glass`, `glassClear`): the bar buttons' fill (`clear` at half of it) and blur; the renderer adds the rim. */
|
|
151
|
+
export const GLASS_BLUR = 'blur(20px) saturate(180%)';
|
|
152
|
+
|
|
153
|
+
/** The opacity a material's fill has (glass: the light fill's 70%, clear glass half of it). */
|
|
154
|
+
export function materialAlpha(name: MaterialName): number {
|
|
155
|
+
if (name === 'glass') return 0.7;
|
|
156
|
+
if (name === 'glassClear') return 0.35;
|
|
157
|
+
return MATERIAL_ALPHA[name] ?? MATERIAL_ALPHA.regular;
|
|
158
|
+
}
|
|
148
159
|
|
|
149
160
|
/**
|
|
150
161
|
* Resolve a ShapeStyle for a role. Unknown shapes resolve to magenta so they
|
|
@@ -180,8 +191,12 @@ export function styleToCSS(style: ShapeStyle, role: PaintRole): PaintCSS {
|
|
|
180
191
|
const dark = withOpacity(`color-mix(in srgb, ${base}, black 12%)`, opacity);
|
|
181
192
|
return { kind: 'gradient', value: `linear-gradient(to bottom, ${light}, ${dark})`, solid: withOpacity(base, opacity) };
|
|
182
193
|
}
|
|
194
|
+
if (isMaterialStyle(style) && (style.material === 'glass' || style.material === 'glassClear')) {
|
|
195
|
+
const value = style.material === 'glass' ? 'var(--sb-glass-background)' : 'color-mix(in srgb, var(--sb-glass-background) 50%, transparent)';
|
|
196
|
+
return { kind: 'material', value, solid: value, backdropFilter: GLASS_BLUR, glass: true };
|
|
197
|
+
}
|
|
183
198
|
if (isMaterialStyle(style)) {
|
|
184
|
-
const alpha = MATERIAL_ALPHA[style.material] ?? MATERIAL_ALPHA.regular;
|
|
199
|
+
const alpha = MATERIAL_ALPHA[style.material as keyof typeof MATERIAL_ALPHA] ?? MATERIAL_ALPHA.regular;
|
|
185
200
|
const value = `rgba(var(--sb-color-material-base), ${round(alpha * (opacity == null || !Number.isFinite(opacity) ? 1 : clamp01(opacity)))})`;
|
|
186
201
|
return { kind: 'material', value, solid: value, backdropFilter: MATERIAL_BLUR };
|
|
187
202
|
}
|
package/src/symbols.ts
CHANGED
|
@@ -141,6 +141,7 @@ import {
|
|
|
141
141
|
Crown,
|
|
142
142
|
Database,
|
|
143
143
|
Delete,
|
|
144
|
+
Eraser,
|
|
144
145
|
Diamond,
|
|
145
146
|
Dices,
|
|
146
147
|
Dog,
|
|
@@ -251,6 +252,7 @@ import {
|
|
|
251
252
|
MonitorPlay,
|
|
252
253
|
Moon,
|
|
253
254
|
Mountain,
|
|
255
|
+
MousePointer2,
|
|
254
256
|
Music,
|
|
255
257
|
Navigation2,
|
|
256
258
|
Network,
|
|
@@ -343,6 +345,7 @@ import {
|
|
|
343
345
|
SquareMinus,
|
|
344
346
|
SquareParking,
|
|
345
347
|
SquarePen,
|
|
348
|
+
SquareDashed,
|
|
346
349
|
SquarePlay,
|
|
347
350
|
SquarePlus,
|
|
348
351
|
SquareUser,
|
|
@@ -558,6 +561,10 @@ export const SF_SYMBOLS: Readonly<Record<string, SymbolGlyph>> = {
|
|
|
558
561
|
trash: outline(Trash),
|
|
559
562
|
'trash.fill': outline(Trash),
|
|
560
563
|
pencil: outline(Pencil),
|
|
564
|
+
// The editing tools of a drawing app: the pointer (macOS's arrow), a marquee, the eraser.
|
|
565
|
+
cursorarrow: outline(MousePointer2),
|
|
566
|
+
'square.dashed': outline(SquareDashed),
|
|
567
|
+
eraser: outline(Eraser),
|
|
561
568
|
'square.and.pencil': outline(SquarePen),
|
|
562
569
|
'doc.on.doc': outline(Copy),
|
|
563
570
|
'doc.on.clipboard': outline(Clipboard),
|