@weft-js/warp-icons 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +16 -0
- package/README.md +27 -2
- package/compile.mjs +524 -0
- package/package.json +42 -4
- package/plugin.mjs +33 -0
- package/src/action.tsx +12 -0
- package/src/all.tsx +9 -0
- package/src/gen/actions/bell.css +9 -0
- package/src/gen/actions/bell.tsx +6 -0
- package/src/gen/actions/check.css +10 -0
- package/src/gen/actions/check.tsx +6 -0
- package/src/gen/actions/copy.css +9 -0
- package/src/gen/actions/copy.tsx +6 -0
- package/src/gen/actions/refresh.css +11 -0
- package/src/gen/actions/refresh.tsx +6 -0
- package/src/gen/actions/trash.css +9 -0
- package/src/gen/actions/trash.tsx +6 -0
- package/src/gen/all.tsx +1966 -0
- package/src/gen/index.tsx +5 -0
- package/src/gen/names.ts +6 -0
- package/src/icon.tsx +18 -0
- package/src/icons.css +29 -0
- package/src/late.tsx +36 -0
- package/src/lookup.tsx +9 -0
- package/src/names.tsx +12 -0
- package/src/set.tsx +13 -0
- package/src/svg.tsx +31 -0
- package/src/tiers.css +15 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ravonus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
6
|
+
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
|
7
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
8
|
+
persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
|
11
|
+
Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
|
14
|
+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
15
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
16
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @weft-js/warp-icons
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Early release (0.1.1).** APIs will change between minor versions.
|
|
4
|
+
|
|
5
|
+
weft's own icon set: 651 icons in warp's three expression tiers, each with a hollow and a solid form. `@weft-js/warp`
|
|
6
|
+
depends on it, so an app with warp already has it.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @weft-js/warp-icons
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { CheckIcon } from '@weft-js/warp-icons'
|
|
14
|
+
|
|
15
|
+
export function Saved() {
|
|
16
|
+
return <p><CheckIcon label="Saved" /> Your changes are saved.</p>
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
An icon you don't import costs nothing, and one you do adds about 164 B of brotli.
|
|
21
|
+
|
|
22
|
+
## Docs
|
|
23
|
+
|
|
24
|
+
`weft docs icons` has the names and variants, sizes, tiers, `icons({ tier })` for `weft.config.ts`, and what happens with
|
|
25
|
+
a name the build can't follow. Its source is [packages/cli/docs/icons.md](../cli/docs/icons.md).
|
|
26
|
+
|
|
27
|
+
`npm run sheet` in this folder writes `contact-sheet.html`, where you can browse every icon by tier, variant, size and
|
|
28
|
+
theme.
|
package/compile.mjs
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
// The build's view of icons: walk an app's sources, follow every name to the literals it can be, and ship only those.
|
|
2
|
+
// Names go through variables, conditionals, static lists and maps, imports and props handed down through components.
|
|
3
|
+
// A name it can't follow is legal: that use gets the pack, lazily. An unknown literal is an error. No Rust: the pass is
|
|
4
|
+
// a parse and a walk, so it runs the same in `vite build` and `weft check`.
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import { dirname, extname, join, relative, resolve as path } from 'node:path'
|
|
7
|
+
import { brotliCompressSync, constants } from 'node:zlib'
|
|
8
|
+
import { parseAst } from 'vite'
|
|
9
|
+
|
|
10
|
+
/** warp's expression stops, in order. */
|
|
11
|
+
export const STOPS = ['quiet', 'balanced', 'vivid']
|
|
12
|
+
/** The stop a page has unless it says otherwise. */
|
|
13
|
+
export const DEFAULT_STOP = 'balanced'
|
|
14
|
+
const SKIP = new Set(['node_modules', 'dist', '.weft', '.git', 'target', 'coverage'])
|
|
15
|
+
const CODE = /\.[cm]?[jt]sx?$/
|
|
16
|
+
const WRAPPERS = new Set(['TSAsExpression', 'TSSatisfiesExpression', 'TSNonNullExpression', 'ParenthesizedExpression', 'TSTypeAssertion'])
|
|
17
|
+
const ITERATORS = new Set(['map', 'flatMap', 'filter', 'forEach', 'find', 'some', 'every', 'sort', 'toSorted'])
|
|
18
|
+
const ICON_SOURCES = new Set(['@weft-js/warp-icons/all', '@weft-js/warp-icons/icon', '@weft-js/warp'])
|
|
19
|
+
|
|
20
|
+
const title = name => name.replace(/(^|-)(.)/g, (_, __, c) => c.toUpperCase())
|
|
21
|
+
/** The export a name and variant compile to: `check`, `solid` is `CheckIconSolid`. */
|
|
22
|
+
export const pascal = (name, variant = '') => `${title(name)}Icon${variant ? variant[0].toUpperCase() + variant.slice(1) : ''}`
|
|
23
|
+
/** The variant flags `<Icon>` takes. */
|
|
24
|
+
export const VARIANTS = ['solid']
|
|
25
|
+
/** Which variants an element asks for: '' is hollow, the default. A flag it can't read may be either. */
|
|
26
|
+
function variants(element) {
|
|
27
|
+
const out = new Set(), flags = element.attributes.filter(a => a.type === 'JSXAttribute' && (VARIANTS.includes(a.name.name) || a.name.name === 'action'))
|
|
28
|
+
for (const a of flags) {
|
|
29
|
+
const v = a.value?.type === 'JSXExpressionContainer' ? unwrap(a.value.expression) : null
|
|
30
|
+
if (!a.value || v?.value === true || typeof a.value.value === 'string' || (typeof v?.value === 'string' && v.value)) return { set: new Set([a.name.name]), fixed: flags.length === 1 }
|
|
31
|
+
if (v?.value === false) continue
|
|
32
|
+
out.add(a.name.name); out.add('')
|
|
33
|
+
}
|
|
34
|
+
if (!out.size) out.add('')
|
|
35
|
+
return { set: out, fixed: out.size === 1 }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const line = (code, at) => { const before = code.slice(0, at).split('\n'); return { line: before.length, column: before.at(-1).length + 1 } }
|
|
39
|
+
|
|
40
|
+
function distance(a, b) {
|
|
41
|
+
let row = Array.from({ length: b.length + 1 }, (_, i) => i)
|
|
42
|
+
for (let i = 1; i <= a.length; i++) {
|
|
43
|
+
const next = [i]
|
|
44
|
+
for (let j = 1; j <= b.length; j++) next[j] = Math.min(next[j - 1] + 1, row[j] + 1, row[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1))
|
|
45
|
+
row = next
|
|
46
|
+
}
|
|
47
|
+
return row[b.length]
|
|
48
|
+
}
|
|
49
|
+
/** The name nearest to a typo, for "did you mean": a prefix counts as one edit closer. */
|
|
50
|
+
export const closest = (name, names) => names.reduce((best, n) => { const d = distance(name, n) - (n.startsWith(name) ? 1 : 0); return d < best.d ? { n, d } : best }, { n: null, d: Infinity }).n
|
|
51
|
+
|
|
52
|
+
function sources(root) {
|
|
53
|
+
const out = []
|
|
54
|
+
const walk = dir => {
|
|
55
|
+
for (const entry of readdirSync(dir)) {
|
|
56
|
+
if (SKIP.has(entry) || entry.startsWith('.')) continue
|
|
57
|
+
const p = join(dir, entry)
|
|
58
|
+
if (statSync(p).isDirectory()) { if (entry !== 'test' && entry !== 'tests' && entry !== '__tests__') walk(p) }
|
|
59
|
+
else if (CODE.test(entry) && !/\.(test|spec|d)\./.test(entry) && !/\.config\./.test(entry)) out.push(p)
|
|
60
|
+
else if (extname(entry) === '.html') out.push(p)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
walk(root)
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function moduleFile(from, source) {
|
|
68
|
+
const base = path(dirname(from), source)
|
|
69
|
+
for (const c of [base, ...['.tsx', '.ts', '.jsx', '.js', '.mjs'].map(e => base + e), ...['tsx', 'ts', 'jsx', 'js'].map(e => join(base, `index.${e}`)), base.replace(/\.js$/, '.ts'), base.replace(/\.js$/, '.tsx')])
|
|
70
|
+
if (existsSync(c) && statSync(c).isFile()) return c
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parse(file, code) {
|
|
75
|
+
try { return parseAst(code, { lang: file.endsWith('.tsx') ? 'tsx' : file.endsWith('.ts') ? 'ts' : 'jsx' }) } catch { return null }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** One source: its imports, what its names are bound to, and every place an icon name, a stop or a pack is used. */
|
|
79
|
+
function read(file, code) {
|
|
80
|
+
const unit = { file, code, imports: new Map(), bindings: new Map(), exports: new Map(), uses: [], sites: [], stops: [], related: [], members: [], idents: [], warp: false }
|
|
81
|
+
if (extname(file) === '.html') {
|
|
82
|
+
for (const m of code.matchAll(/data-expression=["']?([a-z]+)/g)) unit.stops.push({ value: m[1] })
|
|
83
|
+
return unit
|
|
84
|
+
}
|
|
85
|
+
for (const m of code.matchAll(/setAttribute\(\s*['"]data-expression['"]\s*,\s*(?:['"]([a-z]+)['"])?/g)) unit.stops.push(m[1] ? { value: m[1] } : { dynamic: true, at: m.index })
|
|
86
|
+
if (/dataset\.expression\s*=/.test(code)) unit.stops.push({ dynamic: true, at: code.search(/dataset\.expression\s*=/) })
|
|
87
|
+
const ast = parse(file, code)
|
|
88
|
+
if (!ast) return unit
|
|
89
|
+
unit.ast = ast
|
|
90
|
+
const bind = (name, binding) => { if (!unit.bindings.has(name)) unit.bindings.set(name, []); unit.bindings.get(name).push(binding) }
|
|
91
|
+
const pattern = (id, make) => {
|
|
92
|
+
if (id?.type === 'Identifier') bind(id.name, make(null))
|
|
93
|
+
else if (id?.type === 'AssignmentPattern') pattern(id.left, make)
|
|
94
|
+
else if (id?.type === 'ObjectPattern') for (const p of id.properties) {
|
|
95
|
+
const key = p.key?.name ?? p.key?.value
|
|
96
|
+
if (key !== undefined) pattern(p.value, inner => make(inner ? [key, ...inner] : [key]))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const component = (name, params) => {
|
|
100
|
+
if (!name || !/^[A-Z]/.test(name)) return
|
|
101
|
+
const first = params?.[0] ?? params?.items?.[0]
|
|
102
|
+
if (first?.type === 'Identifier') bind(first.name, { kind: 'props', comp: name })
|
|
103
|
+
else if (first) pattern(first, keys => ({ kind: 'prop', comp: name, key: keys?.[0] }))
|
|
104
|
+
}
|
|
105
|
+
const visit = node => {
|
|
106
|
+
if (!node || typeof node !== 'object') return
|
|
107
|
+
if (Array.isArray(node)) { for (const n of node) visit(n); return }
|
|
108
|
+
if (typeof node.type !== 'string') return
|
|
109
|
+
switch (node.type) {
|
|
110
|
+
case 'ImportDeclaration': {
|
|
111
|
+
const source = node.source.value
|
|
112
|
+
if (source === '@weft-js/warp' || source.startsWith('@weft-js/warp/')) unit.warp = true
|
|
113
|
+
const target = source.startsWith('.') ? moduleFile(file, source) : null
|
|
114
|
+
if (target) unit.related.push(target)
|
|
115
|
+
for (const s of node.specifiers ?? []) {
|
|
116
|
+
const imported = s.type === 'ImportNamespaceSpecifier' ? '*' : s.type === 'ImportDefaultSpecifier' ? 'default' : s.imported.name ?? s.imported.value
|
|
117
|
+
unit.imports.set(s.local.name, { source, imported, target })
|
|
118
|
+
}
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
case 'ExportNamedDeclaration':
|
|
122
|
+
if (node.source && node.source.value.startsWith('.')) { const t = moduleFile(file, node.source.value); if (t) { unit.related.push(t); for (const s of node.specifiers ?? []) unit.exports.set(s.exported.name, { reexport: t, name: s.local.name ?? s.local.value }) } }
|
|
123
|
+
break
|
|
124
|
+
case 'ExportAllDeclaration':
|
|
125
|
+
if (node.source.value.startsWith('.')) { const t = moduleFile(file, node.source.value); if (t) { unit.related.push(t); (unit.stars ??= []).push(t) } }
|
|
126
|
+
break
|
|
127
|
+
case 'VariableDeclarator':
|
|
128
|
+
if (node.init) {
|
|
129
|
+
pattern(node.id, keys => ({ kind: 'value', expr: node.init, keys }))
|
|
130
|
+
const fn = unwrap(node.init)
|
|
131
|
+
if (node.id.type === 'Identifier' && /Function/.test(fn.type)) component(node.id.name, fn.params)
|
|
132
|
+
}
|
|
133
|
+
break
|
|
134
|
+
case 'FunctionDeclaration':
|
|
135
|
+
component(node.id?.name, node.params)
|
|
136
|
+
break
|
|
137
|
+
case 'CallExpression': {
|
|
138
|
+
const callee = node.callee
|
|
139
|
+
if (callee.type === 'MemberExpression' && !callee.computed && ITERATORS.has(callee.property.name)) {
|
|
140
|
+
const fn = node.arguments[0] && unwrap(node.arguments[0])
|
|
141
|
+
if (fn && /Function/.test(fn.type)) pattern(fn.params?.[0] ?? fn.params?.items?.[0], keys => ({ kind: 'each', of: callee.object, keys }))
|
|
142
|
+
}
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
case 'MemberExpression':
|
|
146
|
+
if (node.object.type === 'Identifier') unit.members.push({ local: node.object.name, node })
|
|
147
|
+
break
|
|
148
|
+
case 'JSXMemberExpression':
|
|
149
|
+
if (node.object.type === 'JSXIdentifier') unit.members.push({ local: node.object.name, node, jsx: true })
|
|
150
|
+
break
|
|
151
|
+
case 'Identifier':
|
|
152
|
+
case 'JSXIdentifier':
|
|
153
|
+
unit.idents.push(node)
|
|
154
|
+
break
|
|
155
|
+
case 'JSXOpeningElement': {
|
|
156
|
+
const comp = node.name.type === 'JSXIdentifier' ? node.name.name : node.name.type === 'JSXMemberExpression' ? `${node.name.object.name}.${node.name.property.name}` : null
|
|
157
|
+
if (!comp) break
|
|
158
|
+
for (const attr of node.attributes) {
|
|
159
|
+
if (attr.type !== 'JSXAttribute' || attr.name.type !== 'JSXIdentifier') continue
|
|
160
|
+
const value = attr.value?.type === 'JSXExpressionContainer' ? attr.value.expression : attr.value
|
|
161
|
+
if (attr.name.name === 'data-expression' && value) unit.stops.push({ expr: value, at: attr.start })
|
|
162
|
+
if (value && /^[A-Z]/.test(comp)) unit.uses.push({ comp, prop: attr.name.name, expr: value, at: attr.start, end: attr.end, element: node })
|
|
163
|
+
}
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
case 'Property':
|
|
167
|
+
case 'ObjectProperty':
|
|
168
|
+
if (!node.computed && (node.key.name ?? node.key.value) === 'icon' && !node.method) unit.sites.push({ soft: true, expr: node.value, at: node.start })
|
|
169
|
+
break
|
|
170
|
+
}
|
|
171
|
+
for (const k of Object.keys(node)) { if (k !== 'type' && k !== 'start' && k !== 'end') visit(node[k]) }
|
|
172
|
+
}
|
|
173
|
+
visit(ast.body)
|
|
174
|
+
return unit
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const isSpecifier = (unit, id) => unit.ast.body.some(n => n.type === 'ImportDeclaration' && n.specifiers?.some(s => s.local === id || s.local.start === id.start))
|
|
178
|
+
const packLocals = unit => [...unit.imports].filter(([, i]) => i.source === '@weft-js/warp-icons' && (i.imported === 'default' || i.imported === '*')).map(([l]) => l)
|
|
179
|
+
const kebab = s => s.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`).replace(/^-/, '')
|
|
180
|
+
const TOOLS = new Set(['iconSet'])
|
|
181
|
+
/** `X`, `XHollow`, `XIcon`, `XIconSolid`: a pack member as a name and a variant. */
|
|
182
|
+
function member(id) {
|
|
183
|
+
const [, base, variant = ''] = /^(.+?)(Hollow|Solid|Action)?$/.exec(id.replace(/Icon(?=$|Hollow$|Solid$|Action$)/, ''))
|
|
184
|
+
return { name: kebab(base), variant: variant === 'Hollow' ? '' : variant.toLowerCase() }
|
|
185
|
+
}
|
|
186
|
+
function packName(id, catalog, names, missing) {
|
|
187
|
+
if (TOOLS.has(id)) return
|
|
188
|
+
const { name, variant } = member(id)
|
|
189
|
+
if (catalog.includes(name)) names.add(variant ? `${name}:${variant}` : name)
|
|
190
|
+
else missing()
|
|
191
|
+
}
|
|
192
|
+
const packIds = catalog => catalog.map(n => title(n))
|
|
193
|
+
|
|
194
|
+
const unwrap = node => { while (node && WRAPPERS.has(node.type)) node = node.expression; return node }
|
|
195
|
+
|
|
196
|
+
/** Everything a build needs to know: the names, the stops, and where either escaped. */
|
|
197
|
+
export function analyze(root, { catalog }) {
|
|
198
|
+
const units = new Map()
|
|
199
|
+
const queue = sources(root)
|
|
200
|
+
while (queue.length) {
|
|
201
|
+
const file = queue.pop()
|
|
202
|
+
if (units.has(file)) continue
|
|
203
|
+
let code
|
|
204
|
+
try { code = readFileSync(file, 'utf8') } catch { continue }
|
|
205
|
+
const unit = read(file, code)
|
|
206
|
+
units.set(file, unit)
|
|
207
|
+
queue.push(...unit.related)
|
|
208
|
+
}
|
|
209
|
+
const usesOf = new Map()
|
|
210
|
+
for (const u of units.values()) for (const use of u.uses) {
|
|
211
|
+
const key = `${use.comp}.${use.prop}`
|
|
212
|
+
if (!usesOf.has(key)) usesOf.set(key, [])
|
|
213
|
+
usesOf.get(key).push({ unit: u, use })
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const empty = () => ({ values: new Set(), shapes: [], dynamic: null })
|
|
217
|
+
const merge = (into, r) => { for (const v of r.values) into.values.add(v); into.shapes.push(...r.shapes); into.dynamic ??= r.dynamic; return into }
|
|
218
|
+
const text = (unit, n) => unit.code.slice(n.start, n.end).slice(0, 60)
|
|
219
|
+
|
|
220
|
+
function exported(file, name, seen) {
|
|
221
|
+
const u = units.get(file)
|
|
222
|
+
if (!u) return { ...empty(), dynamic: `export ${name}` }
|
|
223
|
+
const re = u.exports.get(name)
|
|
224
|
+
if (re?.reexport) return exported(re.reexport, re.name, seen)
|
|
225
|
+
if (u.bindings.has(name)) return identifier(u, name, seen)
|
|
226
|
+
for (const star of u.stars ?? []) { const r = exported(star, name, seen); if (!r.dynamic) return r }
|
|
227
|
+
return { ...empty(), dynamic: `export ${name}` }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function identifier(unit, name, seen) {
|
|
231
|
+
if (name === 'undefined') return empty()
|
|
232
|
+
const id = `${unit.file}#${name}`
|
|
233
|
+
if (seen.has(id)) return empty()
|
|
234
|
+
seen.add(id)
|
|
235
|
+
const imp = unit.imports.get(name)
|
|
236
|
+
if (imp) {
|
|
237
|
+
if (imp.source === '@weft-js/warp-icons' || imp.source.startsWith('@weft-js/warp-icons/')) return empty()
|
|
238
|
+
if (imp.target && imp.imported !== '*') return exported(imp.target, imp.imported, seen)
|
|
239
|
+
return { ...empty(), dynamic: name }
|
|
240
|
+
}
|
|
241
|
+
const list = unit.bindings.get(name)
|
|
242
|
+
if (!list) return { ...empty(), dynamic: name }
|
|
243
|
+
const out = empty()
|
|
244
|
+
for (const b of list) merge(out, binding(unit, b, seen))
|
|
245
|
+
seen.delete(id)
|
|
246
|
+
return out
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function pick(unit, from, keys, seen) {
|
|
250
|
+
let cur = from
|
|
251
|
+
for (const key of keys ?? []) {
|
|
252
|
+
const next = empty(); next.dynamic = cur.dynamic
|
|
253
|
+
for (const { shape, unit: u } of cur.shapes) merge(next, property(u, shape, key, seen))
|
|
254
|
+
cur = next
|
|
255
|
+
}
|
|
256
|
+
return cur
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function binding(unit, b, seen) {
|
|
260
|
+
if (b.kind === 'value') return pick(unit, expression(unit, b.expr, seen), b.keys, seen)
|
|
261
|
+
if (b.kind === 'each') {
|
|
262
|
+
const list = expression(unit, b.of, seen), items = empty()
|
|
263
|
+
items.dynamic = list.dynamic
|
|
264
|
+
for (const { shape, unit: u } of list.shapes) if (shape.type === 'ArrayExpression') for (const el of shape.elements) if (el) merge(items, expression(u, el, seen))
|
|
265
|
+
return pick(unit, items, b.keys, seen)
|
|
266
|
+
}
|
|
267
|
+
if (b.kind === 'prop') return b.key ? forwarded(b.comp, b.key, seen) : { ...empty(), dynamic: `${b.comp} props` }
|
|
268
|
+
return { ...empty(), dynamic: `${b.comp} props` }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function forwarded(comp, prop, seen) {
|
|
272
|
+
const out = empty()
|
|
273
|
+
for (const { unit, use } of usesOf.get(`${comp}.${prop}`) ?? []) merge(out, expression(unit, use.expr, seen))
|
|
274
|
+
return out
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function property(unit, shape, key, seen) {
|
|
278
|
+
const out = empty()
|
|
279
|
+
if (shape.type === 'ObjectExpression') {
|
|
280
|
+
for (const p of shape.properties) {
|
|
281
|
+
if (p.type === 'SpreadElement') { const s = expression(unit, p.argument, seen); for (const x of s.shapes) merge(out, property(x.unit, x.shape, key, seen)); out.dynamic ??= s.dynamic; continue }
|
|
282
|
+
const k = p.key?.name ?? p.key?.value
|
|
283
|
+
if (key === null || k === key) merge(out, expression(unit, p.value, seen))
|
|
284
|
+
}
|
|
285
|
+
} else if (shape.type === 'ArrayExpression') for (const el of shape.elements) if (el) merge(out, expression(unit, el, seen))
|
|
286
|
+
return out
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function expression(unit, raw, seen = new Set()) {
|
|
290
|
+
const node = unwrap(raw)
|
|
291
|
+
if (!node) return empty()
|
|
292
|
+
switch (node.type) {
|
|
293
|
+
case 'Literal':
|
|
294
|
+
case 'StringLiteral':
|
|
295
|
+
return typeof node.value === 'string' ? { ...empty(), values: new Set([node.value]) } : empty()
|
|
296
|
+
case 'TemplateLiteral':
|
|
297
|
+
return node.expressions.length ? { ...empty(), dynamic: text(unit, node) } : { ...empty(), values: new Set([node.quasis[0].value.cooked]) }
|
|
298
|
+
case 'ConditionalExpression':
|
|
299
|
+
return merge(expression(unit, node.consequent, seen), expression(unit, node.alternate, seen))
|
|
300
|
+
case 'LogicalExpression':
|
|
301
|
+
return merge(expression(unit, node.left, seen), expression(unit, node.right, seen))
|
|
302
|
+
case 'ArrayExpression':
|
|
303
|
+
case 'ObjectExpression':
|
|
304
|
+
return { ...empty(), shapes: [{ shape: node, unit }] }
|
|
305
|
+
case 'Identifier':
|
|
306
|
+
return identifier(unit, node.name, seen)
|
|
307
|
+
case 'ArrowFunctionExpression':
|
|
308
|
+
case 'FunctionExpression':
|
|
309
|
+
case 'JSXElement':
|
|
310
|
+
case 'JSXFragment':
|
|
311
|
+
return empty()
|
|
312
|
+
case 'MemberExpression': {
|
|
313
|
+
const object = unwrap(node.object)
|
|
314
|
+
const key = node.computed ? (unwrap(node.property)?.value ?? null) : node.property.name
|
|
315
|
+
if (object.type === 'Identifier') {
|
|
316
|
+
const props = unit.bindings.get(object.name)?.find(b => b.kind === 'props')
|
|
317
|
+
if (props && key) return forwarded(props.comp, key, seen)
|
|
318
|
+
const imp = unit.imports.get(object.name)
|
|
319
|
+
if (imp?.imported === '*' && imp.target && key) return exported(imp.target, key, seen)
|
|
320
|
+
}
|
|
321
|
+
const base = expression(unit, object, seen), out = empty()
|
|
322
|
+
out.dynamic = base.dynamic
|
|
323
|
+
for (const { shape, unit: u } of base.shapes) merge(out, property(u, shape, key, seen))
|
|
324
|
+
return out
|
|
325
|
+
}
|
|
326
|
+
case 'CallExpression': { // a list method hands back the list, or an item of it
|
|
327
|
+
const c = node.callee
|
|
328
|
+
if (c.type === 'MemberExpression' && !c.computed && ['filter', 'sort', 'toSorted', 'slice', 'reverse', 'concat'].includes(c.property.name)) return expression(unit, c.object, seen)
|
|
329
|
+
if (c.type === 'MemberExpression' && !c.computed && c.property.name === 'find') {
|
|
330
|
+
const base = expression(unit, c.object, seen), out = empty()
|
|
331
|
+
out.dynamic = base.dynamic
|
|
332
|
+
for (const { shape, unit: u } of base.shapes) merge(out, property(u, shape, null, seen))
|
|
333
|
+
return out
|
|
334
|
+
}
|
|
335
|
+
return { ...empty(), dynamic: text(unit, node) }
|
|
336
|
+
}
|
|
337
|
+
default:
|
|
338
|
+
return { ...empty(), dynamic: text(unit, node) }
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const names = new Set(), unknown = [], dynamic = [], escaped = [], stops = new Set([DEFAULT_STOP]), stopsDynamic = []
|
|
343
|
+
const known = new Set(catalog)
|
|
344
|
+
const where = (unit, at) => ({ file: relative(root, unit.file), abs: unit.file, ...line(unit.code, at) })
|
|
345
|
+
const site = (unit, expr, at, soft, element) => {
|
|
346
|
+
const r = expression(unit, expr)
|
|
347
|
+
const kinds = element ? variants(element).set : new Set([''])
|
|
348
|
+
for (const v of r.values) {
|
|
349
|
+
if (known.has(v)) for (const k of kinds) {
|
|
350
|
+
if (k === 'action' && !actionList().includes(v)) unknown.push({ name: v, why: actionMessage(v).slice(5), ...where(unit, unwrap(expr).start ?? at) })
|
|
351
|
+
else names.add(k ? `${v}:${k}` : v)
|
|
352
|
+
}
|
|
353
|
+
else if (!soft) unknown.push({ name: v, suggest: closest(v, catalog), ...where(unit, unwrap(expr).start ?? at) })
|
|
354
|
+
}
|
|
355
|
+
if (r.dynamic) dynamic.push({ why: r.dynamic, ...where(unit, at) })
|
|
356
|
+
}
|
|
357
|
+
const iconLocal = (unit, comp) => { const imp = unit.imports.get(comp); return imp && imp.imported === 'Icon' && ICON_SOURCES.has(imp.source) }
|
|
358
|
+
const warpLocal = (unit, comp) => { const imp = unit.imports.get(comp.split('.')[0]); return imp && (imp.source === '@weft-js/warp' || imp.source.startsWith('@weft-js/warp/')) }
|
|
359
|
+
for (const unit of units.values()) {
|
|
360
|
+
for (const use of unit.uses) {
|
|
361
|
+
if (use.prop === 'name' && iconLocal(unit, use.comp)) site(unit, use.expr, use.at, false, use.element)
|
|
362
|
+
else if (use.prop === 'icon' && warpLocal(unit, use.comp)) {
|
|
363
|
+
// a literal becomes the icon itself (`rewrite`), so it needs no entry in every page's table
|
|
364
|
+
const v = literal(use.expr)
|
|
365
|
+
if (v !== undefined && known.has(v)) continue
|
|
366
|
+
site(unit, use.expr, use.at, false)
|
|
367
|
+
}
|
|
368
|
+
if (use.prop === 'tier') { for (const v of expression(unit, use.expr).values) if (STOPS.includes(v)) stops.add(v) }
|
|
369
|
+
}
|
|
370
|
+
if (unit.warp) for (const s of unit.sites) site(unit, s.expr, s.at, true)
|
|
371
|
+
for (const local of packLocals(unit)) {
|
|
372
|
+
const own = unit.members.filter(m => m.local === local), inner = new Set(own.map(m => m.node.object))
|
|
373
|
+
for (const { node } of own) {
|
|
374
|
+
if (node.computed) {
|
|
375
|
+
const r = expression(unit, node.property)
|
|
376
|
+
for (const v of r.values) packName(v, catalog, names, () => unknown.push({ name: v, suggest: closest(v, packIds(catalog)), ...where(unit, node.start) }))
|
|
377
|
+
if (r.dynamic) dynamic.push({ why: r.dynamic, ...where(unit, node.start) })
|
|
378
|
+
} else packName(node.property.name, catalog, names, () => unknown.push({ name: node.property.name, suggest: closest(node.property.name, packIds(catalog)), ...where(unit, node.property.start) }))
|
|
379
|
+
}
|
|
380
|
+
for (const id of unit.idents) if (id.name === local && !inner.has(id) && !isSpecifier(unit, id)) escaped.push({ why: `${local} is used as a value`, ...where(unit, id.start) })
|
|
381
|
+
}
|
|
382
|
+
for (const s of unit.stops) {
|
|
383
|
+
if (s.value) { if (STOPS.includes(s.value)) stops.add(s.value); continue }
|
|
384
|
+
if (s.dynamic) { stopsDynamic.push(where(unit, s.at)); continue }
|
|
385
|
+
const r = expression(unit, s.expr)
|
|
386
|
+
for (const v of r.values) if (STOPS.includes(v)) stops.add(v)
|
|
387
|
+
if (r.dynamic) stopsDynamic.push({ why: r.dynamic, ...where(unit, s.at) })
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return { names, unknown, dynamic, escaped, stops: stopsDynamic.length ? new Set(STOPS) : stops, stopsDynamic, files: units.size }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** The module warp's name lookup becomes in a build: these names and no others, and the pack only if a name escaped. */
|
|
394
|
+
/** @param {Iterable<string>} names */
|
|
395
|
+
export function namesModule(names, { lazy = false, source } = {}) {
|
|
396
|
+
const list = [...names].sort()
|
|
397
|
+
const id = key => pascal(...key.split(':'))
|
|
398
|
+
const imports = list.length ? `import { ${list.map(id).join(', ')} } from '@weft-js/warp-icons'\n` : ''
|
|
399
|
+
const table = `const names = { ${list.map(n => `${JSON.stringify(n)}: ${id(n)}`).join(', ')} }\nconst key = (name, variant) => (variant ? name + ':' + variant : name)\n`
|
|
400
|
+
const packExport = `export const pack = id => { const [, b, v = ''] = /^(.+?)(Hollow|Solid|Action)?$/.exec(id.replace(/Icon(?=$|Hollow$|Solid$|Action$)/, '')); return resolve(b.replace(/[A-Z]/g, c => '-' + c.toLowerCase()).replace(/^-/, ''), v.toLowerCase()) }\n`
|
|
401
|
+
if (source) return `${imports}import { remote } from '@weft-js/warp-icons/late'\n${table}const get = remote(${JSON.stringify(source)})\nexport const resolve = (name, variant) => names[key(name, variant)] ?? get(name, variant)\n${packExport}`
|
|
402
|
+
if (lazy) return `${imports}import { late } from '@weft-js/warp-icons/late'\n${table}const load = () => import('@weft-js/warp-icons/all').then(m => m.lookup)\nexport const resolve = (name, variant) => names[key(name, variant)] ?? late(name, variant, load)\n${packExport}`
|
|
403
|
+
return `${imports}${table}export const resolve = (name, variant) => names[key(name, variant)]\n${packExport}`
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const VISIBLE = { quiet: 'oq', balanced: 'ob', vivid: 'v' }
|
|
407
|
+
/** Drop the drawings no shipped stop can show. With one stop left there is nothing to switch, so the switch goes too. */
|
|
408
|
+
export function trimTiers(source, stops) {
|
|
409
|
+
// a build imports the pack as a namespace (rewrite), and this default would keep every icon alive
|
|
410
|
+
const code = source
|
|
411
|
+
if (stops.size === 3) return code
|
|
412
|
+
const keep = [...stops].map(s => VISIBLE[s]).join('')
|
|
413
|
+
const out = code.replace(/<path data-t="([oqbv])"[^>]*?\/>/g, (m, t) => (keep.includes(t) ? m : ''))
|
|
414
|
+
return stops.size === 1 ? out.replaceAll(/ data-t="[oqbv]"/g, '').replace("import '../tiers.css'\n", '') : out
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const literal = expr => {
|
|
418
|
+
const v = unwrap(expr)
|
|
419
|
+
return (v.type === 'Literal' || v.type === 'StringLiteral') && typeof v.value === 'string' ? v.value : undefined
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** `<Icon name="check">` becomes `<CheckIcon>`, the same output a named import gives, and `icon="check"` on a warp component takes `CheckIcon`. */
|
|
423
|
+
export function rewrite(code, file, catalog) {
|
|
424
|
+
if (!/@weft-js\/(warp-icons|warp)/.test(code)) return null
|
|
425
|
+
const unit = read(file, code)
|
|
426
|
+
if (!unit.ast) return null
|
|
427
|
+
const local = [...unit.imports].find(([, i]) => i.imported === 'Icon' && ICON_SOURCES.has(i.source))?.[0]
|
|
428
|
+
const edits = [], added = new Set()
|
|
429
|
+
// `Icon` alone is a small file over the names module, so this import doesn't bring the set
|
|
430
|
+
for (const n of unit.ast.body) if (n.type === 'ImportDeclaration' && n.source.value === '@weft-js/warp-icons/all' && n.specifiers.length === 1 && n.specifiers[0].imported?.name === 'Icon') edits.push({ start: n.source.start, end: n.source.end, text: "'@weft-js/warp-icons/icon'" })
|
|
431
|
+
// the pack: a default import is the namespace, so a static member is an ordinary named import to the bundler
|
|
432
|
+
let lookups = false
|
|
433
|
+
for (const pack of packLocals(unit)) {
|
|
434
|
+
for (const n of unit.ast.body) if (n.type === 'ImportDeclaration' && n.source.value === '@weft-js/warp-icons') for (const s of n.specifiers) if (s.local.name === pack && s.type === 'ImportDefaultSpecifier') edits.push({ start: s.start, end: s.end, text: `* as ${pack}` })
|
|
435
|
+
for (const { local: l, node } of unit.members) {
|
|
436
|
+
if (l !== pack) continue
|
|
437
|
+
if (!node.computed) { if (catalog) packName(node.property.name, catalog, new Set(), () => { throw Object.assign(new Error(unknownMessage(node.property.name, packIds(catalog))), { loc: { file, ...line(code, node.property.start) } }) }); continue }
|
|
438
|
+
edits.push({ start: node.start, end: node.property.start, text: '__icon(' }, { start: node.property.end, end: node.end, text: ')' })
|
|
439
|
+
lookups = true
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
for (const use of unit.uses) {
|
|
443
|
+
if (use.prop === 'icon') {
|
|
444
|
+
const v = literal(use.expr), imp = unit.imports.get(use.comp.split('.')[0])
|
|
445
|
+
if (v === undefined || !imp || !(imp.source === '@weft-js/warp' || imp.source.startsWith('@weft-js/warp/'))) continue
|
|
446
|
+
if (catalog && !catalog.includes(v)) throw Object.assign(new Error(unknownMessage(v, catalog)), { loc: { file, ...line(code, unwrap(use.expr).start) } })
|
|
447
|
+
const name = pascal(v)
|
|
448
|
+
added.add(name)
|
|
449
|
+
edits.push({ start: use.at, end: use.end, text: `icon={${name}}` })
|
|
450
|
+
continue
|
|
451
|
+
}
|
|
452
|
+
if (!local || use.comp !== local || use.prop !== 'name') continue
|
|
453
|
+
const v = unwrap(use.expr)
|
|
454
|
+
if ((v.type !== 'Literal' && v.type !== 'StringLiteral') || typeof v.value !== 'string') continue
|
|
455
|
+
if (catalog && !catalog.includes(v.value)) throw Object.assign(new Error(unknownMessage(v.value, catalog)), { loc: { file, ...line(code, v.start) } })
|
|
456
|
+
if (!use.element.selfClosing) continue
|
|
457
|
+
const wanted = variants(use.element)
|
|
458
|
+
if (!wanted.fixed) continue
|
|
459
|
+
if (wanted.set.has('action') && !actionList().includes(v.value)) throw Object.assign(new Error(actionMessage(v.value)), { loc: { file, ...line(code, v.start) } })
|
|
460
|
+
const name = pascal(v.value, [...wanted.set][0])
|
|
461
|
+
added.add(name)
|
|
462
|
+
edits.push({ start: use.element.start, end: use.element.name.end, text: `<${name}` }, { start: use.at, end: use.end, text: '' })
|
|
463
|
+
for (const a of use.element.attributes) if (a.type === 'JSXAttribute' && VARIANTS.includes(a.name.name)) edits.push({ start: a.start, end: a.end, text: '' })
|
|
464
|
+
}
|
|
465
|
+
if (!edits.length) return null
|
|
466
|
+
let out = code
|
|
467
|
+
for (const e of edits.sort((a, b) => b.start - a.start)) out = out.slice(0, e.start) + e.text + out.slice(e.end)
|
|
468
|
+
const head = (added.size ? `import { ${[...added].join(', ')} } from '@weft-js/warp-icons'\n` : '') + (lookups ? "import { pack as __icon } from '@weft-js/warp-icons/names'\n" : '')
|
|
469
|
+
return head + out
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** I001 for `action` on an icon that has none. */
|
|
473
|
+
export const actionMessage = name => `I001 no action for icon '${name}', did you mean '${closest(name, actionList())}'? The action icons are ${actionList().join(', ')}.`
|
|
474
|
+
/** I001 for a name that isn't in the catalog. */
|
|
475
|
+
export const unknownMessage = (name, catalog) => `I001 no icon '${name}', did you mean '${closest(name, catalog)}'?`
|
|
476
|
+
|
|
477
|
+
/** The icons that have an action, read from the generated names. */
|
|
478
|
+
export const actionList = () => [...readFileSync(new URL('./src/gen/names.ts', import.meta.url), 'utf8').match(/const actions = (\[.*?\])/)[1].matchAll(/"([a-z0-9-]+)"/g)].map(m => m[1])
|
|
479
|
+
/** Every icon name, read from the generated names. */
|
|
480
|
+
export const list = () => [...new Set([...readFileSync(new URL('./src/gen/names.ts', import.meta.url), 'utf8').split('\n')[1].matchAll(/"([a-z0-9-]+)"/g)].map(m => m[1]))]
|
|
481
|
+
|
|
482
|
+
/** The vite side, loaded by @weft-js/core/vite when @weft-js/warp-icons is in the build. */
|
|
483
|
+
export function pass({ root, command, tier, source }) {
|
|
484
|
+
const build = command === 'build'
|
|
485
|
+
const catalog = list()
|
|
486
|
+
const result = build ? analyze(root, { catalog }) : null
|
|
487
|
+
const lazy = !!result?.dynamic.length
|
|
488
|
+
const stops = tier && tier !== 'all' ? new Set([tier]) : result && !tier ? result.stops : new Set(STOPS)
|
|
489
|
+
return { catalog, result, lazy, stops, module: () => namesModule(result.names, { lazy, source }) }
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** What the pack costs if a page takes it: every icon in one tier, brotli. */
|
|
493
|
+
export function packCost() {
|
|
494
|
+
const code = readFileSync(new URL('./src/gen/all.tsx', import.meta.url), 'utf8')
|
|
495
|
+
return brotliCompressSync(Buffer.from(code), { params: { [constants.BROTLI_PARAM_QUALITY]: 5 } }).length
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** `weft check`'s view: the same walk a build does, as findings. */
|
|
499
|
+
export function findings(root) {
|
|
500
|
+
const catalog = list(), r = analyze(root, { catalog })
|
|
501
|
+
const kb = `${(packCost() / 1024).toFixed(0)} KB`
|
|
502
|
+
return [
|
|
503
|
+
...r.unknown.map(u => ({ code: 'I001', file: u.file, line: u.line, column: u.column, why: u.why ?? `no icon '${u.name}', did you mean '${u.suggest}'?` })),
|
|
504
|
+
...r.dynamic.map(d => ({ code: 'I002', file: d.file, line: d.line, column: d.column, why: `\`${d.why}\` isn't known until runtime, so this use loads the icon pack lazily (about ${kb} br, only on pages that reach it).` })),
|
|
505
|
+
...r.escaped.map(d => ({ code: 'I003', file: d.file, line: d.line, column: d.column, why: `${d.why}, so every icon ships with the page (about ${kb} br).` })),
|
|
506
|
+
]
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Every icon and variant as a static .svg, for a CDN or bucket. Writes files; nothing is uploaded. */
|
|
510
|
+
export async function publish(dir) {
|
|
511
|
+
const { load } = await import('./scripts/build.mjs')
|
|
512
|
+
const { forms } = await import('./scripts/forms.mjs')
|
|
513
|
+
const icons = await load()
|
|
514
|
+
mkdirSync(dir, { recursive: true })
|
|
515
|
+
const svg = html => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">${html}</svg>\n`
|
|
516
|
+
let files = 0
|
|
517
|
+
for (const [name, entry] of Object.entries(icons)) {
|
|
518
|
+
const f = forms(name, entry)
|
|
519
|
+
const all = { '': f.hollow.line + f.hollow.pixels, '.solid': f.solid.line + f.solid.pixels }
|
|
520
|
+
for (const [suffix, html] of Object.entries(all)) { writeFileSync(join(dir, `${name}${suffix}.svg`), svg(html)); files++ }
|
|
521
|
+
}
|
|
522
|
+
writeFileSync(join(dir, 'manifest.json'), JSON.stringify({ icons: Object.keys(icons), variants: ['', 'solid'] }) + '\n')
|
|
523
|
+
return { dir, files, icons: Object.keys(icons).length }
|
|
524
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weft-js/warp-icons",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Icons for warp and weft",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"*.css"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/gen/index.tsx",
|
|
12
|
+
"./all": "./src/all.tsx",
|
|
13
|
+
"./quiet": "./src/gen/all.tsx",
|
|
14
|
+
"./balanced": "./src/gen/all.tsx",
|
|
15
|
+
"./vivid": "./src/gen/all.tsx",
|
|
16
|
+
"./names": "./src/names.tsx",
|
|
17
|
+
"./icon": "./src/icon.tsx",
|
|
18
|
+
"./late": "./src/late.tsx",
|
|
19
|
+
"./compile": "./compile.mjs",
|
|
20
|
+
"./plugin": "./plugin.mjs",
|
|
21
|
+
"./icons.css": "./src/icons.css",
|
|
22
|
+
"./tiers.css": "./src/tiers.css"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"vite": "*",
|
|
26
|
+
"@weft-js/core": "^0.1.1"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=26"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"weft"
|
|
36
|
+
],
|
|
37
|
+
"files": [
|
|
38
|
+
"src",
|
|
39
|
+
"plugin.mjs",
|
|
40
|
+
"compile.mjs",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
]
|
|
44
|
+
}
|
package/plugin.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// icons() for weft.config.ts. With no tier, every icon carries its three tiers and CSS shows the one for the nearest
|
|
2
|
+
// data-expression. `tier` compiles one in: `@weft-js/warp-icons` resolves to that tier alone, warp's own icons included.
|
|
3
|
+
import { definePlugin } from '@weft-js/core/plugin'
|
|
4
|
+
|
|
5
|
+
const TIERS = ['quiet', 'balanced', 'vivid']
|
|
6
|
+
|
|
7
|
+
function options(input) {
|
|
8
|
+
for (const key of Object.keys(input)) if (key !== 'tier' && key !== 'source') throw Object.assign(new Error(`unknown option \`${key}\``), { path: key })
|
|
9
|
+
if (input.tier !== undefined && !TIERS.includes(input.tier)) throw Object.assign(new Error(`must be one of ${TIERS.join(', ')}`), { path: 'tier' })
|
|
10
|
+
if (input.source !== undefined && !/^https?:\/\/.+\/$/.test(input.source)) throw Object.assign(new Error('must be an http(s) URL ending in /'), { path: 'source' })
|
|
11
|
+
return { ...(input.tier && { tier: input.tier }), ...(input.source && { source: input.source }) }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const agents = "Import icons from `@weft-js/warp-icons`: `CheckIcon` is hollow, the default, `CheckIconSolid` the other form (`<Icon name=\"check\" solid />`); `<Icons.Check />` and warp's `<Icon name=\"check\" />` build to the same bytes. Tiers are a prop, `tier`, or the page's `data-expression`. Decorative by default; `label` gives one a name. An unknown literal name is I001 at the build and in `weft check`. A name only known at runtime is legal and loads the pack lazily (I002 reports it), or single files with `icons({ source })` and `weft icons publish <dir>`. `trash`, `bell`, `check`, `refresh` and `copy` animate with `action`. `weft docs icons` has the rest."
|
|
15
|
+
|
|
16
|
+
/** `icons()` for `weft.config.ts`: pins a tier if asked, and tells agents how the names work. */
|
|
17
|
+
export const icons = definePlugin({
|
|
18
|
+
name: '@weft-js/warp-icons',
|
|
19
|
+
version: '0.1.0',
|
|
20
|
+
weft: '*',
|
|
21
|
+
options,
|
|
22
|
+
contribute(o) {
|
|
23
|
+
return {
|
|
24
|
+
tokens: { namespace: 'icons', values: { tier: o.tier ?? 'runtime' }, ...((o.tier || o.source) && { alias: { ...(o.tier && { '@weft-js/warp-icons': `@weft-js/warp-icons/${o.tier}` }), ...(o.source && { '@weft-js/warp-icons/source': o.source }) } }) },
|
|
25
|
+
agents: { title: 'Icons', text: agents },
|
|
26
|
+
catalog: [{ name: 'Icon', import: '@weft-js/warp-icons/all', summary: 'Any icon by name, for names that come from data; costs the whole set' }],
|
|
27
|
+
map: [{ want: 'An icon', call: 'Import `NameIcon` from `@weft-js/warp-icons`; `npm run sheet` in the package draws them all', have: 'one export per icon' }],
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
/** `icons()`. */
|
|
33
|
+
export default icons
|