gerillass 2.1.1 → 2.2.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/SKILL.md CHANGED
@@ -5,7 +5,7 @@ description: Use the Gerillass Sass mixin library — loading it, the mixin cata
5
5
 
6
6
  # Gerillass
7
7
 
8
- A Sass mixin library: 53 mixins and 23 functions that emit CSS from
8
+ A Sass mixin library: 56 mixins and 23 functions that emit CSS from
9
9
  semantic declarations. It is Sass source only — there is no runtime and no
10
10
  utility classes, so styles live in your stylesheet and your markup stays clean.
11
11
 
@@ -84,11 +84,13 @@ a dropped declaration rather than an error.
84
84
  | `container` | `.card { @include container("card", sideways); }` |
85
85
  | `escape-to-parent` | `.a { @include escape-to-parent(42) { color: red; } }` |
86
86
  | `except` | `.a { @include except(#ff0000) { margin: 0; } }` |
87
+ | `focus-ring` | `@include focus-ring;` |
87
88
  | `font-face` | `.a { @include font-face("Inter", "/fonts/inter"); }` |
88
89
  | `hide` | `.a { @include hide(nonsense); }` |
89
90
  | `line-clamp` | `.a { @include line-clamp(0); }` |
90
91
  | `linear-gradient` | `.a { @include linear-gradient(sideways, (red, blue)); }` |
91
92
  | `loadify` | `@include loadify(nonsense);` |
93
+ | `motion-safe` | `.card { @include motion-safe; }` |
92
94
  | `only` | `.a { @include only(#ff0000) { margin: 0; } }` |
93
95
  | `radial-gradient` | `.a { @include radial-gradient(42, "center", (red, blue)); }` |
94
96
  | `remove` | `.a { @include remove(a, b, c); }` |
@@ -102,6 +104,7 @@ a dropped declaration rather than an error.
102
104
  | `text-gradient` | `.a { @include text-gradient(sideways, (red, blue)); }` |
103
105
  | `text-selection` | `.a { @include text-selection(bogus) { background: yellow; } }` |
104
106
  | `text-shadow` | `.a { @include text-shadow(42); }` |
107
+ | `tokens` | `:root { @include tokens(#fff); }` |
105
108
  | `triangle` | `.caret { @include triangle(sideways); }` |
106
109
 
107
110
  ## Traps a signature does not show
@@ -120,9 +123,13 @@ a dropped declaration rather than an error.
120
123
 
121
124
  **`breakpoint`**
122
125
 
123
- - With one argument the query matches exactly that width, `(width: 768px)`, which is a single pixel. Use `min`, `max` or a range for anything wider.
126
+ - With one argument the query matches exactly that width, `(width: 768px)`, which is a single pixel, and the mixin prints a warning. Write `only` for that width, or `min`, `max` or a range for anything wider. 3.0.0 will refuse the one-argument form.
124
127
  - Declarations written after the include, in the same rule, are emitted after the `@media` block and win over it. Write them before the include.
125
128
 
129
+ **`container-query`**
130
+
131
+ - A size on its own matches exactly that width, `(width: 400px)`, which is a single pixel, and the mixin prints a warning. Write `only` for that width, or `min`, `max` or a range for anything wider. 3.0.0 will refuse the one-argument form.
132
+
126
133
  **`container`**
127
134
 
128
135
  - An element does not match a `@container` query that reads its own container, and nothing warns. Put the `container-query` on a descendant.
@@ -135,9 +142,13 @@ a dropped declaration rather than an error.
135
142
 
136
143
  - `init` and every call must be in the same module, or the module with the call must `@use` the one that calls `init`. Otherwise Sass fails with "The target selector was not found".
137
144
 
145
+ **`motion-safe`**
146
+
147
+ - Keep the resting state outside the block. An element hidden in its base rule and revealed by an animation inside the block stays hidden for a user who asked for less motion; put the start state in the keyframes instead.
148
+
138
149
  **`remove`**
139
150
 
140
- - With one argument the element is hidden at exactly that width, `(width: 768px)`, which is a single pixel. Use `min`, `max` or a range for anything wider.
151
+ - With one argument the element is hidden at exactly that width, `(width: 768px)`, which is a single pixel, and the mixin prints a warning. Write `only` for that width, or `min`, `max` or a range for anything wider. 3.0.0 will refuse the one-argument form.
141
152
  - A `display` written after the include, in the same rule, is emitted after the `@media` block and wins over it. Write it before the include.
142
153
 
143
154
  ## Mixins
@@ -169,17 +180,19 @@ a dropped declaration rather than an error.
169
180
  | `ellipsis($width: 100%, $display: inline-block)` | Truncates a single line of text with an ellipsis. |
170
181
  | `escape-to-parent($selector: null)` | Re-roots the current selector under another one using @at-root. |
171
182
  | `except($params...)` | Selects every sibling except the ones named. |
172
- | `font-face($font-family, $file-path, $font-style: normal, $font-weight: 400, $file-formats: eot woff2 woff ttf svg)` | Emits an @font-face rule for one family across several file formats. Must be called at the root. |
183
+ | `focus-ring($width: 2px, $offset: 2px, $color: currentColor)` | Draws a keyboard focus ring with outline on :focus-visible, which survives forced-colors mode where a box-shadow ring disappears. |
184
+ | `font-face($font-family, $file-path, $font-style: normal, $font-weight: 400, $file-formats: eot woff2 woff ttf svg, $font-display: null)` | Emits an @font-face rule for one family across several file formats. Must be called at the root. |
173
185
  | `hide($toggle: "hide")` | Visually hides an element while keeping it available to screen readers, or reverses that. |
174
186
  | `line-clamp($lines: 3)` | Truncates text after a number of lines, where ellipsis truncates one. It emits five declarations rather than one because -webkit-line-clamp does nothing on its own: without display: -webkit-box or without -webkit-box-orient: vertical the text is not clamped at all and nothing warns you, and without overflow: hidden the clamped text spills out below the box. The unprefixed line-clamp is emitted too, for when it becomes Baseline. |
175
187
  | `linear-gradient($direction, $colors)` | Linear gradient background from a direction name or an angle. |
176
188
  | `loadify($params...)` | Fades elements in on page load. Call once at the root to set up, then on each element. Under prefers-reduced-motion: reduce the end state is applied directly and no animation runs. Switching the animation off alone would not do, because the element starts invisible and the animation is what reveals it, so the content would stay hidden for good. |
189
+ | `motion-safe` | Wraps its content in @media (prefers-reduced-motion: no-preference), so motion is opt-in: a user who asked their system for less motion gets none of it. |
177
190
  | `only($params...)` | Selects only the siblings named. |
178
191
  | `placeholder-shown` | Styles an input while its placeholder is visible. |
179
192
  | `placeholder` | Styles the placeholder text of an input across vendor prefixes. |
180
193
  | `position($position: absolute, $offsets: 0)` | Sets position and offsets in one call, using shorthand order. |
181
194
  | `radial-gradient($shape, $position, $colors)` | Radial gradient background from a shape and a position. |
182
- | `remove($params...)` | Hides an element outright, or within a media query: from a breakpoint up with min, up to it with max, or between two breakpoints. One breakpoint on its own hides the element at exactly that width, a single pixel. |
195
+ | `remove($params...)` | Hides an element outright, or within a media query: from a breakpoint up with min, up to it with max, or between two breakpoints. One breakpoint on its own hides the element at exactly that width, a single pixel, and prints a warning: pass only for that. |
183
196
  | `reset-css` | Meyer reset. Must be called at the root of the stylesheet. |
