@dinoreic/fez 0.3.0 → 0.4.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.
@@ -46,7 +46,7 @@ export default (Fez) => {
46
46
  script.type = 'module';
47
47
  script.textContent = config.script;
48
48
  document.head.appendChild(script);
49
- setTimeout(()=>script.remove(), 100)
49
+ requestAnimationFrame(()=>script.remove())
50
50
  } else {
51
51
  try {
52
52
  new Function(config.script)();
@@ -121,6 +121,110 @@ export default (Fez) => {
121
121
  return element;
122
122
  }
123
123
 
124
+ // Fetch wrapper with automatic caching and data handling
125
+ // Usage:
126
+ // Fez.fetch(url) - GET request (default)
127
+ // Fez.fetch(url, callback) - GET with callback
128
+ // Fez.fetch(url, data) - GET with query params (?foo=bar&baz=qux)
129
+ // Fez.fetch(url, data, callback) - GET with query params and callback
130
+ // Fez.fetch('POST', url, data) - POST with FormData body (multipart/form-data)
131
+ // Fez.fetch('POST', url, data, callback) - POST with FormData and callback
132
+ // Data object is automatically converted:
133
+ // - GET: appended as URL query parameters
134
+ // - POST: sent as FormData (multipart/form-data) without custom headers
135
+ Fez.fetch = function(...args) {
136
+ // Initialize cache if not exists
137
+ Fez._fetchCache ||= {};
138
+
139
+ let method = 'GET';
140
+ let url;
141
+ let callback;
142
+
143
+ // Check if first arg is HTTP method (uppercase letters)
144
+ if (typeof args[0] === 'string' && /^[A-Z]+$/.test(args[0])) {
145
+ method = args.shift();
146
+ }
147
+
148
+ // URL is required
149
+ url = args.shift();
150
+
151
+ // Check for data/options object
152
+ let opts = {};
153
+ let data = null;
154
+ if (typeof args[0] === 'object') {
155
+ data = args.shift();
156
+ }
157
+
158
+ // Check for callback function
159
+ if (typeof args[0] === 'function') {
160
+ callback = args.shift();
161
+ }
162
+
163
+ // Handle data based on method
164
+ if (data) {
165
+ if (method === 'GET') {
166
+ // For GET, append data as query parameters
167
+ const params = new URLSearchParams(data);
168
+ url += (url.includes('?') ? '&' : '?') + params.toString();
169
+ } else if (method === 'POST') {
170
+ // For POST, convert to FormData
171
+ const formData = new FormData();
172
+ for (const [key, value] of Object.entries(data)) {
173
+ formData.append(key, value);
174
+ }
175
+ opts.body = formData;
176
+ }
177
+ }
178
+
179
+ // Set method
180
+ opts.method = method;
181
+
182
+ // Create cache key from method, url, and stringified opts
183
+ const cacheKey = `${method}:${url}:${JSON.stringify(opts)}`;
184
+
185
+ // Check cache first
186
+ if (Fez._fetchCache[cacheKey]) {
187
+ const cachedData = Fez._fetchCache[cacheKey];
188
+ Fez.log(`fetch cache hit: ${method} ${url}`);
189
+ if (callback) {
190
+ callback(cachedData);
191
+ return;
192
+ }
193
+ return Promise.resolve(cachedData);
194
+ }
195
+
196
+ // Log live fetch
197
+ Fez.log(`fetch live: ${method} ${url}`);
198
+
199
+ // Helper to process and cache response
200
+ const processResponse = (response) => {
201
+ if (response.headers.get('content-type')?.includes('application/json')) {
202
+ return response.json();
203
+ }
204
+ return response.text();
205
+ };
206
+
207
+ // If callback provided, execute and handle
208
+ if (callback) {
209
+ fetch(url, opts)
210
+ .then(processResponse)
211
+ .then(data => {
212
+ Fez._fetchCache[cacheKey] = data;
213
+ callback(data);
214
+ })
215
+ .catch(error => Fez.onError('fetch', error));
216
+ return;
217
+ }
218
+
219
+ // Return promise with automatic JSON parsing
220
+ return fetch(url, opts)
221
+ .then(processResponse)
222
+ .then(data => {
223
+ Fez._fetchCache[cacheKey] = data;
224
+ return data;
225
+ });
226
+ }
227
+
124
228
  Fez.darkenColor = (color, percent = 20) => {
125
229
  // Convert hex to RGB
126
230
  const num = parseInt(color.replace("#", ""), 16)
@@ -181,4 +285,109 @@ export default (Fez) => {
181
285
  Fez.isTrue = (val) => {
182
286
  return ['1', 'true', 'on'].includes(String(val).toLowerCase())
183
287
  }
288
+
289
+ // Resolve a function from a string or function reference
290
+ Fez.getFunction = (pointer) => {
291
+ if (!pointer) {
292
+ return ()=>{}
293
+ }
294
+ else if (typeof pointer === 'function') {
295
+ return pointer;
296
+ }
297
+ else if (typeof pointer === 'string') {
298
+ // Check if it's a function expression (arrow function or function keyword)
299
+ // Arrow function: (args) => or args =>
300
+ const arrowFuncPattern = /^\s*\(?\s*\w+(\s*,\s*\w+)*\s*\)?\s*=>/;
301
+ const functionPattern = /^\s*function\s*\(/;
302
+
303
+ if (arrowFuncPattern.test(pointer) || functionPattern.test(pointer)) {
304
+ return new Function('return ' + pointer)();
305
+ } else if (pointer.includes('.') && !pointer.includes('(')) {
306
+ // It's a property access like "this.focus" - return a function that calls it
307
+ return new Function(`return function() { return ${pointer}(); }`);
308
+ } else {
309
+ // It's a function body
310
+ return new Function(pointer);
311
+ }
312
+ }
313
+ }
314
+
315
+ // Execute a function when DOM is ready or immediately if already loaded
316
+ Fez.onReady = (callback) => {
317
+ if (document.readyState === 'loading') {
318
+ document.addEventListener('DOMContentLoaded', ()=>{
319
+ callback()
320
+ }, { once: true })
321
+ } else {
322
+ callback()
323
+ }
324
+ }
325
+
326
+ // get unique id from string
327
+ Fez.fnv1 = (str) => {
328
+ var FNV_OFFSET_BASIS, FNV_PRIME, hash, i, j, ref;
329
+ FNV_OFFSET_BASIS = 2166136261;
330
+ FNV_PRIME = 16777619;
331
+ hash = FNV_OFFSET_BASIS;
332
+ for (i = j = 0, ref = str.length - 1; (0 <= ref ? j <= ref : j >= ref); i = 0 <= ref ? ++j : --j) {
333
+ hash ^= str.charCodeAt(i);
334
+ hash *= FNV_PRIME;
335
+ }
336
+ return hash.toString(36).replaceAll('-', '');
337
+ }
338
+
339
+ Fez.tag = (tag, opts = {}, html = '') => {
340
+ const json = encodeURIComponent(JSON.stringify(opts))
341
+ return `<${tag} data-props="${json}">${html}</${tag}>`
342
+ // const json = JSON.stringify(opts, null, 2)
343
+ // const data = `<script type="text/template">${json}</script><${tag} data-json-template="true">${html}</${tag}>`
344
+ // return data
345
+ }
346
+
347
+ // execute function until it returns true
348
+ Fez.untilTrue = (func, pingRate) => {
349
+ pingRate ||= 200
350
+
351
+ if (!func()) {
352
+ setTimeout(()=>{
353
+ Fez.untilTrue(func, pingRate)
354
+ } ,pingRate)
355
+ }
356
+ }
357
+
358
+ // throttle function calls
359
+ Fez.throttle = (func, delay = 200) => {
360
+ let lastRun = 0;
361
+ let timeout;
362
+
363
+ return function(...args) {
364
+ const now = Date.now();
365
+
366
+ if (now - lastRun >= delay) {
367
+ func.apply(this, args);
368
+ lastRun = now;
369
+ } else {
370
+ clearTimeout(timeout);
371
+ timeout = setTimeout(() => {
372
+ func.apply(this, args);
373
+ lastRun = Date.now();
374
+ }, delay - (now - lastRun));
375
+ }
376
+ };
377
+ }
378
+
379
+ // const firstTimeHash = new Map()
380
+ // Fez.firstTime = (key, func) => {
381
+ // if ( !firstTimeHash.get(key) ) {
382
+ // firstTimeHash.set(key, true)
383
+
384
+ // if (func) {
385
+ // func()
386
+ // }
387
+
388
+ // return true
389
+ // }
390
+
391
+ // return false
392
+ // }
184
393
  }
@@ -0,0 +1,25 @@
1
+ // define custom style macro - simple scss mixin
2
+ // :mobile { ... } -> @media (max-width: 768px) { ... }
3
+ // @include mobile { ... } -> @media (max-width: 768px) { ... }
4
+ // demo/fez/ui-style.fez
5
+
6
+ const CssMixins = {}
7
+
8
+ export default (Fez) => {
9
+ Fez.cssMixin = (name, content) => {
10
+ if (content) {
11
+ CssMixins[name] = content
12
+ } else {
13
+ Object.entries(CssMixins).forEach(([key, val])=>{
14
+ name = name.replaceAll(`:${key} `, `${val} `)
15
+ name = name.replaceAll(`@include ${key} `, `${val} `)
16
+ })
17
+
18
+ return name
19
+ }
20
+ }
21
+
22
+ Fez.cssMixin('mobile', '@media (max-width: 767px)')
23
+ Fez.cssMixin('tablet', '@media (min-width: 768px) and (max-width: 1023px)')
24
+ Fez.cssMixin('desktop', '@media (min-width: 1200px)')
25
+ }
@@ -0,0 +1,240 @@
1
+ // pretty print HTML
2
+ const log_pretty_print = (html) => {
3
+ const parts = html
4
+ .split(/(<\/?[^>]+>)/g)
5
+ .map(p => p.trim())
6
+ .filter(p => p);
7
+
8
+ let indent = 0;
9
+ const lines = [];
10
+
11
+ for (let i = 0; i < parts.length; i++) {
12
+ const part = parts[i];
13
+ const nextPart = parts[i + 1];
14
+ const nextNextPart = parts[i + 2];
15
+
16
+ // Check if it's a tag
17
+ if (part.startsWith('<')) {
18
+ // Check if this is an opening tag followed by text and then its closing tag
19
+ if (!part.startsWith('</') && !part.endsWith('/>') && nextPart && !nextPart.startsWith('<') && nextNextPart && nextNextPart.startsWith('</')) {
20
+ // Combine them on one line
21
+ const actualIndent = Math.max(0, indent);
22
+ lines.push(' '.repeat(actualIndent) + part + nextPart + nextNextPart);
23
+ i += 2; // Skip the next two parts
24
+ }
25
+ // Closing tag
26
+ else if (part.startsWith('</')) {
27
+ indent--;
28
+ const actualIndent = Math.max(0, indent);
29
+ lines.push(' '.repeat(actualIndent) + part);
30
+ }
31
+ // Self-closing tag
32
+ else if (part.endsWith('/>') || part.includes(' />')) {
33
+ const actualIndent = Math.max(0, indent);
34
+ lines.push(' '.repeat(actualIndent) + part);
35
+ }
36
+ // Opening tag
37
+ else {
38
+ const actualIndent = Math.max(0, indent);
39
+ lines.push(' '.repeat(actualIndent) + part);
40
+ indent++;
41
+ }
42
+ }
43
+ // Text node
44
+ else if (part) {
45
+ const actualIndent = Math.max(0, indent);
46
+ lines.push(' '.repeat(actualIndent) + part);
47
+ }
48
+ }
49
+
50
+ return lines.join('\n');
51
+ }
52
+
53
+ const LOG = (() => {
54
+ const logs = [];
55
+ const logTypes = []; // Track the original type of each log
56
+ let currentIndex = 0;
57
+ let renderContent = null; // Will hold the render function
58
+
59
+ // Add ESC key handler and arrow key navigation
60
+ document.addEventListener('keydown', (e) => {
61
+ if (e.key === 'Escape') {
62
+ e.preventDefault();
63
+ const dialog = document.getElementById('dump-dialog');
64
+ const button = document.getElementById('log-reopen-button');
65
+
66
+ if (dialog) {
67
+ // Close dialog
68
+ dialog.remove();
69
+ createLogButton();
70
+ } else if (button) {
71
+ // Open dialog
72
+ button.remove();
73
+ showLogDialog();
74
+ }
75
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') {
76
+ const dialog = document.getElementById('dump-dialog');
77
+ if (dialog && logs.length > 0) {
78
+ e.preventDefault();
79
+ if (e.key === 'ArrowLeft' && currentIndex > 0) {
80
+ currentIndex--;
81
+ localStorage.setItem('_LOG_INDEX', currentIndex);
82
+ renderContent();
83
+ } else if (e.key === 'ArrowRight' && currentIndex < logs.length - 1) {
84
+ currentIndex++;
85
+ localStorage.setItem('_LOG_INDEX', currentIndex);
86
+ renderContent();
87
+ } else if (e.key === 'ArrowUp' && currentIndex > 0) {
88
+ currentIndex = Math.max(0, currentIndex - 5);
89
+ localStorage.setItem('_LOG_INDEX', currentIndex);
90
+ renderContent();
91
+ } else if (e.key === 'ArrowDown' && currentIndex < logs.length - 1) {
92
+ currentIndex = Math.min(logs.length - 1, currentIndex + 5);
93
+ localStorage.setItem('_LOG_INDEX', currentIndex);
94
+ renderContent();
95
+ }
96
+ }
97
+ }
98
+ });
99
+
100
+ const createLogButton = () => {
101
+ let btn = document.getElementById('log-reopen-button');
102
+ if (!btn) {
103
+ btn = document.body.appendChild(document.createElement('button'));
104
+ btn.id = 'log-reopen-button';
105
+ btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>LOG';
106
+ btn.style.cssText =
107
+ 'position:fixed; top: 10px; right: 10px;' +
108
+ 'padding:10px 20px;background:#ff3333;color:#fff;border:none;' +
109
+ 'cursor:pointer;font:14px/1.4 monospace;z-index:2147483647;' +
110
+ 'border-radius:8px;display:flex;align-items:center;' +
111
+ 'opacity:1;visibility:visible;box-shadow:0 4px 12px rgba(255,51,51,0.3)';
112
+ btn.onclick = () => {
113
+ btn.remove();
114
+ showLogDialog();
115
+ };
116
+ }
117
+ };
118
+
119
+ const showLogDialog = () => {
120
+ let d = document.getElementById('dump-dialog');
121
+ if (!d) {
122
+ d = document.body.appendChild(document.createElement('div'));
123
+ d.id = 'dump-dialog';
124
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
125
+ d.style.cssText =
126
+ 'position:absolute; top:' + (scrollTop + 20) + 'px; left: 20px; right:20px;' +
127
+ 'background:#fff; border:1px solid #333; box-shadow:0 0 10px rgba(0,0,0,0.5);' +
128
+ 'padding:20px; overflow:auto; z-index:2147483646; font:13px/1.4 monospace;' +
129
+ 'white-space:pre; display:block; opacity:1; visibility:visible';
130
+ }
131
+
132
+ // Check if we have a saved index and it's still valid
133
+ const savedIndex = parseInt(localStorage.getItem('_LOG_INDEX'));
134
+ if (!isNaN(savedIndex) && savedIndex >= 0 && savedIndex < logs.length) {
135
+ currentIndex = savedIndex;
136
+ } else {
137
+ currentIndex = logs.length - 1;
138
+ }
139
+
140
+ renderContent = () => {
141
+ const buttons = logs.map((_, i) => {
142
+ let bgColor = '#f0f0f0'; // default
143
+ if (i !== currentIndex) {
144
+ if (logTypes[i] === 'object') {
145
+ bgColor = '#d6e3ef'; // super light blue
146
+ } else if (logTypes[i] === 'array') {
147
+ bgColor = '#d8d5ef'; // super light indigo
148
+ }
149
+ }
150
+ return `<button style="font-size: 14px; font-weight: 400; padding:2px 6px; margin: 0 2px 2px 0;cursor:pointer;background:${i === currentIndex ? '#333' : bgColor};color:${i === currentIndex ? '#fff' : '#000'}" data-index="${i}">${i + 1}</button>`
151
+ }).join('');
152
+
153
+ d.innerHTML =
154
+ '<div style="display:flex;flex-direction:column;height:100%">' +
155
+ '<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px">' +
156
+ '<div style="display:flex;flex-wrap:wrap;gap:4px;flex:1;margin-right:10px">' + buttons + '</div>' +
157
+ '<button style="padding:4px 8px;cursor:pointer;flex-shrink:0">&times;</button>' +
158
+ '</div>' +
159
+ '<xmp style="font-family:monospace;flex:1;overflow:auto;margin:0;padding:0;color:#000;background:#fff;font-size:14px;line-height:22px">' + logs[currentIndex] + '</xmp>' +
160
+ '</div>';
161
+
162
+ d.querySelector('button[style*="flex-shrink:0"]').onclick = () => {
163
+ d.remove();
164
+ createLogButton();
165
+ };
166
+
167
+ d.querySelectorAll('button[data-index]').forEach(btn => {
168
+ btn.onclick = () => {
169
+ currentIndex = parseInt(btn.dataset.index);
170
+ localStorage.setItem('_LOG_INDEX', currentIndex);
171
+ renderContent();
172
+ };
173
+ });
174
+ };
175
+
176
+ renderContent();
177
+ };
178
+
179
+ return o => {
180
+ if (!document.body) {
181
+ window.requestAnimationFrame( () => LOG(o) )
182
+ return
183
+ }
184
+
185
+ // Store the original type
186
+ let originalType = typeof o;
187
+
188
+ if (o instanceof Node) {
189
+ if (o.nodeType === Node.TEXT_NODE) {
190
+ o = o.textContent || String(o)
191
+ } else {
192
+ o = log_pretty_print(o.outerHTML)
193
+ }
194
+ }
195
+
196
+ if (o === undefined) { o = 'undefined' }
197
+ if (o === null) { o = 'null' }
198
+
199
+ if (Array.isArray(o)) {
200
+ originalType = 'array';
201
+ } else if (typeof o === 'object' && o !== null) {
202
+ originalType = 'object';
203
+ }
204
+
205
+ if (typeof o != 'string') {
206
+ o = JSON.stringify(o, (key, value) => {
207
+ if (typeof value === 'function') {
208
+ return String(value);
209
+ }
210
+ return value;
211
+ }, 2).replaceAll('<', '&lt;')
212
+ }
213
+
214
+ o = o.trim()
215
+
216
+ logs.push(o + `\n\ntype: ${originalType}`);
217
+ logTypes.push(originalType);
218
+
219
+ // Check if log dialog is open by checking for element
220
+ const isOpen = !!document.getElementById('dump-dialog');
221
+
222
+ if (!isOpen) {
223
+ // Show log dialog by default
224
+ showLogDialog();
225
+ } else {
226
+ // Update current index to the new log and refresh
227
+ currentIndex = logs.length - 1;
228
+ localStorage.setItem('_LOG_INDEX', currentIndex);
229
+ if (renderContent) {
230
+ renderContent();
231
+ }
232
+ }
233
+ };
234
+ })();
235
+
236
+ if (typeof window !== 'undefined' && !window.LOG) {
237
+ window.LOG = LOG
238
+ }
239
+
240
+ export default LOG
@@ -0,0 +1,98 @@
1
+ // Highlight all Fez elements with their names
2
+ const highlightAll = () => {
3
+ // Only work if Fez.DEV is true OR (port is above 2999 and Fez.DEV is not false)
4
+ const port = parseInt(window.location.port) || 80;
5
+ if (!(Fez.DEV === true || (port > 2999 && Fez.DEV !== false))) return;
6
+
7
+ // Check if highlights already exist
8
+ const existingHighlights = document.querySelectorAll('.fez-highlight-overlay');
9
+
10
+ if (existingHighlights.length > 0) {
11
+ // Remove existing highlights
12
+ existingHighlights.forEach(el => el.remove());
13
+ return;
14
+ }
15
+
16
+ // Find all Fez and Svelte elements
17
+ const allElements = document.querySelectorAll('.fez, .svelte');
18
+
19
+ allElements.forEach(el => {
20
+ let componentName = null;
21
+ let componentType = null;
22
+
23
+ // Check for Fez component
24
+ if (el.classList.contains('fez') && el.fez && el.fez.fezName) {
25
+ componentName = el.fez.fezName;
26
+ componentType = 'fez';
27
+ }
28
+ // Check for Svelte component
29
+ else if (el.classList.contains('svelte') && el.svelte && el.svelte.svelteName) {
30
+ componentName = el.svelte.svelteName;
31
+ componentType = 'svelte';
32
+ }
33
+
34
+ if (componentName) {
35
+ // Create overlay div
36
+ const overlay = document.createElement('div');
37
+ overlay.className = 'fez-highlight-overlay';
38
+
39
+ // Get element position
40
+ const rect = el.getBoundingClientRect();
41
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
42
+ const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
43
+
44
+ // Style the overlay
45
+ overlay.style.cssText = `
46
+ position: absolute;
47
+ top: ${rect.top + scrollTop}px;
48
+ left: ${rect.left + scrollLeft}px;
49
+ width: ${rect.width}px;
50
+ height: ${rect.height}px;
51
+ border: 1px solid ${componentType === 'svelte' ? 'blue' : 'red'};
52
+ pointer-events: none;
53
+ z-index: 9999;
54
+ `;
55
+
56
+ // Create label for component name
57
+ const label = document.createElement('div');
58
+ label.textContent = componentName;
59
+ label.style.cssText = `
60
+ position: absolute;
61
+ top: -20px;
62
+ left: 0;
63
+ background: ${componentType === 'svelte' ? 'blue' : 'red'};
64
+ color: white;
65
+ padding: 4px 6px 2px 6px;
66
+ font-size: 14px;
67
+ font-family: monospace;
68
+ line-height: 1;
69
+ white-space: nowrap;
70
+ cursor: pointer;
71
+ pointer-events: auto;
72
+ text-transform: uppercase;
73
+ `;
74
+
75
+ // Add click handler to dump the node
76
+ label.addEventListener('click', (e) => {
77
+ e.stopPropagation();
78
+ Fez.dump(el);
79
+ });
80
+
81
+ overlay.appendChild(label);
82
+ document.body.appendChild(overlay);
83
+ }
84
+ });
85
+ }
86
+
87
+ // Bind Ctrl+E to highlightAll
88
+ document.addEventListener('keydown', (event) => {
89
+ if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
90
+ // Check if target is not inside a form
91
+ if (!event.target.closest('form')) {
92
+ event.preventDefault();
93
+ highlightAll();
94
+ }
95
+ }
96
+ });
97
+
98
+ export default highlightAll