@fiyuu/core 0.1.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/src/media.js ADDED
@@ -0,0 +1,87 @@
1
+ import { escapeHtml } from "./template.js";
2
+ export function optimizedImage(props) {
3
+ const loading = props.loading ?? "lazy";
4
+ const decoding = props.decoding ?? "async";
5
+ const fetchPriority = props.fetchPriority ?? "auto";
6
+ const imageAttributes = [
7
+ createAttribute("src", props.src),
8
+ createAttribute("alt", props.alt),
9
+ createAttribute("loading", loading),
10
+ createAttribute("decoding", decoding),
11
+ createAttribute("fetchpriority", fetchPriority),
12
+ createAttribute("sizes", props.sizes),
13
+ createAttribute("srcset", props.srcSet),
14
+ createAttribute("width", props.width),
15
+ createAttribute("height", props.height),
16
+ createAttribute("class", props.class),
17
+ createAttribute("style", props.style),
18
+ createAttribute("id", props.id),
19
+ ]
20
+ .filter(Boolean)
21
+ .join(" ");
22
+ const sourceTags = (props.sources ?? [])
23
+ .map((source) => {
24
+ const attributes = [
25
+ createAttribute("srcset", source.srcSet),
26
+ createAttribute("media", source.media),
27
+ createAttribute("type", source.type),
28
+ createAttribute("sizes", source.sizes),
29
+ ]
30
+ .filter(Boolean)
31
+ .join(" ");
32
+ return `<source ${attributes}>`;
33
+ })
34
+ .join("");
35
+ if (sourceTags.length > 0) {
36
+ return `<picture>${sourceTags}<img ${imageAttributes}></picture>`;
37
+ }
38
+ return `<img ${imageAttributes}>`;
39
+ }
40
+ export function optimizedVideo(props) {
41
+ const preload = props.preload ?? "metadata";
42
+ const controls = props.controls ?? true;
43
+ const playsInline = props.playsInline ?? true;
44
+ const sources = props.sources && props.sources.length > 0
45
+ ? props.sources
46
+ : props.src
47
+ ? [{ src: props.src }]
48
+ : [];
49
+ if (sources.length === 0) {
50
+ throw new Error("optimizedVideo requires either `src` or `sources`.");
51
+ }
52
+ const videoAttributes = [
53
+ createAttribute("poster", props.poster),
54
+ createAttribute("preload", preload),
55
+ createAttribute("width", props.width),
56
+ createAttribute("height", props.height),
57
+ createAttribute("class", props.class),
58
+ createAttribute("style", props.style),
59
+ createAttribute("id", props.id),
60
+ controls ? "controls" : "",
61
+ props.muted ? "muted" : "",
62
+ props.loop ? "loop" : "",
63
+ props.autoPlay ? "autoplay" : "",
64
+ playsInline ? "playsinline" : "",
65
+ ]
66
+ .filter(Boolean)
67
+ .join(" ");
68
+ const sourceTags = sources
69
+ .map((source) => {
70
+ const attributes = [
71
+ createAttribute("src", source.src),
72
+ createAttribute("type", source.type),
73
+ createAttribute("media", source.media),
74
+ ]
75
+ .filter(Boolean)
76
+ .join(" ");
77
+ return `<source ${attributes}>`;
78
+ })
79
+ .join("");
80
+ return `<video ${videoAttributes}>${sourceTags}</video>`;
81
+ }
82
+ function createAttribute(name, value) {
83
+ if (value === undefined || value === null || value === "") {
84
+ return "";
85
+ }
86
+ return `${name}="${escapeHtml(value)}"`;
87
+ }
@@ -0,0 +1,53 @@
1
+ import { type RawHtml } from "./template.js";
2
+ export type Signal<T> = {
3
+ get(): T;
4
+ set(next: T): void;
5
+ subscribe(fn: () => void): () => void;
6
+ };
7
+ type RenderBlock = string | (() => string);
8
+ export declare function createSignal<T>(initial: T): Signal<T>;
9
+ export declare function when(condition: unknown, content: string | (() => string)): string;
10
+ export type ElseBranch = {
11
+ kind: "Else";
12
+ render: () => string;
13
+ };
14
+ export declare function Else(content: RenderBlock): ElseBranch;
15
+ export declare function If(input: {
16
+ condition: unknown;
17
+ then: RenderBlock;
18
+ else?: RenderBlock | ElseBranch;
19
+ }): string;
20
+ export declare function each<T>(items: T[] | readonly T[], renderItem: (item: T, index: number) => string): string;
21
+ export declare function For<T>(input: {
22
+ each: readonly T[] | T[];
23
+ render: (item: T, index: number) => string;
24
+ empty?: RenderBlock;
25
+ }): string;
26
+ export declare function onEvent(handler: (event: Event) => void): string;
27
+ export declare function getEventHandler(id: string): ((event: Event) => void) | undefined;
28
+ export declare function clearEventRegistry(): void;
29
+ export declare function mount(root: HTMLElement, render: () => string, signals?: Signal<unknown>[]): () => void;
30
+ export declare function createFiyuuStore<T>(initialValue: T): {
31
+ get: () => T;
32
+ set: (next: T) => void;
33
+ subscribe: (fn: () => void) => () => void;
34
+ };
35
+ export declare function createFlatStore<T>(initialValue: T): {
36
+ get: () => T;
37
+ set: (next: T) => void;
38
+ subscribe: (fn: () => void) => () => void;
39
+ };
40
+ export declare function scopedStyles(name: string, css: string): {
41
+ scopeClass: string;
42
+ style: RawHtml;
43
+ };
44
+ export declare function island(options: {
45
+ id: string;
46
+ placeholder: string;
47
+ bootCode: string;
48
+ trigger?: "click" | "hover" | "visible";
49
+ }): string;
50
+ export declare function debugTag(name: string, content: string): string;
51
+ export declare function humanDebugOverlay(): RawHtml;
52
+ export type FiyuuStore<T> = ReturnType<typeof createFiyuuStore<T>>;
53
+ export {};
@@ -0,0 +1,160 @@
1
+ import { html, raw } from "./template.js";
2
+ export function createSignal(initial) {
3
+ let value = initial;
4
+ const listeners = new Set();
5
+ return {
6
+ get() {
7
+ return value;
8
+ },
9
+ set(next) {
10
+ if (value === next)
11
+ return;
12
+ value = next;
13
+ for (const fn of listeners)
14
+ fn();
15
+ },
16
+ subscribe(fn) {
17
+ listeners.add(fn);
18
+ return () => listeners.delete(fn);
19
+ },
20
+ };
21
+ }
22
+ export function when(condition, content) {
23
+ if (!condition)
24
+ return "";
25
+ return typeof content === "function" ? content() : content;
26
+ }
27
+ export function Else(content) {
28
+ return {
29
+ kind: "Else",
30
+ render: () => renderBlock(content),
31
+ };
32
+ }
33
+ export function If(input) {
34
+ if (input.condition) {
35
+ return renderBlock(input.then);
36
+ }
37
+ if (!input.else) {
38
+ return "";
39
+ }
40
+ if (isElseBranch(input.else)) {
41
+ return input.else.render();
42
+ }
43
+ return renderBlock(input.else);
44
+ }
45
+ function isElseBranch(value) {
46
+ return Boolean(value &&
47
+ typeof value === "object" &&
48
+ "kind" in value &&
49
+ "render" in value &&
50
+ value.kind === "Else" &&
51
+ typeof value.render === "function");
52
+ }
53
+ function renderBlock(block) {
54
+ if (block == null) {
55
+ return "";
56
+ }
57
+ if (typeof block === "function") {
58
+ return block();
59
+ }
60
+ return block;
61
+ }
62
+ export function each(items, renderItem) {
63
+ if (!items || items.length === 0)
64
+ return "";
65
+ return items.map(renderItem).join("");
66
+ }
67
+ export function For(input) {
68
+ if (!input.each || input.each.length === 0) {
69
+ return renderBlock(input.empty);
70
+ }
71
+ return input.each.map((item, index) => input.render(item, index)).join("");
72
+ }
73
+ let _eventId = 0;
74
+ const _eventRegistry = new Map();
75
+ export function onEvent(handler) {
76
+ const id = `fiyuu_evt_${_eventId++}`;
77
+ _eventRegistry.set(id, handler);
78
+ return id;
79
+ }
80
+ export function getEventHandler(id) {
81
+ return _eventRegistry.get(id);
82
+ }
83
+ export function clearEventRegistry() {
84
+ _eventRegistry.clear();
85
+ _eventId = 0;
86
+ }
87
+ export function mount(root, render, signals = []) {
88
+ function update() {
89
+ root.innerHTML = render();
90
+ attachDeclarativeEvents(root);
91
+ }
92
+ update();
93
+ const unsubscribers = signals.map((s) => s.subscribe(update));
94
+ return () => {
95
+ unsubscribers.forEach((u) => u());
96
+ root.innerHTML = "";
97
+ };
98
+ }
99
+ function attachDeclarativeEvents(root) {
100
+ const elements = root.querySelectorAll("[data-fiyuu-on]");
101
+ for (const el of elements) {
102
+ const parts = el.getAttribute("data-fiyuu-on").split(":");
103
+ if (parts.length !== 2)
104
+ continue;
105
+ const [eventName, handlerId] = parts;
106
+ const handler = _eventRegistry.get(handlerId);
107
+ if (handler) {
108
+ el.addEventListener(eventName, handler);
109
+ }
110
+ }
111
+ }
112
+ export function createFiyuuStore(initialValue) {
113
+ const signal = createSignal(initialValue);
114
+ return {
115
+ get: signal.get,
116
+ set: signal.set,
117
+ subscribe: signal.subscribe,
118
+ };
119
+ }
120
+ export function createFlatStore(initialValue) {
121
+ return createFiyuuStore(initialValue);
122
+ }
123
+ let scopedStyleCounter = 0;
124
+ function toScopeSlug(value) {
125
+ return value
126
+ .toLowerCase()
127
+ .replace(/[^a-z0-9]+/g, "-")
128
+ .replace(/^-+|-+$/g, "")
129
+ .slice(0, 24) || "scope";
130
+ }
131
+ export function scopedStyles(name, css) {
132
+ scopedStyleCounter += 1;
133
+ const scopeClass = `fx-${toScopeSlug(name)}-${scopedStyleCounter}`;
134
+ const scopedCss = css.replaceAll(":scope", `.${scopeClass}`);
135
+ return {
136
+ scopeClass,
137
+ style: raw(`<style>${scopedCss}</style>`),
138
+ };
139
+ }
140
+ function escapeSingleQuotes(value) {
141
+ return value.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
142
+ }
143
+ export function island(options) {
144
+ const trigger = options.trigger ?? "click";
145
+ const safeId = escapeSingleQuotes(options.id);
146
+ 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})();`;
147
+ return html `
148
+ <section data-fiyuu-island="${options.id}">
149
+ ${raw(options.placeholder)}
150
+ </section>
151
+ <script>${raw(script)}</script>
152
+ `;
153
+ }
154
+ export function debugTag(name, content) {
155
+ return html `<section data-fiyuu-debug-tag="${name}">${raw(content)}</section>`;
156
+ }
157
+ export function humanDebugOverlay() {
158
+ 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})();`;
159
+ return raw(`<script>${script}</script>`);
160
+ }
@@ -0,0 +1,18 @@
1
+ export type BreakpointPreset = "mobile-first" | "desktop-first" | "full-width" | "article" | "card" | "dashboard" | "narrow";
2
+ export type PreviewDevice = "phone" | "tablet" | "watch" | "desktop";
3
+ export type PreviewMode = "mobile" | "desktop" | "both";
4
+ export interface ResponsiveWrapperOptions {
5
+ content: string;
6
+ class?: string;
7
+ style?: string;
8
+ maxWidth?: number;
9
+ padding?: "none" | "sm" | "md" | "lg" | "xl" | string;
10
+ previewEnabled?: boolean;
11
+ previewLabel?: string;
12
+ id?: string;
13
+ preset?: BreakpointPreset;
14
+ /** Default preview mode */
15
+ defaultPreviewMode?: PreviewMode;
16
+ }
17
+ export declare function responsiveWrapper(options: ResponsiveWrapperOptions): string;
18
+ export declare function responsiveWrapperScript(): string;
@@ -0,0 +1,285 @@
1
+ import { escapeHtml } from "./template.js";
2
+ const PRESETS = {
3
+ "mobile-first": { maxWidth: 768, padding: "1rem", class: "rw-mobile-first" },
4
+ "desktop-first": { maxWidth: 1280, padding: "1rem", class: "rw-desktop-first" },
5
+ "full-width": { maxWidth: 0, padding: "0", class: "rw-full-width" },
6
+ "article": { maxWidth: 720, padding: "1.5rem", class: "rw-article" },
7
+ "card": { maxWidth: 480, padding: "1rem", class: "rw-card" },
8
+ "dashboard": { maxWidth: 1440, padding: "1rem", class: "rw-dashboard" },
9
+ "narrow": { maxWidth: 540, padding: "1rem", class: "rw-narrow" },
10
+ };
11
+ const DEVICES = {
12
+ phone: {
13
+ width: 390,
14
+ height: 844,
15
+ label: "iPhone",
16
+ scale: 0.72,
17
+ 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>',
18
+ },
19
+ tablet: {
20
+ width: 768,
21
+ height: 1024,
22
+ label: "iPad",
23
+ scale: 0.56,
24
+ 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>',
25
+ },
26
+ watch: {
27
+ width: 200,
28
+ height: 240,
29
+ label: "Watch",
30
+ scale: 0.85,
31
+ 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>',
32
+ },
33
+ desktop: {
34
+ width: 1280,
35
+ height: 800,
36
+ label: "Desktop",
37
+ scale: 0.52,
38
+ 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>',
39
+ },
40
+ };
41
+ export function responsiveWrapper(options) {
42
+ const { content, class: customClass = "", style: customStyle = "", maxWidth: customMaxWidth, padding: customPadding, previewEnabled = true, previewLabel = "Responsive", id, preset = "mobile-first", defaultPreviewMode = "both", } = options;
43
+ const presetConfig = PRESETS[preset];
44
+ const maxWidth = customMaxWidth ?? presetConfig.maxWidth;
45
+ const containerClass = customClass || presetConfig.class;
46
+ const widthStyle = maxWidth > 0 ? `max-width:${maxWidth}px;` : "";
47
+ const paddingStyle = getPaddingStyle(customPadding ?? presetConfig.padding);
48
+ const wrapperId = id || `rw-${generateId()}`;
49
+ const previewButton = previewEnabled ? buildPreviewButton(wrapperId) : "";
50
+ const previewPanel = previewEnabled ? buildPreviewPanel(wrapperId, content, preset, defaultPreviewMode) : "";
51
+ const previewStyles = previewEnabled ? buildPreviewStyles() : "";
52
+ const centerStyle = maxWidth > 0 ? "margin-left:auto;margin-right:auto;" : "";
53
+ return `<div id="${wrapperId}" class="fiyuu-responsive-wrapper ${containerClass}" style="position:relative;width:100%;${widthStyle}${centerStyle}${paddingStyle}${customStyle}">${previewButton}${content}${previewPanel}</div>${previewStyles}`;
54
+ }
55
+ function getPaddingStyle(padding) {
56
+ if (!padding || padding === "none")
57
+ return "";
58
+ const map = { sm: "0.75rem", md: "1rem", lg: "1.5rem", xl: "2rem" };
59
+ return `padding:${map[padding] || padding};`;
60
+ }
61
+ function generateId() {
62
+ return Math.random().toString(36).substring(2, 8);
63
+ }
64
+ function buildPreviewButton(wrapperId) {
65
+ return `<button type="button" class="fiyuu-rw-btn" onclick="window.__fiyuu_rw_open('${wrapperId}')" aria-label="Responsive Preview" title="Responsive Preview">
66
+ <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
+ <span>Responsive Preview</span>
68
+ </button>`;
69
+ }
70
+ function buildPreviewPanel(wrapperId, content, preset, defaultMode) {
71
+ const phoneDevice = DEVICES.phone;
72
+ const tabletDevice = DEVICES.tablet;
73
+ const watchDevice = DEVICES.watch;
74
+ const desktopDevice = DEVICES.desktop;
75
+ // Scale wrappers clip to actual visual size so height matches
76
+ const phoneW = Math.round(phoneDevice.width * phoneDevice.scale);
77
+ const phoneH = Math.round(phoneDevice.height * phoneDevice.scale);
78
+ const desktopW = Math.round(desktopDevice.width * desktopDevice.scale);
79
+ const desktopH = Math.round(desktopDevice.height * desktopDevice.scale);
80
+ return `<div id="${wrapperId}-panel" class="fiyuu-rw-panel" style="display:none;">
81
+ <!-- Left float sidebar -->
82
+ <div class="fiyuu-rw-sidebar">
83
+ <button type="button" class="fiyuu-rw-close" onclick="window.__fiyuu_rw_close('${wrapperId}')" aria-label="Close" title="Close">
84
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px"><path d="M18 6 6 18M6 6l12 12"/></svg>
85
+ </button>
86
+ <span class="fiyuu-rw-preset-badge">${escapeHtml(preset)}</span>
87
+ <div class="fiyuu-rw-sidebar-divider"></div>
88
+ <span class="fiyuu-rw-sidebar-label">VIEW</span>
89
+ <button type="button" class="fiyuu-rw-mode-btn" data-mode="mobile" onclick="window.__fiyuu_rw_setmode('${wrapperId}','mobile')" title="Mobile">
90
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" style="width:16px;height:16px"><rect x="5" y="2" width="14" height="20" rx="2"/><path d="M12 18h.01"/></svg>
91
+ <span>Mobile</span>
92
+ </button>
93
+ <button type="button" class="fiyuu-rw-mode-btn" data-mode="desktop" onclick="window.__fiyuu_rw_setmode('${wrapperId}','desktop')" title="Desktop">
94
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" style="width:16px;height:16px"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
95
+ <span>Desktop</span>
96
+ </button>
97
+ <button type="button" class="fiyuu-rw-mode-btn active" data-mode="both" onclick="window.__fiyuu_rw_setmode('${wrapperId}','both')" title="Both">
98
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" style="width:16px;height:16px"><rect x="1" y="7" width="9" height="13" rx="1"/><rect x="14" y="4" width="9" height="13" rx="1"/></svg>
99
+ <span>Both</span>
100
+ </button>
101
+ <div class="fiyuu-rw-sidebar-divider"></div>
102
+ <span class="fiyuu-rw-sidebar-label">DEVICE</span>
103
+ <button type="button" class="fiyuu-rw-dev-btn active" data-device="phone" onclick="window.__fiyuu_rw_setdevice('${wrapperId}','phone')" title="iPhone (375px)">${phoneDevice.icon}</button>
104
+ <button type="button" class="fiyuu-rw-dev-btn" data-device="tablet" onclick="window.__fiyuu_rw_setdevice('${wrapperId}','tablet')" title="iPad (768px)">${tabletDevice.icon}</button>
105
+ <button type="button" class="fiyuu-rw-dev-btn" data-device="watch" onclick="window.__fiyuu_rw_setdevice('${wrapperId}','watch')" title="Watch (200px)">${watchDevice.icon}</button>
106
+ <button type="button" class="fiyuu-rw-dev-btn" data-device="desktop" onclick="window.__fiyuu_rw_setdevice('${wrapperId}','desktop')" title="Desktop (1280px)">${desktopDevice.icon}</button>
107
+ </div>
108
+
109
+ <!-- Viewport area -->
110
+ <div class="fiyuu-rw-viewport">
111
+ <!-- Mobile View -->
112
+ <div id="${wrapperId}-mobile-view" class="fiyuu-rw-device-col" style="display:none;">
113
+ <div class="fiyuu-rw-device-frame fiyuu-rw-phone-frame">
114
+ <div class="fiyuu-rw-phone-notch"></div>
115
+ <div class="fiyuu-rw-screen-wrapper" id="${wrapperId}-mobile-screen-wrapper" style="width:${phoneW}px;height:${phoneH}px;">
116
+ <div class="fiyuu-rw-device-screen" style="width:${phoneDevice.width}px;height:${phoneDevice.height}px;transform:scale(${phoneDevice.scale});transform-origin:top left;">
117
+ ${content}
118
+ </div>
119
+ </div>
120
+ <div class="fiyuu-rw-phone-home"></div>
121
+ </div>
122
+ <div class="fiyuu-rw-device-info">${phoneDevice.icon} ${phoneDevice.label} — ${phoneDevice.width}px</div>
123
+ </div>
124
+
125
+ <!-- Desktop View -->
126
+ <div id="${wrapperId}-desktop-view" class="fiyuu-rw-device-col" style="display:none;">
127
+ <div class="fiyuu-rw-device-frame fiyuu-rw-desktop-frame">
128
+ <div class="fiyuu-rw-desktop-toolbar">
129
+ <span class="fiyuu-rw-dot" style="background:#FF5F57;"></span>
130
+ <span class="fiyuu-rw-dot" style="background:#FEBC2E;"></span>
131
+ <span class="fiyuu-rw-dot" style="background:#28C840;"></span>
132
+ <span class="fiyuu-rw-url-bar">${"localhost".padEnd(42, " ")}</span>
133
+ </div>
134
+ <div class="fiyuu-rw-screen-wrapper" style="width:${desktopW}px;height:${desktopH}px;">
135
+ <div class="fiyuu-rw-device-screen" style="width:${desktopDevice.width}px;height:${desktopDevice.height}px;transform:scale(${desktopDevice.scale});transform-origin:top left;">
136
+ ${content}
137
+ </div>
138
+ </div>
139
+ </div>
140
+ <div class="fiyuu-rw-device-info">${desktopDevice.icon} ${desktopDevice.label} — ${desktopDevice.width}px</div>
141
+ </div>
142
+ </div>
143
+ </div>`;
144
+ }
145
+ function buildPreviewStyles() {
146
+ return `<style id="fiyuu-rw-styles">
147
+ .fiyuu-responsive-wrapper{position:relative;width:100%;box-sizing:border-box;}
148
+
149
+ /* Preview Button (FAB) */
150
+ .fiyuu-rw-btn{position:fixed;bottom:24px;right:24px;z-index:999;display:inline-flex;align-items:center;gap:6px;padding:10px 16px;border-radius:12px;border:none;background:var(--accent,#ca6242);color:white;font-size:12px;font-weight:600;font-family:var(--font-sans,system-ui,sans-serif);cursor:pointer;opacity:0.9;transition:all .2s ease;box-shadow:0 4px 16px rgba(0,0,0,.15);letter-spacing:.01em;}
151
+ .fiyuu-rw-btn:hover{opacity:1;transform:scale(1.05);box-shadow:0 6px 24px rgba(0,0,0,.2);}
152
+ .fiyuu-rw-btn svg{flex-shrink:0;}
153
+
154
+ /* Panel — row: sidebar + viewport */
155
+ .fiyuu-rw-panel{position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,.65);backdrop-filter:blur(6px);display:flex;flex-direction:row;animation:fiyuu-rw-fade .18s ease;}
156
+ @keyframes fiyuu-rw-fade{from{opacity:0}to{opacity:1}}
157
+
158
+ /* Left Sidebar */
159
+ .fiyuu-rw-sidebar{display:flex;flex-direction:column;align-items:center;gap:6px;padding:14px 8px;background:var(--bg-primary,#fff);border-right:1px solid var(--border,#e5e7eb);width:76px;flex-shrink:0;overflow-y:auto;}
160
+ .fiyuu-rw-close{background:none;border:none;color:var(--text-muted,#9ca3af);cursor:pointer;padding:0;border-radius:50%;display:flex;align-items:center;justify-content:center;width:36px;height:36px;transition:all .15s;flex-shrink:0;}
161
+ .fiyuu-rw-close:hover{background:var(--bg-secondary,#f3f4f6);color:var(--text-primary,#111827);}
162
+ .fiyuu-rw-preset-badge{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--accent,#ca6242);background:rgba(202,98,66,.1);padding:3px 6px;border-radius:6px;text-align:center;word-break:break-all;line-height:1.3;}
163
+ .fiyuu-rw-sidebar-divider{width:80%;height:1px;background:var(--border,#e5e7eb);margin:4px 0;flex-shrink:0;}
164
+ .fiyuu-rw-sidebar-label{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted,#9ca3af);font-family:var(--font-sans,system-ui);}
165
+
166
+ /* Mode buttons — vertical pill blob */
167
+ .fiyuu-rw-mode-btn{display:flex;flex-direction:column;align-items:center;gap:4px;padding:9px 6px;border-radius:999px;border:none;background:var(--bg-secondary,#f3f4f6);color:var(--text-secondary,#4b5563);font-size:9px;font-weight:700;cursor:pointer;font-family:var(--font-sans,system-ui);transition:all .15s;width:56px;text-align:center;flex-shrink:0;}
168
+ .fiyuu-rw-mode-btn span{line-height:1.2;}
169
+ .fiyuu-rw-mode-btn:hover{background:rgba(202,98,66,.12);color:var(--accent,#ca6242);}
170
+ .fiyuu-rw-mode-btn.active{background:var(--accent,#ca6242);color:white;}
171
+
172
+ /* Device buttons — circle blob icon-only */
173
+ .fiyuu-rw-dev-btn{display:flex;align-items:center;justify-content:center;padding:0;border-radius:999px;border:none;background:var(--bg-secondary,#f3f4f6);color:var(--text-muted,#9ca3af);cursor:pointer;transition:all .15s;width:40px;height:40px;flex-shrink:0;}
174
+ .fiyuu-rw-dev-btn:hover{background:rgba(202,98,66,.12);color:var(--accent,#ca6242);}
175
+ .fiyuu-rw-dev-btn.active{background:var(--accent,#ca6242);color:white;}
176
+
177
+ /* Viewport */
178
+ .fiyuu-rw-viewport{flex:1;overflow:auto;display:flex;align-items:flex-start;justify-content:center;gap:32px;padding:32px 24px;}
179
+ .fiyuu-rw-device-col{display:flex;flex-direction:column;align-items:center;gap:12px;flex-shrink:0;}
180
+ .fiyuu-rw-device-info{font-size:11px;font-weight:600;color:var(--text-muted,#9ca3af);display:flex;align-items:center;gap:5px;font-family:var(--font-sans,system-ui);}
181
+
182
+ /* Device Frame */
183
+ .fiyuu-rw-device-frame{background:#111;border-radius:12px;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.4);}
184
+
185
+ /* Screen wrapper clips to actual scaled dimensions */
186
+ .fiyuu-rw-screen-wrapper{position:relative;overflow:hidden;flex-shrink:0;}
187
+ .fiyuu-rw-device-screen{background:var(--bg-primary,#fff);overflow:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;position:absolute;top:0;left:0;}
188
+
189
+ /* Phone Frame */
190
+ .fiyuu-rw-phone-frame{border-radius:36px;padding:28px 8px 20px;position:relative;}
191
+ .fiyuu-rw-phone-notch{position:absolute;top:8px;left:50%;transform:translateX(-50%);width:72px;height:20px;background:#111;border-radius:0 0 14px 14px;z-index:10;}
192
+ .fiyuu-rw-phone-home{position:absolute;bottom:8px;left:50%;transform:translateX(-50%);width:72px;height:4px;background:rgba(255,255,255,.25);border-radius:2px;}
193
+
194
+ /* Desktop Frame */
195
+ .fiyuu-rw-desktop-frame{border-radius:10px;border:1px solid #2a2a2e;}
196
+ .fiyuu-rw-desktop-toolbar{display:flex;align-items:center;gap:6px;padding:8px 12px;background:#1a1a1d;border-bottom:1px solid #2a2a2e;}
197
+ .fiyuu-rw-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;}
198
+ .fiyuu-rw-url-bar{flex:1;margin-left:8px;background:#0f0f10;border:1px solid #2a2a2e;border-radius:6px;padding:3px 10px;font-size:10px;color:#6b7280;font-family:monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
199
+
200
+ @media(max-width:900px){.fiyuu-rw-viewport{flex-direction:column;align-items:center;padding:20px 12px;gap:24px;}}
201
+ @media(max-width:600px){.fiyuu-rw-sidebar{width:56px;padding:10px 6px;gap:4px;}.fiyuu-rw-mode-btn{width:44px;font-size:8px;padding:7px 4px;}.fiyuu-rw-dev-btn{width:36px;height:36px;}.fiyuu-rw-viewport{padding:16px 8px;gap:16px;}}
202
+ @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
+ </style>`;
204
+ }
205
+ export function responsiveWrapperScript() {
206
+ return `<script>
207
+ (function(){
208
+ if (window.__fiyuu_rw_open) return;
209
+ window.__fiyuu_rw_open = function(id) {
210
+ var el = document.getElementById(id + "-panel");
211
+ if (!el) return;
212
+ el.style.display = "flex";
213
+ document.body.style.overflow = "hidden";
214
+ var mode = window.innerWidth < 600 ? "mobile" : "both";
215
+ window.__fiyuu_rw_setmode(id, mode);
216
+ window.__fiyuu_rw_setdevice(id, "phone");
217
+ };
218
+ window.__fiyuu_rw_close = function(id) {
219
+ var el = document.getElementById(id + "-panel");
220
+ if (!el) return;
221
+ el.style.display = "none";
222
+ document.body.style.overflow = "";
223
+ };
224
+ window.__fiyuu_rw_setmode = function(id, mode) {
225
+ var mobile = document.getElementById(id + "-mobile-view");
226
+ var desktop = document.getElementById(id + "-desktop-view");
227
+ if (!mobile || !desktop) return;
228
+ mobile.style.display = (mode === "mobile" || mode === "both") ? "flex" : "none";
229
+ desktop.style.display = (mode === "desktop" || mode === "both") ? "flex" : "none";
230
+ var panel = document.getElementById(id + "-panel");
231
+ if (!panel) return;
232
+ panel.querySelectorAll(".fiyuu-rw-mode-btn").forEach(function(btn) {
233
+ btn.classList.toggle("active", btn.getAttribute("data-mode") === mode);
234
+ });
235
+ };
236
+ window.__fiyuu_rw_setdevice = function(id, device) {
237
+ var panel = document.getElementById(id + "-panel");
238
+ if (!panel) return;
239
+ panel.querySelectorAll(".fiyuu-rw-dev-btn").forEach(function(btn) {
240
+ btn.classList.toggle("active", btn.getAttribute("data-device") === device);
241
+ });
242
+ var mobileView = document.getElementById(id + "-mobile-view");
243
+ if (mobileView) {
244
+ var screen = mobileView.querySelector(".fiyuu-rw-device-screen");
245
+ var wrapper = mobileView.querySelector(".fiyuu-rw-screen-wrapper");
246
+ var sizes = { phone: { w: 390, h: 844, s: 0.72 }, tablet: { w: 768, h: 1024, s: 0.56 }, watch: { w: 200, h: 240, s: 0.85 }, desktop: { w: 1280, h: 800, s: 0.52 } };
247
+ var size = sizes[device] || sizes.phone;
248
+ if (screen) {
249
+ screen.style.width = size.w + "px";
250
+ screen.style.height = size.h + "px";
251
+ screen.style.transform = "scale(" + size.s + ")";
252
+ }
253
+ if (wrapper) {
254
+ wrapper.style.width = Math.round(size.w * size.s) + "px";
255
+ wrapper.style.height = Math.round(size.h * size.s) + "px";
256
+ }
257
+ var frame = mobileView.querySelector(".fiyuu-rw-device-frame");
258
+ if (frame) {
259
+ if (device === "tablet" || device === "desktop") {
260
+ frame.className = "fiyuu-rw-device-frame fiyuu-rw-desktop-frame";
261
+ var notch = frame.querySelector(".fiyuu-rw-phone-notch");
262
+ if (notch) notch.style.display = "none";
263
+ var home = frame.querySelector(".fiyuu-rw-phone-home");
264
+ if (home) home.style.display = "none";
265
+ if (!frame.querySelector(".fiyuu-rw-desktop-toolbar")) {
266
+ var tb = document.createElement("div");
267
+ tb.className = "fiyuu-rw-desktop-toolbar";
268
+ tb.innerHTML = '<span class="fiyuu-rw-dot" style="background:#FF5F57;"></span><span class="fiyuu-rw-dot" style="background:#FEBC2E;"></span><span class="fiyuu-rw-dot" style="background:#28C840;"></span>';
269
+ frame.insertBefore(tb, frame.firstChild);
270
+ }
271
+ } else {
272
+ frame.className = "fiyuu-rw-device-frame fiyuu-rw-phone-frame";
273
+ var notch2 = frame.querySelector(".fiyuu-rw-phone-notch");
274
+ if (notch2) notch2.style.display = "";
275
+ var home2 = frame.querySelector(".fiyuu-rw-phone-home");
276
+ if (home2) home2.style.display = "";
277
+ var dtb = frame.querySelector(".fiyuu-rw-desktop-toolbar");
278
+ if (dtb) dtb.remove();
279
+ }
280
+ }
281
+ }
282
+ };
283
+ })();
284
+ </script>`;
285
+ }
@@ -0,0 +1,15 @@
1
+ export declare const BREAKPOINTS: {
2
+ readonly xs: 480;
3
+ readonly sm: 640;
4
+ readonly md: 768;
5
+ readonly lg: 1024;
6
+ readonly xl: 1280;
7
+ readonly "2xl": 1536;
8
+ };
9
+ export type BreakpointName = keyof typeof BREAKPOINTS;
10
+ export declare function mediaUp(name: BreakpointName, css: string): string;
11
+ export declare function mediaDown(name: BreakpointName, css: string): string;
12
+ export declare function mediaBetween(min: BreakpointName, max: BreakpointName, css: string): string;
13
+ export declare function fluid(minSizePx: number, maxSizePx: number, minViewportPx?: number, maxViewportPx?: number): string;
14
+ export declare function responsiveSizes(config: Partial<Record<BreakpointName, string>>, fallback?: string): string;
15
+ export declare function responsiveStyle(selector: string, baseCss: string, overrides: Partial<Record<BreakpointName, string>>): string;