ExklusivBeta
Stone Soup <mv-stone-soup>
Beitragsboard für ein gemeinsames Ziel, auf dem der Beitrag jedes Teilnehmers sichtbar bleibt und das Ganze sichtbar mehr wird als die Summe seiner Teile: Anmeldungen für Potlucks und Events („wer bringt was mit“), Gruppengeschenke im Team, Fundraising, Bounties, Community-Wunschlisten, Schichtabdeckung oder die Kapazität eines Sprints pro Person. Das Ziel ist eine Menge benannter Bedarfe mit Mengenangaben (needs="Mains ×3, Desserts ×2, Drinks") und/oder ein Zielbetrag (goal, in jeder Währung oder Einheit); jeder Beitrag fällt als Initialen- oder Avatar-Chip in einen runden Topf, eingefärbt nach dem Bedarf, den er deckt (Sonnenblumen-Anordnung, Füllstand und Rand steigen mit dem Fortschritt, gestrichelte leere Plätze zeigen, was fehlt), und hakt den Bedarf ab. Daneben stehen eine Zusammenfassung in Klartext („5 of 7 covered · Still needed: 1 dessert and drinks“), behutsame Hinweise auf Doppelungen („3 people bringing salads (2 needed), while 1 main and drinks still needed“), eine Bedarfsliste mit „I’ll bring this“-Zusagen per Klick (optional mit der Frage, was genau), wieder freigebbare Zusagen und eine nach Personen gruppierte Liste der Beitragenden. Hover über einen Chip, einen Bedarf oder eine Person hebt die jeweils zugehörigen anderen hervor. Hinzufügen und Freigeben laufen über abbrechbare Events, deren waitUntil(promise) einen Speicherzustand anzeigt und bei einem Fehler zurückrollt; mv-need-met und mv-goal-met werden bei Übergängen ausgelöst.
| Kategorie | Datenanzeige |
|---|---|
| Typ | Web Component (<mv-stone-soup>) |
| Status | Beta |
| Installiert auch | button, input |
| Keywords | exclusive, culture, sign-up, potluck, contribution, crowdfunding, fundraising, gift-pool, pledge, claim, volunteer, shift-coverage, capacity, wish-list, bounty, goal, progress, collaboration, avatars |
When to use
- A potluck, event or volunteer sign-up must show who brings what, what is still missing and let people claim a slot in one click
- A team gift pool, fundraiser or bounty should make each contribution visible on the way to an amount target
- Shift coverage or sprint capacity must show which roles are staffed, by whom, and where people double up
- A community wish list should nudge newcomers toward what is missing instead of piling onto what is already covered
Avoid when
- Progress is a single number with no individual contributions to show → use Progress instead
- People pick among many time slots or seats with capacity and dates: that is a scheduling or booking grid, not a shared goal → use Calendar instead
- Contributions are private (salaries, anonymous donations) and must not be attributed to a person
Installation
node scripts/add.mjs stone-soup --out ./src/marvelousKI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["stone-soup"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Kopierte Dateien (inklusive Abhängigkeiten): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/stone-soup/stone-soup.js, components/stone-soup/stone-soup.css, components/button/button.css, components/input/input.css.
Verwendung
Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:
<div id="ss-demo" style="width:min(100%,60rem);margin-inline:auto">
<style>
#ss-demo { display:grid; gap:1.25rem; align-content:start }
#ss-demo .ss-card { border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm); overflow:hidden }
#ss-demo .ss-head { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding:1rem 1.25rem; border-bottom:1px solid var(--mv-border) }
#ss-demo .ss-head h3 { margin:0; font-size:1rem; font-weight:650; letter-spacing:-.01em }
#ss-demo .ss-head p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
#ss-demo .ss-body { padding:1.25rem }
#ss-demo .ss-controls { display:flex; align-items:center; gap:.75rem 1.25rem; flex-wrap:wrap; padding:.75rem 1.25rem; border-top:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
#ss-demo .ss-controls .mv-choice { font-size:.8125rem }
#ss-demo .ss-log { flex:1 1 14rem; margin:0; min-height:1.25rem; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono); text-align:end }
#ss-demo .ss-row { display:grid; grid-template-columns:minmax(0,1.15fr) minmax(0,1fr); gap:1.25rem; align-items:start }
#ss-demo .ss-actions { display:flex; gap:.5rem; flex-wrap:wrap }
#ss-demo .ss-row mv-stone-soup { --mv-stone-soup-size: 10.5rem }
@media (max-width:52rem) { #ss-demo .ss-row { grid-template-columns:minmax(0,1fr) } }
</style>
<!-- Main: a team potluck with named needs, one-click claims, "what are you bringing?" -->
<section class="ss-card" aria-labelledby="ss-potluck-title">
<header class="ss-head">
<div>
<h3 id="ss-potluck-title">Friday team potluck</h3>
<p>Fri, Oct 16, 2026 · 12:30 PM · 4th-floor kitchen · 14 invited</p>
</div>
<span class="mv-badge" data-variant="secondary" data-shape="pill">Signed in as Priya Nair</span>
</header>
<div class="ss-body">
<mv-stone-soup id="ss-potluck" label="Potluck sign-up" me="Priya Nair" ask-what
needs="Mains ×3, Salads ×2, Desserts ×2, Drinks, Plates & cups"></mv-stone-soup>
</div>
<footer class="ss-controls">
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="ss-sim">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="9" cy="8" r="3.5"/><path d="M3 20c.8-3.4 3.2-5 6-5s5.2 1.6 6 5M17 8v6M14 11h6"/></svg>
A teammate signs up
</button>
<label class="mv-choice">
<input type="checkbox" role="switch" class="mv-switch" id="ss-org">
<span class="mv-choice-text"><span class="mv-choice-title">Organizer view</span></span>
</label>
<label class="mv-choice">
<input type="checkbox" role="switch" class="mv-switch" id="ss-fail">
<span class="mv-choice-text"><span class="mv-choice-title">Make saving fail</span></span>
</label>
<p class="ss-log" id="ss-log">Saving takes 0.5 s (fake server).</p>
</footer>
</section>
<div class="ss-row">
<!-- Amount only: a farewell gift pool -->
<section class="ss-card" aria-labelledby="ss-gift-title">
<header class="ss-head">
<div>
<h3 id="ss-gift-title">Farewell gift for Dana</h3>
<p>Design team pool · closes Oct 9</p>
</div>
</header>
<div class="ss-body">
<mv-stone-soup id="ss-gift" label="Gift pool" me="Priya Nair" goal="400"></mv-stone-soup>
</div>
<footer class="ss-controls">
<span class="ss-actions">
<button type="button" class="mv-button" data-size="sm" data-chip="20">Chip in $20</button>
<button type="button" class="mv-button" data-variant="outline" data-size="sm" data-chip="40">Chip in $40</button>
</span>
</footer>
</section>
<!-- Capacity: a sprint staffed by role, in story points -->
<section class="ss-card" aria-labelledby="ss-sprint-title">
<header class="ss-head">
<div>
<h3 id="ss-sprint-title">Sprint 42 capacity</h3>
<p>Checkout revamp · Oct 5-Oct 16</p>
</div>
</header>
<div class="ss-body">
<mv-stone-soup id="ss-sprint" label="Sprint capacity" me="Priya Nair" goal="40" unit="pts"
needs="Backend ×2, Frontend ×2, QA"></mv-stone-soup>
</div>
</section>
</div>
<script type="module">
const potluck = document.getElementById("ss-potluck");
const gift = document.getElementById("ss-gift");
const sprint = document.getElementById("ss-sprint");
const log = document.getElementById("ss-log");
const fail = document.getElementById("ss-fail");
const say = (text) => { log.textContent = text; };
potluck.needs = [
{ label: "Mains", qty: 3, hint: "e.g., Chicken tikka, lasagna" },
{ label: "Salads", qty: 2 },
{ label: "Desserts", qty: 2, hint: "e.g., Lemon tart, brownies" },
{ label: "Drinks", hint: "e.g., Iced tea, sparkling water" },
{ label: "Plates & cups", singular: "Plates & cups" },
];
potluck.contributions = [
{ who: "Tomás Silva", need: "mains", what: "Bacalhau bake" },
{ who: "Aiko Tanaka", need: "mains", what: "Veggie gyoza" },
{ who: "Lena Fischer", need: "salads", what: "Greek salad" },
{ who: "Omar Haddad", need: "salads", what: "Tabbouleh" },
{ who: "Sofia Rossi", need: "salads", what: "Caesar salad" },
{ who: "Marco Bianchi", need: "desserts", what: "Tiramisu" },
];
// A fake server: every add or release takes 500 ms and can be made to fail.
const save = () => new Promise((resolve, reject) => setTimeout(() => (fail.checked ? reject(new Error("503")) : resolve()), 500));
potluck.addEventListener("mv-contribute", (e) => {
const c = e.detail.contribution;
say(`Saving “${c.what || "…"}” for ${c.who}…`);
e.detail.waitUntil(save());
});
potluck.addEventListener("mv-release", (e) => {
say(`Releasing “${e.detail.contribution.what}”…`);
e.detail.waitUntil(save());
});
potluck.addEventListener("mv-claim", (e) => say(`You claimed ${e.detail.need.label.toLowerCase()}: ${e.detail.contribution.what}.`));
potluck.addEventListener("mv-need-met", (e) => say(`${e.detail.need.label} covered.`));
potluck.addEventListener("mv-goal-met", () => say("Everything’s covered. Time to eat!"));
potluck.addEventListener("mv-error", () => say("Server error: change rolled back."));
const queue = [
{ who: "Kwame Mensah", need: "drinks", what: "Hibiscus iced tea" },
{ who: "Chloé Martin", need: "desserts", what: "Lemon tart" },
{ who: "Ravi Iyer", need: "mains", what: "Chana masala" },
{ who: "Hana Kim", need: "plates-cups", what: "Plates & cups" },
{ who: "Diego Álvarez", what: "Guacamole" },
{ who: "Emma Johansson", what: "Guacamole" },
];
let next = 0;
document.getElementById("ss-sim").addEventListener("click", () => {
const item = queue[next % queue.length];
next++;
potluck.contribute({ ...item, id: undefined });
});
document.getElementById("ss-org").addEventListener("change", (e) => { potluck.manage = e.target.checked; });
gift.contributions = [
{ who: "Noah Williams", amount: 40 },
{ who: "Mei Chen", amount: 25 },
{ who: "Lucas Moreau", amount: 50 },
{ who: "Fatima Zahra", amount: 30 },
{ who: "Jonas Berg", amount: 20 },
{ who: "Isabela Costa", amount: 35 },
{ who: "Arjun Patel", amount: 25 },
{ who: "Grace O’Neill", amount: 45 },
];
for (const btn of document.querySelectorAll("#ss-demo [data-chip]")) {
btn.addEventListener("click", () => gift.contribute({ who: "Priya Nair", amount: Number(btn.dataset.chip) }));
}
sprint.strings = { collected: "committed", peopleTitle: "Team", claim: "Take this role" };
sprint.contributions = [
{ who: "Yusuf Demir", need: "backend", what: "Payments API", amount: 8 },
{ who: "Ingrid Nilsen", need: "frontend", what: "Checkout UI", amount: 10 },
{ who: "Pablo Ortega", need: "backend", what: "Order service", amount: 8 },
{ who: "Mariam Said", need: "qa", what: "Regression suite", amount: 6 },
];
</script>
</div>Kulturelle Referenz
Die Steinsuppe, Europäisches Volksmärchen (seit den 1720er-Jahren gedruckt überliefert) (1720, Fabel). Reisende beginnen eine Suppe mit nichts als einem Stein im Topf, und neugierige Dorfbewohner geben jeder eine Zutat dazu, bis sie gemeinsam eine echte Mahlzeit gekocht haben, die keiner von ihnen allein hätte zubereiten können. In der UI beginnt ein gemeinsames Ziel als fast leerer Topf, der Beitrag jedes Teilnehmers fällt sichtbar hinein und hakt einen benannten Bedarf ab, und das Board zeigt Neuankömmlingen immer wieder, was noch fehlt.
API
Attributes
| Name | Typ | Default | Description |
|---|---|---|---|
needs | string | Named needs, comma-separated, with an optional quantity: "Mains ×3, Desserts x2, 2 × Salads, Drinks". Ids are slugs of the labels ("plates-cups"). Set the needs property to an array for ids, singulars and hints. | |
goal | number | Amount target. Contributions with an amount add up toward it; progress averages needs coverage and the amount ratio when both exist. | |
currency | ISO 4217 code | USD | Currency used to format amounts (Intl.NumberFormat; whole amounts without decimals). |
unit | string | Formats amounts as a plain number with this unit instead of a currency ("pts", "hours", "shifts"). | |
locale | BCP 47 tag | en-US | Locale for numbers and list joining (“1 dessert, drinks, and plates”). |
me | string | The viewer’s name. Contributions whose who matches it (accent and case-insensitive) are theirs: ringed in the pot, a “You” tag, releasable. Claims are made in this name ("You" when absent). | |
me-avatar | URL | Avatar image for the viewer’s claims and row. | |
ask-what | boolean | “I’ll bring this” opens an inline field (“What are you bringing for Desserts?”, placeholder from the need’s hint) before claiming. Enter adds, Escape cancels; empty means the need’s singular (“Dessert”). | |
manage | boolean | Organizer mode: every contribution gets a release button in the contributors list, not only the viewer’s. | |
label | string | Contributions | Accessible name of the whole board (role="group"). |
data-met / data-empty / data-mode | set by the component | data-met when every need is covered and the goal reached, data-empty with no contribution, data-mode="needs | amount" (amount: no needs, the contributors list carries the story). |
Properties
| Name | Typ | Description |
|---|---|---|
needs | string | Array<{ id?, label, qty?, singular?, hint? }> | Need definitions. singular overrides the naive English singular used in sentences (“1 dessert”); hint is the ask-what placeholder. Reading returns the normalized array. |
contributions | Array<{ id?, who, what?, amount?, need?, qty?, avatar?, mine? }> | Hydrate from your data (silent: no events, no announcements). need is a need id or label; without it, a what matching a need label (“Dessert”) is linked automatically, otherwise the item is free. qty counts toward the need (someone bringing 2 of 3 drinks). Reading returns copies with the resolved need and a pending flag. |
state | object | Read-only snapshot: { needs: [{ id, label, qty, count, remaining, covered, extra, contributions }], filled, slots, total, goal, progress (0..1), met, missing (sentence), duplicates: [{ what, count, need }] }. |
strings | Partial<Record<string, string>> | Overrides for every visible text and announcement (needsTitle, peopleTitle, covered, collected, toGo, over, stillNeeded, allCovered, goalReached, qty, coveredTag, extraTag, claim, release, releaseItem, you, pending, askLabel, askPlaceholder, add, cancel, duplicate, duplicateMissing, overNeed, overNeedMissing, empty, ghost, announce*…). English defaults. |
goal / currency / unit / locale / me / meAvatar / askWhat / manage / label | reflected | Mirror the attributes. |
Methods
| Name | Description |
|---|---|
contribute({ who, what?, amount?, need?, qty?, avatar?, mine?, id? }) | Adds a contribution (from your realtime feed, a form, a payment callback). Emits the cancelable mv-contribute, drops the chip into the pot, announces it. Returns Promise<contribution | null> (null if vetoed or its waitUntil promise rejected). |
claim(needId, { what? }) | The viewer claims a need (what defaults to the need’s singular). Same flow as contribute with source "claim", then mv-claim. Returns Promise<contribution | null>. |
release(id) | Removes a contribution: emits the cancelable mv-release, removes it optimistically, restores it if a waitUntil promise rejects. Returns Promise<boolean>. |
Events
| Name | Description |
|---|---|
mv-contribute | Cancelable, before any add. detail: { contribution (editable draft: change what, need, amount…), need, source: "api" | "claim", waitUntil(promise) }. With waitUntil the chip shows a saving state and the add rolls back (mv-error) on rejection. preventDefault() refuses it. |
mv-claim | The viewer’s claim was accepted (after waitUntil). detail: { contribution, need }. |
mv-release | Cancelable, before a contribution is removed. detail: { contribution, need, waitUntil(promise) }. |
mv-need-met | A need just became fully covered. detail: { need }. |
mv-goal-met | Every need is covered and the amount goal reached (fires on the transition, again if it is lost and regained; never on hydration). detail: { total, goal, contributions }. |
mv-change | After every add or release (including rollbacks). detail: { contributions, state }. |
mv-error | A waitUntil promise rejected and the change was rolled back. detail: { action: "contribute" | "release", contribution, error }. |
Content structure
| Name | Description |
|---|---|
(content) | Children stay first (a title or intro); the board is appended after them. |
CSS classes
| Name | Description |
|---|---|
mv-stone-soup-pot | The aria-hidden pot: -rim (SVG progress ring, -rim-track / -rim-arc), -bowl with -level, -bits holding one .mv-stone-soup-bit per contribution (data-tint, data-mine, data-pending, data-hot), per missing place (data-ghost) and an overflow “+12” (data-more). Each bit has -bit-face and a -bit-label shown on hover and arrival. |
mv-stone-soup-summary | -metrics (.mv-stone-soup-metric[data-kind=needs|amount|count] with -metric-value, -metric-label, -meter), -missing (data-state=open|met) and -notices (.mv-stone-soup-notice). |
mv-stone-soup-needs / mv-stone-soup-need | Need list and rows (data-need, data-tint, data-state=open|covered|over): -need-mark, -need-label, -need-count, -pips, -tag, -need-items, -need-action with .mv-stone-soup-claim, .mv-stone-soup-release or the .mv-stone-soup-ask form. |
mv-stone-soup-people / mv-stone-soup-person | Contributors grouped by person: -avatar, -person-name, -you, -person-items, -person-amount. |
mv-stone-soup-item | Ingredient chip: -item-dot, -item-what, -item-who, -item-need, -item-amount, -item-pending, -item-x (release). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-stone-soup-1 … --mv-stone-soup-6 | accent, teal, green, amber, rose, blue (light-dark) | Need tints, assigned in order and cycled. |
--mv-stone-soup-neutral | var(--mv-fg-subtle) | Tint of free contributions (not tied to a need). |
--mv-stone-soup-met | var(--mv-success) | Rim, level and meters once the goal is met. |
--mv-stone-soup-size | 15rem | Pot column width (the board stacks under 34rem of container width). |
Accessibility
The board is a labelled group whose pot is aria-hidden: everything it shows exists as text. The summary states progress in words (“5 of 7 covered”, “$315 of $400 collected · $85 to go”) and lists what is still needed; duplicate notices are plain text with an icon. Needs and contributors are real lists (announced with their item counts) under visible headings, each row reading as a sentence (“Desserts, 1 of 2: Tiramisu from Marco Bianchi, I’ll bring this: Desserts”). Covered and over-subscribed needs carry a word and an icon (“Covered”, “+1 extra”), never color alone; tints are only a secondary cue. Claims and releases are native buttons with explicit names (“Release Tiramisu”); the ask-what field has an accessible label naming the need, Enter adds, Escape returns focus to the claim button. Lists are rebuilt on change but focus is kept on the same control, or moved to the logical next one (after a claim, its Release button; after a release, the claim button); saving states are aria-busy and spoken as “Saving…”. A polite live region announces each addition and release (“Kwame Mensah is bringing Hibiscus iced tea. Drinks covered.”, “Everything’s covered. Goal met!”) and an assertive one reports failed saves; hydrating contributions announces nothing. Hovering or focusing a row highlights the matching chips in the pot. Reduced motion (OS or data-motion="reduce"): chips appear in place without dropping, rippling or sliding, the level and ring jump. Forced colors: the pot switches to system colors with outlined chips and a Highlight ring.