simplify components
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
This commit is contained in:
34
dist/sigpro-ui.esm.js
vendored
34
dist/sigpro-ui.esm.js
vendored
@@ -727,17 +727,12 @@ __export(exports_Button, {
|
||||
Button: () => Button
|
||||
});
|
||||
var Button = (props, children) => {
|
||||
const { class: className, loading, icon, ...rest } = props;
|
||||
const iconEl = getIcon(icon);
|
||||
const { class: className, ...rest } = props;
|
||||
return S("button", {
|
||||
...rest,
|
||||
class: ui("btn", className),
|
||||
disabled: () => val2(loading) || val2(props.disabled)
|
||||
}, () => [
|
||||
val2(loading) && S("span", { class: "loading loading-spinner" }),
|
||||
iconEl,
|
||||
children
|
||||
].filter(Boolean));
|
||||
disabled: () => val2(props.disabled)
|
||||
}, () => children);
|
||||
};
|
||||
|
||||
// src/components/Checkbox.js
|
||||
@@ -1282,6 +1277,17 @@ var Fileinput = (props) => {
|
||||
]);
|
||||
};
|
||||
|
||||
// src/components/Icon.js
|
||||
var exports_Icon = {};
|
||||
__export(exports_Icon, {
|
||||
Icon: () => Icon
|
||||
});
|
||||
var Icon = (iconClass) => {
|
||||
if (!iconClass)
|
||||
return null;
|
||||
return S("span", { class: iconClass });
|
||||
};
|
||||
|
||||
// src/components/Indicator.js
|
||||
var exports_Indicator = {};
|
||||
__export(exports_Indicator, {
|
||||
@@ -1526,6 +1532,16 @@ var Select = (props) => {
|
||||
]);
|
||||
};
|
||||
|
||||
// src/components/Spinner.js
|
||||
var exports_Spinner = {};
|
||||
__export(exports_Spinner, {
|
||||
Spinner: () => Spinner
|
||||
});
|
||||
var Spinner = (props) => {
|
||||
const { value, ...rest } = props;
|
||||
return If(() => val2(value), () => S("span", { class: "loading loading-spinner", ...rest }));
|
||||
};
|
||||
|
||||
// src/components/Stack.js
|
||||
var exports_Stack = {};
|
||||
__export(exports_Stack, {
|
||||
@@ -1827,6 +1843,7 @@ var Components = {
|
||||
...exports_Fab,
|
||||
...exports_Fieldset,
|
||||
...exports_Fileinput,
|
||||
...exports_Icon,
|
||||
...exports_Indicator,
|
||||
...exports_Input,
|
||||
...exports_Label,
|
||||
@@ -1838,6 +1855,7 @@ var Components = {
|
||||
...exports_Range,
|
||||
...exports_Rating,
|
||||
...exports_Select,
|
||||
...exports_Spinner,
|
||||
...exports_Stack,
|
||||
...exports_Stat,
|
||||
...exports_Swap,
|
||||
|
||||
6
dist/sigpro-ui.esm.min.js
vendored
6
dist/sigpro-ui.esm.min.js
vendored
File diff suppressed because one or more lines are too long
34
dist/sigpro-ui.js
vendored
34
dist/sigpro-ui.js
vendored
@@ -758,17 +758,12 @@
|
||||
Button: () => Button
|
||||
});
|
||||
var Button = (props, children) => {
|
||||
const { class: className, loading, icon, ...rest } = props;
|
||||
const iconEl = getIcon(icon);
|
||||
const { class: className, ...rest } = props;
|
||||
return S("button", {
|
||||
...rest,
|
||||
class: ui("btn", className),
|
||||
disabled: () => val2(loading) || val2(props.disabled)
|
||||
}, () => [
|
||||
val2(loading) && S("span", { class: "loading loading-spinner" }),
|
||||
iconEl,
|
||||
children
|
||||
].filter(Boolean));
|
||||
disabled: () => val2(props.disabled)
|
||||
}, () => children);
|
||||
};
|
||||
|
||||
// src/components/Checkbox.js
|
||||
@@ -1313,6 +1308,17 @@
|
||||
]);
|
||||
};
|
||||
|
||||
// src/components/Icon.js
|
||||
var exports_Icon = {};
|
||||
__export(exports_Icon, {
|
||||
Icon: () => Icon
|
||||
});
|
||||
var Icon = (iconClass) => {
|
||||
if (!iconClass)
|
||||
return null;
|
||||
return S("span", { class: iconClass });
|
||||
};
|
||||
|
||||
// src/components/Indicator.js
|
||||
var exports_Indicator = {};
|
||||
__export(exports_Indicator, {
|
||||
@@ -1557,6 +1563,16 @@
|
||||
]);
|
||||
};
|
||||
|
||||
// src/components/Spinner.js
|
||||
var exports_Spinner = {};
|
||||
__export(exports_Spinner, {
|
||||
Spinner: () => Spinner
|
||||
});
|
||||
var Spinner = (props) => {
|
||||
const { value, ...rest } = props;
|
||||
return If(() => val2(value), () => S("span", { class: "loading loading-spinner", ...rest }));
|
||||
};
|
||||
|
||||
// src/components/Stack.js
|
||||
var exports_Stack = {};
|
||||
__export(exports_Stack, {
|
||||
@@ -1858,6 +1874,7 @@
|
||||
...exports_Fab,
|
||||
...exports_Fieldset,
|
||||
...exports_Fileinput,
|
||||
...exports_Icon,
|
||||
...exports_Indicator,
|
||||
...exports_Input,
|
||||
...exports_Label,
|
||||
@@ -1869,6 +1886,7 @@
|
||||
...exports_Range,
|
||||
...exports_Rating,
|
||||
...exports_Select,
|
||||
...exports_Spinner,
|
||||
...exports_Stack,
|
||||
...exports_Stat,
|
||||
...exports_Swap,
|
||||
|
||||
6
dist/sigpro-ui.min.js
vendored
6
dist/sigpro-ui.min.js
vendored
File diff suppressed because one or more lines are too long
25
dist/sigpro.css
vendored
25
dist/sigpro.css
vendored
@@ -4163,6 +4163,19 @@
|
||||
mask-size: 100% 100%;
|
||||
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 9h16M4 15h16M10 3L8 21m8-18l-2 18'/%3E%3C/svg%3E");
|
||||
}
|
||||
.icon-\[lucide--heart\] {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
background-color: currentColor;
|
||||
-webkit-mask-image: var(--svg);
|
||||
mask-image: var(--svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 9.5a5.5 5.5 0 0 1 9.591-3.676a.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5'/%3E%3C/svg%3E");
|
||||
}
|
||||
.icon-\[lucide--info\] {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
@@ -6926,18 +6939,6 @@
|
||||
color: oklch(28% 0.01 260);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.collapse .collapse-content {
|
||||
transform: scaleY(0);
|
||||
transform-origin: top;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
.collapse:has(input:checked) .collapse-content {
|
||||
transform: scaleY(1);
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
.tab-content-inner {
|
||||
animation: tabFadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform-origin: top;
|
||||
|
||||
2
dist/sigpro.min.css
vendored
2
dist/sigpro.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -17,13 +17,13 @@
|
||||
|
||||
## Classes (daisyUI)
|
||||
|
||||
| Category | Keywords | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| Color | `btn-primary`, `btn-secondary`, `btn-accent`, `btn-ghost`, `btn-info`, `btn-success`, `btn-warning`, `btn-error` | Visual color variants |
|
||||
| Size | `btn-xs`, `btn-sm`, `btn-md`, `btn-lg`, `btn-xl` | Button scale |
|
||||
| Style | `btn-outline`, `btn-soft`, `btn-dash`, `btn-link` | Visual style variants |
|
||||
| Shape | `btn-circle`, `btn-square`, `btn-wide`, `btn-block` | Button shape |
|
||||
| State | `btn-active`, `btn-disabled` | Forced visual states |
|
||||
| Category | Keywords | Description |
|
||||
| :------- | :--------------------------------------------------------------------------------------------------------------- | :-------------------- |
|
||||
| Color | `btn-primary`, `btn-secondary`, `btn-accent`, `btn-ghost`, `btn-info`, `btn-success`, `btn-warning`, `btn-error` | Visual color variants |
|
||||
| Size | `btn-xs`, `btn-sm`, `btn-md`, `btn-lg`, `btn-xl` | Button scale |
|
||||
| Style | `btn-outline`, `btn-soft`, `btn-dash`, `btn-link` | Visual style variants |
|
||||
| Shape | `btn-circle`, `btn-square`, `btn-wide`, `btn-block` | Button shape |
|
||||
| State | `btn-active`, `btn-disabled` | Forced visual states |
|
||||
|
||||
> SigProUI supports styling via daisyUI independently or combined with the `ui` prop.
|
||||
> For further details, check the [daisyUI Button Documentation](https://daisyui.com/components/button) – Full reference for CSS classes.
|
||||
@@ -31,7 +31,7 @@
|
||||
### Example
|
||||
|
||||
```javascript
|
||||
Button({ class: "btn-primary btn-lg btn-circle gap-4"}, "Click Me");
|
||||
Button({ class: "btn-primary btn-lg btn-circle gap-4" }, "Click Me");
|
||||
// Applies: primary color, large size, circular shape
|
||||
// class is any css class from pure css or favorite framework
|
||||
```
|
||||
@@ -62,14 +62,14 @@ const LoadingDemo = () => {
|
||||
return Button(
|
||||
{
|
||||
class: "btn-success",
|
||||
loading: isSaving,
|
||||
disabled: isSaving,
|
||||
onclick: async () => {
|
||||
isSaving(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
isSaving(false);
|
||||
},
|
||||
},
|
||||
"Save Changes",
|
||||
[Spinner({ value: isSaving }), "Save Changes"],
|
||||
);
|
||||
};
|
||||
Mount(LoadingDemo, "#demo-loading");
|
||||
@@ -81,13 +81,9 @@ Mount(LoadingDemo, "#demo-loading");
|
||||
|
||||
```javascript
|
||||
const IconDemo = () => {
|
||||
return Button(
|
||||
{
|
||||
class: "btn-primary",
|
||||
icon: "⭐",
|
||||
},
|
||||
"Favorite",
|
||||
);
|
||||
return Div({ class: "flex flex-wrap gap-2 justify-center" }, [
|
||||
Button({ class: "btn-primary" }, [Icon("icon-[lucide--x]"), "Favorite"]),
|
||||
]);
|
||||
};
|
||||
Mount(IconDemo, "#demo-icon");
|
||||
```
|
||||
@@ -112,7 +108,10 @@ Mount(BadgeDemo, "#demo-badge");
|
||||
|
||||
```javascript
|
||||
const TooltipDemo = () => {
|
||||
return Tooltip({ tip: "Delete item" }, Button({ class: "btn-ghost" }, "Delete"));
|
||||
return Tooltip(
|
||||
{ tip: "Delete item" },
|
||||
Button({ class: "btn-ghost" }, "Delete"),
|
||||
);
|
||||
};
|
||||
Mount(TooltipDemo, "#demo-tooltip");
|
||||
```
|
||||
@@ -131,11 +130,10 @@ const CombinedDemo = () => {
|
||||
Button(
|
||||
{
|
||||
class: "btn-primary btn-lg",
|
||||
icon: "👍",
|
||||
onclick: () => count(count() + 1),
|
||||
},
|
||||
"Like",
|
||||
)
|
||||
["👍", "Like", Icon("icon-[lucide--heart]")],
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
@@ -154,6 +152,7 @@ const VariantsDemo = () => {
|
||||
Button({ class: "btn-accent" }, "Accent"),
|
||||
Button({ class: "btn-ghost" }, "Ghost"),
|
||||
Button({ class: "btn-outline" }, "Outline"),
|
||||
Button({ class: "btn-disabled" }, "Disabled"),
|
||||
]);
|
||||
};
|
||||
Mount(VariantsDemo, "#demo-variants");
|
||||
|
||||
6
docs/sigpro-ui.min.js
vendored
6
docs/sigpro-ui.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
index.js
4
index.js
@@ -11,6 +11,7 @@ import * as DropdownModule from './src/components/Dropdown.js';
|
||||
import * as FabModule from './src/components/Fab.js';
|
||||
import * as FieldsetModule from './src/components/Fieldset.js';
|
||||
import * as FileinputModule from './src/components/Fileinput.js';
|
||||
import * as IconModule from './src/components/Icon.js';
|
||||
import * as IndicatorModule from './src/components/Indicator.js';
|
||||
import * as InputModule from './src/components/Input.js';
|
||||
import * as LabelModule from './src/components/Label.js';
|
||||
@@ -22,6 +23,7 @@ import * as RadioModule from './src/components/Radio.js';
|
||||
import * as RangeModule from './src/components/Range.js';
|
||||
import * as RatingModule from './src/components/Rating.js';
|
||||
import * as SelectModule from './src/components/Select.js';
|
||||
import * as SpinnerModule from './src/components/Spinner.js';
|
||||
import * as StackModule from './src/components/Stack.js';
|
||||
import * as StatModule from './src/components/Stat.js';
|
||||
import * as SwapModule from './src/components/Swap.js';
|
||||
@@ -46,6 +48,7 @@ export const Components = {
|
||||
...FabModule,
|
||||
...FieldsetModule,
|
||||
...FileinputModule,
|
||||
...IconModule,
|
||||
...IndicatorModule,
|
||||
...InputModule,
|
||||
...LabelModule,
|
||||
@@ -57,6 +60,7 @@ export const Components = {
|
||||
...RangeModule,
|
||||
...RatingModule,
|
||||
...SelectModule,
|
||||
...SpinnerModule,
|
||||
...StackModule,
|
||||
...StatModule,
|
||||
...SwapModule,
|
||||
|
||||
@@ -14,17 +14,11 @@ import { ui, val, getIcon } from "../utils.js";
|
||||
* - btn-active, btn-disabled
|
||||
*/
|
||||
export const Button = (props, children) => {
|
||||
const { class: className, loading, icon, ...rest } = props;
|
||||
|
||||
const iconEl = getIcon(icon);
|
||||
const { class: className, ...rest } = props;
|
||||
|
||||
return Tag("button", {
|
||||
...rest,
|
||||
class: ui('btn', className),
|
||||
disabled: () => val(loading) || val(props.disabled),
|
||||
}, () => [
|
||||
val(loading) && Tag("span", { class: "loading loading-spinner" }),
|
||||
iconEl,
|
||||
children
|
||||
].filter(Boolean));
|
||||
disabled: () => val(props.disabled),
|
||||
}, () => children);
|
||||
};
|
||||
@@ -1,45 +1,52 @@
|
||||
import { $$ } from "sigpro";
|
||||
export const Fetch = ({ url, options = {}, fallback = "Cargando..." }, { children }) => {
|
||||
const state = $$({ data: null, loading: true, error: null });
|
||||
let controller = null;
|
||||
import { $$, Tag, isFunc } from "sigpro";
|
||||
|
||||
const run = async () => {
|
||||
if (controller) controller.abort();
|
||||
controller = new AbortController();
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
const _cache = new Map();
|
||||
|
||||
try {
|
||||
const targetUrl = isFunc(url) ? url() : url;
|
||||
const fetchOpts = {
|
||||
...(isFunc(options) ? options() : options),
|
||||
signal: controller.signal
|
||||
};
|
||||
const getStore = (key) => {
|
||||
if (!_cache.has(key)) {
|
||||
_cache.set(key, $$({ data: null, loading: false, error: null }));
|
||||
}
|
||||
return _cache.get(key);
|
||||
};
|
||||
|
||||
const res = await fetch(targetUrl, fetchOpts);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const contentType = res.headers.get("content-type");
|
||||
state.data = contentType?.includes("application/json")
|
||||
? await res.json()
|
||||
: await res.text();
|
||||
} catch (err) {
|
||||
if (err.name !== 'AbortError') state.error = err.message;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
export const Request = async (key, url, opts = {}) => {
|
||||
const store = getStore(key);
|
||||
const { body, ...rest } = opts;
|
||||
|
||||
store.loading = true;
|
||||
store.error = null;
|
||||
|
||||
if (isFunc(url)) Watch(url, run);
|
||||
else run();
|
||||
try {
|
||||
const config = {
|
||||
method: rest.method || 'GET',
|
||||
headers: { 'Content-Type': 'application/json', ...rest.headers },
|
||||
...rest
|
||||
};
|
||||
|
||||
onUnmount(() => controller?.abort());
|
||||
if (body) config.body = typeof body === 'object' ? JSON.stringify(body) : body;
|
||||
|
||||
return Tag("div", { class: "sigpro-fetch" }, [
|
||||
const res = await fetch(url, config);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const result = await res.json();
|
||||
store.data = result;
|
||||
return result;
|
||||
} catch (err) {
|
||||
store.error = err.message;
|
||||
throw err;
|
||||
} finally {
|
||||
store.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
export const Response = ({ name, loading, error }, { children }) => {
|
||||
const store = getStore(name);
|
||||
|
||||
return Tag("div", { style: "display:contents" }, [
|
||||
() => {
|
||||
if (state.loading) return fallback;
|
||||
if (state.error) return Tag("span", { style: "color:red" }, state.error);
|
||||
if (state.data) return isFunc(children[0]) ? children[0](state.data) : children;
|
||||
if (store.loading) return loading || "Cargando...";
|
||||
if (store.error) return isFunc(error) ? error(store.error) : Tag("p", {}, store.error);
|
||||
if (store.data) return isFunc(children[0]) ? children[0](store.data) : children;
|
||||
return null;
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { $$, Tag, isFunc } from "sigpro";
|
||||
|
||||
const _cache = new Map();
|
||||
|
||||
const getStore = (key) => {
|
||||
if (!_cache.has(key)) {
|
||||
_cache.set(key, $$({ data: null, loading: false, error: null }));
|
||||
}
|
||||
return _cache.get(key);
|
||||
};
|
||||
|
||||
export const Request = async (key, url, opts = {}) => {
|
||||
const store = getStore(key);
|
||||
const { body, ...rest } = opts;
|
||||
|
||||
store.loading = true;
|
||||
store.error = null;
|
||||
|
||||
try {
|
||||
const config = {
|
||||
method: rest.method || 'GET',
|
||||
headers: { 'Content-Type': 'application/json', ...rest.headers },
|
||||
...rest
|
||||
};
|
||||
|
||||
if (body) config.body = typeof body === 'object' ? JSON.stringify(body) : body;
|
||||
|
||||
const res = await fetch(url, config);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const result = await res.json();
|
||||
store.data = result;
|
||||
return result;
|
||||
} catch (err) {
|
||||
store.error = err.message;
|
||||
throw err;
|
||||
} finally {
|
||||
store.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
export const Response = ({ name, loading, error }, { children }) => {
|
||||
const store = getStore(name);
|
||||
|
||||
return Tag("div", { style: "display:contents" }, [
|
||||
() => {
|
||||
if (store.loading) return loading || "Cargando...";
|
||||
if (store.error) return isFunc(error) ? error(store.error) : Tag("p", {}, store.error);
|
||||
if (store.data) return isFunc(children[0]) ? children[0](store.data) : children;
|
||||
return null;
|
||||
}
|
||||
]);
|
||||
};
|
||||
11
src/components/Spinner.js
Normal file
11
src/components/Spinner.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// components/Spinner.js
|
||||
import { Tag } from "sigpro";
|
||||
import { val } from "../utils.js";
|
||||
|
||||
export const Spinner = (props) => {
|
||||
const { value, ...rest } = props;
|
||||
return If(
|
||||
() => val(value),
|
||||
() => Tag("span", { class: "loading loading-spinner", ...rest })
|
||||
);
|
||||
};
|
||||
@@ -2,8 +2,6 @@
|
||||
@plugin "daisyui";
|
||||
@plugin "@iconify/tailwind4";
|
||||
|
||||
/* join join-vertical lg:join-horizontal divider divider-horizontal validator validator-hint glass */
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "light";
|
||||
default: true;
|
||||
@@ -99,6 +97,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.floating-label span {
|
||||
color: oklch(30% 0.01 260); /* Gris más oscuro (30% es más oscuro que 45%) */
|
||||
font-size: 1.1rem; /* text-base: más grande que 0.875rem */
|
||||
@@ -115,18 +114,6 @@
|
||||
font-size: 1.1rem; /* Mantiene el mismo tamaño */
|
||||
}
|
||||
|
||||
.collapse .collapse-content {
|
||||
transform: scaleY(0);
|
||||
transform-origin: top;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
.collapse:has(input:checked) .collapse-content {
|
||||
transform: scaleY(1);
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tab-content-inner {
|
||||
animation: tabFadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
@@ -144,8 +131,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* sigpro-ui daisyUI classes - extracted from components */
|
||||
|
||||
/* join join-vertical lg:join-horizontal divider divider-horizontal validator validator-hint glass */
|
||||
|
||||
/* Accordion */
|
||||
/* .input, .input-bordered, .input-ghost, .input-primary, .input-secondary, .input-accent, .input-info, .input-success, .input-warning, .input-error, .input-xs, .input-sm, .input-md, .input-lg, .floating-label, */
|
||||
|
||||
@@ -353,4 +343,7 @@
|
||||
/* .hover:bg-base-200, */
|
||||
|
||||
/* Misc */
|
||||
/* .active, .hr, .label, .label-text, */
|
||||
/* .active, .hr, .label, .label-text, */
|
||||
|
||||
/* Icons */
|
||||
/* .icon-[lucide--heart] */
|
||||
Reference in New Issue
Block a user