tlp-icons 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +21 -0
- package/README.md +230 -2
- package/dist/core.js +185 -0
- package/dist/element.js +75 -0
- package/dist/icons/arrows.js +369 -0
- package/dist/icons/brands.js +379 -0
- package/dist/icons/buildings.js +113 -0
- package/dist/icons/charts.js +77 -0
- package/dist/icons/commerce.js +129 -0
- package/dist/icons/communication.js +199 -0
- package/dist/icons/connectivity.js +105 -0
- package/dist/icons/design.js +284 -0
- package/dist/icons/development.js +163 -0
- package/dist/icons/devices.js +425 -0
- package/dist/icons/emoji.js +167 -0
- package/dist/icons/files.js +315 -0
- package/dist/icons/finance.js +223 -0
- package/dist/icons/food.js +119 -0
- package/dist/icons/health.js +145 -0
- package/dist/icons/home.js +127 -0
- package/dist/icons/interface.js +504 -0
- package/dist/icons/layout.js +208 -0
- package/dist/icons/legal.js +123 -0
- package/dist/icons/maps.js +256 -0
- package/dist/icons/math.js +329 -0
- package/dist/icons/media.js +179 -0
- package/dist/icons/nature.js +113 -0
- package/dist/icons/notifications.js +118 -0
- package/dist/icons/people.js +82 -0
- package/dist/icons/photography.js +175 -0
- package/dist/icons/science.js +38 -0
- package/dist/icons/security.js +119 -0
- package/dist/icons/shapes.js +110 -0
- package/dist/icons/sport.js +148 -0
- package/dist/icons/text.js +459 -0
- package/dist/icons/time.js +134 -0
- package/dist/icons/tools.js +52 -0
- package/dist/icons/vehicles.js +134 -0
- package/dist/icons/weather.js +105 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +45 -0
- package/dist/manifest.js +6 -0
- package/dist/tlp-icon.js +12 -0
- package/dist/tlp-icons.all.js +6888 -0
- package/dist/tlp-icons.css +52 -0
- package/dist/tlp-icons.json +1 -0
- package/dist/tlp-icons.runtime.js +265 -0
- package/licenses/lucide.LICENSE.txt +43 -0
- package/licenses/tabler.LICENSE.txt +21 -0
- package/licenses/tlp.LICENSE.txt +21 -0
- package/package.json +61 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tamer and tlp-icons contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Notice
|
|
2
|
+
|
|
3
|
+
tlp-icons is released under the MIT License (see `LICENSE`). It contains icon geometry from the following
|
|
4
|
+
open-source projects. Their license texts are kept in `licenses/`.
|
|
5
|
+
|
|
6
|
+
| Source | License | Copyright | Website |
|
|
7
|
+
| --- | --- | --- | --- |
|
|
8
|
+
| Lucide | ISC | Copyright (c) 2026 Lucide Icons and Contributors | https://lucide.dev |
|
|
9
|
+
| Tabler Icons | MIT | Copyright (c) 2020-2026 Paweł Kuna | https://tabler.io/icons |
|
|
10
|
+
| tlp-icons originals | MIT | Copyright (c) 2026 tlp-icons contributors | https://github.com/Tamer-Mansour/tlp-icons |
|
|
11
|
+
|
|
12
|
+
What tlp-icons adds on top of the upstream geometry: the curated category taxonomy, the motion choreographies
|
|
13
|
+
(hand-made overrides and the rule engine output), part tagging of the SVG elements, the runtime, and the
|
|
14
|
+
"tlp-icons originals" (icons drawn for this project, in `originals/`).
|
|
15
|
+
|
|
16
|
+
## Trademarks
|
|
17
|
+
|
|
18
|
+
The **Brands** category (icons named `brand-*`, from Tabler Icons) shows logos that are trademarks of their
|
|
19
|
+
respective owners (for example Facebook, Instagram and WhatsApp are trademarks of Meta Platforms, Inc.).
|
|
20
|
+
They are included only to identify those products and services. Their inclusion does not imply any
|
|
21
|
+
affiliation with or endorsement by the trademark owners. Follow each owner's brand guidelines when you use them.
|
package/README.md
CHANGED
|
@@ -1,3 +1,231 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://tlp-icons.netlify.app"><img src="https://raw.githubusercontent.com/Tamer-Mansour/tlp-icons/main/web/public/favicon.svg" width="88" height="88" alt="tlp-icons logo"></a>
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">tlp-icons</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<b>Every icon, animated.</b><br>
|
|
9
|
+
<a href="https://tlp-icons.netlify.app/icons/"><b>Browse the icons</b></a> ·
|
|
10
|
+
<a href="https://tlp-icons.netlify.app/docs/"><b>Documentation</b></a> ·
|
|
11
|
+
<a href="https://tlp-icons.netlify.app/categories/brands/">Brands</a> ·
|
|
12
|
+
<a href="https://github.com/Tamer-Mansour/tlp-icons/blob/main/ROADMAP.md">Roadmap</a> ·
|
|
13
|
+
<a href="https://github.com/Tamer-Mansour/tlp-icons">GitHub</a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<img alt="icons" src="https://img.shields.io/badge/icons-6%2C489-4f46e5">
|
|
18
|
+
<img alt="animated" src="https://img.shields.io/badge/animated-100%25-4f46e5">
|
|
19
|
+
<img alt="license" src="https://img.shields.io/badge/license-MIT-16a34a">
|
|
20
|
+
<img alt="dependencies" src="https://img.shields.io/badge/dependencies-0-0ea5e9">
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
A free, open-source (MIT) library of 6,489 stroke icons, built from the best free sets (Lucide and Tabler) plus
|
|
24
|
+
our own **tlp originals**, sorted into 35 hand-curated categories, including **376 brand logos** (Facebook,
|
|
25
|
+
Instagram, WhatsApp, X, TikTok, YouTube...). Every single one moves on hover: clock hands sweep around the face,
|
|
26
|
+
gears spin on their axle, bells swing from the top, trash lids lift, arrows move the way they point.
|
|
27
|
+
|
|
28
|
+
[](https://tlp-icons.netlify.app)
|
|
29
|
+
|
|
30
|
+
<details>
|
|
31
|
+
<summary>Motion frames (each row is one hover animation frozen at 10 points)</summary>
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
</details>
|
|
35
|
+
|
|
36
|
+
- **6,489 icons**: 24×24 grid, 2px stroke, round caps, `currentColor`. One consistent style.
|
|
37
|
+
- **100% animated**: 300+ hand-made choreographies, plus a motion engine that reads each icon's geometry.
|
|
38
|
+
- **Deep fields**: we draw our own icons where the free sets stop. Legal went from 18 to 59 first; the
|
|
39
|
+
[roadmap](https://github.com/Tamer-Mansour/tlp-icons/blob/main/ROADMAP.md) takes it to 1,180 and the whole library to 26,000.
|
|
40
|
+
- **Brand logos**: 376 brands in the same outline style, animated (trademarks of their owners, see `NOTICE.md`).
|
|
41
|
+
- **Tiny runtime**: one custom element, `<tlp-icon>`. Works in plain HTML, React, Angular, Vue, Svelte...
|
|
42
|
+
- **Lazy**: icons load per category, so a page that uses 10 icons doesn't download 6,000.
|
|
43
|
+
- **Polite motion**: plays once to the end, never animates a whole card at once, respects `prefers-reduced-motion`.
|
|
44
|
+
- **Works offline**: the gallery opens straight from disk, and the build never needs the network.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install tlp-icons
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or with no build step, from a CDN:
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<script type="module" src="https://unpkg.com/tlp-icons/dist/tlp-icon.js"></script>
|
|
56
|
+
<tlp-icon name="bell"></tlp-icon>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Offline / no modules (for example opened from `file://`): one classic script with every icon inside (about 2.2 MB):
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<script src="node_modules/tlp-icons/dist/tlp-icons.all.js"></script>
|
|
63
|
+
<tlp-icon name="bell"></tlp-icon>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<tlp-icon name="bell"></tlp-icon>
|
|
70
|
+
<tlp-icon name="settings" size="32" stroke="1.5"></tlp-icon>
|
|
71
|
+
<tlp-icon name="loader-circle" trigger="loop"></tlp-icon>
|
|
72
|
+
<tlp-icon name="heart" trigger="click" duotone></tlp-icon>
|
|
73
|
+
<tlp-icon name="arrow-right" flip="rtl"></tlp-icon>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Attribute | Values | Default |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| `name` | any icon name (or a Lucide alias) | |
|
|
79
|
+
| `size` | number (px) or any CSS length | `24` (or `--tlp-size`) |
|
|
80
|
+
| `stroke` | stroke width | `2` (or `--tlp-stroke`) |
|
|
81
|
+
| `trigger` | `hover`, `loop`, `click`, `none` | `hover` |
|
|
82
|
+
| `duotone` | boolean, adds a soft tinted body fill | off |
|
|
83
|
+
| `flip` | `rtl` (mirror only in right-to-left pages), `x` (always mirror) | off |
|
|
84
|
+
|
|
85
|
+
The icon takes its color from `color`, like text. Theme everything with CSS variables:
|
|
86
|
+
|
|
87
|
+
```css
|
|
88
|
+
tlp-icon { --tlp-size: 20px; --tlp-stroke: 1.75; }
|
|
89
|
+
.card { --tlp-duo: .2; } /* duotone strength */
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Play it from code: `document.querySelector('tlp-icon').play()`.
|
|
93
|
+
|
|
94
|
+
**Hover rule.** Hovering an icon animates it. Hovering anything else walks up to the nearest element that holds
|
|
95
|
+
exactly one icon (a button, a menu item) and animates that icon. If it reaches an element with several icons first
|
|
96
|
+
(a card, a menu, a table row), nothing animates. So hovering a button animates its icon, while a big card never
|
|
97
|
+
lights up all its icons at once.
|
|
98
|
+
|
|
99
|
+
### React
|
|
100
|
+
|
|
101
|
+
```jsx
|
|
102
|
+
import 'tlp-icons/element';
|
|
103
|
+
|
|
104
|
+
export function SaveButton() {
|
|
105
|
+
return <button><tlp-icon name="save" /> Save</button>;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
React 19 supports custom elements directly. For TypeScript JSX, declare `tlp-icon` in `JSX.IntrinsicElements`.
|
|
110
|
+
|
|
111
|
+
### Angular
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// main.ts
|
|
115
|
+
import 'tlp-icons/element';
|
|
116
|
+
|
|
117
|
+
// component
|
|
118
|
+
@Component({ schemas: [CUSTOM_ELEMENTS_SCHEMA], template: `<tlp-icon name="bell"></tlp-icon>` })
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Vue
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// main.ts
|
|
125
|
+
import 'tlp-icons/element';
|
|
126
|
+
// vite.config.ts -> vue({ template: { compilerOptions: { isCustomElement: tag => tag === 'tlp-icon' } } })
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```html
|
|
130
|
+
<tlp-icon name="bell" />
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Raw data and SVG strings (Node / SSR)
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
import { icons, categories, getSvg } from 'tlp-icons';
|
|
137
|
+
|
|
138
|
+
getSvg('bell'); // static SVG markup
|
|
139
|
+
getSvg('bell', { size: 32, stroke: 1.5 });
|
|
140
|
+
getSvg('bell', { animated: true }); // SVG with the motion baked in (needs dist/tlp-icons.css)
|
|
141
|
+
icons.bell; // { svg, m: [[part, anim, dur, delay, origin?], ...] }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Importing `icons` pulls in the whole set. For pages, use `<tlp-icon>`, which loads one category chunk at a time.
|
|
145
|
+
|
|
146
|
+
## Website
|
|
147
|
+
|
|
148
|
+
**https://tlp-icons.netlify.app** is built with **Next.js (React)** in `web/` and uses this package itself
|
|
149
|
+
(`"tlp-icons": "file:.."`). Every page is pre-rendered static HTML for search engines:
|
|
150
|
+
|
|
151
|
+
| Page | URL |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| Home | `/` |
|
|
154
|
+
| Icon gallery (search, filters, size / stroke / color / duotone) | `/icons/` |
|
|
155
|
+
| One page per icon: preview, sizes, buttons, code for React / HTML / Angular / Vue / SVG, related icons | `/icons/<name>/` (6,489 pages) |
|
|
156
|
+
| Categories, one page each, including Brands | `/categories/`, `/categories/<id>/` |
|
|
157
|
+
| Documentation with live examples and the animation library | `/docs/` |
|
|
158
|
+
| Roadmap with live progress | `/roadmap/` |
|
|
159
|
+
| `sitemap.xml`, `robots.txt`, Open Graph image | |
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npm run build # the library (repository root)
|
|
163
|
+
cd web && npm install
|
|
164
|
+
npm run dev # http://localhost:3000
|
|
165
|
+
npm run build # static site in web/out (about 270 MB, 6,700 files)
|
|
166
|
+
node scripts/serve.mjs # serve web/out on http://localhost:4321
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Netlify runs the same steps (see `netlify.toml`) and publishes `web/out` on every push to `main`.
|
|
170
|
+
For a manual deploy, drag the `web/out` folder onto the site's **Deploys** tab in Netlify.
|
|
171
|
+
|
|
172
|
+
**Offline use:** `dist/tlp-icons.all.js` is a single classic script with every icon; it works from `file://`
|
|
173
|
+
with no server and no internet.
|
|
174
|
+
|
|
175
|
+
## How it works
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
npm run sync online: latest Lucide + Tabler -> normalised snapshot in upstream/ -> build
|
|
179
|
+
npm run build offline: upstream/ + config/ + overrides/ + originals/ -> dist/ (library)
|
|
180
|
+
npm test offline: node:test suite (data, motion, keyframes, build idempotency)
|
|
181
|
+
npm run test:browser optional Playwright tests + frame strips (npm run setup:browser once)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- `config/sources.json`: upstream versions (written by sync). Brand logos are on (`includeBrands`).
|
|
185
|
+
- `config/categories.json`: the taxonomy, plus explicit mapping tables for every upstream category, name patterns
|
|
186
|
+
and the featured order.
|
|
187
|
+
- `config/rules.json`: keyword → motion recipe. Data, not code.
|
|
188
|
+
- `overrides/<name>.json`: hand choreographies. These always win over the engine.
|
|
189
|
+
- `originals/<name>.json`: **tlp originals**, icons we draw ourselves (with their category, tags and motion).
|
|
190
|
+
- `config/roadmap.json`: per-category goals and planned icon lists → `ROADMAP.md` (`node scripts/roadmap.mjs`)
|
|
191
|
+
and the `/roadmap/` page.
|
|
192
|
+
- `scripts/adapters/`: one adapter per icon source (`latest`, `fetch`, `load`). Add a set by adding an adapter.
|
|
193
|
+
- `scripts/engine/`: SVG parsing, geometry, the motion engine and serialisation.
|
|
194
|
+
- `src/runtime/`: `core.js` (registry, baking, run engine, hover rule), `element.js` (`<tlp-icon>`) and
|
|
195
|
+
`tlp-icons.css` (the 34 `tlp-` keyframes).
|
|
196
|
+
|
|
197
|
+
**Motion engine.** Every icon is split into atoms (one per element or sub-path) and measured. Then:
|
|
198
|
+
|
|
199
|
+
1. **Hand override**, if there is one.
|
|
200
|
+
2. **Mechanics detectors.** Clocks: find the face and split the hands at its centre, then sweep the minute hand
|
|
201
|
+
and advance the hour hand (it keeps its position between hovers). Gears spin on their axle, and several gears
|
|
202
|
+
turn opposite ways. Arrows: find each arrow head and its shaft, then move them the way they point.
|
|
203
|
+
3. **Composites**: `file-plus`, `user-cog` and `circle-check` animate as base + badge, or container + glyph.
|
|
204
|
+
`-off` icons draw their slash last.
|
|
205
|
+
4. **Keyword rules** from `config/rules.json`: bells swing, trash lids lift, locks open on their hinge, eyes
|
|
206
|
+
blink, charts grow, rain falls, vehicles drive...
|
|
207
|
+
5. **Fallback**: a duotone body fills in, the outline draws, details follow, then a soft finishing gesture.
|
|
208
|
+
The gesture varies so neighbouring icons don't all look the same.
|
|
209
|
+
|
|
210
|
+
Every motion is small and springy and ends exactly at rest. The default `speed` is 0.6, which averages about
|
|
211
|
+
0.9 s per icon (1.56 s max). Change it with `import { config } from 'tlp-icons/element'; config.speed = 0.8`.
|
|
212
|
+
|
|
213
|
+
## Contributing
|
|
214
|
+
|
|
215
|
+
- New icon: add `originals/<name>.json` (24×24, 2px stroke, round caps, parts `p1`..`p4`, `motion` steps).
|
|
216
|
+
Pick one from the planned lists in [ROADMAP.md](https://github.com/Tamer-Mansour/tlp-icons/blob/main/ROADMAP.md). Check it with
|
|
217
|
+
`node scripts/sheet.mjs review <name>` and `node scripts/strips.mjs review <name>` (images in `test/browser/shots/`).
|
|
218
|
+
- Better motion for one icon: add `overrides/<name>.json` with the SVG retagged into parts (`class="p1"`...)
|
|
219
|
+
and its `motion` steps. See any existing override.
|
|
220
|
+
- Better motion for many icons: add or adjust a rule in `config/rules.json`.
|
|
221
|
+
- Wrong category: add a name pattern or an explicit entry in `config/categories.json`.
|
|
222
|
+
- Run `npm run build && npm test` before opening a PR.
|
|
223
|
+
|
|
224
|
+
The GitHub Actions (CI and upstream sync) are manual: run them from the Actions tab when needed.
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT © tlp-icons contributors. Icon geometry from [Lucide](https://lucide.dev) (ISC) and
|
|
229
|
+
[Tabler Icons](https://tabler.io/icons) (MIT), plus tlp originals (MIT). See [NOTICE.md](https://github.com/Tamer-Mansour/tlp-icons/blob/main/NOTICE.md) and `licenses/`.
|
|
230
|
+
|
|
231
|
+
Brand logos are trademarks of their respective owners and are included only to identify their products.
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/* tlp-icons 0.1.0 - MIT. Icons from Lucide 1.55.0 (ISC), Tabler Icons 3.49.0 (MIT). Generated file, do not edit. */
|
|
2
|
+
/* tlp-icons runtime core: icon registry, choreography baking, run engine and the hover rule.
|
|
3
|
+
Plain ES module without imports so the build can also wrap it into a classic <script> bundle. */
|
|
4
|
+
|
|
5
|
+
export const config = { speed: 0.6 };
|
|
6
|
+
|
|
7
|
+
const OUT = 'cubic-bezier(.22,1,.36,1)';
|
|
8
|
+
const TURN = 'cubic-bezier(.65,0,.35,1)';
|
|
9
|
+
const SINE = 'cubic-bezier(.45,.05,.55,.95)';
|
|
10
|
+
export const EASE = {
|
|
11
|
+
draw: 'cubic-bezier(.65,.05,.36,1)', fillIn: 'ease', popIn: OUT, fadeIn: 'ease', slideIn: OUT, riseIn: OUT, dropIn: OUT,
|
|
12
|
+
grow: OUT, growX: OUT, spin: TURN, spinBack: TURN, spinHalf: TURN, sweep: TURN, sweepHour: TURN, orbit: TURN,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
16
|
+
const DRAWABLE = 'path,circle,rect,line,polyline,polygon,ellipse';
|
|
17
|
+
const registry = Object.create(null);
|
|
18
|
+
const aliases = Object.create(null);
|
|
19
|
+
let loader = null;
|
|
20
|
+
|
|
21
|
+
/** Adds icons: { name: { svg, m } }. */
|
|
22
|
+
const waiting = Object.create(null);
|
|
23
|
+
export function register(icons, aliasMap) {
|
|
24
|
+
for (const k in icons) registry[k] = icons[k];
|
|
25
|
+
if (aliasMap) for (const k in aliasMap) aliases[k] = aliasMap[k];
|
|
26
|
+
for (const k in waiting) {
|
|
27
|
+
const icon = getIcon(k);
|
|
28
|
+
if (icon) { waiting[k].forEach(fn => fn(icon)); delete waiting[k]; }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function setLoader(fn) { loader = fn; }
|
|
32
|
+
export function resolve(name) { return registry[name] ? name : aliases[name] || name; }
|
|
33
|
+
export function getIcon(name) { return registry[resolve(name)] || null; }
|
|
34
|
+
/** Resolves with the icon data. Without a loader it waits for a later register() (classic script bundles). */
|
|
35
|
+
export function loadIcon(name) {
|
|
36
|
+
const icon = getIcon(name);
|
|
37
|
+
if (icon) return Promise.resolve(icon);
|
|
38
|
+
if (loader) return Promise.resolve(loader(resolve(name))).then(() => getIcon(name));
|
|
39
|
+
return new Promise(res => { (waiting[name] = waiting[name] || []).push(res); });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Scaled timing of every step and the total length in ms. */
|
|
43
|
+
export function timings(icon, speed = config.speed) {
|
|
44
|
+
let total = 0;
|
|
45
|
+
const steps = (icon.m || []).map(([part, anim, dur, delay, origin]) => {
|
|
46
|
+
const t = Math.round(dur * speed), dl = Math.round((delay || 0) * speed);
|
|
47
|
+
total = Math.max(total, t + dl);
|
|
48
|
+
return { part, anim, t, dl, origin };
|
|
49
|
+
});
|
|
50
|
+
return { steps, total };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const varsOf = s => {
|
|
54
|
+
const v = { '--a': `tlp-${s.anim}`, '--t': `${s.t}ms`, '--dl': `${s.dl}ms`, '--e': EASE[s.anim] || SINE };
|
|
55
|
+
if (s.origin) v['--o'] = s.origin;
|
|
56
|
+
return v;
|
|
57
|
+
};
|
|
58
|
+
const clsOf = s => `p${s.origin && /px/.test(s.origin) ? ' vb' : ''}${s.anim === 'sweepHour' ? ' hh' : ''}`;
|
|
59
|
+
const hasClass = (cls, part) => (` ${cls} `).indexOf(` ${part} `) >= 0;
|
|
60
|
+
|
|
61
|
+
/** Builds a live <svg> element (CSP friendly: CSS variables are set through the CSSOM). */
|
|
62
|
+
export function createSvg(icon, doc = document) {
|
|
63
|
+
const svg = doc.createElementNS(NS, 'svg');
|
|
64
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
65
|
+
svg.setAttribute('class', 'tlp');
|
|
66
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
67
|
+
svg.setAttribute('focusable', 'false');
|
|
68
|
+
svg.innerHTML = icon.svg;
|
|
69
|
+
const { steps, total } = timings(icon);
|
|
70
|
+
svg.dataset.total = String(total);
|
|
71
|
+
for (const s of steps) {
|
|
72
|
+
let targets;
|
|
73
|
+
if (s.part === 'all') {
|
|
74
|
+
const g = doc.createElementNS(NS, 'g');
|
|
75
|
+
while (svg.firstChild) g.appendChild(svg.firstChild);
|
|
76
|
+
svg.appendChild(g);
|
|
77
|
+
targets = [g];
|
|
78
|
+
} else {
|
|
79
|
+
targets = Array.prototype.filter.call(svg.querySelectorAll('[class]'), el => hasClass(el.getAttribute('class'), s.part));
|
|
80
|
+
}
|
|
81
|
+
const vars = varsOf(s);
|
|
82
|
+
for (const el of targets) {
|
|
83
|
+
el.setAttribute('class', `${el.getAttribute('class') || ''} ${clsOf(s)}`.trim());
|
|
84
|
+
for (const k in vars) el.style.setProperty(k, vars[k]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
svg.querySelectorAll(DRAWABLE).forEach(el => el.setAttribute('pathLength', '1'));
|
|
88
|
+
return svg;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Static or animated SVG markup string (animated markup uses inline style for the CSS variables). */
|
|
92
|
+
export function toSvgString(icon, opts = {}) {
|
|
93
|
+
const sw = opts.stroke ?? 2, size = opts.size ?? 24;
|
|
94
|
+
const head = `<svg xmlns="${NS}" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round"`;
|
|
95
|
+
if (!opts.animated) {
|
|
96
|
+
const clean = icon.svg.replace(/ class="([^"]*)"/g, (_, c) => {
|
|
97
|
+
const keep = c.split(/\s+/).filter(x => x === 'd' || x === 'f');
|
|
98
|
+
return keep.length ? ` class="${keep.join(' ')}"` : '';
|
|
99
|
+
});
|
|
100
|
+
const duo = opts.duotone ? '<style>.d{fill:currentColor;fill-opacity:.16}.f{fill:currentColor}</style>' : '<style>.f{fill:currentColor}</style>';
|
|
101
|
+
return `${head}>${clean.includes('class="') ? duo : ''}${clean}</svg>`;
|
|
102
|
+
}
|
|
103
|
+
let inner = icon.svg;
|
|
104
|
+
const { steps, total } = timings(icon);
|
|
105
|
+
for (const s of steps) {
|
|
106
|
+
const style = Object.entries(varsOf(s)).map(([k, v]) => `${k}:${v}`).join(';');
|
|
107
|
+
if (s.part === 'all') { inner = `<g class="${clsOf(s)}" style="${style}">${inner}</g>`; continue; }
|
|
108
|
+
inner = inner.replace(new RegExp(`<([a-z]+)([^>]*?) class="([^"]*\\b${s.part}\\b[^"]*)"`, 'g'), (_, tag, pre, c) => `<${tag}${pre} class="${c} ${clsOf(s)}" style="${style}"`);
|
|
109
|
+
}
|
|
110
|
+
inner = inner.replace(/<(path|circle|rect|line|polyline|polygon|ellipse)\b/g, '<$1 pathLength="1"');
|
|
111
|
+
return `${head} class="tlp" data-tlp-icon data-total="${total}">${inner}</svg>`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const reduced = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
115
|
+
|
|
116
|
+
/** Plays an icon's choreography once. It always finishes, even if the pointer leaves. */
|
|
117
|
+
export function run(svg, delay = 0) {
|
|
118
|
+
if (!svg || svg._tlpBusy || reduced()) return false;
|
|
119
|
+
svg._tlpBusy = true;
|
|
120
|
+
const go = () => {
|
|
121
|
+
svg.classList.remove('tlp-run');
|
|
122
|
+
void svg.getBoundingClientRect();
|
|
123
|
+
svg.classList.add('tlp-run');
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
svg.querySelectorAll('.hh').forEach(h => h.style.setProperty('--h0', `${(parseFloat(h.style.getPropertyValue('--h0')) || 0) + 30}deg`));
|
|
126
|
+
svg.classList.remove('tlp-run');
|
|
127
|
+
svg._tlpBusy = false;
|
|
128
|
+
}, (+svg.dataset.total || 0) + 40);
|
|
129
|
+
};
|
|
130
|
+
if (delay) setTimeout(go, delay); else go();
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const ICON_SELECTOR = 'tlp-icon,svg[data-tlp-icon]';
|
|
135
|
+
const HOST = 'button,a,[role=button],[role=menuitem],[role=tab],[role=option],li,tr,td,th,label,summary,input,select,textarea';
|
|
136
|
+
|
|
137
|
+
function svgOf(icon) { return icon.tagName.toLowerCase() === 'svg' ? icon : icon.querySelector('svg.tlp'); }
|
|
138
|
+
function triggerOf(icon) { return icon.getAttribute('trigger') || icon.getAttribute('data-trigger') || 'hover'; }
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Hover rule: hovering an icon animates it. Otherwise walk up to the nearest element that contains exactly one
|
|
142
|
+
* icon and animate that one. If an element with several icons is reached first (card, menu, table row), do nothing.
|
|
143
|
+
*/
|
|
144
|
+
export function hoverTarget(target) {
|
|
145
|
+
let el = target && target.nodeType === 1 ? target : target && target.parentElement;
|
|
146
|
+
if (!el) return null;
|
|
147
|
+
const own = el.closest(ICON_SELECTOR);
|
|
148
|
+
if (own) return own.closest('tlp-icon') || own;
|
|
149
|
+
for (let depth = 0; el && el.nodeType === 1 && el !== el.ownerDocument.body && depth < 8; depth++, el = el.parentElement) {
|
|
150
|
+
const list = el.querySelectorAll(ICON_SELECTOR);
|
|
151
|
+
let n = 0, found = null;
|
|
152
|
+
for (const i of list) { if (i.tagName.toLowerCase() === 'svg' && i.closest('tlp-icon')) continue; n++; found = i; }
|
|
153
|
+
if (n > 1) return null;
|
|
154
|
+
if (n === 1) return { icon: found, scope: el };
|
|
155
|
+
if (el.matches(HOST)) return null;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let hoverInstalled = false;
|
|
161
|
+
export function installHover(doc = document) {
|
|
162
|
+
if (hoverInstalled) return;
|
|
163
|
+
hoverInstalled = true;
|
|
164
|
+
doc.addEventListener('mouseover', e => {
|
|
165
|
+
const hit = hoverTarget(e.target);
|
|
166
|
+
if (!hit) return;
|
|
167
|
+
const icon = hit.icon || hit;
|
|
168
|
+
const scope = hit.scope || icon;
|
|
169
|
+
if (scope.contains(e.relatedTarget)) return;
|
|
170
|
+
if (triggerOf(icon) !== 'hover') return;
|
|
171
|
+
run(svgOf(icon));
|
|
172
|
+
}, true);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let styleInjected = false;
|
|
176
|
+
export const CSS = "tlp-icon{display:inline-flex;align-items:center;justify-content:center;flex:none;width:var(--tlp-size,24px);height:var(--tlp-size,24px);line-height:0;vertical-align:middle;color:inherit}tlp-icon>svg,svg.tlp{width:100%;height:100%}svg.tlp{fill:none;stroke:currentColor;stroke-width:var(--tlp-stroke,2);stroke-linecap:round;stroke-linejoin:round;overflow:visible}svg.tlp .d{fill:currentColor;fill-opacity:var(--tlp-duo,0)}tlp-icon[duotone],.tlp-duotone{--tlp-duo:.16}tlp-icon[duotone=false]{--tlp-duo:0}svg.tlp .f{fill:currentColor}svg.tlp .p{transform-box:fill-box;transform-origin:var(--o,center)}svg.tlp .p.vb{transform-box:view-box}svg.tlp .p.hh{transform:rotate(var(--h0,0deg))}svg.tlp.tlp-run .p{animation:var(--a) var(--t,1000ms) var(--e,cubic-bezier(.45,.05,.55,.95)) var(--dl,0ms) 1 both}tlp-icon[flip=x]>svg,[dir=rtl] tlp-icon[flip=rtl]>svg{transform:scaleX(-1)}@keyframes tlp-draw{0%{stroke-dasharray:1;stroke-dashoffset:1}100%{stroke-dasharray:1;stroke-dashoffset:0}}@keyframes tlp-fillIn{0%{fill-opacity:0}100%{fill-opacity:var(--tlp-duo,0)}}@keyframes tlp-popIn{0%{opacity:0;transform:scale(.4)}55%{opacity:1;transform:scale(1.08)}78%{transform:scale(.98)}100%{opacity:1;transform:scale(1)}}@keyframes tlp-fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes tlp-slideIn{0%{opacity:0;transform:translateX(-4px)}100%{opacity:1;transform:translateX(0)}}@keyframes tlp-riseIn{0%{opacity:0;transform:translateY(4px)}100%{opacity:1;transform:translateY(0)}}@keyframes tlp-dropIn{0%{opacity:0;transform:translateY(-5px)}60%{opacity:1;transform:translateY(.8px)}80%{transform:translateY(-.3px)}100%{opacity:1;transform:translateY(0)}}@keyframes tlp-grow{0%{transform:scaleY(0)}70%{transform:scaleY(1.06)}100%{transform:scaleY(1)}}@keyframes tlp-growX{0%{transform:scaleX(0)}100%{transform:scaleX(1)}}@keyframes tlp-pop{0%,100%{transform:scale(1)}40%{transform:scale(1.14)}68%{transform:scale(.97)}86%{transform:scale(1.02)}}@keyframes tlp-breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.07)}}@keyframes tlp-jelly{0%,100%{transform:scale(1,1)}30%{transform:scale(1.1,.9)}50%{transform:scale(.94,1.06)}70%{transform:scale(1.03,.97)}85%{transform:scale(.99,1.01)}}@keyframes tlp-swing{0%,100%{transform:rotate(0)}15%{transform:rotate(13deg)}32%{transform:rotate(-10deg)}50%{transform:rotate(6deg)}68%{transform:rotate(-3deg)}84%{transform:rotate(1deg)}}@keyframes tlp-wiggle{0%,100%{transform:rotate(0)}20%{transform:rotate(-8deg)}45%{transform:rotate(6deg)}68%{transform:rotate(-3deg)}86%{transform:rotate(1deg)}}@keyframes tlp-tilt{0%,100%{transform:rotate(0)}40%{transform:rotate(-12deg)}72%{transform:rotate(3deg)}88%{transform:rotate(-1deg)}}@keyframes tlp-nod{0%,100%{transform:rotate(0)}45%{transform:rotate(7deg)}75%{transform:rotate(-1.5deg)}}@keyframes tlp-strike{0%,100%{transform:rotate(0)}38%{transform:rotate(-24deg)}56%{transform:rotate(7deg)}70%{transform:rotate(-2deg)}84%{transform:rotate(.6deg)}}@keyframes tlp-hop{0%,100%{transform:translateY(0) scale(1,1)}10%{transform:translateY(0) scale(1.04,.95)}40%{transform:translateY(-3px) scale(.97,1.04)}68%{transform:translateY(0) scale(1.04,.96)}84%{transform:translateY(0) scale(.99,1.01)}}@keyframes tlp-lift{0%,100%{transform:translateY(0)}45%{transform:translateY(-2.5px)}75%{transform:translateY(.4px)}}@keyframes tlp-sink{0%,100%{transform:translateY(0)}45%{transform:translateY(2.5px)}75%{transform:translateY(-.4px)}}@keyframes tlp-nudge{0%,100%{transform:translateX(0)}45%{transform:translateX(2.6px)}75%{transform:translateX(-.4px)}}@keyframes tlp-nudgeBack{0%,100%{transform:translateX(0)}45%{transform:translateX(-2.6px)}75%{transform:translateX(.4px)}}@keyframes tlp-float{0%,100%{transform:translateY(0)}25%{transform:translateY(-2px)}50%{transform:translateY(0)}75%{transform:translateY(-1.2px)}}@keyframes tlp-flip{0%,100%{transform:scaleX(1)}50%{transform:scaleX(-1)}}@keyframes tlp-spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}@keyframes tlp-spinBack{0%{transform:rotate(0)}100%{transform:rotate(-360deg)}}@keyframes tlp-spinHalf{0%{transform:rotate(0)}80%{transform:rotate(188deg)}100%{transform:rotate(180deg)}}@keyframes tlp-sweep{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}@keyframes tlp-sweepHour{0%{transform:rotate(var(--h0,0deg))}100%{transform:rotate(calc(var(--h0,0deg) + 30deg))}}@keyframes tlp-blink{0%,100%{transform:scaleY(1)}35%{transform:scaleY(.08)}60%{transform:scaleY(1)}}@keyframes tlp-shake{0%,100%{transform:translateX(0)}18%{transform:translateX(-1.6px)}36%{transform:translateX(1.4px)}54%{transform:translateX(-1px)}72%{transform:translateX(.6px)}88%{transform:translateX(-.2px)}}@keyframes tlp-pulseRing{0%,100%{opacity:1;transform:scale(1)}55%{opacity:0;transform:scale(1.5)}56%{opacity:0;transform:scale(.6)}}@keyframes tlp-orbit{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}@keyframes tlp-stretch{0%,100%{transform:scaleX(1)}45%{transform:scaleX(1.12)}75%{transform:scaleX(.98)}}@media (prefers-reduced-motion:reduce){svg.tlp .p{animation:none!important}}";
|
|
177
|
+
export function ensureStyles(doc = document) {
|
|
178
|
+
if (styleInjected || !doc || !doc.head) return;
|
|
179
|
+
styleInjected = true;
|
|
180
|
+
if (doc.getElementById('tlp-icons-style') || CSS.length < 20) return;
|
|
181
|
+
const style = doc.createElement('style');
|
|
182
|
+
style.id = 'tlp-icons-style';
|
|
183
|
+
style.textContent = CSS;
|
|
184
|
+
doc.head.appendChild(style);
|
|
185
|
+
}
|
package/dist/element.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* tlp-icons 0.1.0 - MIT. Icons from Lucide 1.55.0 (ISC), Tabler Icons 3.49.0 (MIT). Generated file, do not edit. */
|
|
2
|
+
/* <tlp-icon name="bell" size="24" stroke="1.75" trigger="hover|loop|click|none" duotone flip="rtl|x"> */
|
|
3
|
+
import { loadIcon, createSvg, run, installHover, ensureStyles } from './core.js';
|
|
4
|
+
|
|
5
|
+
const LOOP_PAUSE = 1200;
|
|
6
|
+
|
|
7
|
+
export class TlpIcon extends HTMLElement {
|
|
8
|
+
static get observedAttributes() { return ['name', 'size', 'stroke', 'trigger']; }
|
|
9
|
+
|
|
10
|
+
connectedCallback() {
|
|
11
|
+
ensureStyles(this.ownerDocument);
|
|
12
|
+
installHover(this.ownerDocument);
|
|
13
|
+
if (!this.hasAttribute('role')) this.setAttribute('aria-hidden', 'true');
|
|
14
|
+
this.render();
|
|
15
|
+
this.setupTrigger();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
disconnectedCallback() {
|
|
19
|
+
clearTimeout(this._loop);
|
|
20
|
+
this._loop = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
attributeChangedCallback(attr) {
|
|
24
|
+
if (!this.isConnected) return;
|
|
25
|
+
if (attr === 'size' || attr === 'stroke') this.applyVars();
|
|
26
|
+
else if (attr === 'name') this.render();
|
|
27
|
+
else if (attr === 'trigger') this.setupTrigger();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
applyVars() {
|
|
31
|
+
const size = this.getAttribute('size');
|
|
32
|
+
const stroke = this.getAttribute('stroke');
|
|
33
|
+
if (size) this.style.setProperty('--tlp-size', /^\d+(\.\d+)?$/.test(size) ? `${size}px` : size);
|
|
34
|
+
else this.style.removeProperty('--tlp-size');
|
|
35
|
+
if (stroke) this.style.setProperty('--tlp-stroke', stroke);
|
|
36
|
+
else this.style.removeProperty('--tlp-stroke');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render() {
|
|
40
|
+
this.applyVars();
|
|
41
|
+
const name = this.getAttribute('name');
|
|
42
|
+
if (!name) { this.replaceChildren(); return; }
|
|
43
|
+
loadIcon(name).then(icon => {
|
|
44
|
+
if (this.getAttribute('name') !== name) return;
|
|
45
|
+
if (!icon) { this.replaceChildren(); this.dataset.missing = ''; return; }
|
|
46
|
+
delete this.dataset.missing;
|
|
47
|
+
this.replaceChildren(createSvg(icon, this.ownerDocument));
|
|
48
|
+
if (icon.r) this.dataset.dir = ''; else delete this.dataset.dir;
|
|
49
|
+
this.dispatchEvent(new CustomEvent('tlp-ready', { bubbles: false }));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setupTrigger() {
|
|
54
|
+
clearTimeout(this._loop);
|
|
55
|
+
this._loop = null;
|
|
56
|
+
const trigger = this.getAttribute('trigger');
|
|
57
|
+
if (trigger === 'loop') {
|
|
58
|
+
const tick = () => {
|
|
59
|
+
const svg = this.querySelector('svg.tlp');
|
|
60
|
+
if (svg) run(svg);
|
|
61
|
+
this._loop = setTimeout(tick, ((svg && +svg.dataset.total) || 900) + LOOP_PAUSE);
|
|
62
|
+
};
|
|
63
|
+
this._loop = setTimeout(tick, 60);
|
|
64
|
+
}
|
|
65
|
+
if (trigger === 'click' && !this._clickBound) {
|
|
66
|
+
this._clickBound = true;
|
|
67
|
+
this.addEventListener('click', () => { if (this.getAttribute('trigger') === 'click') run(this.querySelector('svg.tlp')); });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Plays the animation once. */
|
|
72
|
+
play() { return run(this.querySelector('svg.tlp')); }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof customElements !== 'undefined' && !customElements.get('tlp-icon')) customElements.define('tlp-icon', TlpIcon);
|