@jjlmoya/utils-shared 1.0.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.
@@ -0,0 +1,123 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+
4
+ interface Props {
5
+ items?: string[] | undefined;
6
+ icon?: string | undefined;
7
+ }
8
+
9
+ const { items, icon = "mdi:chevron-right" } = Astro.props;
10
+ ---
11
+
12
+ <ul class="seo-list-premium">
13
+ {
14
+ items ? (
15
+ items.map((item) => (
16
+ <li class="list-card">
17
+ <div class="marker-wrapper">
18
+ <div class="marker-box">
19
+ <Icon name={icon} class="marker-svg" />
20
+ </div>
21
+ </div>
22
+ <div class="card-content">
23
+ <Fragment set:html={item} />
24
+ </div>
25
+ </li>
26
+ ))
27
+ ) : (
28
+ <slot />
29
+ )
30
+ }
31
+ </ul>
32
+
33
+ <style>
34
+ .seo-list-premium {
35
+ list-style: none;
36
+ padding: 0;
37
+ margin: 3rem 0;
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: 1.5rem;
41
+ }
42
+
43
+ .list-card {
44
+ display: flex;
45
+ align-items: flex-start;
46
+ gap: 1.5rem;
47
+ padding: 1.5rem 1.75rem;
48
+ background: var(--bg-surface);
49
+ border: 1px solid var(--border-base);
50
+ border-radius: 20px;
51
+ transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
52
+ box-shadow: 0 4px 6px -1px var(--shadow-base), 0 2px 4px -2px var(--shadow-base);
53
+ }
54
+
55
+ .list-card:hover {
56
+ border-color: var(--accent);
57
+ transform: scale(1.01) translateX(8px);
58
+ box-shadow: 0 20px 25px -5px var(--shadow-hover), 0 8px 10px -6px var(--shadow-hover);
59
+ }
60
+
61
+ .marker-wrapper {
62
+ flex-shrink: 0;
63
+ margin-top: 4px;
64
+ }
65
+
66
+ .marker-box {
67
+ width: 32px;
68
+ height: 32px;
69
+ background: var(--bg-page);
70
+ border: 1px solid var(--border-base);
71
+ border-radius: 10px;
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ color: var(--accent);
76
+ transition: all 0.3s ease;
77
+ }
78
+
79
+ .list-card:hover .marker-box {
80
+ background: var(--accent);
81
+ color: var(--text-on-accent);
82
+ transform: rotate(90deg);
83
+ border-color: var(--accent);
84
+ box-shadow: 0 0 15px var(--accent-bg);
85
+ }
86
+
87
+ .marker-svg {
88
+ width: 18px;
89
+ height: 18px;
90
+ }
91
+
92
+ .card-content {
93
+ font-size: 1.05rem;
94
+ color: var(--text-muted);
95
+ line-height: 1.7;
96
+ font-weight: 500;
97
+ }
98
+
99
+ .card-content :global(strong) {
100
+ color: var(--text-base);
101
+ font-weight: 800;
102
+ display: block;
103
+ margin-bottom: 4px;
104
+ font-size: 1.1rem;
105
+ letter-spacing: -0.01em;
106
+ }
107
+
108
+ @media (max-width: 640px) {
109
+ .list-card {
110
+ padding: 1.25rem;
111
+ gap: 1rem;
112
+ }
113
+
114
+ .list-card:hover {
115
+ transform: translateX(4px);
116
+ }
117
+
118
+ .marker-box {
119
+ width: 28px;
120
+ height: 28px;
121
+ }
122
+ }
123
+ </style>
@@ -0,0 +1,54 @@
1
+ ---
2
+ interface Props {
3
+ title?: string | undefined;
4
+ ariaLabel?: string | undefined;
5
+ }
6
+
7
+ const { title, ariaLabel } = Astro.props;
8
+ ---
9
+
10
+ <div class="seo-message-box" aria-label={ariaLabel}>
11
+ {title && <div class="box-header">{title}</div>}
12
+ <div class="box-inner">
13
+ <div class="box-content"><slot /></div>
14
+ </div>
15
+ </div>
16
+
17
+ <style>
18
+ .seo-message-box {
19
+ margin: 2.5rem 0;
20
+ background: var(--bg-surface);
21
+ border: 1px solid var(--border-base);
22
+ border-radius: 12px;
23
+ overflow: hidden;
24
+ box-shadow: 0 4px 6px -1px var(--shadow-base);
25
+ }
26
+
27
+ .box-header {
28
+ background: var(--bg-page);
29
+ border-bottom: 1px solid var(--border-base);
30
+ padding: 10px 20px;
31
+ font-size: 0.7rem;
32
+ font-weight: 800;
33
+ color: var(--text-dimmed);
34
+ text-transform: uppercase;
35
+ letter-spacing: 0.05em;
36
+ }
37
+
38
+ .box-inner {
39
+ padding: 24px;
40
+ background: radial-gradient(
41
+ circle at top right,
42
+ var(--accent-bg),
43
+ transparent
44
+ );
45
+ }
46
+
47
+ .box-content {
48
+ font-size: 1rem;
49
+ color: var(--text-base);
50
+ line-height: 1.6;
51
+ white-space: pre-line;
52
+ text-align: left;
53
+ }
54
+ </style>
@@ -0,0 +1,153 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+
4
+ interface ProsConsItem {
5
+ pro: string;
6
+ con: string;
7
+ }
8
+
9
+ interface Props {
10
+ title?: string | undefined;
11
+ items: ProsConsItem[];
12
+ proTitle?: string | undefined;
13
+ conTitle?: string | undefined;
14
+ }
15
+
16
+ const { title, items, proTitle = "Ventajas", conTitle = "Desventajas" } = Astro.props;
17
+ ---
18
+
19
+ <div class="seo-proscons-box">
20
+ {title && <h4 class="proscons-header-title">{title}</h4>}
21
+ <div class="proscons-grid">
22
+ <div class="pros-column">
23
+ <span class="column-title pro-text">
24
+ <Icon name="mdi:check-circle" class="title-icon" />
25
+ {proTitle}
26
+ </span>
27
+ <ul class="proscons-list">
28
+ {items.map((item) => (
29
+ <li class="proscons-item">
30
+ <Icon name="mdi:plus" class="item-icon pro-text" />
31
+ <span>{item.pro}</span>
32
+ </li>
33
+ ))}
34
+ </ul>
35
+ </div>
36
+ <div class="cons-column">
37
+ <span class="column-title con-text">
38
+ <Icon name="mdi:alert-circle" class="title-icon" />
39
+ {conTitle}
40
+ </span>
41
+ <ul class="proscons-list">
42
+ {items.map((item) => (
43
+ <li class="proscons-item">
44
+ <Icon name="mdi:minus" class="item-icon con-text" />
45
+ <span>{item.con}</span>
46
+ </li>
47
+ ))}
48
+ </ul>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <style>
54
+ .seo-proscons-box {
55
+ margin: 3.5rem 0;
56
+ background: var(--bg-surface);
57
+ border: 1px solid var(--border-base);
58
+ border-radius: 24px;
59
+ padding: 2.5rem;
60
+ box-shadow: 0 4px 6px -1px var(--shadow-base);
61
+ }
62
+
63
+ .proscons-header-title {
64
+ text-align: center;
65
+ font-size: 1.25rem;
66
+ font-weight: 800;
67
+ color: var(--text-base);
68
+ margin-bottom: 2.5rem;
69
+ letter-spacing: -0.01em;
70
+ }
71
+
72
+ .proscons-grid {
73
+ display: grid;
74
+ grid-template-columns: 1fr 1fr;
75
+ gap: 2.5rem;
76
+ position: relative;
77
+ }
78
+
79
+ .proscons-grid::after {
80
+ content: "";
81
+ position: absolute;
82
+ left: 50%;
83
+ top: 0;
84
+ bottom: 0;
85
+ width: 1px;
86
+ background: var(--border-base);
87
+ transform: translateX(-50%);
88
+ }
89
+
90
+ .column-title {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 10px;
94
+ font-size: 1rem;
95
+ font-weight: 900;
96
+ text-transform: uppercase;
97
+ letter-spacing: 0.1em;
98
+ margin-bottom: 1.5rem;
99
+ }
100
+
101
+ .pro-text {
102
+ color: var(--color-success);
103
+ }
104
+
105
+ .con-text {
106
+ color: var(--color-error);
107
+ }
108
+
109
+ .title-icon {
110
+ width: 22px;
111
+ height: 22px;
112
+ }
113
+
114
+ .proscons-list {
115
+ list-style: none;
116
+ padding: 0;
117
+ margin: 0;
118
+ display: flex;
119
+ flex-direction: column;
120
+ gap: 1.25rem;
121
+ }
122
+
123
+ .proscons-item {
124
+ display: flex;
125
+ align-items: flex-start;
126
+ gap: 12px;
127
+ font-size: 0.95rem;
128
+ color: var(--text-muted);
129
+ font-weight: 500;
130
+ line-height: 1.5;
131
+ }
132
+
133
+ .item-icon {
134
+ width: 18px;
135
+ height: 18px;
136
+ margin-top: 2px;
137
+ flex-shrink: 0;
138
+ }
139
+
140
+ @media (max-width: 768px) {
141
+ .proscons-grid {
142
+ grid-template-columns: 1fr;
143
+ }
144
+
145
+ .proscons-grid::after {
146
+ display: none;
147
+ }
148
+
149
+ .seo-proscons-box {
150
+ padding: 1.5rem;
151
+ }
152
+ }
153
+ </style>
@@ -0,0 +1,87 @@
1
+ ---
2
+ import type { UtilitySEOContent } from "../types/index.ts";
3
+ import SEOArticle from "./SEOArticle.astro";
4
+ import SEOTitle from "./SEOTitle.astro";
5
+ import SEOList from "./SEOList.astro";
6
+ import SEOTable from "./SEOTable.astro";
7
+ import SEOTip from "./SEOTip.astro";
8
+ import SEOCard from "./SEOCard.astro";
9
+ import SEOStats from "./SEOStats.astro";
10
+ import SEOGlossary from "./SEOGlossary.astro";
11
+ import SEOCode from "./SEOCode.astro";
12
+ import SEOComparative from "./SEOComparative.astro";
13
+ import SEODiagnostic from "./SEODiagnostic.astro";
14
+ import SEOProsCons from "./SEOProsCons.astro";
15
+ import SEOSummary from "./SEOSummary.astro";
16
+ import SEOGrid from "./SEOGrid.astro";
17
+ import SEOMessageTemplate from "./SEOMessageTemplate.astro";
18
+
19
+ interface Props {
20
+ content: UtilitySEOContent;
21
+ }
22
+
23
+ const { content } = Astro.props;
24
+ ---
25
+
26
+ <SEOArticle>
27
+ {content.sections.map((section) => {
28
+ if (section.type === "title") {
29
+ return <SEOTitle title={section.text} level={section.level ?? 2} />;
30
+ }
31
+ if (section.type === "paragraph") {
32
+ return <Fragment set:html={section.html} />;
33
+ }
34
+ if (section.type === "list") {
35
+ return <SEOList items={section.items} icon={section.icon} />;
36
+ }
37
+ if (section.type === "table") {
38
+ return (
39
+ <SEOTable headers={section.headers}>
40
+ {section.rows.map((row) => (
41
+ <tr>
42
+ {row.map((cell) => <td set:html={cell} />)}
43
+ </tr>
44
+ ))}
45
+ </SEOTable>
46
+ );
47
+ }
48
+ if (section.type === "tip") {
49
+ return <SEOTip title={section.title}><Fragment set:html={section.html} /></SEOTip>;
50
+ }
51
+ if (section.type === "card") {
52
+ return <SEOCard icon={section.icon} title={section.title}><Fragment set:html={section.html} /></SEOCard>;
53
+ }
54
+ if (section.type === "stats") {
55
+ return <SEOStats stats={section.items} columns={section.columns} />;
56
+ }
57
+ if (section.type === "glossary") {
58
+ return <SEOGlossary items={section.items} />;
59
+ }
60
+ if (section.type === "code") {
61
+ return <SEOCode code={section.code} ariaLabel={section.ariaLabel} />;
62
+ }
63
+ if (section.type === "comparative") {
64
+ return <SEOComparative items={section.items} columns={section.columns} />;
65
+ }
66
+ if (section.type === "diagnostic") {
67
+ return (
68
+ <SEODiagnostic title={section.title} icon={section.icon} type={section.variant} badge={section.badge}>
69
+ <Fragment set:html={section.html} />
70
+ </SEODiagnostic>
71
+ );
72
+ }
73
+ if (section.type === "proscons") {
74
+ return <SEOProsCons title={section.title} items={section.items} proTitle={section.proTitle} conTitle={section.conTitle} />;
75
+ }
76
+ if (section.type === "summary") {
77
+ return <SEOSummary title={section.title} items={section.items} />;
78
+ }
79
+ if (section.type === "grid") {
80
+ return <SEOGrid columns={section.columns} />;
81
+ }
82
+ if (section.type === "message") {
83
+ return <SEOMessageTemplate title={section.title} ariaLabel={section.ariaLabel}><Fragment set:html={section.html} /></SEOMessageTemplate>;
84
+ }
85
+ return null;
86
+ })}
87
+ </SEOArticle>
@@ -0,0 +1,140 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+
4
+ interface StatItem {
5
+ value: string;
6
+ label: string;
7
+ icon?: string;
8
+ trend?: { value: string; positive: boolean };
9
+ }
10
+
11
+ interface Props {
12
+ stats: StatItem[];
13
+ columns?: 2 | 3 | 4 | undefined;
14
+ }
15
+
16
+ const { stats, columns = 3 } = Astro.props;
17
+ ---
18
+
19
+ <div class={`seo-stats-grid cols-${columns}`}>
20
+ {stats.map((stat) => (
21
+ <div class="stat-card">
22
+ <div class="stat-header">
23
+ {stat.icon && <Icon name={stat.icon} class="stat-icon" />}
24
+ {stat.trend && (
25
+ <span class={`stat-trend ${stat.trend.positive ? "is-positive" : "is-negative"}`}>
26
+ <Icon name={stat.trend.positive ? "mdi:trending-up" : "mdi:trending-down"} />
27
+ {stat.trend.value}
28
+ </span>
29
+ )}
30
+ </div>
31
+ <div class="stat-main">
32
+ <span class="stat-value">{stat.value}</span>
33
+ <span class="stat-label">{stat.label}</span>
34
+ </div>
35
+ </div>
36
+ ))}
37
+ </div>
38
+
39
+ <style>
40
+ .seo-stats-grid {
41
+ display: grid;
42
+ gap: 1.5rem;
43
+ margin: 2.5rem 0;
44
+ }
45
+
46
+ .cols-2 {
47
+ grid-template-columns: repeat(2, 1fr);
48
+ }
49
+
50
+ .cols-3 {
51
+ grid-template-columns: repeat(3, 1fr);
52
+ }
53
+
54
+ .cols-4 {
55
+ grid-template-columns: repeat(4, 1fr);
56
+ }
57
+
58
+ .stat-card {
59
+ background: var(--bg-surface);
60
+ border: 1px solid var(--border-base);
61
+ border-radius: 20px;
62
+ padding: 1.5rem;
63
+ display: flex;
64
+ flex-direction: column;
65
+ gap: 1rem;
66
+ transition: all 0.3s ease;
67
+ }
68
+
69
+ .stat-card:hover {
70
+ transform: translateY(-4px);
71
+ box-shadow: 0 12px 20px -5px var(--shadow-base);
72
+ border-color: var(--accent);
73
+ }
74
+
75
+ .stat-header {
76
+ display: flex;
77
+ justify-content: space-between;
78
+ align-items: center;
79
+ }
80
+
81
+ .stat-icon {
82
+ width: 24px;
83
+ height: 24px;
84
+ color: var(--accent);
85
+ }
86
+
87
+ .stat-trend {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 4px;
91
+ font-size: 0.75rem;
92
+ font-weight: 700;
93
+ padding: 4px 8px;
94
+ border-radius: 100px;
95
+ }
96
+
97
+ .is-positive {
98
+ background: var(--color-success);
99
+ color: var(--text-on-primary);
100
+ }
101
+
102
+ .is-negative {
103
+ background: var(--color-error);
104
+ color: var(--text-on-primary);
105
+ }
106
+
107
+ .stat-main {
108
+ display: flex;
109
+ flex-direction: column;
110
+ }
111
+
112
+ .stat-value {
113
+ font-size: 2.25rem;
114
+ font-weight: 900;
115
+ color: var(--text-base);
116
+ letter-spacing: -0.02em;
117
+ line-height: 1;
118
+ margin-bottom: 0.25rem;
119
+ }
120
+
121
+ .stat-label {
122
+ font-size: 0.85rem;
123
+ font-weight: 600;
124
+ color: var(--text-dimmed);
125
+ text-transform: uppercase;
126
+ letter-spacing: 0.05em;
127
+ }
128
+
129
+ @media (max-width: 768px) {
130
+ .seo-stats-grid {
131
+ grid-template-columns: repeat(2, 1fr);
132
+ }
133
+ }
134
+
135
+ @media (max-width: 480px) {
136
+ .seo-stats-grid {
137
+ grid-template-columns: 1fr;
138
+ }
139
+ }
140
+ </style>