184
197
  | `reset-figure` | Removes default figure margins and makes the image inside responsive. |
185
198
  | `resizable($direction: both, $overflow: auto)` | Makes an element user-resizable. |
@@ -196,6 +209,7 @@ a dropped declaration rather than an error.
196
209
  | `text-selection($value: null)` | Styles the ::selection pseudo-element. |
197
210
  | `text-shadow($params...)` | Layered text shadows built from a direction, a colour and an offset. |
198
211
  | `text-stroke($fallback-color: black, $color: transparent, $stroke-color: black, $stroke-width: 1px)` | Outlines text using the webkit text-stroke properties. |
212
+ | `tokens($map, $prefix: null)` | Writes a Sass map out as CSS custom properties, for any kind of token: colours, spacing, sizes, radii, type, shadows, durations. With the prefix space, (4: 1rem) becomes --space-4: 1rem. A null value is skipped, and a quoted string keeps its quotes. |
199
213
  | `triangle($direction: "bottom", $color: black, $size: 10px 8px)` | Draws a CSS triangle out of borders, pointing in a given direction. |
200
214
 
201
215
  ## Functions
package/gerillass.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerillass",
3
- "version": "2.1.1",
3
+ "version": "2.2.1",
4
4
  "description": "Gerillass is an open-source toolkit that contains a set of Sass mixins to help designers and developers to create better, faster and consistent user interfaces.",
5
5
  "homepage": "https://gerillass.com",
6
6
  "documentation": "https://docs.gerillass.com",
@@ -106,6 +106,8 @@
106
106
  "invalid",
107
107
  "required",
108
108
  "disabled",
109
+ "focus-visible",
110
+ "user-invalid",
109
111
  "null for the plain state"
110
112
  ]
111
113
  }
@@ -115,7 +117,9 @@
115
117
  "summary": "Targets every text-like input at once, optionally in one pseudo-class state.",
116
118
  "examples": [
117
119
  "@include all-text-inputs { border: 1px solid; }",
118
- "@include all-text-inputs(\"focus\") { outline: 2px solid; }"
120
+ "@include all-text-inputs(\"focus\") { outline: 2px solid; }",
121
+ "@include all-text-inputs(\"focus-visible\") { outline: 2px solid; }",
122
+ "@include all-text-inputs(\"user-invalid\") { border-color: crimson; }"
119
123
  ],
120
124
  "rejects": [
121
125
  "@include all-text-inputs(nonsense) { color: red; }"
@@ -417,6 +421,7 @@
417
421
  "accepts": [
418
422
  "one length for every corner",
419
423
  "a corner name plus a length",
424
+ "a logical corner or edge plus a length: start-start, start-end, end-start, end-end, inline-start, inline-end, block-start or block-end",
420
425
  "four lengths: top-left, top-right, bottom-right, bottom-left"
421
426
  ]
422
427
  }
@@ -427,7 +432,9 @@
427
432
  "examples": [
428
433
  ".a { @include border-radius(8px); }",
429
434
  ".a { @include border-radius(\"top\", 8px); }",
430
- ".a { @include border-radius(1px, 2px, 3px, 4px); }"
435
+ ".a { @include border-radius(1px, 2px, 3px, 4px); }",
436
+ ".tab { @include border-radius(\"inline-start\", 8px); }",
437
+ ".bubble { @include border-radius(\"end-end\", 12px); }"
431
438
  ],
432
439
  "rejects": [
433
440
  ".a { @include border-radius(1px, 2px, 3px); }",
@@ -478,7 +485,8 @@
478
485
  "name": "$params",
479
486
  "variadic": true,
480
487
  "accepts": [
481
- "one breakpoint name, for exactly that width",
488
+ "one breakpoint name, for exactly that width, which prints a warning: write only instead",
489
+ "only plus a breakpoint, for exactly that width",
482
490
  "min or max plus a breakpoint",
483
491
  "between plus two breakpoints",
484
492
  "a start and an end value"
@@ -489,11 +497,11 @@
489
497
  "signature": "breakpoint($params...)",
490
498
  "summary": "Media query built from the breakpoint map, or from raw lengths.",
491
499
  "caveats": [
492
- "With one argument the query matches exactly that width, `(width: 768px)`, which is a single pixel. Use `min`, `max` or a range for anything wider.",
500
+ "With one argument the query matches exactly that width, `(width: 768px)`, which is a single pixel, and the mixin prints a warning. Write `only` for that width, or `min`, `max` or a range for anything wider. 3.0.0 will refuse the one-argument form.",
493
501
  "Declarations written after the include, in the same rule, are emitted after the `@media` block and win over it. Write them before the include."
494
502
  ],
495
503
  "examples": [
496
- ".a { @include breakpoint(\"medium\") { color: red; } }",
504
+ ".a { @include breakpoint(\"only\", \"medium\") { color: red; } }",
497
505
  ".a { @include breakpoint(\"min\", \"medium\") { color: red; } }",
498
506
  ".a { @include breakpoint(\"between\", \"medium\" \"large\") { color: red; } }"
499
507
  ],
@@ -502,6 +510,9 @@
502
510
  ".a { @include breakpoint() { color: red; } }",
503
511
  ".a { @include breakpoint(\"min\", var(--wide)) { color: red; } }",
504
512
  ".a { @include breakpoint(\"between\", var(--start) \"large\") { color: red; } }"
513
+ ],
514
+ "warns": [
515
+ ".a { @include breakpoint(\"medium\") { color: red; } }"
505
516
  ]
506
517
  },
507
518
  {
@@ -623,7 +634,7 @@
623
634
  "name": "$params",
624
635
  "variadic": true,
625
636
  "accepts": [
626
- "a size",
637
+ "a size, for exactly that width, which prints a warning: write only instead",
627
638
  "two sizes for a range",
628
639
  "one of min, max, only or between followed by a size",
629
640
  "$name as a keyword, to query one named container"
@@ -633,9 +644,13 @@
633
644
  "file": "scss/library/_container-query.scss",
634
645
  "signature": "container-query($params...)",
635
646
  "summary": "A @container rule, taking the same argument shapes as breakpoint so the two read alike. Sizes may be a key from $map-for-breakpoints or a raw length, and a length is the common case because a container is usually narrower than the viewport. Nothing matches at all unless an ancestor was declared with the container mixin.",
647
+ "caveats": [
648
+ "A size on its own matches exactly that width, `(width: 400px)`, which is a single pixel, and the mixin prints a warning. Write `only` for that width, or `min`, `max` or a range for anything wider. 3.0.0 will refuse the one-argument form."
649
+ ],
636
650
  "examples": [
637
651
  ".title { @include container-query(\"min\", 400px) { font-size: 2rem; } }",
638
652
  ".title { @include container-query(\"max\", 399px) { font-size: 1rem; } }",
653
+ ".title { @include container-query(\"only\", 400px) { color: red; } }",
639
654
  ".title { @include container-query(\"between\", 300px 500px) { color: red; } }",
640
655
  ".title { @include container-query(300px, 500px) { color: red; } }",
641
656
  ".title { @include container-query(\"min\", \"medium\") { color: red; } }",
@@ -644,7 +659,15 @@
644
659
  "rejects": [
645
660
  ".title { @include container-query(\"min\", 400px, 800px) { color: red; } }",
646
661
  ".title { @include container-query(\"min\", 400px, $name: 42) { color: red; } }",
647
- ".title { @include container-query(\"min\", var(--wide)) { color: red; } }"
662
+ ".title { @include container-query(\"min\", var(--wide)) { color: red; } }",
663
+ ".title { @include container-query(\"min\", 400px, $name: var(--container)) { color: red; } }",
664
+ ".title { @include container-query(\"min\", 400px, $name: \"none\") { color: red; } }",
665
+ ".title { @include container-query(\"min\", 400px, $name: \"not\") { color: red; } }",
666
+ ".title { @include container-query(\"min\", 400px, $name: \"my card\") { color: red; } }",
667
+ ".title { @include container-query(\"min\", 400px, $name: \"1card\") { color: red; } }"
668
+ ],
669
+ "warns": [
670
+ ".title { @include container-query(400px) { color: red; } }"
648
671
  ]
649
672
  },
650
673
  {
@@ -682,7 +705,9 @@
682
705
  ],
683
706
  "rejects": [
684
707
  ".card { @include container(\"card\", sideways); }",
685
- ".card { @include container(42); }"
708
+ ".card { @include container(42); }",
709
+ ".card { @include container(\"and\"); }",
710
+ ".card { @include container(\"1card\"); }"
686
711
  ]
687
712
  },
688
713
  {
@@ -791,6 +816,43 @@
791
816
  ".a { @include except(#ff0000) { margin: 0; } }"
792
817
  ]
793
818
  },
