@vyckr/tachyon 1.1.9 → 1.1.10

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/bun.lock CHANGED
@@ -8,7 +8,7 @@
8
8
  "jsdom": "^26.0.0",
9
9
  },
10
10
  "devDependencies": {
11
- "@types/bun": "^1.2.4",
11
+ "@types/bun": "~1.2.8",
12
12
  "@types/deno": "^2.0.0",
13
13
  "@types/jsdom": "^21.1.7",
14
14
  "@types/node": "^20.4.2",
@@ -28,7 +28,7 @@
28
28
 
29
29
  "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.3", "", {}, "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw=="],
30
30
 
31
- "@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="],
31
+ "@types/bun": ["@types/bun@1.2.8", "", { "dependencies": { "bun-types": "1.2.7" } }, "sha512-t8L1RvJVUghW5V+M/fL3Thbxcs0HwNsXsnTEBEfEVqGteiJToOlZ/fyOEaR1kZsNqnu+3XA4RI/qmnX4w6+S+w=="],
32
32
 
33
33
  "@types/deno": ["@types/deno@2.0.0", "", {}, "sha512-O9/jRVlq93kqfkl4sYR5N7+Pz4ukzXVIbMnE/VgvpauNHsvjQ9iBVnJ3X0gAvMa2khcoFD8DSO7mQVCuiuDMPg=="],
34
34
 
@@ -44,7 +44,7 @@
44
44
 
45
45
  "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
46
46
 
47
- "bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="],
47
+ "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="],
48
48
 
49
49
  "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
50
50
 
@@ -0,0 +1,30 @@
1
+ <script>
2
+ let title = "Default Title"
3
+ let description = "Default description for this component"
4
+ let clicks = 0
5
+ </script>
6
+
7
+ <div class="card component-card">
8
+ <h3 class="component-title">{title}</h3>
9
+ <p>{description}</p>
10
+ <button @click="clicks++">Clicked {clicks} times</button>
11
+ </div>
12
+
13
+ <style>
14
+ .component-card {
15
+ border-left: 4px solid #2563eb;
16
+ background-color: #f0f7ff;
17
+ }
18
+ .component-title {
19
+ color: #2563eb;
20
+ margin-top: 0;
21
+ }
22
+
23
+ .card {
24
+ background-color: white;
25
+ border-radius: 8px;
26
+ padding: 1.5rem;
27
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
28
+ margin-bottom: 1rem;
29
+ }
30
+ </style>
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@vyckr/tachyon",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "author": "Chidelma",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/Chidelma/Tachyon.git"
8
8
  },
