snice 1.1.0 → 1.3.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/README.md +58 -8
- package/bin/snice.js +100 -0
- package/bin/templates/base/README.md +33 -0
- package/bin/templates/base/index.html +13 -0
- package/bin/templates/base/package.json +20 -0
- package/bin/templates/base/public/vite.svg +1 -0
- package/bin/templates/base/src/components/counter-button.ts +101 -0
- package/bin/templates/base/src/controllers/counter-controller.ts +24 -0
- package/bin/templates/base/src/main.ts +16 -0
- package/bin/templates/base/src/pages/about-page.ts +56 -0
- package/bin/templates/base/src/pages/home-page.ts +77 -0
- package/bin/templates/base/src/pages/not-found-page.ts +52 -0
- package/bin/templates/base/src/router.ts +15 -0
- package/bin/templates/base/src/styles/global.css +27 -0
- package/bin/templates/base/src/styles/shared.ts +82 -0
- package/bin/templates/base/src/types/counter-button.ts +7 -0
- package/bin/templates/base/tsconfig.json +21 -0
- package/bin/templates/base/vite.config.ts +21 -0
- package/package.json +6 -1
- package/src/route-parser.d.ts +8 -0
- package/src/router.ts +3 -2
package/README.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight TypeScript framework for building web components with decorators and routing.
|
|
4
4
|
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
Create a new Snice app with one command:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx snice create-app my-app
|
|
11
|
+
cd my-app
|
|
12
|
+
npm run dev
|
|
13
|
+
```
|
|
14
|
+
|
|
5
15
|
## Core Philosophy: Imperative, Not Reactive
|
|
6
16
|
|
|
7
17
|
Snice takes an **imperative approach** to web components. Unlike reactive frameworks that automatically re-render when data changes, Snice components:
|
|
@@ -23,7 +33,7 @@ Snice provides a clear separation of concerns through decorators:
|
|
|
23
33
|
- **`@page`** - Sets up routing and navigation between different views
|
|
24
34
|
|
|
25
35
|
### Property & Query Decorators
|
|
26
|
-
- **`@property`** - Declares
|
|
36
|
+
- **`@property`** - Declares properties that can reflect to attributes
|
|
27
37
|
- **`@query`** - Queries a single element from shadow DOM
|
|
28
38
|
- **`@queryAll`** - Queries multiple elements from shadow DOM
|
|
29
39
|
|
|
@@ -219,7 +229,7 @@ class MyClicker extends HTMLElement {
|
|
|
219
229
|
Automatically dispatch custom events with `@dispatch`:
|
|
220
230
|
|
|
221
231
|
```typescript
|
|
222
|
-
import { element, dispatch, on } from 'snice';
|
|
232
|
+
import { element, dispatch, on, query } from 'snice';
|
|
223
233
|
|
|
224
234
|
@element('toggle-switch')
|
|
225
235
|
class ToggleSwitch extends HTMLElement {
|
|
@@ -315,11 +325,25 @@ class AboutPage extends HTMLElement {
|
|
|
315
325
|
}
|
|
316
326
|
}
|
|
317
327
|
|
|
328
|
+
// Page with URL parameter
|
|
329
|
+
import { property } from 'snice';
|
|
330
|
+
|
|
331
|
+
@page({ tag: 'user-page', routes: ['/users/:userId'] })
|
|
332
|
+
class UserPage extends HTMLElement {
|
|
333
|
+
@property()
|
|
334
|
+
userId = '';
|
|
335
|
+
|
|
336
|
+
html() {
|
|
337
|
+
return `<h1>User ${this.userId}</h1>`;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
318
341
|
// Start the router
|
|
319
342
|
initialize();
|
|
320
343
|
|
|
321
344
|
// Navigate programmatically
|
|
322
345
|
navigate('/about');
|
|
346
|
+
navigate('/users/123'); // Sets id="123" on UserPage
|
|
323
347
|
```
|
|
324
348
|
|
|
325
349
|
## Controllers (Data Fetching)
|
|
@@ -339,7 +363,7 @@ class UserController {
|
|
|
339
363
|
(element as any).setUsers(users);
|
|
340
364
|
}
|
|
341
365
|
|
|
342
|
-
async detach() {
|
|
366
|
+
async detach(element: HTMLElement) {
|
|
343
367
|
// Cleanup
|
|
344
368
|
}
|
|
345
369
|
}
|
|
@@ -358,7 +382,9 @@ class UserList extends HTMLElement {
|
|
|
358
382
|
|
|
359
383
|
setUsers(users: any[]) {
|
|
360
384
|
this.users = users;
|
|
361
|
-
|
|
385
|
+
if (this.shadowRoot) {
|
|
386
|
+
this.shadowRoot.innerHTML = this.html();
|
|
387
|
+
}
|
|
362
388
|
}
|
|
363
389
|
}
|
|
364
390
|
```
|
|
@@ -393,6 +419,11 @@ class UserCard extends HTMLElement {
|
|
|
393
419
|
// --- controllers/user-controller.ts
|
|
394
420
|
@controller('user-controller')
|
|
395
421
|
class UserController {
|
|
422
|
+
element: HTMLElement | null = null;
|
|
423
|
+
|
|
424
|
+
async attach(element: HTMLElement) {}
|
|
425
|
+
async detach(element: HTMLElement) {}
|
|
426
|
+
|
|
396
427
|
@channel('get-data')
|
|
397
428
|
handleGetData(request) {
|
|
398
429
|
console.log(request); // { id: 123 }
|
|
@@ -491,7 +522,6 @@ class WeatherController {
|
|
|
491
522
|
element: HTMLElement | null = null;
|
|
492
523
|
|
|
493
524
|
async attach(element: HTMLElement) {
|
|
494
|
-
this.element = element;
|
|
495
525
|
|
|
496
526
|
// Simulate fetching weather data
|
|
497
527
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
@@ -512,6 +542,10 @@ class WeatherController {
|
|
|
512
542
|
`);
|
|
513
543
|
(element as any).setFooter('Updated just now');
|
|
514
544
|
}
|
|
545
|
+
|
|
546
|
+
async detach(element: HTMLElement) {
|
|
547
|
+
// Cleanup if needed
|
|
548
|
+
}
|
|
515
549
|
}
|
|
516
550
|
```
|
|
517
551
|
|
|
@@ -557,9 +591,16 @@ Use the same card with different controllers:
|
|
|
557
591
|
|
|
558
592
|
```typescript
|
|
559
593
|
interface PropertyOptions {
|
|
560
|
-
type?: typeof String | typeof Number | typeof Boolean; // Type converter
|
|
561
|
-
reflect?: boolean;
|
|
562
|
-
attribute?: string;
|
|
594
|
+
type?: typeof String | typeof Number | typeof Boolean | typeof Array | typeof Object; // Type converter
|
|
595
|
+
reflect?: boolean; // Reflect property to attribute
|
|
596
|
+
attribute?: string | boolean; // Custom attribute name or false to disable
|
|
597
|
+
converter?: PropertyConverter; // Custom converter
|
|
598
|
+
hasChanged?: (value: any, oldValue: any) => boolean; // Custom change detector
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
interface PropertyConverter {
|
|
602
|
+
fromAttribute?(value: string | null, type?: any): any;
|
|
603
|
+
toAttribute?(value: any, type?: any): string | null;
|
|
563
604
|
}
|
|
564
605
|
```
|
|
565
606
|
|
|
@@ -571,6 +612,15 @@ interface DispatchOptions extends EventInit {
|
|
|
571
612
|
}
|
|
572
613
|
```
|
|
573
614
|
|
|
615
|
+
## Documentation
|
|
616
|
+
|
|
617
|
+
- [Elements API](./docs/elements.md) - Complete guide to creating elements with properties, queries, and styling
|
|
618
|
+
- [Controllers API](./docs/controllers.md) - Data fetching, business logic, and controller patterns
|
|
619
|
+
- [Events API](./docs/events.md) - Event handling, dispatching, and custom events
|
|
620
|
+
- [Channels API](./docs/channels.md) - Bidirectional communication between elements and controllers
|
|
621
|
+
- [Routing API](./docs/routing.md) - Single-page application routing with transitions
|
|
622
|
+
- [Migration Guide](./docs/migration-guide.md) - Migrating from React, Vue, Angular, and other frameworks
|
|
623
|
+
|
|
574
624
|
## License
|
|
575
625
|
|
|
576
626
|
MIT
|
package/bin/snice.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, join, resolve, basename } from 'path';
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, cpSync } from 'fs';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const command = args[0];
|
|
12
|
+
|
|
13
|
+
if (command === 'create-app') {
|
|
14
|
+
const projectPath = args[1] || '.';
|
|
15
|
+
createApp(projectPath);
|
|
16
|
+
} else {
|
|
17
|
+
console.log(`
|
|
18
|
+
Snice CLI
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
snice create-app <project-name> Create a new Snice application
|
|
22
|
+
snice create-app . Initialize in current directory
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
snice create-app my-app
|
|
26
|
+
npx snice create-app my-app
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createApp(projectPath) {
|
|
31
|
+
const targetDir = resolve(process.cwd(), projectPath);
|
|
32
|
+
const projectName = projectPath === '.' ? basename(process.cwd()) : basename(targetDir);
|
|
33
|
+
|
|
34
|
+
console.log(`\n🚀 Creating Snice app in ${targetDir}...\n`);
|
|
35
|
+
|
|
36
|
+
// Check if directory exists and is empty
|
|
37
|
+
if (projectPath !== '.') {
|
|
38
|
+
if (existsSync(targetDir)) {
|
|
39
|
+
const files = readdirSync(targetDir);
|
|
40
|
+
if (files.length > 0 && !files.every(f => f.startsWith('.'))) {
|
|
41
|
+
console.error(`❌ Directory ${targetDir} is not empty!`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
mkdirSync(targetDir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
// Check current directory
|
|
49
|
+
const files = readdirSync(targetDir);
|
|
50
|
+
const hasNonDotFiles = files.some(f => !f.startsWith('.') && f !== 'node_modules');
|
|
51
|
+
if (hasNonDotFiles) {
|
|
52
|
+
console.error(`❌ Current directory is not empty!`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Path to templates
|
|
58
|
+
const templateDir = join(__dirname, 'templates', 'base');
|
|
59
|
+
|
|
60
|
+
// Copy template files
|
|
61
|
+
copyTemplateFiles(templateDir, targetDir, projectName);
|
|
62
|
+
|
|
63
|
+
console.log(`\n✨ Project created successfully!\n`);
|
|
64
|
+
console.log('Next steps:');
|
|
65
|
+
|
|
66
|
+
if (projectPath !== '.') {
|
|
67
|
+
console.log(` cd ${projectPath}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(' npm install');
|
|
71
|
+
console.log(' npm run dev\n');
|
|
72
|
+
console.log('Happy coding! 🎉\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function copyTemplateFiles(sourceDir, targetDir, projectName) {
|
|
76
|
+
const files = readdirSync(sourceDir, { withFileTypes: true });
|
|
77
|
+
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
const sourcePath = join(sourceDir, file.name);
|
|
80
|
+
const targetPath = join(targetDir, file.name);
|
|
81
|
+
|
|
82
|
+
if (file.isDirectory()) {
|
|
83
|
+
// Create directory and recursively copy contents
|
|
84
|
+
if (!existsSync(targetPath)) {
|
|
85
|
+
mkdirSync(targetPath, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
copyTemplateFiles(sourcePath, targetPath, projectName);
|
|
88
|
+
} else {
|
|
89
|
+
// Read file, replace placeholders, and write to target
|
|
90
|
+
console.log(` Creating ${file.name}...`);
|
|
91
|
+
|
|
92
|
+
let content = readFileSync(sourcePath, 'utf8');
|
|
93
|
+
|
|
94
|
+
// Replace {{projectName}} placeholders
|
|
95
|
+
content = content.replace(/\{\{projectName\}\}/g, projectName);
|
|
96
|
+
|
|
97
|
+
writeFileSync(targetPath, content);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
A Snice application
|
|
4
|
+
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm run dev
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Build
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm run build
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Preview Production Build
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm run preview
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Project Structure
|
|
24
|
+
|
|
25
|
+
- `src/pages/` - Application pages/routes
|
|
26
|
+
- `src/components/` - Reusable components
|
|
27
|
+
- `src/controllers/` - Business logic controllers
|
|
28
|
+
- `src/styles/` - Global styles and shared styles
|
|
29
|
+
- `src/types/` - TypeScript interfaces and types
|
|
30
|
+
- `src/router.ts` - Router configuration
|
|
31
|
+
- `src/main.ts` - Application entry point
|
|
32
|
+
|
|
33
|
+
Built with [Snice](https://github.com/yourusername/snice)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{{projectName}}</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"type-check": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"snice": "^1.2.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/node": "^20.0.0",
|
|
16
|
+
"terser": "^5.24.0",
|
|
17
|
+
"typescript": "^5.3.3",
|
|
18
|
+
"vite": "^5.0.10"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { element, property, query, on, dispatch } from 'snice';
|
|
2
|
+
import type { ICounterButton } from '../types/counter-button';
|
|
3
|
+
|
|
4
|
+
@element('counter-button')
|
|
5
|
+
export class CounterButton extends HTMLElement implements ICounterButton {
|
|
6
|
+
@property({ type: Number })
|
|
7
|
+
count = 0;
|
|
8
|
+
|
|
9
|
+
@query('.count')
|
|
10
|
+
countDisplay?: HTMLElement;
|
|
11
|
+
|
|
12
|
+
html() {
|
|
13
|
+
return /*html*/`
|
|
14
|
+
<div class="counter">
|
|
15
|
+
<button class="btn minus">-</button>
|
|
16
|
+
<span class="count">${this.count}</span>
|
|
17
|
+
<button class="btn plus">+</button>
|
|
18
|
+
</div>
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
css() {
|
|
23
|
+
return /*css*/`
|
|
24
|
+
.counter {
|
|
25
|
+
display: inline-flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
gap: 1rem;
|
|
28
|
+
padding: 1rem;
|
|
29
|
+
background: white;
|
|
30
|
+
border-radius: 8px;
|
|
31
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.count {
|
|
35
|
+
font-size: 1.5rem;
|
|
36
|
+
font-weight: bold;
|
|
37
|
+
min-width: 3rem;
|
|
38
|
+
text-align: center;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.btn {
|
|
42
|
+
width: 2rem;
|
|
43
|
+
height: 2rem;
|
|
44
|
+
border: 2px solid var(--primary-color);
|
|
45
|
+
background: white;
|
|
46
|
+
color: var(--primary-color);
|
|
47
|
+
border-radius: 4px;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
font-size: 1.2rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.btn:hover {
|
|
53
|
+
background: var(--primary-color);
|
|
54
|
+
color: white;
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Imperative methods that controller can call
|
|
60
|
+
setCount(value: number) {
|
|
61
|
+
this.count = value;
|
|
62
|
+
this.updateDisplay();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@dispatch('count-changed')
|
|
66
|
+
increment() {
|
|
67
|
+
this.count++;
|
|
68
|
+
this.updateDisplay();
|
|
69
|
+
return { count: this.count };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@dispatch('count-changed')
|
|
73
|
+
decrement() {
|
|
74
|
+
this.count--;
|
|
75
|
+
this.updateDisplay();
|
|
76
|
+
return { count: this.count };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@dispatch('count-changed')
|
|
80
|
+
reset() {
|
|
81
|
+
this.count = 0;
|
|
82
|
+
this.updateDisplay();
|
|
83
|
+
return { count: this.count };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private updateDisplay() {
|
|
87
|
+
if (this.countDisplay) {
|
|
88
|
+
this.countDisplay.textContent = String(this.count);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@on('click', '.plus')
|
|
93
|
+
handlePlus() {
|
|
94
|
+
this.increment();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@on('click', '.minus')
|
|
98
|
+
handleMinus() {
|
|
99
|
+
this.decrement();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { controller, on } from 'snice';
|
|
2
|
+
import type { ICounterButton } from '../types/counter-button';
|
|
3
|
+
|
|
4
|
+
@controller('counter')
|
|
5
|
+
export class CounterController {
|
|
6
|
+
element!: ICounterButton;
|
|
7
|
+
|
|
8
|
+
async attach() {
|
|
9
|
+
// Load saved count from localStorage
|
|
10
|
+
const saved = localStorage.getItem('counter-value');
|
|
11
|
+
if (saved) {
|
|
12
|
+
this.element.setCount(parseInt(saved));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async detach() {
|
|
17
|
+
// any clean up you need
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@on('count-changed')
|
|
21
|
+
handleCountChanged(e: CustomEvent) {
|
|
22
|
+
localStorage.setItem('counter-value', String(e.detail.count));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { initialize } from './router';
|
|
2
|
+
import './styles/global.css';
|
|
3
|
+
|
|
4
|
+
// Import components
|
|
5
|
+
import './components/counter-button';
|
|
6
|
+
|
|
7
|
+
// Import controllers
|
|
8
|
+
import './controllers/counter-controller';
|
|
9
|
+
|
|
10
|
+
// Import pages
|
|
11
|
+
import './pages/home-page';
|
|
12
|
+
import './pages/about-page';
|
|
13
|
+
import './pages/not-found-page';
|
|
14
|
+
|
|
15
|
+
// Initialize router
|
|
16
|
+
initialize();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { page } from '../router';
|
|
2
|
+
|
|
3
|
+
@page({ tag: 'about-page', routes: ['/about'] })
|
|
4
|
+
export class AboutPage extends HTMLElement {
|
|
5
|
+
html() {
|
|
6
|
+
return /*html*/`
|
|
7
|
+
<div class="container">
|
|
8
|
+
<h1>About</h1>
|
|
9
|
+
<p>This app was built with Snice, a modern web components framework.</p>
|
|
10
|
+
<p>Version 1.0.0</p>
|
|
11
|
+
|
|
12
|
+
<div class="nav">
|
|
13
|
+
<a href="#/" class="btn">Back to Home</a>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
css() {
|
|
20
|
+
return /*css*/`
|
|
21
|
+
.container {
|
|
22
|
+
padding: 3rem;
|
|
23
|
+
max-width: 800px;
|
|
24
|
+
margin: 0 auto;
|
|
25
|
+
text-align: center;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
h1 {
|
|
29
|
+
color: var(--primary-color);
|
|
30
|
+
margin-bottom: 1rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
p {
|
|
34
|
+
color: var(--text-light);
|
|
35
|
+
margin-bottom: 1rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.nav {
|
|
39
|
+
margin-top: 3rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.btn {
|
|
43
|
+
display: inline-block;
|
|
44
|
+
padding: 0.75rem 1.5rem;
|
|
45
|
+
background: var(--primary-color);
|
|
46
|
+
color: white;
|
|
47
|
+
text-decoration: none;
|
|
48
|
+
border-radius: 6px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.btn:hover {
|
|
52
|
+
background: var(--secondary-color);
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { page } from '../router';
|
|
2
|
+
|
|
3
|
+
@page({ tag: 'home-page', routes: ['/'] })
|
|
4
|
+
export class HomePage extends HTMLElement {
|
|
5
|
+
html() {
|
|
6
|
+
return /*html*/`
|
|
7
|
+
<div class="container">
|
|
8
|
+
<h1>Welcome to {{projectName}}</h1>
|
|
9
|
+
<p>Built with Snice</p>
|
|
10
|
+
|
|
11
|
+
<div class="demo-section">
|
|
12
|
+
<h2>Interactive Counter Demo</h2>
|
|
13
|
+
<p>This counter persists its state using a controller:</p>
|
|
14
|
+
<counter-button controller="counter"></counter-button>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="nav">
|
|
18
|
+
<a href="#/about" class="btn">About</a>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
css() {
|
|
25
|
+
return /*css*/`
|
|
26
|
+
.container {
|
|
27
|
+
padding: 3rem;
|
|
28
|
+
text-align: center;
|
|
29
|
+
max-width: 800px;
|
|
30
|
+
margin: 0 auto;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
h1 {
|
|
34
|
+
color: var(--primary-color);
|
|
35
|
+
margin-bottom: 1rem;
|
|
36
|
+
font-size: 2.5rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
h2 {
|
|
40
|
+
color: var(--primary-color);
|
|
41
|
+
margin-bottom: 1rem;
|
|
42
|
+
font-size: 1.5rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
p {
|
|
46
|
+
color: var(--text-light);
|
|
47
|
+
margin-bottom: 2rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.demo-section {
|
|
51
|
+
margin: 3rem 0;
|
|
52
|
+
padding: 2rem;
|
|
53
|
+
background: rgba(255, 255, 255, 0.5);
|
|
54
|
+
border-radius: 12px;
|
|
55
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.nav {
|
|
59
|
+
margin-top: 3rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.btn {
|
|
63
|
+
display: inline-block;
|
|
64
|
+
padding: 0.75rem 1.5rem;
|
|
65
|
+
background: var(--primary-color);
|
|
66
|
+
color: white;
|
|
67
|
+
text-decoration: none;
|
|
68
|
+
border-radius: 6px;
|
|
69
|
+
transition: background 0.3s ease;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.btn:hover {
|
|
73
|
+
background: var(--secondary-color);
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { page } from '../router';
|
|
2
|
+
|
|
3
|
+
@page({ tag: 'not-found-page', routes: ['/404'] })
|
|
4
|
+
export class NotFoundPage extends HTMLElement {
|
|
5
|
+
html() {
|
|
6
|
+
return /*html*/`
|
|
7
|
+
<div class="container">
|
|
8
|
+
<h1>404</h1>
|
|
9
|
+
<p>Page not found</p>
|
|
10
|
+
<a href="#/" class="btn">Go Home</a>
|
|
11
|
+
</div>
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
css() {
|
|
16
|
+
return /*css*/`
|
|
17
|
+
.container {
|
|
18
|
+
padding: 3rem;
|
|
19
|
+
text-align: center;
|
|
20
|
+
min-height: 100vh;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
align-items: center;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
h1 {
|
|
28
|
+
font-size: 4rem;
|
|
29
|
+
color: var(--primary-color);
|
|
30
|
+
margin-bottom: 1rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
p {
|
|
34
|
+
color: var(--text-light);
|
|
35
|
+
margin-bottom: 2rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.btn {
|
|
39
|
+
display: inline-block;
|
|
40
|
+
padding: 0.75rem 1.5rem;
|
|
41
|
+
background: var(--primary-color);
|
|
42
|
+
color: white;
|
|
43
|
+
text-decoration: none;
|
|
44
|
+
border-radius: 6px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.btn:hover {
|
|
48
|
+
background: var(--secondary-color);
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Router } from 'snice';
|
|
2
|
+
|
|
3
|
+
const { page, initialize, navigate } = Router({
|
|
4
|
+
target: '#app',
|
|
5
|
+
routing_type: 'hash',
|
|
6
|
+
transition: {
|
|
7
|
+
mode: 'simultaneous',
|
|
8
|
+
outDuration: 200,
|
|
9
|
+
inDuration: 200,
|
|
10
|
+
out: 'opacity: 0; transform: translateX(-10px);',
|
|
11
|
+
in: 'opacity: 1; transform: translateX(0);'
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export { page, initialize, navigate };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--primary-color: #667eea;
|
|
3
|
+
--secondary-color: #764ba2;
|
|
4
|
+
--text-color: #333;
|
|
5
|
+
--text-light: #666;
|
|
6
|
+
--bg-color: #f7f7f7;
|
|
7
|
+
--white: #ffffff;
|
|
8
|
+
--shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
9
|
+
--shadow-lg: 0 10px 30px rgba(0,0,0,0.15);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
* {
|
|
13
|
+
margin: 0;
|
|
14
|
+
padding: 0;
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
20
|
+
color: var(--text-color);
|
|
21
|
+
background: var(--bg-color);
|
|
22
|
+
line-height: 1.6;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#app {
|
|
26
|
+
min-height: 100vh;
|
|
27
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export const containerStyles = `
|
|
2
|
+
.container {
|
|
3
|
+
max-width: 1200px;
|
|
4
|
+
margin: 0 auto;
|
|
5
|
+
padding: 2rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.btn {
|
|
9
|
+
padding: 0.75rem 1.5rem;
|
|
10
|
+
border-radius: 6px;
|
|
11
|
+
text-decoration: none;
|
|
12
|
+
font-weight: 600;
|
|
13
|
+
transition: all 0.3s ease;
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
border: none;
|
|
16
|
+
display: inline-block;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.btn-primary {
|
|
20
|
+
background: var(--primary-color);
|
|
21
|
+
color: white;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.btn-primary:hover {
|
|
25
|
+
background: var(--secondary-color);
|
|
26
|
+
transform: translateY(-2px);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.btn-secondary {
|
|
30
|
+
background: transparent;
|
|
31
|
+
color: var(--primary-color);
|
|
32
|
+
border: 2px solid var(--primary-color);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.btn-secondary:hover {
|
|
36
|
+
background: var(--primary-color);
|
|
37
|
+
color: white;
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
export const navbarStyles = `
|
|
42
|
+
.navbar {
|
|
43
|
+
background: var(--white);
|
|
44
|
+
box-shadow: var(--shadow);
|
|
45
|
+
position: sticky;
|
|
46
|
+
top: 0;
|
|
47
|
+
z-index: 100;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.nav-container {
|
|
51
|
+
max-width: 1200px;
|
|
52
|
+
margin: 0 auto;
|
|
53
|
+
padding: 1rem 2rem;
|
|
54
|
+
display: flex;
|
|
55
|
+
justify-content: space-between;
|
|
56
|
+
align-items: center;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.nav-brand {
|
|
60
|
+
font-size: 1.5rem;
|
|
61
|
+
font-weight: 700;
|
|
62
|
+
color: var(--primary-color);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.nav-menu {
|
|
66
|
+
display: flex;
|
|
67
|
+
list-style: none;
|
|
68
|
+
gap: 2rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.nav-link {
|
|
72
|
+
color: var(--text-light);
|
|
73
|
+
text-decoration: none;
|
|
74
|
+
font-weight: 500;
|
|
75
|
+
transition: color 0.3s;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.nav-link:hover,
|
|
79
|
+
.nav-link.active {
|
|
80
|
+
color: var(--primary-color);
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"noUnusedLocals": true,
|
|
14
|
+
"noUnusedParameters": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"experimentalDecorators": true,
|
|
17
|
+
"emitDecoratorMetadata": true,
|
|
18
|
+
"types": ["vite/client", "node"]
|
|
19
|
+
},
|
|
20
|
+
"include": ["src"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
build: {
|
|
5
|
+
target: 'es2015',
|
|
6
|
+
minify: 'terser',
|
|
7
|
+
cssMinify: true,
|
|
8
|
+
rollupOptions: {
|
|
9
|
+
output: {
|
|
10
|
+
manualChunks: {
|
|
11
|
+
vendor: ['snice']
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
sourcemap: true,
|
|
16
|
+
chunkSizeWarningLimit: 500
|
|
17
|
+
},
|
|
18
|
+
esbuild: {
|
|
19
|
+
drop: process.env.NODE_ENV === 'production' ? ['debugger'] : []
|
|
20
|
+
}
|
|
21
|
+
});
|
package/package.json
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A TypeScript library",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"module": "src/index.ts",
|
|
8
8
|
"types": "src/index.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"snice": "./bin/snice.js"
|
|
11
|
+
},
|
|
9
12
|
"files": [
|
|
10
13
|
"src",
|
|
14
|
+
"bin",
|
|
15
|
+
"templates",
|
|
11
16
|
"!src/**/*.test.ts",
|
|
12
17
|
"!src/**/*.spec.ts"
|
|
13
18
|
],
|
package/src/router.ts
CHANGED
|
@@ -108,6 +108,9 @@ export function Router(options: RouterOptions) {
|
|
|
108
108
|
const originalDisconnectedCallback = constructor.prototype.disconnectedCallback;
|
|
109
109
|
|
|
110
110
|
constructor.prototype.connectedCallback = function() {
|
|
111
|
+
// Call original connectedCallback first to allow property initialization
|
|
112
|
+
originalConnectedCallback?.call(this);
|
|
113
|
+
|
|
111
114
|
// Create shadow root if it doesn't exist
|
|
112
115
|
if (!this.shadowRoot) {
|
|
113
116
|
this.attachShadow({ mode: 'open' });
|
|
@@ -139,8 +142,6 @@ export function Router(options: RouterOptions) {
|
|
|
139
142
|
if (shadowContent) {
|
|
140
143
|
this.shadowRoot.innerHTML = shadowContent;
|
|
141
144
|
}
|
|
142
|
-
|
|
143
|
-
originalConnectedCallback?.call(this);
|
|
144
145
|
// Setup @on event handlers - use element for host events, shadow root for delegated events
|
|
145
146
|
setupEventHandlers(this, this);
|
|
146
147
|
};
|