819
+ {
820
+ "name": "focus-ring",
821
+ "kind": "mixin",
822
+ "arguments": [
823
+ {
824
+ "name": "$width",
825
+ "default": "2px",
826
+ "accepts": [
827
+ "an outline width, such as 2px"
828
+ ]
829
+ },
830
+ {
831
+ "name": "$offset",
832
+ "default": "2px",
833
+ "accepts": [
834
+ "a length; negative draws the ring inside the element"
835
+ ]
836
+ },
837
+ {
838
+ "name": "$color",
839
+ "default": "currentColor",
840
+ "accepts": [
841
+ "any CSS colour, including currentColor and var()"
842
+ ]
843
+ }
844
+ ],
845
+ "file": "scss/library/_focus-ring.scss",
846
+ "signature": "focus-ring($width: 2px, $offset: 2px, $color: currentColor)",
847
+ "summary": "Draws a keyboard focus ring with outline on :focus-visible, which survives forced-colors mode where a box-shadow ring disappears.",
848
+ "examples": [
849
+ ".button { @include focus-ring; }",
850
+ ".card-link { @include focus-ring(3px, 4px, var(--focus)); }"
851
+ ],
852
+ "rejects": [
853
+ "@include focus-ring;"
854
+ ]
855
+ },
794
856
  {
795
857
  "name": "font-face",
796
858
  "kind": "mixin",
@@ -829,14 +891,24 @@
829
891
  "accepts": [
830
892
  "any of eot, woff2, woff, ttf, svg"
831
893
  ]
894
+ },
895
+ {
896
+ "name": "$font-display",
897
+ "default": "null",
898
+ "accepts": [
899
+ "auto, block, swap, fallback or optional",
900
+ "null, the default, to leave font-display out"
901
+ ]
832
902
  }
833
903
  ],
834
904
  "file": "scss/library/_font-face.scss",
835
- "signature": "font-face($font-family, $file-path, $font-style: normal, $font-weight: 400, $file-formats: eot woff2 woff ttf svg)",
905
+ "signature": "font-face($font-family, $file-path, $font-style: normal, $font-weight: 400, $file-formats: eot woff2 woff ttf svg, $font-display: null)",
836
906
  "summary": "Emits an @font-face rule for one family across several file formats. Must be called at the root.",
837
907
  "examples": [
838
908
  "@include font-face(\"Inter\", \"/fonts/inter\");",
839
- "@include font-face(\"Inter\", \"/fonts/inter\", italic, 700, woff2 woff);"
909
+ "@include font-face(\"Inter\", \"/fonts/inter\", italic, 700, woff2 woff);",
910
+ "@include font-face(\"Readex Pro\", \"/fonts/readex\", $font-weight: 160 700, $file-formats: woff2, $font-display: swap);",
911
+ "@include font-face(\"Inter\", \"/fonts/inter\", \"italic\", $font-weight: \"bold\", $file-formats: woff2);"
840
912
  ],
841
913
  "rejects": [
842
914
  ".a { @include font-face(\"Inter\", \"/fonts/inter\"); }",
@@ -845,7 +917,16 @@
845
917
  "@include font-face(\"Inter\", \"/fonts/inter\", $file-formats: otf);",
846
918
  "@include font-face(\"Inter\", \"/fonts/inter\", $file-formats: woff2 wof);",
847
919
  "@include font-face(\"Inter\", \"/fonts/inter\", $file-formats: null);",
848
- "@include font-face(\"Inter\", var(--font-path));"
920
+ "@include font-face(\"Inter\", var(--font-path));",
921
+ "@include font-face(\"Inter\", \"/fonts/inter\", $file-formats: woff2, $font-display: fast);",
922
+ "@include font-face(\"Inter\", \"/fonts/inter\", $file-formats: woff2, $font-display: var(--display));",
923
+ "@include font-face(var(--font-family), \"/fonts/inter\", $file-formats: woff2);",
924
+ "@include font-face(\"Inter\", \"/fonts/inter\", var(--font-style), $file-formats: woff2);",
925
+ "@include font-face(\"Inter\", \"/fonts/inter\", $font-weight: var(--font-weight), $file-formats: woff2);",
926
+ "@include font-face(\"Inter\", \"/fonts/inter\", $font-weight: 100 var(--max-weight), $file-formats: woff2);",
927
+ "@include font-face(sans-serif, \"/fonts/inter\", $file-formats: woff2);",
928
+ "@include font-face(inherit, \"/fonts/inter\", $file-formats: woff2);",
929
+ "@include font-face(env(--font-family), \"/fonts/inter\", $file-formats: woff2);"
849
930
  ]
850
931
  },
851
932
  {
@@ -963,6 +1044,24 @@
963
1044
  "@include loadify(nonsense);"
964
1045
  ]
965
1046
  },
1047
+ {
1048
+ "name": "motion-safe",
1049
+ "kind": "mixin",
1050
+ "arguments": [],
1051
+ "file": "scss/library/_motion-safe.scss",
1052
+ "signature": "motion-safe",
1053
+ "summary": "Wraps its content in @media (prefers-reduced-motion: no-preference), so motion is opt-in: a user who asked their system for less motion gets none of it.",
1054
+ "caveats": [
1055
+ "Keep the resting state outside the block. An element hidden in its base rule and revealed by an animation inside the block stays hidden for a user who asked for less motion; put the start state in the keyframes instead."
1056
+ ],
1057
+ "examples": [
1058
+ ".card { @include motion-safe { transition: transform 0.2s ease; } }",
1059
+ "@include motion-safe { .spinner { animation: spin 1s linear infinite; } }"
1060
+ ],
1061
+ "rejects": [
1062
+ ".card { @include motion-safe; }"
1063
+ ]
1064
+ },
966
1065
  {
967
1066
  "name": "only",
968
1067
  "kind": "mixin",
@@ -1092,7 +1191,8 @@
1092
1191
  "variadic": true,
1093
1192
  "accepts": [
1094
1193
  "nothing, to hide it always",
1095
- "one breakpoint, for exactly that width and no other",
1194
+ "one breakpoint, for exactly that width and no other, which prints a warning: write only instead",
1195
+ "only plus a breakpoint, for exactly that width",
1096
1196
  "min or max plus a breakpoint",
1097
1197
  "a start and an end breakpoint"
1098
1198
  ]
@@ -1100,20 +1200,24 @@
1100
1200
  ],