9
9
  "devDependencies": {
10
- "@types/bun": "^1.2.4",
10
+ "@types/bun": "~1.2.8",
11
11
  "@types/deno": "^2.0.0",
12
12
  "@types/jsdom": "^21.1.7",
13
13
  "@types/node": "^20.4.2"
package/routes/HTML CHANGED
@@ -1,28 +1,131 @@
1
1
  <script>
2
- const { default: dayjs } = await import('/modules/dayjs.js')
3
-
4
- console.log(dayjs().format())
5
2
  document.title = "Home"
6
-
7
- console.log("Hello from JavaScript")
8
3
 
9
- let greeting = "Hello from HTML!"
4
+ let count = 0
5
+ let showInfo = false
6
+ let newTodo = ''
7
+ let todos = [
8
+ { text: "Learn Yon framework", completed: false },
9
+ { text: "Build an application", completed: false }
10
+ ]
11
+
12
+ let componentTitle = "Component Test"
13
+ let componentDesc = "This tests custom components in Yon"
14
+
15
+ function addTodo() {
16
+ if (newTodo) {
17
+ todos = [...todos, { text: newTodo, completed: false }]
18
+ newTodo = ''
19
+ }
20
+ }
10
21
 
11
- const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
22
+ function toggleTodo(index) {
23
+ todos = todos.map((todo, i) =>
24
+ i === index ? { ...todo, completed: !todo.completed } : todo
25
+ )
26
+ }
12
27
 
13
- const start = 5
28
+ function removeTodo(index) {
29
+ todos = todos.filter((_, i) => i !== index)
30
+ }
14
31
  </script>
15
32
 
16
- <h1>${greeting}</h1>
17
- <input type="text" placeholder="Write Something" :value="greeting" @input="(value) => greeting = value" />
18
- <br/>
19
- <ty-counter :count="start" />
20
- <ty-loop :for="const num of arr">
21
- <ty-logic :if="num % 5 === 0">
22
- <p>Half way there! Number ${num}</p>
23
- </ty-logic>
24
- <ty-logic :else>
25
- <p>Number ${num}</p>
26
- </ty-logic>
33
+ <h1>Yon Framework Test</h1>
34
+
35
+ <ty-clicker :clicks="count"/>
36
+
37
+ <div class="card">
38
+ <h2>Counter Example</h2>
39
+ <p>Count: <span>{count}</span></p>
40
+ <button @click="count++">Increment</button>
41
+ <button @click="count--">Decrement</button>
42
+ </div>
43
+
44
+ <h2>Todo List</h2>
45
+ <input type="text" placeholder="Add new todo" :value="newTodo" />
46
+ <button @click="addTodo()">Add</button>
47
+
48
+ <ty-loop :for="let i = 0; i < todos.length; i++">
49
+ <div class="todo-item">
50
+ <input type="checkbox" :checked="todos[i].completed" @change="toggleTodo(i)" />
51
+ <span :class="todos[i].completed ? 'completed' : ''">{todos[i].text}</span>
52
+ <button @click="removeTodo(i)" style="margin-left: auto; background-color: #ef4444;">Remove</button>
53
+ </div>
27
54
  </ty-loop>
28
- <ty-counter :count="start" />
55
+
56
+ <ty-logic :if="showInfo">
57
+ <div class="card">
58
+ <h2>Framework Info</h2>
59
+ <p>This is testing the conditional rendering feature of the Yon framework.</p>
60
+ <button @click="showInfo = false">Hide Info</button>
61
+ </div>
62
+ </ty-logic>
63
+ <ty-logic :else>
64
+ <div class="card">
65
+ <button @click="showInfo = true">Show Info</button>
66
+ </div>
67
+ </ty-logic>
68
+
69
+ <ty-clicker :clicks="count"/>
70
+
71
+ <style>
72
+ body {
73
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
74
+ line-height: 1.5;
75
+ max-width: 800px;
76
+ margin: 0 auto;
77
+ padding: 1rem;
78
+ color: #333;
79
+ background-color: #f5f5f5;
80
+ }
81
+
82
+ .card {
83
+ background-color: white;
84
+ border-radius: 8px;
85
+ padding: 1.5rem;
86
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
87
+ margin-bottom: 1rem;
88
+ }
89
+
90
+ h1 {
91
+ color: #2563eb;
92
+ }
93
+
94
+ button {
95
+ background-color: #2563eb;
96
+ color: white;
97
+ border: none;
98
+ border-radius: 4px;
99
+ padding: 0.5rem 1rem;
100
+ cursor: pointer;
101
+ font-size: 1rem;
102
+ }
103
+
104
+ button:hover {
105
+ background-color: #1d4ed8;
106
+ }
107
+
108
+ input {
109
+ padding: 0.5rem;
110
+ border: 1px solid #ccc;
111
+ border-radius: 4px;
112
+ margin-right: 0.5rem;
113
+ font-size: 1rem;
114
+ }
115
+
116
+ .todo-item {
117
+ display: flex;
118
+ align-items: center;
119
+ padding: 0.5rem;
120
+ border-bottom: 1px solid #eee;
121
+ }
122
+
123
+ .todo-item:last-child {
124
+ border-bottom: none;
125
+ }
126
+
127
+ .completed {
128
+ text-decoration: line-through;
129
+ opacity: 0.7;
130
+ }
131
+ </style>
@@ -0,0 +1,417 @@
1
+ let render: Function
2
+ const routes = new Map<string, Record<string, number>>()
3
+ let params: any[] = []
4
+ const slugs: Record<string, any> = {}
5
+ let previousRender: string
6
+ let madeRequest: boolean = false
7
+ let elementId: string | null;
8
+ let selectionStart: number | null;
9
+ const parser = new DOMParser()
10
+ let firstRender = routes.size === 0
11
+ const elementEvents: Record<string, EventListener> = {}
12
+
13
+ if(firstRender) {
14
+
15
+ fetch('/routes.json')
16
+ .then(res => res.json())
17
+ .then(data => {
18
+ for (const [path, slugs] of Object.entries(data)) {
19
+ routes.set(path, slugs as Record<string, number>)
20
+ }
21
+ setPageTemplate(window.location.pathname)
22
+ firstRender = false
23
+ })
24
+ }
25
+
26
+ function mergeBodyHTML(html: string) {
27
+
28
+ if(html === previousRender) return
29
+
30
+ previousRender = html
31
+
32
+ // document.body.innerHTML = html
33
+
34
+ if(madeRequest) {
35
+ document.body.innerHTML = html
36
+ } else {
37
+ const nextDom = parser.parseFromString(html, 'text/html')
38
+ updateDOM(document.body, nextDom.body)
39
+ deleteDOM(document.body, nextDom.body)
40
+ insertDOM(document.body, nextDom.body)
41
+ }
42
+
43
+ addEvents()
44
+
45
+ if(elementId) {
46
+
47
+ const element = document.getElementById(elementId)
48
+
49
+ if(element) {
50
+
51
+ element.focus()
52
+
53
+ if(selectionStart && 'setSelectionRange' in element) {
54
+
55
+ (element as HTMLInputElement).setSelectionRange(selectionStart, selectionStart)
56
+ }
57
+ }
58
+
59
+ elementId = null;
60
+ selectionStart = null
61
+ }
62
+ }
63
+
64
+ function addEvents(elements?: HTMLCollection) {
65
+
66
+ if(!elements) elements = document.body.children
67
+
68
+ for (const element of elements) {
69
+
70
+ const allEvents: string[] = []
71
+
72
+ for (const attribute of element.attributes) {
73
+
74
+ if (attribute.name.startsWith('@')) {
75
+
76
+ const event = attribute.name.substring(1)
77
+
78
+ allEvents.push(event)
79
+
80
+ if(elementEvents[`${element.id}_${event}`]) {
81
+ element.removeEventListener(event, elementEvents[`${element.id}_${event}`])
82
+ }
83
+
84
+ elementEvents[`${element.id}_${event}`] = async function(e) {
85
+
86
+ elementId = element.id
87
+
88
+ if (e.target) {
89
+ selectionStart = (e.target as HTMLInputElement).selectionStart;
90
+ }
91
+
92
+ mergeBodyHTML(await render(elementId))
93
+ }
94
+
95
+ element.addEventListener(event, elementEvents[`${element.id}_${event}`])
96
+ }
97
+
98
+ if(attribute.name.endsWith('ed') && attribute.value === "false") element.removeAttribute(attribute.name)
99
+ }
100
+
101
+ if(element instanceof HTMLSelectElement || element instanceof HTMLInputElement) {
102
+
103
+ element.oninput = async (ev) => {
104
+ if (ev.target) {
105
+
106
+ elementId = element.id
107
+
108
+ if (ev.target) {
109
+ selectionStart = (ev.target as HTMLInputElement).selectionStart;
110
+ }
111
+
112
+ const evt = { value: (ev.target as any).value, defaultValue: (ev.target as any).defaultValue }
113
+
114
+ mergeBodyHTML(await render(elementId, evt))
115
+ }
116
+ }
117
+ }
118
+
119
+ addEvents(element.children)
120
+ }
121
+ }
122
+
123
+ async function onClickEvent(ev: MouseEvent) {
124
+ const target = ev.target as HTMLAnchorElement;
125
+ if(target?.href) {
126
+ const url = new URL(target?.href)
127
+ if(url.origin !== location.origin) return
128
+ ev.preventDefault()
129
+ setPageTemplate(url.pathname)
130
+ } else mergeBodyHTML(await render())
131
+ }
132
+
133
+ function setPageTemplate(pathname: string) {
134
+
135
+ let url;
136
+
137
+ try {
138
+
139
+ let handler = getHandler(pathname)
140
+
141
+ if(handler === '/') handler = ''
142
+
143
+ url = `${handler}/HTML.js`
144
+
145
+ } catch(err) {
146
+ url = `/404.js`
147
+ }
148
+
149
+ import(`/pages${url}`).then(async module => {
150
+ window.history.replaceState({}, '', pathname)
151
+ render = await module.default()
152
+ madeRequest = true
153
+ mergeBodyHTML(await render())
154
+ madeRequest = false
155
+ })
156
+ }
157
+
158
+
159
+ function getHandler(pathname: string) {
160
+
161
+ let handler;
162
+
163
+ if (pathname === '/') return pathname
164
+
165
+ const paths = pathname.split('/').slice(1);
166
+
167
+ let bestMatchKey = '';
168
+ let bestMatchLength = -1;
169
+
170
+ for (const [routeKey] of routes) {
171
+
172
+ const routeSegs = routeKey.split('/')
173
+
174
+ const isMatch = pathsMatch(routeSegs, paths.slice(0, routeSegs.length));
175
+
176
+ if (isMatch && routeSegs.length > bestMatchLength) {
177
+ bestMatchKey = routeKey;
178
+ bestMatchLength = routeSegs.length;
179
+ }
180
+ }
181
+
182
+ if (bestMatchKey) {
183
+
184
+ handler = bestMatchKey
185
+
186
+ params = parseParams(paths.slice(bestMatchLength))
187
+
188
+ const slugMap = routes.get(bestMatchKey) ?? {}
189
+
190
+ Object.entries(slugMap).forEach(([key, idx]) => {
191
+ key = key.replace(':', '')
192
+ slugs[key] = paths[idx]
193
+ })
194
+ }
195
+
196
+ if (!handler) throw new Error(`Route ${pathname} not found`, { cause: 404 });
197
+
198
+ return handler
199
+ }
200
+
201
+
202
+ function pathsMatch(routeSegs: string[], pathSegs: string[]) {
203
+
204
+ if (routeSegs.length !== pathSegs.length) {
205
+ return false;
206
+ }
207
+
208
+ const slugs = routes.get(routeSegs.join('/')) || {}
209
+
210
+ for (let i = 0; i < routeSegs.length; i++) {
211
+ if (!slugs[routeSegs[i]] && routeSegs[i] !== pathSegs[i]) {
212
+ return false;
213
+ }
214
+ }
215
+
216
+ return true;
217
+ }
218
+
219
+
220
+ function parseParams(input: string[]) {
221
+
222
+ const params = []
223
+
224
+ for(const param of input) {
225
+
226
+ const num = Number(param)
227
+
228
+ if(!Number.isNaN(num)) params.push(num)
229
+
230
+ else if(param === 'true') params.push(true)
231
+
232
+ else if(param === 'false') params.push(false)
233
+
234
+ else if(param === 'null') params.push(null)
235
+
236
+ else if(param === 'undefined') params.push(undefined)
237
+
238
+ else params.push(param)
239
+ }
240
+
241
+ return params
242
+ }
243
+
244
+ Object.keys(window).forEach(key => {
245
+ if(/^on/.test(key)) {
246
+ document.addEventListener(key.slice(2), async ev => {
247
+
248
+ switch(ev.type) {
249
+ case 'click':
250
+ await onClickEvent(ev as MouseEvent)
251
+ break
252
+ case 'popstate':
253
+ setPageTemplate(window.location.pathname)
254
+ break
255
+ default:
256
+ mergeBodyHTML(await render())
257
+ break
258
+ }
259
+ })
260
+ }
261
+ })
262
+
263
+
264
+
265
+ /**
266
+ * Updates existing nodes in oldDOM with properties from newDOM
267
+ * @param {Node} oldDOM - The current DOM
268
+ * @param {Node} newDOM - The desired DOM state
269
+ */
270
+ function updateDOM(oldDOM: Node, newDOM: Node): void {
271
+ // Skip if nodes are not of the same type
272
+ if (!areNodesOfSameType(oldDOM, newDOM)) {
273
+ return;
274
+ }
275
+
276
+ // If it's a text node, update the content
277
+ if (oldDOM.nodeType === Node.TEXT_NODE && newDOM.nodeType === Node.TEXT_NODE) {
278
+ if (oldDOM.textContent !== newDOM.textContent) {
279
+ oldDOM.textContent = newDOM.textContent;
280
+ }
281
+ return;
282
+ }
283
+
284
+ // If it's an element node, update attributes
285
+ if (oldDOM.nodeType === Node.ELEMENT_NODE && newDOM.nodeType === Node.ELEMENT_NODE) {
286
+ // Type assertion since we've already checked nodeType
287
+ const oldElement = oldDOM as Element;
288
+ const newElement = newDOM as Element;
289
+
290
+ // Update attributes
291
+ updateAttributes(oldElement, newElement);
292
+
293
+ // Recursively update children that exist in both DOMs
294
+ const oldChildren = Array.from(oldDOM.childNodes);
295
+ const newChildren = Array.from(newDOM.childNodes);
296
+
297
+ const minLength = Math.min(oldChildren.length, newChildren.length);
298
+
299
+ for (let i = 0; i < minLength; i++) {
300
+ updateDOM(oldChildren[i], newChildren[i]);
301
+ }
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Deletes nodes from oldDOM that don't exist in newDOM
307
+ * @param {Node} oldDOM - The current DOM
308
+ * @param {Node} newDOM - The desired DOM state
309
+ */
310
+ function deleteDOM(oldDOM: Node, newDOM: Node): void {
311
+ // If nodes are not of the same type, skip (will be handled by insertDOM)
312
+ if (!areNodesOfSameType(oldDOM, newDOM)) {
313
+ return;
314
+ }
315
+
316
+ // If it's an element node, check children
317
+ if (oldDOM.nodeType === Node.ELEMENT_NODE && newDOM.nodeType === Node.ELEMENT_NODE) {
318
+ const oldChildren = Array.from(oldDOM.childNodes);
319
+ const newChildren = Array.from(newDOM.childNodes);
320
+
321
+ // Remove children that exist in oldDOM but not in newDOM
322
+ // Start from the end to avoid index shifting issues
323
+ for (let i = oldChildren.length - 1; i >= newChildren.length; i--) {
324
+ oldDOM.removeChild(oldChildren[i]);
325
+ }
326
+
327
+ // Recursively delete for remaining children
328
+ const minLength = Math.min(oldChildren.length, newChildren.length);
329
+ for (let i = 0; i < minLength; i++) {
330
+ deleteDOM(oldChildren[i], newChildren[i]);
331
+ }
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Inserts nodes from newDOM that don't exist in oldDOM
337
+ * @param {Node} oldDOM - The current DOM
338
+ * @param {Node} newDOM - The desired DOM state
339
+ */
340
+ function insertDOM(oldDOM: Node, newDOM: Node): void {
341
+ // If nodes are not of the same type, replace the entire node
342
+ if (!areNodesOfSameType(oldDOM, newDOM)) {
343
+ if (oldDOM.parentNode) {
344
+ oldDOM.parentNode.replaceChild(newDOM.cloneNode(true), oldDOM);
345
+ }
346
+ return;
347
+ }
348
+
349
+ // If it's an element node, check children
350
+ if (oldDOM.nodeType === Node.ELEMENT_NODE && newDOM.nodeType === Node.ELEMENT_NODE) {
351
+ const oldChildren = Array.from(oldDOM.childNodes);
352
+ const newChildren = Array.from(newDOM.childNodes);
353
+
354
+ // Add children that exist in newDOM but not in oldDOM
355
+ for (let i = oldChildren.length; i < newChildren.length; i++) {
356
+ oldDOM.appendChild(newChildren[i].cloneNode(true));
357
+ }
358
+
359
+ // Recursively insert for existing children
360
+ const minLength = Math.min(oldChildren.length, newChildren.length);
361
+ for (let i = 0; i < minLength; i++) {
362
+ insertDOM(oldChildren[i], newChildren[i]);
363
+ }
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Helper function to update attributes of an element
369
+ * @param {Element} oldElement - The element to update
370
+ * @param {Element} newElement - The element with the desired attributes
371
+ */
372
+ function updateAttributes(oldElement: Element, newElement: Element): void {
373
+ // Remove attributes not in newElement
374
+ for (const attr of Array.from(oldElement.attributes)) {
375
+ if (!newElement.hasAttribute(attr.name)) {
376
+ oldElement.removeAttribute(attr.name);
377
+ }
378
+ }
379
+
380
+ // Add or update attributes from newElement
381
+ for (const attr of Array.from(newElement.attributes)) {
382
+ // Use strict equality to ensure empty strings are properly handled
383
+ if (!attr.name.startsWith('@') &&
384
+ (oldElement.getAttribute(attr.name) !== attr.value ||
385
+ (!oldElement.hasAttribute(attr.name) && attr.value === ""))) {
386
+ if(oldElement.children.length === 0) {
387
+ oldElement.outerHTML = newElement.outerHTML
388
+ } else oldElement.setAttribute(attr.name, attr.value)
389
+ }
390
+ }
391
+ }
392
+ /**
393
+ * Helper function to check if two nodes are of the same type
394
+ * @param {Node} oldNode - The old node
395
+ * @param {Node} newNode - The new node
396
+ * @returns {boolean} - Whether the nodes are of the same type
397
+ */
398
+ function areNodesOfSameType(oldNode: Node, newNode: Node): boolean {
399
+ // Check if both nodes are defined
400
+ if (!oldNode || !newNode) {
401
+ return false;
402
+ }
403
+
404
+ // Check if node types match
405
+ if (oldNode.nodeType !== newNode.nodeType) {
406
+ return false;
407
+ }
408
+
409
+ // For element nodes, check if tag names match
410
+ if (oldNode.nodeType === Node.ELEMENT_NODE && newNode.nodeType === Node.ELEMENT_NODE) {
411
+ const oldElement = oldNode as Element;
412
+ const newElement = newNode as Element;
413
+ return oldElement.tagName === newElement.tagName;
414
+ }
415
+
416
+ return true;
417
+ }