added claude design's ui framework

This commit is contained in:
2026-08-22 18:13:36 -04:00
parent cac26286b1
commit 18b36870d4
23 changed files with 2446 additions and 0 deletions
@@ -0,0 +1,254 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="_ds/organic-9b68eb3f-20a3-40f4-b132-a5a6542b5b19/styles.css">
<script src="pansy-theme.js"></script>
<script src="_ds/organic-9b68eb3f-20a3-40f4-b132-a5a6542b5b19/_ds_bundle.js"></script>
<style>
:root{--p-field:#efe3c9;--p-grid-ink:#201e1d;--p-ink-strong:#474238;--p-ink-soft:#645c50;--p-ink-mute:#82796a;--p-bed-fill:#e3d0ac;--p-bed-stroke:#b5a37f;--p-ing-fill:#d6bf98;--p-ing-stroke:#b1a07c;--p-path-fill:#ece1cb;--p-path-stroke:#cabfa6;--p-bag-fill:#d8c5a2;--p-bag-stroke:#a99677;--p-bkt-fill:#cfc3ad;--p-bkt-stroke:#9a8d76;--p-tree-fill:#dce9c6;--p-tree-stroke:#8fa073;--p-str-fill:#d9cfbc;--p-str-stroke:#a3947c}
html,body{margin:0;min-height:100%}
a{color:var(--color-accent)} a:hover{color:var(--color-accent-600)}
</style>
</helmet>
<div data-screen-label="Plants" style="min-height:100vh;background:var(--color-bg);font-family:var(--font-body);color:var(--color-text)">
<nav class="nav">
<span class="nav-brand" style="display:flex;align-items:center;gap:8px">
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent-2-600)" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M7 20h10"></path><path d="M10 20c5.5-2.5.8-6.4 3-10"></path><path d="M9.5 9.4c1.1.8 1.8 2.2 2.3 3.7-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2 2.8-.5 4.4 0 5.5.8z"></path><path d="M14.1 6a7 7 0 0 0-1.1 4c1.9-.1 3.3-.6 4.3-1.4 1-1 1.6-2.3 1.7-4.6-2.7.1-4 1-4.9 2z"></path></svg>
pansy
</span>
<a href="Pansy Gardens.dc.html">Gardens</a>
<a href="Pansy Plants.dc.html" aria-current="page">Plants</a>
<span style="margin-left:auto;display:flex;align-items:center;gap:10px">
<button class="btn btn-icon btn-secondary" style="border-radius:999px" onClick="{{ onTheme }}" title="{{ themeTitle }}">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="{{ themeD1 }}"></path><path d="{{ themeD2 }}"></path><path d="{{ themeD3 }}"></path></svg>
</button>
<a href="Pansy Settings.dc.html" class="btn btn-icon btn-secondary" style="border-radius:999px" title="Settings">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0 2-2z"></path></svg>
</a>
<a href="Pansy Login.dc.html" style="width:32px;height:32px;border-radius:999px;background:var(--color-accent-2-300);color:var(--color-accent-2-800);display:inline-flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;text-decoration:none">S</a>
</span>
</nav>
<div style="max-width:1080px;margin:0 auto;padding:28px 24px 56px">
<div style="display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin-bottom:18px">
<h2 style="margin:0">Plants</h2>
<span style="font-size:14px;color:var(--p-ink-mute);font-weight:600">The built-ins plus everything you've added</span>
<span style="margin-left:auto;display:flex;gap:8px">
<button class="btn btn-secondary" style="border-radius:999px" onClick="{{ onScan }}">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"></path><circle cx="12" cy="13" r="3"></circle></svg>
Scan a packet
</button>
<button class="btn btn-primary" style="border-radius:999px" onClick="{{ onAdd }}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round"><path d="M5 12h14"></path><path d="M12 5v14"></path></svg>
Add a plant
</button>
</span>
</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:20px">
<input class="input" style="border-radius:999px;max-width:260px" placeholder="Find a plant…" value="{{ q }}" onChange="{{ onQ }}">
<sc-for list="{{ cats }}" as="c">
<button onClick="{{ c.onClick }}" style="{{ c.style }}">{{ c.label }}</button>
</sc-for>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px">
<sc-for list="{{ cardsR }}" as="p" hint-placeholder-count="8">
<div onClick="{{ p.onClick }}" style="{{ p.cardStyle }}" style-hover="box-shadow:var(--shadow-md)">
<div style="display:flex;align-items:center;gap:12px">
<svg width="40" height="40" style="flex:none"><circle cx="20" cy="20" r="20" fill="{{ p.color }}"></circle></svg>
<span style="position:absolute;width:40px;text-align:center;color:#fffaf1;font-family:var(--font-heading);font-size:17px;pointer-events:none">{{ p.letter }}</span>
<span style="display:flex;flex-direction:column;gap:1px;min-width:0">
<span style="font-family:var(--font-heading);font-size:16.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ p.name }}</span>
<span style="font-size:12px;color:var(--p-ink-mute);font-weight:600">{{ p.sub }}</span>
</span>
<sc-if value="{{ p.builtin }}" hint-placeholder-val="{{ false }}"><span class="tag tag-neutral" style="border-radius:999px;margin-left:auto;flex:none">built-in</span></sc-if>
</div>
<div style="font-size:12.5px;color:var(--p-ink-soft);margin-top:10px">{{ p.lotText }}</div>
<sc-if value="{{ p.open }}" hint-placeholder-val="{{ false }}">
<div style="margin-top:10px;display:flex;flex-direction:column;gap:8px">
<sc-for list="{{ p.lots }}" as="l" hint-placeholder-count="1">
<div style="background:var(--color-bg);border:1px solid var(--color-divider);border-radius:var(--radius-md);padding:10px 13px">
<div style="display:flex;gap:6px;align-items:center">
<span style="font-size:12.5px;font-weight:700">{{ l.vendor }}</span>
<span style="font-size:11.5px;color:var(--p-ink-mute);margin-left:auto">packed for {{ l.year }}</span>
</div>
<div style="font-size:12px;color:var(--p-ink-soft);margin-top:3px">{{ l.detail }}</div>
</div>
</sc-for>
<sc-if value="{{ p.noLots }}"><div style="font-size:12.5px;color:var(--p-ink-mute)">No seed lots yet — scan a packet to add one.</div></sc-if>
</div>
</sc-if>
</div>
</sc-for>
</div>
</div>
<sc-if value="{{ dlgScan }}" hint-placeholder-val="{{ false }}">
<div style="position:fixed;inset:0;background:color-mix(in srgb, #201e1d 40%, transparent);display:flex;align-items:center;justify-content:center;padding:20px;z-index:50" onClick="{{ onCloseDlg }}">
<div style="background:var(--color-neutral-100);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);padding:26px;width:min(560px,100%);display:flex;flex-direction:column;gap:14px" onClick="{{ stop }}">
<h3 style="margin:0;font-size:22px">Scan a seed packet</h3>
<sc-if value="{{ scanStep0 }}" hint-placeholder-val="{{ true }}">
<div style="border:2px dashed var(--color-neutral-400);border-radius:var(--radius-lg);padding:36px 20px;display:flex;flex-direction:column;align-items:center;gap:10px;text-align:center">
<svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent-2-600)" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"></path><circle cx="12" cy="13" r="3"></circle></svg>
<div style="font-size:14px;font-weight:600">Photograph the packet — front is enough</div>
<div style="font-size:12.5px;color:var(--p-ink-mute);max-width:36ch">The vision model reads it into fields. It only reads; nothing is saved until you confirm.</div>
<button class="btn btn-primary" style="border-radius:999px;margin-top:4px" onClick="{{ onScanGo }}">Use a sample photo</button>
</div>
</sc-if>
<sc-if value="{{ scanStep1 }}" hint-placeholder-val="{{ false }}">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div class="field"><label>Variety</label><input class="input" style="border-radius:999px" value="Garlic — Music"></div>
<div class="field"><label>Vendor</label><input class="input" style="border-radius:999px" value="Keene Organics"></div>
<div class="field"><label>Packed for</label><input class="input" style="border-radius:999px" value="2026"></div>
<div class="field"><label>Quantity</label><input class="input" style="border-radius:999px" value="50 cloves"></div>
</div>
<div style="font-size:12.5px;font-weight:700;color:var(--p-ink-soft);margin-top:2px">Match it to your catalog — nothing is auto-created:</div>
<sc-for list="{{ matches }}" as="m">
<button onClick="{{ m.onClick }}" style="{{ m.style }}">
<span style="font-size:13.5px;font-weight:700">{{ m.label }}</span>
<span style="font-size:12px;color:var(--p-ink-mute);margin-left:auto">{{ m.sub }}</span>
</button>
</sc-for>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:4px">
<button class="btn btn-secondary" style="border-radius:999px" onClick="{{ onCloseDlg }}">Cancel</button>
<button class="btn btn-primary" style="border-radius:999px" onClick="{{ onConfirmScan }}">Add the lot</button>
</div>
</sc-if>
</div>
</div>
</sc-if>
<sc-if value="{{ dlgAdd }}" hint-placeholder-val="{{ false }}">
<div style="position:fixed;inset:0;background:color-mix(in srgb, #201e1d 40%, transparent);display:flex;align-items:center;justify-content:center;padding:20px;z-index:50" onClick="{{ onCloseDlg }}">
<div style="background:var(--color-neutral-100);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);padding:26px;width:min(420px,100%);display:flex;flex-direction:column;gap:14px" onClick="{{ stop }}">
<h3 style="margin:0;font-size:22px">A new plant</h3>
<div class="field"><label>Name</label><input class="input" style="border-radius:999px" value="{{ aName }}" onChange="{{ onAName }}" placeholder="Delicata squash"></div>
<div style="display:flex;gap:10px">
<div class="field" style="flex:1"><label>Category</label>
<select class="input" style="border-radius:999px" value="{{ aCat }}" onChange="{{ onACat }}">
<option value="vegetable">Vegetable</option><option value="herb">Herb</option><option value="flower">Flower</option><option value="fruit">Fruit</option>
</select>
</div>
<div class="field" style="flex:1"><label>Spacing (in)</label><input class="input" style="border-radius:999px" type="number" value="{{ aSp }}" onChange="{{ onASp }}"></div>
</div>
<div class="field"><label>Marker color</label>
<div style="display:flex;gap:8px">
<sc-for list="{{ swatches }}" as="sw">
<button onClick="{{ sw.onClick }}" style="{{ sw.style }}" title="{{ sw.hex }}"></button>
</sc-for>
</div>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="btn btn-secondary" style="border-radius:999px" onClick="{{ onCloseDlg }}">Never mind</button>
<button class="btn btn-primary" style="border-radius:999px" onClick="{{ onCreatePlant }}">Add it</button>
</div>
</div>
</div>
</sc-if>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script>
class Component extends DCLogic {
constructor(props) {
super(props);
this.state = {
q: '', cat: 'all', openId: null, dlg: null, scanStep: 0, matchSel: 1,
aName: '', aCat: 'vegetable', aSp: 12, aColor: '#97a97c',
plants: [
{ id: 1, name: 'Garlic', letter: 'G', color: '#97a97c', cat: 'vegetable', spacing: '6″', days: 240, builtin: true, lots: [{ vendor: 'Johnny\u2019s \u2014 Music', year: 2025, detail: '50 cloves \u00b7 ~14 left after this season\u2019s beds' }] },
{ id: 2, name: 'Tomato — Cherokee Purple', letter: 'T', color: '#c8553d', cat: 'vegetable', spacing: '24″', days: 80, builtin: false, lots: [{ vendor: 'Baker Creek', year: 2026, detail: '25 seeds \u00b7 95% germination \u00b7 ~22 left' }] },
{ id: 3, name: 'Cucumber — Marketmore', letter: 'C', color: '#6f8f4f', cat: 'vegetable', spacing: '12″', days: 65, builtin: false, lots: [] },
{ id: 4, name: 'Watermelon — Sugar Baby', letter: 'W', color: '#46683c', cat: 'fruit', spacing: '36″', days: 78, builtin: false, lots: [{ vendor: 'Ferry-Morse', year: 2025, detail: '20 seeds \u00b7 ~16 left' }] },
{ id: 5, name: 'Basil — Genovese', letter: 'B', color: '#5f8f45', cat: 'herb', spacing: '10″', days: 60, builtin: true, lots: [] },
{ id: 6, name: 'Pepper — Jalapeño', letter: 'P', color: '#b2622d', cat: 'vegetable', spacing: '18″', days: 75, builtin: false, lots: [] },
{ id: 7, name: 'Marigold', letter: 'Ma', color: '#d9912f', cat: 'flower', spacing: '8″', days: 50, builtin: true, lots: [] },
{ id: 8, name: 'Melon — Hale\u2019s Best', letter: 'Me', color: '#c2913a', cat: 'fruit', spacing: '36″', days: 85, builtin: false, lots: [] },
{ id: 9, name: 'Carrot — Danvers', letter: 'Cr', color: '#d07a2e', cat: 'vegetable', spacing: '3″', days: 70, builtin: true, lots: [] },
{ id: 10, name: 'Bush bean — Provider', letter: 'Bn', color: '#7c9a55', cat: 'vegetable', spacing: '6″', days: 55, builtin: true, lots: [] },
{ id: 11, name: 'Zinnia', letter: 'Z', color: '#c65a4e', cat: 'flower', spacing: '10″', days: 60, builtin: true, lots: [] },
{ id: 12, name: 'Thyme', letter: 'Th', color: '#6d7f5a', cat: 'herb', spacing: '8″', days: 90, builtin: true, lots: [] },
],
};
this.nid = 100;
}
componentDidMount() { this.themeMount(); }
componentWillUnmount() { this._unwatchTheme && this._unwatchTheme(); }
themeMount() {
const PT = window.PansyTheme; if (!PT) return;
this.setState({ themePref: PT.get() });
this._unwatchTheme = PT.watch(() => { if ((this.state.themePref || 'system') === 'system') PT.apply(PT.isDark('system')); });
}
cycleTheme = () => { const PT = window.PansyTheme; if (!PT) return; const p = PT.next(this.state.themePref || 'system'); PT.set(p); PT.apply(PT.isDark(p)); this.setState({ themePref: p }); };
themeVals() {
const p = this.state.themePref || 'system';
const d = window.PansyTheme ? window.PansyTheme.icon(p) : ['', '', ''];
return { themeD1: d[0], themeD2: d[1], themeD3: d[2], onTheme: this.cycleTheme, themeTitle: 'Theme: ' + p };
}
renderVals() {
const st = this.state;
const ql = st.q.trim().toLowerCase();
const catList = [['all', 'All'], ['vegetable', 'Vegetables'], ['herb', 'Herbs'], ['flower', 'Flowers'], ['fruit', 'Fruit']];
const shown = st.plants.filter(p => (st.cat === 'all' || p.cat === st.cat) && (!ql || p.name.toLowerCase().includes(ql)));
return {
...this.themeVals(),
q: st.q, onQ: (e) => this.setState({ q: e.target.value }),
cats: catList.map(([id, label]) => ({
key: id, label,
style: { cursor: 'pointer', border: '1px solid ' + (st.cat === id ? 'var(--color-accent-400)' : 'var(--color-divider)'), borderRadius: '999px', padding: '7px 16px', fontFamily: 'var(--font-body)', fontSize: '13px', fontWeight: 700, background: st.cat === id ? 'var(--color-accent-200)' : 'transparent', color: st.cat === id ? 'var(--color-accent-800)' : 'var(--p-ink-soft)' },
onClick: () => this.setState({ cat: id }),
})),
cardsR: shown.map(p => ({
key: p.id, name: p.name, letter: p.letter, color: p.color, builtin: p.builtin,
sub: p.cat[0].toUpperCase() + p.cat.slice(1) + ' · ' + p.spacing + ' spacing · ' + p.days + ' days',
lotText: p.lots.length ? p.lots.length + (p.lots.length === 1 ? ' seed lot' : ' seed lots') + ' · click to see' : 'No seed lots · click to expand',
open: st.openId === p.id,
lots: p.lots.map((l, i) => ({ key: i, ...l })),
noLots: !p.lots.length,
cardStyle: { background: 'var(--color-neutral-100)', border: '1px solid ' + (st.openId === p.id ? 'var(--color-accent-400)' : 'var(--color-divider)'), borderRadius: 'var(--radius-lg)', padding: '16px 18px', cursor: 'pointer', position: 'relative' },
onClick: () => this.setState(s2 => ({ openId: s2.openId === p.id ? null : p.id })),
})),
dlgScan: st.dlg === 'scan', dlgAdd: st.dlg === 'add',
scanStep0: st.scanStep === 0, scanStep1: st.scanStep === 1,
onScan: () => this.setState({ dlg: 'scan', scanStep: 0, matchSel: 1 }),
onAdd: () => this.setState({ dlg: 'add', aName: '', aCat: 'vegetable', aSp: 12, aColor: '#97a97c' }),
onCloseDlg: () => this.setState({ dlg: null }),
stop: (e) => e.stopPropagation(),
onScanGo: () => this.setState({ scanStep: 1 }),
matches: [
{ label: 'Garlic (built-in)', sub: 'existing plant \u2014 lot attaches to it' },
{ label: 'Garlic \u2014 Music (yours)', sub: 'best match \u00b7 spacing 6\u2033' },
{ label: 'Create a new plant', sub: 'from the extracted fields' },
].map((m, i) => ({
key: i, ...m,
style: { display: 'flex', alignItems: 'center', gap: '8px', textAlign: 'left', cursor: 'pointer', borderRadius: '999px', padding: '10px 16px', fontFamily: 'var(--font-body)', background: st.matchSel === i ? 'var(--color-accent-200)' : 'var(--color-bg)', border: st.matchSel === i ? '1px solid var(--color-accent-400)' : '1px solid var(--color-divider)' },
onClick: () => this.setState({ matchSel: i }),
})),
onConfirmScan: () => this.setState(s2 => ({
dlg: null,
plants: s2.plants.map(p => p.id === 1 ? { ...p, lots: [...p.lots, { vendor: 'Keene Organics \u2014 Music', year: 2026, detail: '50 cloves \u00b7 untouched' }] } : p),
openId: 1,
})),
aName: st.aName, onAName: (e) => this.setState({ aName: e.target.value }),
aCat: st.aCat, onACat: (e) => this.setState({ aCat: e.target.value }),
aSp: st.aSp, onASp: (e) => this.setState({ aSp: e.target.value }),
swatches: ['#97a97c', '#c8553d', '#5f8f45', '#d9912f', '#b2622d', '#6d7f5a'].map(hex => ({
key: hex, hex,
style: { width: '30px', height: '30px', borderRadius: '999px', cursor: 'pointer', background: hex, border: st.aColor === hex ? '3px solid var(--color-accent)' : '3px solid transparent' },
onClick: () => this.setState({ aColor: hex }),
})),
onCreatePlant: () => this.setState(s2 => ({
dlg: null,
plants: [...s2.plants, { id: this.nid++, name: s2.aName.trim() || 'New plant', letter: (s2.aName.trim()[0] || 'N').toUpperCase(), color: s2.aColor, cat: s2.aCat, spacing: s2.aSp + '″', days: 70, builtin: false, lots: [] }],
})),
};
}
}
</script>
</body>
</html>