# WeightedCriteriaCard

> A human-in-the-loop step: the assistant suggests criteria, the person switches each on or off, sets its weight, adds their own and confirms.

Source: https://docs.nasaqui.com/components/weighted-criteria-card

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/weighted-criteria-card.json
```

Before an assistant compares vendors, ranks candidates or recommends a plan, it can ask what matters. This card shows
the criteria it suggests. The person:

- switches each criterion on or off;
- sets its weight to Low, Medium or High;
- adds their own criteria;
- confirms.

A thin bar under each enabled criterion shows its share of the decision.

## When to use

- Inside a copilot answer, before a comparison or a ranking.
- Anywhere a person tunes a short list of weighted factors before an automated step runs.

## When not to use

- A single or multiple choice with no weights: use the `picker` artifact of [`ArtifactRenderer`](https://docs.nasaqui.com/components/artifact-renderer).
- Numeric weights or long lists: use a form with sliders or a table.

## Import

```tsx
import { WeightedCriteriaCard } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
<WeightedCriteriaCard
  description="I will rank the three vendors with these."
  defaultCriteria={[
    { id: "price", label: "Price", weight: "high", enabled: true },
    { id: "support", label: "Support", description: "Arabic support, response time", weight: "medium", enabled: true },
    { id: "speed", label: "Delivery speed", weight: "low", enabled: false },
  ]}
  onAccept={async (criteria) => {
    await copilot.send("Rank with these criteria", undefined, { hidden: true, data: { type: "criteria", criteria } });
  }}
/>
```

## Anatomy

```
Card                      data-slot="weighted-criteria-card"  data-sent
├─ CardHeader             title, description
├─ ul                     labelled by the title
│  └─ li                  data-slot="weighted-criterion"  data-enabled
│     ├─ Switch           include / exclude
│     ├─ label, description
│     ├─ ToggleGroup      Low / Medium / High
│     ├─ remove           custom criteria only
│     └─ share bar        decorative
├─ form                   add your own (Input + Add)
└─ CardFooter             "2 of 3 included" (polite live region) + Accept
```

## API

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultCriteria` | `WeightedCriterion[]` | `[]` | The suggested criteria, uncontrolled. |
| `criteria` / `onCriteriaChange` | | | Controlled list. |
| `onAccept` | `(criteria) => void \| { error? } \| Promise<…>` | | Confirm. On success the card locks and shows Sent; an `{ error }` or a rejection shows an alert. |
| `title` / `description` | `ReactNode` | "What matters most?" | |
| `allowCustom` | `boolean` | `true` | Show the add form. Custom criteria can be removed; suggested ones can only be switched off. |
| `showShares` | `boolean` | `true` | Bars with each enabled criterion's share. |
| `disabled` | `boolean` | `false` | |
| `labels` | `WeightedCriteriaCardLabels` | | Override any string. |

`WeightedCriterion`: `{ id, label, description?, weight: "low" | "medium" | "high", enabled, custom? }`.

Pure helpers:

- `criteriaShares(criteria)`: a map from id to share. Low counts 1, Medium 2 and High 3, and the enabled shares sum to 1.
- `cycleWeight(weight)`: the next weight.
- `addCriterion(criteria, label, id)`: adds a trimmed label and ignores blanks and duplicates.

## Accessibility

- The list is labelled by the card title.
- Each switch is named "Include {label}", and each weight group "Weight of {label}". The weight group follows arrow keys in the reading direction.
- The weight group is disabled while its criterion is off.
- The count in the footer is a polite live region. Errors show as an alert.
- The share bars are decorative; the weights carry the meaning in text.

## RTL & i18n

Strings ship in English and Arabic. Labels use `dir="auto"`, so mixed-language criteria read correctly. The layout
mirrors with logical properties.

## Styling & tokens

`Card`, `rounded-control` list with `divide-border`, share bars in `bg-primary` on `bg-secondary`, `ease-nq` width
transition.

## Do / Don't

- Do suggest three to six criteria with a short description each.
- Do send the accepted list back to the assistant as data, not as visible text.
- Don't use it for a plain choice; the picker artifact is lighter.

## Related

