create-berna-stencil 2.1.0 → 2.2.1

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/.eleventy.js CHANGED
@@ -24,9 +24,6 @@ module.exports = function (eleventyConfig) {
24
24
  }
25
25
  }
26
26
 
27
- // =====================================================
28
- // SHORTCODE — Markdown file renderer
29
- // =====================================================
30
27
  const md = markdownIt({ html: true });
31
28
 
32
29
  eleventyConfig.addShortcode('mdFile', function(filePath) {
@@ -34,38 +31,21 @@ module.exports = function (eleventyConfig) {
34
31
  return md.render(content);
35
32
  });
36
33
 
37
- // =====================================================
38
- // SHORTCODE — Markdown css
39
- // =====================================================
40
34
  eleventyConfig.addPassthroughCopy({
41
35
  "node_modules/github-markdown-css/github-markdown-dark.css": "css/github-markdown-dark.css",
42
36
  "node_modules/github-markdown-css/github-markdown-light.css": "css/github-markdown-light.css",
43
37
  });
44
38
 
45
- // =====================================================
46
- // ESBUILD — Bundles and minifies JS files before build
47
- // =====================================================
48
- eleventyConfig.on("eleventy.before", async () => {
49
- const entryPoints = glob.sync("src/frontend/js/pages/*.js");
50
- await esbuild.build({
51
- entryPoints,
52
- bundle: true,
53
- outdir: `${OUTPUT_DIR}/js/pages`,
54
- minify: true,
55
- });
39
+ eleventyConfig.on("eleventy.before", () => {
56
40
  copyRecursiveSync("src/backend", `${OUTPUT_DIR}/backend`);
57
41
  });
58
42
 
59
- // =====================================================
60
- // PASSTHROUGH — Static files
61
- // =====================================================
62
43
  eleventyConfig.addPassthroughCopy("src/frontend/.htaccess");
63
44
  eleventyConfig.addPassthroughCopy("src/frontend/web.config");
64
45
  eleventyConfig.addPassthroughCopy("src/frontend/assets");
65
46
  eleventyConfig.addPassthroughCopy("src/frontend/robots.txt");
66
47
 
67
48
  eleventyConfig.addPassthroughCopy({
68
- // Bootstrap
69
49
  "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js": "js/bootstrap.bundle.min.js",
70
50
  "node_modules/bootstrap-icons/font/fonts": "css/fonts",
71
51
 
@@ -79,9 +59,6 @@ module.exports = function (eleventyConfig) {
79
59
  // Bulma — CSS only, no JS passthrough needed
80
60
  });
81
61
 
82
- // =====================================================
83
- // ELEVENTY IMAGE — Responsive images
84
- // =====================================================
85
62
  eleventyConfig.addShortcode("image", async function (src, alt) {
86
63
  let metadata = await Image(src, {
87
64
  widths: [320, 480, 720, 1280, 1920, 2048, 2560, 3840, 4096, 7680],
@@ -98,11 +75,12 @@ module.exports = function (eleventyConfig) {
98
75
  });
99
76
  });
100
77
 
101
- // =====================================================
102
- // WATCH & DIRECTORY CONFIG
103
- // =====================================================
104
78
  eleventyConfig.addWatchTarget("./src/frontend/scss");
105
79
 
80
+ eleventyConfig.setServerOptions({
81
+ watch: [`${OUTPUT_DIR}/js/**/*.js`]
82
+ });
83
+
106
84
  return {
107
85
  dir: {
108
86
  input: "src/frontend",
package/README.md CHANGED
@@ -12,7 +12,7 @@ Building a website from scratch involves a lot of moving parts: templating engin
12
12
  - 📁 **Scalable structure** — a clean, opinionated project layout that grows with your needs
13
13
  - 🌍 **Open source** — free to use, free to modify, free to share
14
14
 
15
- ![Version](https://img.shields.io/badge/version-2.1.0-blue)
15
+ ![Version](https://img.shields.io/badge/version-2.2.1-blue)
16
16
  ![License](https://img.shields.io/badge/license-Apache--2.0-blue)
17
17
  ![Eleventy](https://img.shields.io/badge/11ty-v3.1.2-black)
18
18
 
@@ -0,0 +1,28 @@
1
+ const esbuild = require("esbuild");
2
+ const glob = require("glob");
3
+ const path = require("path");
4
+ const pkg = require("../package.json");
5
+
6
+ const isWatch = process.argv.includes("--watch");
7
+ const outputDir = pkg.outputDir || "out";
8
+
9
+ const jsFiles = glob.sync("src/frontend/js/pages/*.js");
10
+ const tsFiles = glob.sync("src/frontend/ts/pages/*.ts");
11
+ const entryPoints = [...jsFiles, ...tsFiles];
12
+
13
+ if (entryPoints.length === 0) {
14
+ process.exit(0);
15
+ }
16
+
17
+ const options = {
18
+ entryPoints,
19
+ bundle: true,
20
+ outdir: path.join(outputDir, "js/pages"),
21
+ minify: !isWatch,
22
+ };
23
+
24
+ if (isWatch) {
25
+ esbuild.context(options).then(ctx => ctx.watch()).catch(() => process.exit(1));
26
+ } else {
27
+ esbuild.build(options).catch(() => process.exit(1));
28
+ }
@@ -1,24 +1,20 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
- const ELEVENTY_CONFIG = path.resolve(__dirname, '../.eleventy.js');
4
+ const PACKAGE_JSON = path.resolve(__dirname, '../package.json');
5
5
 
6
- const content = fs.readFileSync(ELEVENTY_CONFIG, 'utf-8');
7
- const match = content.match(/const OUTPUT_DIR\s*=\s*['"`]([^'"`]*)['"`]/);
6
+ const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf-8'));
8
7
 
9
- if (!match) {
10
- console.log('(!) OUTPUT_DIR not found in .eleventy.js');
8
+ if (!pkg.outputDir) {
9
+ console.log('(!) outputDir not found in package.json');
11
10
  process.exit(1);
12
11
  }
13
12
 
14
- const outputDir = match[1];
15
- const absPath = path.isAbsolute(outputDir)
16
- ? outputDir
17
- : path.resolve(__dirname, '..', outputDir);
13
+ const outputDir = path.resolve(__dirname, '..', pkg.outputDir);
18
14
 
19
- if (fs.existsSync(absPath)) {
20
- fs.rmSync(absPath, { recursive: true, force: true });
21
- console.log(`(✓) cleaned → ${absPath}`);
15
+ if (fs.existsSync(outputDir)) {
16
+ fs.rmSync(outputDir, { recursive: true, force: true });
17
+ console.log(`(✓) cleaned → ${outputDir}`);
22
18
  } else {
23
- console.log(`(i) nothing to clean → ${absPath}`);
19
+ console.log(`(i) nothing to clean → ${outputDir}`);
24
20
  }
@@ -2,20 +2,22 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
4
  const ELEVENTY_CONFIG = path.resolve(__dirname, '../../.eleventy.js');
5
- const PACKAGE_JSON = path.resolve(__dirname, '../../package.json');
5
+ const PACKAGE_JSON = path.resolve(__dirname, '../../package.json');
6
+ const TSCONFIG = path.resolve(__dirname, '../../tsconfig.json');
6
7
 
7
- // Regex to locate the OUTPUT_DIR declaration in .eleventy.js.
8
- // Extracted as a constant to avoid writing it by hand in multiple places.
9
8
  const OUTPUT_DIR_REGEX = /const OUTPUT_DIR\s*=\s*['"`]([^'"`]*)['"`]/;
10
9
 
11
10
  // --- Helpers ---
12
11
 
13
- // Reads OUTPUT_DIR value from a given file content string, or returns null
14
12
  function parseOutputDir(content) {
15
13
  const match = content.match(OUTPUT_DIR_REGEX);
16
14
  return match ? match[1] : null;
17
15
  }
18
16
 
17
+ function isTypeScriptProject() {
18
+ return fs.existsSync(TSCONFIG);
19
+ }
20
+
19
21
  // --- Updaters ---
20
22
 
21
23
  function updateEleventyConfig(newPath) {
@@ -39,35 +41,55 @@ function updateEleventyConfig(newPath) {
39
41
  function updatePackageJson(newPath) {
40
42
  const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf-8'));
41
43
 
42
- // Reconstruct all output-dependent scripts from scratch to avoid
43
- // partial string replacement bugs on the outdir flag
44
+ pkg.outputDir = newPath;
45
+
46
+ const usesTs = isTypeScriptProject();
47
+
44
48
  pkg.scripts['build:css'] = `sass src/frontend/scss:${newPath}/css --no-source-map --style=compressed --quiet`;
45
- pkg.scripts['build:js'] = `esbuild "src/frontend/js/pages/*.js" --bundle --outdir=${newPath}/js/pages --minify`;
46
49
  pkg.scripts['serve:css'] = `sass --watch src/frontend/scss:${newPath}/css --no-source-map --quiet`;
47
- pkg.scripts['serve:js'] = `esbuild "src/frontend/js/pages/*.js" --bundle --outdir=${newPath}/js/pages --watch`;
50
+
51
+ if (usesTs) {
52
+ pkg.scripts['build:js'] = `esbuild "src/frontend/ts/pages/*.ts" --bundle --outdir=${newPath}/js/pages --minify`;
53
+ pkg.scripts['serve:js'] = `esbuild "src/frontend/ts/pages/*.ts" --bundle --outdir=${newPath}/js/pages --watch`;
54
+ } else {
55
+ pkg.scripts['build:js'] = `esbuild "src/frontend/js/pages/*.js" --bundle --outdir=${newPath}/js/pages --minify`;
56
+ pkg.scripts['serve:js'] = `esbuild "src/frontend/js/pages/*.js" --bundle --outdir=${newPath}/js/pages --watch`;
57
+ }
48
58
 
49
59
  fs.writeFileSync(PACKAGE_JSON, JSON.stringify(pkg, null, 2), 'utf-8');
50
60
  console.log(`(✓) package.json updated → ${newPath}`);
51
61
  return true;
52
62
  }
53
63
 
64
+ const TSCONFIG_OUTDIR_REGEX = /"outDir"\s*:\s*"[^"]*"/;
65
+
66
+ function updateTsConfig(newPath) {
67
+ if (!isTypeScriptProject()) return;
68
+
69
+ const content = fs.readFileSync(TSCONFIG, 'utf-8');
70
+ const updated = content.replace(TSCONFIG_OUTDIR_REGEX, `"outDir": "./${newPath}/ts"`);
71
+
72
+ if (content === updated) {
73
+ console.log('(!) outDir not found in tsconfig.json');
74
+ return;
75
+ }
76
+
77
+ fs.writeFileSync(TSCONFIG, updated, 'utf-8');
78
+ console.log(`(✓) tsconfig.json updated → ${newPath}/ts`);
79
+ }
80
+
54
81
  // --- Public API ---
55
82
 
56
83
  function updateOutputPath(newPath) {
57
84
  const trimmed = newPath.trim().replace(/\\/g, '/');
58
85
 
59
- // Normalize the path: bare "." becomes "out", everything else gets a
60
- // project-scoped suffix to avoid collisions
61
86
  const normalizedPath = trimmed === '.'
62
87
  ? 'out'
63
88
  : `${trimmed.replace(/\/$/, '')}/${path.basename(process.cwd())}-out`;
64
89
 
65
- // Read the config once and reuse it to get the old path —
66
- // avoids a second disk read inside updateEleventyConfig
67
90
  const eleventyContent = fs.readFileSync(ELEVENTY_CONFIG, 'utf-8');
68
91
  const oldPath = parseOutputDir(eleventyContent);
69
92
 
70
- // Delete the old output folder if it exists
71
93
  if (oldPath) {
72
94
  const oldAbsPath = path.resolve(__dirname, '../../', oldPath);
73
95
  if (fs.existsSync(oldAbsPath)) {
@@ -82,9 +104,9 @@ function updateOutputPath(newPath) {
82
104
 
83
105
  updatePackageJson(normalizedPath);
84
106
  updateEleventyConfig(normalizedPath);
107
+ updateTsConfig(normalizedPath);
85
108
  }
86
109
 
87
- // Returns the current OUTPUT_DIR value from .eleventy.js, or null on failure
88
110
  function getCurrentOutputPath() {
89
111
  try {
90
112
  const content = fs.readFileSync(ELEVENTY_CONFIG, 'utf-8');
@@ -7,16 +7,35 @@ const { getCurrentOutputPath } = require('./updateOutputPath');
7
7
  const { toCamelCase } = require('./utils');
8
8
 
9
9
  const TEMPLATES_DIR = path.join(__dirname, '..', 'res', 'templates');
10
+ const TSCONFIG = path.resolve(__dirname, '../../tsconfig.json');
10
11
 
11
12
  // --- Helpers ---
12
13
 
13
- // Returns the three file targets (scss, js, njk) for a given page name
14
+ function isTypeScriptProject() {
15
+ return fileSystem.existsSync(TSCONFIG);
16
+ }
17
+
18
+ // Returns the three file targets (scss, js/ts, njk) for a given page name
14
19
  function getPageTargets(pageName) {
15
20
  const camelName = toCamelCase(pageName);
21
+ const usesTs = isTypeScriptProject();
22
+
16
23
  return [
17
- { folder: 'src/frontend/scss/pages', templateFile: 'template.scss', fileName: `${camelName}.scss` },
18
- { folder: 'src/frontend/js/pages', templateFile: 'template.js', fileName: `${camelName}.js` },
19
- { folder: 'src/frontend/_routes', templateFile: 'template.njk', fileName: `${pageName}.njk` },
24
+ {
25
+ folder: 'src/frontend/scss/pages',
26
+ templateFile: 'template.scss',
27
+ fileName: `${camelName}.scss`,
28
+ },
29
+ {
30
+ folder: usesTs ? 'src/frontend/ts/pages' : 'src/frontend/js/pages',
31
+ templateFile: usesTs ? 'template.ts' : 'template.js',
32
+ fileName: usesTs ? `${camelName}.ts` : `${camelName}.js`,
33
+ },
34
+ {
35
+ folder: 'src/frontend/_routes',
36
+ templateFile: 'template.njk',
37
+ fileName: `${pageName}.njk`,
38
+ },
20
39
  ];
21
40
  }
22
41
 
@@ -34,7 +53,6 @@ function addPage(pageName) {
34
53
  const srcPath = path.join(TEMPLATES_DIR, templateFile);
35
54
 
36
55
  if (templateFile === 'template.njk') {
37
- // Patch the frontmatter placeholders with the actual page values
38
56
  const content = fileSystem.readFileSync(srcPath, 'utf8')
39
57
  .replace(/^title:.*$/m, `title: "${camelName}"`)
40
58
  .replace(/^permalink:.*$/m, `permalink: "/${pageName}/"`);
@@ -53,11 +71,13 @@ function addPage(pageName) {
53
71
  function renamePage(oldName, newName) {
54
72
  const oldCamel = toCamelCase(oldName);
55
73
  const newCamel = toCamelCase(newName);
74
+ const usesTs = isTypeScriptProject();
75
+ const ext = usesTs ? 'ts' : 'js';
76
+ const jsFolder = usesTs ? 'src/frontend/ts/pages' : 'src/frontend/js/pages';
56
77
 
57
- // Use consistent src/frontend/ paths (matching addPage)
58
78
  const filesToRename = [
59
79
  { src: `src/frontend/scss/pages/${oldCamel}.scss`, dest: `src/frontend/scss/pages/${newCamel}.scss` },
60
- { src: `src/frontend/js/pages/${oldCamel}.js`, dest: `src/frontend/js/pages/${newCamel}.js` },
80
+ { src: `${jsFolder}/${oldCamel}.${ext}`, dest: `${jsFolder}/${newCamel}.${ext}` },
61
81
  { src: `src/frontend/_routes/${oldName}.njk`, dest: `src/frontend/_routes/${newName}.njk` },
62
82
  ];
63
83
 
@@ -70,20 +90,20 @@ function renamePage(oldName, newName) {
70
90
  console.log(`[renamed] ${src} → ${dest}`);
71
91
  });
72
92
 
73
- // Use atomic rename helpers instead of remove + add separately
74
93
  renameLayout(oldName, newName);
75
94
  renameSiteData(oldName, newName);
76
95
  }
77
96
 
78
97
  function removePage(pageName) {
79
98
  const camelName = toCamelCase(pageName);
80
-
81
- // Read the actual current output dir instead of hardcoding "out"
99
+ const usesTs = isTypeScriptProject();
100
+ const ext = usesTs ? 'ts' : 'js';
101
+ const jsFolder = usesTs ? 'src/frontend/ts/pages' : 'src/frontend/js/pages';
82
102
  const OUTPUT_DIR = getCurrentOutputPath() || 'out';
83
103
 
84
104
  const filesToDelete = [
85
105
  `src/frontend/scss/pages/${camelName}.scss`,
86
- `src/frontend/js/pages/${camelName}.js`,
106
+ `${jsFolder}/${camelName}.${ext}`,
87
107
  `src/frontend/_routes/${pageName}.njk`,
88
108
  path.join(OUTPUT_DIR, 'js/pages', `${camelName}.js`),
89
109
  path.join(OUTPUT_DIR, 'css/pages', `${camelName}.css`),
@@ -2,20 +2,12 @@
2
2
  // JAVASCRIPT MODULES IMPORTS
3
3
  //===========================
4
4
 
5
- // Call anywhere
6
- import { showNotification } from '../modules/notification.js';
7
-
8
- // Uncomment of pre-existing modules
9
- // import { initTextAreaAutoExpand } from '../modules/forms/textAreaAutoExpand.js';
10
- // import { initNormalizePhoneNumber } from '../modules/forms/normalizePhoneNumber.js';
5
+ // import { initExampleModule } from '../modules/exampleModule.js';
11
6
 
12
7
  //==========================
13
8
  // PAGE CUSTOM JAVASCRIPT
14
9
  //==========================
15
10
 
16
11
  document.addEventListener("DOMContentLoaded", () => {
17
- // initTextAreaAutoExpand();
18
- // initNormalizePhoneNumber();
19
- });
20
-
21
- showNotification("Example notification", "success", 3000);
12
+ // initExampleModule();
13
+ });
@@ -0,0 +1,13 @@
1
+ //===========================
2
+ // TYPESCRIPT MODULES IMPORTS
3
+ //===========================
4
+
5
+ // import { initExampleModule } from '../modules/exampleModule';
6
+
7
+ //==========================
8
+ // PAGE CUSTOM TYPESCRIPT
9
+ //==========================
10
+
11
+ document.addEventListener("DOMContentLoaded", (): void => {
12
+ // initExampleModule();
13
+ });
package/bin/create.js CHANGED
@@ -5,58 +5,87 @@ const path = require('path');
5
5
  const readline = require('readline');
6
6
  const { writeSync } = require('fs');
7
7
 
8
- const targetDir = process.argv[2] ? path.resolve(process.argv[2]) : process.cwd();
8
+ // ── PATHS ────────────────────────────────────────────────────────────────────
9
+
10
+ const targetDir = process.argv[2] ? path.resolve(process.argv[2]) : process.cwd();
9
11
  const templateDir = path.join(__dirname, '..');
10
12
 
11
- const COPY_TARGETS = [
12
- 'src',
13
+ // ── ENUMS ────────────────────────────────────────────────────────────────────
14
+
15
+ const LANGUAGE = Object.freeze({
16
+ JAVASCRIPT: 'javascript',
17
+ TYPESCRIPT: 'typescript',
18
+ });
19
+
20
+ const FRAMEWORK = Object.freeze({
21
+ BOOTSTRAP: 'bootstrap',
22
+ BULMA: 'bulma',
23
+ FOUNDATION: 'foundation',
24
+ UIKIT: 'uikit',
25
+ NONE: 'none',
26
+ });
27
+
28
+ // ── CHOICES ──────────────────────────────────────────────────────────────────
29
+
30
+ const LANGUAGE_CHOICES = [
31
+ { label: 'JavaScript (default)', value: LANGUAGE.JAVASCRIPT },
32
+ { label: 'TypeScript', value: LANGUAGE.TYPESCRIPT },
33
+ ];
34
+
35
+ const FRAMEWORK_CHOICES = [
36
+ { label: 'Bootstrap (default)', value: FRAMEWORK.BOOTSTRAP },
37
+ { label: 'Bulma', value: FRAMEWORK.BULMA },
38
+ { label: 'Foundation', value: FRAMEWORK.FOUNDATION },
39
+ { label: 'UIkit', value: FRAMEWORK.UIKIT },
40
+ { label: 'None', value: FRAMEWORK.NONE },
41
+ ];
42
+
43
+ // ── COPY CONFIG ───────────────────────────────────────────────────────────────
44
+
45
+ const MANDATORY_COPY = [
13
46
  'docs',
14
47
  '_tools',
15
48
  '.eleventy.js',
16
49
  '.eleventyignore',
50
+ 'src/backend',
51
+ 'src/frontend',
17
52
  ];
18
53
 
19
- const GITIGNORE_CONTENT = `
20
- node_modules/
21
- src/backend/_core/vendor/
22
- out/
23
- src/backend/config.php
24
- `;
54
+ const FRONTEND_EXCLUDE = {
55
+ [LANGUAGE.JAVASCRIPT]: ['ts'],
56
+ [LANGUAGE.TYPESCRIPT]: ['js'],
57
+ };
25
58
 
26
59
  const CREATE_DIRS = [
27
60
  'src/frontend/_routes',
28
61
  ];
29
62
 
30
- const ALL_FRAMEWORKS = ['bootstrap', 'bulma', 'foundation', 'uikit'];
63
+ // ── FRAMEWORK CONFIG ──────────────────────────────────────────────────────────
64
+
65
+ const ALL_FRAMEWORKS = Object.values(FRAMEWORK).filter(f => f !== FRAMEWORK.NONE);
31
66
 
32
67
  const FRAMEWORKS = {
33
- bootstrap: {
34
- label: 'Bootstrap',
35
- scss: 'bootstrap',
36
- njk: [
37
- '<script src="/js/bootstrap.bundle.min.js" defer></script>',
38
- ],
68
+ [FRAMEWORK.BOOTSTRAP]: {
69
+ scss: 'bootstrap',
70
+ njk: ['<script src="/js/bootstrap.bundle.min.js" defer></script>'],
39
71
  eleventy: [
40
72
  '"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js": "js/bootstrap.bundle.min.js",',
41
73
  '"node_modules/bootstrap-icons/font/fonts": "css/fonts",',
42
74
  ],
43
75
  },
44
- bulma: {
45
- label: 'Bulma',
46
- scss: 'bulma',
47
- njk: [],
76
+ [FRAMEWORK.BULMA]: {
77
+ scss: 'bulma',
78
+ njk: [],
48
79
  eleventy: [],
49
80
  },
50
- foundation: {
51
- label: 'Foundation',
52
- scss: 'foundation',
53
- njk: ['<script src="/js/foundation.min.js" defer></script>'],
81
+ [FRAMEWORK.FOUNDATION]: {
82
+ scss: 'foundation',
83
+ njk: ['<script src="/js/foundation.min.js" defer></script>'],
54
84
  eleventy: ['"node_modules/foundation-sites/dist/js/foundation.min.js": "js/foundation.min.js",'],
55
85
  },
56
- uikit: {
57
- label: 'UIkit',
58
- scss: 'uikit',
59
- njk: [
86
+ [FRAMEWORK.UIKIT]: {
87
+ scss: 'uikit',
88
+ njk: [
60
89
  '<script src="/js/uikit.min.js" defer></script>',
61
90
  '<script src="/js/uikit-icons.min.js" defer></script>',
62
91
  ],
@@ -65,25 +94,42 @@ const FRAMEWORKS = {
65
94
  '"node_modules/uikit/dist/js/uikit-icons.min.js": "js/uikit-icons.min.js",',
66
95
  ],
67
96
  },
68
- none: {
69
- label: 'None',
70
- scss: null,
71
- njk: [],
97
+ [FRAMEWORK.NONE]: {
98
+ scss: null,
99
+ njk: [],
72
100
  eleventy: [],
73
101
  },
74
102
  };
75
103
 
104
+ // ── LANGUAGE CONFIG ───────────────────────────────────────────────────────────
105
+
106
+ const LANGUAGE_ELEVENTY = Object.freeze({
107
+ jsEntry: 'const entryPoints = glob.sync("src/frontend/js/pages/*.js");',
108
+ tsEntry: 'const entryPoints = glob.sync("src/frontend/ts/pages/*.ts");',
109
+ });
110
+
111
+ // ── GENERATED FILE CONTENTS ───────────────────────────────────────────────────
112
+
113
+ const GITIGNORE_CONTENT = `
114
+ node_modules/
115
+ src/backend/_core/vendor/
116
+ out/
117
+ src/backend/config.php
118
+ `;
119
+
120
+
76
121
  const PROJECT_PACKAGE = {
77
- name: path.basename(targetDir),
78
- version: '2.1.0',
79
- private: true,
80
- scripts: {
122
+ name: path.basename(targetDir),
123
+ version: '2.2.1',
124
+ private: true,
125
+ outputDir: 'out',
126
+ "scripts": {
81
127
  "build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet --load-path=node_modules",
82
- "build:js": "esbuild \"src/frontend/js/pages/*.js\" --bundle --outdir=out/js/pages --minify",
128
+ "build:js": "node _tools/buildJs.js",
83
129
  "build:11ty": "eleventy",
84
130
  "build": "npm run clean && npm run build:css && npm run build:js && npm run build:11ty",
85
131
  "serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet --load-path=node_modules",
86
- "serve:js": "esbuild \"src/frontend/js/pages/*.js\" --bundle --outdir=out/js/pages --watch",
132
+ "serve:js": "node _tools/buildJs.js --watch",
87
133
  "serve:11ty": "eleventy --serve --quiet",
88
134
  "clean": "node _tools/cleanOutput.js",
89
135
  "serve": "npm run clean && concurrently \"npm run serve:11ty\" \"npm run serve:css\" \"npm run serve:js\"",
@@ -91,23 +137,25 @@ const PROJECT_PACKAGE = {
91
137
  "postinstall": "cd src/backend/_core && composer install --quiet"
92
138
  },
93
139
  dependencies: {
94
- '@11ty/eleventy': '^3.1.2',
140
+ '@11ty/eleventy': '^3.1.2',
95
141
  '@11ty/eleventy-img': '^6.0.4',
96
- 'bootstrap': '^5.3.8',
97
- 'bootstrap-icons': '^1.13.1',
98
- 'bulma': '^1.0.4',
99
- 'foundation-sites': '^6.9.0',
142
+ 'bootstrap': '^5.3.8',
143
+ 'bootstrap-icons': '^1.13.1',
144
+ 'bulma': '^1.0.4',
145
+ 'foundation-sites': '^6.9.0',
100
146
  'github-markdown-css': '^5.9.0',
101
- 'glob': '^13.0.6',
102
- 'uikit': '^3.25.13',
147
+ 'glob': '^13.0.6',
148
+ 'uikit': '^3.25.13',
103
149
  },
104
150
  devDependencies: {
105
151
  'concurrently': '^9.2.1',
106
- 'esbuild': '^0.27.3',
107
- 'sass': '^1.77.0',
152
+ 'esbuild': '^0.27.3',
153
+ 'sass': '^1.77.0',
108
154
  },
109
155
  };
110
156
 
157
+ // ── HELPERS ───────────────────────────────────────────────────────────────────
158
+
111
159
  function log(msg) {
112
160
  writeSync(1, msg + '\n');
113
161
  }
@@ -116,13 +164,14 @@ function escapeRegex(str) {
116
164
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
117
165
  }
118
166
 
119
- function copyRecursive(src, dest) {
167
+ function copyRecursive(src, dest, exclude = []) {
120
168
  const stat = fs.statSync(src);
121
169
  if (stat.isDirectory()) {
122
170
  fs.mkdirSync(dest, { recursive: true });
123
171
  for (const child of fs.readdirSync(src)) {
124
172
  if (child === '.git') continue;
125
- copyRecursive(path.join(src, child), path.join(dest, child));
173
+ if (exclude.includes(child)) continue;
174
+ copyRecursive(path.join(src, child), path.join(dest, child), exclude);
126
175
  }
127
176
  } else {
128
177
  fs.mkdirSync(path.dirname(dest), { recursive: true });
@@ -159,6 +208,8 @@ function njkUncomment(content, line) {
159
208
  return content.split(`{# ${line} #}`).join(line);
160
209
  }
161
210
 
211
+ // ── APPLY ─────────────────────────────────────────────────────────────────────
212
+
162
213
  function applyFramework(framework) {
163
214
  const config = FRAMEWORKS[framework];
164
215
 
@@ -178,13 +229,9 @@ function applyFramework(framework) {
178
229
  if (fs.existsSync(baseNjkPath)) {
179
230
  let content = fs.readFileSync(baseNjkPath, 'utf8');
180
231
  ALL_FRAMEWORKS.forEach(fw => {
181
- FRAMEWORKS[fw].njk.forEach(line => {
182
- content = njkComment(content, line);
183
- });
184
- });
185
- config.njk.forEach(line => {
186
- content = njkUncomment(content, line);
232
+ FRAMEWORKS[fw].njk.forEach(line => { content = njkComment(content, line); });
187
233
  });
234
+ config.njk.forEach(line => { content = njkUncomment(content, line); });
188
235
  fs.writeFileSync(baseNjkPath, content);
189
236
  }
190
237
 
@@ -192,49 +239,50 @@ function applyFramework(framework) {
192
239
  if (fs.existsSync(eleventyPath)) {
193
240
  let content = fs.readFileSync(eleventyPath, 'utf8');
194
241
  ALL_FRAMEWORKS.forEach(fw => {
195
- FRAMEWORKS[fw].eleventy.forEach(line => {
196
- content = slashComment(content, line);
197
- });
198
- });
199
- config.eleventy.forEach(line => {
200
- content = slashUncomment(content, line);
242
+ FRAMEWORKS[fw].eleventy.forEach(line => { content = slashComment(content, line); });
201
243
  });
244
+ config.eleventy.forEach(line => { content = slashUncomment(content, line); });
202
245
  fs.writeFileSync(eleventyPath, content);
203
246
  }
204
247
  }
205
248
 
206
- function askFramework() {
249
+ function applyLanguage(language) {
250
+ const eleventyPath = path.join(targetDir, '.eleventy.js');
251
+ if (!fs.existsSync(eleventyPath)) return;
252
+
253
+ let content = fs.readFileSync(eleventyPath, 'utf8');
254
+
255
+ if (language === LANGUAGE.TYPESCRIPT) {
256
+ content = slashComment(content, LANGUAGE_ELEVENTY.jsEntry);
257
+ content = slashUncomment(content, LANGUAGE_ELEVENTY.tsEntry);
258
+ } else {
259
+ content = slashUncomment(content, LANGUAGE_ELEVENTY.jsEntry);
260
+ content = slashComment(content, LANGUAGE_ELEVENTY.tsEntry);
261
+ }
262
+
263
+ fs.writeFileSync(eleventyPath, content);
264
+ }
265
+
266
+ // ── UI ────────────────────────────────────────────────────────────────────────
267
+
268
+ function askChoice(question, choices) {
207
269
  return new Promise((resolve) => {
208
- const choices = [
209
- { label: 'Bootstrap (default)', value: 'bootstrap' },
210
- { label: 'Bulma', value: 'bulma' },
211
- { label: 'Foundation', value: 'foundation' },
212
- { label: 'UIkit', value: 'uikit' },
213
- { label: 'None', value: 'none' }
214
- ];
215
270
  let selectedIndex = 0;
216
271
 
217
- log('\n>> Select a CSS framework (Use arrow keys and press Enter):\n');
272
+ log(`\n>> ${question} (Use arrow keys and press Enter):\n`);
218
273
 
219
274
  const render = (firstTime = false) => {
220
- if (!firstTime) {
221
- process.stdout.write(`\x1B[${choices.length}A`);
222
- }
223
- let output = '';
224
- choices.forEach((choice, index) => {
225
- if (index === selectedIndex) {
226
- output += ` \x1b[36m◉ ${choice.label}\x1b[0m\x1B[K\n`;
227
- } else {
228
- output += ` * ${choice.label}\x1B[K\n`;
229
- }
230
- });
275
+ if (!firstTime) process.stdout.write(`\x1B[${choices.length}A`);
276
+ const output = choices.map((choice, index) =>
277
+ index === selectedIndex
278
+ ? ` \x1b[36m◉ ${choice.label}\x1b[0m\x1B[K\n`
279
+ : ` * ${choice.label}\x1B[K\n`
280
+ ).join('');
231
281
  process.stdout.write(output);
232
282
  };
233
283
 
234
284
  readline.emitKeypressEvents(process.stdin);
235
- if (process.stdin.isTTY) {
236
- process.stdin.setRawMode(true);
237
- }
285
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
238
286
  process.stdin.resume();
239
287
 
240
288
  const onKeyPress = (str, key) => {
@@ -248,9 +296,7 @@ function askFramework() {
248
296
  render();
249
297
  } else if (key.name === 'return' || key.name === 'enter') {
250
298
  process.stdin.removeListener('keypress', onKeyPress);
251
- if (process.stdin.isTTY) {
252
- process.stdin.setRawMode(false);
253
- }
299
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
254
300
  process.stdin.pause();
255
301
  resolve(choices[selectedIndex].value);
256
302
  }
@@ -261,23 +307,26 @@ function askFramework() {
261
307
  });
262
308
  }
263
309
 
310
+ // ── INIT ──────────────────────────────────────────────────────────────────────
311
+
264
312
  async function init() {
265
- if (!fs.existsSync(targetDir)) {
266
- fs.mkdirSync(targetDir, { recursive: true });
267
- }
313
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
268
314
 
269
315
  log(`\n>> Creating berna-stencil project in ${targetDir}\n`);
270
316
 
271
- for (const target of COPY_TARGETS) {
272
- const src = path.join(templateDir, target);
317
+ const language = await askChoice('Select a language', LANGUAGE_CHOICES);
318
+ const framework = await askChoice('Select a CSS framework', FRAMEWORK_CHOICES);
319
+
320
+ for (const target of MANDATORY_COPY) {
321
+ const src = path.join(templateDir, target);
273
322
  const dest = path.join(targetDir, target);
274
- if (fs.existsSync(src)) {
275
- copyRecursive(src, dest);
276
- log(`+ ${target}`);
277
- }
323
+ if (!fs.existsSync(src)) continue;
324
+ const exclude = target === 'src/frontend' ? FRONTEND_EXCLUDE[language] : [];
325
+ copyRecursive(src, dest, exclude);
326
+ log(`+ ${target}`);
278
327
  }
279
328
 
280
- const configDest = path.join(targetDir, 'src/backend/config.php');
329
+ const configDest = path.join(targetDir, 'src/backend/config.php');
281
330
  const configExample = path.join(targetDir, 'src/backend/config.example.php');
282
331
  if (!fs.existsSync(configDest) && fs.existsSync(configExample)) {
283
332
  fs.copyFileSync(configExample, configDest);
@@ -285,30 +334,31 @@ async function init() {
285
334
  }
286
335
  deleteFileRecursive(targetDir, 'config.example.php');
287
336
 
288
- fs.writeFileSync(
289
- path.join(targetDir, 'package.json'),
290
- JSON.stringify(PROJECT_PACKAGE, null, 2)
291
- );
337
+ const pkg = { ...PROJECT_PACKAGE };
338
+
339
+ if (language === LANGUAGE.TYPESCRIPT) {
340
+ const tsSrc = path.join(templateDir, 'tsconfig.json');
341
+ const tsDest = path.join(targetDir, 'tsconfig.json');
342
+ fs.copyFileSync(tsSrc, tsDest);
343
+ log('+ tsconfig.json');
344
+ pkg.devDependencies = { ...pkg.devDependencies, typescript: 'latest' };
345
+ }
346
+
347
+ fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify(pkg, null, 2));
292
348
  log('+ package.json');
293
349
 
294
- fs.writeFileSync(
295
- path.join(targetDir, '.gitignore'),
296
- GITIGNORE_CONTENT
297
- );
350
+ fs.writeFileSync(path.join(targetDir, '.gitignore'), GITIGNORE_CONTENT);
298
351
  log('+ .gitignore');
299
352
 
300
353
  for (const dir of CREATE_DIRS) {
301
- const dest = path.join(targetDir, dir);
302
- fs.mkdirSync(dest, { recursive: true });
354
+ fs.mkdirSync(path.join(targetDir, dir), { recursive: true });
303
355
  }
304
356
 
305
- const framework = await askFramework();
306
357
  applyFramework(framework);
358
+ applyLanguage(language);
307
359
 
308
360
  log(`\n>> Done! Now run:\n`);
309
- if (process.argv[2]) {
310
- log(`cd ${process.argv[2]}`);
311
- }
361
+ if (process.argv[2]) log(`cd ${process.argv[2]}`);
312
362
  log('npm install');
313
363
  log('npm run serve\n');
314
364
  }
@@ -1,5 +1,7 @@
1
1
  # Assistant CLI
2
2
 
3
+ > Examples use JavaScript, but everything applies equally to TypeScript. The only difference is the file extension (`.ts` instead of `.js`), that imports do **not** include the extension, and that paths use `src/frontend/ts/` instead of `src/frontend/js/`.
4
+
3
5
  An interactive CLI to manage pages without touching files manually.
4
6
 
5
7
  ```
@@ -1,5 +1,7 @@
1
1
  # Creating Pages
2
2
 
3
+ > Examples use JavaScript, but everything applies equally to TypeScript. The only difference is the file extension (`.ts` instead of `.js`), that imports do **not** include the extension, and that paths use `src/frontend/ts/` instead of `src/frontend/js/`.
4
+
3
5
  The recommended way is via the **Assistant CLI**
4
6
 
5
7
  ## What gets created
@@ -1,70 +1,63 @@
1
1
  # JavaScript
2
2
 
3
- ## Page JS
3
+ > Examples use JavaScript, but everything applies equally to TypeScript. The only difference is the file extension (`.ts` instead of `.js`), that imports do **not** include the extension, and that paths use `src/frontend/ts/` instead of `src/frontend/js/`.
4
4
 
5
- Each page has its own JS entry point in `src/frontend/js/pages/`
5
+ ## Page JS
6
6
 
7
- It is bundled and minified by esbuild and loaded automatically by `base.njk`
7
+ Each page has its own JS entry point in `src/frontend/js/pages/`, bundled and minified by esbuild and loaded automatically by `base.njk`.
8
8
 
9
9
  Import only what the page needs.
10
10
 
11
11
  ### examplePage.js <small>(`src/frontend/js/pages/`)</small>
12
+
12
13
  ```js
13
- import { showNotification } from '../modules/notification.js';
14
+ //===========================
15
+ // JAVASCRIPT MODULES IMPORTS
16
+ //===========================
17
+
18
+ // import { initExampleModule } from '../modules/exampleModule.js';
14
19
 
15
- import { initNormalizePhoneNumber } from '../modules/forms/normalizePhoneNumber.js';
20
+ //==========================
21
+ // PAGE CUSTOM JAVASCRIPT
22
+ //==========================
16
23
 
17
24
  document.addEventListener("DOMContentLoaded", () => {
18
- initNormalizePhoneNumber();
25
+ // initExampleModule();
19
26
  });
20
-
21
- showNotification("Page loaded", "success", 3000);
22
27
  ```
23
28
 
24
29
  ## Modules
25
30
 
26
- Modules live in `src/frontend/js/modules/`. Some must be called inside `DOMContentLoaded` as they interact with the DOM; others create elements dynamically and can be called anywhere.
27
-
28
- ### Call inside `DOMContentLoaded`
29
-
30
- | Module | Function |
31
- |---|---|
32
- | `modules/langSwitcher.js` | `initLangSwitcher()` |
33
- | `modules/forms/form.js` | `initFormListener()` |
34
- | `modules/forms/textAreaAutoExpand.js` | `initTextAreaAutoExpand()` |
35
- | `modules/forms/normalizePhoneNumber.js` | `initNormalizePhoneNumber()` |
36
-
37
- ### Call anywhere
38
-
39
- | Module | Function |
40
- |---|---|
41
- | `modules/notification.js` | `showNotification(text, type, duration)` |
42
-
43
- ### `showNotification` parameters
44
-
45
- | Parameter | Type | Default | Values |
46
- |---|---|---|---|
47
- | `text` | string | — | Any string |
48
- | `type` | string | `"info"` | `"success"`, `"info"`, `"error"` |
49
- | `duration` | number | `5000` | ms, or `-1` for persistent with spinner |
31
+ Modules live in `src/frontend/js/modules/`. Modules that interact with the DOM must be called inside `DOMContentLoaded`; others can be called anywhere.
50
32
 
51
33
  ## Adding a module
52
34
 
53
- Create a new `.js` file in `src/frontend/js/modules/`. You can organize them into subfolders freely.
35
+ Create a new `.js` file in `src/frontend/js/modules/`. Subfolders are allowed.
54
36
 
55
37
  Use ESM syntax — esbuild handles the bundling:
56
38
 
39
+ ### exampleModule.js <small>(`src/frontend/js/modules/`)</small>
40
+
57
41
  ```js
58
- // _yourModule.js
59
- export function yourFunction() {
60
- // ...
42
+ //==========================
43
+ // EXAMPLE MODULE
44
+ //==========================
45
+
46
+ export function exampleModule() {
47
+ // Example module logic
61
48
  }
62
49
  ```
63
50
 
64
51
  Then import it in the pages that need it:
65
52
 
66
53
  ```js
67
- import { yourFunction } from '../modules/yourModule.js';
54
+ import { exampleModule } from '../modules/exampleModule.js';
55
+ ```
56
+
57
+ In TypeScript, omit the extension:
58
+
59
+ ```ts
60
+ import { exampleModule } from '../modules/exampleModule';
68
61
  ```
69
62
 
70
63
  > ⚠️ Files inside `_tools/` run directly in Node.js without a bundler — use CommonJS (`require` / `module.exports`) there, not ESM.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-berna-stencil",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "Eleventy boilerplate with per-page SCSS/JS pipeline, esbuild bundling, multi-framework CSS support and a built-in page management CLI",
5
5
  "keywords": [],
6
6
  "author": "Michele Garofalo",
@@ -24,6 +24,7 @@
24
24
  ".eleventy.js",
25
25
  ".eleventyignore",
26
26
  ".gitignore",
27
+ "tsconfig.json",
27
28
  "LICENSE",
28
29
  "NOTICE"
29
30
  ],
@@ -47,17 +48,18 @@
47
48
  "esbuild": "^0.27.3",
48
49
  "sass": "^1.77.0"
49
50
  },
50
- "scripts": {
51
- "build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet",
52
- "build:js": "esbuild \"src/frontend/js/pages/*.js\" --bundle --outdir=out/js/pages --minify",
51
+ "scripts": {
52
+ "build:css": "sass src/frontend/scss:out/css --no-source-map --style=compressed --quiet --load-path=node_modules",
53
+ "build:js": "node _tools/buildJs.js",
53
54
  "build:11ty": "eleventy",
54
55
  "build": "npm run clean && npm run build:css && npm run build:js && npm run build:11ty",
55
- "serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet",
56
- "serve:js": "esbuild \"src/frontend/js/pages/*.js\" --bundle --outdir=out/js/pages --watch",
56
+ "serve:css": "sass --watch src/frontend/scss:out/css --no-source-map --quiet --load-path=node_modules",
57
+ "serve:js": "node _tools/buildJs.js --watch",
57
58
  "serve:11ty": "eleventy --serve --quiet",
58
59
  "clean": "node _tools/cleanOutput.js",
59
60
  "serve": "npm run clean && concurrently \"npm run serve:11ty\" \"npm run serve:css\" \"npm run serve:js\"",
60
61
  "assistant": "node _tools/assistant.js",
61
62
  "postinstall": "cd src/backend/_core && composer install --quiet"
62
- }
63
+ },
64
+ "outputDir": "out"
63
65
  }
@@ -0,0 +1,7 @@
1
+ //==========================
2
+ // EXAMPLE MODULE
3
+ //==========================
4
+
5
+ export function initExampleModule() {
6
+ // Example module logic
7
+ }
@@ -2,20 +2,12 @@
2
2
  // JAVASCRIPT MODULES IMPORTS
3
3
  //===========================
4
4
 
5
- // Call anywhere
6
- import { showNotification } from '../modules/notification.js';
7
-
8
- // Uncomment of pre-existing modules
9
- // import { initTextAreaAutoExpand } from '../modules/forms/textAreaAutoExpand.js';
10
- // import { initNormalizePhoneNumber } from '../modules/forms/normalizePhoneNumber.js';
5
+ // import { initExampleModule } from '../modules/exampleModule.js';
11
6
 
12
7
  //==========================
13
8
  // PAGE CUSTOM JAVASCRIPT
14
9
  //==========================
15
10
 
16
11
  document.addEventListener("DOMContentLoaded", () => {
17
- // initTextAreaAutoExpand();
18
- // initNormalizePhoneNumber();
19
- });
20
-
21
- showNotification("Example notification", "success", 3000);
12
+ // initExampleModule();
13
+ });
@@ -2,20 +2,12 @@
2
2
  // JAVASCRIPT MODULES IMPORTS
3
3
  //===========================
4
4
 
5
- // Call anywhere
6
- import { showNotification } from '../modules/notification.js';
7
-
8
- // Uncomment of pre-existing modules
9
- // import { initTextAreaAutoExpand } from '../modules/forms/textAreaAutoExpand.js';
10
- // import { initNormalizePhoneNumber } from '../modules/forms/normalizePhoneNumber.js';
5
+ // import { initExampleModule } from '../modules/exampleModule.js';
11
6
 
12
7
  //==========================
13
8
  // PAGE CUSTOM JAVASCRIPT
14
9
  //==========================
15
10
 
16
11
  document.addEventListener("DOMContentLoaded", () => {
17
- // initTextAreaAutoExpand();
18
- // initNormalizePhoneNumber();
19
- });
20
-
21
- showNotification("Example notification", "success", 3000);
12
+ // initExampleModule();
13
+ });
@@ -0,0 +1,3 @@
1
+ export function initExampleModule(): void {
2
+ // Example module logic
3
+ }
@@ -0,0 +1,13 @@
1
+ // ===========================
2
+ // TYPESCRIPT MODULES IMPORTS
3
+ // ===========================
4
+
5
+ // import { initExampleModule } from '../modules/exampleModule';
6
+
7
+ // ==========================
8
+ // PAGE CUSTOM TYPESCRIPT
9
+ // ==========================
10
+
11
+ document.addEventListener("DOMContentLoaded", (): void => {
12
+ // initExampleModule();
13
+ });
@@ -0,0 +1,13 @@
1
+ // ===========================
2
+ // TYPESCRIPT MODULES IMPORTS
3
+ // ===========================
4
+
5
+ // import { initExampleModule } from '../modules/exampleModule';
6
+
7
+ // ==========================
8
+ // PAGE CUSTOM TYPESCRIPT
9
+ // ==========================
10
+
11
+ document.addEventListener("DOMContentLoaded", (): void => {
12
+ // initExampleModule();
13
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ // https://aka.ms/tsconfig
3
+
4
+ // Add options by uncommenting or adding "<key>": <value> pairs inside compilerOptions
5
+
6
+ "include": ["src/frontend/ts/**/*.ts"], // DO NOT CHANGE
7
+
8
+ "compilerOptions": {
9
+ "rootDir": "./src/frontend/ts", // DO NOT CHANGE
10
+ "outDir": "./out/ts", // DO NOT CHANGE
11
+ "module": "esnext", // DO NOT CHANGE
12
+ "target": "esnext", // DO NOT CHANGE
13
+ "types": [],
14
+ "noUncheckedIndexedAccess": true,
15
+ "exactOptionalPropertyTypes": true,
16
+ "strict": true,
17
+ "jsx": "react-jsx",
18
+ "verbatimModuleSyntax": true,
19
+ "isolatedModules": true,
20
+ "noUncheckedSideEffectImports": true,
21
+ "moduleDetection": "force",
22
+ "skipLibCheck": true
23
+ }
24
+ }
@@ -1,42 +0,0 @@
1
- //==========================
2
- // PHONE NUMBER NORMALIZATION MODULE
3
- //==========================
4
-
5
- // Normalizes and attaches normalization to all existing and future tel inputs
6
- // Must be called inside DOMContentLoaded in the page's JS file
7
- export function initNormalizePhoneNumber() {
8
-
9
- // Formats digits in groups of 3, 3 and 4 (e.g. 333 123 4567)
10
- function formatDigits(digits) {
11
- let formatted = "";
12
- if (digits.length > 0) formatted += digits.slice(0, 3);
13
- if (digits.length > 3) formatted += " " + digits.slice(3, 6);
14
- if (digits.length > 6) formatted += " " + digits.slice(6, 10);
15
- return formatted;
16
- }
17
-
18
- function normalize(input) {
19
- if (!input) return;
20
-
21
- let value = input.value.replace(/[^0-9+]/g, "");
22
-
23
- if (value.startsWith("+")) {
24
- value = "+" + value.slice(1).replace(/\+/g, "");
25
- const prefix = value.slice(0, 3);
26
- const digits = value.slice(3);
27
-
28
- value = prefix + (digits ? " " + formatDigits(digits) : "");
29
- } else {
30
- value = value.replace(/\+/g, "");
31
- value = formatDigits(value);
32
- }
33
-
34
- input.value = value;
35
- }
36
-
37
- document.querySelectorAll('input[type="tel"]').forEach(normalize);
38
-
39
- document.addEventListener("input", ({ target }) => {
40
- if (target.matches('input[type="tel"]')) normalize(target);
41
- });
42
- }
@@ -1,38 +0,0 @@
1
- //==========================
2
- // AUTO-EXPAND TEXTAREA MODULE
3
- //==========================
4
-
5
- export function initTextAreaAutoExpand() {
6
- const MAX_ROWS = 10;
7
-
8
- function setup(element) {
9
- if (element.dataset.autoExpand) return;
10
- element.dataset.autoExpand = "true";
11
- element.style.resize = "none";
12
- element.style.overflow = "hidden";
13
- element.dataset.minRows = element.rows || 1;
14
- element.addEventListener("input", () => expand(element));
15
- if (element.value) expand(element);
16
- }
17
-
18
- function expand(element) {
19
- element.rows = element.dataset.minRows;
20
- while (element.scrollHeight > element.clientHeight && element.rows < MAX_ROWS) {
21
- element.rows += 1;
22
- }
23
- }
24
-
25
- document.querySelectorAll("textarea").forEach(setup);
26
-
27
- const observer = new MutationObserver((mutations) => {
28
- for (const mutation of mutations) {
29
- for (const node of mutation.addedNodes) {
30
- if (node.nodeType !== 1) continue;
31
- if (node.matches("textarea")) setup(node);
32
- node.querySelectorAll?.("textarea").forEach(setup);
33
- }
34
- }
35
- });
36
-
37
- observer.observe(document.body, { childList: true, subtree: true });
38
- }
@@ -1,39 +0,0 @@
1
- //==========================
2
- // NOTIFICATION MODULE
3
- //==========================
4
-
5
- let currentNotification = null;
6
-
7
- export function showNotification(text, type = "info", duration = 5000) {
8
- if (currentNotification) {
9
- currentNotification.remove();
10
- currentNotification = null;
11
- }
12
-
13
- const notificationBox = document.createElement("div");
14
- notificationBox.classList.add("notificationBox", type);
15
-
16
- const notificationText = document.createElement("span");
17
- notificationText.textContent = text;
18
- notificationBox.appendChild(notificationText);
19
-
20
- if (duration === -1) {
21
- const spinner = document.createElement("div");
22
- spinner.classList.add("spinner");
23
- notificationBox.appendChild(spinner);
24
- }
25
-
26
- document.body.appendChild(notificationBox);
27
- currentNotification = notificationBox;
28
-
29
- if (duration !== -1) {
30
- setTimeout(() => {
31
- if (!notificationBox.parentElement) return;
32
- notificationBox.classList.add("fade-out");
33
- notificationBox.addEventListener("transitionend", () => {
34
- notificationBox.remove();
35
- if (currentNotification === notificationBox) currentNotification = null;
36
- }, { once: true });
37
- }, duration);
38
- }
39
- }