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 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 reactive properties that can reflect to attributes
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
- this.innerHTML = this.html();
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; // Reflect property to attribute
562
- attribute?: string; // Custom attribute name
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,7 @@
1
+ export interface ICounterButton extends HTMLElement {
2
+ count: number;
3
+ setCount(count: number): void;
4
+ increment(): void;
5
+ decrement(): void;
6
+ reset(): void;
7
+ }
@@ -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.1.0",
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
  ],
@@ -0,0 +1,8 @@
1
+ declare module 'route-parser' {
2
+ export default class Route {
3
+ constructor(spec: string);
4
+ match(path: string): Record<string, string> | false;
5
+ reverse(params?: Record<string, any>): string;
6
+ spec: string;
7
+ }
8
+ }
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
  };