Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,20 @@ When a form is submitted, the framework resolves the action in this order:
- `profile-progressive/` — Simple Tier 1 form with validation
- `live-preview/` — Tier 1 with `Change()` method for live updates
- `chat/` — Tier 1+2 (uses `lvt-scroll` for auto-scroll)

## CSS

All examples use [Pico CSS](https://picocss.com/docs) exclusively:

- Include via CDN: `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">`
- Use semantic HTML — Pico auto-styles: `<article>` (cards), `<dialog>` (modals), `<details>` (accordions), `<table>`, `<nav>`, `<progress>`
- Use Pico classes sparingly: `.container`, `.grid`, `.secondary`, `.contrast`, `.outline`
- Use `aria-invalid="true"` for form validation errors, `<small>` for helper/error text
- Use `<ins>` for success messages, `<del>` for error messages (with `style="display:block;text-decoration:none"`)
- Use `<mark>` for highlighted/badge text
- Use `<progress>` for progress bars
- Use `<hgroup>` for title + subtitle groupings
- Use `<fieldset role="group">` for inline input+button groups
- Use `<blockquote>` for callout/info boxes
- Do NOT write custom CSS. If Pico cannot express a style, ask before adding custom CSS.
- Pico CSS variables (`--pico-*`) may be used for theming when semantic markup is insufficient
332 changes: 54 additions & 278 deletions avatar-upload/avatar-upload.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,321 +4,97 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Avatar Upload Example - LiveTemplate</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}

.container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 100%;
padding: 40px;
}

h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}

.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}

.avatar-section {
text-align: center;
margin-bottom: 30px;
}

.avatar-display {
width: 150px;
height: 150px;
border-radius: 50%;
margin: 0 auto 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 48px;
font-weight: bold;
overflow: hidden;
border: 4px solid #f0f0f0;
}

.avatar-display img {
width: 100%;
height: 100%;
object-fit: cover;
}

.form-group {
margin-bottom: 20px;
}

label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}

input[type="text"],
input[type="email"] {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}

input[type="text"]:focus,
input[type="email"]:focus {
outline: none;
border-color: #667eea;
}

input[type="file"] {
width: 100%;
padding: 12px;
border: 2px dashed #e0e0e0;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}

input[type="file"]:hover {
border-color: #667eea;
background: #f8f9ff;
}

.upload-preview {
margin-top: 15px;
}

.upload-entry {
background: #f8f9fa;
padding: 12px;
border-radius: 8px;
margin-bottom: 10px;
}

.upload-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}

.upload-name {
font-weight: 500;
color: #333;
font-size: 14px;
}

.upload-progress {
color: #667eea;
font-size: 12px;
font-weight: 600;
}

.progress-bar {
width: 100%;
height: 4px;
background: #e0e0e0;
border-radius: 2px;
overflow: hidden;
}

.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s;
}

.error {
color: #dc3545;
font-size: 12px;
margin-top: 5px;
}

.success {
color: #28a745;
font-size: 12px;
margin-top: 5px;
}

.uploading {
color: #667eea;
font-size: 12px;
margin-top: 5px;
}

.upload-status {
min-height: 20px;
}

button {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}

button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
}

button:active {
transform: translateY(0);
}

.info-box {
background: #f8f9ff;
border-left: 4px solid #667eea;
padding: 12px;
margin-bottom: 20px;
border-radius: 4px;
font-size: 13px;
color: #555;
}

.powered-by {
text-align: center;
margin-top: 30px;
color: #999;
font-size: 12px;
}

.powered-by a {
color: #667eea;
text-decoration: none;
}
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
</head>
<body>
<div class="container">
<h1>Profile Settings</h1>
<p class="subtitle">Upload your avatar and update your profile</p>

<div class="info-box">
📸 Accepted: JPEG, PNG, GIF • Max size: 5MB • Click "Save Profile" to upload
</div>

<div class="avatar-section">
<div class="avatar-display">
<main class="container">
<article>
<header>
<h1>Profile Settings</h1>
<small>Upload your avatar and update your profile</small>
</header>

