@tidyfactor/design 1.5.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.
Files changed (136) hide show
  1. package/.tidyfactor +52 -0
  2. package/AGENTS.md +75 -0
  3. package/CHANGELOG.md +210 -0
  4. package/LICENSE +17 -0
  5. package/README.ar.md +398 -0
  6. package/README.de.md +44 -0
  7. package/README.es.md +44 -0
  8. package/README.fa.md +44 -0
  9. package/README.fr.md +44 -0
  10. package/README.md +418 -0
  11. package/README.pt.md +44 -0
  12. package/README.zh.md +44 -0
  13. package/SKILL-REGISTRY.md +69 -0
  14. package/SKILL.md +46 -0
  15. package/VISION.md +43 -0
  16. package/assets/blog.png +0 -0
  17. package/assets/content_layout.png +0 -0
  18. package/assets/content_output.png +0 -0
  19. package/assets/dark.png +0 -0
  20. package/assets/dashboard_layout.png +0 -0
  21. package/assets/dashboard_output.png +0 -0
  22. package/assets/ecommerce_layout.png +0 -0
  23. package/assets/ecommerce_output.png +0 -0
  24. package/assets/github-social-preview-2.png +0 -0
  25. package/assets/github-social-preview.png +0 -0
  26. package/assets/hero-banner.png +0 -0
  27. package/assets/light.png +0 -0
  28. package/assets/media_layout.png +0 -0
  29. package/assets/media_output.png +0 -0
  30. package/assets/og-default.png +0 -0
  31. package/assets/video.png +0 -0
  32. package/bin/add-skill.js +31 -0
  33. package/bin/create-kit.js +372 -0
  34. package/bin/remove-skill.js +137 -0
  35. package/brand.json +279 -0
  36. package/memory/01-design-schools.md +85 -0
  37. package/memory/02-design-tokens.md +41 -0
  38. package/memory/03-narrative-conversion.md +47 -0
  39. package/memory/04-motion-principles.md +44 -0
  40. package/memory/05-component-anatomy.md +34 -0
  41. package/memory/06-quality-bar.md +83 -0
  42. package/memory/07-consistency-contract.md +33 -0
  43. package/memory/08-arabic-bilingual.md +54 -0
  44. package/memory/09-prototype-flow.md +36 -0
  45. package/memory/10-python-tooling.md +64 -0
  46. package/memory/11-brand-json-v2.md +86 -0
  47. package/memory/12-typography-matrix.md +31 -0
  48. package/memory/13-layout-archetypes.md +30 -0
  49. package/memory/14-nav-footer-catalog.md +26 -0
  50. package/memory/15-performance-budget.md +18 -0
  51. package/memory/16-design-movements-guide.md +1084 -0
  52. package/memory/17-storytelling-industries-ux.md +143 -0
  53. package/memory/18-design-decision-engine.md +81 -0
  54. package/package.json +64 -0
  55. package/references/commands/_template.md +27 -0
  56. package/references/commands/assets.md +35 -0
  57. package/references/commands/audit.md +15 -0
  58. package/references/commands/brand.md +85 -0
  59. package/references/commands/brief.md +107 -0
  60. package/references/commands/clone.md +26 -0
  61. package/references/commands/components.md +46 -0
  62. package/references/commands/dashboard.md +45 -0
  63. package/references/commands/deploy.md +45 -0
  64. package/references/commands/flow.md +44 -0
  65. package/references/commands/handoff.md +53 -0
  66. package/references/commands/i18n.md +64 -0
  67. package/references/commands/init.md +30 -0
  68. package/references/commands/layout.md +81 -0
  69. package/references/commands/motion.md +62 -0
  70. package/references/commands/nav-footer.md +54 -0
  71. package/references/commands/page.md +45 -0
  72. package/references/commands/palette.md +35 -0
  73. package/references/commands/perf.md +57 -0
  74. package/references/commands/retrofit.md +15 -0
  75. package/references/commands/school.md +41 -0
  76. package/references/commands/states.md +48 -0
  77. package/references/commands/study.md +98 -0
  78. package/references/commands/tokens.md +45 -0
  79. package/references/commands/typography.md +87 -0
  80. package/references/foundations/daisyui.md +44 -0
  81. package/references/foundations/hybrid.md +32 -0
  82. package/references/foundations/native.md +31 -0
  83. package/references/foundations/tailwind-utility.md +43 -0
  84. package/references/memory/01-design-schools.md +85 -0
  85. package/references/memory/02-design-tokens.md +41 -0
  86. package/references/memory/03-narrative-conversion.md +47 -0
  87. package/references/memory/04-motion-principles.md +44 -0
  88. package/references/memory/05-component-anatomy.md +34 -0
  89. package/references/memory/06-quality-bar.md +83 -0
  90. package/references/memory/07-consistency-contract.md +33 -0
  91. package/references/memory/08-arabic-bilingual.md +54 -0
  92. package/references/memory/09-prototype-flow.md +36 -0
  93. package/references/memory/10-python-tooling.md +64 -0
  94. package/references/memory/11-brand-json-v2.md +86 -0
  95. package/references/memory/12-typography-matrix.md +31 -0
  96. package/references/memory/13-layout-archetypes.md +30 -0
  97. package/references/memory/14-nav-footer-catalog.md +26 -0
  98. package/references/memory/15-performance-budget.md +18 -0
  99. package/references/memory/16-design-movements-guide.md +1084 -0
  100. package/references/memory/17-storytelling-industries-ux.md +143 -0
  101. package/references/memory/18-design-decision-engine.md +81 -0
  102. package/references/memory/architecture.md +39 -0
  103. package/references/memory/decision-points.md +50 -0
  104. package/references/memory/foundations.md +15 -0
  105. package/references/memory/quality-bar.md +20 -0
  106. package/references/tidyfactor-vision.md +66 -0
  107. package/references/workflow.md +49 -0
  108. package/references/workflows/audit-prototype.md +26 -0
  109. package/references/workflows/brief.md +30 -0
  110. package/references/workflows/clone-prototype.md +27 -0
  111. package/references/workflows/init-prototype.md +28 -0
  112. package/references/workflows/retrofit-prototype.md +25 -0
  113. package/scripts/__pycache__/_utils.cpython-312.pyc +0 -0
  114. package/scripts/_utils.py +60 -0
  115. package/scripts/build.py +194 -0
  116. package/scripts/check_alpha.py +52 -0
  117. package/scripts/extract_palette.py +139 -0
  118. package/scripts/generate_transitions.py +105 -0
  119. package/scripts/inspect_images.py +48 -0
  120. package/scripts/minify_assets.py +140 -0
  121. package/scripts/optimize_assets.py +91 -0
  122. package/scripts/optimize_images.py +176 -0
  123. package/scripts/prepare_images.py +67 -0
  124. package/scripts/remove_backgrounds.py +61 -0
  125. package/scripts/test_build.py +228 -0
  126. package/templates/design-system/base.css +75 -0
  127. package/templates/design-system/components.css +229 -0
  128. package/templates/design-system/interactions.js +31 -0
  129. package/templates/design-system/motion.js +42 -0
  130. package/templates/design-system/tokens.css +81 -0
  131. package/templates/design-system/utilities.css +47 -0
  132. package/templates/index.html +84 -0
  133. package/templates/proto-nav.js +56 -0
  134. package/tools/build-skill.js +126 -0
  135. package/tools/validate-skill.js +144 -0
  136. package/tools/validate_skill.py +121 -0
