@fiyuu/core 0.1.0 → 0.2.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/package.json +5 -5
- package/src/artifacts.ts +328 -0
- package/src/config.ts +260 -0
- package/src/contracts.ts +247 -0
- package/src/{generator.js → generator.ts} +68 -41
- package/src/media.ts +143 -0
- package/src/reactive.ts +229 -0
- package/src/{responsive-wrapper.js → responsive-wrapper.ts} +118 -74
- package/src/responsive.ts +54 -0
- package/src/scanner.ts +289 -0
- package/src/template.ts +110 -0
- package/src/{virtual.js → virtual.tsx} +16 -7
- package/LICENSE +0 -674
- package/README.md +0 -194
- package/src/artifacts.d.ts +0 -20
- package/src/artifacts.js +0 -274
- package/src/client.js +0 -8
- package/src/config.d.ts +0 -179
- package/src/config.js +0 -58
- package/src/contracts.d.ts +0 -176
- package/src/contracts.js +0 -45
- package/src/generator.d.ts +0 -2
- package/src/index.js +0 -11
- package/src/media.d.ts +0 -44
- package/src/media.js +0 -87
- package/src/reactive.d.ts +0 -53
- package/src/reactive.js +0 -160
- package/src/responsive-wrapper.d.ts +0 -18
- package/src/responsive.d.ts +0 -15
- package/src/responsive.js +0 -48
- package/src/scanner.d.ts +0 -65
- package/src/scanner.js +0 -200
- package/src/state.js +0 -1
- package/src/template.d.ts +0 -48
- package/src/template.js +0 -98
- package/src/virtual.d.ts +0 -14
- /package/src/{client.d.ts → client.ts} +0 -0
- /package/src/{index.d.ts → index.ts} +0 -0
- /package/src/{state.d.ts → state.ts} +0 -0
package/src/reactive.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { html, raw, unsafeHtml, type ComponentProps, type RawHtml } from "./template.js";
|
|
2
|
+
|
|
3
|
+
export type Signal<T> = {
|
|
4
|
+
get(): T;
|
|
5
|
+
set(next: T): void;
|
|
6
|
+
subscribe(fn: () => void): () => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type RenderBlock = string | (() => string);
|
|
10
|
+
|
|
11
|
+
export function createSignal<T>(initial: T): Signal<T> {
|
|
12
|
+
let value = initial;
|
|
13
|
+
const listeners = new Set<() => void>();
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
get() {
|
|
17
|
+
return value;
|
|
18
|
+
},
|
|
19
|
+
set(next: T) {
|
|
20
|
+
if (value === next) return;
|
|
21
|
+
value = next;
|
|
22
|
+
for (const fn of listeners) fn();
|
|
23
|
+
},
|
|
24
|
+
subscribe(fn: () => void) {
|
|
25
|
+
listeners.add(fn);
|
|
26
|
+
return () => listeners.delete(fn);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function when(condition: unknown, content: string | (() => string)): string {
|
|
32
|
+
if (!condition) return "";
|
|
33
|
+
return typeof content === "function" ? content() : content;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ElseBranch = {
|
|
37
|
+
kind: "Else";
|
|
38
|
+
render: () => string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function Else(content: RenderBlock): ElseBranch {
|
|
42
|
+
return {
|
|
43
|
+
kind: "Else",
|
|
44
|
+
render: () => renderBlock(content),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function If(input: {
|
|
49
|
+
condition: unknown;
|
|
50
|
+
then: RenderBlock;
|
|
51
|
+
else?: RenderBlock | ElseBranch;
|
|
52
|
+
}): string {
|
|
53
|
+
if (input.condition) {
|
|
54
|
+
return renderBlock(input.then);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!input.else) {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isElseBranch(input.else)) {
|
|
62
|
+
return input.else.render();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return renderBlock(input.else);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isElseBranch(value: unknown): value is ElseBranch {
|
|
69
|
+
return Boolean(
|
|
70
|
+
value &&
|
|
71
|
+
typeof value === "object" &&
|
|
72
|
+
"kind" in value &&
|
|
73
|
+
"render" in value &&
|
|
74
|
+
(value as { kind?: unknown }).kind === "Else" &&
|
|
75
|
+
typeof (value as { render?: unknown }).render === "function",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderBlock(block?: RenderBlock): string {
|
|
80
|
+
if (block == null) {
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (typeof block === "function") {
|
|
85
|
+
return block();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return block;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function each<T>(
|
|
92
|
+
items: T[] | readonly T[],
|
|
93
|
+
renderItem: (item: T, index: number) => string,
|
|
94
|
+
): string {
|
|
95
|
+
if (!items || items.length === 0) return "";
|
|
96
|
+
return items.map(renderItem).join("");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function For<T>(input: {
|
|
100
|
+
each: readonly T[] | T[];
|
|
101
|
+
render: (item: T, index: number) => string;
|
|
102
|
+
empty?: RenderBlock;
|
|
103
|
+
}): string {
|
|
104
|
+
if (!input.each || input.each.length === 0) {
|
|
105
|
+
return renderBlock(input.empty);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return input.each.map((item, index) => input.render(item, index)).join("");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let _eventId = 0;
|
|
112
|
+
const _eventRegistry = new Map<string, (event: Event) => void>();
|
|
113
|
+
|
|
114
|
+
export function onEvent(handler: (event: Event) => void): string {
|
|
115
|
+
const id = `fiyuu_evt_${_eventId++}`;
|
|
116
|
+
_eventRegistry.set(id, handler);
|
|
117
|
+
return id;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getEventHandler(id: string): ((event: Event) => void) | undefined {
|
|
121
|
+
return _eventRegistry.get(id);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function clearEventRegistry(): void {
|
|
125
|
+
_eventRegistry.clear();
|
|
126
|
+
_eventId = 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function mount(
|
|
130
|
+
root: HTMLElement,
|
|
131
|
+
render: () => string,
|
|
132
|
+
signals: Signal<unknown>[] = [],
|
|
133
|
+
): () => void {
|
|
134
|
+
function update(): void {
|
|
135
|
+
root.innerHTML = render();
|
|
136
|
+
attachDeclarativeEvents(root);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
update();
|
|
140
|
+
|
|
141
|
+
const unsubscribers = signals.map((s) => s.subscribe(update));
|
|
142
|
+
return () => {
|
|
143
|
+
unsubscribers.forEach((u) => u());
|
|
144
|
+
root.innerHTML = "";
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function attachDeclarativeEvents(root: HTMLElement): void {
|
|
149
|
+
const elements = root.querySelectorAll("[data-fiyuu-on]");
|
|
150
|
+
for (const el of elements) {
|
|
151
|
+
const parts = el.getAttribute("data-fiyuu-on")!.split(":");
|
|
152
|
+
if (parts.length !== 2) continue;
|
|
153
|
+
|
|
154
|
+
const [eventName, handlerId] = parts as [string, string];
|
|
155
|
+
const handler = _eventRegistry.get(handlerId);
|
|
156
|
+
if (handler) {
|
|
157
|
+
el.addEventListener(eventName, handler);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function createFiyuuStore<T>(initialValue: T) {
|
|
163
|
+
const signal = createSignal(initialValue);
|
|
164
|
+
return {
|
|
165
|
+
get: signal.get,
|
|
166
|
+
set: signal.set,
|
|
167
|
+
subscribe: signal.subscribe,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function createFlatStore<T>(initialValue: T) {
|
|
172
|
+
return createFiyuuStore(initialValue);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let scopedStyleCounter = 0;
|
|
176
|
+
|
|
177
|
+
function toScopeSlug(value: string): string {
|
|
178
|
+
return value
|
|
179
|
+
.toLowerCase()
|
|
180
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
181
|
+
.replace(/^-+|-+$/g, "")
|
|
182
|
+
.slice(0, 24) || "scope";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function scopedStyles(name: string, css: string): { scopeClass: string; style: RawHtml } {
|
|
186
|
+
scopedStyleCounter += 1;
|
|
187
|
+
const scopeClass = `fx-${toScopeSlug(name)}-${scopedStyleCounter}`;
|
|
188
|
+
const scopedCss = css.replaceAll(":scope", `.${scopeClass}`);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
scopeClass,
|
|
192
|
+
style: raw(`<style>${scopedCss}</style>`),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function escapeSingleQuotes(value: string): string {
|
|
197
|
+
return value.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function island(options: {
|
|
201
|
+
id: string;
|
|
202
|
+
placeholder: string;
|
|
203
|
+
bootCode: string;
|
|
204
|
+
trigger?: "click" | "hover" | "visible";
|
|
205
|
+
}): string {
|
|
206
|
+
const trigger = options.trigger ?? "click";
|
|
207
|
+
const safeId = escapeSingleQuotes(options.id);
|
|
208
|
+
|
|
209
|
+
const script = `(function(){\n var root = document.querySelector('[data-fiyuu-island="${safeId}"]');\n if (!root) return;\n\n var started = false;\n\n function start() {\n if (started) return;\n started = true;\n ${options.bootCode}\n }\n\n if ("${trigger}" === "click") {\n root.addEventListener("click", start, { once: true });\n return;\n }\n\n if ("${trigger}" === "hover") {\n root.addEventListener("mouseenter", start, { once: true });\n root.addEventListener("focusin", start, { once: true });\n return;\n }\n\n if (typeof window.IntersectionObserver !== "function") {\n start();\n return;\n }\n\n var observer = new IntersectionObserver(function(entries) {\n for (var i = 0; i < entries.length; i += 1) {\n if (!entries[i].isIntersecting) continue;\n observer.disconnect();\n start();\n break;\n }\n }, { rootMargin: "220px" });\n\n observer.observe(root);\n})();`;
|
|
210
|
+
|
|
211
|
+
return html`
|
|
212
|
+
<section data-fiyuu-island="${options.id}">
|
|
213
|
+
${raw(options.placeholder)}
|
|
214
|
+
</section>
|
|
215
|
+
<script>${raw(script)}</script>
|
|
216
|
+
`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function debugTag(name: string, content: string): string {
|
|
220
|
+
return html`<section data-fiyuu-debug-tag="${name}">${raw(content)}</section>`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function humanDebugOverlay(): RawHtml {
|
|
224
|
+
const script = `(function() {\n var lastTag = "unknown";\n\n function findTag(target) {\n if (!target || !target.closest) return null;\n var tagged = target.closest("[data-fiyuu-debug-tag]");\n if (!tagged) return null;\n return tagged.getAttribute("data-fiyuu-debug-tag");\n }\n\n document.addEventListener("pointerdown", function(event) {\n var tag = findTag(event.target);\n if (tag) lastTag = tag;\n }, true);\n\n document.addEventListener("focusin", function(event) {\n var tag = findTag(event.target);\n if (tag) lastTag = tag;\n }, true);\n\n function inferHint(message) {\n var text = String(message || "").toLowerCase();\n if (text.includes("undefined") || text.includes("null")) {\n return "Eksik null/undefined kontrolu var. Verinin geldigi yeri kontrol et.";\n }\n if (text.includes("is not a function")) {\n return "Fonksiyon yerine farkli bir tip geciyor. Cagrilan methodu dogrula.";\n }\n if (text.includes("json")) {\n return "Beklenen veri formati ile gelen cevap uyusmuyor. Query/action ciktisini kontrol et.";\n }\n return "Kosul bloklarinda beklenmeyen bir durum olabilir. Son degisen bolumu kontrol et.";\n }\n\n function getPanel() {\n var panel = document.getElementById("fiyuu-human-debug");\n if (panel) return panel;\n\n panel = document.createElement("aside");\n panel.id = "fiyuu-human-debug";\n panel.style.position = "fixed";\n panel.style.right = "16px";\n panel.style.bottom = "16px";\n panel.style.maxWidth = "min(92vw, 480px)";\n panel.style.padding = "12px 14px";\n panel.style.borderRadius = "10px";\n panel.style.border = "1px solid #fecaca";\n panel.style.background = "#fff1f2";\n panel.style.color = "#7f1d1d";\n panel.style.fontFamily = "ui-monospace, SFMono-Regular, Menlo, monospace";\n panel.style.fontSize = "12px";\n panel.style.lineHeight = "1.45";\n panel.style.whiteSpace = "pre-line";\n panel.style.zIndex = "99999";\n panel.style.boxShadow = "0 10px 28px rgba(0,0,0,0.12)";\n document.body.appendChild(panel);\n return panel;\n }\n\n if (!window.fiyuu || typeof window.fiyuu.onError !== "function") {\n return;\n }\n\n window.fiyuu.onError(function(detail) {\n var source = detail && detail.source ? detail.source : "unknown file";\n var line = detail && detail.line ? ":" + detail.line : "";\n var message = detail && detail.message ? detail.message : "Unknown runtime error";\n var hint = inferHint(message);\n var tag = lastTag;\n var activeElement = document.activeElement;\n\n if (tag === "unknown" && activeElement && activeElement.tagName) {\n tag = activeElement.tagName.toLowerCase();\n }\n\n var panel = getPanel();\n panel.textContent = [\n "Insancil Hata Analizi",\n "Mesaj: " + message,\n "Dosya: " + source + line,\n "Etiket/Bolum: " + tag,\n "Ihtimal: " + hint,\n ].join("\\n");\n });\n})();`;
|
|
225
|
+
|
|
226
|
+
return raw(`<script>${script}</script>`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export type FiyuuStore<T> = ReturnType<typeof createFiyuuStore<T>>;
|
|
@@ -1,83 +1,125 @@
|
|
|
1
1
|
import { escapeHtml } from "./template.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
|
|
3
|
+
export type BreakpointPreset = "mobile-first" | "desktop-first" | "full-width" | "article" | "card" | "dashboard" | "narrow";
|
|
4
|
+
export type PreviewDevice = "phone" | "tablet" | "watch" | "desktop";
|
|
5
|
+
export type PreviewMode = "mobile" | "desktop" | "both";
|
|
6
|
+
|
|
7
|
+
export interface ResponsiveWrapperOptions {
|
|
8
|
+
content: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
style?: string;
|
|
11
|
+
maxWidth?: number;
|
|
12
|
+
padding?: "none" | "sm" | "md" | "lg" | "xl" | string;
|
|
13
|
+
previewEnabled?: boolean;
|
|
14
|
+
previewLabel?: string;
|
|
15
|
+
id?: string;
|
|
16
|
+
preset?: BreakpointPreset;
|
|
17
|
+
/** Default preview mode */
|
|
18
|
+
defaultPreviewMode?: PreviewMode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PRESETS: Record<BreakpointPreset, { maxWidth: number; padding: string; class: string }> = {
|
|
22
|
+
"mobile-first": { maxWidth: 768, padding: "1rem", class: "rw-mobile-first" },
|
|
23
|
+
"desktop-first": { maxWidth: 1280, padding: "1rem", class: "rw-desktop-first" },
|
|
24
|
+
"full-width": { maxWidth: 0, padding: "0", class: "rw-full-width" },
|
|
25
|
+
"article": { maxWidth: 720, padding: "1.5rem", class: "rw-article" },
|
|
26
|
+
"card": { maxWidth: 480, padding: "1rem", class: "rw-card" },
|
|
27
|
+
"dashboard": { maxWidth: 1440, padding: "1rem", class: "rw-dashboard" },
|
|
28
|
+
"narrow": { maxWidth: 540, padding: "1rem", class: "rw-narrow" },
|
|
10
29
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
|
|
31
|
+
const DEVICES: Record<PreviewDevice, { width: number; height: number; label: string; icon: string; scale: number }> = {
|
|
32
|
+
phone: {
|
|
33
|
+
width: 390,
|
|
34
|
+
height: 844,
|
|
35
|
+
label: "iPhone",
|
|
36
|
+
scale: 0.72,
|
|
37
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" style="width:15px;height:15px"><rect x="5" y="2" width="14" height="20" rx="2"/><path d="M12 18h.01"/></svg>',
|
|
38
|
+
},
|
|
39
|
+
tablet: {
|
|
40
|
+
width: 768,
|
|
41
|
+
height: 1024,
|
|
42
|
+
label: "iPad",
|
|
43
|
+
scale: 0.56,
|
|
44
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" style="width:15px;height:15px"><rect x="4" y="2" width="16" height="20" rx="2"/><path d="M12 18h.01"/></svg>',
|
|
45
|
+
},
|
|
46
|
+
watch: {
|
|
47
|
+
width: 200,
|
|
48
|
+
height: 240,
|
|
49
|
+
label: "Watch",
|
|
50
|
+
scale: 0.85,
|
|
51
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" style="width:15px;height:15px"><rect x="6" y="5" width="12" height="14" rx="3"/><path d="M12 12h.01M9 5V3M15 5V3M9 19v2M15 19v2"/></svg>',
|
|
52
|
+
},
|
|
53
|
+
desktop: {
|
|
54
|
+
width: 1280,
|
|
55
|
+
height: 800,
|
|
56
|
+
label: "Desktop",
|
|
57
|
+
scale: 0.52,
|
|
58
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" style="width:15px;height:15px"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>',
|
|
59
|
+
},
|
|
40
60
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
|
|
62
|
+
export function responsiveWrapper(options: ResponsiveWrapperOptions): string {
|
|
63
|
+
const {
|
|
64
|
+
content,
|
|
65
|
+
class: customClass = "",
|
|
66
|
+
style: customStyle = "",
|
|
67
|
+
maxWidth: customMaxWidth,
|
|
68
|
+
padding: customPadding,
|
|
69
|
+
previewEnabled = true,
|
|
70
|
+
previewLabel = "Responsive",
|
|
71
|
+
id,
|
|
72
|
+
preset = "mobile-first",
|
|
73
|
+
defaultPreviewMode = "both",
|
|
74
|
+
} = options;
|
|
75
|
+
|
|
76
|
+
const presetConfig = PRESETS[preset];
|
|
77
|
+
const maxWidth = customMaxWidth ?? presetConfig.maxWidth;
|
|
78
|
+
const containerClass = customClass || presetConfig.class;
|
|
79
|
+
|
|
80
|
+
const widthStyle = maxWidth > 0 ? `max-width:${maxWidth}px;` : "";
|
|
81
|
+
const paddingStyle = getPaddingStyle(customPadding ?? presetConfig.padding);
|
|
82
|
+
|
|
83
|
+
const wrapperId = id || `rw-${generateId()}`;
|
|
84
|
+
|
|
85
|
+
const previewButton = previewEnabled ? buildPreviewButton(wrapperId) : "";
|
|
86
|
+
const previewPanel = previewEnabled ? buildPreviewPanel(wrapperId, content, preset, defaultPreviewMode) : "";
|
|
87
|
+
const previewStyles = previewEnabled ? buildPreviewStyles() : "";
|
|
88
|
+
|
|
89
|
+
const centerStyle = maxWidth > 0 ? "margin-left:auto;margin-right:auto;" : "";
|
|
90
|
+
return `<div id="${wrapperId}" class="fiyuu-responsive-wrapper ${containerClass}" style="position:relative;width:100%;${widthStyle}${centerStyle}${paddingStyle}${customStyle}">${previewButton}${content}${previewPanel}</div>${previewStyles}`;
|
|
54
91
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
92
|
+
|
|
93
|
+
function getPaddingStyle(padding: string): string {
|
|
94
|
+
if (!padding || padding === "none") return "";
|
|
95
|
+
const map: Record<string, string> = { sm: "0.75rem", md: "1rem", lg: "1.5rem", xl: "2rem" };
|
|
96
|
+
return `padding:${map[padding] || padding};`;
|
|
60
97
|
}
|
|
61
|
-
|
|
62
|
-
|
|
98
|
+
|
|
99
|
+
function generateId(): string {
|
|
100
|
+
return Math.random().toString(36).substring(2, 8);
|
|
63
101
|
}
|
|
64
|
-
|
|
65
|
-
|
|
102
|
+
|
|
103
|
+
function buildPreviewButton(wrapperId: string): string {
|
|
104
|
+
return `<button type="button" class="fiyuu-rw-btn" onclick="window.__fiyuu_rw_open('${wrapperId}')" aria-label="Responsive Preview" title="Responsive Preview">
|
|
66
105
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" style="width:15px;height:15px"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
67
106
|
<span>Responsive Preview</span>
|
|
68
107
|
</button>`;
|
|
69
108
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
109
|
+
|
|
110
|
+
function buildPreviewPanel(wrapperId: string, content: string, preset: string, defaultMode: PreviewMode): string {
|
|
111
|
+
const phoneDevice = DEVICES.phone;
|
|
112
|
+
const tabletDevice = DEVICES.tablet;
|
|
113
|
+
const watchDevice = DEVICES.watch;
|
|
114
|
+
const desktopDevice = DEVICES.desktop;
|
|
115
|
+
|
|
116
|
+
// Scale wrappers clip to actual visual size so height matches
|
|
117
|
+
const phoneW = Math.round(phoneDevice.width * phoneDevice.scale);
|
|
118
|
+
const phoneH = Math.round(phoneDevice.height * phoneDevice.scale);
|
|
119
|
+
const desktopW = Math.round(desktopDevice.width * desktopDevice.scale);
|
|
120
|
+
const desktopH = Math.round(desktopDevice.height * desktopDevice.scale);
|
|
121
|
+
|
|
122
|
+
return `<div id="${wrapperId}-panel" class="fiyuu-rw-panel" style="display:none;">
|
|
81
123
|
<!-- Left float sidebar -->
|
|
82
124
|
<div class="fiyuu-rw-sidebar">
|
|
83
125
|
<button type="button" class="fiyuu-rw-close" onclick="window.__fiyuu_rw_close('${wrapperId}')" aria-label="Close" title="Close">
|
|
@@ -142,8 +184,9 @@ function buildPreviewPanel(wrapperId, content, preset, defaultMode) {
|
|
|
142
184
|
</div>
|
|
143
185
|
</div>`;
|
|
144
186
|
}
|
|
145
|
-
|
|
146
|
-
|
|
187
|
+
|
|
188
|
+
function buildPreviewStyles(): string {
|
|
189
|
+
return `<style id="fiyuu-rw-styles">
|
|
147
190
|
.fiyuu-responsive-wrapper{position:relative;width:100%;box-sizing:border-box;}
|
|
148
191
|
|
|
149
192
|
/* Preview Button (FAB) */
|
|
@@ -202,8 +245,9 @@ function buildPreviewStyles() {
|
|
|
202
245
|
@media(max-width:480px){.fiyuu-rw-mode-btn span{display:none;}.fiyuu-rw-mode-btn{padding:8px;gap:0;width:40px;height:40px;}.fiyuu-rw-viewport{padding:10px 6px;}}
|
|
203
246
|
</style>`;
|
|
204
247
|
}
|
|
205
|
-
|
|
206
|
-
|
|
248
|
+
|
|
249
|
+
export function responsiveWrapperScript(): string {
|
|
250
|
+
return `<script>
|
|
207
251
|
(function(){
|
|
208
252
|
if (window.__fiyuu_rw_open) return;
|
|
209
253
|
window.__fiyuu_rw_open = function(id) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const BREAKPOINTS = {
|
|
2
|
+
xs: 480,
|
|
3
|
+
sm: 640,
|
|
4
|
+
md: 768,
|
|
5
|
+
lg: 1024,
|
|
6
|
+
xl: 1280,
|
|
7
|
+
"2xl": 1536,
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export type BreakpointName = keyof typeof BREAKPOINTS;
|
|
11
|
+
|
|
12
|
+
export function mediaUp(name: BreakpointName, css: string): string {
|
|
13
|
+
return `@media (min-width:${BREAKPOINTS[name]}px){${css}}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function mediaDown(name: BreakpointName, css: string): string {
|
|
17
|
+
return `@media (max-width:${BREAKPOINTS[name] - 0.02}px){${css}}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function mediaBetween(min: BreakpointName, max: BreakpointName, css: string): string {
|
|
21
|
+
return `@media (min-width:${BREAKPOINTS[min]}px) and (max-width:${BREAKPOINTS[max] - 0.02}px){${css}}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function fluid(minSizePx: number, maxSizePx: number, minViewportPx = 360, maxViewportPx = 1440): string {
|
|
25
|
+
if (maxViewportPx <= minViewportPx) {
|
|
26
|
+
throw new Error("fluid requires maxViewportPx to be larger than minViewportPx.");
|
|
27
|
+
}
|
|
28
|
+
const slope = ((maxSizePx - minSizePx) / (maxViewportPx - minViewportPx)) * 100;
|
|
29
|
+
const intercept = minSizePx - (slope / 100) * minViewportPx;
|
|
30
|
+
return `clamp(${minSizePx}px, ${intercept.toFixed(4)}px + ${slope.toFixed(4)}vw, ${maxSizePx}px)`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function responsiveSizes(config: Partial<Record<BreakpointName, string>>, fallback = "100vw"): string {
|
|
34
|
+
const ordered = Object.entries(BREAKPOINTS)
|
|
35
|
+
.sort((a, b) => b[1] - a[1])
|
|
36
|
+
.map(([name, width]) => {
|
|
37
|
+
const value = config[name as BreakpointName];
|
|
38
|
+
if (!value) return "";
|
|
39
|
+
return `(min-width: ${width}px) ${value}`;
|
|
40
|
+
})
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
ordered.push(fallback);
|
|
43
|
+
return ordered.join(", ");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function responsiveStyle(selector: string, baseCss: string, overrides: Partial<Record<BreakpointName, string>>): string {
|
|
47
|
+
const blocks = [`${selector}{${baseCss}}`];
|
|
48
|
+
for (const [name] of Object.entries(BREAKPOINTS)) {
|
|
49
|
+
const css = overrides[name as BreakpointName];
|
|
50
|
+
if (!css) continue;
|
|
51
|
+
blocks.push(mediaUp(name as BreakpointName, `${selector}{${css}}`));
|
|
52
|
+
}
|
|
53
|
+
return `<style>${blocks.join("")}</style>`;
|
|
54
|
+
}
|