scb-wc 0.1.41 → 0.1.42
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 +38 -41
- package/all.js +2 -0
- package/bin/scb-wc.mjs +274 -1
- package/icons.json +178 -0
- package/index.js +90 -88
- package/mvc/components/all.js +2 -0
- package/mvc/components/scb-accordion/scb-accordion-item.js +1 -1
- package/mvc/components/scb-card/scb-card.js +0 -1
- package/mvc/{vendor → components/scb-chevron}/scb-chevron.js +1 -1
- package/mvc/components/scb-collapse/scb-collapse.js +30 -24
- package/mvc/components/scb-datepicker/scb-datepicker.js +296 -0
- package/mvc/components/scb-drawer/scb-drawer.js +1 -1
- package/mvc/components/scb-dropdown/scb-dropdown.js +1 -1
- package/mvc/components/scb-footer/scb-footer.js +1 -0
- package/mvc/components/scb-grid/scb-grid-item.js +4 -1
- package/mvc/components/scb-header/scb-header.js +48 -39
- package/mvc/components/scb-menu/scb-menu-item.js +58 -11
- package/mvc/components/scb-menu/scb-menu.js +4 -4
- package/mvc/components/scb-select/scb-select.js +1 -1
- package/mvc/components/scb-textfield/scb-textfield.js +24 -319
- package/mvc/components/scb-toc/scb-toc-item.js +3 -21
- package/mvc/scb-wc-core.css +1 -0
- package/mvc/scb-wc-selfhost.css +29 -0
- package/package.json +5 -2
- package/scb-card/scb-card.js +0 -1
- package/scb-chevron/scb-chevron.js +1 -0
- package/scb-collapse/scb-collapse.js +145 -90
- package/scb-components/index.d.ts +94 -0
- package/scb-components/scb-chevron/scb-chevron.d.ts +11 -0
- package/scb-components/scb-collapse/scb-collapse.d.ts +0 -2
- package/scb-components/scb-datepicker/scb-datepicker.d.ts +39 -0
- package/scb-components/scb-header/scb-header.d.ts +7 -0
- package/scb-components/scb-menu/scb-menu-item.d.ts +5 -0
- package/scb-components/scb-menu/scb-menu.d.ts +24 -0
- package/scb-datepicker/scb-datepicker.js +1 -0
- package/scb-footer/scb-footer.js +1 -0
- package/scb-grid/scb-grid-item.js +4 -1
- package/scb-header/scb-header.js +73 -40
- package/scb-menu/scb-menu-item.js +94 -28
- package/scb-menu/scb-menu.js +126 -3
- package/scb-toc/scb-toc-item.js +2 -21
- package/scb-wc-core.css +1 -0
- package/scb-wc.bundle.js +353 -307
- package/scb-wc.d.ts +188 -184
- package/scb-wc-public-entry/index.d.ts +0 -92
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
# Om SCB Web Components
|
|
1
|
+
# Om SCB Web Components Preview
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> ⚠️ **Previewkanal**
|
|
4
|
+
> Installera `scb-wc` via `@next` för komponenter under utveckling. API, utseende och beteende kan ändras eller tas bort mellan versioner.
|
|
5
|
+
|
|
6
|
+
SCB Web Components preview finns för att underlätta skapandet av enhetliga, tillgängliga och användbara webbapplikationer.
|
|
4
7
|
|
|
5
8
|
Komponenterna bygger på:
|
|
6
9
|
- [Lit](https://lit.dev/)
|
|
@@ -8,54 +11,32 @@ Komponenterna bygger på:
|
|
|
8
11
|
|
|
9
12
|
## Storybook och dokumentation (internt)
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
Previewkomponenterna dokumenteras och demonstreras i Storybook på SCB:s interna testadress:
|
|
12
15
|
|
|
13
|
-
-
|
|
16
|
+
- Test: <https://webcomponentstest.scb.intra>
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
Den publika/stabila Storybooken finns på:
|
|
16
19
|
|
|
17
|
-
-
|
|
20
|
+
- Prod: <https://webcomponents.scb.intra>
|
|
18
21
|
|
|
19
22
|
---
|
|
20
23
|
|
|
21
24
|
## Kom igång
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```sh
|
|
26
|
-
npx scb-wc init html my-app
|
|
27
|
-
npx scb-wc init html-service my-service
|
|
28
|
-
npx scb-wc init react my-react-app
|
|
29
|
-
npx scb-wc init react-service my-react-service
|
|
30
|
-
npx scb-wc init blazor my-blazor-app
|
|
31
|
-
npx scb-wc init blazor-service my-blazor-service
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Webbplatsmallarna skapar en liten starterapp med:
|
|
35
|
-
- `scb-header`
|
|
36
|
-
- `scb-footer`
|
|
37
|
-
- `scb-grid`
|
|
38
|
-
- några vanliga komponenter
|
|
26
|
+
> **Obs:** Kör alla kommandon i mappen där din `package.json` ligger.
|
|
27
|
+
> Har du ingen? Kör `npm init -y` i projektroten (eller annan lämplig mapp).
|
|
39
28
|
|
|
40
|
-
|
|
41
|
-
- `scb-app-bar`
|
|
42
|
-
- `scb-search`
|
|
43
|
-
- `scb-table-advanced`
|
|
44
|
-
- komponenter för actions och status
|
|
45
|
-
|
|
46
|
-
Välj sedan den starter som ligger närmast din app och kör `npm install` i den nya mappen.
|
|
47
|
-
|
|
48
|
-
Om du redan har en app med `package.json`, kör kommandon i den mappen och installera paketet:
|
|
29
|
+
Installera previewkanalen:
|
|
49
30
|
|
|
50
31
|
```sh
|
|
51
|
-
npm install scb-wc
|
|
32
|
+
npm install scb-wc@next
|
|
52
33
|
```
|
|
53
34
|
|
|
54
35
|
---
|
|
55
36
|
|
|
56
37
|
## Alternativ 1: Använd som ES‑moduler (t.ex. i React, Vue, SPA)
|
|
57
38
|
|
|
58
|
-
Importera CSS och de
|
|
39
|
+
Importera CSS och de testkomponenter du använder (bäst för tree‑shaking):
|
|
59
40
|
|
|
60
41
|
```js
|
|
61
42
|
// Global CSS + tokens (måste bara importeras en gång)
|
|
@@ -87,11 +68,27 @@ import 'scb-wc/scb-wc-selfhost.css';
|
|
|
87
68
|
|
|
88
69
|
Då används paketets egna fontfiler under `node_modules/scb-wc/fonts/`.
|
|
89
70
|
|
|
71
|
+
### Optimera Material Symbols med eget ikon-subset
|
|
72
|
+
|
|
73
|
+
Vill du selfhosta bara de Material Symbols-ikoner din app använder kan du generera en liten ikonfont i din app:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
npx scb-wc subset-icons --icons ./src/scb-icons.json --out ./public/scb-icons
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Om `--icons` utelämnas används paketets standardlista `icons.json`. Kommandot skapar `scb-wc-icons.css`, en lokal fontfil och en kopia av ikonlistan i målmappen. Ladda då den fontlösa bas-CSS:en plus den genererade ikon-CSS:en:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import 'scb-wc/scb-wc-core.css';
|
|
83
|
+
import '/scb-icons/scb-wc-icons.css';
|
|
84
|
+
import 'scb-wc/scb-typography.css';
|
|
85
|
+
```
|
|
86
|
+
|
|
90
87
|
---
|
|
91
88
|
|
|
92
89
|
## Alternativ 2: Använd i MVC/MPA via `<script type="module">`
|
|
93
90
|
|
|
94
|
-
Det här läget använder den färdig‑
|
|
91
|
+
Det här läget använder den färdig‑splittade **MVC‑ESM**‑builden som följer med paketet under:
|
|
95
92
|
|
|
96
93
|
```text
|
|
97
94
|
node_modules/scb-wc/mvc/
|
|
@@ -104,7 +101,7 @@ Lägg till i din apps `package.json`:
|
|
|
104
101
|
```jsonc
|
|
105
102
|
{
|
|
106
103
|
"scripts": {
|
|
107
|
-
"ui:install": "node -e \"const fs=require('fs'),p=require('path');const src=p.resolve('node_modules/scb-wc/mvc');if(!fs.existsSync(src)){console.error('Hittar inte '+src+'. Har du kört npm install scb-wc?');process.exit(1);}const start=process.env.INIT_CWD||process.cwd();const ov=process.env.npm_config_ui_wwwroot;function findBase(){if(ov){return p.isAbsolute(ov)?ov:p.resolve(start,ov);}let d=start;while(true){const cand=p.join(d,'wwwroot');if(fs.existsSync(cand)) return cand;const up=p.dirname(d);if(up===d) return p.resolve(start,'wwwroot');d=up;}}const base=findBase();const dst=p.resolve(base,'ui');fs.rmSync(dst,{recursive:true,force:true});fs.mkdirSync(base,{recursive:true});fs.cpSync(src,dst,{recursive:true});console.log('Kopierade '+src+' → '+dst);\""
|
|
104
|
+
"ui:install": "node -e \"const fs=require('fs'),p=require('path');const src=p.resolve('node_modules/scb-wc/mvc');if(!fs.existsSync(src)){console.error('Hittar inte '+src+'. Har du kört npm install scb-wc@next?');process.exit(1);}const start=process.env.INIT_CWD||process.cwd();const ov=process.env.npm_config_ui_wwwroot;function findBase(){if(ov){return p.isAbsolute(ov)?ov:p.resolve(start,ov);}let d=start;while(true){const cand=p.join(d,'wwwroot');if(fs.existsSync(cand)) return cand;const up=p.dirname(d);if(up===d) return p.resolve(start,'wwwroot');d=up;}}const base=findBase();const dst=p.resolve(base,'ui');fs.rmSync(dst,{recursive:true,force:true});fs.mkdirSync(base,{recursive:true});fs.cpSync(src,dst,{recursive:true});console.log('Kopierade '+src+' → '+dst);\""
|
|
108
105
|
}
|
|
109
106
|
}
|
|
110
107
|
```
|
|
@@ -151,7 +148,7 @@ Behöver du en annan webbrotsökväg (t.ex. om din `wwwroot` ligger någon annan
|
|
|
151
148
|
|
|
152
149
|
## Alternativ 3: Bundlad version (IIFE) för äldre miljöer
|
|
153
150
|
|
|
154
|
-
Om ESM inte stöds kan du använda den bundlade varianten från paketroten.
|
|
151
|
+
Om ESM inte stöds kan du använda den bundlade test‑varianten från paketroten.
|
|
155
152
|
Flytta följande tre filer från `node_modules/scb-wc` och använd dem i applikationen:
|
|
156
153
|
|
|
157
154
|
```text
|
|
@@ -174,7 +171,7 @@ node_modules/scb-wc/scb-typography.css
|
|
|
174
171
|
|
|
175
172
|
SCB Web Components fungerar även i Blazor‑appar när du vill använda samma komponenter i både MVC/MPA och Blazor.
|
|
176
173
|
|
|
177
|
-
Grundprincipen är
|
|
174
|
+
Grundprincipen är samma som för `scb-wc`:
|
|
178
175
|
|
|
179
176
|
1. Använd MVC‑ESM‑builden (`node_modules/scb-wc/mvc`) och kopiera den till `wwwroot/ui` med `ui:install`.
|
|
180
177
|
2. Ladda `scb-blazor-bridge.js` från `wwwroot/ui` i din Blazor‑layout.
|
|
@@ -187,8 +184,8 @@ Lägg till ett script som kopierar interop-filen från `node_modules` till ditt
|
|
|
187
184
|
```jsonc
|
|
188
185
|
{
|
|
189
186
|
"scripts": {
|
|
190
|
-
"ui:install": "node -e \"const fs=require('fs'),p=require('path');const src=p.resolve('node_modules/scb-wc/mvc');if(!fs.existsSync(src)){console.error('Hittar inte '+src+'. Har du kört npm install scb-wc?');process.exit(1);}const start=process.env.INIT_CWD||process.cwd();const ov=process.env.npm_config_ui_wwwroot;function findBase(){if(ov){return p.isAbsolute(ov)?ov:p.resolve(start,ov);}let d=start;while(true){const cand=p.join(d,'wwwroot');if(fs.existsSync(cand)) return cand;const up=p.dirname(d);if(up===d) return p.resolve(start,'wwwroot');d=up;}}const base=findBase();const dst=p.resolve(base,'ui');fs.rmSync(dst,{recursive:true,force:true});fs.mkdirSync(base,{recursive:true});fs.cpSync(src,dst,{recursive:true});console.log('Kopierade '+src+' → '+dst);\"",
|
|
191
|
-
"ui:blazor:interop": "node -e \"const fs=require('fs'),p=require('path');const start=process.env.INIT_CWD||process.cwd();const src=p.resolve('node_modules/scb-wc/blazor/ScbBlazorInteropBase.cs');if(!fs.existsSync(src)){console.error('Hittar inte '+src+'. Har du kört npm install scb-wc?');process.exit(1);}const dst=p.resolve(start,'ScbBlazor/ScbBlazorInteropBase.cs');fs.mkdirSync(p.dirname(dst),{recursive:true});fs.copyFileSync(src,dst);console.log('Kopierade '+src+' → '+dst);\""
|
|
187
|
+
"ui:install": "node -e \"const fs=require('fs'),p=require('path');const src=p.resolve('node_modules/scb-wc/mvc');if(!fs.existsSync(src)){console.error('Hittar inte '+src+'. Har du kört npm install scb-wc@next?');process.exit(1);}const start=process.env.INIT_CWD||process.cwd();const ov=process.env.npm_config_ui_wwwroot;function findBase(){if(ov){return p.isAbsolute(ov)?ov:p.resolve(start,ov);}let d=start;while(true){const cand=p.join(d,'wwwroot');if(fs.existsSync(cand)) return cand;const up=p.dirname(d);if(up===d) return p.resolve(start,'wwwroot');d=up;}}const base=findBase();const dst=p.resolve(base,'ui');fs.rmSync(dst,{recursive:true,force:true});fs.mkdirSync(base,{recursive:true});fs.cpSync(src,dst,{recursive:true});console.log('Kopierade '+src+' → '+dst);\"",
|
|
188
|
+
"ui:blazor:interop": "node -e \"const fs=require('fs'),p=require('path');const start=process.env.INIT_CWD||process.cwd();const src=p.resolve('node_modules/scb-wc/blazor/ScbBlazorInteropBase.cs');if(!fs.existsSync(src)){console.error('Hittar inte '+src+'. Har du kört npm install scb-wc@next?');process.exit(1);}const dst=p.resolve(start,'ScbBlazor/ScbBlazorInteropBase.cs');fs.mkdirSync(p.dirname(dst),{recursive:true});fs.copyFileSync(src,dst);console.log('Kopierade '+src+' → '+dst);\""
|
|
192
189
|
}
|
|
193
190
|
}
|
|
194
191
|
```
|
|
@@ -202,13 +199,13 @@ Exempel på layout:
|
|
|
202
199
|
<script type="module" src="~/ui/scb-blazor-bridge.js"></script>
|
|
203
200
|
```
|
|
204
201
|
|
|
205
|
-
`ScbBlazorInteropBase` kan
|
|
202
|
+
`ScbBlazorInteropBase` är samma bas‑klass i preview- och public-kanalen. Du kan därför dela samma C#‑fil mellan projekt som använder `scb-wc@next` och `scb-wc@latest`.
|
|
206
203
|
|
|
207
204
|
---
|
|
208
205
|
|
|
209
206
|
## Viktigt
|
|
210
207
|
|
|
211
|
-
- `scb-wc@
|
|
208
|
+
- `scb-wc@next` är **previewkanalen**. Komponenter kan ändras eller tas bort mellan versioner. Använd `scb-wc@latest` i externa produktionsmiljöer.
|
|
212
209
|
- **Kör kommandon i mappen med din `package.json`.** Placeringen av `package.json` styr standardmål för `ui:install` (rot → `wwwroot/ui`, `ClientApp/` → använd `--ui_wwwroot=../wwwroot`).
|
|
213
210
|
- **Blanda inte MVC‑ESM och IIFE på samma sida.** Välj en distributionsform per sida/app.
|
|
214
211
|
- **Blazor:** Se till att `scb-blazor-bridge.js` laddas efter att komponent‑JS:et finns på sidan, och att `ScbBlazorInteropBase` inte dupliceras i flera namespaces i samma lösning.
|
package/all.js
CHANGED
|
@@ -17,9 +17,11 @@ import './scb-card/scb-list-card.js';
|
|
|
17
17
|
import './scb-card/scb-social-card.js';
|
|
18
18
|
import './scb-checkbox/scb-checkbox-group.js';
|
|
19
19
|
import './scb-checkbox/scb-checkbox.js';
|
|
20
|
+
import './scb-chevron/scb-chevron.js';
|
|
20
21
|
import './scb-chip/scb-chip.js';
|
|
21
22
|
import './scb-collapse/scb-collapse.js';
|
|
22
23
|
import './scb-cookies-consent/scb-cookies-consent.js';
|
|
24
|
+
import './scb-datepicker/scb-datepicker.js';
|
|
23
25
|
import './scb-dialog/scb-dialog.js';
|
|
24
26
|
import './scb-divider/scb-divider.js';
|
|
25
27
|
import './scb-drawer/scb-drawer.js';
|
package/bin/scb-wc.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
|
+
import http from 'node:http';
|
|
5
|
+
import https from 'node:https';
|
|
4
6
|
import path from 'node:path';
|
|
5
7
|
import { fileURLToPath } from 'node:url';
|
|
6
8
|
|
|
@@ -10,7 +12,30 @@ const packageRoot = path.resolve(__dirname, '..');
|
|
|
10
12
|
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
11
13
|
const starterRoot = path.join(packageRoot, 'starters');
|
|
12
14
|
const templates = new Set(['html', 'html-service', 'react', 'react-service', 'blazor', 'blazor-service']);
|
|
13
|
-
const usage =
|
|
15
|
+
const usage = [
|
|
16
|
+
'npx scb-wc init <html|html-service|react|react-service|blazor|blazor-service> [mapp]',
|
|
17
|
+
'npx scb-wc subset-icons [--icons icons.json] [--out public/scb-icons] [--css-file scb-wc-icons.css]',
|
|
18
|
+
].join('\n');
|
|
19
|
+
|
|
20
|
+
const defaultSubsetOutDir = 'public/scb-icons';
|
|
21
|
+
const defaultSubsetCssFile = 'scb-wc-icons.css';
|
|
22
|
+
const defaultSubsetFontBase = 'material-symbols-outlined-subset';
|
|
23
|
+
const materialSymbolsCssUrl = 'https://fonts.googleapis.com/css2';
|
|
24
|
+
const materialSymbolsFamilyQuery = 'Material Symbols Outlined:opsz,wght,FILL,GRAD@24,400,0,0';
|
|
25
|
+
const requiredIconNames = [
|
|
26
|
+
'arrow_back',
|
|
27
|
+
'arrow_forward',
|
|
28
|
+
'check',
|
|
29
|
+
'chevron_left',
|
|
30
|
+
'chevron_right',
|
|
31
|
+
'close',
|
|
32
|
+
'expand_less',
|
|
33
|
+
'expand_more',
|
|
34
|
+
'keyboard_arrow_down',
|
|
35
|
+
'keyboard_arrow_up',
|
|
36
|
+
'menu',
|
|
37
|
+
'search',
|
|
38
|
+
];
|
|
14
39
|
|
|
15
40
|
function fail(message) {
|
|
16
41
|
console.error(message);
|
|
@@ -66,8 +91,256 @@ function copyStarter(sourceDir, targetDir, targetName) {
|
|
|
66
91
|
}
|
|
67
92
|
}
|
|
68
93
|
|
|
94
|
+
function parseFlags(args) {
|
|
95
|
+
const flags = {};
|
|
96
|
+
|
|
97
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
98
|
+
const arg = args[index];
|
|
99
|
+
|
|
100
|
+
if (!arg.startsWith('--')) {
|
|
101
|
+
fail(`Okänt argument: ${arg}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const [rawKey, inlineValue] = arg.slice(2).split(/=(.*)/s, 2);
|
|
105
|
+
const key = rawKey.trim();
|
|
106
|
+
|
|
107
|
+
if (!key) {
|
|
108
|
+
fail(`Ogiltigt argument: ${arg}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (key === 'dry-run' || key === 'help') {
|
|
112
|
+
flags[key] = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const value = inlineValue ?? args[index + 1];
|
|
117
|
+
if (!value || value.startsWith('--')) {
|
|
118
|
+
fail(`Saknar värde för --${key}.`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
flags[key] = value;
|
|
122
|
+
if (inlineValue === undefined) {
|
|
123
|
+
index += 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return flags;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveDefaultIconsPath() {
|
|
131
|
+
const candidates = [
|
|
132
|
+
path.join(packageRoot, 'icons.json'),
|
|
133
|
+
path.join(packageRoot, 'scripts', 'icons.json'),
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
return candidates.find((candidate) => fs.existsSync(candidate));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readIconList(iconsPath) {
|
|
140
|
+
if (!iconsPath) {
|
|
141
|
+
fail('Saknar standardlista for ikoner.');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const absolutePath = path.resolve(process.cwd(), iconsPath);
|
|
145
|
+
if (!fs.existsSync(absolutePath)) {
|
|
146
|
+
fail(`Hittar inte ikonfilen: ${absolutePath}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let parsed;
|
|
150
|
+
try {
|
|
151
|
+
parsed = JSON.parse(fs.readFileSync(absolutePath, 'utf8'));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
fail(`Kunde inte lasa ikonfilen ${absolutePath}: ${error.message}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const list = Array.isArray(parsed) ? parsed : parsed?.icons;
|
|
157
|
+
if (!Array.isArray(list)) {
|
|
158
|
+
fail('Ikonfilen ska vara en JSON-array eller ett objekt med egenskapen "icons".');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return list.map((icon) => String(icon ?? '').trim()).filter(Boolean);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeIconNames(iconNames) {
|
|
165
|
+
const normalized = Array.from(new Set([...requiredIconNames, ...iconNames]))
|
|
166
|
+
.map((icon) => icon.trim())
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.sort((a, b) => a.localeCompare(b));
|
|
169
|
+
|
|
170
|
+
const invalid = normalized.filter((icon) => !/^[a-z0-9_]+$/.test(icon));
|
|
171
|
+
if (invalid.length) {
|
|
172
|
+
fail(`Ogiltiga ikon-namn: ${invalid.join(', ')}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return normalized;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildMaterialSymbolsUrl(iconNames, display) {
|
|
179
|
+
const params = new URLSearchParams({
|
|
180
|
+
family: materialSymbolsFamilyQuery,
|
|
181
|
+
icon_names: iconNames.join(','),
|
|
182
|
+
display,
|
|
183
|
+
});
|
|
184
|
+
return `${materialSymbolsCssUrl}?${params.toString()}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function requestUrl(url, redirects = 5) {
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
const client = url.startsWith('http:') ? http : https;
|
|
190
|
+
const request = client.get(
|
|
191
|
+
url,
|
|
192
|
+
{
|
|
193
|
+
headers: {
|
|
194
|
+
'User-Agent': `${packageJson.name}/${packageJson.version} subset-icons`,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
(response) => {
|
|
198
|
+
const statusCode = response.statusCode ?? 0;
|
|
199
|
+
const location = response.headers.location;
|
|
200
|
+
|
|
201
|
+
if ([301, 302, 303, 307, 308].includes(statusCode) && location) {
|
|
202
|
+
response.resume();
|
|
203
|
+
if (redirects <= 0) {
|
|
204
|
+
reject(new Error(`For manga redirects for ${url}`));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
resolve(requestUrl(new URL(location, url).href, redirects - 1));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
212
|
+
response.resume();
|
|
213
|
+
reject(new Error(`HTTP ${statusCode} for ${url}`));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const chunks = [];
|
|
218
|
+
response.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
219
|
+
response.on('end', () => {
|
|
220
|
+
resolve({
|
|
221
|
+
buffer: Buffer.concat(chunks),
|
|
222
|
+
contentType: response.headers['content-type'] ?? '',
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
request.on('error', reject);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function fetchText(url) {
|
|
233
|
+
const { buffer } = await requestUrl(url);
|
|
234
|
+
return buffer.toString('utf8');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function fetchBuffer(url) {
|
|
238
|
+
const { buffer } = await requestUrl(url);
|
|
239
|
+
return buffer;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function fontExtensionForFormat(format) {
|
|
243
|
+
switch (format) {
|
|
244
|
+
case 'woff2':
|
|
245
|
+
return 'woff2';
|
|
246
|
+
case 'woff':
|
|
247
|
+
return 'woff';
|
|
248
|
+
case 'opentype':
|
|
249
|
+
return 'otf';
|
|
250
|
+
case 'truetype':
|
|
251
|
+
default:
|
|
252
|
+
return 'ttf';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function findFontSources(cssText) {
|
|
257
|
+
const matches = Array.from(
|
|
258
|
+
cssText.matchAll(/url\((https:\/\/fonts\.gstatic\.com\/[^)]+)\)\s*format\(['"]([^'"]+)['"]\)/g),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
return matches.map((match) => ({
|
|
262
|
+
url: match[1],
|
|
263
|
+
format: match[2],
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function writeSubsetFiles({ cssText, iconNames, outDir, cssFile }) {
|
|
268
|
+
const sources = findFontSources(cssText);
|
|
269
|
+
if (!sources.length) {
|
|
270
|
+
fail('Google Fonts-svaret innehöll inga fontfiler att ladda ner.');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
274
|
+
|
|
275
|
+
let localCss = cssText;
|
|
276
|
+
for (const [index, source] of sources.entries()) {
|
|
277
|
+
const ext = fontExtensionForFormat(source.format);
|
|
278
|
+
const suffix = sources.length > 1 ? `-${index + 1}` : '';
|
|
279
|
+
const fileName = `${defaultSubsetFontBase}${suffix}.${ext}`;
|
|
280
|
+
const fontBuffer = await fetchBuffer(source.url);
|
|
281
|
+
fs.writeFileSync(path.join(outDir, fileName), fontBuffer);
|
|
282
|
+
localCss = localCss.split(source.url).join(`./${fileName}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const header = [
|
|
286
|
+
'/* Generated by `npx scb-wc subset-icons`. */',
|
|
287
|
+
`/* Icons: ${iconNames.join(', ')} */`,
|
|
288
|
+
'',
|
|
289
|
+
].join('\n');
|
|
290
|
+
|
|
291
|
+
fs.writeFileSync(path.join(outDir, cssFile), `${header}${localCss.trim()}\n`, 'utf8');
|
|
292
|
+
fs.writeFileSync(path.join(outDir, 'icons.json'), `${JSON.stringify(iconNames, null, 2)}\n`, 'utf8');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function runSubsetIcons(args) {
|
|
296
|
+
const flags = parseFlags(args);
|
|
297
|
+
if (flags.help) {
|
|
298
|
+
console.log(usage);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const iconsPath = flags.icons ?? resolveDefaultIconsPath();
|
|
303
|
+
const outDir = path.resolve(process.cwd(), flags.out ?? defaultSubsetOutDir);
|
|
304
|
+
const cssFile = flags['css-file'] ?? defaultSubsetCssFile;
|
|
305
|
+
const display = flags.display ?? 'swap';
|
|
306
|
+
|
|
307
|
+
if (!/^[a-z0-9._-]+\.css$/i.test(cssFile)) {
|
|
308
|
+
fail('--css-file måste vara ett css-filnamn, till exempel scb-wc-icons.css.');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!['auto', 'block', 'swap', 'fallback', 'optional'].includes(display)) {
|
|
312
|
+
fail('--display måste vara auto, block, swap, fallback eller optional.');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const iconNames = normalizeIconNames(readIconList(iconsPath));
|
|
316
|
+
const cssUrl = buildMaterialSymbolsUrl(iconNames, display);
|
|
317
|
+
|
|
318
|
+
if (flags['dry-run']) {
|
|
319
|
+
console.log(`Skulle skapa Material Symbols-subset med ${iconNames.length} ikoner.`);
|
|
320
|
+
console.log(`Källa: ${path.resolve(process.cwd(), iconsPath)}`);
|
|
321
|
+
console.log(`Mål: ${outDir}`);
|
|
322
|
+
console.log(`CSS: ${cssFile}`);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const cssText = await fetchText(cssUrl);
|
|
327
|
+
await writeSubsetFiles({ cssText, iconNames, outDir, cssFile });
|
|
328
|
+
console.log(`Skapade ikon-subset med ${iconNames.length} ikoner i ${outDir}`);
|
|
329
|
+
console.log(`Ladda CSS-filen: ${path.join(outDir, cssFile)}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
69
332
|
const [, , command, template, targetArg] = process.argv;
|
|
70
333
|
|
|
334
|
+
if (!command || command === '--help' || command === 'help') {
|
|
335
|
+
console.log(usage);
|
|
336
|
+
process.exit(0);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (command === 'subset-icons') {
|
|
340
|
+
await runSubsetIcons(process.argv.slice(3));
|
|
341
|
+
process.exit(0);
|
|
342
|
+
}
|
|
343
|
+
|
|
71
344
|
if (command !== 'init') {
|
|
72
345
|
fail('Okänt kommando.');
|
|
73
346
|
}
|
package/icons.json
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
[
|
|
2
|
+
"add",
|
|
3
|
+
"remove",
|
|
4
|
+
"add_circle",
|
|
5
|
+
"remove_circle",
|
|
6
|
+
"expand_more",
|
|
7
|
+
"expand_less",
|
|
8
|
+
"chevron_right",
|
|
9
|
+
"chevron_left",
|
|
10
|
+
"unfold_more",
|
|
11
|
+
"unfold_less",
|
|
12
|
+
"info",
|
|
13
|
+
"warning",
|
|
14
|
+
"menu",
|
|
15
|
+
"code",
|
|
16
|
+
"more_horiz",
|
|
17
|
+
"more_vert",
|
|
18
|
+
"check_circle",
|
|
19
|
+
"check",
|
|
20
|
+
"check_small",
|
|
21
|
+
"close",
|
|
22
|
+
"border_color",
|
|
23
|
+
"error",
|
|
24
|
+
"edit",
|
|
25
|
+
"edit_square",
|
|
26
|
+
"refresh",
|
|
27
|
+
"print",
|
|
28
|
+
"group",
|
|
29
|
+
"person_add",
|
|
30
|
+
"person",
|
|
31
|
+
"download",
|
|
32
|
+
"mobile_arrow_down",
|
|
33
|
+
"upload",
|
|
34
|
+
"mail",
|
|
35
|
+
"calendar_month",
|
|
36
|
+
"calendar_today",
|
|
37
|
+
"volume_up",
|
|
38
|
+
"description",
|
|
39
|
+
"folder",
|
|
40
|
+
"play_arrow",
|
|
41
|
+
"grain",
|
|
42
|
+
"check_box",
|
|
43
|
+
"help",
|
|
44
|
+
"check_box_outline_blank",
|
|
45
|
+
"indeterminate_check_box",
|
|
46
|
+
"radio_button_checked",
|
|
47
|
+
"radio_button_unchecked",
|
|
48
|
+
"face",
|
|
49
|
+
"male",
|
|
50
|
+
"female",
|
|
51
|
+
"grid_on",
|
|
52
|
+
"label",
|
|
53
|
+
"delete",
|
|
54
|
+
"pin",
|
|
55
|
+
"location_on",
|
|
56
|
+
"straighten",
|
|
57
|
+
"compare_arrows",
|
|
58
|
+
"swap_horiz",
|
|
59
|
+
"filter_vintage",
|
|
60
|
+
"deceased",
|
|
61
|
+
"apartment",
|
|
62
|
+
"eco",
|
|
63
|
+
"texture",
|
|
64
|
+
"table_chart",
|
|
65
|
+
"bar_chart",
|
|
66
|
+
"bid_landscape",
|
|
67
|
+
"show_chart",
|
|
68
|
+
"ssid_chart",
|
|
69
|
+
"bubble_chart",
|
|
70
|
+
"touch_app",
|
|
71
|
+
"near_me",
|
|
72
|
+
"toggle_off",
|
|
73
|
+
"toggle_on",
|
|
74
|
+
"cancel",
|
|
75
|
+
"book",
|
|
76
|
+
"public",
|
|
77
|
+
"style",
|
|
78
|
+
"restore",
|
|
79
|
+
"payment",
|
|
80
|
+
"shopping_cart",
|
|
81
|
+
"list",
|
|
82
|
+
"ballot",
|
|
83
|
+
"list_alt",
|
|
84
|
+
"view_list",
|
|
85
|
+
"format_list_bulleted",
|
|
86
|
+
"database",
|
|
87
|
+
"database_search",
|
|
88
|
+
"data_table",
|
|
89
|
+
"share",
|
|
90
|
+
"home",
|
|
91
|
+
"arrow_back",
|
|
92
|
+
"arrow_forward",
|
|
93
|
+
"arrow_upward",
|
|
94
|
+
"arrow_downward",
|
|
95
|
+
"arrow_outward",
|
|
96
|
+
"call_made",
|
|
97
|
+
"arrow_back_ios",
|
|
98
|
+
"arrow_forward_ios",
|
|
99
|
+
"favorite",
|
|
100
|
+
"filter_list",
|
|
101
|
+
"sort",
|
|
102
|
+
"view_module",
|
|
103
|
+
"hearing",
|
|
104
|
+
"event_note",
|
|
105
|
+
"image",
|
|
106
|
+
"language",
|
|
107
|
+
"repeat",
|
|
108
|
+
"insert_chart",
|
|
109
|
+
"insert_chart_filled",
|
|
110
|
+
"leaderboard",
|
|
111
|
+
"article",
|
|
112
|
+
"reorder",
|
|
113
|
+
"subject",
|
|
114
|
+
"visibility",
|
|
115
|
+
"visibility_off",
|
|
116
|
+
"arrow_right",
|
|
117
|
+
"arrow_drop_down",
|
|
118
|
+
"tune",
|
|
119
|
+
"settings",
|
|
120
|
+
"manufacturing",
|
|
121
|
+
"preview",
|
|
122
|
+
"mic",
|
|
123
|
+
"laptop_windows",
|
|
124
|
+
"mobile",
|
|
125
|
+
"tablet",
|
|
126
|
+
"table",
|
|
127
|
+
"sell",
|
|
128
|
+
"shoppingmode",
|
|
129
|
+
"lightbulb",
|
|
130
|
+
"content_copy",
|
|
131
|
+
"colors",
|
|
132
|
+
"link",
|
|
133
|
+
"format_bold",
|
|
134
|
+
"format_color_text",
|
|
135
|
+
"format_align_left",
|
|
136
|
+
"fiber_manual_record",
|
|
137
|
+
"breaking_news",
|
|
138
|
+
"release_alert",
|
|
139
|
+
"person_play",
|
|
140
|
+
"comment",
|
|
141
|
+
"mode_comment",
|
|
142
|
+
"fullscreen",
|
|
143
|
+
"fullscreen_exit",
|
|
144
|
+
"filter_alt",
|
|
145
|
+
"open_in_new",
|
|
146
|
+
"search",
|
|
147
|
+
"support",
|
|
148
|
+
"location_searching",
|
|
149
|
+
"explore",
|
|
150
|
+
"assistant_direction",
|
|
151
|
+
"mouse",
|
|
152
|
+
"thumb_up",
|
|
153
|
+
"checklist",
|
|
154
|
+
"progress_activity",
|
|
155
|
+
"reply",
|
|
156
|
+
"percent",
|
|
157
|
+
"space_bar",
|
|
158
|
+
"graph_7",
|
|
159
|
+
"verified",
|
|
160
|
+
"deployed_code",
|
|
161
|
+
"build",
|
|
162
|
+
"code_blocks",
|
|
163
|
+
"inventory",
|
|
164
|
+
"save",
|
|
165
|
+
"data_info_alert",
|
|
166
|
+
"auto_stories",
|
|
167
|
+
"menu_book",
|
|
168
|
+
"book_2",
|
|
169
|
+
"import_contacts",
|
|
170
|
+
"nature",
|
|
171
|
+
"spa",
|
|
172
|
+
"contact_support",
|
|
173
|
+
"work",
|
|
174
|
+
"person_4",
|
|
175
|
+
"price_change",
|
|
176
|
+
"keyboard_arrow_down",
|
|
177
|
+
"keyboard_arrow_up"
|
|
178
|
+
]
|