moracarta 2.1.3 → 2.1.4

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/index.html CHANGED
@@ -1,31 +1,35 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
6
- <meta name="robots" content="noindex, nofollow, noarchive">
7
- <title data-variable="TITLE"></title>
8
- <link rel="preconnect" href="https://fonts.googleapis.com">
9
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
- <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300..700;1,300..700&family=EB+Garamond:ital,wght@0,400..800;1,400..800&family=Lora:ital,wght@0,400;1,400&family=Montserrat:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&family=Playfair+Display:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
11
- <link rel="stylesheet" href="src/styles/main.css">
12
- </head>
13
- <body>
14
- <div class="header">
15
- <div class="header-ornament">❧ ❦ ☙</div>
16
- <h1 data-variable="TITLE"></h1>
17
- <div class="header-line"></div>
18
- <p class="header-sub love-note" data-variable="TEXT_TOP"></p>
19
- </div>
20
-
21
- <div class="letter-list" id="letter-list"></div>
22
-
23
- <div class="footer">
24
- <div class="footer-line"></div>
25
- <p data-variable="TEXT_BOTTOM"></p>
26
- <p data-variable="YOUR_NAME"></p>
27
- </div>
28
-
29
- <script type="module" src="src/scripts/main.js"></script>
30
- <script type="module" src="src/scripts/templateBinding.js"></script>
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
6
+ <meta name="robots" content="noindex, nofollow, noarchive">
7
+ <title data-variable="TITLE"></title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300..700;1,300..700&family=EB+Garamond:ital,wght@0,400..800;1,400..800&family=Lora:ital,wght@0,400;1,400&family=Montserrat:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&family=Playfair+Display:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
11
+ <link rel="stylesheet" href="src/styles/main.css">
12
+ </head>
13
+ <body>
14
+ <div class="header">
15
+ <div class="header-ornament">❧ ❦ ☙</div>
16
+ <h1 data-variable="TITLE"></h1>
17
+ <div class="header-line"></div>
18
+ <p class="header-sub love-note" data-variable="TEXT_TOP"></p>
19
+ </div>
20
+
21
+ <div class="letter-list" id="letter-list"></div>
22
+
23
+ <div class="bottom-text">
24
+ <div class="bottom-text-line"></div>
25
+ <p data-variable="TEXT_BOTTOM"></p>
26
+ <p data-variable="YOUR_NAME"></p>
27
+ </div>
28
+
29
+ <footer class="footer">
30
+ <a id="footer-link" class="footer-link" href="https://github.com/wesleyhanauer/moracarta"></a>
31
+ </footer>
32
+
33
+ <script type="module" src="src/scripts/main.js"></script>
34
+ <script type="module" src="src/scripts/templateBinding.js"></script>
31
35
  </html>
