leanweb 3.0.1 → 3.0.2

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 CHANGED
@@ -1,693 +1 @@
1
- # <a href="https://leanweb.app"><img src='https://leanweb.app/favicon.svg' alt='Leanweb' width='32'/></a> Leanweb
2
-
3
- Builds framework agnostic web components.
4
-
5
- ## Installation
6
-
7
- - `npm install leanweb -g` as a global tool, or
8
- - `npm install leanweb -D` in the project as a dev dependency.
9
-
10
- If leanweb is installed as a dev dependency, you will need to run
11
- `npx lw`, otherwise just run `lw` if it is installed as global tool.
12
-
13
- I don't see any reason leanweb should be installed as `npm install leanweb`.
14
-
15
- ## Background
16
-
17
- I like the idea in Angular that 3 files (html/js/scss) as a component are in
18
- charge of a box, like a div, a rectangle area. But I don't like Angular in that
19
- my code has to be depending on so many bloated dependencies to run. I created
20
- leanweb as a set of tools to help create web components based web projects,
21
- which:
22
-
23
- - are based on native DOM and web components api
24
- - are pure Javascript, no fancy framework
25
- - are assistive, not restrictive
26
- - are more standards, less proprietary
27
- - are built to last
28
-
29
- The principle is simply that 3 files (html/js/scss) as a web component will
30
- control a box.
31
-
32
- ## Getting started
33
-
34
- In this demo, I assume leanweb is installed as a global tool by running
35
-
36
- ```
37
- npm i leanweb -g
38
- ```
39
-
40
- ### `leanweb init` or `lw init`
41
-
42
- Create a directory called `demo` for this demo project.
43
-
44
- ```bash
45
- $ mkdir demo
46
- $ cd demo
47
- demo$ lw init
48
- demo$
49
- ```
50
-
51
- Now a `src/` directory are created at the project root. `src/leanweb.json`
52
- looks like:
53
-
54
- ```json
55
- {
56
- "name": "demo",
57
- "version": "0.4.5",
58
- "components": ["root"],
59
- "resources": ["resources/"]
60
- }
61
- ```
62
-
63
- which suggests a root web component `demo-root` is created. In `src/` directory,
64
- an `index.html`, an empty `demo.scss` and an empty `global-styles.scss` files
65
- are created, in `global-styles.scss` we can add global styles. `demo-root` web
66
- component directory is created at `src/components/root/`. There are 3 files in
67
- this directory:
68
-
69
- - root.html
70
- - root.js
71
- - root.scss
72
-
73
- `root.html`
74
-
75
- ```html
76
- <div>demo-root works!</div>
77
- ```
78
-
79
- `root.js` defines your new web component `demo-root`, which is a web component
80
- based on standard DOM api.
81
-
82
- `root.js`
83
-
84
- ```javascript
85
- import LWElement from "./../../lib/lw-element.js";
86
- import ast from "./ast.js";
87
-
88
- customElements.define(
89
- "demo-root",
90
- class extends LWElement {
91
- // LWElement extends HTMLElement
92
- constructor() {
93
- super(ast);
94
- }
95
- }
96
- );
97
- ```
98
-
99
- `root.scss` is empty, which is for you to add web component specific styles.
100
-
101
- ### `leanweb serve` or `lw serve`
102
-
103
- Run `lw serve` and you should see a browser window open. Try make some
104
- changes in the code, and save, the browser should refresh automatically to
105
- reflect your changes.
106
- <img src='https://leanweb.app/images/leanweb-serve.png' alt='lw serve' width='640'/>
107
-
108
- ### `leanweb generate` or `lw generate`
109
-
110
- Let's create a `login` web component with `lw generate` or `lw g`.
111
-
112
- ```bash
113
- demo$ lw g login
114
- demo$
115
- ```
116
-
117
- Now the `leanweb.json` has one more entry in the component list:
118
-
119
- ```json
120
- {
121
- "name": "demo",
122
- "version": "0.4.5",
123
- "components": ["root", "login"],
124
- "resources": ["resources/"]
125
- }
126
- ```
127
-
128
- `demo-login` is the newly generated web component. The web component name is
129
- prefixed with project name `demo-`. Inside `src/components/`, a new web
130
- component directory `login` is created containing 3 files:
131
-
132
- - login.html
133
- - login.js
134
- - login.scss
135
-
136
- Now let's make two changes, first open up `src/components/root/root.html`, and
137
- add a new line `<demo-login></demo-login>`. The new `root.html` should look
138
- like the following after the change:
139
-
140
- ```html
141
- <div>demo-root works!</div>
142
- <demo-login></demo-login>
143
- ```
144
-
145
- Then open up `src/components/login/login.scss`, and add the following style:
146
-
147
- ```scss
148
- div {
149
- color: red;
150
- }
151
- ```
152
-
153
- And you should see the changes in the browser. Please note the styles added to
154
- the `login` component does not affect other components.
155
-
156
- <img src='https://leanweb.app/images/leanweb-serve-1.png' alt='lw serve' width='640'/>
157
-
158
- ### `leanweb dist` or `lw dist`
159
-
160
- Run `lw dist`, and a `dist` directory will be created with minified files
161
- for production.
162
-
163
- ### `leanweb clean` or `lw clean`
164
-
165
- `lw clean` will delete `build/` and `dist/` directories.
166
-
167
- ### `leanweb upgrade` or `lw u`
168
-
169
- `lw upgrade` will upgrade `src/lib/` directory if there is a new version
170
- available.
171
-
172
- ### `leanweb destroy` or `lw destroy`
173
-
174
- `lw destroy project-name` will remove the `src/`, `build/` and `dist/`
175
- directory. Please note the `src/` directory will be deleted by this command.
176
-
177
- ### `leanweb help` or `lw help`
178
-
179
- `lw help command-name` will print help information for the command. For
180
- example, `lw help dist` or `lw h di` will print help information for
181
- `lean dist`.
182
-
183
- ### `leanweb version` or `lw version`
184
-
185
- `lw version` will print version information.
186
-
187
- ## lw directives
188
-
189
- ### lw
190
-
191
- Contents inside a tag with `lw` directive are considered expressions that will
192
- be evaluated. In the example below, the `<span lw>name</span>` will be
193
- evaluated as `<span>Leanweb</span>`, because the variable `name` is defined
194
- in the web component js file with the value `Leanweb`.
195
-
196
- ```html
197
- Hello <span lw>name</span>!
198
- ```
199
-
200
- ```javascript
201
- // ...
202
- name = "Leanweb";
203
- // ...
204
- ```
205
-
206
- ```
207
- Hello Leanweb!
208
- ```
209
-
210
- ### lw-if
211
-
212
- ```html
213
- <span lw-if='name==="Leanweb"'>Leanweb</span>
214
- ```
215
-
216
- The `span` DOM node will be shown if `name==="Leanweb"` will evaluate true,
217
- otherwise, it will not be shown.
218
-
219
- ### lw-for
220
-
221
- The following example shows how `lw-for` directive helps to generate DOM nodes
222
- for each `item` in the `items` array.
223
-
224
- ```html
225
- <div lw lw-for="item, $index in items">$index+': '+item</div>
226
- ```
227
-
228
- ```javascript
229
- // ...
230
- items = ["one", "two", "three"];
231
- // ...
232
- ```
233
-
234
- ```
235
- 0: one
236
- 1: two
237
- 2: three
238
- ```
239
-
240
- #### access DOM from lw-for
241
-
242
- You could access DOM nodes for each element in a `lw-for` loop by calling
243
- `elem.getDom()` as long as `typeof elem` evaluates `object`.
244
-
245
- ### lw-model and lw-on:
246
-
247
- ```html
248
- <input type="text" lw-model="name" />
249
- <span lw>name</span>
250
- <br />
251
- <button lw-on:click="resetName()">Reset Name</button>
252
- ```
253
-
254
- You could bind multiple events like `lw-on:click,change=handler($event, $node)`.
255
-
256
- ```javascript
257
- // ...
258
- resetName() {
259
- this.name = 'Leanweb';
260
- }
261
- // ...
262
- ```
263
-
264
- <img src='https://leanweb.app/images/lw-model.gif' alt='lw-model'/>
265
-
266
- ### lw-class:
267
-
268
- ```html
269
- <div lw lw-for="item, $index in items" lw-class:active="isActive($index)">
270
- item
271
- </div>
272
- ```
273
-
274
- ```javascript
275
- // ...
276
- items = ['one', 'two', 'three'];
277
- isActive(index) {
278
- return index === 1;
279
- }
280
- // ...
281
- ```
282
-
283
- ```scss
284
- .active {
285
- color: red;
286
- }
287
- ```
288
-
289
- <img src='https://leanweb.app/images/lw-class.png' alt='lw-class' width='640'/>
290
-
291
- ### lw-bind:
292
-
293
- ```html
294
- <img lw-bind:src="imgSrc" lw-bind:width="imageWidth" />
295
- ```
296
-
297
- ```javascript
298
- // ...
299
- imgSrc = "https://leanweb.app/images/az.gif";
300
- imageWidth = 400;
301
- // ...
302
- ```
303
-
304
- <img src='https://leanweb.app/images/lw-bind.png' alt='lw-bind' width='640'/>
305
-
306
- ### lw-input:
307
-
308
- `lw-input` is used to pass and share data from parent to children.
309
-
310
- `demo-parent.html`
311
-
312
- ```html
313
- <demo-child lw-input:parent="this" lw-input:userData="user"></demo-child>
314
- ```
315
-
316
- `demo-parent.js`
317
-
318
- ```javascript
319
- // ...
320
- user = { firstname: "Qian", lastname: "Chen" };
321
- // ...
322
- ```
323
-
324
- The child is able to access the `parent` and `user` object passed in with
325
- `lw-input:` directive from `inputReady()` method.
326
- `demo-child.js`
327
-
328
- ```javascript
329
- // ...
330
- inputReady() {
331
- console.log(this.parent);
332
- console.log(this.userData);
333
- }
334
- // ...
335
- ```
336
-
337
- ## Form Binding
338
-
339
- Here is a few examples how Leanweb helps web components work with form binding.
340
-
341
- ### Checkbox
342
-
343
- #### Multiple chechboxes bound to an array
344
-
345
- ```javascript
346
- // ...
347
- items = ['one', 'two', 'three'];
348
- toggleCheckboxes() {
349
- if (this.checkedValues.length) {
350
- this.checkedValues.length = 0;
351
- } else {
352
- this.checkedValues = [...this.items];
353
- }
354
- }
355
- checkedValues = [];
356
- // ...
357
- ```
358
-
359
- ```html
360
- <button lw-on:click="toggleCheckboxes()">Toggle Checkboxes</button>
361
- <div lw-for="item, $index in items">
362
- <input type="checkbox" lw-bind:value="item" lw-model="checkedValues" />
363
- <span lw>item</span>
364
- </div>
365
- <span lw>checkedValues</span>
366
- ```
367
-
368
- <img src='https://leanweb.app/images/leanweb-form-binding-checkbox.gif' alt='Leanweb Form Binding Checkbox'/>
369
-
370
- #### Single checkbox bound to a boolean value
371
-
372
- ```javascript
373
- checked = false;
374
- toggleCheckbox() {
375
- this.checked = !this.checked;
376
- }
377
- ```
378
-
379
- ```html
380
- <button lw-on:click="toggleCheckbox()">Toggle Checkbox</button>
381
- <div>
382
- <input type="checkbox" lw-model="checked" />
383
- <span lw>checked</span>
384
- </div>
385
- ```
386
-
387
- ### Select
388
-
389
- ```javascript
390
- // ...
391
- items = ['one', 'two', 'three'];
392
- selectTwo() {
393
- this.selectedOption = 'two';
394
- }
395
- selectedOption;
396
- // ...
397
- ```
398
-
399
- ```html
400
- <button lw-on:click="selectTwo()">Select Two</button>
401
- <div>
402
- <select lw-model="selectedOption">
403
- <option lw lw-for="item, $index in items">item</option>
404
- </select>
405
- </div>
406
- <span lw> selectedOption </span>
407
- ```
408
-
409
- <img src='https://leanweb.app/images/leanweb-form-binding-select.gif' alt='Leanweb Form Binding Select' />
410
-
411
- ### Multiple Select
412
-
413
- ```javascript
414
- // ...
415
- items = ['one', 'two', 'three'];
416
- toggleAllOptions() {
417
- if (this.selectedOptions.length) {
418
- this.selectedOptions.length = 0;
419
- } else {
420
- this.selectedOptions = [...this.items];
421
- }
422
- }
423
- selectedOptions = [];
424
- // ...
425
- ```
426
-
427
- ```html
428
- <button lw-on:click="toggleAllOptions()">Toggle All</button>
429
- <div>
430
- <select lw-model="selectedOptions" multiple>
431
- <option lw lw-for="item, $index in items">item</option>
432
- </select>
433
- </div>
434
- <span lw> selectedOptions </span>
435
- ```
436
-
437
- <img src='https://leanweb.app/images/leanweb-form-binding-multiple-select.gif' alt='Leanweb Form Binding Multiple Select' />
438
-
439
- ### Radio Button
440
-
441
- ```javascript
442
- // ...
443
- items = ['one', 'two', 'three'];
444
- chooseTwo() {
445
- this.picked = 'two';
446
- }
447
- picked;
448
- // ...
449
- ```
450
-
451
- ```html
452
- <button lw-on:click="chooseTwo()">Choose Two</button>
453
- <div lw-for="item, $index in items">
454
- <input
455
- type="radio"
456
- name="pickOne"
457
- lw-bind:value="item"
458
- lw-model="picked"
459
- /><span lw>item</span>
460
- </div>
461
- <span lw>picked</span>
462
- ```
463
-
464
- <img src='https://leanweb.app/images/leanweb-form-binding-radio-button.gif' alt='Leanweb Form Binding Radio Button' />
465
-
466
- ### Range
467
-
468
- ```javascript
469
- // ...
470
- selectRange50() {
471
- this.selectedRange = 50;
472
- }
473
- selectedRange = 10;
474
- // ...
475
- ```
476
-
477
- ```html
478
- <button lw-on:click="selectRange50()">Select Range 50</button> <br />
479
- <input type="range" lw-model="selectedRange" />
480
- <span lw>selectedRange</span>
481
- ```
482
-
483
- <img src='https://leanweb.app/images/leanweb-form-binding-range.gif' alt='Leanweb Form Binding Range' />
484
-
485
- ## Component Communication
486
-
487
- The following project demonstrates how Leanweb helps web components to talk to
488
- each other.
489
-
490
- <img src='https://leanweb.app/images/leanweb-pub-sub.gif' alt='Leanweb Component Communication'/>
491
-
492
- `pub.js`
493
-
494
- ```javascript
495
- // import LWElement from './../../lib/lw-element.js';
496
- // import ast from './ast.js';
497
-
498
- // customElements.define('demo-pub',
499
- // class extends LWElement { // LWElement extends HTMLElement
500
- // constructor() {
501
- // super(ast);
502
-
503
- setInterval(() => {
504
- this.time = new Date(Date.now()).toLocaleString();
505
- leanweb.eventBus.dispatchEvent("time", this.time);
506
- this.update();
507
- }, 1000);
508
-
509
- // }
510
- // }
511
- // );
512
- ```
513
-
514
- `pub.html`
515
-
516
- ```html
517
- <div class="pub">
518
- <span>Time Publisher</span>
519
- <span lw>time</span>
520
- </div>
521
- ```
522
-
523
- `sub.js`
524
-
525
- ```javascript
526
- // import LWElement from './../../lib/lw-element.js';
527
- // import ast from './ast.js';
528
-
529
- // customElements.define('demo-sub',
530
- // class extends LWElement { // LWElement extends HTMLElement
531
- // constructor() {
532
- // super(ast);
533
- // }
534
-
535
- sub() {
536
- this.listener = leanweb.eventBus.addEventListener('time', event => {
537
- this.time = event.data;
538
- this.update();
539
- });
540
- this.subscribed = true;
541
- }
542
-
543
- unsub() {
544
- leanweb.eventBus.removeEventListener(this.listener);
545
- this.subscribed = false;
546
- }
547
- // }
548
- // );
549
- ```
550
-
551
- `sub.html`
552
-
553
- ```html
554
- <div class="sub">
555
- <span>Time Subscriber</span>
556
- <span lw>time</span>
557
- <div class="buttons">
558
- <button lw-bind:disabled="subscribed" lw-on:click="sub()">
559
- Subscribe Time
560
- </button>
561
- <button lw-bind:disabled="!subscribed" lw-on:click="unsub()">
562
- UnSubscribe Time
563
- </button>
564
- </div>
565
- </div>
566
- ```
567
-
568
- Source code of this demo https://github.com/elgs/leanweb-pub-sub-demo.
569
-
570
- ## API
571
-
572
- ### globalThis.leanweb
573
-
574
- `leanweb` is the only foot print on `globalThis` scope.
575
-
576
- #### updateComponents(...tagNames)
577
-
578
- `updateComponents` is used to update all component DOMs or DOMs of specific
579
- component tag names. `updateComponents` takes any number of component tag
580
- names as arguments. If no argument is provided, it will update all component
581
- DOMs app wide.
582
-
583
- #### eventBus
584
-
585
- An instance of `LWEventBus` managed by `leanweb` to pass DOM update events.
586
-
587
- #### urlHash
588
-
589
- `urlHash` is a reference to `window.location.hash` which can be used for
590
- routing.
591
-
592
- #### urlHashPath
593
-
594
- `urlHashPath` is used to set or get the `path` part in the urlHash. If the
595
- `urlHash` is `#/login?a=b&a=b&c=d`, `urlHashPath` will be `#/login`.
596
-
597
- #### urlHashParams
598
-
599
- `urlHashParams` is used to set or get the `parameters` in the urlHash. If the
600
- `urlHash` is `#/login?a=b&a=b&c=d`, `urlHashParams` will be
601
- `{a: ['b', 'b'], c: 'd'}`.
602
-
603
- ### LWElement
604
-
605
- `LWElement` extends `HTMLElement`, and Leanweb components extend `LWElement`.
606
- So Leanweb components are just more specific versions of the standard
607
- `HTMLElement`. `LWElement` helps to wire up the `lw` directives in the HTML and
608
- provides some convenient methods to update the DOM.
609
-
610
- #### update(rootNode = this.shadowRoot)
611
-
612
- The `update` method provides a convenient way to update the DOM when the model
613
- changes. You should feel free to use old way to update DOM. The `update` just
614
- makes life a little easier. `update` takes `rootNode` as parameter, which
615
- allows you to specify which DOM element to start with. The default value is
616
- the current`shadowRoot`.
617
-
618
- LWElement will call update in the following scenarios:
619
-
620
- 1. after all `lw` directives are initially bound to DOM;
621
- 2. after `lw-on:` event is fired;
622
- 3. after `lw-model` change is fired;
623
-
624
- You may need to call the `update()` method manually in other events. For
625
- example:
626
-
627
- 1. in your setTimeout/setInterval callbacks;
628
- 2. in `LWEventBus` callbacks;
629
- 3. in any network api callbacks;
630
-
631
- #### domReady()
632
-
633
- `domReady()` will be called after all initial DOM events are bound, and all
634
- DOM interpolations are evaluated. This method is meant to be overridden and is a
635
- great place to send events to the event bus.
636
-
637
- #### inputReady()
638
-
639
- `inputReady()` will be called after all input data from parent's `lw-input:`
640
- are ready. In this method, children are able to access the passed in data
641
- shared by parents.
642
-
643
- #### turnedOn()
644
-
645
- Called when the componnet `lw-if` is evaluated `true`.
646
-
647
- #### turnedOff()
648
-
649
- Called when the componnet `lw-if` is evaluated `false`.
650
-
651
- #### urlHashChanged()
652
-
653
- If `urlHashChanged()` is defined as a function, it will be called whenever the
654
- urlHash changes. This could be useful to update the DOM in component routing.
655
-
656
- ### LWEventBus
657
-
658
- `LWElement` comes with a global instance of `LWEventBus` that helps web
659
- components to talk to each other by sending and receiving events and data. You
660
- could use your own way for component communication. `LWEventBus` is just a
661
- choice for you.
662
-
663
- #### addEventListener(eventName, callback)
664
-
665
- You can use `leanweb.eventBus` to get the global instance of event bus, and
666
- use `leanweb.eventBus.addEventListener(eventName, callback)` to subscribe to
667
- a type of event from the event bus. `addEventListener` takes two parameters.
668
- The first `eventName` is the name of the event, and the second `callback` is a
669
- function that will get called when a event is sent to the event bus. The
670
- callback function that takes a parameter `event`, which contains `eventName`
671
- and `data` fields. `addEventListener` returns the eventListener instance
672
- being added, which could be passed in `removeEventListener` as parameter.
673
-
674
- #### removeEventListener(listener)
675
-
676
- `removeEventListener` removes the listener from the event bus, so it stops
677
- being notified when a next event is fired.
678
-
679
- #### dispatchEvent(eventName, data = null)
680
-
681
- `dispatchEvent` is used to send an event to the event bus. It takes two
682
- parameters. `eventName` is the name of the event, and `data` is the payload data
683
- of the event.
684
-
685
- ### Post dist hook
686
-
687
- If `post-dist` file is present in the project root directory, it will be called
688
- after `lw dist` is done. This could be useful to copy `dist/\*` to somewhere
689
- else.
690
-
691
- ## More examples and tutorials
692
-
693
- https://leanweb.app
1
+ Please visit <a href="https://leanweb.app">leanweb.app</a>
@@ -75,6 +75,11 @@ const walkNode = (node, interpolation) => {
75
75
  const lwType = lw[0];
76
76
  const lwValue = lw[1];
77
77
 
78
+ if (attr.name === 'lw-bind:class') {
79
+ const classAttr = node.attrs.find(a => a.name === 'class');
80
+ node.attrs.push({ name: 'lw-init-class', value: classAttr.value });
81
+ }
82
+
78
83
  const ast = getAST(attr.value);
79
84
  removeASTLocation(ast);
80
85
  interpolation[key] = { ast, loc, lwType, lwValue };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leanweb",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "Builds framework agnostic web components.",
5
5
  "bin": {
6
6
  "leanweb": "leanweb.js",
@@ -2,6 +2,7 @@
2
2
  <html>
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5
6
  <title>${project.name}</title>
6
7
  <script type="module" src="${project.name}.js"></script>
7
8
  <link rel="stylesheet" href="${project.name}.css">
@@ -418,10 +418,19 @@ export default class LWElement extends HTMLElement {
418
418
  const interpolation = this.ast[attrValue];
419
419
  const parsed = parser.evaluate(interpolation.ast, context, interpolation.loc);
420
420
 
421
- if (parsed[0] !== false && parsed[0] !== undefined && parsed[0] !== null) {
422
- bindNode.setAttribute(interpolation.lwValue, parsed[0]);
421
+ if (interpolation.lwValue === 'class') {
422
+ const initClass = bindNode.getAttribute('lw-init-class');
423
+ if (!parsed[0]) {
424
+ bindNode.classList.remove(parsed[0]);
425
+ } else {
426
+ bindNode.classList = initClass + ' ' + parsed[0];
427
+ }
423
428
  } else {
424
- bindNode.removeAttribute(interpolation.lwValue);
429
+ if (parsed[0] !== undefined && parsed[0] !== null) {
430
+ bindNode.setAttribute(interpolation.lwValue, parsed[0]);
431
+ } else {
432
+ bindNode.removeAttribute(interpolation.lwValue);
433
+ }
425
434
  }
426
435
  }
427
436
  }
@@ -175,7 +175,8 @@ const immediateContext = (node, context) => {
175
175
  if (context.length === 0) {
176
176
  return null;
177
177
  }
178
- return context.find(contextObj => node.name in contextObj) ?? context[0];
178
+ const qualifiedContext = context.filter(contextObj => !(('$event' in contextObj && '$node' in contextObj) || 'this' in contextObj));
179
+ return context.find(contextObj => node.name in contextObj) ?? qualifiedContext[0];
179
180
  } else if (typeof context === 'object') {
180
181
  return context;
181
182
  }
@@ -2,6 +2,7 @@
2
2
  <html>
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5
6
  <title>${page.name} - ${project.name}</title>
6
7
  <script type="module" src="${pathLevels}${project.name}.js"></script>
7
8
  <link rel="stylesheet" href="${pathLevels}${project.name}.css">