<blockquote>
Accepted: JPEG, PNG, GIF — Max size: 5MB — Click "Save Profile" to upload
</blockquote>

<div>
{{if .AvatarURL}}
<img src="{{.AvatarURL}}" alt="Avatar">
{{else}}
{{slice .Name 0 1}}
<mark>{{slice .Name 0 1}}</mark>
{{end}}
</div>
</div>

<form method="POST" name="updateProfile">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" value="{{.Name}}" required>
</div>

<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" value="{{.Email}}" required>
</div>

<div class="form-group">
<label for="avatar">Avatar</label>
<input type="file"
id="avatar"
lvt-upload="avatar"
accept="image/jpeg,image/png,image/gif">
</div>
<form method="POST" name="updateProfile">
<label for="name">
Name
<input type="text" id="name" name="name" value="{{.Name}}" required>
</label>

<label for="email">
Email
<input type="email" id="email" name="email" value="{{.Email}}" required>
</label>

<label for="avatar">
Avatar
<input type="file"
id="avatar"
lvt-upload="avatar"
accept="image/jpeg,image/png,image/gif">
</label>

<div class="upload-preview">
{{range .lvt.Uploads "avatar"}}
<div class="upload-entry">
<div class="upload-info">
<span class="upload-name">{{.ClientName}}</span>
<span class="upload-progress">{{.Progress}}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: {{.Progress}}%"></div>
</div>
<div class="upload-status">
<div>
<small><strong>{{.ClientName}}</strong> — {{.Progress}}%</small>
<progress value="{{.Progress}}" max="100"></progress>
{{if .Error}}
<div class="error">❌ {{.Error}}</div>
<del style="display:block;text-decoration:none">{{.Error}}</del>
{{else if .Done}}
<div class="success">✅ Upload complete!</div>
<ins style="display:block;text-decoration:none">Upload complete!</ins>
{{else}}
<div class="uploading">⏳ Uploading...</div>
<small>Uploading...</small>
{{end}}
</div>
</div>
{{end}}

{{if .lvt.HasUploadError "avatar"}}
<div class="error">⚠️ {{.lvt.UploadError "avatar"}}</div>
<del style="display:block;text-decoration:none">{{.lvt.UploadError "avatar"}}</del>
{{end}}
</div>

<button type="submit">Save Profile</button>
</form>
<button type="submit">Save Profile</button>
</form>

<div class="powered-by">
Powered by <a href="https://github.com/livetemplate/livetemplate" target="_blank">LiveTemplate v0.3.0</a>
</div>
</div>
<footer>
<small>Powered by <a href="https://github.com/livetemplate/livetemplate" target="_blank">LiveTemplate v0.3.0</a></small>
</footer>
</article>
</main>

{{if .lvt.DevMode}}
<script src="/livetemplate-client.js"></script>
{{else}}
<script src="https://cdn.jsdelivr.net/npm/@livetemplate/client@latest/dist/livetemplate-client.browser.js"></script>
{{end}}
<script>
// Listen for upload events
document.addEventListener('DOMContentLoaded', () => {
const wrapper = document.querySelector('[data-lvt-id]');
if (wrapper) {
wrapper.addEventListener('lvt:upload:progress', (e) => {
console.log('📤 Upload progress:', e.detail.entry.file.name, e.detail.entry.progress + '%');
console.log('Upload progress:', e.detail.entry.file.name, e.detail.entry.progress + '%');
});

wrapper.addEventListener('lvt:upload:complete', (e) => {
console.log('Upload complete:', e.detail.uploadName);
console.log('Upload complete:', e.detail.uploadName);
});

wrapper.addEventListener('lvt:upload:error', (e) => {
console.error('Upload error:', e.detail.error);
console.error('Upload error:', e.detail.error);
});
}
});
</script>
{{if .lvt.DevMode}}
<script src="/livetemplate-client.js"></script>
{{else}}
<script src="https://cdn.jsdelivr.net/npm/@livetemplate/client@latest/dist/livetemplate-client.browser.js"></script>
{{end}}
</body>
</html>
Loading
Loading