package/package.json CHANGED
@@ -1,21 +1,26 @@
1
1
  {
2
2
  "name": "moracarta",
3
- "version": "2.1.3",
3
+ "version": "2.1.4",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "license": "MIT",
7
7
  "main": "index.js",
8
8
  "keywords": [
9
- "cli",
10
- "scaffold",
11
- "static-site",
12
- "cloudflare",
13
- "wrangler",
14
- "gift",
15
- "envelope",
16
- "letters",
9
+ "love letter",
10
+ "digital love letter",
11
+ "romantic website",
12
+ "love website",
17
13
  "romantic",
18
- "html-css-js"
14
+ "love",
15
+ "letter",
16
+ "valentines",
17
+ "valentines day",
18
+ "anniversary",
19
+ "romantic website template",
20
+ "website template",
21
+ "static website",
22
+ "npm",
23
+ "open source"
19
24
  ],
20
25
  "files": [
21
26
  "index.html",
@@ -1,8 +1,4 @@
1
- import {
2
- select,
3
- input,
4
- confirm
5
- } from "@inquirer/prompts";
1
+ import { select, input, confirm } from "@inquirer/prompts";
6
2
  import { writeFile, mkdir, cp, readFile } from "node:fs/promises";
7
3
  import { resolve, dirname } from "node:path";
8
4
  import { fileURLToPath } from "node:url";
@@ -17,12 +13,12 @@ const PROJECT_NAME = await input({
17
13
  });
18
14
 
19
15
  const TITLE = await input({
20
- message: "Enter the title of the application:",
16
+ message: "Enter the title of the application: ",
21
17
  default: "Letters to my love"
22
18
  });
23
19
 
24
20
  const FONT = await select({
25
- message: "What font would you like to use?",
21
+ message: "What font style would you like to use?",
26
22
  choices: [
27
23
  { name: "Handwritten", value: "handwritten" },
28
24
  { name: "Classic", value: "classic" },
@@ -102,9 +98,15 @@ const DEFAULT_CLOSED_LETTER_TEXT_TOP_LINE =
102
98
  const DEFAULT_CLOSED_LETTER_TEXT_BOTTOM_LINE =
103
99
  "&#10084;&#65039; I love you &#10084;&#65039;";
104
100
 
105
- const EDIT_ENVELOPE_TEXT = await confirm({
106
- message: "Would you like to customize the envelope text?",
107
- default: false
101
+ const EDIT_ENVELOPE_TEXT = await select({
102
+ message: "Would you like to customize the envelope text? ",
103
+ choices: [{
104
+ name: "yes",
105
+ value: true
106
+ },{
107
+ name: "no",
108
+ value: false
109
+ }],
108
110
  });
109
111
 
110
112
  let CLOSED_LETTER_TEXT_TOP_LINE = DEFAULT_CLOSED_LETTER_TEXT_TOP_LINE;
@@ -122,6 +124,17 @@ if (EDIT_ENVELOPE_TEXT) {
122
124
  });
123
125
  }
124
126
 
127
+ const SHOW_BRANDING = await select({
128
+ message: "Would you like to display 'Made with 💌 Moracarta' branding in the footer? ",
129
+ choices: [{
130
+ name: "yes",
131
+ value: true
132
+ },{
133
+ name: "no",
134
+ value: false
135
+ }],
136
+ });
137
+
125
138
  const config = `/**
126
139
  * This file was generated automatically by Moracarta setup.
127
140
  */
@@ -141,6 +154,8 @@ const CLOSED_LETTER_TEXT_TOP_LINE =
141
154
  const CLOSED_LETTER_TEXT_BOTTOM_LINE =
142
155
  ${JSON.stringify(CLOSED_LETTER_TEXT_BOTTOM_LINE)};
143
156
 
157
+ const SHOW_BRANDING = ${SHOW_BRANDING};
158
+
144
159
  const globalVariables = {
145
160
  PROJECT_NAME,
146
161
  TITLE,
@@ -149,7 +164,8 @@ const globalVariables = {
149
164
  TEXT_TOP,
150
165
  TEXT_BOTTOM,
151
166
  CLOSED_LETTER_TEXT_TOP_LINE,
152
- CLOSED_LETTER_TEXT_BOTTOM_LINE
167
+ CLOSED_LETTER_TEXT_BOTTOM_LINE,
168
+ SHOW_BRANDING
153
169
  };
154
170
 
155
171
  export default globalVariables;
@@ -1,3 +1,12 @@
1
+ // This file is the placeholder for the letters, this one is used until the user
2
+ // runs 'npx moracarta add' for the first time, then letters.js is created.
3
+ // This file is also usefull in the live showcase since it's built together
4
+ // with the rest of the app.
5
+
6
+ // Each different letter is a different object inside const letters.
7
+ // Color palettes are generated from the id, date and title.
8
+ // Since they are generated from a hashed string letters with
9
+ // the same information will always generate the same colors.
1
10
  export const letters = [
2
11
  {
3
12
  id: 1,
@@ -1,3 +1,9 @@
1
+ // This file checks wheter or not src/content/letters.js exists, if not
2
+ // it loads letters.example.js as the letter provider file.
3
+ // It is done this way so that the user doesn't have to manually remove the
4
+ // placeholder example letters before adding his own letters.
5
+
6
+ // Uses meta.glob to check files with given name.
1
7
  const modules = import.meta.glob("../content/letters*.js", {
2
8
  eager: true,
3
9
  import: "letters",
@@ -0,0 +1,51 @@
1
+ // A set of base color families used to generate a unique
2
+ // envelope palette for each letter based on its id, title, and date.
3
+ // Same code as lettersPage.js so that the same color is applied on both
4
+ // main page and the envelope opening animation.
5
+ const paletteFamilies = [
6
+ { hue: 18, sat: 54, light: 88, sealHue: 345, sealSat: 76, sealLight: 42 },
7
+ { hue: 26, sat: 48, light: 86, sealHue: 12, sealSat: 72, sealLight: 38 },
8
+ { hue: 4, sat: 50, light: 84, sealHue: 5, sealSat: 78, sealLight: 34 },
9
+ { hue: 38, sat: 42, light: 90, sealHue: 24, sealSat: 66, sealLight: 36 },
10
+ { hue: 8, sat: 58, light: 86, sealHue: 6, sealSat: 82, sealLight: 30 },
11
+ { hue: 12, sat: 50, light: 88, sealHue: 11, sealSat: 74, sealLight: 33 },
12
+ { hue: 22, sat: 46, light: 92, sealHue: 20, sealSat: 68, sealLight: 40 },
13
+ { hue: 16, sat: 60, light: 84, sealHue: 5, sealSat: 86, sealLight: 32 },
14
+ { hue: 28, sat: 52, light: 90, sealHue: 15, sealSat: 70, sealLight: 38 },
15
+ { hue: 14, sat: 44, light: 88, sealHue: 8, sealSat: 80, sealLight: 36 }
16
+ ];
17
+
18
+ // Simple hash function to determine which color palette to use on the envelope.
19
+ function hashString(value) {
20
+ let hash = 0;
21
+ for (let index = 0; index < value.length; index += 1) {
22
+ hash = ((hash << 5) - hash) + value.charCodeAt(index);
23
+ hash |= 0;
24
+ }
25
+ return Math.abs(hash);
26
+ }
27
+
28
+ // Keeps color value within a set range.
29
+ function clamp(value, min, max) {
30
+ return Math.min(Math.max(value, min), max);
31
+ }
32
+
33
+ // Build a deterministic palette based on the letter's identity.
34
+ // The base hash selects one of the palette families, while the variant
35
+ // arrays add subtle shifts to keep similar letters visually distinct.
36
+ export default function generatePalette(letter) {
37
+ const baseHash = hashString(`${letter.id}|${letter.title}|${letter.date}`);
38
+ const family = paletteFamilies[baseHash % paletteFamilies.length];
39
+ const variant = Math.floor(baseHash / paletteFamilies.length) % 7;
40
+ const hueShift = [0, 14, -12, 20, -18, 26, -22][variant];
41
+ const satShift = [0, 8, -6, 12, -10, 14, -8][variant];
42
+ const lightShift = [0, -8, 8, -5, 6, -10, 10][variant];
43
+ const envelope = `hsl(${clamp(family.hue + hueShift, 0, 360)}, ${clamp(family.sat + satShift, 42, 72)}%, ${clamp(family.light + lightShift, 78, 92)}%)`;
44
+ const flap = `hsl(${clamp(family.hue + hueShift + 2, 0, 360)}, ${clamp(family.sat + satShift + 8, 50, 82)}%, ${clamp(family.light - 18 + lightShift, 60, 76)}%)`;
45
+ const shadow = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 12, 30, 58)}%, ${clamp(family.light - 36 + lightShift, 42, 58)}%)`;
46
+ const base = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 10, 30, 58)}%, ${clamp(family.light - 30 + lightShift, 44, 62)}%)`;
47
+ const seal = `hsl(${clamp(family.sealHue + hueShift, 0, 360)}, ${clamp(family.sealSat + satShift, 68, 92)}%, ${clamp(family.sealLight + lightShift * 0.8, 28, 46)}%)`;
48
+ const text = `hsl(${family.hue}, 22%, 18%)`;
49
+
50
+ return { envelope, flap, shadow, base, seal, text };
51
+ }
@@ -1,72 +1,22 @@
1
1
  import { letters } from '../loaders/lettersLoader.js';
2
+ import generatePalette from './generatePalette.js';
2
3
 
3
- console.log('letters.script loaded');
4
-
4
+ // Checks the URL to see what ID is select.
5
+ // Then loads letter with that same ID.
5
6
  const params = new URLSearchParams(window.location.search);
6
7
  const requestedId = parseInt(params.get('id'), 10);
7
8
  const letter = letters.find((entry) => entry.id === requestedId);
8
9
 
9
- // A set of base color families used to generate a unique yet consistent
10
- // envelope palette for each letter based on its id, title, and date.
11
- const paletteFamilies = [
12
- { hue: 18, sat: 54, light: 88, sealHue: 345, sealSat: 76, sealLight: 42 },
13
- { hue: 26, sat: 48, light: 86, sealHue: 12, sealSat: 72, sealLight: 38 },
14
- { hue: 4, sat: 50, light: 84, sealHue: 5, sealSat: 78, sealLight: 34 },
15
- { hue: 38, sat: 42, light: 90, sealHue: 24, sealSat: 66, sealLight: 36 },
16
- { hue: 8, sat: 58, light: 86, sealHue: 6, sealSat: 82, sealLight: 30 },
17
- { hue: 12, sat: 50, light: 88, sealHue: 11, sealSat: 74, sealLight: 33 },
18
- { hue: 22, sat: 46, light: 92, sealHue: 20, sealSat: 68, sealLight: 40 },
19
- { hue: 16, sat: 60, light: 84, sealHue: 5, sealSat: 86, sealLight: 32 },
20
- { hue: 28, sat: 52, light: 90, sealHue: 15, sealSat: 70, sealLight: 38 },
21
- { hue: 14, sat: 44, light: 88, sealHue: 8, sealSat: 80, sealLight: 36 }
22
- ];
23
-
24
- function hashString(value) {
25
- // Simple deterministic string hash used to select a palette and variant.
26
- // This keeps each letter appearance stable across page loads.
27
- let hash = 0;
28
- for (let index = 0; index < value.length; index += 1) {
29
- hash = ((hash << 5) - hash) + value.charCodeAt(index);
30
- hash |= 0;
31
- }
32
- return Math.abs(hash);
33
- }
34
-
35
- function clamp(value, min, max) {
36
- return Math.min(Math.max(value, min), max);
37
- }
38
-
39
- function generatePalette(letterItem) {
40
- // Build a deterministic palette based on the letter's identity.
41
- // The base hash selects one of the palette families, while the variant
42
- // arrays add subtle shifts to keep similar letters visually distinct.
43
- const baseHash = hashString(`${letterItem.id}|${letterItem.title}|${letterItem.date}`);
44
- const family = paletteFamilies[baseHash % paletteFamilies.length];
45
- const variant = Math.floor(baseHash / paletteFamilies.length) % 7;
46
- const hueShift = [0, 14, -12, 20, -18, 26, -22][variant];
47
- const satShift = [0, 8, -6, 12, -10, 14, -8][variant];
48
- const lightShift = [0, -8, 8, -5, 6, -10, 10][variant];
49
-
50
- const envelope = `hsl(${clamp(family.hue + hueShift, 0, 360)}, ${clamp(family.sat + satShift, 42, 72)}%, ${clamp(family.light + lightShift, 78, 92)}%)`;
51
- const flap = `hsl(${clamp(family.hue + hueShift + 2, 0, 360)}, ${clamp(family.sat + satShift + 8, 50, 82)}%, ${clamp(family.light - 18 + lightShift, 60, 76)}%)`;
52
- const shadow = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 12, 30, 58)}%, ${clamp(family.light - 36 + lightShift, 42, 58)}%)`;
53
- const base = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 10, 30, 58)}%, ${clamp(family.light - 30 + lightShift, 44, 62)}%)`;
54
- const seal = `hsl(${clamp(family.sealHue + hueShift, 0, 360)}, ${clamp(family.sealSat + satShift, 68, 92)}%, ${clamp(family.sealLight + lightShift * 0.8, 28, 46)}%)`;
55
- const text = `hsl(${family.hue}, 22%, 18%)`;
56
-
57
- return { envelope, flap, shadow, base, seal, text };
58
- }
59
-
60
10
  function applyEnvelopePalette(letterItem) {
61
11
  const palette = generatePalette(letterItem);
62
- document.querySelector('.env-fundo').style.background = palette.envelope;
12
+ document.querySelector('.env-background').style.background = palette.envelope;
63
13
 
64
- const flapPolygon = document.querySelector('.env-aba svg polygon');
14
+ const flapPolygon = document.querySelector('.env-flap svg polygon');
65
15
  if (flapPolygon) {
66
16
  flapPolygon.setAttribute('fill', palette.flap);
67
17
  }
68
18
 
69
- const sidePolygons = document.querySelectorAll('.env-corpo-svg svg polygon');
19
+ const sidePolygons = document.querySelectorAll('.env-body-svg svg polygon');
70
20
  if (sidePolygons.length === 3) {
71
21
  sidePolygons[0].setAttribute('fill', palette.shadow);
72
22
  sidePolygons[1].setAttribute('fill', palette.shadow);
@@ -136,10 +86,10 @@ if (!letter) {
136
86
  }, 2100);
137
87
  }
138
88
 
139
- function startHeartAnimation() {
140
89
  // Floating heart particle animation for the letter view background.
141
90
  // Particles are born after a delay, float upward, fade at the top, and die
142
91
  // when they leave the canvas to keep the animation visually light.
92
+ function startHeartAnimation() {
143
93
  const canvas = document.getElementById('canvas-hearts');
144
94
  const context = canvas.getContext('2d');
145
95
  const devicePixelRatio = window.devicePixelRatio || 1;
@@ -251,6 +201,5 @@ function startHeartAnimation() {
251
201
  context.clearRect(0, 0, width, height);
252
202
  }
253
203
  }
254
-
255
204
  window.requestAnimationFrame(animate);
256
205
  }
@@ -1,51 +1,9 @@
1
1
  import { letters } from '../loaders/lettersLoader.js';
2
+ import globalVariables from '../config/globalVariables.js';
3
+ import generatePalette from './generatePalette.js';
2
4
 
3
5
  const listContainer = document.getElementById('letter-list');
4
6
 
5
- const paletteFamilies = [
6
- { hue: 18, sat: 54, light: 88, sealHue: 345, sealSat: 76, sealLight: 42 },
7
- { hue: 26, sat: 48, light: 86, sealHue: 12, sealSat: 72, sealLight: 38 },
8
- { hue: 4, sat: 50, light: 84, sealHue: 5, sealSat: 78, sealLight: 34 },
9
- { hue: 38, sat: 42, light: 90, sealHue: 24, sealSat: 66, sealLight: 36 },
10
- { hue: 8, sat: 58, light: 86, sealHue: 6, sealSat: 82, sealLight: 30 },
11
- { hue: 12, sat: 50, light: 88, sealHue: 11, sealSat: 74, sealLight: 33 },
12
- { hue: 22, sat: 46, light: 92, sealHue: 20, sealSat: 68, sealLight: 40 },
13
- { hue: 16, sat: 60, light: 84, sealHue: 5, sealSat: 86, sealLight: 32 },
14
- { hue: 28, sat: 52, light: 90, sealHue: 15, sealSat: 70, sealLight: 38 },
15
- { hue: 14, sat: 44, light: 88, sealHue: 8, sealSat: 80, sealLight: 36 }
16
- ];
17
-
18
- function hashString(value) {
19
- let hash = 0;
20
- for (let index = 0; index < value.length; index += 1) {
21
- hash = ((hash << 5) - hash) + value.charCodeAt(index);
22
- hash |= 0;
23
- }
24
- return Math.abs(hash);
25
- }
26
-
27
- function clamp(value, min, max) {
28
- return Math.min(Math.max(value, min), max);
29
- }
30
-
31
- function generatePalette(letter) {
32
- const baseHash = hashString(`${letter.id}|${letter.title}|${letter.date}`);
33
- const family = paletteFamilies[baseHash % paletteFamilies.length];
34
- const variant = Math.floor(baseHash / paletteFamilies.length) % 7;
35
- const hueShift = [0, 14, -12, 20, -18, 26, -22][variant];
36
- const satShift = [0, 8, -6, 12, -10, 14, -8][variant];
37
- const lightShift = [0, -8, 8, -5, 6, -10, 10][variant];
38
-
39
- const envelope = `hsl(${clamp(family.hue + hueShift, 0, 360)}, ${clamp(family.sat + satShift, 42, 72)}%, ${clamp(family.light + lightShift, 78, 92)}%)`;
40
- const flap = `hsl(${clamp(family.hue + hueShift + 2, 0, 360)}, ${clamp(family.sat + satShift + 8, 50, 82)}%, ${clamp(family.light - 18 + lightShift, 60, 76)}%)`;
41
- const shadow = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 12, 30, 58)}%, ${clamp(family.light - 36 + lightShift, 42, 58)}%)`;
42
- const base = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 10, 30, 58)}%, ${clamp(family.light - 30 + lightShift, 44, 62)}%)`;
43
- const seal = `hsl(${clamp(family.sealHue + hueShift, 0, 360)}, ${clamp(family.sealSat + satShift, 68, 92)}%, ${clamp(family.sealLight + lightShift * 0.8, 28, 46)}%)`;
44
- const text = `hsl(${family.hue}, 22%, 18%)`;
45
-
46
- return { envelope, flap, shadow, base, seal, text };
47
- }
48
-
49
7
  function renderLetters() {
50
8
  if (!listContainer) {
51
9
  return;
@@ -59,11 +17,12 @@ function renderLetters() {
59
17
  link.href = `./src/views/letters.html?id=${letter.id}`;
60
18
  link.className = 'envelope-link';
61
19
 
20
+ // Creates a new envelope object for each loaded letter.
62
21
  link.innerHTML = `
