@press2ai/theme-therapy-soft 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/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # @press2ai/theme-therapy-soft
2
+
3
+ Warm classless theme for therapy verticals (`otwarty-terapeuta`).
4
+
5
+ Sage greens + warm beiges, Crimson Text serif headings, Inter body. Structurally compatible with `@press2ai/engine` `TrenerTheme` interface — drop-in replacement for `@press2ai/theme-specialist-glossy` in CEIDG verticals.
6
+
7
+ ## Usage
8
+
9
+ ```ts
10
+ import {
11
+ layout, profileCard, profileArticle, esc,
12
+ catalogHero, catalogGrid, statBar, categoryNav, pagination,
13
+ } from '@press2ai/theme-therapy-soft/templates';
14
+ import { composeCss } from '@press2ai/theme-therapy-soft/styles';
15
+
16
+ const CSS_TAG = `<style>${composeCss('hero', 'statBar', 'categoryNav', 'catalogGrid', 'profileCard', 'profileArticle', 'pagination', 'forms')}</style>`;
17
+ ```
18
+
19
+ Pair with `createVerticalApp` from `@press2ai/engine/ceidg-vertical`.
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@press2ai/theme-therapy-soft",
3
+ "version": "0.1.0",
4
+ "description": "Warm sage/beige classless theme for therapy verticals. Crimson Text serif + Inter. Structurally compatible with @press2ai/engine TrenerTheme.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "homepage": "https://www.npmjs.com/package/@press2ai/theme-therapy-soft",
8
+ "keywords": [
9
+ "hono",
10
+ "cloudflare-workers",
11
+ "classless",
12
+ "semantic",
13
+ "therapy",
14
+ "schema-org",
15
+ "press2ai"
16
+ ],
17
+ "files": [
18
+ "src",
19
+ "README.md"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "exports": {
25
+ ".": "./src/index.ts",
26
+ "./templates": "./src/templates/index.ts",
27
+ "./styles": "./src/styles/index.ts"
28
+ }
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { composeCss, allCss } from './styles/index.ts';
2
+ export type { TherapyProfile } from './templates/helpers.ts';
@@ -0,0 +1,86 @@
1
+ export const css = `:root {
2
+ --font-sans: 'Inter', system-ui, -apple-system, sans-serif;
3
+ --font-serif: 'Crimson Text', Georgia, 'Times New Roman', serif;
4
+
5
+ --g1: 0.5rem; --g2: 0.8125rem; --g3: 1.3125rem; --g4: 2.125rem; --g5: 3.4375rem; --g6: 5.5625rem; --g7: 9rem;
6
+ --t-xs: 0.8125rem; --t-sm: 1rem; --t-md: 1.25rem; --t-lg: 1.625rem; --t-xl: 2.125rem; --t-2xl: 2.875rem; --t-3xl: 3.75rem;
7
+ --container: 1180px; --pad: var(--g4);
8
+ --w-6: 520px; --w-8: 720px; --w-10: 880px; --measure: 38rem;
9
+
10
+ --sage-50: #f7f5f2;
11
+ --sage-100: #efece7;
12
+ --sage-200: #dde2dd;
13
+ --sage-400: #a8baa8;
14
+ --sage-500: #8ea78e;
15
+ --sage-600: #7a8f7a;
16
+ --sage-700: #6a7f6a;
17
+ --sage-800: #4f5f4f;
18
+
19
+ --beige-50: #faf5ef;
20
+ --beige-100: #f3ece4;
21
+ --beige-200: #e8ddd0;
22
+
23
+ --stone-300: #d6d3d1;
24
+ --stone-400: #a8a29e;
25
+ --stone-500: #78716c;
26
+ --stone-600: #57534e;
27
+ --stone-700: #44403c;
28
+ --stone-900: #1c1917;
29
+
30
+ --emerald-100: #d1fae5;
31
+ --emerald-700: #047857;
32
+ --amber-100: #fef3c7;
33
+ --amber-700: #b45309;
34
+
35
+ --bg: var(--sage-50);
36
+ --card: var(--beige-50);
37
+ --ink: var(--stone-900);
38
+ --ink-2: var(--stone-600);
39
+ --ink-3: var(--stone-500);
40
+ --line: var(--stone-300);
41
+ --line-soft: var(--beige-200);
42
+ --accent: var(--sage-600);
43
+ --accent-h: var(--sage-700);
44
+ --accent-soft: var(--sage-100);
45
+
46
+ --r: 4px; --r-lg: 8px; --pill: 999px;
47
+ --shadow: 0 1px 2px rgba(60,50,40,.04);
48
+ --shadow-md: 0 4px 16px rgba(60,50,40,.06), 0 1px 3px rgba(60,50,40,.04);
49
+ --shadow-up: 0 12px 28px rgba(60,50,40,.10), 0 2px 6px rgba(60,50,40,.04);
50
+ --ease: 220ms cubic-bezier(.4,0,.2,1);
51
+ }
52
+ *, *::before, *::after { box-sizing: border-box; margin: 0; }
53
+ html { font-size: 16px; -webkit-font-smoothing: antialiased; scroll-behavior: smooth; }
54
+ body {
55
+ font-family: var(--font-sans); font-weight: 400;
56
+ color: var(--ink); background: var(--bg); line-height: 1.65; overflow-x: clip;
57
+ }
58
+ h1, h2, h3, h4 {
59
+ color: var(--ink); font-family: var(--font-serif); font-weight: 600;
60
+ letter-spacing: -0.01em; line-height: 1.15;
61
+ }
62
+ h1 { font-size: clamp(var(--t-2xl), 5.5vw, var(--t-3xl)); margin-bottom: var(--g3); }
63
+ h2 { font-size: clamp(var(--t-lg), 3vw, var(--t-xl)); margin-bottom: var(--g2); }
64
+ h3 { font-size: var(--t-md); font-weight: 600; line-height: 1.35; margin-bottom: var(--g2); }
65
+ h4 { font-size: var(--t-sm); font-weight: 600; }
66
+ p {
67
+ font-size: var(--t-sm); line-height: 1.7; color: var(--ink-2);
68
+ margin-bottom: var(--g2); max-width: var(--measure);
69
+ }
70
+ a { color: var(--accent-h); text-decoration: none; transition: color var(--ease); }
71
+ a:hover { color: var(--ink); }
72
+ small { font-size: var(--t-xs); color: var(--ink-3); }
73
+ strong { color: var(--ink); font-weight: 600; }
74
+ main {
75
+ max-width: var(--container); margin-inline: auto;
76
+ padding-inline: var(--pad); padding-top: 0; padding-bottom: 0;
77
+ }
78
+ main:not(:has(> section, > article, > div, > nav)) {
79
+ padding-top: var(--g5); padding-bottom: var(--g5);
80
+ }
81
+ main > * + * { margin-top: var(--g6); }
82
+ @media (max-width: 640px) {
83
+ :root { --pad: var(--g3); }
84
+ main { padding: 0 var(--pad) var(--g4); }
85
+ main > * + * { margin-top: var(--g5); }
86
+ }`;
@@ -0,0 +1,21 @@
1
+ export const css = `section:has(> article[itemscope]) {
2
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
3
+ gap: var(--g3);
4
+ }
5
+ section:has(> article[itemscope]) > h2 {
6
+ grid-column: 1 / -1; text-align: center; margin-bottom: var(--g4);
7
+ font-family: var(--font-serif); font-weight: 400;
8
+ font-size: clamp(var(--t-lg), 3vw, var(--t-xl));
9
+ }
10
+ section:has(> article[itemscope]) > h2::before {
11
+ content: 'Nasi terapeuci'; display: block;
12
+ font-family: var(--font-sans); font-size: var(--t-xs); font-weight: 500;
13
+ color: var(--sage-700); letter-spacing: 0.1em; text-transform: uppercase;
14
+ margin-bottom: var(--g2);
15
+ }
16
+ section:has(> article[itemscope]) > nav[aria-label] {
17
+ grid-column: 1 / -1; margin-bottom: var(--g3);
18
+ }
19
+ @media (max-width: 640px) {
20
+ section:has(> article[itemscope]) { grid-template-columns: 1fr; }
21
+ }`;
@@ -0,0 +1,12 @@
1
+ export const css = `nav[aria-label] {
2
+ display: flex; flex-wrap: wrap; gap: var(--g1); justify-content: center;
3
+ }
4
+ nav[aria-label] a {
5
+ display: inline-flex; align-items: center; height: 40px; padding: 0 var(--g3);
6
+ font-size: var(--t-xs); font-weight: 500;
7
+ background: var(--beige-50); border: 1px solid var(--line);
8
+ color: var(--ink-2); transition: all var(--ease);
9
+ }
10
+ nav[aria-label] a:hover {
11
+ background: var(--sage-100); border-color: var(--sage-400); color: var(--sage-700);
12
+ }`;
@@ -0,0 +1,29 @@
1
+ export const css = `body > footer {
2
+ border-top: 1px solid var(--line-soft); background: var(--beige-50);
3
+ padding: var(--g6) 0 var(--g4); margin-top: var(--g6);
4
+ }
5
+ body > footer > small {
6
+ display: block; max-width: var(--container); margin-inline: auto; padding-inline: var(--pad);
7
+ color: var(--ink-3); font-size: var(--t-xs);
8
+ }
9
+ body > footer a { color: var(--ink-2); transition: color var(--ease); }
10
+ body > footer a:hover { color: var(--accent-h); }
11
+ body > footer > small > nav {
12
+ display: grid; grid-template-columns: 2fr 1fr 1fr; gap: var(--g5);
13
+ margin-bottom: var(--g5); padding-bottom: var(--g4);
14
+ border-bottom: 1px solid var(--line-soft);
15
+ }
16
+ body > footer > small > nav section { all: unset; display: block; }
17
+ body > footer > small > nav strong {
18
+ display: block; font-family: var(--font-sans); font-size: var(--t-xs);
19
+ font-weight: 600; color: var(--ink); margin-bottom: var(--g2);
20
+ }
21
+ body > footer > small > nav p { font-size: var(--t-xs); line-height: 1.7; color: var(--ink-3); margin: 0; max-width: 36ch; }
22
+ body > footer > small > nav a { display: block; font-size: var(--t-xs); line-height: 2; color: var(--ink-2); }
23
+ body > footer > small > nav a:hover { color: var(--accent-h); }
24
+ body > footer > small > p {
25
+ font-size: var(--t-xs); color: var(--ink-3); text-align: left;
26
+ }
27
+ @media (max-width: 640px) {
28
+ body > footer > small > nav { grid-template-columns: 1fr; gap: var(--g3); }
29
+ }`;
@@ -0,0 +1,20 @@
1
+ export const css = `form:not([role="search"]) label {
2
+ display: block; font-size: var(--t-sm); color: var(--ink-2);
3
+ font-weight: 500; margin-bottom: var(--g1); font-family: var(--font-sans);
4
+ }
5
+ form:not([role="search"]) input[type="text"] {
6
+ display: block; width: 100%; margin-top: var(--g1); height: 48px;
7
+ padding: 0 var(--g3); background: var(--beige-50); border: 1px solid var(--line);
8
+ color: var(--ink); font-size: var(--t-sm); font-family: var(--font-sans);
9
+ transition: border-color var(--ease);
10
+ }
11
+ form:not([role="search"]) input[type="text"]:focus {
12
+ outline: none; border-color: var(--sage-600);
13
+ }
14
+ form:not([role="search"]) button[type="submit"] {
15
+ margin-top: var(--g3); height: 48px; padding: 0 var(--g4);
16
+ border: none; background: var(--sage-600); color: var(--beige-50);
17
+ font-size: var(--t-sm); font-weight: 500; font-family: var(--font-sans);
18
+ cursor: pointer; transition: background var(--ease);
19
+ }
20
+ form:not([role="search"]) button[type="submit"]:hover { background: var(--sage-700); }`;
@@ -0,0 +1,16 @@
1
+ export const css = `body > header {
2
+ background: var(--beige-50); border-bottom: 1px solid var(--line-soft);
3
+ }
4
+ body > header nav {
5
+ max-width: var(--container); margin-inline: auto; padding-inline: var(--pad);
6
+ height: 80px; display: flex; align-items: center; justify-content: space-between;
7
+ }
8
+ body > header nav a {
9
+ color: var(--ink-2); text-decoration: none; font-size: var(--t-xs);
10
+ font-weight: 500; transition: color var(--ease);
11
+ }
12
+ body > header nav a:hover { color: var(--ink); }
13
+ body > header nav a strong {
14
+ font-family: var(--font-serif); font-size: var(--t-md); font-weight: 600;
15
+ color: var(--ink); letter-spacing: 0;
16
+ }`;
@@ -0,0 +1,68 @@
1
+ export const css = `hgroup {
2
+ position: relative; text-align: center; isolation: isolate;
3
+ padding: var(--g7) var(--pad) var(--g6);
4
+ margin-inline: calc(50% - 50vw);
5
+ background: var(--sage-100);
6
+ border-bottom: 1px solid var(--line-soft);
7
+ }
8
+ hgroup > * { position: relative; max-width: var(--w-10); margin-inline: auto; }
9
+ hgroup > p:first-child {
10
+ display: inline-flex; margin: 0 auto var(--g3); padding: 0;
11
+ }
12
+ hgroup > p:first-child small {
13
+ display: inline-flex; align-items: center; gap: var(--g1);
14
+ background: var(--beige-50); padding: var(--g1) var(--g2);
15
+ border: 1px solid var(--sage-200); border-radius: var(--pill);
16
+ font-size: var(--t-xs); font-weight: 500; color: var(--sage-700);
17
+ letter-spacing: 0.04em; text-transform: uppercase;
18
+ }
19
+ hgroup > p:first-child small::before {
20
+ content: ''; width: 6px; height: 6px; border-radius: 50%;
21
+ background: var(--sage-500); box-shadow: 0 0 0 3px rgba(142,167,142,.25);
22
+ }
23
+ hgroup h1 {
24
+ font-family: var(--font-serif); font-weight: 400;
25
+ max-width: 22ch; margin: 0 auto var(--g4); color: var(--stone-900);
26
+ letter-spacing: -0.015em;
27
+ }
28
+ hgroup > p:not(:first-child) {
29
+ font-family: var(--font-sans); font-size: var(--t-md); font-weight: 400;
30
+ line-height: 1.6; color: var(--ink-2); max-width: var(--measure);
31
+ margin: 0 auto var(--g4);
32
+ }
33
+ hgroup > nav {
34
+ display: flex; gap: var(--g2); flex-wrap: wrap; justify-content: center;
35
+ margin-top: var(--g4); margin-bottom: 0;
36
+ }
37
+ hgroup > nav > a {
38
+ display: inline-flex; align-items: center; height: 52px; padding: 0 var(--g4);
39
+ font-weight: 500; font-size: var(--t-sm); transition: all var(--ease);
40
+ background: var(--beige-50); color: var(--ink); border: 1px solid var(--line);
41
+ }
42
+ hgroup > nav > a:first-child {
43
+ background: var(--sage-600); color: var(--beige-50); border-color: transparent;
44
+ }
45
+ hgroup > nav > a:first-child:hover { background: var(--sage-700); }
46
+ hgroup > nav > a:not(:first-child):hover { background: var(--beige-100); }
47
+ form[role="search"] {
48
+ display: flex; max-width: var(--w-6); height: 52px;
49
+ margin: var(--g4) auto 0; background: var(--beige-50);
50
+ border: 1px solid var(--line); overflow: hidden; transition: all var(--ease);
51
+ }
52
+ form[role="search"]:focus-within { border-color: var(--sage-600); }
53
+ form[role="search"] input {
54
+ flex: 1; min-width: 0; border: none; background: transparent; color: var(--ink);
55
+ padding: 0 var(--g3); font-size: var(--t-sm); font-family: var(--font-sans); outline: none;
56
+ -webkit-appearance: none; appearance: none;
57
+ }
58
+ form[role="search"] input::placeholder { color: var(--ink-3); }
59
+ form[role="search"] button {
60
+ flex-shrink: 0; height: 100%; border: none; background: var(--sage-600); color: var(--beige-50);
61
+ padding: 0 var(--g4); font-size: var(--t-sm); font-weight: 500;
62
+ font-family: var(--font-sans); cursor: pointer; transition: background var(--ease);
63
+ }
64
+ form[role="search"] button:hover { background: var(--sage-700); }
65
+ @media (max-width: 640px) {
66
+ hgroup { padding: var(--g5) var(--pad) var(--g4); }
67
+ hgroup > nav > a, form[role="search"] { height: 44px; }
68
+ }`;
@@ -0,0 +1,32 @@
1
+ import { css as base } from './base.ts';
2
+ import { css as header } from './header.ts';
3
+ import { css as footer } from './footer.ts';
4
+ import { css as hero } from './hero.ts';
5
+ import { css as statBar } from './stat-bar.ts';
6
+ import { css as categoryNav } from './category-nav.ts';
7
+ import { css as catalogGrid } from './catalog-grid.ts';
8
+ import { css as profileCard } from './profile-card.ts';
9
+ import { css as profileArticle } from './profile-article.ts';
10
+ import { css as pagination } from './pagination.ts';
11
+ import { css as forms } from './forms.ts';
12
+
13
+ const components: Record<string, string> = {
14
+ hero,
15
+ statBar,
16
+ categoryNav,
17
+ catalogGrid,
18
+ profileCard,
19
+ profileArticle,
20
+ pagination,
21
+ forms,
22
+ };
23
+
24
+ const layoutCss = base + '\n' + header + '\n' + footer;
25
+
26
+ export function composeCss(...names: string[]): string {
27
+ return layoutCss + '\n' + names.map(n => components[n] ?? '').join('\n');
28
+ }
29
+
30
+ export const allCss = layoutCss + '\n' + Object.values(components).join('\n');
31
+
32
+ export { base, header, footer, hero, statBar, categoryNav, catalogGrid, profileCard, profileArticle, pagination, forms };
@@ -0,0 +1,13 @@
1
+ export const css = `main > nav p {
2
+ display: flex; align-items: center; justify-content: center; gap: var(--g3);
3
+ font-size: var(--t-xs); color: var(--ink-3); font-family: var(--font-sans);
4
+ }
5
+ main > nav a {
6
+ height: 40px; display: inline-flex; align-items: center; padding: 0 var(--g3);
7
+ background: var(--beige-50); border: 1px solid var(--line);
8
+ font-size: var(--t-xs); font-weight: 500; color: var(--ink-2);
9
+ transition: all var(--ease);
10
+ }
11
+ main > nav a:hover {
12
+ background: var(--sage-100); border-color: var(--sage-400); color: var(--sage-700);
13
+ }`;
@@ -0,0 +1,56 @@
1
+ export const css = `main > article {
2
+ max-width: var(--w-8); margin-inline: auto;
3
+ padding: var(--g6) 0;
4
+ }
5
+ main > article > header {
6
+ padding-bottom: var(--g4); margin-bottom: var(--g4);
7
+ border-bottom: 1px solid var(--line-soft);
8
+ }
9
+ main > article > header h1 {
10
+ font-family: var(--font-serif); font-weight: 400;
11
+ font-size: clamp(var(--t-xl), 5vw, var(--t-2xl));
12
+ letter-spacing: -0.01em; line-height: 1.1; margin-bottom: var(--g1);
13
+ }
14
+ main > article > header p {
15
+ font-size: var(--t-md); color: var(--sage-700); margin: 0; font-weight: 400;
16
+ font-style: italic; font-family: var(--font-serif);
17
+ }
18
+ main > article address {
19
+ font-style: normal; font-size: var(--t-sm); color: var(--ink-2);
20
+ padding: var(--g2) 0; margin-bottom: var(--g3); border-bottom: 1px solid var(--line-soft);
21
+ }
22
+ main > article address strong { color: var(--ink-3); font-weight: 500; }
23
+ main > article section {
24
+ margin: var(--g4) 0; padding-top: var(--g4); border-top: 1px solid var(--line-soft);
25
+ }
26
+ main > article section h2 {
27
+ font-family: var(--font-sans); font-size: var(--t-xs); font-weight: 600;
28
+ text-transform: uppercase; letter-spacing: 0.1em; color: var(--sage-700);
29
+ margin: 0 0 var(--g3);
30
+ }
31
+ main > article section > p {
32
+ font-family: var(--font-serif); font-size: var(--t-md); line-height: 1.7;
33
+ color: var(--ink-2); max-width: none;
34
+ }
35
+ main > article ul {
36
+ list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: var(--g1);
37
+ }
38
+ main > article ul li {
39
+ font-size: var(--t-xs); font-weight: 500; color: var(--sage-800);
40
+ padding: var(--g1) var(--g2); background: var(--sage-100);
41
+ }
42
+ main > article dl {
43
+ display: grid; grid-template-columns: 9rem 1fr; gap: var(--g2) var(--g3);
44
+ }
45
+ main > article dt {
46
+ font-size: var(--t-xs); color: var(--ink-3); font-weight: 500;
47
+ text-transform: uppercase; letter-spacing: 0.06em;
48
+ }
49
+ main > article dd { margin: 0; font-size: var(--t-sm); color: var(--ink); }
50
+ main > article dd a { color: var(--sage-700); }
51
+ main > article dd a:hover { color: var(--ink); }
52
+ @media (max-width: 640px) {
53
+ main > article { padding: var(--g4) 0; }
54
+ main > article dl { grid-template-columns: 1fr; gap: 2px 0; }
55
+ main > article dt { margin-top: var(--g2); }
56
+ }`;
@@ -0,0 +1,57 @@
1
+ const S = 'section:has(> article[itemscope]) > article[itemscope]';
2
+ export const css = `${S} {
3
+ position: relative; background: var(--beige-50); border: 1px solid var(--line-soft);
4
+ padding: var(--g3); display: flex; flex-direction: column;
5
+ box-shadow: var(--shadow); transition: all var(--ease);
6
+ }
7
+ ${S}:hover {
8
+ border-color: var(--sage-400); box-shadow: var(--shadow-md);
9
+ }
10
+ ${S} > div[aria-hidden] {
11
+ width: 56px; height: 56px; border-radius: 50%;
12
+ display: flex; align-items: center; justify-content: center;
13
+ font-family: var(--font-serif); font-size: var(--t-md); font-weight: 600;
14
+ color: var(--beige-50); background: var(--sage-600);
15
+ margin-bottom: var(--g3); flex-shrink: 0;
16
+ }
17
+ ${S} header { margin-bottom: var(--g2); }
18
+ ${S} h2 {
19
+ font-family: var(--font-serif); font-size: var(--t-md); font-weight: 600;
20
+ line-height: 1.25; margin: 0 0 var(--g1); letter-spacing: 0;
21
+ }
22
+ ${S} h2 a { color: var(--ink); }
23
+ ${S} h2 a:hover { color: var(--sage-700); }
24
+ ${S} header p {
25
+ font-size: var(--t-xs); line-height: 1.5; color: var(--ink-3); margin: 0 0 2px;
26
+ }
27
+ ${S} header p:has([itemprop="address"]) {
28
+ display: inline-flex; align-items: center; gap: 6px; margin-top: var(--g1);
29
+ color: var(--stone-500);
30
+ }
31
+ ${S} header p:has([itemprop="address"])::before {
32
+ content: '\u00b7'; color: var(--sage-500); font-weight: 700;
33
+ }
34
+ ${S} [itemprop="description"] {
35
+ font-size: var(--t-xs); line-height: 1.6; margin: var(--g2) 0 0; color: var(--ink-2);
36
+ }
37
+ ${S} ul {
38
+ list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 4px;
39
+ margin: var(--g2) 0 0;
40
+ }
41
+ ${S} ul li {
42
+ background: var(--sage-100); color: var(--sage-800);
43
+ padding: 3px var(--g1); font-size: 0.75rem; font-weight: 500;
44
+ }
45
+ ${S} > a:last-child {
46
+ display: inline-flex; align-items: center; justify-content: center;
47
+ margin-top: auto; padding: var(--g2) 0;
48
+ background: var(--sage-600); color: var(--beige-50);
49
+ font-size: var(--t-xs); font-weight: 500;
50
+ margin-left: calc(-1 * var(--g3)); margin-right: calc(-1 * var(--g3));
51
+ margin-bottom: calc(-1 * var(--g3));
52
+ transition: background var(--ease);
53
+ }
54
+ ${S} > article > a:last-child:hover,
55
+ ${S} > a:last-child:hover { background: var(--sage-700); color: var(--beige-50); }
56
+ ${S} > * + * { margin-top: 0; }
57
+ ${S} > a:last-child { margin-top: var(--g3); }`;
@@ -0,0 +1,31 @@
1
+ export const css = `section[aria-label="Statystyki"] {
2
+ background: var(--beige-50); border-top: 1px solid var(--line-soft);
3
+ border-bottom: 1px solid var(--line-soft);
4
+ margin-inline: calc(50% - 50vw); padding: var(--g5) var(--pad);
5
+ }
6
+ section[aria-label="Statystyki"] > p {
7
+ text-align: center; font-size: var(--t-md); font-weight: 400; line-height: 1.6;
8
+ color: var(--ink); margin: 0 auto var(--g4); max-width: var(--measure);
9
+ font-family: var(--font-serif); font-style: italic;
10
+ }
11
+ section[aria-label="Statystyki"] > dl {
12
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
13
+ gap: var(--g4); max-width: var(--w-10); margin: 0 auto; padding: 0;
14
+ }
15
+ section[aria-label="Statystyki"] > dl div { text-align: center; }
16
+ section[aria-label="Statystyki"] > dl div > span:has(> svg) {
17
+ display: flex; align-items: center; justify-content: center; width: 44px; height: 44px;
18
+ margin: 0 auto var(--g2); color: var(--sage-600);
19
+ }
20
+ section[aria-label="Statystyki"] > dl div > span:has(> svg) > svg { width: 24px; height: 24px; }
21
+ section[aria-label="Statystyki"] > dl dt {
22
+ font-family: var(--font-serif); font-size: var(--t-2xl); font-weight: 400;
23
+ line-height: 1.1; color: var(--stone-900);
24
+ }
25
+ section[aria-label="Statystyki"] > dl dd {
26
+ margin: var(--g1) 0 0; font-size: var(--t-xs); line-height: 1.4;
27
+ color: var(--ink-3); font-weight: 400;
28
+ }
29
+ @media (max-width: 640px) {
30
+ section[aria-label="Statystyki"] > dl { grid-template-columns: repeat(2, 1fr); gap: var(--g3); }
31
+ }`;
@@ -0,0 +1,77 @@
1
+ import { esc } from './helpers.ts';
2
+
3
+ export interface CatalogHeroProps {
4
+ badge?: string;
5
+ title: string;
6
+ subtitle?: string;
7
+ searchAction?: string;
8
+ searchPlaceholder?: string;
9
+ searchValue?: string;
10
+ ctas?: { label: string; href: string }[];
11
+ }
12
+
13
+ export function catalogHero(p: CatalogHeroProps): string {
14
+ const badge = p.badge ? `<p><small>${esc(p.badge)}</small></p>` : '';
15
+ const subtitle = p.subtitle ? `<p>${esc(p.subtitle)}</p>` : '';
16
+ const search = p.searchAction !== undefined ? `<form role="search" action="${esc(p.searchAction ?? '/')}" method="get">
17
+ <input type="search" name="q" placeholder="${esc(p.searchPlaceholder ?? 'Szukaj...')}" value="${esc(p.searchValue ?? '')}" />
18
+ <button type="submit">Szukaj</button>
19
+ </form>` : '';
20
+ const ctas = p.ctas?.length ? `<nav>${p.ctas.map(
21
+ c => `<a href="${esc(c.href)}">${esc(c.label)}</a>`
22
+ ).join('\n')}</nav>` : '';
23
+ return `<hgroup>
24
+ ${badge}
25
+ <h1>${p.title}</h1>
26
+ ${subtitle}
27
+ ${search}
28
+ ${ctas}
29
+ </hgroup>`;
30
+ }
31
+
32
+ const STAT_ICONS: Record<string, string> = {
33
+ people: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>',
34
+ city: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 21h18"/><path d="M5 21V7l8-4v18"/><path d="M19 21V11l-6-4"/></svg>',
35
+ free: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
36
+ };
37
+
38
+ export function statBar(items: { value: string; label: string; icon?: string }[], summary?: string): string {
39
+ const divs = items.map(i => {
40
+ const icon = i.icon && STAT_ICONS[i.icon] ? `<span>${STAT_ICONS[i.icon]}</span>` : '';
41
+ return `<div>${icon}<dt>${esc(i.value)}</dt><dd>${esc(i.label)}</dd></div>`;
42
+ }).join('\n');
43
+ const summaryP = summary ? `<p>${esc(summary)}</p>` : '';
44
+ return `<section aria-label="Statystyki">${summaryP}<dl aria-hidden="true">\n${divs}\n</dl></section>`;
45
+ }
46
+
47
+ export function categoryNav(items: { href: string; label: string }[], ariaLabel = 'Kategorie'): string {
48
+ if (!items.length) return '';
49
+ const links = items.map(i => `<a href="${esc(i.href)}">${esc(i.label)}</a>`).join('');
50
+ return `<nav aria-label="${esc(ariaLabel)}">${links}</nav>`;
51
+ }
52
+
53
+ export function catalogGrid(p: { title: string; filters?: string; cards: string }): string {
54
+ return `<section>
55
+ <h2>${esc(p.title)}</h2>
56
+ ${p.filters ?? ''}
57
+ ${p.cards}
58
+ </section>`;
59
+ }
60
+
61
+ export interface PaginationProps {
62
+ current: number;
63
+ total: number;
64
+ baseHref?: string;
65
+ extraParams?: string;
66
+ prevLabel?: string;
67
+ nextLabel?: string;
68
+ }
69
+
70
+ export function pagination(p: PaginationProps): string {
71
+ if (p.total <= 1) return '';
72
+ const base = p.baseHref ?? '/';
73
+ const extra = p.extraParams ?? '';
74
+ const prev = p.current > 1 ? `<a href="${esc(base)}?p=${p.current - 1}${extra}">${esc(p.prevLabel ?? '\u2190 Poprzednia')}</a>` : '';
75
+ const next = p.current < p.total ? `<a href="${esc(base)}?p=${p.current + 1}${extra}">${esc(p.nextLabel ?? 'Następna \u2192')}</a>` : '';
76
+ return `<nav><p>${prev} Strona ${p.current} z ${p.total} ${next}</p></nav>`;
77
+ }
@@ -0,0 +1,22 @@
1
+ export function esc(s: string | undefined | null): string {
2
+ if (!s) return '';
3
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
4
+ }
5
+
6
+ export interface TherapyProfile {
7
+ slug: string;
8
+ firstName: string;
9
+ lastName: string;
10
+ jobTitle: string;
11
+ city?: string;
12
+ bio?: string;
13
+ photo?: string;
14
+ specialties: string[];
15
+ languages: string[];
16
+ business: { name?: string; taxId?: string; classification: string[] };
17
+ social: Record<string, string>;
18
+ }
19
+
20
+ export function fullName(p: TherapyProfile): string {
21
+ return `${p.firstName} ${p.lastName}`;
22
+ }
@@ -0,0 +1,5 @@
1
+ export { layout, type LayoutProps } from './layout.ts';
2
+ export { profileCard } from './profile-card.ts';
3
+ export { profileArticle } from './profile-article.ts';
4
+ export { catalogHero, catalogGrid, statBar, categoryNav, pagination, type CatalogHeroProps, type PaginationProps } from './catalog.ts';
5
+ export { esc, fullName, type TherapyProfile } from './helpers.ts';
@@ -0,0 +1,61 @@
1
+ import { esc } from './helpers.ts';
2
+
3
+ export interface LayoutProps {
4
+ title: string;
5
+ description?: string;
6
+ jsonLd?: object;
7
+ siteName?: string;
8
+ homeHref?: string;
9
+ baseUrl?: string;
10
+ headExtra?: string;
11
+ navExtra?: string;
12
+ footerContent?: string;
13
+ }
14
+
15
+ const THEME_VERSION = '0.1.0';
16
+
17
+ export function layout(props: LayoutProps, body: string): string {
18
+ const {
19
+ title,
20
+ description,
21
+ jsonLd,
22
+ siteName = 'press2ai',
23
+ homeHref = '/',
24
+ headExtra = '',
25
+ navExtra = '',
26
+ footerContent,
27
+ } = props;
28
+
29
+ const footer = footerContent ??
30
+ `Powered by <a href="https://www.npmjs.com/org/press2ai">press2ai</a> &middot;
31
+ <a href="https://www.npmjs.com/package/@press2ai/theme-therapy-soft/v/${THEME_VERSION}">@press2ai/theme-therapy-soft v${THEME_VERSION}</a>`;
32
+
33
+ return `<!doctype html>
34
+ <html lang="pl">
35
+ <head>
36
+ <meta charset="UTF-8">
37
+ <meta name="viewport" content="width=device-width, initial-scale=1">
38
+ <title>${esc(title)}</title>
39
+ ${description ? `<meta name="description" content="${esc(description)}">` : ''}
40
+ <meta property="og:title" content="${esc(title)}">
41
+ ${description ? `<meta property="og:description" content="${esc(description)}">` : ''}
42
+ ${jsonLd ? `<script type="application/ld+json">${JSON.stringify(jsonLd).replace(/</g, '\\u003c')}</script>` : ''}
43
+ <link rel="preconnect" href="https://fonts.googleapis.com">
44
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
45
+ <link href="https://fonts.googleapis.com/css2?family=Crimson+Text:ital,wght@0,400;0,600;1,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
46
+ ${headExtra}
47
+ </head>
48
+ <body>
49
+ <header>
50
+ <nav>
51
+ <a href="${esc(homeHref)}"><strong>${esc(siteName)}</strong></a>
52
+ ${navExtra}
53
+ </nav>
54
+ </header>
55
+ <main>${body}</main>
56
+ <footer>
57
+ <small>${footer}</small>
58
+ </footer>
59
+ </body>
60
+ </html>`;
61
+ }
@@ -0,0 +1,85 @@
1
+ import { esc, fullName, type TherapyProfile } from './helpers.ts';
2
+
3
+ export function profileArticle(p: TherapyProfile): string {
4
+ const name = fullName(p);
5
+ const socials = Object.entries(p.social ?? {}).filter(([, v]) => v);
6
+
7
+ const address = p.city
8
+ ? `<address itemprop="address" itemscope itemtype="https://schema.org/PostalAddress">
9
+ <strong>Miasto: </strong>
10
+ <span itemprop="addressLocality">${esc(p.city)}</span>,
11
+ <span itemprop="addressCountry">PL</span>
12
+ </address>`
13
+ : '';
14
+
15
+ const bio = p.bio
16
+ ? `<section>
17
+ <h2>O mnie</h2>
18
+ <p itemprop="description">${esc(p.bio)}</p>
19
+ </section>`
20
+ : '';
21
+
22
+ const specialties = p.specialties.length > 0
23
+ ? `<section>
24
+ <h2>Specjalizacje</h2>
25
+ <ul>
26
+ ${p.specialties.map(s => `<li itemprop="knowsAbout">${esc(s)}</li>`).join('\n')}
27
+ </ul>
28
+ </section>`
29
+ : '';
30
+
31
+ const languages = p.languages.length > 0
32
+ ? `<section>
33
+ <h2>Języki</h2>
34
+ <ul>
35
+ ${p.languages.map(l => `<li itemprop="knowsLanguage">${esc(l)}</li>`).join('\n')}
36
+ </ul>
37
+ </section>`
38
+ : '';
39
+
40
+ const socialUrlPrefix: Record<string, string> = {
41
+ linkedin: 'https://linkedin.com/in/',
42
+ youtube: 'https://youtube.com/@',
43
+ tiktok: 'https://tiktok.com/@',
44
+ facebook: 'https://facebook.com/',
45
+ twitter: 'https://x.com/',
46
+ instagram: 'https://instagram.com/',
47
+ };
48
+ const contactItems = socials.map(([n, handle]) => {
49
+ const prefix = socialUrlPrefix[n] ?? `https://${esc(n)}.com/`;
50
+ return `<dt>${esc(n)}</dt><dd><a href="${prefix}${esc(handle as string)}" rel="noopener">@${esc(handle as string)}</a></dd>`;
51
+ });
52
+
53
+ const contact = contactItems.length > 0
54
+ ? `<section>
55
+ <h2>Kontakt</h2>
56
+ <dl>
57
+ ${contactItems.join('\n')}
58
+ </dl>
59
+ </section>`
60
+ : '';
61
+
62
+ const business = p.business?.name
63
+ ? `<section>
64
+ <h2>Dane firmy</h2>
65
+ <dl>
66
+ <dt>Nazwa</dt><dd>${esc(p.business.name)}</dd>
67
+ ${p.business.taxId ? `<dt>NIP</dt><dd>${esc(p.business.taxId)}</dd>` : ''}
68
+ ${(p.business.classification?.length ?? 0) > 0 ? `<dt>PKD</dt><dd>${p.business.classification!.map(c => esc(c)).join(', ')}</dd>` : ''}
69
+ </dl>
70
+ </section>`
71
+ : '';
72
+
73
+ return `<article itemscope itemtype="https://schema.org/Person">
74
+ <header>
75
+ <h1 itemprop="name">${esc(name)}</h1>
76
+ <p itemprop="jobTitle">${esc(p.jobTitle)}</p>
77
+ </header>
78
+ ${address}
79
+ ${bio}
80
+ ${specialties}
81
+ ${languages}
82
+ ${contact}
83
+ ${business}
84
+ </article>`;
85
+ }
@@ -0,0 +1,26 @@
1
+ import { esc, fullName, type TherapyProfile } from './helpers.ts';
2
+
3
+ export function profileCard(p: TherapyProfile, href: string): string {
4
+ const name = fullName(p);
5
+ const initials = `${(p.firstName?.[0] ?? '').toUpperCase()}${(p.lastName?.[0] ?? '').toUpperCase()}`;
6
+
7
+ const city = p.city ? `<span itemprop="address" itemscope itemtype="https://schema.org/PostalAddress"><span itemprop="addressLocality">${esc(p.city)}</span></span>` : '';
8
+
9
+ const bio = p.bio ? `<p itemprop="description">${esc(p.bio)}</p>` : '';
10
+
11
+ const specs = p.specialties.length > 0
12
+ ? `<ul>${p.specialties.slice(0, 3).map(s => `<li>${esc(s)}</li>`).join('')}</ul>`
13
+ : '';
14
+
15
+ return `<article itemscope itemtype="https://schema.org/Person">
16
+ <div aria-hidden="true">${initials}</div>
17
+ <header>
18
+ <h2><a href="${esc(href)}" itemprop="url"><span itemprop="name">${esc(name)}</span></a></h2>
19
+ <p><span itemprop="jobTitle">${esc(p.jobTitle)}</span></p>
20
+ ${city ? `<p>${city}</p>` : ''}
21
+ </header>
22
+ ${bio}
23
+ ${specs}
24
+ <a href="${esc(href)}">Zobacz profil &rarr;</a>
25
+ </article>`;
26
+ }