snice 1.2.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 +17 -4
- 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/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:
|
|
@@ -315,13 +325,16 @@ class AboutPage extends HTMLElement {
|
|
|
315
325
|
}
|
|
316
326
|
}
|
|
317
327
|
|
|
318
|
-
// Page with URL parameter
|
|
319
|
-
|
|
328
|
+
// Page with URL parameter
|
|
329
|
+
import { property } from 'snice';
|
|
330
|
+
|
|
331
|
+
@page({ tag: 'user-page', routes: ['/users/:userId'] })
|
|
320
332
|
class UserPage extends HTMLElement {
|
|
321
|
-
|
|
333
|
+
@property()
|
|
334
|
+
userId = '';
|
|
322
335
|
|
|
323
336
|
html() {
|
|
324
|
-
return `<h1>User ${this.
|
|
337
|
+
return `<h1>User ${this.userId}</h1>`;
|
|
325
338
|
}
|
|
326
339
|
}
|
|
327
340
|
|
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
|
],
|