1101
1201
  "file": "scss/library/_remove.scss",
1102
1202
  "signature": "remove($params...)",
1103
- "summary": "Hides an element outright, or within a media query: from a breakpoint up with min, up to it with max, or between two breakpoints. One breakpoint on its own hides the element at exactly that width, a single pixel.",
1203
+ "summary": "Hides an element outright, or within a media query: from a breakpoint up with min, up to it with max, or between two breakpoints. One breakpoint on its own hides the element at exactly that width, a single pixel, and prints a warning: pass only for that.",
1104
1204
  "caveats": [
1105
- "With one argument the element is hidden at exactly that width, `(width: 768px)`, which is a single pixel. Use `min`, `max` or a range for anything wider.",
1205
+ "With one argument the element is hidden at exactly that width, `(width: 768px)`, which is a single pixel, and the mixin prints a warning. Write `only` for that width, or `min`, `max` or a range for anything wider. 3.0.0 will refuse the one-argument form.",
1106
1206
  "A `display` written after the include, in the same rule, is emitted after the `@media` block and wins over it. Write it before the include."
1107
1207
  ],
1108
1208
  "examples": [
1109
1209
  ".a { @include remove; }",
1110
1210
  ".a { @include remove(\"min\", \"medium\"); }",
1111
1211
  ".a { @include remove(\"max\", \"medium\"); }",
1212
+ ".a { @include remove(\"only\", \"medium\"); }",
1112
1213
  ".a { @include remove(\"small\", \"large\"); }"
1113
1214
  ],
1114
1215
  "rejects": [
1115
1216
  ".a { @include remove(a, b, c); }",
1116
1217
  ".a { @include remove(\"max\", var(--narrow)); }"
1218
+ ],
1219
+ "warns": [
1220
+ ".a { @include remove(\"medium\"); }"
1117
1221
  ]
1118
1222
  },
1119
1223
  {
@@ -1514,6 +1618,48 @@
1514
1618
  ],
1515
1619
  "rejects": []
1516
1620
  },
1621
+ {
1622
+ "name": "tokens",
1623
+ "kind": "mixin",
1624
+ "arguments": [
1625
+ {
1626
+ "name": "$map",
1627
+ "required": true,
1628
+ "accepts": [
1629
+ "a map of names to values, where a value is any CSS value: a colour, a length, a font stack, a shadow, a duration, calc() or clamp()",
1630
+ "a value of null, which is skipped",
1631
+ "a value that references another token, such as var(--blue-500)"
1632
+ ]
1633
+ },
1634
+ {
1635
+ "name": "$prefix",
1636
+ "default": "null",
1637
+ "accepts": [
1638
+ "null, the default, for no prefix",
1639
+ "a name, joined to each property name with a hyphen"
1640
+ ]
1641
+ }
1642
+ ],
1643
+ "file": "scss/library/_tokens.scss",
1644
+ "signature": "tokens($map, $prefix: null)",
1645
+ "summary": "Writes a Sass map out as CSS custom properties, for any kind of token: colours, spacing, sizes, radii, type, shadows, durations. With the prefix space, (4: 1rem) becomes --space-4: 1rem. A null value is skipped, and a quoted string keeps its quotes.",
1646
+ "examples": [
1647
+ ":root { @include tokens((1: 0.25rem, 2: 0.5rem, 4: 1rem), space); @include tokens((md: 8px, full: 9999px), radius); }",
1648
+ ":root { @include tokens((md: 0 4px 12px rgba(24, 24, 27, 0.08)), shadow); @include tokens((fast: 150ms, ease: cubic-bezier(0.2, 0, 0, 1)), motion); }",
1649
+ ":root { @include tokens((bg: #fff, text: #111), color); }",
1650
+ ":root { @include tokens((blue-500: #3b82f6, blue-600: #2563eb)); }",
1651
+ ":root { @include tokens((red: #ef4444, 500: #3b82f6), blue); }",
1652
+ "[data-theme=\"dark\"] { @include tokens((bg: #09090b, accent: var(--blue-500)), color); }",
1653
+ ":root { @include tokens((body: (\"Readex Pro\", sans-serif), label: \"New\", focus: null), font); }"
1654
+ ],
1655
+ "rejects": [
1656
+ ":root { @include tokens(#fff); }",
1657
+ ":root { @include tokens((brand: (500: #3b82f6)), color); }",
1658
+ ":root { @include tokens((bg: #fff), (color, text)); }",
1659
+ ":root { @include tokens((\"brand color\": #f00)); }",
1660
+ "@include tokens((bg: #fff));"
1661
+ ]
1662
+ },
1517
1663
  {
1518
1664
  "name": "triangle",
1519
1665
  "kind": "mixin",
@@ -1529,7 +1675,8 @@
1529
1675
  "bottom",
1530
1676
  "bottom-left",
1531
1677
  "left",
1532
- "top-left"
1678
+ "top-left",
1679
+ "inline-start, inline-end, block-start or block-end, which follow the writing direction"
1533
1680
  ]
1534
1681
  },
1535
1682
  {
@@ -1559,7 +1706,9 @@
1559
1706
  ".arrow { @include triangle(\"right\", var(--accent), 6px 8px); }",
1560
1707
  ".arrow { @include triangle(\"right\", currentColor); }",
1561
1708
  ".arrow { @include triangle(\"top\", color-mix(in srgb, red 50%, blue)); }",
1562
- ".arrow { @include triangle(\"bottom\", currentColor, var(--caret-w) var(--caret-h)); }"
1709
+ ".arrow { @include triangle(\"bottom\", currentColor, var(--caret-w) var(--caret-h)); }",
1710
+ ".next { @include triangle(\"inline-end\", currentColor, 8px 12px); }",
1711
+ ".sort { @include triangle(\"block-start\", currentColor); }"
1563
1712
  ],
1564
1713
  "rejects": [
1565
1714
  ".caret { @include triangle(sideways); }",
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  }
17
17
  },
18
18
  "license": "Apache-2.0",