@@ -0,0 +1,60 @@
1
+ """Shared utilities for the Cinematic Landing Kit asset pipeline."""
2
+
3
+ import re
4
+
5
+ try:
6
+ from PIL import Image
7
+ except ImportError:
8
+ Image = None
9
+
10
+
11
+ def crop_cover_16_9(img, target_w: int = 1280, target_h: int = 720):
12
+ """Crop-resize an image to a target 16:9 frame (object-cover style)."""
13
+ if Image is None:
14
+ raise ImportError("Pillow is required for crop_cover_16_9. Run: pip install Pillow")
15
+ img = img.convert("RGB")
16
+ img_ratio = img.width / img.height
17
+ target_ratio = target_w / target_h
18
+ if img_ratio > target_ratio:
19
+ new_width = int(target_ratio * img.height)
20
+ offset = (img.width - new_width) // 2
21
+ img = img.crop((offset, 0, offset + new_width, img.height))
22
+ else:
23
+ new_height = int(img.width / target_ratio)
24
+ offset = (img.height - new_height) // 2
25
+ img = img.crop((0, offset, img.width, offset + new_height))
26
+ return img.resize((target_w, target_h), Image.Resampling.LANCZOS)
27
+
28
+
29
+ _NATURAL_SORT_RE = re.compile(r"(\d+)")
30
+
31
+
32
+ def natural_sort_key(s: str):
33
+ """Sort key for filenames with embedded numbers.
34
+
35
+ 'k2.jpg' sorts before 'k10.jpg', matching human expectations.
36
+ """
37
+ parts = _NATURAL_SORT_RE.split(s)
38
+ return [int(p) if p.isdigit() else p.lower() for p in parts]
39
+
40
+
41
+ def split_spec(spec: str) -> list[str]:
42
+ """Split 'src:dest[:opts]' with Windows drive-letter awareness.
43
+
44
+ A single-alpha character immediately before ':' is treated as a drive-letter
45
+ prefix (e.g. C:\\path) rather than a field separator. All other colons are
46
+ normal field delimiters.
47
+ """
48
+ parts: list[str] = []
49
+ current = ""
50
+ for ch in spec:
51
+ if ch == ":":
52
+ if len(current) == 1 and current[0].isalpha():
53
+ current += ch
54
+ else:
55
+ parts.append(current)
56
+ current = ""
57
+ else:
58
+ current += ch
59
+ parts.append(current)
60
+ return parts
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Main Production Build Orchestrator
4
+ Runs all production build steps in sequence:
5
+ 1. Bundles and minifies JS and CSS assets.
6
+ 2. Scans and optimizes all images in assets/image/.
7
+ 3. Executes the build test suite. Aborts the build on failure.
8
+ 4. Performs cache-busting on index.html, updating asset query strings (?v=) with a timestamp.
9
+ 5. Packages the clean production-ready files into release/deploy.zip.
10
+
11
+ Usage:
12
+ python scripts/build.py
13
+ """
14
+
15
+ import os
16
+ import re
17
+ import sys
18
+ import time
19
+ import zipfile
20
+ import subprocess
21
+ from pathlib import Path
22
+
23
+ # Paths relative to this script (inside scripts/)
24
+ SCRIPTS_DIR = Path(__file__).resolve().parent
25
+ PROJECT_ROOT = SCRIPTS_DIR.parent
26
+
27
+
28
+ def run_command(command_args, description):
29
+ print(f"\n>>> Running: {description}...")
30
+ try:
31
+ # Run sub-script and redirect output to stdout
32
+ result = subprocess.run(
33
+ [sys.executable] + command_args,
34
+ stdout=subprocess.PIPE,
35
+ stderr=subprocess.PIPE,
36
+ text=True,
37
+ check=True
38
+ )
39
+ print(result.stdout)
40
+ return True
41
+ except subprocess.CalledProcessError as e:
42
+ print(f" [ERROR] {description} failed.")
43
+ print(e.stdout)
44
+ print(e.stderr)
45
+ return False
46
+ except Exception as e:
47
+ print(f" [ERROR] Failed to run command: {e}")
48
+ return False
49
+
50
+
51
+ def update_cache_busting_versions():
52
+ print("\n>>> Running: index.html Cache-Busting Version Updates...")
53
+ index_path = PROJECT_ROOT / "index.html"
54
+ if not index_path.exists():
55
+ print(" [ERROR] index.html not found, skipping version updates.")
56
+ return
57
+
58
+ # Use current datetime stamp as unique version query string
59
+ version_stamp = time.strftime("%Y%m%d%H%M%S")
60
+ print(f" Target version code: {version_stamp}")
61
+
62
+ with open(index_path, "r", encoding="utf-8") as f:
63
+ content = f.read()
64
+
65
+ # Regex matches to replace query strings in href and src attributes
66
+ new_content = re.sub(
67
+ r'(href=["\']assets/css/style\.min\.css)\?v=[^"\'\s]*([\x22\x27])',
68
+ r'\1?v=' + version_stamp + r'\2',
69
+ content
70
+ )
71
+
72
+ new_content = re.sub(
73
+ r'(src=["\']assets/js/bundle\.min\.js)\?v=[^"\'\s]*([\x22\x27])',
74
+ r'\1?v=' + version_stamp + r'\2',
75
+ new_content
76
+ )
77
+
78
+ if content != new_content:
79
+ with open(index_path, "w", encoding="utf-8") as f:
80
+ f.write(new_content)
81
+ print(f" [SUCCESS] index.html asset references updated with version code '?v={version_stamp}'.")
82
+ else:
83
+ print(" [INFO] No version references updated in index.html.")
84
+
85
+
86
+ def package_release():
87
+ print("\n>>> Running: Packaging Clean Production Release...")
88
+ release_dir = PROJECT_ROOT / "release"
89
+ release_dir.mkdir(exist_ok=True)
90
+
91
+ zip_path = release_dir / "deploy.zip"
92
+ if zip_path.exists():
93
+ try:
94
+ zip_path.unlink()
95
+ except Exception as e:
96
+ print(f" [ERROR] Could not delete old release package: {e}")
97
+ sys.exit(1)
98
+
99
+ print(f" Creating ZIP archive: {zip_path.relative_to(PROJECT_ROOT)}")
100
+
101
+ try:
102
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
103
+ # 1. Add index.html
104
+ index_path = PROJECT_ROOT / "index.html"
105
+ if index_path.exists():
106
+ zipf.write(index_path, "index.html")
107
+ print(" + index.html")
108
+
109
+ # 2. Recurse assets/
110
+ assets_dir = PROJECT_ROOT / "assets"
111
+ added_count = 0
112
+ for root, _, files in os.walk(assets_dir):
113
+ for file in files:
114
+ file_path = Path(root) / file
115
+
116
+ # Compute relative path string
117
+ rel_path = file_path.relative_to(PROJECT_ROOT)
118
+ rel_path_str = rel_path.as_posix()
119
+
120
+ # Skip backups and system dotfiles
121
+ if "_backup" in file_path.name or file_path.name.startswith("."):
122
+ continue
123
+
124
+ # Clean production exclusions:
125
+ # - Under assets/css/: skip style.css (keep style.min.css)
126
+ if rel_path_str.startswith("assets/css/"):
127
+ if file_path.name == "style.css":
128
+ continue
129
+
130
+ # - Under assets/js/: skip main.js and unneeded library folders (keep only bundle.min.js)
131
+ if rel_path_str.startswith("assets/js/"):
132
+ if file_path.name != "bundle.min.js":
133
+ continue
134
+
135
+ zipf.write(file_path, rel_path)
136
+ added_count += 1
137
+ print(f" + assets/ ({added_count} files included, unminified files and libraries excluded)")
138
+
139
+ print(f" [SUCCESS] Created release package successfully: release/deploy.zip")
140
+ except Exception as e:
141
+ print(f" [ERROR] Failed to compile ZIP archive: {e}")
142
+ sys.exit(1)
143
+
144
+
145
+ def main():
146
+ start_time = time.time()
147
+
148
+ print("="*60)
149
+ print(" NOOR AL-ISLAM LANDING PAGE — PRODUCTION BUILDER")
150
+ print("="*60)
151
+
152
+ # 1. Run Minifier & Bundler
153
+ assets_success = run_command(
154
+ [str(SCRIPTS_DIR / "minify_assets.py")],
155
+ "Minifying and Bundling CSS & JS Assets"
156
+ )
157
+ if not assets_success:
158
+ print("Build aborted due to JS/CSS bundling errors.")
159
+ sys.exit(1)
160
+
161
+ # 2. Run Image Optimizer
162
+ images_success = run_command(
163
+ [str(SCRIPTS_DIR / "optimize_images.py"), "assets/image"],
164
+ "Optimizing Project Images"
165
+ )
166
+ if not images_success:
167
+ print(" [WARNING] Image optimization script reported warnings/errors. Continuing build...")
168
+
169
+ # 3. Run Build Integrity Tests
170
+ tests_success = run_command(
171
+ [str(SCRIPTS_DIR / "test_build.py")],
172
+ "Executing Automated Test Suite"
173
+ )
174
+ if not tests_success:
175
+ print("\n[FAIL] BUILD ABORTED: The automated test suite reported failures.")
176
+ print("Please resolve the failures listed above before deploying.")
177
+ sys.exit(1)
178
+
179
+ # 4. Cache Busting Version Updater
180
+ update_cache_busting_versions()
181
+
182
+ # 5. Package ZIP Release
183
+ package_release()
184
+
185
+ end_time = time.time()
186
+ elapsed = end_time - start_time
187
+
188
+ print("\n" + "="*60)
189
+ print(f"[SUCCESS] PRODUCTION BUILD & PACKAGING COMPLETED IN {elapsed:.2f}s!")
190
+ print("="*60)
191
+
192
+
193
+ if __name__ == "__main__":
194
+ main()
@@ -0,0 +1,52 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+
5
+ try:
6
+ from PIL import Image
7
+ except ImportError:
8
+ print("Pillow is required. Run: pip install Pillow", file=sys.stderr)
9
+ sys.exit(1)
10
+
11
+ def check_alpha(path):
12
+ if not os.path.exists(path):
13
+ print(f"- {path}: file not found")
14
+ return
15
+ try:
16
+ with Image.open(path) as img:
17
+ if img.mode == 'RGBA':
18
+ alpha = img.split()[3]
19
+ extrema = alpha.getextrema()
20
+ has_transparency = extrema[0] < 255
21
+ print(f"- {path}: RGBA alpha_range={extrema} transparent={has_transparency}")
22
+ else:
23
+ print(f"- {path}: {img.mode} no alpha channel")
24
+ except Exception as e:
25
+ print(f"- {path}: ERROR {e}")
26
+
27
+ def main():
28
+ parser = argparse.ArgumentParser(
29
+ description="Check whether images have an alpha (transparency) channel."
30
+ )
31
+ parser.add_argument(
32
+ "paths",
33
+ nargs="*",
34
+ help="One or more image file paths. If a path is a directory, all PNG/WEBP files inside it are checked.",
35
+ )
36
+ args = parser.parse_args()
37
+
38
+ if not args.paths:
39
+ parser.print_help()
40
+ sys.exit(0)
41
+
42
+ print("Alpha Channel Check:")
43
+ for p in args.paths:
44
+ if os.path.isdir(p):
45
+ for f in sorted(os.listdir(p)):
46
+ if f.lower().endswith((".png", ".webp", ".tga")):
47
+ check_alpha(os.path.join(p, f))
48
+ else:
49
+ check_alpha(p)
50
+
51
+ if __name__ == "__main__":
52
+ main()
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ extract_palette.py — Design System Palette & Color Extraction Tool
4
+ Extracts dominant brand colors, background tones, text colors, and WCAG contrast scores
5
+ from reference screenshots, logos, or hero images to generate brand.json and tokens.css.
6
+
7
+ Dependencies:
8
+ pip install pillow
9
+
10
+ Usage:
11
+ python scripts/extract_palette.py assets/logo.png --json brand.json
12
+ python scripts/extract_palette.py reference_shot.png --css design-system/tokens.css
13
+ """
14
+
15
+ import os
16
+ import sys
17
+ import json
18
+ import argparse
19
+ from PIL import Image
20
+
21
+ def get_rgb_luminance(rgb):
22
+ """Calculates relative luminance according to WCAG 2.1 specifications."""
23
+ r, g, b = [x / 255.0 for x in rgb]
24
+ r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4
25
+ g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4
26
+ b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4
27
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
28
+
29
+ def contrast_ratio(rgb1, rgb2):
30
+ """Calculates WCAG contrast ratio between two RGB colors (1 to 21)."""
31
+ l1 = get_rgb_luminance(rgb1)
32
+ l2 = get_rgb_luminance(rgb2)
33
+ lighter = max(l1, l2)
34
+ darker = min(l1, l2)
35
+ return (lighter + 0.05) / (darker + 0.05)
36
+
37
+ def rgb_to_hex(rgb):
38
+ return f"#{rgb[0]:02X}{rgb[1]:02X}{rgb[2]:02X}"
39
+
40
+ def extract_dominant_colors(image_path, num_colors=5):
41
+ if not os.path.exists(image_path):
42
+ print(f"ERROR: File not found: {image_path}", file=sys.stderr)
43
+ return None
44
+
45
+ img = Image.open(image_path).convert("RGB")
46
+ img.thumbnail((200, 200), Image.Resampling.LANCZOS)
47
+
48
+ # Quantize image to extract primary color palette
49
+ quantized = img.quantize(colors=num_colors, method=Image.Quantize.MEDIANCUT)
50
+ palette = quantized.getpalette()[:num_colors * 3]
51
+
52
+ colors = []
53
+ for i in range(0, len(palette), 3):
54
+ rgb = tuple(palette[i:i+3])
55
+ colors.append(rgb)
56
+
57
+ return colors
58
+
59
+ def main():
60
+ parser = argparse.ArgumentParser(description="Extract brand color palettes and WCAG contrast metrics.")
61
+ parser.add_argument("input", help="Path to input image (logo, screenshot, brand photo)")
62
+ parser.add_argument("--json", "-j", help="Output path for brand.json update")
63
+ parser.add_argument("--css", "-c", help="Output path for tokens.css variables")
64
+
65
+ args = parser.parse_args()
66
+
67
+ colors = extract_dominant_colors(args.input)
68
+ if not colors:
69
+ sys.exit(1)
70
+
71
+ hex_colors = [rgb_to_hex(c) for c in colors]
72
+
73
+ primary = hex_colors[0]
74
+ secondary = hex_colors[1] if len(hex_colors) > 1 else "#06B6D4"
75
+ accent = hex_colors[2] if len(hex_colors) > 2 else "#F59E0B"
76
+ dark_bg = "#0F172A"
77
+ light_bg = "#F8FAFC"
78
+
79
+ cr_on_dark = contrast_ratio(colors[0], (15, 23, 42))
80
+ cr_on_light = contrast_ratio(colors[0], (248, 250, 252))
81
+
82
+ if sys.platform == "win32":
83
+ try:
84
+ sys.stdout.reconfigure(encoding='utf-8')
85
+ except Exception:
86
+ pass
87
+ print(f"[OK] Extracted Palette from {args.input}:")
88
+ print(f" Primary: {primary} (WCAG Contrast vs Dark: {cr_on_dark:.2f}:1, Light: {cr_on_light:.2f}:1)")
89
+ print(f" Secondary: {secondary}")
90
+ print(f" Accent: {accent}")
91
+
92
+ if args.json:
93
+ brand_data = {}
94
+ if os.path.exists(args.json):
95
+ try:
96
+ with open(args.json, "r", encoding="utf-8") as f:
97
+ brand_data = json.load(f)
98
+ except Exception:
99
+ brand_data = {}
100
+
101
+ if "colors" not in brand_data or not isinstance(brand_data["colors"], dict):
102
+ brand_data["colors"] = {}
103
+
104
+ # Handle brand.json v2 dual-mode schema if present
105
+ if "light" in brand_data["colors"] and isinstance(brand_data["colors"]["light"], dict):
106
+ brand_data["colors"]["light"]["primary"] = primary
107
+ brand_data["colors"]["light"]["secondary"] = secondary
108
+ brand_data["colors"]["light"]["accent"] = accent
109
+ if "dark" in brand_data["colors"] and isinstance(brand_data["colors"]["dark"], dict):
110
+ brand_data["colors"]["dark"]["secondary"] = secondary
111
+ brand_data["colors"]["dark"]["accent"] = accent
112
+ else:
113
+ # Fallback for flat structure
114
+ brand_data["colors"]["primary"] = primary
115
+ brand_data["colors"]["secondary"] = secondary
116
+ brand_data["colors"]["accent"] = accent
117
+ brand_data["colors"]["neutralDark"] = dark_bg
118
+ brand_data["colors"]["neutralLight"] = light_bg
119
+
120
+ with open(args.json, "w", encoding="utf-8") as f:
121
+ json.dump(brand_data, f, indent=2)
122
+ print(f"✓ Palette updated in: {args.json}")
123
+
124
+ if args.css:
125
+ css_vars = f"""/* Auto-generated Design Tokens from {args.input} */
126
+ :root {{
127
+ --color-primary: {primary};
128
+ --color-secondary: {secondary};
129
+ --color-accent: {accent};
130
+ --color-dark: {dark_bg};
131
+ --color-light: {light_bg};
132
+ }}
133
+ """
134
+ with open(args.css, "a", encoding="utf-8") as f:
135
+ f.write(css_vars)
136
+ print(f"✓ CSS Variables appended to: {args.css}")
137
+
138
+ if __name__ == "__main__":
139
+ main()
@@ -0,0 +1,105 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+
5
+ try:
6
+ from PIL import Image
7
+ except ImportError:
8
+ print("Required package not installed. Run: pip install Pillow", file=sys.stderr)
9
+ sys.exit(1)
10
+
11
+ from _utils import crop_cover_16_9, natural_sort_key
12
+
13
+ TARGET_W, TARGET_H = 1280, 720
14
+
15
+
16
+ def main():
17
+ parser = argparse.ArgumentParser(
18
+ description=(
19
+ "Generate a JPEG frame-sequence from N keyframe images using eased cross-blends. "
20
+ "Output frames are named f000.jpg, f001.jpg, … and are suitable for scrubbing on a <canvas>."
21
+ ),
22
+ )
23
+ parser.add_argument(
24
+ "--keyframes-dir",
25
+ "-k",
26
+ required=True,
27
+ help="Directory containing numbered keyframe images (k1.png k2.png … or any sorted order).",
28
+ )
29
+ parser.add_argument(
30
+ "--output-dir",
31
+ "-o",
32
+ default="assets/seq",
33
+ help="Directory to write the output frame sequence (default: assets/seq).",
34
+ )
35
+ parser.add_argument(
36
+ "--src-dir",
37
+ help="Optional directory to save the normalised 1280x720 keyframes (default: <output-dir>/../seq-src).",
38
+ )
39
+ parser.add_argument(
40
+ "--total-frames",
41
+ "-f",
42
+ type=int,
43
+ default=93,
44
+ help="Total number of output frames (default: 93).",
45
+ )
46
+ parser.add_argument(
47
+ "--quality",
48
+ "-q",
49
+ type=int,
50
+ default=80,
51
+ help="JPEG quality for output frames (default: 80).",
52
+ )
53
+ args = parser.parse_args()
54
+
55
+ kf_dir = args.keyframes_dir
56
+ if not os.path.isdir(kf_dir):
57
+ print(f"Error: keyframes directory '{kf_dir}' does not exist.", file=sys.stderr)
58
+ sys.exit(1)
59
+
60
+ kf_files = sorted(
61
+ (f for f in os.listdir(kf_dir)
62
+ if os.path.splitext(f)[1].lower() in {".png", ".jpg", ".jpeg", ".webp"}),
63
+ key=natural_sort_key,
64
+ )
65
+ if len(kf_files) < 2:
66
+ print(f"Error: at least 2 keyframe images are required; found {len(kf_files)} in '{kf_dir}'.", file=sys.stderr)
67
+ sys.exit(1)
68
+
69
+ os.makedirs(args.output_dir, exist_ok=True)
70
+ src_dir = args.src_dir or os.path.join(os.path.dirname(args.output_dir), "seq-src")
71
+ os.makedirs(src_dir, exist_ok=True)
72
+
73
+ keyframes = []
74
+ for i, fname in enumerate(kf_files):
75
+ path = os.path.join(kf_dir, fname)
76
+ img = crop_cover_16_9(Image.open(path))
77
+ keyframes.append(img)
78
+ dest = os.path.join(src_dir, f"k{i + 1}.jpg")
79
+ img.save(dest, "JPEG", quality=90)
80
+ print(f"[keyframe] {path} -> {dest}")
81
+
82
+ n_segments = len(keyframes) - 1
83
+ total_frames = args.total_frames
84
+ seg_frames = total_frames // n_segments
85
+ remainder = total_frames - seg_frames * n_segments
86
+
87
+ frame_idx = 0
88
+ for seg in range(n_segments):
89
+ start_img = keyframes[seg]
90
+ end_img = keyframes[seg + 1]
91
+ count = seg_frames + (1 if seg < remainder else 0)
92
+ for j in range(count):
93
+ # When count == 1 the single frame is an unblended copy of start_img
94
+ # (t = 0 / 1 = 0, blend weight 0 → pure start_img).
95
+ t = j / max(count - 1, 1)
96
+ ease_t = t * t * (3.0 - 2.0 * t)
97
+ blended = Image.blend(start_img, end_img, ease_t)
98
+ dest = os.path.join(args.output_dir, f"f{frame_idx:03d}.jpg")
99
+ blended.save(dest, "JPEG", quality=args.quality)
100
+ frame_idx += 1
101
+
102
+ print(f"[done] Generated {frame_idx} frames in {args.output_dir}/")
103
+
104
+ if __name__ == "__main__":
105
+ main()
@@ -0,0 +1,48 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+
5
+ try:
6
+ from PIL import Image
7
+ except ImportError:
8
+ print("Pillow is required. Run: pip install Pillow", file=sys.stderr)
9
+ sys.exit(1)
10
+
11
+ IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".tiff", ".bmp", ".tga"}
12
+
13
+ def inspect(path):
14
+ try:
15
+ with Image.open(path) as img:
16
+ print(f"- {path}: size={img.size} format={img.format} mode={img.mode}")
17
+ except Exception as e:
18
+ print(f"- {path}: ERROR {e}")
19
+
20
+ def main():
21
+ parser = argparse.ArgumentParser(
22
+ description="Print size, format, and colour-mode of image files."
23
+ )
24
+ parser.add_argument(
25
+ "paths",
26
+ nargs="*",
27
+ help=(
28
+ "One or more image file paths or directories. "
29
+ "Directories are scanned for common image extensions."
30
+ ),
31
+ )
32
+ args = parser.parse_args()
33
+
34
+ if not args.paths:
35
+ parser.print_help()
36
+ sys.exit(0)
37
+
38
+ print("Image Inspection:")
39
+ for p in args.paths:
40
+ if os.path.isdir(p):
41
+ for f in sorted(os.listdir(p)):
42
+ if os.path.splitext(f)[1].lower() in IMAGE_EXTENSIONS:
43
+ inspect(os.path.join(p, f))
44
+ else:
45
+ inspect(p)
46
+
47
+ if __name__ == "__main__":
48
+ main()