@motion-proto/live-tokens 0.57.0 → 0.59.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/.claude/skills/live-tokens-create-component/SKILL.md +13 -2
- package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +202 -0
- package/.claude/skills/live-tokens-pick-component/SKILL.md +1 -1
- package/CHANGELOG.md +117 -0
- package/dist-plugin/generateColorsAndType/index.cjs +4 -3
- package/dist-plugin/generateColorsAndType/index.js +4 -3
- package/package.json +3 -3
- package/src/editor/component-editor/ImageLightboxEditor.svelte +11 -2
- package/src/editor/core/sketch/maskField.ts +10 -6
- package/src/editor/core/sketch/sketchLayer.ts +107 -29
- package/src/editor/core/sketch/sketchPresets.ts +34 -17
- package/src/editor/core/themes/parsers/shadow.ts +12 -4
- package/src/editor/docs/content/editing-tokens.md +2 -2
- package/src/editor/docs/content/sketch-mode.md +106 -12
- package/src/editor/docs/content.generated.ts +2 -2
- package/src/editor/ui/ColorEditPanel.svelte +13 -9
- package/src/editor/ui/EditorViewSwitcher.svelte +20 -20
- package/src/editor/ui/PaletteEditor.svelte +11 -3
- package/src/editor/ui/UIInlineEditActions.svelte +97 -0
- package/src/editor/ui/sketch/SketchTab.svelte +6 -5
- package/src/live-tokens/data/colors-and-type/autumn.json +5 -5
- package/src/live-tokens/data/colors-and-type/default.json +5 -5
- package/src/live-tokens/data/colors-and-type/halloween.json +5 -5
- package/src/live-tokens/data/colors-and-type/midnight-study.json +5 -5
- package/src/live-tokens/data/colors-and-type/ocean.json +5 -5
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +5 -5
- package/src/live-tokens/data/colors-and-type/{sketches.json → sketchy.json} +6 -6
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +5 -5
- package/src/live-tokens/data/colors-and-type/sunset.json +5 -5
- package/src/live-tokens/data/themes/autumn.json +5 -5
- package/src/live-tokens/data/themes/halloween.json +5 -5
- package/src/live-tokens/data/themes/midnight-study.json +5 -5
- package/src/live-tokens/data/themes/ocean.json +5 -5
- package/src/live-tokens/data/themes/royal-velvet.json +5 -5
- package/src/live-tokens/data/themes/{sketches.json → sketchy.json} +32 -32
- package/src/live-tokens/data/themes/spring-meadow.json +5 -5
- package/src/live-tokens/data/themes/sunset.json +5 -5
- package/src/live-tokens/data/tokens.generated.css +5 -5
- package/src/system/components/Card.svelte +12 -1
- package/src/system/components/ImageLightbox.svelte +26 -2
- package/src/system/components/SectionDivider.svelte +4 -0
- package/src/system/styles/tokens.css +5 -5
|
@@ -26,6 +26,12 @@ const ID = 'lt-sketch';
|
|
|
26
26
|
identical twins, which is the single biggest tell that it is not hand drawn. */
|
|
27
27
|
const SEEDS = [0, 1, 2, 3, 4];
|
|
28
28
|
|
|
29
|
+
/** The soft glyph bank, as a share of the icon travel. Travel is stated in px
|
|
30
|
+
against a glyph whose size the layer cannot know, so the dial that suits a
|
|
31
|
+
card's worth of artwork tears a 16px icon apart. An element that cannot take
|
|
32
|
+
the full amount names this bank instead of going crisp. */
|
|
33
|
+
const ICON_SOFT = 0.35;
|
|
34
|
+
|
|
29
35
|
const HATCH_ANGLE = 45;
|
|
30
36
|
/** The second set of stripes leans and spaces itself a little off the first.
|
|
31
37
|
Half a pixel of pitch against 7 puts the beat about 90px apart, and the two
|
|
@@ -221,10 +227,19 @@ const PART_SPECS: readonly PartSpec[] = [
|
|
|
221
227
|
// outline: a line does not get drawn around.
|
|
222
228
|
{ sel: '.sd-hairline', fill: 'var(--_divider-hairline-color)', strokeless: true },
|
|
223
229
|
|
|
224
|
-
// Consumer opt-in. A page element
|
|
225
|
-
// token-driven surface sets --sketch-fill / --sketch-stroke itself
|
|
226
|
-
// carries
|
|
230
|
+
// Consumer opt-in. A page element or a consumer-authored component that
|
|
231
|
+
// paints a token-driven surface sets --sketch-fill / --sketch-stroke itself
|
|
232
|
+
// and carries one of these; the layer then treats it like any other part.
|
|
233
|
+
// They name no colour, so no rule is emitted for them and whatever the
|
|
234
|
+
// element declared survives.
|
|
235
|
+
//
|
|
236
|
+
// Which one to carry is a question of size, not of kind. The shipped parts
|
|
237
|
+
// are sorted the same way below: a container tilts less than the type inside
|
|
238
|
+
// it can tolerate, a chip is smaller than one blob of the fill mask. A part
|
|
239
|
+
// that is neither takes `.sketch-surface` and the middle treatment.
|
|
227
240
|
{ sel: '.sketch-surface' },
|
|
241
|
+
{ sel: '.sketch-container' },
|
|
242
|
+
{ sel: '.sketch-chip' },
|
|
228
243
|
{ sel: '.sketch-rule', strokeless: true },
|
|
229
244
|
];
|
|
230
245
|
|
|
@@ -438,7 +453,8 @@ export function buildDefsMarkup(s: SketchSettings): string {
|
|
|
438
453
|
// Icons are glyphs a few tens of pixels across, read at a glance, with no
|
|
439
454
|
// redundancy to lose. A high-frequency field at a small amplitude wobbles
|
|
440
455
|
// the outline without pulling a stroke off the shape it belongs to.
|
|
441
|
-
displace(`${ID}-icon-${seed}`, base / s.iconWavelength, seed + 63, s.iconTravel, 40, 0, 0)
|
|
456
|
+
displace(`${ID}-icon-${seed}`, base / s.iconWavelength, seed + 63, s.iconTravel, 40, 0, 0) +
|
|
457
|
+
displace(`${ID}-icon-soft-${seed}`, base / s.iconWavelength, seed + 63, s.iconTravel * ICON_SOFT, 40, 0, 0),
|
|
442
458
|
).join('');
|
|
443
459
|
|
|
444
460
|
// Ink pooling. Blur spreads the stroke, then a steep alpha ramp re-sharpens
|
|
@@ -651,6 +667,10 @@ export function buildStylesheet(s: SketchSettings): string {
|
|
|
651
667
|
`--sketch-stroke-filter:url(#${ID}-stroke-0);` +
|
|
652
668
|
`--sketch-mask:${s.maskOn || s.iconMaskOn ? buildMaskUri(s) : 'none'};` +
|
|
653
669
|
`--sketch-mask-tile:${MASK_TILE}px;` +
|
|
670
|
+
`--sketch-icon-mask-tile:${Math.round(s.iconMaskScale * 100)}%;` +
|
|
671
|
+
// Named here rather than on the icons themselves, so an ancestor asking
|
|
672
|
+
// for the soft bank resolves it against a value it can actually see.
|
|
673
|
+
`--sketch-icon-soft:url(#${ID}-icon-soft-0);` +
|
|
654
674
|
`--sketch-jit-x-base:${s.jitterX}px;` +
|
|
655
675
|
`--sketch-jit-y-base:${s.jitterY}px;` +
|
|
656
676
|
`--sketch-jit-rot-base:${s.jitterRot}deg;` +
|
|
@@ -668,15 +688,14 @@ export function buildStylesheet(s: SketchSettings): string {
|
|
|
668
688
|
* that element's own origin and sampled at a different offset per instance,
|
|
669
689
|
* so two parts the same size are not blotched alike.
|
|
670
690
|
*
|
|
671
|
-
* `mask-clip: no-clip`
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
* which is what ink that is not there would do.
|
|
691
|
+
* `mask-clip: no-clip` asks for the painting area not to be restricted, which
|
|
692
|
+
* is what a layer the filter has already carried outside its own box needs.
|
|
693
|
+
* Chrome does not honour it — see `bleed` below, which is what actually keeps
|
|
694
|
+
* the drawn edge off the border box.
|
|
676
695
|
*/
|
|
677
|
-
const coverage = (
|
|
696
|
+
const coverage = (size: string, pos: string) =>
|
|
678
697
|
`mask-image:var(--sketch-mask, none);` +
|
|
679
|
-
`mask-size:${
|
|
698
|
+
`mask-size:${size};` +
|
|
680
699
|
`mask-mode:luminance;mask-repeat:repeat;mask-clip:no-clip;` +
|
|
681
700
|
`mask-position:var(${pos}, 0 0);`;
|
|
682
701
|
|
|
@@ -684,11 +703,13 @@ export function buildStylesheet(s: SketchSettings): string {
|
|
|
684
703
|
// than through a redrawn ::before. Body type is deliberately left alone: an
|
|
685
704
|
// icon is a shape and survives a wobble, a paragraph is not.
|
|
686
705
|
//
|
|
687
|
-
// `--sketch-icon-off`
|
|
688
|
-
//
|
|
689
|
-
//
|
|
690
|
-
//
|
|
691
|
-
//
|
|
706
|
+
// `--sketch-icon-off` names what to draw a subtree's glyphs with instead:
|
|
707
|
+
// `none` keeps them crisp, `var(--sketch-icon-soft)` draws them at a fraction
|
|
708
|
+
// of the travel. It inherits, so any chrome that lives in the host document
|
|
709
|
+
// (the overlay bar) sets it once on its own root and every icon under it
|
|
710
|
+
// follows regardless of specificity. `svg` covers inline artwork the same
|
|
711
|
+
// way. The injected filter bank is itself an svg in the body, so it has to be
|
|
712
|
+
// excluded or it filters itself.
|
|
692
713
|
const iconSel = `[class*="fa-"], svg:not([${DEFS_ATTR}])`;
|
|
693
714
|
const iconsOn = s.iconTravel > 0 || s.iconMaskOn;
|
|
694
715
|
const icons = iconsOn
|
|
@@ -697,14 +718,24 @@ export function buildStylesheet(s: SketchSettings): string {
|
|
|
697
718
|
? `filter:var(--sketch-icon-off, var(--sketch-icon-filter, url(#${ID}-icon-0)));`
|
|
698
719
|
: '') +
|
|
699
720
|
// The ink mask reads as coverage on a glyph the way it does on a fill,
|
|
700
|
-
// but the
|
|
701
|
-
//
|
|
702
|
-
//
|
|
703
|
-
|
|
721
|
+
// but the fill states its tile in px and a glyph has no fixed size to
|
|
722
|
+
// state one against: the component tile is hundreds of px across, so a
|
|
723
|
+
// whole icon sampled one flat patch of it and came out either untouched
|
|
724
|
+
// or gone.
|
|
725
|
+
//
|
|
726
|
+
// The glyph is the unit instead. A percentage resolves against the
|
|
727
|
+
// element the mask is laid on, so the tile scales with whatever it
|
|
728
|
+
// covers and the dial reads the same on a 16px icon as on a page-wide
|
|
729
|
+
// drawing. `auto` on the other axis keeps the tile square.
|
|
730
|
+
(s.iconMaskOn
|
|
731
|
+
? coverage('auto var(--sketch-icon-mask-tile)', '--sketch-icon-mask-pos')
|
|
732
|
+
: '') +
|
|
704
733
|
`}` +
|
|
705
734
|
SEEDS.map((seed, i) =>
|
|
706
735
|
`${on} :is(${iconSel}):nth-child(5n + ${i + 1})` +
|
|
707
|
-
`{--sketch-icon-filter:url(#${ID}-icon-${seed})
|
|
736
|
+
`{--sketch-icon-filter:url(#${ID}-icon-${seed});` +
|
|
737
|
+
`--sketch-icon-soft:url(#${ID}-icon-soft-${seed});` +
|
|
738
|
+
`--sketch-icon-mask-pos:${MASK_POS[i]};}`,
|
|
708
739
|
).join('')
|
|
709
740
|
: '';
|
|
710
741
|
|
|
@@ -722,6 +753,46 @@ export function buildStylesheet(s: SketchSettings): string {
|
|
|
722
753
|
? `border-radius:${[1, 2, 3, 4].map(cornerRadius).join(' ')};`
|
|
723
754
|
: 'border-radius:inherit;';
|
|
724
755
|
|
|
756
|
+
/**
|
|
757
|
+
* How far past its own box the fill layer is drawn on.
|
|
758
|
+
*
|
|
759
|
+
* A mask is applied AFTER the filter, and its painting area stops at the
|
|
760
|
+
* border box whatever `mask-clip` says: Chrome accepts `no-clip`, computes it
|
|
761
|
+
* back, and clips anyway. Every pixel the displacement pushed outside the box
|
|
762
|
+
* was erased along a straight rectangle, and because `jitterScale` grows each
|
|
763
|
+
* fill one-sidedly the drawn shape always covered its own box — so what
|
|
764
|
+
* survived was the box itself, ruler-straight with perfectly circular
|
|
765
|
+
* corners, under a stroke that wobbled freely because it carries no mask.
|
|
766
|
+
*
|
|
767
|
+
* The fill is drawn on a box this much larger instead, with the paint held to
|
|
768
|
+
* the middle of it, so the mask's painting area covers everything the pen laid
|
|
769
|
+
* down. Only what the FILTER moves has to fit: the jitter transform runs after
|
|
770
|
+
* the mask and carries the finished layer whole.
|
|
771
|
+
*/
|
|
772
|
+
const bleed = s.maskOn ? Math.ceil(s.cornerTravel + s.fillTravel) + 4 : 0;
|
|
773
|
+
|
|
774
|
+
/** Padding subtracts from a corner, so the bleed added here comes back off at
|
|
775
|
+
the content box and the paint turns exactly where it did before. The
|
|
776
|
+
inherit is spent at this point: a bleeding fill has to state its corners,
|
|
777
|
+
and `--sketch-radius` is what it states them from. */
|
|
778
|
+
const fillCorners = bleed === 0
|
|
779
|
+
? corners
|
|
780
|
+
: `border-radius:${s.cornerSpread > 0
|
|
781
|
+
? [1, 2, 3, 4].map((i) => `calc(${cornerRadius(i)} + ${bleed}px)`).join(' ')
|
|
782
|
+
: `calc(var(--sketch-radius, 0px) + ${bleed}px)`};`;
|
|
783
|
+
|
|
784
|
+
/** Grow the box, hold the paint to the middle of it. `content-box` sizing
|
|
785
|
+
keeps the content area at the element's own size, which a rule two pixels
|
|
786
|
+
tall cannot do under `border-box`; the two `auto`s are for Button, whose
|
|
787
|
+
shimmer ::before states `width: 100%` and would otherwise pin the grown box
|
|
788
|
+
back to the element. `background` is a shorthand and resets both box
|
|
789
|
+
properties, so `bleedPaint` follows every declaration of it. */
|
|
790
|
+
const bleedBox = bleed === 0
|
|
791
|
+
? 'inset:0 !important;'
|
|
792
|
+
: `inset:-${bleed}px !important;padding:${bleed}px;box-sizing:content-box;` +
|
|
793
|
+
`width:auto !important;height:auto !important;`;
|
|
794
|
+
const bleedPaint = bleed === 0 ? '' : 'background-origin:content-box;background-clip:content-box;';
|
|
795
|
+
|
|
725
796
|
const host =
|
|
726
797
|
`${el}{` +
|
|
727
798
|
`--sketch-jit-x:var(--sketch-jit-x-base);` +
|
|
@@ -754,12 +825,17 @@ export function buildStylesheet(s: SketchSettings): string {
|
|
|
754
825
|
// effect is switched on, because `left` animates from -100% to 0.
|
|
755
826
|
const fill =
|
|
756
827
|
`${el}::before{` +
|
|
757
|
-
`content:'';position:absolute
|
|
758
|
-
`z-index:-1;${
|
|
828
|
+
`content:'';position:absolute;${bleedBox}transition:none !important;` +
|
|
829
|
+
`z-index:-1;${fillCorners}` +
|
|
759
830
|
`background:var(--sketch-fill, var(--surface-neutral-lower));` +
|
|
760
|
-
|
|
831
|
+
bleedPaint +
|
|
832
|
+
// A shadow is cast from the border box, and the bleed is not where the
|
|
833
|
+
// drawing is. The five parts that name one (card, dialog, menu, table,
|
|
834
|
+
// tooltip) go without while coverage is on rather than wear a halo the
|
|
835
|
+
// width of the bleed; every other part's token is --shadow-none anyway.
|
|
836
|
+
(bleed === 0 ? 'box-shadow:var(--sketch-shadow, none);' : '') +
|
|
761
837
|
`filter:var(--sketch-fill-filter);` +
|
|
762
|
-
(s.maskOn ? coverage('var(--sketch-mask-tile)', '--sketch-mask-pos') : '') +
|
|
838
|
+
(s.maskOn ? coverage('var(--sketch-mask-tile) var(--sketch-mask-tile)', '--sketch-mask-pos') : '') +
|
|
763
839
|
`transform:translate(` +
|
|
764
840
|
`calc(var(--sketch-jx, 0) * var(--sketch-jit-x, 0px)),` +
|
|
765
841
|
`calc(var(--sketch-jy, 0) * var(--sketch-jit-y, 0px))` +
|
|
@@ -799,6 +875,7 @@ export function buildStylesheet(s: SketchSettings): string {
|
|
|
799
875
|
`${hatchInk('1')} 0 1.5px,` +
|
|
800
876
|
`transparent 1.5px 7px),` +
|
|
801
877
|
`var(--sketch-fill, var(--surface-neutral-lower));` +
|
|
878
|
+
bleedPaint +
|
|
802
879
|
`}`;
|
|
803
880
|
|
|
804
881
|
// A translucent nib. The retrace pass below overlaps this one, so where both
|
|
@@ -890,7 +967,8 @@ export function buildStylesheet(s: SketchSettings): string {
|
|
|
890
967
|
// Large panels tilt less than the type inside them can tolerate; small chips
|
|
891
968
|
// can take more rotation but less travel.
|
|
892
969
|
const perPart =
|
|
893
|
-
`${el}:is(.card, .card-header, .panel, .dialog, .table-wrapper, .sidenavigation
|
|
970
|
+
`${el}:is(.card, .card-header, .panel, .dialog, .table-wrapper, .sidenavigation,` +
|
|
971
|
+
` .sketch-container)` +
|
|
894
972
|
`{--sketch-jit-rot:calc(var(--sketch-jit-rot-base) * 0.3);}` +
|
|
895
973
|
// A rule is one or two pixels tall. The full-size displacement tears it into
|
|
896
974
|
// dashes and a translate that large lifts it clean off its own row, so it
|
|
@@ -904,7 +982,7 @@ export function buildStylesheet(s: SketchSettings): string {
|
|
|
904
982
|
// rather than as a hand.
|
|
905
983
|
`--sketch-corner-spread:0px;` +
|
|
906
984
|
`}` +
|
|
907
|
-
`${el}:is(.badge, .toggle .track){` +
|
|
985
|
+
`${el}:is(.badge, .toggle .track, .sketch-chip){` +
|
|
908
986
|
// A chip is smaller than one blob at the tile the cards read it at, so it
|
|
909
987
|
// lands wholly inside a patch and comes out either untouched or gone.
|
|
910
988
|
// Shrinking the tile puts several blotches across it, which is the same
|
|
@@ -925,8 +1003,8 @@ export function buildStylesheet(s: SketchSettings): string {
|
|
|
925
1003
|
`--sketch-stroke-filter:url(#${ID}-stroke-sm-2);` +
|
|
926
1004
|
`}`;
|
|
927
1005
|
|
|
928
|
-
// `.sketch-
|
|
929
|
-
// whatever the page set.
|
|
1006
|
+
// The `.sketch-*` opt-in classes name their own colours, so they emit no rule
|
|
1007
|
+
// and keep whatever the page set.
|
|
930
1008
|
const colours = PART_SPECS.map((p) => {
|
|
931
1009
|
const f = p.fill ?? (p.stem ? `var(--${p.stem}-surface)` : null);
|
|
932
1010
|
const st = p.stroke ?? (p.stem ? `var(--${p.stem}-border)` : null);
|
|
@@ -109,10 +109,15 @@ export interface SketchSettings {
|
|
|
109
109
|
iconWavelength: number;
|
|
110
110
|
/** Ink coverage over icons. Independent of `maskOn`, which governs fills. */
|
|
111
111
|
iconMaskOn: boolean;
|
|
112
|
-
/** Icon mask tile,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
/** Icon mask tile, as a share of the glyph it covers. A px size cannot be
|
|
113
|
+
right for both a 16px icon and a page-wide illustration: the tile that
|
|
114
|
+
blotches the drawing swallows the icon whole, and one cut for the icon
|
|
115
|
+
is dust on the drawing. Stated against the glyph, the tile scales with
|
|
116
|
+
whatever it is laid on. At 1 every glyph gets one period of the field
|
|
117
|
+
across it; below that the field repeats inside the glyph and the blotches
|
|
118
|
+
get finer; above it a glyph reads part of a blotch and the mask acts as
|
|
119
|
+
an uneven wash over the whole thing. */
|
|
120
|
+
iconMaskScale: number;
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
const base: SketchSettings = {
|
|
@@ -122,11 +127,11 @@ const base: SketchSettings = {
|
|
|
122
127
|
strokeWidth: 1.5, doubleStroke: false, retraceOffset: 1.2, retracePass: 'copy',
|
|
123
128
|
fillStyle: 'solid', hatchInk: 0.4, strokeStyle: 'solid',
|
|
124
129
|
pressure: 0.25, pressureMod: 0.35, pooling: 1.2, strokeInk: 1,
|
|
125
|
-
maskOn: true, maskBlob: 100, maskOutputMin: 0.
|
|
130
|
+
maskOn: true, maskBlob: 100, maskOutputMin: 0.45, maskOutputMax: 1,
|
|
126
131
|
maskOctaves: 2, maskGrain: 'fractal', maskPosterize: 1, maskSoftness: 1.5,
|
|
127
132
|
jitterX: 2.5, jitterY: 2.5, jitterRot: 0.6, jitterScale: 0.035,
|
|
128
133
|
cornerSpread: 10, cornerTravel: 8,
|
|
129
|
-
iconTravel: 1.25, iconWavelength: 0.625, iconMaskOn: true,
|
|
134
|
+
iconTravel: 1.25, iconWavelength: 0.625, iconMaskOn: true, iconMaskScale: 1,
|
|
130
135
|
};
|
|
131
136
|
|
|
132
137
|
export const SKETCH_PRESETS: Record<string, SketchSettings> = {
|
|
@@ -135,7 +140,7 @@ export const SKETCH_PRESETS: Record<string, SketchSettings> = {
|
|
|
135
140
|
blurb: 'Two graphite passes on their own seeds, so the outline disagrees with itself the way a hand coming back round does. Tight grain, little else.',
|
|
136
141
|
fillTravel: 0.75, strokeTravel: 1.25, wobble: 30, roughness: 3, waveform: 1,
|
|
137
142
|
strokeWidth: 1.25, doubleStroke: true, retracePass: 'reseeded', retraceOffset: 1.5, strokeInk: 0.85,
|
|
138
|
-
maskBlob: 40, maskOutputMin: 0.
|
|
143
|
+
maskBlob: 40, maskOutputMin: 0.62, maskOutputMax: 1, maskOctaves: 3, maskPosterize: 1, maskSoftness: 0.6,
|
|
139
144
|
jitterX: 1.5, jitterY: 1.5, jitterRot: 0.35, jitterScale: 0.022,
|
|
140
145
|
cornerSpread: 6, cornerTravel: 4.5,
|
|
141
146
|
pressure: 0.15, pressureMod: 0.3, pooling: 0, iconTravel: 0.75,
|
|
@@ -146,10 +151,10 @@ export const SKETCH_PRESETS: Record<string, SketchSettings> = {
|
|
|
146
151
|
blurb: 'Broad translucent nib gone round twice on the same line, so the overlap darkens and the ink pools where it slows.',
|
|
147
152
|
fillTravel: 2, strokeTravel: 1.5, wobble: 56, waveform: 1.4, borderWavelength: 1.3,
|
|
148
153
|
strokeWidth: 4, doubleStroke: true, retracePass: 'copy', strokeInk: 0.52, retraceOffset: 2.2,
|
|
149
|
-
maskBlob:
|
|
154
|
+
maskBlob: 115, maskOutputMin: 0.21, maskOutputMax: 1, maskOctaves: 3, maskPosterize: 4, maskSoftness: 2.5,
|
|
150
155
|
jitterX: 3, jitterY: 3, jitterRot: 0.8, jitterScale: 0.045,
|
|
151
156
|
cornerSpread: 10, cornerTravel: 8,
|
|
152
|
-
pressure: 0.2, pressureMod: 0.25, pooling: 2, iconTravel: 1.25,
|
|
157
|
+
pressure: 0.2, pressureMod: 0.25, pooling: 2, iconTravel: 1.25, iconMaskScale: 4,
|
|
153
158
|
},
|
|
154
159
|
|
|
155
160
|
whiteboard: {
|
|
@@ -157,11 +162,11 @@ export const SKETCH_PRESETS: Record<string, SketchSettings> = {
|
|
|
157
162
|
blurb: 'The fattest nib on glass. One long smooth undulation, and a veined mask that streaks the fill like a half-wiped board.',
|
|
158
163
|
fillTravel: 2.5, strokeTravel: 2.5, wobble: 90, roughness: 1, waveform: 1, borderWavelength: 1.5,
|
|
159
164
|
strokeWidth: 5.5, doubleStroke: true, retracePass: 'copy', strokeInk: 0.66, retraceOffset: 3,
|
|
160
|
-
maskGrain: 'turbulence', maskBlob: 180, maskOutputMin: 0.
|
|
165
|
+
maskGrain: 'turbulence', maskBlob: 180, maskOutputMin: 0.42, maskOutputMax: 0.9,
|
|
161
166
|
maskOctaves: 1, maskPosterize: 3, maskSoftness: 5,
|
|
162
167
|
jitterX: 4.5, jitterY: 4.5, jitterRot: 1.2, jitterScale: 0.07,
|
|
163
168
|
cornerSpread: 14, cornerTravel: 11,
|
|
164
|
-
pressure: 0.15, pressureMod: 0.15, pooling: 3.5, iconTravel: 1.75,
|
|
169
|
+
pressure: 0.15, pressureMod: 0.15, pooling: 3.5, iconTravel: 1.75, iconMaskScale: 1.5,
|
|
165
170
|
},
|
|
166
171
|
|
|
167
172
|
hatched: {
|
|
@@ -191,22 +196,22 @@ export const SKETCH_PRESETS: Record<string, SketchSettings> = {
|
|
|
191
196
|
blurb: 'Ballpoint in a hurry. Everything loose at once: a square wave sends every edge to full travel, and the second pass lands wherever it lands.',
|
|
192
197
|
fillTravel: 3, strokeTravel: 2.25, wobble: 50, roughness: 3, waveform: 2.5,
|
|
193
198
|
strokeWidth: 2.25, doubleStroke: true, retracePass: 'reseeded', retraceOffset: 4, strokeInk: 1,
|
|
194
|
-
maskBlob: 150, maskOutputMin: 0.
|
|
199
|
+
maskBlob: 150, maskOutputMin: 0.43, maskOutputMax: 0.95, maskOctaves: 2, maskPosterize: 2, maskSoftness: 6.7,
|
|
195
200
|
jitterX: 6, jitterY: 6, jitterRot: 1.8, jitterScale: 0.1,
|
|
196
201
|
cornerSpread: 20, cornerTravel: 17,
|
|
197
|
-
pressure: 0.45, pressureMod: 0.6, pooling: 2.5, iconTravel: 2.25,
|
|
202
|
+
pressure: 0.45, pressureMod: 0.6, pooling: 2.5, iconTravel: 2.25, iconMaskScale: 3.6,
|
|
198
203
|
},
|
|
199
204
|
|
|
200
205
|
dry: {
|
|
201
206
|
...base, label: 'Dry marker',
|
|
202
|
-
blurb: 'Ink that ran out. One scratchy pass that breaks up along its length, over a fill the
|
|
207
|
+
blurb: 'Ink that ran out. One scratchy pass that breaks up along its length, over a fill the mask has worn nearly through in patches.',
|
|
203
208
|
fillTravel: 2.25, strokeTravel: 1.75, wobble: 50, waveform: 2, borderWavelength: 0.5,
|
|
204
209
|
strokeWidth: 3.5, doubleStroke: false, strokeInk: 0.4,
|
|
205
|
-
|
|
206
|
-
maskOctaves: 2, maskPosterize:
|
|
210
|
+
maskBlob: 150, maskOutputMin: 0.15, maskOutputMax: 0.91,
|
|
211
|
+
maskOctaves: 2, maskPosterize: 4, maskSoftness: 1.75,
|
|
207
212
|
jitterX: 5, jitterY: 5, jitterRot: 1.4, jitterScale: 0.08,
|
|
208
213
|
cornerSpread: 16, cornerTravel: 13,
|
|
209
|
-
pressure: 0.4, pressureMod: 0.8, pooling: 1.5, iconTravel: 1.75,
|
|
214
|
+
pressure: 0.4, pressureMod: 0.8, pooling: 1.5, iconTravel: 1.75, iconMaskScale: 3.8,
|
|
210
215
|
},
|
|
211
216
|
};
|
|
212
217
|
|
|
@@ -230,6 +235,7 @@ export function hydrateSketchSettings(raw: unknown): SketchSettings {
|
|
|
230
235
|
carryLevelsToOutput(stored as Record<string, unknown>, out);
|
|
231
236
|
halveSwingDials(stored as Record<string, unknown>, out);
|
|
232
237
|
convertCyclesToWavelength(stored as Record<string, unknown>, out);
|
|
238
|
+
convertIconTileToScale(stored as Record<string, unknown>, out);
|
|
233
239
|
restoreDerivedRetrace(stored as Record<string, unknown>, out);
|
|
234
240
|
return out;
|
|
235
241
|
}
|
|
@@ -275,6 +281,17 @@ function convertCyclesToWavelength(stored: Record<string, unknown>, out: SketchS
|
|
|
275
281
|
if (typeof layers === 'number') out.roughness = Math.min(3, Math.max(1, layers));
|
|
276
282
|
}
|
|
277
283
|
|
|
284
|
+
/** The icon mask tile used to be a px size, which is a unit the glyph it covers
|
|
285
|
+
has no say in: 90px against a 16px icon put a whole glyph inside one patch
|
|
286
|
+
of the field, so it came out either untouched or gone. It is a share of the
|
|
287
|
+
glyph now, and the old default reads as one period across the glyph, which
|
|
288
|
+
is what that size was aiming at. */
|
|
289
|
+
function convertIconTileToScale(stored: Record<string, unknown>, out: SketchSettings): void {
|
|
290
|
+
const tile = stored.iconMaskTile;
|
|
291
|
+
if (typeof tile !== 'number') return;
|
|
292
|
+
out.iconMaskScale = Math.min(5, Math.max(0.5, Number((tile / 90).toFixed(2))));
|
|
293
|
+
}
|
|
294
|
+
|
|
278
295
|
function halveSwingDials(stored: Record<string, unknown>, out: SketchSettings): void {
|
|
279
296
|
for (const [legacy, key] of Object.entries(SWING_DIALS)) {
|
|
280
297
|
const value = stored[legacy];
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The single source of truth for the CSS form of a shadow scale token:
|
|
3
3
|
*
|
|
4
|
-
* <x>px <y>px <blur>px <spread>px hsla(<h>, <s>%, <l>%, <a>)
|
|
4
|
+
* <x>px <y>px <blur>px [<spread>px] hsla(<h>, <s>%, <l>%, <a>)
|
|
5
|
+
*
|
|
6
|
+
* The spread slot is written only when it is non-zero. A three-length shadow is
|
|
7
|
+
* legal in `box-shadow` (spread defaults to 0) and in `filter: drop-shadow()`,
|
|
8
|
+
* which has no spread slot at all — so one token dresses both, and a component
|
|
9
|
+
* casting from an image's alpha reads the same variable as one casting from a
|
|
10
|
+
* box. Dial a spread and the token grows its fourth length: still a shadow,
|
|
11
|
+
* no longer a filter.
|
|
5
12
|
*
|
|
6
13
|
* Everything that reads or writes that form goes through `parseShadowCss` /
|
|
7
14
|
* `shadowTokenCss`, so the theme generator (Node, no store) and the editor
|
|
@@ -33,16 +40,17 @@ export function computeAngleDistance(x: number, y: number): { angle: number; dis
|
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
export function shadowTokenCss(t: ShadowValue): string {
|
|
36
|
-
|
|
43
|
+
const spread = t.spread ? `${t.spread}px ` : '';
|
|
44
|
+
return `${t.x}px ${t.y}px ${t.blur}px ${spread}hsla(${t.hue}, ${t.saturation}%, ${t.lightness}%, ${t.opacity})`;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
export function parseShadowCss(variable: string, raw: string): ShadowToken | null {
|
|
40
|
-
const m = raw.trim().match(/^(-?\d+)px\s+(-?\d+)px\s+(\d+)px\s+(-?\d+)px\s+hsla\(([\d.]+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\)$/);
|
|
48
|
+
const m = raw.trim().match(/^(-?\d+)px\s+(-?\d+)px\s+(\d+)px\s+(?:(-?\d+)px\s+)?hsla\(([\d.]+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\)$/);
|
|
41
49
|
if (!m) return null;
|
|
42
50
|
const x = parseInt(m[1], 10);
|
|
43
51
|
const y = parseInt(m[2], 10);
|
|
44
52
|
const blur = parseInt(m[3], 10);
|
|
45
|
-
const spread = parseInt(m[4], 10);
|
|
53
|
+
const spread = m[4] === undefined ? 0 : parseInt(m[4], 10);
|
|
46
54
|
const hue = Math.round(parseFloat(m[5]));
|
|
47
55
|
const saturation = Math.round(parseFloat(m[6]));
|
|
48
56
|
const lightness = Math.round(parseFloat(m[7]));
|
|
@@ -7,11 +7,11 @@ The editor has four views:
|
|
|
7
7
|
|
|
8
8
|
- **Tokens**: the design-system primitives (colour, type, spacing, and so on).
|
|
9
9
|
They apply everywhere your site uses them.
|
|
10
|
-
- **
|
|
10
|
+
- **Color Wheel**: the harmony wheel, the palette curves, and the story your colours
|
|
11
11
|
tell across a page.
|
|
12
12
|
- **Components**: per-component editors. Re-Assign what tokens a component uses
|
|
13
13
|
without changing the underlying system.
|
|
14
|
-
- **Sketch**: an effect layer that redraws the page by hand. See
|
|
14
|
+
- **Sketch Style**: an effect layer that redraws the page by hand. See
|
|
15
15
|
[Sketch mode](sketch-mode.md).
|
|
16
16
|
|
|
17
17
|
This page covers **Tokens**. For components, see
|
|
@@ -9,7 +9,7 @@ theme and writes nothing back, so it never touches a token, never lands in a
|
|
|
9
9
|
theme file, and never reaches the CSS you ship. Turn it off and every trace of
|
|
10
10
|
it goes.
|
|
11
11
|
|
|
12
|
-
Open the **Sketch** view in the editor and switch **Sketch mode** on. The effect
|
|
12
|
+
Open the **Sketch Style** view in the editor and switch **Sketch mode** on. The effect
|
|
13
13
|
applies to the page behind the editor as well as to the preview, so what you see
|
|
14
14
|
in context is what it does.
|
|
15
15
|
|
|
@@ -63,12 +63,93 @@ your own, alongside the shipped seven, as a file under
|
|
|
63
63
|
sit on it, and the shape of its wave. A square wave sends nearly every edge to
|
|
64
64
|
full travel, which is what makes the effect stronger rather than bigger.
|
|
65
65
|
|
|
66
|
+
## Where the settings live
|
|
67
|
+
|
|
68
|
+
The dials you are moving live in your browser, so the effect follows you across
|
|
69
|
+
reloads and stays off everyone else's screen. **Save** writes a named preset to
|
|
70
|
+
`src/live-tokens/data/sketch-presets/`, which is the only thing that reaches
|
|
71
|
+
disk.
|
|
72
|
+
|
|
73
|
+
Sketch mode is a tool for looking at the page, not a layer the page can ship.
|
|
74
|
+
Nothing is written into a theme, `tokens.generated.css` never sees it, and a
|
|
75
|
+
production build has no sketch layer in it at all.
|
|
76
|
+
|
|
66
77
|
## Drawing your own elements
|
|
67
78
|
|
|
68
|
-
The
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
The layer draws a fixed set of parts: the shipped components, and four classes
|
|
80
|
+
it reserves for you. Nothing else is touched, so a page element or a
|
|
81
|
+
consumer-authored component is left crisp until it carries one of them.
|
|
82
|
+
|
|
83
|
+
| Class | For |
|
|
84
|
+
|---------------------|-----------------------------------------------------------|
|
|
85
|
+
| `sketch-surface` | A box. The default treatment. |
|
|
86
|
+
| `sketch-container` | A large box. Tilts less, so the type inside stays readable. |
|
|
87
|
+
| `sketch-chip` | A small box. Finer fill mask, more rotation, less travel. |
|
|
88
|
+
| `sketch-rule` | A line rather than a box. No rotation, no rounded ends. |
|
|
89
|
+
|
|
90
|
+
Pick by size, not by kind: a card and a modal both take `sketch-container`, a
|
|
91
|
+
badge and a pill both take `sketch-chip`.
|
|
92
|
+
|
|
93
|
+
The class opts the element in; it names no colours, so the element states its
|
|
94
|
+
own. `--sketch-fill`, `--sketch-stroke`, `--sketch-hatch-color`,
|
|
95
|
+
`--sketch-radius` and `--sketch-shadow` name the fill, the outline, the hatching
|
|
96
|
+
ink, the corners and the shadow for one element and everything inside it. The
|
|
97
|
+
layer blanks the real background and border, so an element whose fill matters
|
|
98
|
+
under Sketch mode has to name it here as well as paint it.
|
|
99
|
+
|
|
100
|
+
The layer also paints on the element's `::before` and `::after`, forces its
|
|
101
|
+
`overflow` visible, and gives it a stacking context of its own. Keep the class
|
|
102
|
+
off anything that owns a pseudo-element, clips its content, or is positioned
|
|
103
|
+
absolutely, and put it on a wrapper instead.
|
|
104
|
+
|
|
105
|
+
```css
|
|
106
|
+
.my-callout {
|
|
107
|
+
background: var(--surface-brand-lowest);
|
|
108
|
+
border: var(--border-width-1) solid var(--border-brand);
|
|
109
|
+
border-radius: var(--radius-xl);
|
|
110
|
+
|
|
111
|
+
--sketch-fill: var(--surface-brand-lowest);
|
|
112
|
+
--sketch-stroke: var(--border-brand);
|
|
113
|
+
--sketch-radius: var(--radius-xl);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A gradient is a valid fill: the shorthand's last layer takes a colour or an
|
|
118
|
+
image, so `--sketch-fill` accepts either. States work the same way, since
|
|
119
|
+
nothing is competing with you for the value:
|
|
120
|
+
|
|
121
|
+
```css
|
|
122
|
+
.my-callout:hover { --sketch-stroke: var(--border-brand-strong); }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Images inside a drawn part
|
|
71
126
|
|
|
127
|
+
A drawn part's `overflow` is forced visible, because the fill and outline are
|
|
128
|
+
painted on pseudo-elements that travel past the box and would otherwise be cut
|
|
129
|
+
off at its edge. A background that bleeds is the effect working. An image that
|
|
130
|
+
bleeds is not: it keeps its square corners while the card around it turns.
|
|
131
|
+
|
|
132
|
+
Media that runs to a part's edge therefore has to carry that part's corners
|
|
133
|
+
itself. `--sketch-radius` is the radius the layer drew, and it inherits, so a
|
|
134
|
+
child can read it and fall back to its own value when Sketch mode is off:
|
|
135
|
+
|
|
136
|
+
```css
|
|
137
|
+
.cover {
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
border-top-left-radius: var(--sketch-radius, var(--card-default-radius));
|
|
140
|
+
border-top-right-radius: var(--sketch-radius, var(--card-default-radius));
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Corner spread is per-corner and per-instance, so at high spread the crop is the
|
|
145
|
+
mean rather than an exact trace of the drawn edge.
|
|
146
|
+
|
|
147
|
+
A rule made from a `border` is not a box and cannot be displaced. Make it an
|
|
148
|
+
element, give it `sketch-rule`, and name its ink:
|
|
149
|
+
|
|
150
|
+
```html
|
|
151
|
+
<div class="rule sketch-rule"></div>
|
|
152
|
+
```
|
|
72
153
|
```css
|
|
73
154
|
.rule {
|
|
74
155
|
height: var(--border-width-2);
|
|
@@ -77,13 +158,26 @@ what to draw it with:
|
|
|
77
158
|
}
|
|
78
159
|
```
|
|
79
160
|
|
|
80
|
-
`--sketch-fill`, `--sketch-stroke`, `--sketch-hatch-color` and `--sketch-radius`
|
|
81
|
-
name the fill, the outline, the hatching ink and the corners for one element and
|
|
82
|
-
everything inside it. Since the layer blanks the real background, an element
|
|
83
|
-
whose fill matters under Sketch mode should name it here as well as paint it.
|
|
84
|
-
|
|
85
161
|
Icons and inline SVG take the wobble directly, since a glyph has no box to
|
|
86
162
|
redraw. Body type is left alone: an icon is a shape and survives a wobble, a
|
|
87
|
-
paragraph is not.
|
|
88
|
-
|
|
89
|
-
|
|
163
|
+
paragraph is not.
|
|
164
|
+
|
|
165
|
+
`--sketch-icon-off` names what a subtree's glyphs are drawn with instead. It
|
|
166
|
+
inherits, so one declaration covers everything under it:
|
|
167
|
+
|
|
168
|
+
```css
|
|
169
|
+
/* Crisp. Chrome, a logo, anything that has to stay exact. */
|
|
170
|
+
.app-bar { --sketch-icon-off: none; }
|
|
171
|
+
|
|
172
|
+
/* Drawn back rather than off, at a third of the travel. Small artwork, and
|
|
173
|
+
type set as an SVG, which the layer reads as one large glyph. */
|
|
174
|
+
.wordmark { --sketch-icon-off: var(--sketch-icon-soft); }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The **Blotch size** dial under Icons and SVG is a share of the glyph rather than
|
|
178
|
+
a px size, because no px size is right for both a 16px icon and a page-wide
|
|
179
|
+
drawing. At 100% every glyph gets one period of the field across it whatever its
|
|
180
|
+
size. Below that the field repeats inside the glyph and the blotches get finer.
|
|
181
|
+
Above it a glyph reads part of one blotch, so the mask thins the whole glyph
|
|
182
|
+
unevenly instead of breaking it up. The fill's blotches stay in px, since a
|
|
183
|
+
component does have a size to state one against.
|