feat: update desktop workflows and app center
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Zara Zhang
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,110 @@
|
||||
# Animation Patterns Reference
|
||||
|
||||
Use this reference when generating presentations. Match animations to the intended feeling.
|
||||
|
||||
## Effect-to-Feeling Guide
|
||||
|
||||
| Feeling | Animations | Visual Cues |
|
||||
|---------|-----------|-------------|
|
||||
| **Dramatic / Cinematic** | Slow fade-ins (1-1.5s), large scale transitions (0.9 to 1), parallax scrolling | Dark backgrounds, spotlight effects, full-bleed images |
|
||||
| **Techy / Futuristic** | Neon glow (box-shadow), glitch/scramble text, grid reveals | Particle systems (canvas), grid patterns, monospace accents, cyan/magenta/electric blue |
|
||||
| **Playful / Friendly** | Bouncy easing (spring physics), floating/bobbing | Rounded corners, pastel/bright colors, hand-drawn elements |
|
||||
| **Professional / Corporate** | Subtle fast animations (200-300ms), clean slides | Navy/slate/charcoal, precise spacing, data visualization focus |
|
||||
| **Calm / Minimal** | Very slow subtle motion, gentle fades | High whitespace, muted palette, serif typography, generous padding |
|
||||
| **Editorial / Magazine** | Staggered text reveals, image-text interplay | Strong type hierarchy, pull quotes, grid-breaking layouts, serif headlines + sans body |
|
||||
|
||||
## Entrance Animations
|
||||
|
||||
```css
|
||||
/* Fade + Slide Up (most versatile) */
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: opacity 0.6s var(--ease-out-expo),
|
||||
transform 0.6s var(--ease-out-expo);
|
||||
}
|
||||
.visible .reveal {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Scale In */
|
||||
.reveal-scale {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
transition: opacity 0.6s, transform 0.6s var(--ease-out-expo);
|
||||
}
|
||||
|
||||
/* Slide from Left */
|
||||
.reveal-left {
|
||||
opacity: 0;
|
||||
transform: translateX(-50px);
|
||||
transition: opacity 0.6s, transform 0.6s var(--ease-out-expo);
|
||||
}
|
||||
|
||||
/* Blur In */
|
||||
.reveal-blur {
|
||||
opacity: 0;
|
||||
filter: blur(10px);
|
||||
transition: opacity 0.8s, filter 0.8s var(--ease-out-expo);
|
||||
}
|
||||
```
|
||||
|
||||
## Background Effects
|
||||
|
||||
```css
|
||||
/* Gradient Mesh — layered radial gradients for depth */
|
||||
.gradient-bg {
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 80%, rgba(120, 0, 255, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 20%, rgba(0, 255, 200, 0.2) 0%, transparent 50%),
|
||||
var(--bg-primary);
|
||||
}
|
||||
|
||||
/* Noise Texture — inline SVG for grain */
|
||||
.noise-bg {
|
||||
background-image: url("data:image/svg+xml,..."); /* Inline SVG noise */
|
||||
}
|
||||
|
||||
/* Grid Pattern — subtle structural lines */
|
||||
.grid-bg {
|
||||
background-image:
|
||||
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
}
|
||||
```
|
||||
|
||||
## Interactive Effects
|
||||
|
||||
```javascript
|
||||
/* 3D Tilt on Hover — adds depth to cards/panels */
|
||||
class TiltEffect {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
this.element.style.transformStyle = 'preserve-3d';
|
||||
this.element.style.perspective = '1000px';
|
||||
|
||||
this.element.addEventListener('mousemove', (e) => {
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / rect.width - 0.5;
|
||||
const y = (e.clientY - rect.top) / rect.height - 0.5;
|
||||
this.element.style.transform = `rotateY(${x * 10}deg) rotateX(${-y * 10}deg)`;
|
||||
});
|
||||
|
||||
this.element.addEventListener('mouseleave', () => {
|
||||
this.element.style.transform = 'rotateY(0) rotateX(0)';
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| Fonts not loading | Check Fontshare/Google Fonts URL; ensure font names match in CSS |
|
||||
| Animations not triggering | Verify Intersection Observer is running; check `.visible` class is being added |
|
||||
| Scroll snap not working | Ensure `scroll-snap-type: y mandatory` on html; each slide needs `scroll-snap-align: start` |
|
||||
| Mobile issues | Disable heavy effects at 768px breakpoint; test touch events; reduce particle count |
|
||||
| Performance issues | Use `will-change` sparingly; prefer `transform`/`opacity` animations; throttle scroll handlers |
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Extract all content from a PowerPoint file (.pptx).
|
||||
Returns a JSON structure with slides, text, and images.
|
||||
|
||||
Usage:
|
||||
python extract-pptx.py <input.pptx> [output_dir]
|
||||
|
||||
Requires: pip install python-pptx
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pptx import Presentation
|
||||
|
||||
|
||||
def extract_pptx(file_path, output_dir="."):
|
||||
"""
|
||||
Extract all content from a PowerPoint file.
|
||||
Returns a list of slide data dicts with text, images, and notes.
|
||||
"""
|
||||
prs = Presentation(file_path)
|
||||
slides_data = []
|
||||
|
||||
# Create assets directory for extracted images
|
||||
assets_dir = os.path.join(output_dir, "assets")
|
||||
os.makedirs(assets_dir, exist_ok=True)
|
||||
|
||||
for slide_num, slide in enumerate(prs.slides):
|
||||
slide_data = {
|
||||
"number": slide_num + 1,
|
||||
"title": "",
|
||||
"content": [],
|
||||
"images": [],
|
||||
"notes": "",
|
||||
}
|
||||
|
||||
for shape in slide.shapes:
|
||||
# Extract text content
|
||||
if shape.has_text_frame:
|
||||
if shape == slide.shapes.title:
|
||||
slide_data["title"] = shape.text
|
||||
else:
|
||||
slide_data["content"].append(
|
||||
{"type": "text", "content": shape.text}
|
||||
)
|
||||
|
||||
# Extract images
|
||||
if shape.shape_type == 13: # Picture type
|
||||
image = shape.image
|
||||
image_bytes = image.blob
|
||||
image_ext = image.ext
|
||||
image_name = f"slide{slide_num + 1}_img{len(slide_data['images']) + 1}.{image_ext}"
|
||||
image_path = os.path.join(assets_dir, image_name)
|
||||
|
||||
with open(image_path, "wb") as f:
|
||||
f.write(image_bytes)
|
||||
|
||||
slide_data["images"].append(
|
||||
{
|
||||
"path": f"assets/{image_name}",
|
||||
"width": shape.width,
|
||||
"height": shape.height,
|
||||
}
|
||||
)
|
||||
|
||||
# Extract speaker notes
|
||||
if slide.has_notes_slide:
|
||||
notes_frame = slide.notes_slide.notes_text_frame
|
||||
slide_data["notes"] = notes_frame.text
|
||||
|
||||
slides_data.append(slide_data)
|
||||
|
||||
return slides_data
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python extract-pptx.py <input.pptx> [output_dir]")
|
||||
sys.exit(1)
|
||||
|
||||
input_file = sys.argv[1]
|
||||
output_dir = sys.argv[2] if len(sys.argv) > 2 else "."
|
||||
|
||||
slides = extract_pptx(input_file, output_dir)
|
||||
|
||||
# Write extracted data as JSON
|
||||
output_path = os.path.join(output_dir, "extracted-slides.json")
|
||||
with open(output_path, "w") as f:
|
||||
json.dump(slides, f, indent=2)
|
||||
|
||||
print(f"Extracted {len(slides)} slides to {output_path}")
|
||||
for s in slides:
|
||||
img_count = len(s["images"])
|
||||
print(f" Slide {s['number']}: {s['title'] or '(no title)'} — {img_count} image(s)")
|
||||
@@ -0,0 +1,153 @@
|
||||
/* ===========================================
|
||||
VIEWPORT FITTING: MANDATORY BASE STYLES
|
||||
Include this ENTIRE file in every presentation.
|
||||
These styles ensure slides fit exactly in the viewport.
|
||||
=========================================== */
|
||||
|
||||
/* 1. Lock html/body to viewport */
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-snap-type: y mandatory;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* 2. Each slide = exact viewport height */
|
||||
.slide {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100dvh; /* Dynamic viewport height for mobile browsers */
|
||||
overflow: hidden; /* CRITICAL: Prevent ANY overflow */
|
||||
scroll-snap-align: start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 3. Content container with flex for centering */
|
||||
.slide-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
max-height: 100%;
|
||||
overflow: hidden; /* Double-protection against overflow */
|
||||
padding: var(--slide-padding);
|
||||
}
|
||||
|
||||
/* 4. ALL typography uses clamp() for responsive scaling */
|
||||
:root {
|
||||
/* Titles scale from mobile to desktop */
|
||||
--title-size: clamp(1.5rem, 5vw, 4rem);
|
||||
--h2-size: clamp(1.25rem, 3.5vw, 2.5rem);
|
||||
--h3-size: clamp(1rem, 2.5vw, 1.75rem);
|
||||
|
||||
/* Body text */
|
||||
--body-size: clamp(0.75rem, 1.5vw, 1.125rem);
|
||||
--small-size: clamp(0.65rem, 1vw, 0.875rem);
|
||||
|
||||
/* Spacing scales with viewport */
|
||||
--slide-padding: clamp(1rem, 4vw, 4rem);
|
||||
--content-gap: clamp(0.5rem, 2vw, 2rem);
|
||||
--element-gap: clamp(0.25rem, 1vw, 1rem);
|
||||
}
|
||||
|
||||
/* 5. Cards/containers use viewport-relative max sizes */
|
||||
.card, .container, .content-box {
|
||||
max-width: min(90vw, 1000px);
|
||||
max-height: min(80vh, 700px);
|
||||
}
|
||||
|
||||
/* 6. Lists auto-scale with viewport */
|
||||
.feature-list, .bullet-list {
|
||||
gap: clamp(0.4rem, 1vh, 1rem);
|
||||
}
|
||||
|
||||
.feature-list li, .bullet-list li {
|
||||
font-size: var(--body-size);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 7. Grids adapt to available space */
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
|
||||
gap: clamp(0.5rem, 1.5vw, 1rem);
|
||||
}
|
||||
|
||||
/* 8. Images constrained to viewport */
|
||||
img, .image-container {
|
||||
max-width: 100%;
|
||||
max-height: min(50vh, 400px);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
RESPONSIVE BREAKPOINTS
|
||||
Aggressive scaling for smaller viewports
|
||||
=========================================== */
|
||||
|
||||
/* Short viewports (< 700px height) */
|
||||
@media (max-height: 700px) {
|
||||
:root {
|
||||
--slide-padding: clamp(0.75rem, 3vw, 2rem);
|
||||
--content-gap: clamp(0.4rem, 1.5vw, 1rem);
|
||||
--title-size: clamp(1.25rem, 4.5vw, 2.5rem);
|
||||
--h2-size: clamp(1rem, 3vw, 1.75rem);
|
||||
}
|
||||
}
|
||||
|
||||
/* Very short viewports (< 600px height) */
|
||||
@media (max-height: 600px) {
|
||||
:root {
|
||||
--slide-padding: clamp(0.5rem, 2.5vw, 1.5rem);
|
||||
--content-gap: clamp(0.3rem, 1vw, 0.75rem);
|
||||
--title-size: clamp(1.1rem, 4vw, 2rem);
|
||||
--body-size: clamp(0.7rem, 1.2vw, 0.95rem);
|
||||
}
|
||||
|
||||
/* Hide non-essential elements */
|
||||
.nav-dots, .keyboard-hint, .decorative {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Extremely short (landscape phones, < 500px height) */
|
||||
@media (max-height: 500px) {
|
||||
:root {
|
||||
--slide-padding: clamp(0.4rem, 2vw, 1rem);
|
||||
--title-size: clamp(1rem, 3.5vw, 1.5rem);
|
||||
--h2-size: clamp(0.9rem, 2.5vw, 1.25rem);
|
||||
--body-size: clamp(0.65rem, 1vw, 0.85rem);
|
||||
}
|
||||
}
|
||||
|
||||
/* Narrow viewports (< 600px width) */
|
||||
@media (max-width: 600px) {
|
||||
:root {
|
||||
--title-size: clamp(1.25rem, 7vw, 2.5rem);
|
||||
}
|
||||
|
||||
/* Stack grids vertically */
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
REDUCED MOTION
|
||||
Respect user preferences
|
||||
=========================================== */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.2s !important;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user