@greese/ui 1.1.0 → 1.2.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/README.md +19 -14
- package/dist/_maps.scss +1 -1
- package/dist/_tokens-base.scss +30 -6
- package/dist/_tokens.scss +31 -2
- package/dist/_utilities-table.scss +1615 -0
- package/dist/clipboard.d.ts +18 -0
- package/dist/clipboard.js +62 -0
- package/dist/css/Button.css +2 -2
- package/dist/css/Carousel.css +145 -47
- package/dist/css/Checkbox.css +2 -2
- package/dist/css/Chip.css +38 -8
- package/dist/css/ChoiceGroup.css +42 -1
- package/dist/css/Collapsible.css +1 -0
- package/dist/css/ColorPicker.css +84 -0
- package/dist/css/Combobox.css +2 -2
- package/dist/css/Dialog.css +24 -22
- package/dist/css/EditableField.css +219 -0
- package/dist/css/Input.css +11 -2
- package/dist/css/Menu.css +9 -0
- package/dist/css/NumberField.css +53 -8
- package/dist/css/NumberStepper.css +9 -0
- package/dist/css/OtpField.css +1 -1
- package/dist/css/Radio.css +2 -2
- package/dist/css/ScrollCarousel.css +225 -0
- package/dist/css/SegmentedControl.css +2 -2
- package/dist/css/Select.css +3 -2
- package/dist/css/Slider.css +45 -5
- package/dist/css/Switch.css +6 -6
- package/dist/css/Text.css +14 -14
- package/dist/css/Textarea.css +1 -1
- package/dist/css/core.css +36 -6
- package/dist/css/resets.css +162 -0
- package/dist/css.d.ts +2 -0
- package/dist/css.js +2 -0
- package/dist/greese.css +978 -138
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/scss/components/_Accordion.scss +2 -0
- package/dist/scss/components/_ActionSelect.scss +2 -0
- package/dist/scss/components/_Alert.scss +2 -0
- package/dist/scss/components/_AppBar.scss +2 -0
- package/dist/scss/components/_AspectRatio.scss +2 -0
- package/dist/scss/components/_AutoLayout.scss +2 -0
- package/dist/scss/components/_Avatar.scss +2 -0
- package/dist/scss/components/_Badge.scss +2 -0
- package/dist/scss/components/_Box.scss +2 -0
- package/dist/scss/components/_Breadcrumbs.scss +2 -0
- package/dist/scss/components/_Button.scss +2 -0
- package/dist/scss/components/_ButtonGroup.scss +2 -0
- package/dist/scss/components/_Card.scss +2 -0
- package/dist/scss/components/_Carousel.scss +2 -0
- package/dist/scss/components/_Center.scss +2 -0
- package/dist/scss/components/_ChatBubble.scss +2 -0
- package/dist/scss/components/_Checkbox.scss +2 -0
- package/dist/scss/components/_Chip.scss +2 -0
- package/dist/scss/components/_ChoiceGroup.scss +2 -0
- package/dist/scss/components/_Code.scss +2 -0
- package/dist/scss/components/_Collapsible.scss +2 -0
- package/dist/scss/components/_ColorPicker.scss +2 -0
- package/dist/scss/components/_Combobox.scss +2 -0
- package/dist/scss/components/_Container.scss +2 -0
- package/dist/scss/components/_Countdown.scss +2 -0
- package/dist/scss/components/_DateField.scss +2 -0
- package/dist/scss/components/_Dialog.scss +2 -0
- package/dist/scss/components/_Diff.scss +2 -0
- package/dist/scss/components/_Divider.scss +2 -0
- package/dist/scss/components/_Dock.scss +2 -0
- package/dist/scss/components/_EditableField.scss +2 -0
- package/dist/scss/components/_Field.scss +2 -0
- package/dist/scss/components/_Fieldset.scss +2 -0
- package/dist/scss/components/_FileInput.scss +2 -0
- package/dist/scss/components/_Footer.scss +2 -0
- package/dist/scss/components/_Grid.scss +2 -0
- package/dist/scss/components/_Header.scss +2 -0
- package/dist/scss/components/_Heading.scss +2 -0
- package/dist/scss/components/_Icon.scss +2 -0
- package/dist/scss/components/_IconButton.scss +2 -0
- package/dist/scss/components/_Image.scss +2 -0
- package/dist/scss/components/_Indicator.scss +2 -0
- package/dist/scss/components/_Input.scss +2 -0
- package/dist/scss/components/_Key.scss +2 -0
- package/dist/scss/components/_Link.scss +2 -0
- package/dist/scss/components/_List.scss +2 -0
- package/dist/scss/components/_Media.scss +2 -0
- package/dist/scss/components/_Menu.scss +2 -0
- package/dist/scss/components/_Motion.scss +2 -0
- package/dist/scss/components/_NumberField.scss +2 -0
- package/dist/scss/components/_NumberStepper.scss +2 -0
- package/dist/scss/components/_OtpField.scss +2 -0
- package/dist/scss/components/_PageTransition.scss +2 -0
- package/dist/scss/components/_Pagination.scss +2 -0
- package/dist/scss/components/_Popover.scss +2 -0
- package/dist/scss/components/_Progress.scss +2 -0
- package/dist/scss/components/_RadialProgress.scss +2 -0
- package/dist/scss/components/_Radio.scss +2 -0
- package/dist/scss/components/_Rating.scss +2 -0
- package/dist/scss/components/_ScrollCarousel.scss +2 -0
- package/dist/scss/components/_ScrollSpy.scss +2 -0
- package/dist/scss/components/_SegmentedControl.scss +2 -0
- package/dist/scss/components/_Select.scss +2 -0
- package/dist/scss/components/_Sidebar.scss +2 -0
- package/dist/scss/components/_Skeleton.scss +2 -0
- package/dist/scss/components/_Slider.scss +2 -0
- package/dist/scss/components/_Spacer.scss +2 -0
- package/dist/scss/components/_SpeedDial.scss +2 -0
- package/dist/scss/components/_Spinner.scss +2 -0
- package/dist/scss/components/_Stack.scss +2 -0
- package/dist/scss/components/_Stat.scss +2 -0
- package/dist/scss/components/_Status.scss +2 -0
- package/dist/scss/components/_Steps.scss +2 -0
- package/dist/scss/components/_Swap.scss +2 -0
- package/dist/scss/components/_Switch.scss +2 -0
- package/dist/scss/components/_Table.scss +2 -0
- package/dist/scss/components/_Tabs.scss +2 -0
- package/dist/scss/components/_Text.scss +2 -0
- package/dist/scss/components/_TextRotate.scss +2 -0
- package/dist/scss/components/_Textarea.scss +2 -0
- package/dist/scss/components/_ThemeProvider.scss +2 -0
- package/dist/scss/components/_Tile.scss +2 -0
- package/dist/scss/components/_Toast.scss +2 -0
- package/dist/scss/components/_ToggleButton.scss +2 -0
- package/dist/scss/components/_Tooltip.scss +2 -0
- package/dist/scss/components/_Type.scss +2 -0
- package/dist/scss/components/_Visibility.scss +2 -0
- package/dist/scss/components/_VisuallyHidden.scss +2 -0
- package/dist/shapes.d.ts +12 -1
- package/dist/shapes.js +11 -10
- package/dist/style-manifest.json +18830 -132
- package/dist/theme.js +6 -1
- package/dist/themeFiles/index.d.ts +9 -0
- package/dist/themeFiles/index.js +9 -0
- package/dist/themeFiles/scssModule.d.ts +100 -0
- package/dist/themeFiles/scssModule.js +384 -0
- package/dist/themeFiles/spec.d.ts +198 -0
- package/dist/themeFiles/spec.js +877 -0
- package/dist/tokens.css +30 -1
- package/dist/typography.js +1 -1
- package/dist/utilities.css +435 -0
- package/dist/utilities.d.ts +8 -0
- package/dist/utilities.js +75 -0
- package/package.json +20 -1
- package/src/components/AutoLayout/AutoLayout.scss +2 -26
- package/src/components/Carousel/Carousel.scss +122 -126
- package/src/components/Checkbox/Checkbox.scss +2 -2
- package/src/components/Chip/Chip.scss +63 -8
- package/src/components/ChoiceGroup/ChoiceGroup.scss +19 -1
- package/src/components/Collapsible/Collapsible.scss +3 -0
- package/src/components/ColorPicker/ColorPicker.scss +95 -0
- package/src/components/Combobox/Combobox.scss +4 -2
- package/src/components/Dialog/Dialog.scss +24 -7
- package/src/components/EditableField/EditableField.scss +275 -0
- package/src/components/Grid/Grid.scss +2 -70
- package/src/components/Input/Input.scss +29 -0
- package/src/components/Menu/Menu.scss +18 -0
- package/src/components/NumberField/NumberField.scss +56 -9
- package/src/components/NumberStepper/NumberStepper.scss +13 -0
- package/src/components/Radio/Radio.scss +2 -2
- package/src/components/ScrollCarousel/ScrollCarousel.scss +142 -0
- package/src/components/Select/Select.scss +4 -0
- package/src/components/Slider/Slider.scss +51 -6
- package/src/components/Stack/Stack.scss +2 -48
- package/src/components/Switch/Switch.scss +9 -6
- package/src/components/Text/Text.scss +5 -2
- package/src/styles/_breakpoint-vars.scss +17 -11
- package/src/styles/_controls.scss +2 -2
- package/src/styles/_core.scss +1 -0
- package/src/styles/_kit.scss +483 -56
- package/src/styles/_layout-props.scss +197 -0
- package/src/styles/_manifest.scss +53 -23
- package/src/styles/_mixins.scss +53 -15
- package/src/styles/_slides.scss +112 -0
- package/src/styles/_utilities.scss +104 -0
- package/src/styles/breakpoints.test.ts +21 -1
- package/src/styles/entryPoints.test.ts +25 -2
- package/src/styles/index.scss +3 -1
- package/src/styles/kit.test.ts +171 -1
- package/src/styles/public/_breakpoints.scss +43 -0
- package/src/styles/public/_slides.scss +3 -0
- package/src/styles/public/_utilities.scss +3 -0
- package/src/styles/resets.scss +7 -0
- package/src/styles/utilities.test.ts +39 -0
- package/themes/bevel.css +187 -187
- package/themes/bevel.scss +960 -958
- package/themes/extrude.css +42 -42
- package/themes/folio.css +200 -200
- package/themes/folio.scss +880 -878
- package/themes/outline.css +18 -18
- package/themes/pebble.css +8 -8
- package/themes/ridge.css +188 -188
- package/themes/ridge.scss +820 -818
- package/themes/soft.css +11 -11
- package/themes/ticket.css +180 -180
- package/themes/ticket.scss +881 -879
- package/themes/tilt.css +25 -25
package/dist/theme.js
CHANGED
|
@@ -141,10 +141,15 @@ const addExtended = (extend, base, light, dark) => {
|
|
|
141
141
|
const themeCss = (name, base, light, dark, media) => {
|
|
142
142
|
const T = `[${THEME_ATTR}='${name}']`;
|
|
143
143
|
const S = (scheme) => `[${SCHEME_ATTR}='${scheme}']`;
|
|
144
|
+
// `body` reads --greese-font-body from :root, so a theme scoped to a wrapper
|
|
145
|
+
// below <body> would set the variable but never reach any text: text inherits
|
|
146
|
+
// font-family from <body>, not from the variable. A theme scope therefore
|
|
147
|
+
// applies its own body font.
|
|
148
|
+
const scopeFont = [['font-family', 'var(--greese-font-body)']];
|
|
144
149
|
return `${LAYER_ORDER}
|
|
145
150
|
@layer greese.theme {
|
|
146
151
|
${T} {
|
|
147
|
-
${decls([...base, ...light], ' ')}
|
|
152
|
+
${decls([...base, ...light, ...scopeFont], ' ')}
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
@media (prefers-color-scheme: dark) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@greese/ui/theme-files`: a theme as data (`ThemeSpec`, the JSON Theme
|
|
3
|
+
* Studio saves) and the SCSS it becomes, for any tool that makes or keeps
|
|
4
|
+
* themes. `specToLayout()` writes the documented layout, a folder of SCSS
|
|
5
|
+
* modules to keep in your source (ADR 0034); `specToScss()` one file. Pure,
|
|
6
|
+
* with no dependencies: Node and the browser both run it.
|
|
7
|
+
*/
|
|
8
|
+
export * from './spec.ts';
|
|
9
|
+
export * from './scssModule.ts';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@greese/ui/theme-files`: a theme as data (`ThemeSpec`, the JSON Theme
|
|
3
|
+
* Studio saves) and the SCSS it becomes, for any tool that makes or keeps
|
|
4
|
+
* themes. `specToLayout()` writes the documented layout, a folder of SCSS
|
|
5
|
+
* modules to keep in your source (ADR 0034); `specToScss()` one file. Pure,
|
|
6
|
+
* with no dependencies: Node and the browser both run it.
|
|
7
|
+
*/
|
|
8
|
+
export * from "./spec.js";
|
|
9
|
+
export * from "./scssModule.js";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A component's SCSS in a theme (ADR 0021): leading `@use` statements, then a
|
|
3
|
+
* body written inside `component(Name) { }`. The Studio splits the two, checks
|
|
4
|
+
* the imports, rewrites a library path copied from a component's source to its
|
|
5
|
+
* `pkg:` form, and merges every component's imports into one list at the top of
|
|
6
|
+
* the theme file. Pure.
|
|
7
|
+
*/
|
|
8
|
+
/** The `@greese/ui` SCSS entry points a component's SCSS may `@use`: the ones that write no CSS. */
|
|
9
|
+
export declare const AUTHORING_ENTRIES: readonly ["kit", "config", "maps", "mixins", "tones", "floating", "controls", "slides", "utilities"];
|
|
10
|
+
export interface ScssUse {
|
|
11
|
+
url: string;
|
|
12
|
+
/** `map` for `@use 'sass:map'`, the name after `as`, or `*`. */
|
|
13
|
+
namespace: string;
|
|
14
|
+
/** Its line in the component's text, from 1. */
|
|
15
|
+
line: number;
|
|
16
|
+
}
|
|
17
|
+
/** A top-level `$variable`, `@mixin` or `@function` in the body, which Sass allows only at a file's top level. */
|
|
18
|
+
export interface ScssDeclaration {
|
|
19
|
+
kind: 'variable' | 'mixin' | 'function';
|
|
20
|
+
name: string;
|
|
21
|
+
text: string;
|
|
22
|
+
/** Its first line in the component's text, from 1. */
|
|
23
|
+
line: number;
|
|
24
|
+
}
|
|
25
|
+
export interface SplitScss {
|
|
26
|
+
uses: ScssUse[];
|
|
27
|
+
/** Hoisted to the theme file's top level, after the imports. */
|
|
28
|
+
declarations: ScssDeclaration[];
|
|
29
|
+
/** The rest, declarations blanked out line for line, so its lines still match the box's. */
|
|
30
|
+
body: string;
|
|
31
|
+
/** The line of the component's text the body starts on, from 1. */
|
|
32
|
+
bodyLine: number;
|
|
33
|
+
problems: string[];
|
|
34
|
+
}
|
|
35
|
+
/** The namespace Sass gives a `@use` without `as`: the URL's last segment, without `_` or extension. */
|
|
36
|
+
export declare const defaultNamespace: (url: string) => string;
|
|
37
|
+
/**
|
|
38
|
+
* A path as a component's own stylesheet writes it, as the theme must:
|
|
39
|
+
* `../../styles/mixins` → `pkg:@greese/ui/scss/mixins`, `../../../dist/maps` →
|
|
40
|
+
* `pkg:@greese/ui/scss/maps`. Undefined for anything else.
|
|
41
|
+
*/
|
|
42
|
+
export declare const libraryUrl: (url: string) => string | undefined;
|
|
43
|
+
/** Splits the leading `@use` statements (with the comments and blank lines between them) from the body. */
|
|
44
|
+
export declare const splitScss: (text: string) => SplitScss;
|
|
45
|
+
/** The body's top-level items (rules, at-rules, declarations), as offsets into it, with comments and space between left out. */
|
|
46
|
+
export declare const topLevelItems: (text: string) => Array<{
|
|
47
|
+
start: number;
|
|
48
|
+
end: number;
|
|
49
|
+
}>;
|
|
50
|
+
/** Declarations two components both make: Sass would let the later one replace the other's silently. */
|
|
51
|
+
export declare const declarationProblems: (components: ReadonlyArray<[name: string, declarations: readonly ScssDeclaration[]]>) => string[];
|
|
52
|
+
/** Everything wrong with a component's SCSS: its imports, and a body whose braces don't balance or that imports. */
|
|
53
|
+
export declare const scssProblems: (text: string) => string[];
|
|
54
|
+
/**
|
|
55
|
+
* The text with each library path a component's source uses rewritten to its
|
|
56
|
+
* `pkg:` form, so pasted source compiles in a project. Returns what changed.
|
|
57
|
+
*/
|
|
58
|
+
export declare const rewriteLibraryUrls: (text: string) => {
|
|
59
|
+
text: string;
|
|
60
|
+
rewritten: string[];
|
|
61
|
+
};
|
|
62
|
+
/** A `@use` the theme file writes itself (the kit, the breakpoints), which a component's own can't contradict. */
|
|
63
|
+
export interface ReservedUse {
|
|
64
|
+
url: string;
|
|
65
|
+
namespace: string;
|
|
66
|
+
}
|
|
67
|
+
export interface MergedUses {
|
|
68
|
+
/** The statements, once each, in the order first written. */
|
|
69
|
+
lines: Array<{
|
|
70
|
+
text: string;
|
|
71
|
+
component: string;
|
|
72
|
+
line: number;
|
|
73
|
+
}>;
|
|
74
|
+
problems: string[];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Every component's imports as one list for the top of the theme file. Sass
|
|
78
|
+
* refuses one namespace for two statements, even the same one twice, so an
|
|
79
|
+
* identical import (or one the theme already writes) is kept once, and two
|
|
80
|
+
* imports that claim one namespace are an error naming both components.
|
|
81
|
+
*/
|
|
82
|
+
export declare const mergeUses: (components: ReadonlyArray<[name: string, uses: readonly ScssUse[]]>, reserved: readonly ReservedUse[]) => MergedUses;
|
|
83
|
+
/**
|
|
84
|
+
* Where a compile error points in the file that was compiled, from 1: the
|
|
85
|
+
* innermost frame of Sass's stack that is in that file (the root stylesheet's),
|
|
86
|
+
* so an error inside a mixin call in a component's body points at the call.
|
|
87
|
+
*/
|
|
88
|
+
export declare const errorLine: (error: unknown) => number | undefined;
|
|
89
|
+
/** A compile error in a component's SCSS: which box, which line, and what Sass said. */
|
|
90
|
+
export interface ErrorAt {
|
|
91
|
+
component: string;
|
|
92
|
+
line: number;
|
|
93
|
+
message: string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The text with `url` imported or not, for the imports menu. Adding writes
|
|
97
|
+
* `@use` after the last import (the kit as `greese`, as the theme file names
|
|
98
|
+
* it); removing deletes every `@use` of it. The rest is untouched.
|
|
99
|
+
*/
|
|
100
|
+
export declare const toggleImport: (text: string, url: string, on: boolean) => string;
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A component's SCSS in a theme (ADR 0021): leading `@use` statements, then a
|
|
3
|
+
* body written inside `component(Name) { }`. The Studio splits the two, checks
|
|
4
|
+
* the imports, rewrites a library path copied from a component's source to its
|
|
5
|
+
* `pkg:` form, and merges every component's imports into one list at the top of
|
|
6
|
+
* the theme file. Pure.
|
|
7
|
+
*/
|
|
8
|
+
/** The `@greese/ui` SCSS entry points a component's SCSS may `@use`: the ones that write no CSS. */
|
|
9
|
+
export const AUTHORING_ENTRIES = [
|
|
10
|
+
'kit',
|
|
11
|
+
'config',
|
|
12
|
+
'maps',
|
|
13
|
+
'mixins',
|
|
14
|
+
'tones',
|
|
15
|
+
'floating',
|
|
16
|
+
'controls',
|
|
17
|
+
'slides',
|
|
18
|
+
'utilities',
|
|
19
|
+
];
|
|
20
|
+
const PKG = 'pkg:@greese/ui/scss/';
|
|
21
|
+
/** The namespace Sass gives a `@use` without `as`: the URL's last segment, without `_` or extension. */
|
|
22
|
+
export const defaultNamespace = (url) => {
|
|
23
|
+
const last = url.split(/[/:]/).pop() ?? url;
|
|
24
|
+
return last.replace(/^_/, '').replace(/\.s[ac]ss$/, '');
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* A path as a component's own stylesheet writes it, as the theme must:
|
|
28
|
+
* `../../styles/mixins` → `pkg:@greese/ui/scss/mixins`, `../../../dist/maps` →
|
|
29
|
+
* `pkg:@greese/ui/scss/maps`. Undefined for anything else.
|
|
30
|
+
*/
|
|
31
|
+
export const libraryUrl = (url) => {
|
|
32
|
+
const partial = /^(?:\.\.\/)+styles\/_?([\w-]+)$/.exec(url)?.[1];
|
|
33
|
+
if (partial && AUTHORING_ENTRIES.includes(partial))
|
|
34
|
+
return `${PKG}${partial}`;
|
|
35
|
+
const generated = /^(?:\.\.\/)+dist\/_?(maps)$/.exec(url)?.[1];
|
|
36
|
+
return generated ? `${PKG}${generated}` : undefined;
|
|
37
|
+
};
|
|
38
|
+
const USE = /^@use\s+(['"])([^'"]+)\1\s*(?:as\s+(\*|[\w-]+))?\s*(with\b[\s\S]*)?$/;
|
|
39
|
+
/** The end of a statement: the next `;` outside quotes and parentheses. */
|
|
40
|
+
const statementEnd = (text, from) => {
|
|
41
|
+
let depth = 0;
|
|
42
|
+
let quote = '';
|
|
43
|
+
for (let i = from; i < text.length; i++) {
|
|
44
|
+
const c = text[i];
|
|
45
|
+
if (quote) {
|
|
46
|
+
if (c === quote)
|
|
47
|
+
quote = '';
|
|
48
|
+
}
|
|
49
|
+
else if (c === '"' || c === "'")
|
|
50
|
+
quote = c;
|
|
51
|
+
else if (c === '(')
|
|
52
|
+
depth++;
|
|
53
|
+
else if (c === ')')
|
|
54
|
+
depth--;
|
|
55
|
+
else if (c === ';' && depth <= 0)
|
|
56
|
+
return i;
|
|
57
|
+
}
|
|
58
|
+
return text.length;
|
|
59
|
+
};
|
|
60
|
+
const lineAt = (text, index) => text.slice(0, index).split('\n').length;
|
|
61
|
+
/** Splits the leading `@use` statements (with the comments and blank lines between them) from the body. */
|
|
62
|
+
export const splitScss = (text) => {
|
|
63
|
+
const uses = [];
|
|
64
|
+
const problems = [];
|
|
65
|
+
let i = 0;
|
|
66
|
+
for (;;) {
|
|
67
|
+
const rest = text.slice(i);
|
|
68
|
+
const space = /^\s+/.exec(rest)?.[0] ?? '';
|
|
69
|
+
if (space) {
|
|
70
|
+
i += space.length;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (rest.startsWith('//')) {
|
|
74
|
+
const end = text.indexOf('\n', i);
|
|
75
|
+
i = end < 0 ? text.length : end + 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (rest.startsWith('/*')) {
|
|
79
|
+
const end = text.indexOf('*/', i + 2);
|
|
80
|
+
i = end < 0 ? text.length : end + 2;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const rule = /^@(use|forward|import)\b/.exec(rest)?.[1];
|
|
84
|
+
if (!rule)
|
|
85
|
+
break;
|
|
86
|
+
const end = statementEnd(text, i);
|
|
87
|
+
const statement = text.slice(i, end).trim();
|
|
88
|
+
const line = lineAt(text, i);
|
|
89
|
+
if (rule !== 'use')
|
|
90
|
+
problems.push(`line ${line}: @${rule} is not allowed; use @use`);
|
|
91
|
+
else {
|
|
92
|
+
const match = USE.exec(statement);
|
|
93
|
+
if (!match)
|
|
94
|
+
problems.push(`line ${line}: "${statement}" is not a @use this box can read`);
|
|
95
|
+
else {
|
|
96
|
+
const [, , url, namespace, configured] = match;
|
|
97
|
+
if (configured)
|
|
98
|
+
problems.push(`line ${line}: @use … with (…) is not allowed here; the theme's breakpoints configure the library`);
|
|
99
|
+
uses.push({ url: url, namespace: namespace ?? defaultNamespace(url), line });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
i = end + 1;
|
|
103
|
+
}
|
|
104
|
+
const bodyLine = lineAt(text, i);
|
|
105
|
+
const { body, declarations } = hoist(text.slice(i), bodyLine);
|
|
106
|
+
return { uses, declarations, body, bodyLine, problems };
|
|
107
|
+
};
|
|
108
|
+
/** Past a string or comment starting at `i`, or `i` itself. */
|
|
109
|
+
const skipLiteral = (text, i) => {
|
|
110
|
+
const c = text[i];
|
|
111
|
+
if (c === '"' || c === "'") {
|
|
112
|
+
for (let j = i + 1; j < text.length; j++) {
|
|
113
|
+
if (text[j] === '\\')
|
|
114
|
+
j++;
|
|
115
|
+
else if (text[j] === c)
|
|
116
|
+
return j + 1;
|
|
117
|
+
}
|
|
118
|
+
return text.length;
|
|
119
|
+
}
|
|
120
|
+
if (text.startsWith('//', i)) {
|
|
121
|
+
const end = text.indexOf('\n', i);
|
|
122
|
+
return end < 0 ? text.length : end;
|
|
123
|
+
}
|
|
124
|
+
if (text.startsWith('/*', i)) {
|
|
125
|
+
const end = text.indexOf('*/', i + 2);
|
|
126
|
+
return end < 0 ? text.length : end + 2;
|
|
127
|
+
}
|
|
128
|
+
return i;
|
|
129
|
+
};
|
|
130
|
+
/** The end (exclusive) of the top-level item starting at `i`: a `;` statement, or a `{ … }` block. */
|
|
131
|
+
const itemEnd = (text, i) => {
|
|
132
|
+
let depth = 0;
|
|
133
|
+
for (let j = i; j < text.length;) {
|
|
134
|
+
const past = skipLiteral(text, j);
|
|
135
|
+
if (past !== j) {
|
|
136
|
+
j = past;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const c = text[j];
|
|
140
|
+
if (c === '(' || c === '[')
|
|
141
|
+
depth++;
|
|
142
|
+
else if (c === ')' || c === ']')
|
|
143
|
+
depth--;
|
|
144
|
+
else if (c === '{')
|
|
145
|
+
depth++;
|
|
146
|
+
else if (c === '}') {
|
|
147
|
+
depth--;
|
|
148
|
+
if (depth <= 0)
|
|
149
|
+
return j + 1;
|
|
150
|
+
}
|
|
151
|
+
else if (c === ';' && depth <= 0)
|
|
152
|
+
return j + 1;
|
|
153
|
+
j++;
|
|
154
|
+
}
|
|
155
|
+
return text.length;
|
|
156
|
+
};
|
|
157
|
+
const DECLARATION = /^(?:\$([\w-]+)\s*:|@(mixin|function)\s+([\w-]+))/;
|
|
158
|
+
/**
|
|
159
|
+
* Takes out of the body what Sass allows only at a file's top level: every
|
|
160
|
+
* top-level `@mixin` and `@function`, and the top-level variables they read
|
|
161
|
+
* (directly or through each other). Other variables stay in the body, where a
|
|
162
|
+
* loop that updates one (`$list: list.append($list, …)`) still updates it.
|
|
163
|
+
* Each is replaced by as many blank lines, so the body's lines stay put.
|
|
164
|
+
*/
|
|
165
|
+
const hoist = (body, bodyLine) => {
|
|
166
|
+
const items = [];
|
|
167
|
+
let i = 0;
|
|
168
|
+
while (i < body.length) {
|
|
169
|
+
const past = skipLiteral(body, i);
|
|
170
|
+
if (past !== i || /\s/.test(body[i])) {
|
|
171
|
+
i = past !== i ? past : i + 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const end = itemEnd(body, i);
|
|
175
|
+
const item = body.slice(i, end);
|
|
176
|
+
const found = DECLARATION.exec(item);
|
|
177
|
+
items.push({
|
|
178
|
+
start: i,
|
|
179
|
+
end,
|
|
180
|
+
declaration: found
|
|
181
|
+
? {
|
|
182
|
+
kind: found[1] ? 'variable' : found[2],
|
|
183
|
+
name: found[1] ?? found[3],
|
|
184
|
+
text: item,
|
|
185
|
+
line: bodyLine + body.slice(0, i).split('\n').length - 1,
|
|
186
|
+
}
|
|
187
|
+
: undefined,
|
|
188
|
+
});
|
|
189
|
+
i = end;
|
|
190
|
+
}
|
|
191
|
+
const declared = items.flatMap((item) => (item.declaration ? [item.declaration] : []));
|
|
192
|
+
const hoisted = new Set(declared.filter((d) => d.kind !== 'variable'));
|
|
193
|
+
// The variables the hoisted ones read, until nothing new is needed.
|
|
194
|
+
for (let grew = true; grew;) {
|
|
195
|
+
grew = false;
|
|
196
|
+
for (const d of declared)
|
|
197
|
+
if (d.kind === 'variable' &&
|
|
198
|
+
!hoisted.has(d) &&
|
|
199
|
+
[...hoisted].some((h) => new RegExp(`\\$${d.name}(?![\\w-])`).test(h.text))) {
|
|
200
|
+
hoisted.add(d);
|
|
201
|
+
grew = true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
let out = '';
|
|
205
|
+
let at = 0;
|
|
206
|
+
for (const { start, end, declaration } of items) {
|
|
207
|
+
if (!declaration || !hoisted.has(declaration))
|
|
208
|
+
continue;
|
|
209
|
+
out += body.slice(at, start) + body.slice(start, end).replace(/[^\n]/g, '');
|
|
210
|
+
at = end;
|
|
211
|
+
}
|
|
212
|
+
out += body.slice(at);
|
|
213
|
+
return { body: out, declarations: declared.filter((d) => hoisted.has(d)) };
|
|
214
|
+
};
|
|
215
|
+
/** The body's top-level items (rules, at-rules, declarations), as offsets into it, with comments and space between left out. */
|
|
216
|
+
export const topLevelItems = (text) => {
|
|
217
|
+
const items = [];
|
|
218
|
+
let i = 0;
|
|
219
|
+
while (i < text.length) {
|
|
220
|
+
const past = skipLiteral(text, i);
|
|
221
|
+
if (past !== i || /\s/.test(text[i])) {
|
|
222
|
+
i = past !== i ? past : i + 1;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const end = itemEnd(text, i);
|
|
226
|
+
items.push({ start: i, end });
|
|
227
|
+
i = end;
|
|
228
|
+
}
|
|
229
|
+
return items;
|
|
230
|
+
};
|
|
231
|
+
/** Declarations two components both make: Sass would let the later one replace the other's silently. */
|
|
232
|
+
export const declarationProblems = (components) => {
|
|
233
|
+
const seen = new Map();
|
|
234
|
+
const problems = [];
|
|
235
|
+
for (const [name, declarations] of components)
|
|
236
|
+
for (const { kind, name: member, line } of declarations) {
|
|
237
|
+
const key = `${kind} ${member}`;
|
|
238
|
+
const other = seen.get(key);
|
|
239
|
+
const shown = kind === 'variable' ? `$${member}` : `${kind} ${member}`;
|
|
240
|
+
if (other && other !== name)
|
|
241
|
+
problems.push(`${name} scss: line ${line}: ${other} also declares ${shown}; the theme file holds both, so rename one`);
|
|
242
|
+
else
|
|
243
|
+
seen.set(key, name);
|
|
244
|
+
}
|
|
245
|
+
return problems;
|
|
246
|
+
};
|
|
247
|
+
/** What is wrong with one import. */
|
|
248
|
+
const scssUseProblem = ({ url, line }) => {
|
|
249
|
+
if (url.startsWith('sass:'))
|
|
250
|
+
return undefined;
|
|
251
|
+
if (url.startsWith(PKG)) {
|
|
252
|
+
const entry = url.slice(PKG.length);
|
|
253
|
+
return AUTHORING_ENTRIES.includes(entry)
|
|
254
|
+
? undefined
|
|
255
|
+
: `line ${line}: ${url} is not one of the entry points a component can use (${AUTHORING_ENTRIES.map((e) => `scss/${e}`).join(', ')})`;
|
|
256
|
+
}
|
|
257
|
+
if (url === 'pkg:@greese/ui/scss')
|
|
258
|
+
return `line ${line}: pkg:@greese/ui/scss would write the whole library again; use its parts (${PKG}mixins…)`;
|
|
259
|
+
if (libraryUrl(url))
|
|
260
|
+
return `line ${line}: write "${url}" as "${libraryUrl(url)}"`;
|
|
261
|
+
return `line ${line}: only sass: modules and ${PKG}… can be used here, not "${url}"`;
|
|
262
|
+
};
|
|
263
|
+
/** Everything wrong with a component's SCSS: its imports, and a body whose braces don't balance or that imports. */
|
|
264
|
+
export const scssProblems = (text) => {
|
|
265
|
+
const { uses, body, bodyLine, problems } = splitScss(text);
|
|
266
|
+
const out = [...problems];
|
|
267
|
+
for (const use of uses) {
|
|
268
|
+
const problem = scssUseProblem(use);
|
|
269
|
+
if (problem)
|
|
270
|
+
out.push(problem);
|
|
271
|
+
}
|
|
272
|
+
const bare = body
|
|
273
|
+
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '))
|
|
274
|
+
.replace(/\/\/[^\n]*/g, '')
|
|
275
|
+
.replace(/(['"])(?:\\.|(?!\1)[^\\\n])*\1/g, '""');
|
|
276
|
+
const late = /^\s*@(use|forward|import)\b/m.exec(bare);
|
|
277
|
+
if (late)
|
|
278
|
+
out.push(`line ${lineAt(bare, late.index + late[0].indexOf('@')) + bodyLine - 1}: @${late[1]} must come before the styles`);
|
|
279
|
+
let depth = 0;
|
|
280
|
+
for (let k = 0; k < bare.length; k++) {
|
|
281
|
+
const c = bare[k];
|
|
282
|
+
depth += c === '{' ? 1 : c === '}' ? -1 : 0;
|
|
283
|
+
if (depth < 0) {
|
|
284
|
+
out.push(`line ${lineAt(bare, k) + bodyLine - 1}: a closing brace has no opening one`);
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (depth > 0)
|
|
289
|
+
out.push('a block is never closed');
|
|
290
|
+
return out;
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* The text with each library path a component's source uses rewritten to its
|
|
294
|
+
* `pkg:` form, so pasted source compiles in a project. Returns what changed.
|
|
295
|
+
*/
|
|
296
|
+
export const rewriteLibraryUrls = (text) => {
|
|
297
|
+
const rewritten = [];
|
|
298
|
+
const { uses } = splitScss(text);
|
|
299
|
+
let out = text;
|
|
300
|
+
for (const { url } of uses) {
|
|
301
|
+
const to = libraryUrl(url);
|
|
302
|
+
if (!to)
|
|
303
|
+
continue;
|
|
304
|
+
out = out.replace(new RegExp(`(['"])${url.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')}\\1`), `'${to}'`);
|
|
305
|
+
rewritten.push(`${url} → ${to}`);
|
|
306
|
+
}
|
|
307
|
+
return { text: out, rewritten };
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* Every component's imports as one list for the top of the theme file. Sass
|
|
311
|
+
* refuses one namespace for two statements, even the same one twice, so an
|
|
312
|
+
* identical import (or one the theme already writes) is kept once, and two
|
|
313
|
+
* imports that claim one namespace are an error naming both components.
|
|
314
|
+
*/
|
|
315
|
+
export const mergeUses = (components, reserved) => {
|
|
316
|
+
const lines = [];
|
|
317
|
+
const problems = [];
|
|
318
|
+
const claimed = new Map();
|
|
319
|
+
const starred = new Set();
|
|
320
|
+
for (const { url, namespace } of reserved)
|
|
321
|
+
claimed.set(namespace, { url, from: 'the theme' });
|
|
322
|
+
for (const [name, uses] of components)
|
|
323
|
+
for (const { url, namespace, line } of uses) {
|
|
324
|
+
if (namespace === '*') {
|
|
325
|
+
if (starred.has(url))
|
|
326
|
+
continue;
|
|
327
|
+
starred.add(url);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
const taken = claimed.get(namespace);
|
|
331
|
+
if (taken?.url === url)
|
|
332
|
+
continue;
|
|
333
|
+
if (taken) {
|
|
334
|
+
problems.push(`${name} scss: line ${line}: "${namespace}" is already ${taken.from === 'the theme' ? "the theme's" : `${taken.from}'s`} name for ${taken.url}; give one of them another name with "as"`);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
claimed.set(namespace, { url, from: name });
|
|
338
|
+
}
|
|
339
|
+
const as = namespace === '*'
|
|
340
|
+
? ' as *'
|
|
341
|
+
: namespace === defaultNamespace(url)
|
|
342
|
+
? ''
|
|
343
|
+
: ` as ${namespace}`;
|
|
344
|
+
lines.push({ text: `@use '${url}'${as};`, component: name, line });
|
|
345
|
+
}
|
|
346
|
+
return { lines, problems };
|
|
347
|
+
};
|
|
348
|
+
/**
|
|
349
|
+
* Where a compile error points in the file that was compiled, from 1: the
|
|
350
|
+
* innermost frame of Sass's stack that is in that file (the root stylesheet's),
|
|
351
|
+
* so an error inside a mixin call in a component's body points at the call.
|
|
352
|
+
*/
|
|
353
|
+
export const errorLine = (error) => {
|
|
354
|
+
const stack = error.sassStack ?? '';
|
|
355
|
+
const frames = stack
|
|
356
|
+
.split('\n')
|
|
357
|
+
.map((line) => /^(.*?)\s+(\d+):\d+\s+(.*)$/.exec(line.trim()))
|
|
358
|
+
.filter((m) => !!m);
|
|
359
|
+
const root = frames.find((m) => m[3] === 'root stylesheet');
|
|
360
|
+
const own = root && frames.find((m) => m[1] === root[1]);
|
|
361
|
+
return own ? Number(own[2]) : undefined;
|
|
362
|
+
};
|
|
363
|
+
/**
|
|
364
|
+
* The text with `url` imported or not, for the imports menu. Adding writes
|
|
365
|
+
* `@use` after the last import (the kit as `greese`, as the theme file names
|
|
366
|
+
* it); removing deletes every `@use` of it. The rest is untouched.
|
|
367
|
+
*/
|
|
368
|
+
export const toggleImport = (text, url, on) => {
|
|
369
|
+
const { uses } = splitScss(text);
|
|
370
|
+
const present = uses.some((u) => u.url === url);
|
|
371
|
+
if (on === present)
|
|
372
|
+
return text;
|
|
373
|
+
const lines = text.split('\n');
|
|
374
|
+
if (!on) {
|
|
375
|
+
const drop = new Set(uses.filter((u) => u.url === url).map((u) => u.line - 1));
|
|
376
|
+
return lines.filter((_, i) => !drop.has(i)).join('\n');
|
|
377
|
+
}
|
|
378
|
+
const statement = `@use '${url}'${url === 'pkg:@greese/ui/scss/kit' ? ' as greese' : ''};`;
|
|
379
|
+
const after = uses.at(-1)?.line ?? 0;
|
|
380
|
+
if (!after)
|
|
381
|
+
return text.trim() ? `${statement}\n\n${text}` : `${statement}\n`;
|
|
382
|
+
lines.splice(after, 0, statement);
|
|
383
|
+
return lines.join('\n');
|
|
384
|
+
};
|