63
22
  <div class="envelope-wrap">
64
- <div class="envelope-sombra"></div>
65
- <div class="envelope" style="background:${palette.envelope}; --envelope-sombra:${palette.shadow};">
66
- <div class="env-corpo-svg">
23
+ <div class="envelope-shadow"></div>
24
+ <div class="envelope" style="background:${palette.envelope}; --envelope-shadow:${palette.shadow};">
25
+ <div class="env-body-svg">
67
26
  <svg viewBox="0 0 100 63" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
68
27
  <polygon points="0,0 0,63 50,31" fill="${palette.shadow}" />
69
28
  <polygon points="100,0 100,63 50,31" fill="${palette.shadow}" />
@@ -71,22 +30,22 @@ function renderLetters() {
71
30
  </svg>
72
31
  </div>
73
32
 
74
- <div class="env-aba" style="border-top-color:${palette.flap};">
33
+ <div class="env-flap" style="border-top-color:${palette.flap};">
75
34
  <svg viewBox="0 0 100 44" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
76
35
  <polygon points="0,0 100,0 50,44" fill="${palette.flap}" />
77
36
  </svg>
78
- <div class="env-lacre" style="background:${palette.seal};">♡</div>
37
+ <div class="env-seal" style="background:${palette.seal};">♡</div>
79
38
  </div>
80
39
 
81
- <div class="envelope-centro">
82
- <span class="envelope-data" style="color:${palette.text};">${letter.date}</span>
83
- <span class="envelope-titulo" style="color:${palette.text};">${letter.title}</span>
40
+ <div class="envelope-center">
41
+ <span class="envelope-date" style="color:${palette.text};">${letter.date}</span>
42
+ <span class="envelope-title" style="color:${palette.text};">${letter.title}</span>
84
43
  <span class="envelope-hint" style="color:${palette.text};">Tap to open</span>
85
44
  </div>
86
- <div class="envelope-canto tl"></div>
87
- <div class="envelope-canto tr"></div>
88
- <div class="envelope-canto bl"></div>
89
- <div class="envelope-canto br"></div>
45
+ <div class="envelope-corner tl"></div>
46
+ <div class="envelope-corner tr"></div>
47
+ <div class="envelope-corner bl"></div>
48
+ <div class="envelope-corner br"></div>
90
49
  </div>
91
50
  </div>
92
51
  `;
@@ -97,3 +56,7 @@ function renderLetters() {
97
56
 
98
57
  renderLetters();
99
58
 
59
+ if(globalVariables.SHOW_BRANDING){
60
+ let footer = document.getElementById('footer-link');
61
+ footer.textContent = "Made with 💌 Moracarta";
62
+ }
@@ -1,6 +1,7 @@
1
1
  import globalVariables from '../config/globalVariables.js';
2
2
  import fonts from '../config/fonts.js';
3
3
 
4
+ // Sets the main font for the application
4
5
  document.documentElement.style.setProperty(
5
6
  '--main-font',
6
7
  fonts[globalVariables.FONT] || fonts.handwritten
@@ -8,17 +9,13 @@ document.documentElement.style.setProperty(
8
9
 
9
10
  // Template variables are injected from `src/config/globalVariables.js` into
10
11
  // HTML elements using the `data-variable` attribute.
11
- // HTML entities are preserved when the value contains markup characters.
12
12
  document.querySelectorAll("[data-variable]").forEach(element => {
13
13
  const value = globalVariables[element.dataset.variable];
14
14
 
15
+ //Checks if the value is to be interpreted as literal HTML or pure text
15
16
  if (typeof value === 'string' && (value.includes('&') || value.includes('<') || value.includes('>'))) {
16
17
  element.innerHTML = value;
17
18
  } else {
18
19
  element.textContent = value;
19
20
  }
20
- });
21
-
22
- document.querySelectorAll("[data-variable-src]").forEach(element => {
23
- element.src = globalVariables[element.dataset.variableSrc];
24
21
  });
@@ -74,7 +74,7 @@ body {
74
74
  opacity: 1;
75
75
  }
76
76
 
77
- .env-fundo {
77
+ .env-background {
78
78
  position: absolute;
79
79
  inset: 0;
80
80
  background: #e8d5b0;
@@ -82,7 +82,7 @@ body {
82
82
  z-index: 1;
83
83
  }
84
84
 
85
- .env-papel {
85
+ .env-paper {
86
86
  position: absolute;
87
87
  left: 11%;
88
88
  right: 11%;
@@ -97,7 +97,7 @@ body {
97
97
  overflow: hidden;
98
98
  }
99
99
 
100
- .env-papel.leaving {
100
+ .env-paper.leaving {
101
101
  transform: translateY(-120%);
102
102
  }
103
103
 
@@ -111,19 +111,19 @@ body {
111
111
  font-style: italic;
112
112
  }
113
113
 
114
- .env-corpo-svg {
114
+ .env-body-svg {
115
115
  position: absolute;
116
116
  inset: 0;
117
117
  z-index: 3;
118
118
  pointer-events: none;
119
119
  }
120
120
 
121
- .env-corpo-svg svg {
121
+ .env-body-svg svg {
122
122
  width: 100%;
123
123
  height: 100%;
124
124
  }
125
125
 
126
- .env-aba {
126
+ .env-flap {
127
127
  position: absolute;
128
128
  top: 0;
129
129
  left: 0;
@@ -136,20 +136,20 @@ body {
136
136
  overflow: visible;
137
137
  }
138
138
 
139
- .env-aba.open {
139
+ .env-flap.open {
140
140
  transform: rotateX(180deg);
141
141
  }
142
142
 
143
- .env-aba.behind {
143
+ .env-flap.behind {
144
144
  z-index: 0;
145
145
  }
146
146
 
147
- .env-aba svg {
147
+ .env-flap svg {
148
148
  width: 100%;
149
149
  height: 100%;
150
150
  }
151
151
 
152
- .env-lacre {
152
+ .env-seal {
153
153
  position: absolute;
154
154
  z-index: 5;
155
155
  width: 50px;
@@ -176,7 +176,7 @@ body {
176
176
  will-change: transform;
177
177
  }
178
178
 
179
- .env-lacre.fading {
179
+ .env-seal.fading {
180
180
  opacity: 0;
181
181
  transform: translate(-50%, -120%) scale(0.5);
182
182
  }
@@ -143,7 +143,7 @@ body {
143
143
  transform: scale(0.97) rotate(0deg);
144
144
  }
145
145
 
146
- .envelope-sombra {
146
+ .envelope-shadow {
147
147
  position: absolute;
148
148
  bottom: -6px;
149
149
  left: 6px;
@@ -180,20 +180,20 @@ body {
180
180
  background-repeat: no-repeat;
181
181
  }
182
182
 
183
- .env-corpo-svg {
183
+ .env-body-svg {
184
184
  position: absolute;
185
185
  inset: 0;
186
186
  z-index: 1;
187
187
  pointer-events: none;
188
188
  }
189
189
 
190
- .env-corpo-svg svg {
190
+ .env-body-svg svg {
191
191
  width: 100%;
192
192
  height: 100%;
193
193
  display: block;
194
194
  }
195
195
 
196
- .env-aba {
196
+ .env-flap {
197
197
  position: relative;
198
198
  z-index: 4;
199
199
  pointer-events: none;
@@ -202,13 +202,13 @@ body {
202
202
  width: 100%;
203
203
  }
204
204
 
205
- .env-aba svg {
205
+ .env-flap svg {
206
206
  width: 100%;
207
207
  height: 100%;
208
208
  display: block;
209
209
  }
210
210
 
211
- .env-lacre {
211
+ .env-seal {
212
212
  position: absolute;
213
213
  z-index: 5;
214
214
  width: 48px;
@@ -226,7 +226,7 @@ body {
226
226
  bottom: -24px;
227
227
  }
228
228
 
229
- .envelope-centro {
229
+ .envelope-center {
230
230
  position: relative;
231
231
  z-index: 2;
232
232
  padding: 12px 16px 16px;
@@ -235,7 +235,7 @@ body {
235
235
  gap: 2px;
236
236
  }
237
237
 
238
- .envelope-data {
238
+ .envelope-date {
239
239
  font-size: 10px;
240
240
  letter-spacing: 0.5px;
241
241
  text-transform: uppercase;
@@ -244,7 +244,7 @@ body {
244
244
  font-style: italic;
245
245
  }
246
246
 
247
- .envelope-titulo {
247
+ .envelope-title {
248
248
  font-family: 'Playfair Display', Georgia, serif;
249
249
  font-size: 16px;
250
250
  font-weight: 600;
@@ -267,7 +267,7 @@ body {
267
267
  color: var(--red);
268
268
  }
269
269
 
270
- .envelope-canto {
270
+ .envelope-corner {
271
271
  position: absolute;
272
272
  width: 20px;
273
273
  height: 20px;
@@ -276,25 +276,25 @@ body {
276
276
  opacity: 0.5;
277
277
  }
278
278
 
279
- .envelope-canto.tl {
279
+ .envelope-corner.tl {
280
280
  top: 6px;
281
281
  left: 6px;
282
282
  border-width: 1px 0 0 1px;
283
283
  }
284
284
 
285
- .envelope-canto.tr {
285
+ .envelope-corner.tr {
286
286
  top: 6px;
287
287
  right: 6px;
288
288
  border-width: 1px 1px 0 0;
289
289
  }
290
290
 
291
- .envelope-canto.bl {
291
+ .envelope-corner.bl {
292
292
  bottom: 6px;
293
293
  left: 6px;
294
294
  border-width: 0 0 1px 1px;
295
295
  }
296
296
 
297
- .envelope-canto.br {
297
+ .envelope-corner.br {
298
298
  bottom: 6px;
299
299
  right: 6px;
300
300
  border-width: 0 1px 1px 0;
@@ -310,7 +310,7 @@ body {
310
310
  }
311
311
  }
312
312
 
313
- .footer {
313
+ .bottom-text {
314
314
  text-align: center;
315
315
  margin-top: 40px;
316
316
  padding: 0 20px;
@@ -319,14 +319,14 @@ body {
319
319
  margin-right: auto;
320
320
  }
321
321
 
322
- .footer-line {
322
+ .bottom-text-line {
323
323
  width: 60px;
324
324
  height: 1px;
325
325
  background: linear-gradient(to right, transparent, #6b4c2a, transparent);
326
326
  margin: 0 auto 16px;
327
327
  }
328
328
 
329
- .footer p {
329
+ .bottom-text p {
330
330
  font-size: 12px;
331
331
  color: #6b4c2a;
332
332
  font-style: italic;
@@ -335,4 +335,13 @@ body {
335
335
  text-align: justify;
336
336
  }
337
337
 
338
-
338
+ .footer{
339
+ padding-top: 11vh;
340
+ display: flex;
341
+ justify-content: center;
342
+ }
343
+
344
+ .footer-link{
345
+ text-decoration: none;
346
+ color: rgba(176, 53, 16, 0.571);
347
+ }
@@ -23,16 +23,16 @@
23
23
 
24
24
  <div class="envelope-animation-wrap" id="envelope-wrap">
25
25
  <div class="envelope-stage">
26
- <div class="env-fundo"></div>
26
+ <div class="env-background"></div>
27
27
 
28
- <div class="env-papel" id="paper">
28
+ <div class="env-paper" id="paper">
29
29
  <div class="paper-text">
30
30
  <p data-variable="CLOSED_LETTER_TEXT_TOP_LINE"></p><br>
31
31
  <p data-variable="CLOSED_LETTER_TEXT_BOTTOM_LINE"></p>
32
32
  </div>
33
33
  </div>
34
34
 
35
- <div class="env-corpo-svg">
35
+ <div class="env-body-svg">
36
36
  <svg viewBox="0 0 100 63" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
37
37
  <polygon points="0,0 0,63 47,42" fill="#c9a87c" />
38
38
  <polygon points="100,0 100,63 53,42" fill="#c4a07a" />
@@ -40,13 +40,13 @@
40
40
  </svg>
41
41
  </div>
42
42
 
43
- <div class="env-aba" id="flap">
43
+ <div class="env-flap" id="flap">
44
44
  <svg viewBox="0 0 100 44" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
45
45
  <polygon points="0,0 100,0 50,44" fill="#d4b896" />
46
46
  </svg>
47
47
  </div>
48
48
 
49
- <div class="env-lacre" id="seal"><span class="seal-heart" aria-hidden="true">&#9825;</span></div>
49
+ <div class="env-seal" id="seal"><span class="seal-heart" aria-hidden="true">&#9825;</span></div>
50
50
  </div>
51
51
  </div>
52
52
 
@@ -76,7 +76,7 @@
76
76
  <span class="nav-counter" id="letter-counter"></span>
77
77
  <a id="nav-next" class="nav-button invisible" href="#"><p>Next</p> &#8594;</a>
78
78
  </nav>
79
-
79
+
80
80
  <script type="module" src="../scripts/lettersPage.js"></script>
81
81
  <script type="module" src="../scripts/templateBinding.js"></script>
82
82
  </html>
@@ -1,256 +0,0 @@
1
- import { letters } from '../loaders/lettersLoader.js';
2
-
3
- console.log('letters.script loaded');
4
-
5
- const params = new URLSearchParams(window.location.search);
6
- const requestedId = parseInt(params.get('id'), 10);
7
- const letter = letters.find((entry) => entry.id === requestedId);
8
-
9
- // A set of base color families used to generate a unique yet consistent
10
- // envelope palette for each letter based on its id, title, and date.
11
- const paletteFamilies = [
12
- { hue: 18, sat: 54, light: 88, sealHue: 345, sealSat: 76, sealLight: 42 },
13
- { hue: 26, sat: 48, light: 86, sealHue: 12, sealSat: 72, sealLight: 38 },
14
- { hue: 4, sat: 50, light: 84, sealHue: 5, sealSat: 78, sealLight: 34 },
15
- { hue: 38, sat: 42, light: 90, sealHue: 24, sealSat: 66, sealLight: 36 },
16
- { hue: 8, sat: 58, light: 86, sealHue: 6, sealSat: 82, sealLight: 30 },
17
- { hue: 12, sat: 50, light: 88, sealHue: 11, sealSat: 74, sealLight: 33 },
18
- { hue: 22, sat: 46, light: 92, sealHue: 20, sealSat: 68, sealLight: 40 },
19
- { hue: 16, sat: 60, light: 84, sealHue: 5, sealSat: 86, sealLight: 32 },
20
- { hue: 28, sat: 52, light: 90, sealHue: 15, sealSat: 70, sealLight: 38 },
21
- { hue: 14, sat: 44, light: 88, sealHue: 8, sealSat: 80, sealLight: 36 }
22
- ];
23
-
24
- function hashString(value) {
25
- // Simple deterministic string hash used to select a palette and variant.
26
- // This keeps each letter appearance stable across page loads.
27
- let hash = 0;
28
- for (let index = 0; index < value.length; index += 1) {
29
- hash = ((hash << 5) - hash) + value.charCodeAt(index);
30
- hash |= 0;
31
- }
32
- return Math.abs(hash);
33
- }
34
-
35
- function clamp(value, min, max) {
36
- return Math.min(Math.max(value, min), max);
37
- }
38
-
39
- function generatePalette(letterItem) {
40
- // Build a deterministic palette based on the letter's identity.
41
- // The base hash selects one of the palette families, while the variant
42
- // arrays add subtle shifts to keep similar letters visually distinct.
43
- const baseHash = hashString(`${letterItem.id}|${letterItem.title}|${letterItem.date}`);
44
- const family = paletteFamilies[baseHash % paletteFamilies.length];
45
- const variant = Math.floor(baseHash / paletteFamilies.length) % 7;
46
- const hueShift = [0, 14, -12, 20, -18, 26, -22][variant];
47
- const satShift = [0, 8, -6, 12, -10, 14, -8][variant];
48
- const lightShift = [0, -8, 8, -5, 6, -10, 10][variant];
49
-
50
- const envelope = `hsl(${clamp(family.hue + hueShift, 0, 360)}, ${clamp(family.sat + satShift, 42, 72)}%, ${clamp(family.light + lightShift, 78, 92)}%)`;
51
- const flap = `hsl(${clamp(family.hue + hueShift + 2, 0, 360)}, ${clamp(family.sat + satShift + 8, 50, 82)}%, ${clamp(family.light - 18 + lightShift, 60, 76)}%)`;
52
- const shadow = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 12, 30, 58)}%, ${clamp(family.light - 36 + lightShift, 42, 58)}%)`;
53
- const base = `hsl(${clamp(family.hue + hueShift + 8, 0, 360)}, ${clamp(family.sat + satShift - 10, 30, 58)}%, ${clamp(family.light - 30 + lightShift, 44, 62)}%)`;
54
- const seal = `hsl(${clamp(family.sealHue + hueShift, 0, 360)}, ${clamp(family.sealSat + satShift, 68, 92)}%, ${clamp(family.sealLight + lightShift * 0.8, 28, 46)}%)`;
55
- const text = `hsl(${family.hue}, 22%, 18%)`;
56
-
57
- return { envelope, flap, shadow, base, seal, text };
58
- }
59
-
60
- function applyEnvelopePalette(letterItem) {
61
- const palette = generatePalette(letterItem);
62
- document.querySelector('.env-fundo').style.background = palette.envelope;
63
-
64
- const flapPolygon = document.querySelector('.env-aba svg polygon');
65
- if (flapPolygon) {
66
- flapPolygon.setAttribute('fill', palette.flap);
67
- }
68
-
69
- const sidePolygons = document.querySelectorAll('.env-corpo-svg svg polygon');
70
- if (sidePolygons.length === 3) {
71
- sidePolygons[0].setAttribute('fill', palette.shadow);
72
- sidePolygons[1].setAttribute('fill', palette.shadow);
73
- sidePolygons[2].setAttribute('fill', palette.base);
74
- }
75
- }
76
-
77
- if (!letter) {
78
- document.getElementById('error-message').style.display = 'block';
79
- document.getElementById('envelope-wrap').style.display = 'none';
80
- document.getElementById('letter-wrap').style.display = 'none';
81
- document.getElementById('letter-nav').style.display = 'none';
82
- } else {
83
- document.title = letter.title;
84
-
85
- document.getElementById('letter-date').textContent = letter.date;
86
- document.getElementById('letter-title').textContent = letter.title;
87
- document.getElementById('letter-body').innerHTML = letter.content
88
- .split(/\n\s*\n/)
89
- .filter(paragraph => paragraph.trim())
90
- .map(paragraph => `<p>${paragraph.trimEnd()}</p>`)
91
- .join('');
92
-
93
- applyEnvelopePalette(letter);
94
- document.querySelector('.envelope-stage').classList.add('ready');
95
-
96
- const sortedLetters = letters.slice().sort((first, second) => first.id - second.id);
97
- const currentIndex = sortedLetters.findIndex((entry) => entry.id === requestedId);
98
- const previousLetter = currentIndex > 0 ? sortedLetters[currentIndex - 1] : null;
99
- const nextLetter = currentIndex < sortedLetters.length - 1 ? sortedLetters[currentIndex + 1] : null;
100
-
101
- document.getElementById('letter-counter').textContent = `${currentIndex + 1} Of ${sortedLetters.length}`;
102
-
103
- if (previousLetter) {
104
- const previousButton = document.getElementById('nav-previous');
105
- previousButton.href = `./letters.html?id=${previousLetter.id}`;
106
- previousButton.classList.remove('invisible');
107
- }
108
-
109
- if (nextLetter) {
110
- const nextButton = document.getElementById('nav-next');
111
- nextButton.href = `./letters.html?id=${nextLetter.id}`;
112
- nextButton.classList.remove('invisible');
113
- }
114
-
115
- startHeartAnimation();
116
-
117
- // Animate the envelope opening, sealing, and letter reveal in timed stages.
118
- window.setTimeout(() => {
119
- document.getElementById('flap').classList.add('open');
120
- document.getElementById('seal').classList.add('fading');
121
- }, 400);
122
-
123
- window.setTimeout(() => {
124
- document.getElementById('flap').classList.add('behind');
125
- }, 1050);
126
-
127
- window.setTimeout(() => {
128
- document.getElementById('paper').classList.add('leaving');
129
- }, 900);
130
-
131
- window.setTimeout(() => {
132
- document.getElementById('letter-wrap').classList.add('visible');
133
- document.getElementById('letter-nav').classList.add('visible');
134
- document.getElementById('envelope-wrap').style.display = 'none';
135
-
136
- }, 2100);
137
- }
138
-
139
- function startHeartAnimation() {
140
- // Floating heart particle animation for the letter view background.
141
- // Particles are born after a delay, float upward, fade at the top, and die
142
- // when they leave the canvas to keep the animation visually light.
143
- const canvas = document.getElementById('canvas-hearts');
144
- const context = canvas.getContext('2d');
145
- const devicePixelRatio = window.devicePixelRatio || 1;
146
-
147
- canvas.width = window.innerWidth * devicePixelRatio;
148
- canvas.height = window.innerHeight * devicePixelRatio;
149
- canvas.style.width = `${window.innerWidth}px`;
150
- canvas.style.height = `${window.innerHeight}px`;
151
- context.scale(devicePixelRatio, devicePixelRatio);
152
-
153
- const width = window.innerWidth;
154
- const height = window.innerHeight;
155
- const colors = ['#c0392b', '#e74c3c', '#e91e63', '#ff4081', '#ad1457', '#f06292', '#ff6b6b', '#d63031', '#ff8a80'];
156
-
157
- function drawHeart(radius) {
158
- context.beginPath();
159
- context.moveTo(0, -radius * 0.3);
160
- context.bezierCurveTo(radius, -radius * 1.1, radius * 1.5, radius * 0.4, 0, radius);
161
- context.bezierCurveTo(-radius * 1.5, radius * 0.4, -radius, -radius * 1.1, 0, -radius * 0.3);
162
- context.closePath();
163
- }
164
-
165
- const totalParticles = 55;
166
- const particles = [];
167
-
168
- for (let index = 0; index < totalParticles; index += 1) {
169
- const angle = (Math.random() - 0.5) * (Math.PI / 1.5);
170
- const velocity = 1.2 + Math.random() * 2.2;
171
- const radius = 8 + Math.random() * 18;
172
-
173
- particles.push({
174
- x: width * 0.05 + Math.random() * width * 0.9,
175
- y: height + radius * 2,
176
- velocityX: Math.sin(angle) * velocity,
177
- velocityY: -Math.cos(angle) * velocity,
178
- radius,
179
- rotation: (Math.random() - 0.5) * 0.06,
180
- currentAngle: 0,
181
- color: colors[Math.floor(Math.random() * colors.length)],
182
- alpha: 0,
183
- delay: Math.random() * 2500,
184
- born: false,
185
- dead: false
186
- });
187
- }
188
-
189
- let startTime = null;
190
-
191
- function animate(timestamp) {
192
- if (!startTime) {
193
- startTime = timestamp;
194
- }
195
-
196
- const elapsed = timestamp - startTime;
197
- context.clearRect(0, 0, width, height);
198
-
199
- let allDead = true;
200
-
201
- particles.forEach((particle) => {
202
- if (elapsed < particle.delay) {
203
- allDead = false;
204
- return;
205
- }
206
-
207
- if (!particle.born) {
208
- particle.born = true;
209
- }
210
-
211
- if (particle.dead) {
212
- return;
213
- }
214
-
215
- allDead = false;
216
- particle.x += particle.velocityX;
217
- particle.y += particle.velocityY;
218
- particle.velocityY -= 0.012;
219
- particle.velocityX *= 0.998;
220
- particle.currentAngle += particle.rotation;
221
-
222
- const fraction = 1 - (particle.y / height);
223
- if (fraction < 0.08) {
224
- particle.alpha = fraction / 0.08;
225
- } else if (fraction > 0.55) {
226
- particle.alpha = Math.max(0, 1 - (fraction - 0.55) / 0.45);
227
- } else {
228
- particle.alpha = 1;
229
- }
230
-
231
- if (particle.y < -particle.radius * 3) {
232
- particle.dead = true;
233
- return;
234
- }
235
-
236
- context.save();
237
- context.translate(particle.x, particle.y);
238
- context.rotate(particle.currentAngle);
239
- context.globalAlpha = particle.alpha;
240
- context.shadowColor = particle.color;
241
- context.shadowBlur = 6;
242
- context.fillStyle = particle.color;
243
- drawHeart(particle.radius);
244
- context.fill();
245
- context.restore();
246
- });
247
-
248
- if (!allDead) {
249
- window.requestAnimationFrame(animate);
250
- } else {
251
- context.clearRect(0, 0, width, height);
252
- }
253
- }
254
-
255
- window.requestAnimationFrame(animate);
256
- }