- [`artifact-renderer`](https://docs.nasaqui.com/components/artifact-renderer)
- [`copilot-chat`](https://docs.nasaqui.com/components/copilot-chat)
- [`toggle-group`](https://docs.nasaqui.com/components/toggle-group)
- [`switch`](https://docs.nasaqui.com/components/switch)

## Lab

https://docs.nasaqui.com/?path=/docs/components-ai-assistant-weighted-criteria-card--docs

## Code

### React

```tsx
<WeightedCriteriaCard
  description="I will rank the three vendors with these."
  defaultCriteria={[
    { id: "price", label: "Price", weight: "high", enabled: true },
    { id: "support", label: "Support", description: "Arabic support, response time", weight: "medium", enabled: true },
    { id: "speed", label: "Delivery speed", weight: "low", enabled: false },
  ]}
  onAccept={async (criteria) => {
    await copilot.send("Rank with these criteria", undefined, { hidden: true, data: { type: "criteria", criteria } });
  }}
/>
```

### shadcn

```tsx
<WeightedCriteriaCard
  description="I will rank the three vendors with these."
  defaultCriteria={[
    { id: "price", label: "Price", weight: "high", enabled: true },
    { id: "support", label: "Support", description: "Arabic support, response time", weight: "medium", enabled: true },
    { id: "speed", label: "Delivery speed", weight: "low", enabled: false },
  ]}
  onAccept={async (criteria) => {
    await copilot.send("Rank with these criteria", undefined, { hidden: true, data: { type: "criteria", criteria } });
  }}
/>
```

### Vue

```vue
<script setup lang="ts">
import { NqWeightedCriteriaCard, type WeightedCriterion } from "@fadymondy/nasaq/vue";

const criteria: WeightedCriterion[] = [
  { id: "price", label: "Price", weight: "high", enabled: true },
  { id: "support", label: "Support", description: "Arabic support, response time", weight: "medium", enabled: true },
  { id: "speed", label: "Delivery speed", weight: "low", enabled: false },
];

async function accept(_list: WeightedCriterion[]) {
  await new Promise((r) => setTimeout(r, 400));
}
</script>

<template>
  <div class="max-w-xl">
    <NqWeightedCriteriaCard description="I will rank the three vendors with these." :default-criteria="criteria" :on-accept="accept" />
  </div>
</template>
```

### Blade

```blade
<div class="max-w-xl">
    <x-nq::weighted-criteria-card description="I will rank the three vendors with these." :criteria="[
        ['id' => 'price', 'label' => 'Price', 'weight' => 'high', 'enabled' => true],
        ['id' => 'support', 'label' => 'Support', 'description' => 'Arabic support, response time', 'weight' => 'medium', 'enabled' => true],
        ['id' => 'speed', 'label' => 'Delivery speed', 'weight' => 'low', 'enabled' => false],
    ]" />
</div>
```

### HTML + Alpine

```html
<div class="max-w-xl">
    <div data-slot="weighted-criteria-card" x-data="nqWeightedCriteriaCard(JSON.parse('[{\u0022weight\u0022:\u0022high\u0022,\u0022enabled\u0022:true,\u0022id\u0022:\u0022price\u0022,\u0022label\u0022:\u0022Price\u0022},{\u0022weight\u0022:\u0022medium\u0022,\u0022enabled\u0022:true,\u0022id\u0022:\u0022support\u0022,\u0022label\u0022:\u0022Support\u0022,\u0022description\u0022:\u0022Arabic support, response time\u0022},{\u0022weight\u0022:\u0022low\u0022,\u0022enabled\u0022:false,\u0022id\u0022:\u0022speed\u0022,\u0022label\u0022:\u0022Delivery speed\u0022}]'), JSON.parse('{\u0022labels\u0022:{\u0022low\u0022:\u0022Low\u0022,\u0022medium\u0022:\u0022Medium\u0022,\u0022high\u0022:\u0022High\u0022,\u0022include\u0022:\u0022Include {label}\u0022,\u0022weight\u0022:\u0022Weight of {label}\u0022,\u0022remove\u0022:\u0022Remove {label}\u0022,\u0022count\u0022:\u0022{on} of {all} included\u0022,\u0022failed\u0022:\u0022That did not work. Try again.\u0022}}'))" x-modelable="criteria"
    x-bind:data-sent="sent ? '' : null"
    class="flex flex-col gap-4 rounded-card border border-border bg-card py-4 text-card-foreground min-w-0">
    <div data-slot="card-header" class="grid auto-rows-min items-start gap-1 px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto]">
        <div data-slot="card-title" id="nq-wc-751e3500-title" class="text-label text-foreground">What matters most?</div>
                    <div data-slot="card-description" class="text-body-sm text-muted-foreground">I will rank the three vendors with these.</div>
            </div>
    <div data-slot="card-content" class="flex flex-col gap-3 px-4">
        <ul aria-labelledby="nq-wc-751e3500-title" class="flex flex-col divide-y divide-border rounded-control border border-border">
            <template x-for="c in criteria" :key="c.id">
                <li data-slot="weighted-criterion" x-bind:data-enabled="c.enabled ? '' : null" class="flex flex-col gap-2 p-3">
                    <div class="flex flex-wrap items-center gap-3">
                        <button type="button" role="switch" data-slot="switch" x-bind:aria-label="includeLabel(c)" x-bind:aria-checked="String(c.enabled)"
                            x-bind:data-checked="c.enabled ? '' : null" x-bind:data-unchecked="c.enabled ? null : ''"
                            x-bind:disabled="locked ? '' : null" x-bind:data-disabled="locked ? '' : null" x-on:click="toggle(c)"
                            class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent bg-nq-line-strong p-0.5 outline-none transition-colors duration-150 ease-nq data-checked:bg-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus data-disabled:cursor-not-allowed data-disabled:opacity-50">
                            <span data-slot="switch-thumb" x-bind:data-checked="c.enabled ? '' : null" x-bind:data-unchecked="c.enabled ? null : ''"
                                class="block size-4 rounded-full bg-background shadow-xs data-checked:bg-primary-foreground transition-[translate] duration-150 ease-nq data-checked:translate-x-3.5 rtl:data-checked:-translate-x-3.5"></span>
                        </button>
                        <span x-bind:class="c.enabled ? '' : 'opacity-60'" class="flex min-w-0 flex-1 flex-col">
                            <span dir="auto" class="text-body-sm font-medium text-foreground" x-text="c.label"></span>
                            <span x-show="c.description" dir="auto" class="text-caption text-muted-foreground" x-text="c.description" style="display: none"></span>
                        </span>
                        <div role="group" data-slot="toggle-group" data-variant="segmented" data-orientation="horizontal" x-bind:aria-label="weightLabel(c)"
                            x-bind:data-disabled="(locked || ! c.enabled) ? '' : null" x-on:keydown="weightKey($event)"
                            class="flex w-fit max-w-full gap-0.5 rounded-control bg-secondary p-0.5">
                            <template x-for="w in weights" :key="w">
                                <button type="button" data-slot="toggle" x-bind:aria-pressed="String(c.weight === w)" x-bind:data-pressed="c.weight === w ? '' : null"
                                    x-bind:disabled="(locked || ! c.enabled) ? '' : null" x-bind:data-disabled="(locked || ! c.enabled) ? '' : null"
                                    x-bind:tabindex="c.weight === w ? 0 : -1" x-on:click="setWeight(c, w)" x-text="labels[w]"
                                    class="inline-flex h-7 shrink-0 items-center justify-center gap-1.5 whitespace-nowrap px-3 text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground [&_svg]:size-4 [&_svg]:shrink-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus data-disabled:pointer-events-none data-disabled:opacity-50 rounded-[calc(var(--radius-control)-2px)] data-pressed:bg-card data-pressed:text-foreground data-pressed:shadow-xs h-6 px-2 text-caption"></button>
                            </template>
                        </div>
                        <button x-show="c.custom" type="button" data-slot="button" x-bind:aria-label="removeLabel(c)" x-bind:disabled="locked ? '' : null" x-bind:data-disabled="locked ? '' : null" x-on:click="remove(c)" style="display: none"
                            class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0">
                            <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M18 6 6 18"/>
  <path d="m6 6 12 12"/>
</svg>                        </button>
                    </div>
                    <span x-show="showShares && c.enabled" aria-hidden="true" class="h-1 overflow-hidden rounded-full bg-secondary" style="display: none">
                        <span class="block h-full rounded-full bg-primary transition-[width] duration-200 ease-nq" x-bind:style="'width: ' + sharePct(c)"></span>
                    </span>
                </li>
            </template>
        </ul>
                    <form x-on:submit.prevent="add()" class="flex items-center gap-2">
                <input data-slot="input" type="text" x-model="draft" placeholder="Add your own…" aria-label="Add your own…" maxlength="120" x-bind:disabled="locked ? '' : null"                     class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-control flex-1">
                <button type="submit" data-slot="button" x-bind:disabled="(locked || ! draft.trim()) ? '' : null" x-bind:data-disabled="(locked || ! draft.trim()) ? '' : null" disabled data-disabled
                    class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
                    <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M5 12h14"/>
  <path d="M12 5v14"/>
</svg>                    Add
                </button>
            </form>
                <div x-show="error" role="alert" data-slot="alert" data-tone="danger" style="display: none"
            class="relative grid grid-cols-[auto_1fr_auto] items-start gap-x-3 rounded-card border p-3 text-start border-nq-danger/30 bg-nq-danger-soft">
            <svg aria-hidden="true" data-slot="alert-icon" class="mt-0.5 size-4 text-nq-danger-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m15 9-6 6"/>
  <path d="m9 9 6 6"/>
</svg>            <div data-slot="alert-body" class="flex min-w-0 flex-col gap-0.5">
                <div data-slot="alert-description" class="text-body-sm text-foreground" x-text="error"></div>
            </div>
        </div>
    </div>
    <div data-slot="card-footer" class="flex items-center gap-2 px-4 justify-between gap-3">
        <span aria-live="polite" class="text-caption text-muted-foreground tabular-nums" x-text="countText">2 of 3 included</span>
        <button type="button" data-slot="button" x-bind:aria-busy="busy ? 'true' : null"
            x-bind:disabled="(disabled || sent || busy || onCount === 0) ? '' : null" x-bind:data-disabled="(disabled || sent || busy || onCount === 0) ? '' : null"
             x-on:click="accept()"
            class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control px-[var(--nq-control-pad)]">
            <span x-show="busy" style="display: none"><svg data-slot="spinner" aria-hidden="true" class="size-4 animate-spin motion-reduce:animate-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg></span>
            <span x-show="sent" aria-hidden="true" class="flex [&_svg]:size-4" style="display: none"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
            <span x-text="sent ? 'Sent' : 'Use these'">Use these</span>
        </button>
    </div>
</div>
</div>
```
