@greese/ui 1.0.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.
Files changed (210) hide show
  1. package/README.md +19 -14
  2. package/dist/_maps.scss +1 -1
  3. package/dist/_tokens-base.scss +30 -6
  4. package/dist/_tokens.scss +31 -2
  5. package/dist/_utilities-table.scss +1615 -0
  6. package/dist/clipboard.d.ts +18 -0
  7. package/dist/clipboard.js +62 -0
  8. package/dist/css/Button.css +2 -2
  9. package/dist/css/Carousel.css +145 -47
  10. package/dist/css/Checkbox.css +2 -2
  11. package/dist/css/Chip.css +38 -8
  12. package/dist/css/ChoiceGroup.css +42 -1
  13. package/dist/css/Collapsible.css +1 -0
  14. package/dist/css/ColorPicker.css +84 -0
  15. package/dist/css/Combobox.css +2 -2
  16. package/dist/css/Dialog.css +24 -22
  17. package/dist/css/EditableField.css +219 -0
  18. package/dist/css/Input.css +11 -2
  19. package/dist/css/Menu.css +9 -0
  20. package/dist/css/NumberField.css +53 -8
  21. package/dist/css/NumberStepper.css +9 -0
  22. package/dist/css/OtpField.css +1 -1
  23. package/dist/css/PageTransition.css +12 -0
  24. package/dist/css/Radio.css +2 -2
  25. package/dist/css/ScrollCarousel.css +225 -0
  26. package/dist/css/SegmentedControl.css +2 -2
  27. package/dist/css/Select.css +3 -2
  28. package/dist/css/Slider.css +45 -5
  29. package/dist/css/Switch.css +6 -6
  30. package/dist/css/Text.css +14 -14
  31. package/dist/css/Textarea.css +1 -1
  32. package/dist/css/core.css +36 -6
  33. package/dist/css/resets.css +162 -0
  34. package/dist/css.d.ts +2 -0
  35. package/dist/css.js +2 -0
  36. package/dist/greese.css +990 -138
  37. package/dist/index.d.ts +2 -1
  38. package/dist/index.js +2 -1
  39. package/dist/scss/components/_Accordion.scss +2 -0
  40. package/dist/scss/components/_ActionSelect.scss +2 -0
  41. package/dist/scss/components/_Alert.scss +2 -0
  42. package/dist/scss/components/_AppBar.scss +2 -0
  43. package/dist/scss/components/_AspectRatio.scss +2 -0
  44. package/dist/scss/components/_AutoLayout.scss +2 -0
  45. package/dist/scss/components/_Avatar.scss +2 -0
  46. package/dist/scss/components/_Badge.scss +2 -0
  47. package/dist/scss/components/_Box.scss +2 -0
  48. package/dist/scss/components/_Breadcrumbs.scss +2 -0
  49. package/dist/scss/components/_Button.scss +2 -0
  50. package/dist/scss/components/_ButtonGroup.scss +2 -0
  51. package/dist/scss/components/_Card.scss +2 -0
  52. package/dist/scss/components/_Carousel.scss +2 -0
  53. package/dist/scss/components/_Center.scss +2 -0
  54. package/dist/scss/components/_ChatBubble.scss +2 -0
  55. package/dist/scss/components/_Checkbox.scss +2 -0
  56. package/dist/scss/components/_Chip.scss +2 -0
  57. package/dist/scss/components/_ChoiceGroup.scss +2 -0
  58. package/dist/scss/components/_Code.scss +2 -0
  59. package/dist/scss/components/_Collapsible.scss +2 -0
  60. package/dist/scss/components/_ColorPicker.scss +2 -0
  61. package/dist/scss/components/_Combobox.scss +2 -0
  62. package/dist/scss/components/_Container.scss +2 -0
  63. package/dist/scss/components/_Countdown.scss +2 -0
  64. package/dist/scss/components/_DateField.scss +2 -0
  65. package/dist/scss/components/_Dialog.scss +2 -0
  66. package/dist/scss/components/_Diff.scss +2 -0
  67. package/dist/scss/components/_Divider.scss +2 -0
  68. package/dist/scss/components/_Dock.scss +2 -0
  69. package/dist/scss/components/_EditableField.scss +2 -0
  70. package/dist/scss/components/_Field.scss +2 -0
  71. package/dist/scss/components/_Fieldset.scss +2 -0
  72. package/dist/scss/components/_FileInput.scss +2 -0
  73. package/dist/scss/components/_Footer.scss +2 -0
  74. package/dist/scss/components/_Grid.scss +2 -0
  75. package/dist/scss/components/_Header.scss +2 -0
  76. package/dist/scss/components/_Heading.scss +2 -0
  77. package/dist/scss/components/_Icon.scss +2 -0
  78. package/dist/scss/components/_IconButton.scss +2 -0
  79. package/dist/scss/components/_Image.scss +2 -0
  80. package/dist/scss/components/_Indicator.scss +2 -0
  81. package/dist/scss/components/_Input.scss +2 -0
  82. package/dist/scss/components/_Key.scss +2 -0
  83. package/dist/scss/components/_Link.scss +2 -0
  84. package/dist/scss/components/_List.scss +2 -0
  85. package/dist/scss/components/_Media.scss +2 -0
  86. package/dist/scss/components/_Menu.scss +2 -0
  87. package/dist/scss/components/_Motion.scss +2 -0
  88. package/dist/scss/components/_NumberField.scss +2 -0
  89. package/dist/scss/components/_NumberStepper.scss +2 -0
  90. package/dist/scss/components/_OtpField.scss +2 -0
  91. package/dist/scss/components/_PageTransition.scss +2 -0
  92. package/dist/scss/components/_Pagination.scss +2 -0
  93. package/dist/scss/components/_Popover.scss +2 -0
  94. package/dist/scss/components/_Progress.scss +2 -0
  95. package/dist/scss/components/_RadialProgress.scss +2 -0
  96. package/dist/scss/components/_Radio.scss +2 -0
  97. package/dist/scss/components/_Rating.scss +2 -0
  98. package/dist/scss/components/_ScrollCarousel.scss +2 -0
  99. package/dist/scss/components/_ScrollSpy.scss +2 -0
  100. package/dist/scss/components/_SegmentedControl.scss +2 -0
  101. package/dist/scss/components/_Select.scss +2 -0
  102. package/dist/scss/components/_Sidebar.scss +2 -0
  103. package/dist/scss/components/_Skeleton.scss +2 -0
  104. package/dist/scss/components/_Slider.scss +2 -0
  105. package/dist/scss/components/_Spacer.scss +2 -0
  106. package/dist/scss/components/_SpeedDial.scss +2 -0
  107. package/dist/scss/components/_Spinner.scss +2 -0
  108. package/dist/scss/components/_Stack.scss +2 -0
  109. package/dist/scss/components/_Stat.scss +2 -0
  110. package/dist/scss/components/_Status.scss +2 -0
  111. package/dist/scss/components/_Steps.scss +2 -0
  112. package/dist/scss/components/_Swap.scss +2 -0
  113. package/dist/scss/components/_Switch.scss +2 -0
  114. package/dist/scss/components/_Table.scss +2 -0
  115. package/dist/scss/components/_Tabs.scss +2 -0
  116. package/dist/scss/components/_Text.scss +2 -0
  117. package/dist/scss/components/_TextRotate.scss +2 -0
  118. package/dist/scss/components/_Textarea.scss +2 -0
  119. package/dist/scss/components/_ThemeProvider.scss +2 -0
  120. package/dist/scss/components/_Tile.scss +2 -0
  121. package/dist/scss/components/_Toast.scss +2 -0
  122. package/dist/scss/components/_ToggleButton.scss +2 -0
  123. package/dist/scss/components/_Tooltip.scss +2 -0
  124. package/dist/scss/components/_Type.scss +2 -0
  125. package/dist/scss/components/_Visibility.scss +2 -0
  126. package/dist/scss/components/_VisuallyHidden.scss +2 -0
  127. package/dist/shapes.d.ts +12 -1
  128. package/dist/shapes.js +11 -10
  129. package/dist/style-manifest.json +18837 -132
  130. package/dist/theme.js +6 -1
  131. package/dist/themeFiles/index.d.ts +9 -0
  132. package/dist/themeFiles/index.js +9 -0
  133. package/dist/themeFiles/scssModule.d.ts +100 -0
  134. package/dist/themeFiles/scssModule.js +384 -0
  135. package/dist/themeFiles/spec.d.ts +198 -0
  136. package/dist/themeFiles/spec.js +877 -0
  137. package/dist/tokens.css +30 -1
  138. package/dist/typography.js +1 -1
  139. package/dist/utilities.css +435 -0
  140. package/dist/utilities.d.ts +8 -0
  141. package/dist/utilities.js +75 -0
  142. package/package.json +20 -1
  143. package/src/components/AutoLayout/AutoLayout.scss +2 -26
  144. package/src/components/Carousel/Carousel.scss +122 -126
  145. package/src/components/Checkbox/Checkbox.scss +2 -2
  146. package/src/components/Chip/Chip.scss +63 -8
  147. package/src/components/ChoiceGroup/ChoiceGroup.scss +19 -1
  148. package/src/components/Collapsible/Collapsible.scss +3 -0
  149. package/src/components/ColorPicker/ColorPicker.scss +95 -0
  150. package/src/components/Combobox/Combobox.scss +4 -2
  151. package/src/components/Dialog/Dialog.scss +24 -7
  152. package/src/components/EditableField/EditableField.scss +275 -0
  153. package/src/components/Grid/Grid.scss +2 -70
  154. package/src/components/Input/Input.scss +29 -0
  155. package/src/components/Menu/Menu.scss +18 -0
  156. package/src/components/NumberField/NumberField.scss +56 -9
  157. package/src/components/NumberStepper/NumberStepper.scss +13 -0
  158. package/src/components/PageTransition/PageTransition.scss +29 -0
  159. package/src/components/Radio/Radio.scss +2 -2
  160. package/src/components/ScrollCarousel/ScrollCarousel.scss +142 -0
  161. package/src/components/Select/Select.scss +4 -0
  162. package/src/components/Slider/Slider.scss +51 -6
  163. package/src/components/Stack/Stack.scss +2 -48
  164. package/src/components/Switch/Switch.scss +9 -6
  165. package/src/components/Text/Text.scss +5 -2
  166. package/src/styles/_breakpoint-vars.scss +17 -11
  167. package/src/styles/_controls.scss +2 -2
  168. package/src/styles/_core.scss +1 -0
  169. package/src/styles/_kit.scss +483 -56
  170. package/src/styles/_layout-props.scss +197 -0
  171. package/src/styles/_manifest.scss +55 -25
  172. package/src/styles/_mixins.scss +53 -15
  173. package/src/styles/_slides.scss +112 -0
  174. package/src/styles/_utilities.scss +104 -0
  175. package/src/styles/breakpoints.test.ts +21 -1
  176. package/src/styles/entryPoints.test.ts +25 -2
  177. package/src/styles/index.scss +3 -1
  178. package/src/styles/kit.test.ts +171 -1
  179. package/src/styles/public/_breakpoints.scss +43 -0
  180. package/src/styles/public/_slides.scss +3 -0
  181. package/src/styles/public/_utilities.scss +3 -0
  182. package/src/styles/resets.scss +7 -0
  183. package/src/styles/utilities.test.ts +39 -0
  184. package/themes/bevel.css +187 -187
  185. package/themes/bevel.scss +960 -958
  186. package/themes/candy.css +87 -0
  187. package/themes/candy.scss +32 -0
  188. package/themes/extrude.css +42 -42
  189. package/themes/folio.css +200 -200
  190. package/themes/folio.scss +880 -878
  191. package/themes/forest.css +87 -0
  192. package/themes/forest.scss +32 -0
  193. package/themes/grape.css +87 -0
  194. package/themes/grape.scss +32 -0
  195. package/themes/greese.css +36 -0
  196. package/themes/greese.scss +21 -0
  197. package/themes/mono.css +87 -0
  198. package/themes/mono.scss +32 -0
  199. package/themes/ocean.css +87 -0
  200. package/themes/ocean.scss +32 -0
  201. package/themes/outline.css +18 -18
  202. package/themes/pebble.css +8 -8
  203. package/themes/ridge.css +188 -188
  204. package/themes/ridge.scss +820 -818
  205. package/themes/soft.css +11 -11
  206. package/themes/sunset.css +87 -0
  207. package/themes/sunset.scss +32 -0
  208. package/themes/ticket.css +180 -180
  209. package/themes/ticket.scss +881 -879
  210. 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
+ };