Patterns · Settings
Numbered item
The numbered wrapper for a repeatable composite item — a grey card with a muted number chip, a "<Label> N" header, a hover delete, and a body slot (SETTINGS-43).
Examples
Variant
Contained (default) — the whole item in ONE grey card; hover to reveal the delete
The fields for question 1 go here (prompt, options, …).
Question 2 — delete disabled at the minimum count.
Plain — no fill, for single-field items only
A single-field item with no grey card.
<!-- Paste-and-run: save as .html and open in a browser. No build step.
<aha-numbered-item> is the SAME shared custom element React and Vue consume — here in its native
form. Theming comes only from the --aha-* tokens in tokens.css. Hover an item to reveal its delete;
the whole item sits in ONE grey card (contained) with a muted-grey number chip. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/tokens.css">
<script type="module">
import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/aha-numbered-item.js'; // registers <aha-numbered-item>
</script>
<div style="display:flex; flex-direction:column; gap:8px; max-width:420px">
<aha-numbered-item n="1" label="Question" deletable id="q1">
<div>The fields for question 1 go here (prompt, options, …).</div>
</aha-numbered-item>
<!-- candelete="false" keeps the delete visible but disabled (e.g. at the minimum item count) -->
<aha-numbered-item n="2" label="Question" deletable candelete="false">
<div>The fields for question 2 go here.</div>
</aha-numbered-item>
</div>
<script>
document.getElementById('q1').addEventListener('delete', (e) => console.log('delete item', e.detail.n));
</script>
import '@ahaslides-product/design/aha-numbered-item'; // registers <aha-numbered-item>
import { useRef, useEffect } from 'react';
// React 18 needs a thin wrapper; React 19 can use <aha-numbered-item> directly.
// The item's fields are the children; onDelete fires when the hover trash is pressed.
function AhaNumberedItem({ n, label, variant, deletable, canDelete, onDelete, children }) {
const ref = useRef();
useEffect(() => {
const el = ref.current;
const h = (e) => onDelete?.(e.detail.n); // composed CustomEvent
el.addEventListener('delete', h);
return () => el.removeEventListener('delete', h);
}, [onDelete]);
return (
<aha-numbered-item
ref={ref}
n={String(n)}
label={label}
{...(variant ? { variant } : {})}
{...(deletable ? { deletable: '' } : {})}
{...(canDelete === false ? { candelete: 'false' } : {})}
>{children}</aha-numbered-item>
);
}
// usage
<AhaNumberedItem n={1} label="Question" deletable canDelete={items.length > 1} onDelete={removeQuestion}>
{/* the question's fields */}
</AhaNumberedItem>
// main.ts — register the element + mark aha-* as custom elements
import '@ahaslides-product/design/aha-numbered-item'; // registers <aha-numbered-item>
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');
// Component.vue
<script setup>
import { ref } from 'vue';
const questions = ref([{ id: 1 }, { id: 2 }]);
function remove(n) { questions.value = questions.value.filter((_, i) => i + 1 !== n); }
</script>
<template>
<!-- the item's fields are the default slot; candelete disables (not hides) the trash at the min count -->
<aha-numbered-item
v-for="(q, i) in questions"
:key="q.id"
:n="String(i + 1)"
label="Question"
deletable
:candelete="questions.length > 1 ? 'true' : 'false'"
@delete="remove($event.detail.n)"
>
<div>Fields for question {{ i + 1 }}…</div>
</aha-numbered-item>
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
variant | contained | plain | contained | contained = grey card (composite items); plain = no fill (single-field items only) |
n | number | — | The item's number, shown in the chip and the header |
label | string | — | Header noun — rendered as "<Label> N" |
deletable | boolean | false | Show the hover delete; omit / false hides it entirely |
candelete | boolean | true | false disables the delete (at the minimum item count) without hiding it |
collapsible | boolean | false | Show a header caret that collapses the body in place (the QuestionList case) |
delete | CustomEvent<{n}> | — | Composed event when the delete is pressed; read e.detail.n |
toggle | CustomEvent<{collapsed}> | — | Composed event when the collapse caret is toggled; read e.detail.collapsed |
Install
# .npmrc — once: point the @ahaslides-product scope at GitHub Packages
@ahaslides-product:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} # a GitHub token with read:packages
npm i @ahaslides-product/design
import '@ahaslides-product/design/tokens.css'; // once, at the app root
import '@ahaslides-product/design/aha-numbered-item'; // registers <aha-numbered-item>
Agent feed for this component (absolute, fetchable anywhere): numbered-item.agent.json · numbered-item.md · numbered-item.llms.txt
When to use
When to use
- Numbered item — a repeatable COMPOSITE item — more than one field, or a nested option list (a Question, a Card, a Round)
- Option row — a flat list of single values — not wrapped in a numbered card
- Plain variant — a single-field repeatable item that does not warrant a grey card
This is the ONE exception to "never a container around settings" (SETTINGS-15): it wraps a repeatable composite item, not plain settings. A composite item must keep the contained grey card — dropping the fill (plain, or a hand-rolled header over a fill-less body) is a build mistake (SETTINGS-43). The chip is muted grey, never a purple badge.
Surfaces
editor settings
Spec
Contained whole item in ONE #F7F7F7 grey card, radius 12, no border, padding 12 · Chip 22px muted-grey pill (#F1F1F1 / #4A4A4A), semibold 12 — never radical-purple · Header chip + "<Label> N" title (semibold 14) + hover delete · Delete tertiary text trash (system-trash), hover-only, space reserved; #F5222D on hover · deletable=false the delete control is hidden entirely (non-removable item) · candelete=false the delete is shown but disabled (minimum-count case) · Plain no fill — for SINGLE-FIELD items only