19
- "version": "2.1.1",
19
+ "version": "2.2.1",
20
20
  "repository": {
21
21
  "type": "git",
22
22
  "url": "git+https://github.com/selfishprimate/gerillass.git"
@@ -5,7 +5,7 @@
5
5
  @use "../utilities/pseudo-selector" as *;
6
6
 
7
7
  @mixin all-text-inputs($pseudo: null) {
8
- $list: "hover", "focus", "active", "invalid", "required", "disabled";
8
+ $list: "hover", "focus", "focus-visible", "active", "invalid", "user-invalid", "required", "disabled";
9
9
  @if not $pseudo {
10
10
  #{$list-of-text-inputs} {
11
11
  @content;
@@ -5,7 +5,11 @@
5
5
  @use "../utilities/fill-nulls" as *;
6
6
 
7
7
  @mixin border-radius($args...) {
8
- $list: list.join($list-of-directions, ("cross-left", "cross-right", "all"));
8
+ // Logical corners are named block side first, inline side second, like the
9
+ // properties they map to. Every name here needs a branch below: the chain has
10
+ // no final @else, so a name without one would be accepted and emit nothing.
11
+ $logical: ("start-start", "start-end", "end-start", "end-end", "inline-start", "inline-end", "block-start", "block-end");
12
+ $list: list.join(list.join($list-of-directions, ("cross-left", "cross-right", "all")), $logical);
9
13
  @if list.length($args) == 1 {
10
14
  $value: list.nth($args, 1);
11
15
  border-radius: fillNulls($value, space);
@@ -41,6 +45,26 @@
41
45
  } @else if $corner == "cross-right" {
42
46
  border-top-right-radius: $value;
43
47
  border-bottom-left-radius: $value;
48
+ } @else if $corner == "start-start" {
49
+ border-start-start-radius: $value;
50
+ } @else if $corner == "start-end" {
51
+ border-start-end-radius: $value;
52
+ } @else if $corner == "end-start" {
53
+ border-end-start-radius: $value;
54
+ } @else if $corner == "end-end" {
55
+ border-end-end-radius: $value;
56
+ } @else if $corner == "inline-start" {
57
+ border-start-start-radius: $value;
58
+ border-end-start-radius: $value;
59
+ } @else if $corner == "inline-end" {
60
+ border-start-end-radius: $value;
61
+ border-end-end-radius: $value;
62
+ } @else if $corner == "block-start" {
63
+ border-start-start-radius: $value;
64
+ border-start-end-radius: $value;
65
+ } @else if $corner == "block-end" {
66
+ border-end-start-radius: $value;
67
+ border-end-end-radius: $value;
44
68
  }
45
69
  } @else {
46
70
  @error "Corner value must be one of the followings: #{$list}.";
@@ -33,6 +33,11 @@
33
33
  }
34
34
  @if list.length($params) == 1 {
35
35
  $value: list.nth($params, 1);
36
+ $width: if(map.has-key($map-for-breakpoints, $value), map.get($map-for-breakpoints, $value), $value);
37
+ // One argument matches a single pixel, which two agent trials took for
38
+ // "from this width up". Still emitted, so no stylesheet changes, but said
39
+ // out loud until 3.0.0 refuses it.
40
+ @warn "`breakpoint(#{$value})` matches only a viewport exactly #{$width} wide, `(width: #{$width})`. Write `breakpoint(only, #{$value})` for that width, or `breakpoint(min, #{$value})` from it upwards. The one-argument form will be refused in 3.0.0.";
36
41
  @if map.has-key($map-for-breakpoints, $value) {
37
42
  @media (width: map.get($map-for-breakpoints, $value)) {
38
43
  @content;
@@ -26,6 +26,45 @@
26
26
  @return null;
27
27
  }
28
28
 
29
+ // Characters and names a browser will not keep as the name in an @container
30
+ // rule. Measured in Chrome 152, the rule was dropped for `var(--n)`, `none`,
31
+ // `and`, `or`, `default`, `inherit`, `initial`, `unset`, `revert`,
32
+ // `revert-layer`, `my card`, `1card`, `-1card`, `a.b`, `#card` and `card!`,
33
+ // in any case. `not` was worse: the rule was kept with no name, as a negated
34
+ // query, so it matched the opposite of what was asked. `--card`, `-card`,
35
+ // `_card`, `c-`, `ünlü`, `auto` and `normal` were kept.
36
+ $-reserved-names: "none" "and" "or" "not" "default" "inherit" "initial" "unset" "revert" "revert-layer";
37
+ $-forbidden-in-name: "." "," "/" "#" ":" ";" "(" ")" "[" "]" "{" "}" "!" "@" "$" "%" "^" "&" "*" "+" "=" "<" ">" "?" "\\" "|" "~" "`" "'" "\"";
38
+
39
+ @function -name-problem($name) {
40
+ // An empty name has always compiled to an unnamed query, which works, so it
41
+ // is left alone rather than refused.
42
+ $text: "#{$name}";
43
+ @if string.length($text) == 0 {
44
+ @return null;
45
+ }
46
+ @if string.index($text, "var(") {
47
+ @return "a custom property is not evaluated in a @container condition, so the rule would never apply. Pass the name itself, such as `\"card\"`; the `container` mixin can still take its name from a custom property";
48
+ }
49
+ @if string.index($text, " ") {
50
+ @return "a query names one container. Pass one name, such as `\"card\"`";
51
+ }
52
+ @if list.index($-reserved-names, string.to-lower-case($text)) {
53
+ @return "`none`, `and`, `or`, `not`, `default` and the CSS-wide keywords are reserved there. A browser drops the rule, and reads `not` as negating the query instead. Pass another name, such as `\"card\"`";
54
+ }
55
+ $first: string.slice($text, 1, 1);
56
+ $second: string.slice($text, 2, 2);
57
+ @if string.index("0123456789", $first) or ($first == "-" and string.length($text) > 1 and string.index("0123456789", $second)) {
58
+ @return "a container name cannot start with a digit, or with a hyphen and a digit. Pass a name such as `\"card\"`";
59
+ }
60
+ @each $character in $-forbidden-in-name {
61
+ @if string.index($text, $character) {
62
+ @return "a container name cannot contain `#{$character}`. Use letters, digits, hyphens and underscores";
63
+ }
64
+ }
65
+ @return null;
66
+ }
67
+
29
68
  // A @container rule, taking the same argument shapes as `breakpoint` so the two
30
69
  // read alike. Sizes may be a key from $map-for-breakpoints or a raw length; a
31
70
  // container is usually narrower than the viewport, so a length is the common
@@ -42,6 +81,12 @@
42
81
  @if $name != null and meta.type-of($name) != "string" {
43
82
  @error "`#{$name}` is not a valid $name for `container-query`. Pass a container name as a string, such as `\"card\"`.";
44
83
  }
84
+ @if $name != null {
85
+ $problem: -name-problem($name);
86
+ @if $problem {
87
+ @error "`#{$name}` is not a valid $name for `container-query`: #{$problem}.";
88
+ }
89
+ }
45
90
 
46
91
  $custom-property: -custom-property-in($params);
47
92
  @if $custom-property {
@@ -50,7 +95,11 @@
50
95
  $condition: null;
51
96
 
52
97
  @if $count == 1 {
53
- $condition: "(width: #{validateBreakpoint(list.nth($params, 1))})";
98
+ $value: list.nth($params, 1);
99
+ $width: validateBreakpoint($value);
100
+ $condition: "(width: #{$width})";
101
+ // A single pixel, as in `breakpoint`; still emitted until 3.0.0 refuses it.
102
+ @warn "`container-query(#{$value})` matches only a container exactly #{$width} wide, `(width: #{$width})`. Write `container-query(only, #{$value})` for that width, or `container-query(min, #{$value})` from it upwards. The one-argument form will be refused in 3.0.0.";
54
103
  } @else if $count == 2 {
55
104
  $mode: list.nth($params, 1);
56
105
  @if not list.index("only" "min" "max" "between", $mode) {
@@ -4,6 +4,63 @@
4
4
  @use "sass:meta";
5
5
  @use "sass:string";
6
6
 
7
+ // What a browser will not keep in `container-name`. Measured in Chrome 152, the
8
+ // declaration was dropped for `and`, `or`, `not` and `default` in any case, a
9
+ // name starting with a digit or a hyphen and a digit, and a name holding `.`,
10
+ // `#` or `!`; and for a list with any one of those in it, such as `card and`,
11
+ // and for `none` or a CSS-wide keyword beside another name, such as `NONE card`
12
+ // or `card inherit`. `none`, `inherit`, `revert` and the other CSS-wide
13
+ // keywords were kept on their own, as were several valid names,
14
+ // `-card`, and `var()`, which a container can take its name from.
15
+ $-reserved-names: "and" "or" "not" "default";
16
+ $-alone-names: "none" "inherit" "initial" "unset" "revert" "revert-layer";
17
+ $-forbidden-in-name: "." "," "/" "#" ":" ";" "(" ")" "[" "]" "{" "}" "!" "@" "$" "%" "^" "&" "*" "+" "=" "<" ">" "?" "\\" "|" "~" "`" "'" "\"";
18
+
19
+ @function -words($text) {
20
+ $words: ();
21
+ $rest: $text;
22
+ $at: string.index($rest, " ");
23
+ @while $at {
24
+ $word: string.slice($rest, 1, $at - 1);
25
+ @if string.length($word) > 0 {
26
+ $words: list.append($words, $word);
27
+ }
28
+ $rest: string.slice($rest, $at + 1);
29
+ $at: string.index($rest, " ");
30
+ }
31
+ @if string.length($rest) > 0 {
32
+ $words: list.append($words, $rest);
33
+ }
34
+ @return $words;
35
+ }
36
+
37
+ @function -name-problem($name) {
38
+ $text: "#{$name}";
39
+ @if string.index($text, "var(") {
40
+ @return null;
41
+ }
42
+ $words: -words($text);
43
+ @each $word in $words {
44
+ $lower: string.to-lower-case($word);
45
+ @if list.index($-alone-names, $lower) and list.length($words) > 1 {
46
+ @return "`#{$word}` only works as the whole value, not beside another name";
47
+ }
48
+ @if list.index($-reserved-names, $lower) {
49
+ @return "`and`, `or`, `not` and `default` cannot name a container";
50
+ }
51
+ $first: string.slice($word, 1, 1);
52
+ @if string.index("0123456789", $first) or ($first == "-" and string.length($word) > 1 and string.index("0123456789", string.slice($word, 2, 2))) {
53
+ @return "a container name cannot start with a digit, or with a hyphen and a digit";
54
+ }
55
+ @each $character in $-forbidden-in-name {
56
+ @if string.index($word, $character) {
57
+ @return "a container name cannot contain `#{$character}`";
58
+ }
59
+ }
60
+ }
61
+ @return null;
62
+ }
63
+
7
64
  // Marks an element as a query container, so `container-query` can ask about its
8
65
  // width instead of the viewport's.
9
66
  //
@@ -19,6 +76,12 @@
19
76
  @if $name != null and meta.type-of($name) != "string" {
20
77
  @error "`#{$name}` is not a valid $name for `container`. Pass a name as a string, such as `\"card\"`, or no name at all.";
21
78
  }
79
+ @if $name != null {
80
+ $problem: -name-problem($name);
81
+ @if $problem {
82
+ @error "`#{$name}` is not a valid $name for `container`: #{$problem}, and a browser drops the whole declaration. Pass a name such as `\"card\"`.";
83
+ }
84
+ }
22
85
  container-type: $type;
23
86
  @if $name {
24
87
  container-name: string.unquote($name);
@@ -0,0 +1,34 @@
1
+ @charset "UTF-8";
2
+
3
+ // Draws a focus ring for keyboard users. Two things are usually got wrong by
4
+ // hand, and this writes the version that avoids both.
5
+ //
6
+ // The ring goes on :focus-visible, not :focus. Measured in Chrome 152: a mouse
7
+ // click on a button or a link does not match :focus-visible, so the ring stays
8
+ // out of the way of pointer users, while Tab does match it. Text inputs are the
9
+ // exception and match it on a click too, which is the browser's own choice.
10
+ // Hiding the ring with `:focus { outline: none; }` is the common mistake it
11
+ // replaces: tabbing to that button gave no outline and no shadow at all.
12
+ //
13
+ // The ring is an outline, not a box-shadow. A box-shadow ring looks the same
14
+ // in an ordinary page, but forced-colors mode, as Windows High Contrast is
15
+ // exposed to CSS, sets box-shadow to none and repaints outline-color in a
16
+ // system colour, so a shadow ring vanishes for the users who most need it.
17
+ // That part follows the CSS Color Adjustment specification; it was not
18
+ // measured here, because the browser used could not emulate the mode.
19
+ //
20
+ // Two things measured about the outline itself. It follows border-radius: on a
21
+ // button with a 12px radius the ring is rounded to match. And an ancestor with
22
+ // overflow: hidden clips it: a 2px offset left only the ring's corners showing,
23
+ // while an offset of -4px drew it inside the element, fully visible. Pass a
24
+ // negative $offset where the element sits in a clipping container.
25
+ @mixin focus-ring($width: 2px, $offset: 2px, $color: currentColor) {
26
+ @if not & {
27
+ @error "`focus-ring` styles the element it is called in, so call it inside a selector, such as `.button { @include focus-ring; }`.";
28
+ }
29
+
30
+ &:focus-visible {
31
+ outline: $width solid $color;
32
+ outline-offset: $offset;
33
+ }
34
+ }
@@ -12,7 +12,8 @@
12
12
  $file-path,
13
13
  $font-style: normal,
14
14
  $font-weight: 400,
15
- $file-formats: eot woff2 woff ttf svg
15
+ $file-formats: eot woff2 woff ttf svg,
16
+ $font-display: null
16
17
  ) {
17
18
  @if & {
18
19
  @error "You must call the mixin at the root level of your style sheet, not in the `#{&+'{'+'}'}` selector.";
@@ -24,10 +25,32 @@
24
25
  // The path is written into url("..."), where var() is only text: the
25
26
  // browser would request a file literally named var(--x).woff2.
26
27
  @error "`#{$file-path}` is not a valid $file-path for `font-face`. A custom property cannot supply a font path, because the path is written into url() as text. Pass the path as a string.";
28
+ } @else if string.slice(meta.inspect($font-family), 1, 1) != "\"" and string.index($font-family, "var(") {
29
+ // Measured in Chrome 152, a @font-face rule dropped `font-family: var(--f)`,
30
+ // and a rule with no family is never used. A quoted "var(--f)" is only a
31
+ // name, which the browser kept, so it is left alone like a quoted path.
32
+ @error "`#{$font-family}` is not a valid $font-family for `font-face`. A custom property cannot supply it, because a @font-face rule drops var() in a descriptor, and without a family the font is never used. Pass the family name as a string, such as `\"Inter\"`.";
33
+ } @else if string.slice(meta.inspect($font-family), 1, 1) != "\"" and (list.index("serif" "sans-serif" "monospace" "cursive" "fantasy" "system-ui" "math" "inherit" "initial" "unset" "revert" "revert-layer" "default", string.to-lower-case($font-family)) or string.index($font-family, "env(") or string.index($font-family, "attr(")) {
34
+ // Measured in Chrome 152, a @font-face rule dropped an unquoted `serif`,
35
+ // `sans-serif`, `monospace`, `cursive`, `fantasy`, `system-ui`, `math`,
36
+ // `default` or CSS-wide keyword, in any case, and `env()` and `attr()`. The
37
+ // same words quoted are names and were kept, as were `emoji`, `fangsong` and
38
+ // the `ui-` families unquoted.
39
+ @error "`#{$font-family}` is not a valid $font-family for `font-face`. A @font-face rule drops a generic family, a CSS-wide keyword or a function as its family, and without a family the font is never used. Pass the family name as a quoted string, such as `\"Inter\"`.";
27
40
  } @else {
28
41
 
29
42
  $list: ();
30
43
 
44
+ // The same rule dropped `font-style: var(--s)`, `font-weight: var(--w)` and
45
+ // `font-weight: 100 var(--w)`, so var() is refused anywhere in either,
46
+ // before $font-style is reinterpreted below.
47
+ @if string.index(meta.inspect($font-style), "var(") {
48
+ @error "`#{meta.inspect($font-style)}` is not a valid $font-style for `font-face`. A custom property cannot supply it, because a @font-face rule drops var() in a descriptor. Pass the style itself, such as `italic`.";
49
+ }
50
+ @if string.index(meta.inspect($font-weight), "var(") {
51
+ @error "`#{meta.inspect($font-weight)}` is not a valid $font-weight for `font-face`. A custom property cannot supply it, because a @font-face rule drops var() in a descriptor. Pass the weight itself, such as `700`, or a range such as `160 700`.";
52
+ }
53
+
31
54
  @if list.index(100 200 300 400 500 600 700 800 900, $font-style) {
32
55
  $font-weight: $font-style;
33
56
  $font-style: normal;
@@ -48,6 +71,19 @@
48
71
  }
49
72
  }
50
73
 
74
+ // $font-display comes last so the reinterpretation of $font-style above is
75
+ // left alone, and it is written only when given: the browser's default is
76
+ // what a call without it has always had. Measured in Chrome 152, a
77
+ // @font-face rule drops `font-display: "swap"` and `font-display: var(--d)`
78
+ // without a word, so a quoted keyword is unquoted and var() is refused.
79
+ @if $font-display != null {
80
+ @if meta.type-of($font-display) == "string" and string.index($font-display, "var(") {
81
+ @error "`#{meta.inspect($font-display)}` is not a valid $font-display for `font-face`. A custom property cannot supply it, because a @font-face rule drops var() in a descriptor. Pass one of: auto, block, swap, fallback, optional.";
82
+ } @else if not list.index("auto" "block" "swap" "fallback" "optional", $font-display) {
83
+ @error "`#{meta.inspect($font-display)}` is not a valid $font-display for `font-face`. Pass one of: auto, block, swap, fallback, optional.";
84
+ }
85
+ }
86
+
51
87
  // fontSource returns nothing for a format it does not know, so a typo
52
88
  // dropped that source and a list of nothing but typos dropped `src`.
53
89
  @each $format in $file-formats {
@@ -65,8 +101,23 @@
65
101
  $list: list.append($list, fontSource($font-family, $file-path, list.nth($file-formats, $i)), comma);
66
102
  }
67
103
  src: $list;
68
- font-style: $font-style;
69
- font-weight: $font-weight;
104
+ // A quoted keyword is written without its quotes. Measured in Chrome 152,
105
+ // `font-style: "italic"` and `font-weight: "bold"` were dropped, and this
106
+ // is what a quoted argument used to produce. The family keeps its quotes,
107
+ // since there they make it a name.
108
+ @if meta.type-of($font-style) == "string" {
109
+ font-style: string.unquote($font-style);
110
+ } @else {
111
+ font-style: $font-style;
112
+ }
113
+ @if meta.type-of($font-weight) == "string" {
114
+ font-weight: string.unquote($font-weight);
115
+ } @else {
116
+ font-weight: $font-weight;
117
+ }
118
+ @if $font-display {
119
+ font-display: string.unquote($font-display);
120
+ }
70
121
  @content;
71
122
  }
72
123
  }
@@ -25,11 +25,13 @@
25
25
  @forward "ellipsis";
26
26
  @forward "escape-to-parent";
27
27
  @forward "except";
28
+ @forward "focus-ring";
28
29
  @forward "font-face";
29
30
  @forward "hide";
30
31
  @forward "line-clamp";
31
32
  @forward "linear-gradient";
32
33
  @forward "loadify";
34
+ @forward "motion-safe";
33
35
  @forward "only";
34
36
  @forward "placeholder";
35
37
  @forward "placeholder-shown";
@@ -52,4 +54,5 @@
52
54
  @forward "text-selection";
53
55
  @forward "text-shadow";
54
56
  @forward "text-stroke";
57
+ @forward "tokens";
55
58
  @forward "triangle";
@@ -0,0 +1,28 @@
1
+ @charset "UTF-8";
2
+
3
+ @use "sass:meta";
4
+
5
+ // Applies its content only for users who have not asked their system for less
6
+ // motion. Motion is opt-in here rather than switched off afterwards: the usual
7
+ // way round writes the animation and then overrides it under
8
+ // `prefers-reduced-motion: reduce`, and that override has to catch every
9
+ // animation and transition, including the ones added later. Written inside this
10
+ // block, nothing moves for a user who asked for less, without a list to keep.
11
+ //
12
+ // Two agent trials wrote this same wrapper by hand, as `motion-safe` and
13
+ // `motion-ok`. Both media queries were checked to parse in Chrome 152; the
14
+ // reduced-motion setting itself could not be emulated in the browser used.
15
+ //
16
+ // The trap is the resting state. An element hidden in its base rule and
17
+ // revealed by an animation inside the block stays hidden when the block does
18
+ // not apply, which is what `loadify` avoids by applying the end state. Keep the
19
+ // visible state in the base rule and the start of the motion in the keyframes.
20
+ @mixin motion-safe {
21
+ @if not meta.content-exists() {
22
+ @error "`motion-safe` wraps the motion you pass it, so call it with a block, such as `.card { @include motion-safe { transition: transform 0.2s; } }`.";
23
+ }
24
+
25
+ @media (prefers-reduced-motion: no-preference) {
26
+ @content;
27
+ }
28
+ }
@@ -8,9 +8,12 @@
8
8
  display: none;
9
9
  } @else if list.length($params) == 1 {
10
10
  $value: list.nth($params, 1);
11
- @include breakpoint($value) {
11
+ // `only` rather than the one-argument form, which compiles to the same
12
+ // query, so the warning below names `remove` and is printed once.
13
+ @include breakpoint(only, $value) {
12
14
  display: none;
13
15
  }
16
+ @warn "`remove(#{$value})` hides the element only at a viewport exactly that wide, a single pixel. Write `remove(only, #{$value})` for that width, or `remove(min, #{$value})` from it upwards. The one-argument form will be refused in 3.0.0.";
14
17
  } @else if list.length($params) == 2 {
15
18
  @if list.index("min" "max", list.nth($params, 1)) {
16
19
  $mode: list.nth($params, 1);
@@ -0,0 +1,111 @@
1
+ @charset "UTF-8";
2
+
3
+ @use "sass:list";
4
+ @use "sass:meta";
5
+ @use "sass:string";
6
+
7
+ // Characters a custom property name cannot hold. Measured in Chrome 152: a
8
+ // declaration named `--space-0.5`, `--w-1/2`, `--a b` or `--#fff` was dropped,
9
+ // and `--font:body` became a property called `--font`. Letters, digits,
10
+ // hyphens, underscores and non-ASCII letters all survived.
11
+ $-forbidden: " " "." "," "/" "#" ":" ";" "(" ")" "[" "]" "{" "}" "!" "@" "$" "%" "^" "&" "*" "+" "=" "<" ">" "?" "\\" "|" "~" "`" "'" "\"";
12
+
13
+ @function -forbidden-in($text) {
14
+ @each $character in $-forbidden {
15
+ @if string.index($text, $character) {
16
+ @return $character;
17
+ }
18
+ }
19
+ @return null;
20
+ }
21
+
22
+ // Writes a value the way Sass writes it in an ordinary declaration. A custom
23
+ // property value is interpolated, and neither obvious spelling matches that:
24
+ // plain interpolation drops the quotes of a string, and meta.inspect keeps them
25
+ // but prints numbers at full precision, so oklch(0.637 0.237 25.331) came out
26
+ // with a hue of 25.331000000000017deg and math.div(1, 3) as 0.3333333333333333.
27
+ // So only strings go through meta.inspect, and a list is walked into.
28
+ @function -serialize($value) {
29
+ @if meta.type-of($value) == "string" {
30
+ @return meta.inspect($value);
31
+ }
32
+ @if meta.type-of($value) == "list" or meta.type-of($value) == "arglist" {
33
+ $joiner: " ";
34
+ @if list.separator($value) == "comma" {
35
+ $joiner: ", ";
36
+ } @else if list.separator($value) == "slash" {
37
+ $joiner: " / ";
38
+ }
39
+ $out: "";
40
+ @each $item in $value {
41
+ @if $out != "" {
42
+ $out: $out + $joiner;
43
+ }
44
+ $out: $out + -serialize($item);
45
+ }
46
+ @if list.is-bracketed($value) {
47
+ $out: "[" + $out + "]";
48
+ }
49
+ @return $out;
50
+ }
51
+ @return "#{$value}";
52
+ }
53
+
54
+ // Writes a Sass map out as custom properties, which two agent trials wrote by
55
+ // hand for a light and a dark palette. Three things it does that interpolating
56
+ // the map in a loop does not:
57
+ //
58
+ // A null is left out. `--focus: null` is a value, not an absence: measured in
59
+ // Chrome 152, `color: var(--focus, red)` did not fall back to red.
60
+ //
61
+ // A quoted string keeps its quotes, through -serialize above. With plain
62
+ // interpolation `"→"` became `→`, and `content: var(--arrow)` then computed to
63
+ // none.
64
+ //
65
+ // A nested map is refused rather than flattened, so a later version can decide
66
+ // how to flatten it without changing what a call already produces.
67
+ @mixin tokens($map, $prefix: null) {
68
+ @if not & {
69
+ @error "`tokens` writes custom properties into the rule it is called in, so call it inside a selector, such as `:root { @include tokens((bg: #fff), color); }`.";
70
+ }
71
+ @if meta.type-of($map) != "map" {
72
+ @error "`#{meta.inspect($map)}` is not a valid $map for `tokens`. Pass a map of names to values, such as `(bg: #fff, text: #111)`.";
73
+ }
74
+ // Checked by its characters, not its type, like the names below: Sass reads
75
+ // an unquoted `blue` or `red` as a colour, and `tokens($blue, blue)` has to
76
+ // work. A colour written `#00f` or `rgb()` is still caught by `#` and `(`.
77
+ @if $prefix != null {
78
+ @if not list.index("string" "color" "number", meta.type-of($prefix)) or string.length("#{$prefix}") == 0 {
79
+ @error "`#{meta.inspect($prefix)}` is not a valid $prefix for `tokens`. Pass a name, such as `color`, or leave it out.";
80
+ }
81
+ $character: -forbidden-in("#{$prefix}");
82
+ @if $character {
83
+ @error "`#{$prefix}` is not a valid $prefix for `tokens`: a custom property name cannot contain `#{$character}`. Use letters, digits, hyphens and underscores.";
84
+ }
85
+ }
86
+
87
+ @each $name, $value in $map {
88
+ $text: "#{$name}";
89
+ @if string.length($text) == 0 {
90
+ @error "`#{meta.inspect($name)}` is not a valid token name for `tokens`. Pass a name such as `bg` or `500`.";
91
+ }
92
+ $character: -forbidden-in($text);
93
+ @if $character {
94
+ @error "`#{$text}` is not a valid token name for `tokens`: a custom property name cannot contain `#{$character}`. Use letters, digits, hyphens and underscores, such as `space-0_5`.";
95
+ }
96
+ @if meta.type-of($value) == "map" {
97
+ @error "`#{$text}` holds a map, which `tokens` does not flatten. Pass each group on its own, such as `@include tokens($blue, blue)` for `$blue: (500: #3b82f6)`.";
98
+ }
99
+ @if meta.type-of($value) == "list" and list.length($value) == 0 {
100
+ @error "`#{$text}` has an empty value in `tokens`. Pass a value, or null to leave the property out.";
101
+ }
102
+
103
+ @if $value != null {
104
+ $property: --#{$text};
105
+ @if $prefix {
106
+ $property: --#{$prefix}-#{$text};
107
+ }
108
+ #{$property}: #{-serialize($value)};
109
+ }
110
+ }
111
+ }
@@ -58,8 +58,15 @@
58
58
  @return false;
59
59
  }
60
60
 
61
+ // The logical directions point along the writing direction rather than a fixed
62
+ // side: inline-end is right in a left-to-right page and left in a right-to-left
63
+ // one. They are kept in a list of their own on purpose. $list-of-directions is
64
+ // shared with border-radius, whose corner branches have no final @else, so a
65
+ // keyword added there without a branch would be accepted and emit nothing.
66
+ $-logical-directions: ("inline-start", "inline-end", "block-start", "block-end");
67
+
61
68
  @mixin triangle($direction: "bottom", $color: black, $size: 10px 8px) {
62
- @if list.index($list-of-directions, $direction) {
69
+ @if list.index($list-of-directions, $direction) or list.index($-logical-directions, $direction) {
63
70
  // Halving a size in calc() rather than math.div() keeps var() and clamp()
64
71
  // working instead of emitting `var(--w)/2`, which is not valid CSS.
65
72
  @each $item in $size {
@@ -99,10 +106,41 @@
99
106
  } @else if $direction == "top-left" {
100
107
  border-color: $color transparent transparent;
101
108
  border-width: list.nth($size, 1) list.nth($size, 1) 0 0;
109
+ } @else {
110
+ // The same shapes as right, left, bottom and top, drawn with logical
111
+ // borders so the browser picks the side. The first size is the length
112
+ // the triangle points along, the second the width across it.
113
+ $along: list.nth($size, 1);
114
+ $across: $along;
115
+ @if list.length($size) == 2 {
116
+ $across: list.nth($size, 2);
117
+ }
118
+ border-color: transparent;
119
+ @if $direction == "inline-end" {
120
+ border-inline-start-color: $color;
121
+ border-block-width: calc($across / 2);
122
+ border-inline-start-width: $along;
123
+ border-inline-end-width: 0;
124
+ } @else if $direction == "inline-start" {
125
+ border-inline-end-color: $color;
126
+ border-block-width: calc($across / 2);
127
+ border-inline-start-width: 0;
128
+ border-inline-end-width: $along;
129
+ } @else if $direction == "block-end" {
130
+ border-block-start-color: $color;
131
+ border-inline-width: calc($along / 2);
132
+ border-block-start-width: $across;
133
+ border-block-end-width: 0;
134
+ } @else {
135
+ border-block-end-color: $color;
136
+ border-inline-width: calc($along / 2);
137
+ border-block-start-width: 0;
138
+ border-block-end-width: $across;
139
+ }
102
140
  }
103
141
  @content;
104
142
  }
105
143
  } @else {
106
- @error "The argument for direction must be one of the followings: #{$list-of-directions}";
144
+ @error "The argument for direction must be one of the followings: #{list.join($list-of-directions, $-logical-directions)}";
107
145
  }
108
146
  }