@greendrake/scss-kit 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eugene Greendrake
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # @greendrake/scss-kit
2
+
3
+ Framework-free SCSS primitives: viewport breakpoints (single-sourced with a TypeScript twin), a z-index layer map, a data-URI icon-mask registry, an `@font-face` generator and a handful of mixins. `@use`-ing the root entry emits no CSS; everything is a map, function or mixin the consumer includes.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ bun add @greendrake/scss-kit
9
+ ```
10
+
11
+ `npm install @greendrake/scss-kit` works equally. The package ships SCSS and TypeScript source with no build step and no `.d.ts`. It is consumed via the bare specifier `@use '@greendrake/scss-kit'`, which Vite's Sass importer resolves from `node_modules`.
12
+
13
+ ## Usage
14
+
15
+ ```scss
16
+ @use '@greendrake/scss-kit' as kit;
17
+
18
+ .sidebar {
19
+ z-index: kit.z('sticky');
20
+ @include kit.respond-below('narrow') {
21
+ display: none;
22
+ }
23
+ }
24
+
25
+ .close-button {
26
+ @include kit.icon;
27
+ @include kit.i('close');
28
+ }
29
+ ```
30
+
31
+ Every name-keyed function below (`breakpoint`, `z`, `icon-svg`) raises a Sass `@error` listing the known keys when given an unknown name. `_lookup.scss`, which implements that, is internal and not forwarded.
32
+
33
+ ## Breakpoints
34
+
35
+ `$breakpoints` in `_breakpoints.scss` and the TypeScript module `src/breakpoints.ts` are both generated from `src/breakpoints.json` by `scripts/generate.ts` (`bun run generate` in the package source, runs under Bun), so the values and the boundary semantics cannot drift apart. The JSON is the only editable source; the two generated files carry a GENERATED header and both ship in the package.
36
+
37
+ ```json
38
+ {
39
+ "tooNarrow": 320,
40
+ "narrow": 600
41
+ }
42
+ ```
43
+
44
+ A breakpoint value is the inclusive upper bound of the viewport range below it: "below" matches `width <= value`, "above" matches `width >= value + 1px`. `narrow` (600) is the content/wide-screen boundary: a viewport is wide from 601px up.
45
+
46
+ SCSS (root entry):
47
+
48
+ - `$breakpoints` — `('tooNarrow': 320px, 'narrow': 600px)`.
49
+ - `breakpoint($name)` — the value for a name.
50
+ - `respond-below($name) { … }` — emits `@media (max-width: <value>)`.
51
+ - `respond-above($name) { … }` — emits `@media (min-width: <value + 1px>)`.
52
+ - `respond-above-height($name) { … }` — emits `@media (min-height: <value + 1px>)`.
53
+
54
+ TypeScript (`@greendrake/scss-kit/breakpoints`):
55
+
56
+ ```ts
57
+ import { breakpoints, mediaAbove, mediaBelow, type BreakpointName } from '@greendrake/scss-kit/breakpoints'
58
+
59
+ breakpoints.narrow // 600
60
+ mediaBelow('narrow') // '(max-width: 600px)'
61
+ mediaAbove('narrow') // '(min-width: 601px)'
62
+ window.matchMedia(mediaAbove('narrow')).matches
63
+ ```
64
+
65
+ `breakpoints` is `as const`; `BreakpointName` is `keyof typeof breakpoints`; `mediaAbove`/`mediaBelow` return media-query strings with the same semantics as the SCSS mixins.
66
+
67
+ ## Z layers
68
+
69
+ - `$z-layers` — `('below': -1, 'base': 0, 'raised': 1, 'spinner': 10, 'sticky': 50, 'modal': 100, 'toast': 200)`.
70
+ - `z($name)` — the value for a name.
71
+
72
+ ```scss
73
+ .toast {
74
+ z-index: kit.z('toast');
75
+ }
76
+ ```
77
+
78
+ ## Icons
79
+
80
+ Data-URI SVG icons rendered through `mask-image`, so the element's `background-color` paints them.
81
+
82
+ - `$icons` — map of name → `url("data:image/svg+xml,…")`. Names: `eye`, `eye-off`, `copy`, `delete`, `close`, `menu`, `expand`, `collapse`, `download`, `chevron-left`, `chevron-right`, `link`, `refresh`, `edit`, `quote`, `qr`, `logout`, `user`, `settings`, `sun`, `moonstar`.
83
+ - `icon-svg($name)` — the `url()` for a name.
84
+ - `icon-mask($svg)` — emits `mask-image: $svg`. The extension point for icons outside the registry: wrap it in an app mixin with your own data-URI SVG.
85
+ - `i($name)` — `icon-mask(icon-svg($name))`.
86
+ - `icon-base` — emits `border: 0; display: inline-block; mask-repeat: no-repeat; background-color: var(--font-color); background-image: none`.
87
+ - `icon` — `icon-base` plus `mask-position: center center` and `width`/`height` of `var(--icon-size)`.
88
+
89
+ `--font-color` and `--icon-size` are read with no fallback; the consuming app defines them (`@greendrake/theme` documents the custom-property contract).
90
+
91
+ ```scss
92
+ .eye-toggle {
93
+ @include kit.icon;
94
+ @include kit.i('eye');
95
+ &[aria-pressed='true'] {
96
+ @include kit.i('eye-off');
97
+ }
98
+ }
99
+
100
+ @mixin i-foo {
101
+ @include kit.icon-mask(url('data:image/svg+xml,…'));
102
+ }
103
+ ```
104
+
105
+ ## Font faces
106
+
107
+ - `font-faces($family, $faces, $display: swap)` — emits one `@font-face` per (weight, style) entry with `font-family`, `src: <url> format('woff2')`, `font-weight`, `font-style` and `font-display: $display`. `$faces` is a map `(weight: (style: url))`.
108
+ - `use-as-default($family, $fallbacks...)` — emits `:root { font-family: $family, <fallbacks> }`. Registering faces deliberately does not touch `:root`; a family becomes the document default only through this mixin.
109
+
110
+ ```scss
111
+ @use '@greendrake/scss-kit' as kit;
112
+
113
+ @include kit.font-faces(
114
+ 'Example',
115
+ (
116
+ 400: (
117
+ normal: url('./fonts/Example-Regular.woff2'),
118
+ italic: url('./fonts/Example-Italic.woff2')
119
+ ),
120
+ 700: (
121
+ normal: url('./fonts/Example-Bold.woff2')
122
+ )
123
+ )
124
+ );
125
+ @include kit.use-as-default('Example', Helvetica, Arial, sans-serif);
126
+ ```
127
+
128
+ The `url()` literals must be written in the font package's own `.scss` file: Vite rebases them relative to that file when the package is consumed from `node_modules`, and only when the package is imported by bare specifier (never a `pkg:` URL, never with Sass's `NodePackageImporter` registered). `@greendrake/font-inter` is a font package built on exactly these two mixins.
129
+
130
+ ## Mixins
131
+
132
+ - `monospace` — `font-family: monospace, monospace`.
133
+ - `disabled` — `pointer-events: none; opacity: 0.5; cursor: default; user-select: none`.
134
+ - `apply-props($props)` — emits each `key: value` pair of a map as a declaration.
135
+ - `control-clear` — the × clear button rendered inside a select-shaped control, before the chevron: an inline-flex box held at the WCAG 2.2 target-size minimum (24×24, SC 2.5.8) with a negative block margin (computed from `var(--line-height-tight, 18px)`) so it grows into the control's padding instead of its height; glyph size `var(--control-clear-font-size, 18px)`; opacity 0.5, 1 on hover.
136
+ - `control-chevron($point: down)` — a dropdown/drill chevron built from two borders (no icon dependency): 7×7px, `currentColor`, opacity 0.6, `transform` transition; `$point: right` rotates it to point right.
137
+ - `img-spinner($size: 24px)` — a centred spinner `::before` on an `img` container. Requires the global `spinner` keyframes (provided by `@greendrake/theme`) and the `--spinner-color`/`--spinner-side-color` custom properties.
138
+
139
+ ```scss
140
+ .select-clear {
141
+ @include kit.control-clear;
142
+ }
143
+ .select-chevron {
144
+ @include kit.control-chevron;
145
+ }
146
+ .drill {
147
+ @include kit.control-chevron(right);
148
+ }
149
+ .thumb {
150
+ @include kit.img-spinner(32px);
151
+ }
152
+ .code {
153
+ @include kit.monospace;
154
+ }
155
+ .card {
156
+ @include kit.apply-props((padding: 8px, gap: 4px));
157
+ }
158
+ ```
@@ -0,0 +1,35 @@
1
+ // GENERATED from src/breakpoints.json by scripts/generate.ts — do not edit, regenerate via `bun run generate`.
2
+ // A breakpoint value is the inclusive upper bound of the viewport range below it:
3
+ // "below" matches width <= value; "above" matches width >= value + 1px.
4
+ // narrow (600): the content/wide-screen boundary — a viewport is "wide" from 601px up.
5
+ @use 'lookup' as *;
6
+
7
+ $breakpoints: (
8
+ 'tooNarrow': 320px,
9
+ 'narrow': 600px
10
+ );
11
+
12
+ @function breakpoint($name) {
13
+ @return lookup($breakpoints, $name, 'breakpoint');
14
+ }
15
+
16
+ @mixin respond-below($name) {
17
+ $max: breakpoint($name);
18
+ @media (max-width: $max) {
19
+ @content;
20
+ }
21
+ }
22
+
23
+ @mixin respond-above($name) {
24
+ $min: breakpoint($name) + 1px;
25
+ @media (min-width: $min) {
26
+ @content;
27
+ }
28
+ }
29
+
30
+ @mixin respond-above-height($name) {
31
+ $min: breakpoint($name) + 1px;
32
+ @media (min-height: $min) {
33
+ @content;
34
+ }
35
+ }
@@ -0,0 +1,27 @@
1
+ @use 'sass:list';
2
+
3
+ // Emits one @font-face per (weight, style) entry.
4
+ // $faces: (weight: (style: url)), e.g. (400: (normal: url('./fonts/X-Regular.woff2'), italic: url('./fonts/X-Italic.woff2'))).
5
+ // The url() literals must be written in the calling package's own .scss file so that
6
+ // Vite rebases them relative to that file when consuming the package from node_modules.
7
+ @mixin font-faces($family, $faces, $display: swap) {
8
+ @each $weight, $styles in $faces {
9
+ @each $style, $file in $styles {
10
+ @font-face {
11
+ font-family: $family;
12
+ src: $file format('woff2');
13
+ font-weight: $weight;
14
+ font-style: $style;
15
+ font-display: $display;
16
+ }
17
+ }
18
+ }
19
+ }
20
+
21
+ // Opt-in document-default font: registering faces (font-faces above) deliberately
22
+ // does NOT touch :root — consumers adopt a family explicitly via this mixin.
23
+ @mixin use-as-default($family, $fallbacks...) {
24
+ :root {
25
+ font-family: list.join(($family,), $fallbacks, $separator: comma);
26
+ }
27
+ }
package/_icons.scss ADDED
@@ -0,0 +1,57 @@
1
+ @use 'lookup' as *;
2
+
3
+ // Data-URI SVG icon registry. Icons render via `mask-image`, so the element's
4
+ // `background-color` (--font-color by default) paints them.
5
+ $icons: (
6
+ 'eye': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2.42012 12.7132C2.28394 12.4975 2.21584 12.3897 2.17772 12.2234C2.14909 12.0985 2.14909 11.9015 2.17772 11.7766C2.21584 11.6103 2.28394 11.5025 2.42012 11.2868C3.54553 9.50484 6.8954 5 12.0004 5C17.1054 5 20.4553 9.50484 21.5807 11.2868C21.7169 11.5025 21.785 11.6103 21.8231 11.7766C21.8517 11.9015 21.8517 12.0985 21.8231 12.2234C21.785 12.3897 21.7169 12.4975 21.5807 12.7132C20.4553 14.4952 17.1054 19 12.0004 19C6.8954 19 3.54553 14.4952 2.42012 12.7132Z' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M12.0004 15C13.6573 15 15.0004 13.6569 15.0004 12C15.0004 10.3431 13.6573 9 12.0004 9C10.3435 9 9.0004 10.3431 9.0004 12C9.0004 13.6569 10.3435 15 12.0004 15Z' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
7
+ 'eye-off': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10.7429 5.09232C11.1494 5.03223 11.5686 5 12.0004 5C17.1054 5 20.4553 9.50484 21.5807 11.2868C21.7169 11.5025 21.785 11.6103 21.8231 11.7767C21.8518 11.9016 21.8517 12.0987 21.8231 12.2236C21.7849 12.3899 21.7164 12.4985 21.5792 12.7156C21.2793 13.1901 20.8222 13.8571 20.2165 14.5805M6.72432 6.71504C4.56225 8.1817 3.09445 10.2194 2.42111 11.2853C2.28428 11.5019 2.21587 11.6102 2.17774 11.7765C2.1491 11.9014 2.14909 12.0984 2.17771 12.2234C2.21583 12.3897 2.28393 12.4975 2.42013 12.7132C3.54554 14.4952 6.89541 19 12.0004 19C14.0588 19 15.8319 18.2676 17.2888 17.2766M3.00042 3L21.0004 21M9.8791 9.87868C9.3362 10.4216 9.00042 11.1716 9.00042 12C9.00042 13.6569 10.3436 15 12.0004 15C12.8288 15 13.5788 14.6642 14.1217 14.1213' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
8
+ 'copy': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 8V5.2C8 4.0799 8 3.51984 8.21799 3.09202C8.40973 2.71569 8.71569 2.40973 9.09202 2.21799C9.51984 2 10.0799 2 11.2 2H18.8C19.9201 2 20.4802 2 20.908 2.21799C21.2843 2.40973 21.5903 2.71569 21.782 3.09202C22 3.51984 22 4.0799 22 5.2V12.8C22 13.9201 22 14.4802 21.782 14.908C21.5903 15.2843 21.2843 15.5903 20.908 15.782C20.4802 16 19.9201 16 18.8 16H16M5.2 22H12.8C13.9201 22 14.4802 22 14.908 21.782C15.2843 21.5903 15.5903 21.2843 15.782 20.908C16 20.4802 16 19.9201 16 18.8V11.2C16 10.0799 16 9.51984 15.782 9.09202C15.5903 8.71569 15.2843 8.40973 14.908 8.21799C14.4802 8 13.9201 8 12.8 8H5.2C4.0799 8 3.51984 8 3.09202 8.21799C2.71569 8.40973 2.40973 8.71569 2.21799 9.09202C2 9.51984 2 10.0799 2 11.2V18.8C2 19.9201 2 20.4802 2.21799 20.908C2.40973 21.2843 2.71569 21.5903 3.09202 21.782C3.51984 22 4.07989 22 5.2 22Z' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
9
+ 'delete': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16 6V5.2C16 4.0799 16 3.51984 15.782 3.09202C15.5903 2.71569 15.2843 2.40973 14.908 2.21799C14.4802 2 13.9201 2 12.8 2H11.2C10.0799 2 9.51984 2 9.09202 2.21799C8.71569 2.40973 8.40973 2.71569 8.21799 3.09202C8 3.51984 8 4.0799 8 5.2V6M10 11.5V16.5M14 11.5V16.5M3 6H21M19 6V17.2C19 18.8802 19 19.7202 18.673 20.362C18.3854 20.9265 17.9265 21.3854 17.362 21.673C16.7202 22 15.8802 22 14.2 22H9.8C8.11984 22 7.27976 22 6.63803 21.673C6.07354 21.3854 5.6146 20.9265 5.32698 20.362C5 19.7202 5 18.8802 5 17.2V6' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
10
+ 'close': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M18 6L6 18M6 6L18 18' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
11
+ 'menu': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 12H21M3 6H21M3 18H21' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
12
+ 'expand': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14 10L21 3M21 3H15M21 3V9M10 14L3 21M3 21H9M3 21V15' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
13
+ 'collapse': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 14H10V20M20 10H14V4' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
14
+ 'download': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M21 15V16.2C21 17.8802 21 18.7202 20.673 19.362C20.3854 19.9265 19.9265 20.3854 19.362 20.673C18.7202 21 17.8802 21 16.2 21H7.8C6.11984 21 5.27976 21 4.63803 20.673C4.07354 20.3854 3.6146 19.9265 3.32698 19.362C3 18.7202 3 17.8802 3 16.2V15M17 10L12 15M12 15L7 10M12 15V3' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
15
+ 'chevron-left': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15 18L9 12L15 6' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
16
+ 'chevron-right': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9 18L15 12L9 6' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
17
+ 'link': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.17 14.83L14.83 9.17M7.05 11.29L4.22 14.12C2.59 15.75 2.59 18.42 4.22 20.05V20.05C5.85 21.68 8.52 21.68 10.15 20.05L12.98 17.22M11.02 6.78L13.85 3.95C15.48 2.32 18.15 2.32 19.78 3.95V3.95C21.41 5.58 21.41 8.25 19.78 9.88L16.95 12.71' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
18
+ 'refresh': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M21 10C21 10 18.995 7.26822 17.3662 5.63824C15.7373 4.00827 13.4864 3 11 3C6.02944 3 2 7.02944 2 12C2 16.9706 6.02944 21 11 21C15.1031 21 18.5649 18.2543 19.6482 14.5M21 10V4M21 10H15' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
19
+ 'edit': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 3.99998H6.8C5.11984 3.99998 4.27976 3.99998 3.63803 4.32696C3.07354 4.61458 2.6146 5.07353 2.32698 5.63801C2 6.27975 2 7.11983 2 8.79998V17.2C2 18.8801 2 19.7202 2.32698 20.362C2.6146 20.9264 3.07354 21.3854 3.63803 21.673C4.27976 22 5.11984 22 6.8 22H15.2C16.8802 22 17.7202 22 18.362 21.673C18.9265 21.3854 19.3854 20.9264 19.673 20.362C20 19.7202 20 18.8801 20 17.2V13M7.99997 16H9.67452C10.1637 16 10.4083 16 10.6385 15.9447C10.8425 15.8957 11.0376 15.8149 11.2166 15.7053C11.4184 15.5816 11.5914 15.4086 11.9373 15.0627L21.5 5.49998C22.3284 4.67156 22.3284 3.32841 21.5 2.49998C20.6716 1.67156 19.3284 1.67155 18.5 2.49998L8.93723 12.0627C8.59133 12.4086 8.41838 12.5816 8.29469 12.7834C8.18504 12.9624 8.10423 13.1574 8.05523 13.3615C7.99997 13.5917 7.99997 13.8363 7.99997 14.3255V16Z' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
20
+ 'quote': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='currentColor' d='M9.983 3v7.391c0 5.704-3.731 9.57-8.983 10.609l-.995-2.151c2.432-.917 3.995-3.638 3.995-5.849h-4v-10h9.983zm14.017 0v7.391c0 5.704-3.748 9.571-9 10.609l-.996-2.151c2.433-.917 3.996-3.638 3.996-5.849h-3.983v-10h9.983z'/%3E%3C/svg%3E"),
21
+ 'qr': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='5' height='5' rx='1'/%3E%3Crect x='16' y='3' width='5' height='5' rx='1'/%3E%3Crect x='3' y='16' width='5' height='5' rx='1'/%3E%3Cpath d='M21 16h-3a2 2 0 0 0-2 2v3'/%3E%3Cpath d='M21 21v.01'/%3E%3Cpath d='M12 7v3a2 2 0 0 1-2 2H7'/%3E%3Cpath d='M3 12h.01'/%3E%3Cpath d='M12 3h.01'/%3E%3Cpath d='M12 16v.01'/%3E%3Cpath d='M16 12h1'/%3E%3Cpath d='M21 12v.01'/%3E%3Cpath d='M12 21v-1'/%3E%3C/g%3E%3C/svg%3E"),
22
+ 'logout': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16 17L21 12M21 12L16 7M21 12H9M9 3H7.8C6.11984 3 5.27976 3 4.63803 3.32698C4.07354 3.6146 3.6146 4.07354 3.32698 4.63803C3 5.27976 3 6.11984 3 7.8V16.2C3 17.8802 3 18.7202 3.32698 19.362C3.6146 19.9265 4.07354 20.3854 4.63803 20.673C5.27976 21 6.11984 21 7.8 21H9' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
23
+ 'user': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20 21C20 19.6044 20 18.9067 19.8278 18.3389C19.44 17.0605 18.4395 16.06 17.1611 15.6722C16.5933 15.5 15.8956 15.5 14.5 15.5H9.5C8.10444 15.5 7.40665 15.5 6.83886 15.6722C5.56045 16.06 4.56004 17.0605 4.17224 18.3389C4 18.9067 4 19.6044 4 21M16.5 7.5C16.5 9.98528 14.4853 12 12 12C9.51472 12 7.5 9.98528 7.5 7.5C7.5 5.01472 9.51472 3 12 3C14.4853 3 16.5 5.01472 16.5 7.5Z' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
24
+ 'settings': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M18.7273 14.7273C18.6063 15.0015 18.5702 15.3056 18.6236 15.6005C18.6771 15.8954 18.8177 16.1676 19.0273 16.3818L19.0818 16.4364C19.2509 16.6052 19.385 16.8057 19.4765 17.0265C19.568 17.2472 19.6151 17.4838 19.6151 17.7227C19.6151 17.9617 19.568 18.1983 19.4765 18.419C19.385 18.6397 19.2509 18.8402 19.0818 19.0091C18.913 19.1781 18.7124 19.3122 18.4917 19.4037C18.271 19.4952 18.0344 19.5423 17.7955 19.5423C17.5565 19.5423 17.3199 19.4952 17.0992 19.4037C16.8785 19.3122 16.678 19.1781 16.5091 19.0091L16.4545 18.9545C16.2403 18.745 15.9682 18.6044 15.6733 18.5509C15.3784 18.4974 15.0742 18.5335 14.8 18.6545C14.5311 18.7698 14.3018 18.9611 14.1403 19.205C13.9788 19.4489 13.8921 19.7347 13.8909 20.0273V20.1818C13.8909 20.664 13.6994 21.1265 13.3584 21.4675C13.0174 21.8084 12.5549 22 12.0727 22C11.5905 22 11.1281 21.8084 10.7871 21.4675C10.4461 21.1265 10.2545 20.664 10.2545 20.1818V20.1C10.2475 19.7991 10.1501 19.5073 9.97501 19.2625C9.79991 19.0176 9.55521 18.8312 9.27273 18.7273C8.99853 18.6063 8.69437 18.5702 8.39947 18.6236C8.10456 18.6771 7.83244 18.8177 7.61818 19.0273L7.56364 19.0818C7.39478 19.2509 7.19425 19.385 6.97353 19.4765C6.7528 19.568 6.51621 19.6151 6.27727 19.6151C6.03834 19.6151 5.80174 19.568 5.58102 19.4765C5.36029 19.385 5.15977 19.2509 4.99091 19.0818C4.82186 18.913 4.68775 18.7124 4.59626 18.4917C4.50476 18.271 4.45766 18.0344 4.45766 17.7955C4.45766 17.5565 4.50476 17.3199 4.59626 17.0992C4.68775 16.8785 4.82186 16.678 4.99091 16.5091L5.04545 16.4545C5.25503 16.2403 5.39562 15.9682 5.4491 15.6733C5.50257 15.3784 5.46647 15.0742 5.34545 14.8C5.23022 14.5311 5.03887 14.3018 4.79497 14.1403C4.55107 13.9788 4.26526 13.8921 3.97273 13.8909H3.81818C3.33597 13.8909 2.87351 13.6994 2.53253 13.3584C2.19156 13.0174 2 12.5549 2 12.0727C2 11.5905 2.19156 11.1281 2.53253 10.7871C2.87351 10.4461 3.33597 10.2545 3.81818 10.2545H3.9C4.2009 10.2475 4.49273 10.1501 4.73754 9.97501C4.98236 9.79991 5.16883 9.55521 5.27273 9.27273C5.39374 8.99853 5.42984 8.69437 5.37637 8.39947C5.3229 8.10456 5.18231 7.83244 4.97273 7.61818L4.91818 7.56364C4.74913 7.39478 4.61503 7.19425 4.52353 6.97353C4.43203 6.7528 4.38493 6.51621 4.38493 6.27727C4.38493 6.03834 4.43203 5.80174 4.52353 5.58102C4.61503 5.36029 4.74913 5.15977 4.91818 4.99091C5.08704 4.82186 5.28757 4.68775 5.50829 4.59626C5.72901 4.50476 5.96561 4.45766 6.20455 4.45766C6.44348 4.45766 6.68008 4.50476 6.9008 4.59626C7.12152 4.68775 7.32205 4.82186 7.49091 4.99091L7.54545 5.04545C7.75971 5.25503 8.03183 5.39562 8.32674 5.4491C8.62164 5.50257 8.9258 5.46647 9.2 5.34545H9.27273C9.54161 5.23022 9.77093 5.03887 9.93245 4.79497C10.094 4.55107 10.1807 4.26526 10.1818 3.97273V3.81818C10.1818 3.33597 10.3734 2.87351 10.7144 2.53253C11.0553 2.19156 11.5178 2 12 2C12.4822 2 12.9447 2.19156 13.2856 2.53253C13.6266 2.87351 13.8182 3.33597 13.8182 3.81818V3.9C13.8193 4.19253 13.906 4.47834 14.0676 4.72224C14.2291 4.96614 14.4584 5.15749 14.7273 5.27273C15.0015 5.39374 15.3056 5.42984 15.6005 5.37637C15.8954 5.3229 16.1676 5.18231 16.3818 4.97273L16.4364 4.91818C16.6052 4.74913 16.8057 4.61503 17.0265 4.52353C17.2472 4.43203 17.4838 4.38493 17.7227 4.38493C17.9617 4.38493 18.1983 4.43203 18.419 4.52353C18.6397 4.61503 18.8402 4.74913 19.0091 4.91818C19.1781 5.08704 19.3122 5.28757 19.4037 5.50829C19.4952 5.72901 19.5423 5.96561 19.5423 6.20455C19.5423 6.44348 19.4952 6.68008 19.4037 6.9008C19.3122 7.12152 19.1781 7.32205 19.0091 7.49091L18.9545 7.54545C18.745 7.75971 18.6044 8.03183 18.5509 8.32674C18.4974 8.62164 18.5335 8.9258 18.6545 9.2V9.27273C18.7698 9.54161 18.9611 9.77093 19.205 9.93245C19.4489 10.094 19.7347 10.1807 20.0273 10.1818H20.1818C20.664 10.1818 21.1265 10.3734 21.4675 10.7144C21.8084 11.0553 22 11.5178 22 12C22 12.4822 21.8084 12.9447 21.4675 13.2856C21.1265 13.6266 20.664 13.8182 20.1818 13.8182H20.1C19.8075 13.8193 19.5217 13.906 19.2778 14.0676C19.0339 14.2291 18.8425 14.4584 18.7273 14.7273Z' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
25
+ 'sun': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 2V4M12 20V22M4 12H2M6.31412 6.31412L4.8999 4.8999M17.6859 6.31412L19.1001 4.8999M6.31412 17.69L4.8999 19.1042M17.6859 17.69L19.1001 19.1042M22 12H20M17 12C17 14.7614 14.7614 17 12 17C9.23858 17 7 14.7614 7 12C7 9.23858 9.23858 7 12 7C14.7614 7 17 9.23858 17 12Z' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
26
+ 'moonstar': url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M18 2L18.6178 3.23558C18.8833 3.76656 19.016 4.03205 19.1934 4.26211C19.3507 4.46626 19.5337 4.64927 19.7379 4.80664C19.9679 4.98397 20.2334 5.11672 20.7644 5.38221L22 6L20.7644 6.61779C20.2334 6.88328 19.9679 7.01603 19.7379 7.19336C19.5337 7.35073 19.3507 7.53374 19.1934 7.73789C19.016 7.96795 18.8833 8.23344 18.6178 8.76442L18 10L17.3822 8.76442C17.1167 8.23344 16.984 7.96795 16.8066 7.73789C16.6493 7.53374 16.4663 7.35073 16.2621 7.19336C16.0321 7.01603 15.7666 6.88328 15.2356 6.61779L14 6L15.2356 5.38221C15.7666 5.11672 16.0321 4.98397 16.2621 4.80664C16.4663 4.64927 16.6493 4.46626 16.8066 4.26211C16.984 4.03205 17.1167 3.76656 17.3822 3.23558L18 2Z' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M21 13.3893C19.689 15.689 17.2145 17.2395 14.3779 17.2395C10.1711 17.2395 6.76075 13.8292 6.76075 9.62233C6.76075 6.78554 8.31149 4.31094 10.6115 3C5.77979 3.45812 2 7.52692 2 12.4785C2 17.7371 6.26292 22 11.5215 22C16.4729 22 20.5415 18.2206 21 13.3893Z' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")
27
+ );
28
+
29
+ @function icon-svg($name) {
30
+ @return lookup($icons, $name, 'icon');
31
+ }
32
+
33
+ // Extension point: apps register icons beyond the built-in set by wrapping this
34
+ // mixin with their own data-URI SVGs, e.g.
35
+ // @mixin i-foo { @include kit.icon-mask(url('data:image/svg+xml,...')); }
36
+ @mixin icon-mask($svg) {
37
+ mask-image: $svg;
38
+ }
39
+
40
+ @mixin i($name) {
41
+ @include icon-mask(icon-svg($name));
42
+ }
43
+
44
+ @mixin icon-base {
45
+ border: 0;
46
+ display: inline-block;
47
+ mask-repeat: no-repeat;
48
+ background-color: var(--font-color);
49
+ background-image: none;
50
+ }
51
+
52
+ @mixin icon {
53
+ mask-position: center center;
54
+ width: var(--icon-size);
55
+ height: var(--icon-size);
56
+ @include icon-base;
57
+ }
package/_index.scss ADDED
@@ -0,0 +1,5 @@
1
+ @forward 'breakpoints';
2
+ @forward 'z';
3
+ @forward 'icons';
4
+ @forward 'font-face';
5
+ @forward 'mixins';
package/_lookup.scss ADDED
@@ -0,0 +1,9 @@
1
+ @use 'sass:map';
2
+
3
+ // Internal: map lookup erring hard on unknown keys. Not forwarded from the package index.
4
+ @function lookup($map, $key, $kind) {
5
+ @if not map.has-key($map, $key) {
6
+ @error 'Unknown #{$kind} "#{$key}". Known: #{map.keys($map)}';
7
+ }
8
+ @return map.get($map, $key);
9
+ }
package/_mixins.scss ADDED
@@ -0,0 +1,95 @@
1
+ @mixin monospace {
2
+ font-family: monospace, monospace;
3
+ }
4
+
5
+ @mixin disabled {
6
+ pointer-events: none;
7
+ opacity: 0.5;
8
+ cursor: default;
9
+ user-select: none;
10
+ }
11
+
12
+ @mixin apply-props($props) {
13
+ @each $prop, $value in $props {
14
+ #{$prop}: #{$value};
15
+ }
16
+ }
17
+
18
+ // In-control affordances shared by select-shaped controls (SelectBox, app
19
+ // picker triggers) so their anatomy cannot drift apart.
20
+
21
+ // The × clear button rendered inside a control, before the chevron. Its glyph
22
+ // size rides one custom property so every control including this mixin tracks
23
+ // the same value.
24
+ //
25
+ // The box is held at the WCAG 2.2 target-size minimum (24×24, SC 2.5.8) while
26
+ // the glyph keeps its own size — a bare × sized to its font is a ~12px target
27
+ // that axe flags as a serious violation.
28
+ //
29
+ // The negative block margin lets that 24px box grow into the control's own
30
+ // padding instead of stretching the control: a populated control must stay the
31
+ // same height as an empty one beside it (--input-height, one text line plus
32
+ // padding). The box still sits entirely within the control's border, so
33
+ // nothing overflows visually.
34
+ @mixin control-clear {
35
+ flex-shrink: 0;
36
+ display: inline-flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ min-width: 24px;
40
+ min-height: 24px;
41
+ margin-block: calc((var(--line-height-tight, 18px) - 24px) / 2);
42
+ border: none;
43
+ background: transparent;
44
+ padding: 0;
45
+ color: inherit;
46
+ font-size: var(--control-clear-font-size, 18px);
47
+ line-height: 1;
48
+ opacity: 0.5;
49
+ &:hover {
50
+ opacity: 1;
51
+ }
52
+ }
53
+
54
+ // Dropdown-trigger / drill chevron built with borders — no icon dependency.
55
+ // The margin re-centres the glyph optically: the arrow's visual mass sits in
56
+ // the half of the rotated box it points from.
57
+ @mixin control-chevron($point: down) {
58
+ flex-shrink: 0;
59
+ width: 7px;
60
+ height: 7px;
61
+ border-right: 2px solid currentColor;
62
+ border-bottom: 2px solid currentColor;
63
+ opacity: 0.6;
64
+ @if $point == right {
65
+ margin: 0 3px 0 2px;
66
+ transform: rotate(-45deg);
67
+ } @else {
68
+ margin: 0 2px 3px;
69
+ transform: rotate(45deg);
70
+ }
71
+ }
72
+
73
+ // Requires the global `spinner` keyframes and the --spinner-color/--spinner-side-color
74
+ // custom properties (@greendrake/theme provides the keyframes; palettes provide the colors).
75
+ @mixin img-spinner($size: 24px) {
76
+ position: relative;
77
+ &::before {
78
+ content: '';
79
+ box-sizing: border-box;
80
+ position: absolute;
81
+ top: 50%;
82
+ left: 50%;
83
+ width: $size;
84
+ height: $size;
85
+ margin-top: calc($size / -2);
86
+ margin-left: calc($size / -2);
87
+ border-radius: 50%;
88
+ border: 2px solid var(--spinner-color);
89
+ border-top-color: var(--spinner-side-color);
90
+ animation: spinner 0.6s linear infinite;
91
+ }
92
+ img {
93
+ position: relative;
94
+ }
95
+ }
package/_z.scss ADDED
@@ -0,0 +1,15 @@
1
+ @use 'lookup' as *;
2
+
3
+ $z-layers: (
4
+ 'below': -1,
5
+ 'base': 0,
6
+ 'raised': 1,
7
+ 'spinner': 10,
8
+ 'sticky': 50,
9
+ 'modal': 100,
10
+ 'toast': 200
11
+ );
12
+
13
+ @function z($name) {
14
+ @return lookup($z-layers, $name, 'z-layer');
15
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@greendrake/scss-kit",
3
+ "version": "0.1.0",
4
+ "description": "Framework-free SCSS primitives: breakpoints (single-sourced with TS via codegen), z-layer map, icon-mask registry, @font-face generator, misc mixins",
5
+ "license": "MIT",
6
+ "author": "Eugene Greendrake <eugene@greendrake.nz>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/greendrake/lib.git",
10
+ "directory": "scss-kit"
11
+ },
12
+ "homepage": "https://github.com/greendrake/lib/tree/main/scss-kit#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/greendrake/lib/issues"
15
+ },
16
+ "type": "module",
17
+ "main": "_index.scss",
18
+ "exports": {
19
+ ".": {
20
+ "sass": "./_index.scss",
21
+ "default": "./_index.scss"
22
+ },
23
+ "./breakpoints": {
24
+ "types": "./src/breakpoints.ts",
25
+ "default": "./src/breakpoints.ts"
26
+ }
27
+ },
28
+ "files": [
29
+ "_index.scss",
30
+ "_breakpoints.scss",
31
+ "_lookup.scss",
32
+ "_z.scss",
33
+ "_icons.scss",
34
+ "_font-face.scss",
35
+ "_mixins.scss",
36
+ "src"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "scripts": {
42
+ "generate": "bun scripts/generate.ts",
43
+ "check": "tsc --noEmit"
44
+ },
45
+ "prettier": "@greendrake/dev-config/prettier",
46
+ "devDependencies": {
47
+ "@greendrake/dev-config": "0.1.0",
48
+ "@types/bun": "^1.3.5",
49
+ "typescript": "6.0.3"
50
+ }
51
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "tooNarrow": 320,
3
+ "narrow": 600
4
+ }
@@ -0,0 +1,15 @@
1
+ // GENERATED from src/breakpoints.json by scripts/generate.ts — do not edit, regenerate via `bun run generate`.
2
+ // A breakpoint value is the inclusive upper bound of the viewport range below it:
3
+ // "below" matches width <= value; "above" matches width >= value + 1px.
4
+ // narrow (600): the content/wide-screen boundary — a viewport is "wide" from 601px up.
5
+
6
+ export const breakpoints = {
7
+ tooNarrow: 320,
8
+ narrow: 600
9
+ } as const
10
+
11
+ export type BreakpointName = keyof typeof breakpoints
12
+
13
+ // Media-query strings with the same semantics as the SCSS respond-above/respond-below mixins
14
+ export const mediaAbove = (name: BreakpointName): string => `(min-width: ${breakpoints[name] + 1}px)`
15
+ export const mediaBelow = (name: BreakpointName): string => `(max-width: ${breakpoints[name]}px)`