Skip to content
Merged
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
22 changes: 19 additions & 3 deletions .claude/skills/components/display-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ the DOM. Dismiss can be controlled locally (closes itself) or by a parent via `v
| `theme` | `SemanticTheme` | `"info"` | `"info" \| "success" \| "warning" \| "error"` |
| `dismissible` | `boolean` | `false` | Shows a close button. |
| `useAutoFocus` | `boolean` | `false` | Focuses the prompt root element on mount. |
| `masked` | `boolean` | `false` | SVG glass border — swaps `AlertContent` for `AlertMaskedContent`. |
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the inner wrapper. Supported modifier: `"outlined"`. |
| `v-model` | `boolean` | `false` | Optional parent control — see dismiss behaviour below. |

Expand All @@ -38,8 +39,9 @@ the DOM. Dismiss can be controlled locally (closes itself) or by a parent via `v
| `"warning"` | `akar-icons:circle-alert` |
| `"error"` | `akar-icons:circle-alert` |

`data-theme` is set on the inner wrapper, activating the CSS palette (`--theme-surface`,
`--theme-text`, `--theme-border`, `--theme-ring`, etc.).
`data-theme` is set on `.display-prompt-wrapper`, activating the CSS palette (`--theme-accent`,
`--theme-text`, `--theme-border`, `--theme-ring`, etc.). The wrapper's left-edge accent uses
`--theme-accent` (step 5/4) — not `--theme-surface` — so it stays visually distinct from buttons.

## Dismiss behaviour

Expand Down Expand Up @@ -111,11 +113,25 @@ Scope overrides using your page or section wrapper class — no `:deep()` needed

```css
.my-section .display-prompt-wrapper {
--theme-surface: oklch(60% 0.18 140);
--theme-accent: oklch(60% 0.18 140); /* left-edge accent strip */
border-radius: 0.8rem;
}
```

## Masked variant

Setting `:masked="true"` swaps `AlertContent` for `AlertMaskedContent`, giving the prompt an SVG-based
border and a semi-transparent background so content behind it is faintly visible. The inner layout is
identical — all the same slots apply. Place the prompt over a coloured or image background for the
glass effect to be visible.

```vue
<DisplayPrompt theme="info" :masked="true">
<template #title>Glass prompt</template>
<template #content>Semi-transparent, backed by AlertMaskedContent.</template>
</DisplayPrompt>
```

## Notes

- `DisplayPromptTheme` is an alias for `SemanticTheme` (`"info" | "success" | "warning" | "error"`).
Expand Down
9 changes: 9 additions & 0 deletions .claude/skills/components/display-toast.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface DisplayToastConfig {
position?: "top" | "bottom" // default: "top"
alignment?: "left" | "center" | "right" // default: "right"
fullWidth?: boolean // default: false — overrides alignment
masked?: boolean // SVG glass border — semi-transparent background — default: false
}
behavior?: {
autoDismiss?: boolean // default: true
Expand Down Expand Up @@ -198,6 +199,14 @@ Override tokens via an unscoped style block scoped to a page or layout class:
}
```

### Masked variant

Setting `appearance.masked: true` swaps `AlertContent` for `AlertMaskedContent`, which uses an SVG-based border and a semi-transparent background (`rgba(0,0,0,0.3)`) so page content is faintly visible beneath the toast.

The SVG border shape matches the standard toast exactly (8px left radius, 4px right, 6px accent-colour left bar via `--theme-accent`). Override any dimension or colour via a `maskConfig` prop if using `AlertMaskedContent` standalone.

The inner content layout is handled by the shared `AlertContentInner` molecule — both variants reuse it. `AlertMaskedContent` overrides `--_alert-content-inner-bg: transparent` so the glass effect shows through.

### Provider notes

- `useToastQueue` state persists across route changes — no `onBeforeRouteLeave` cleanup needed.
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
├── display-pill.md — DisplayPill: pill/badge label with icon slot, 6 variants, 3 sizes, reversible order, full CSS token API for border/outline/colour
├── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay), CSS tokens
├── samaritan-prompt-mixed.md — SamaritanPromptMixed: animated text prompt, typewriter/word-pulse effects, MessageConfig API, aria-live accessibility, CSS tokens
├── display-toast.md — DisplayToast (standalone v-model) + DisplayToastProvider + useToastQueue (app-wide queue): stacking, FLIP dismiss, maxVisible, SemanticTheme × 4
├── display-toast.md — DisplayToast (standalone v-model) + DisplayToastProvider + useToastQueue (app-wide queue): stacking, FLIP dismiss, maxVisible, SemanticTheme × 4, masked SVG glass variant
└── display-prompt.md — DisplayPrompt: inline notification banner, SemanticTheme × 4, local vs parent-controlled dismiss, outlined modifier, CSS token override
```

Expand Down
11 changes: 6 additions & 5 deletions .claude/skills/theming-colour-ramps.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,18 @@ These are used by components that need a specific step by name (e.g. error state

## Semantic slots

Eight shared colour roles are declared in `_theme-slots.css` on the same selector as the ramp.
Nine shared colour roles are declared in `_theme-slots.css` on the same selector as the ramp.
All themed components (buttons, inputs, prompts, toasts) read only these tokens:

| Token | Light (step) | Dark (step) | Role |
|-------------------------|--------------|-------------|-----------------------------------|
| `--theme-surface` | 9 | 7 | Filled button/chip surface |
| `--theme-surface-hover` | 8 | 6 | Hover state of filled surface |
| `--theme-surface-subtle`| 1 | 9 | Hover bg for outline elements |
| `--theme-surface` | 7 | 9 | Filled button/chip surface |
| `--theme-surface-hover` | 9 | 7 | Hover state of filled surface |
| `--theme-accent` | 5 | 4 | Decorative accent strip (prompt/toast left edge) |
| `--theme-surface-subtle`| 1 | 9 | Subtle body bg for prompt/toast, outline element hover |
| `--theme-border` | 6 | 5 | Input/card border |
| `--theme-border-focus` | 4 | 3 | Focused border |
| `--theme-ring` | 2 | 2 | Focus ring (outline) |
| `--theme-ring` | 1 | 9 | Focus ring (outline) |
| `--theme-on-surface` | 0 | 0 | Text/icon on filled surface |
| `--theme-text` | 9 | 2 | Text on page, outline element text|

Expand Down
3 changes: 2 additions & 1 deletion .claude/skills/theming-override-default.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,5 @@ that hue/chroma pair.

- `--slate-*` neutrals ship with the layer — no need to redefine for page backgrounds/text
- To add a custom `[data-theme]` variant using your generated palette, see `theming-colour-ramps.md`
- The `warning` theme overrides `--theme-surface` steps directly for a different surface intensity — this pattern is available for any custom theme that needs a non-default surface step
- The `warning` theme overrides `--theme-surface` and `--theme-surface-hover` to lighter steps (5/4 and 7/5) so warning buttons read as warm/alert rather than the default dark surface — this pattern is available for any custom theme that needs a non-default surface step
- `--theme-accent` (step 5/4) is separate from `--theme-surface` (step 7/9) — accent is for decorative strips in `DisplayPrompt` and `DisplayToast`; surface is for interactive elements like buttons
21 changes: 11 additions & 10 deletions .claude/skills/theming-partial-override.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@ after the layer styles.

## Semantic slots — the tokens to override

All themed components share the same 8-slot vocabulary. Overriding these affects buttons, inputs,
All themed components share a 9-slot vocabulary. Overriding these affects buttons, inputs,
prompts, and toasts simultaneously:

| Token | Default (light → dark) | Role |
|-------------------------|----------------------------|-----------------------------------|
| `--theme-surface` | `--colour-theme-9` / `7` | Filled button/chip surface |
| `--theme-surface-hover` | `--colour-theme-8` / `6` | Hover state of filled surface |
| `--theme-surface-subtle`| `--colour-theme-1` / `9` | Hover bg for outline elements |
| `--theme-surface` | `--colour-theme-7` / `9` | Filled button/chip surface |
| `--theme-surface-hover` | `--colour-theme-9` / `7` | Hover state of filled surface |
| `--theme-accent` | `--colour-theme-5` / `4` | Decorative accent strip (prompt/toast left edge) |
| `--theme-surface-subtle`| `--colour-theme-1` / `9` | Subtle body bg for prompt/toast, outline element hover |
| `--theme-border` | `--colour-theme-6` / `5` | Input/card border |
| `--theme-border-focus` | `--colour-theme-4` / `3` | Focused border |
| `--theme-ring` | `--colour-theme-2` | Focus ring (outline) |
| `--theme-ring` | `--colour-theme-1` / `9` | Focus ring (outline) |
| `--theme-on-surface` | `--colour-theme-0` | Text/icon on filled surface |
| `--theme-text` | `--colour-theme-9` / `2` | Text on page, outline element text|

Expand Down Expand Up @@ -64,10 +65,10 @@ Page-level tokens:
#### Override button surface only

```css
/* Make primary buttons darker/more saturated */
/* Make primary buttons darker/more saturated (step-7/9 defaults, step-9/7 hover) */
:where(html) {
--theme-surface: light-dark(oklch(28% 0.18 85), oklch(45% 0.20 85));
--theme-surface-hover: light-dark(oklch(22% 0.16 85), oklch(38% 0.18 85));
--theme-surface: light-dark(oklch(48% 0.18 85), oklch(32% 0.20 85));
--theme-surface-hover: light-dark(oklch(32% 0.18 85), oklch(48% 0.20 85));
}
```

Expand Down Expand Up @@ -118,8 +119,8 @@ To restrict an override to a specific section, scope to a wrapper class instead

.hero-cta {
/* Just these buttons use a different surface colour */
--theme-surface: light-dark(oklch(32% 0.16 85), oklch(55% 0.18 85));
--theme-surface-hover: light-dark(oklch(25% 0.14 85), oklch(48% 0.16 85));
--theme-surface: light-dark(oklch(48% 0.16 85), oklch(32% 0.18 85));
--theme-surface-hover: light-dark(oklch(32% 0.14 85), oklch(48% 0.16 85));
}
```

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ computes the scale from the two params:

Scale direction: **00 = lightest, 10 = darkest**. Chroma tapers at both ends and peaks at step 06.

Eight semantic slots (`--theme-surface`, `--theme-text`, `--theme-ring`, etc.) are derived from the
Nine semantic slots (`--theme-surface`, `--theme-accent`, `--theme-text`, `--theme-ring`, etc.) are derived from the
scale using `light-dark()` and are shared by all components. Theme variants (`data-theme="success"`,
`"warning"`, `"error"`) swap `--theme-hue` and `--theme-chroma` on their element; the formula
re-evaluates locally so each themed element gets its own full palette without affecting the page.
Expand Down
19 changes: 11 additions & 8 deletions app/assets/styles/setup/03.theming/_theme-slots.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
Declared on the same selector as theme-ramp.css so the formula re-evaluates
per [data-theme] element rather than being inherited from html. */
:where(html, [data-theme], [data-invalid]) {
--theme-surface: light-dark(var(--colour-theme-9), var(--colour-theme-7));
--theme-surface-hover: light-dark(var(--colour-theme-8), var(--colour-theme-6));
--theme-surface-subtle: light-dark(var(--colour-theme-1), var(--colour-theme-9));
--theme-border: light-dark(var(--colour-theme-6), var(--colour-theme-5));
--theme-border-focus: light-dark(var(--colour-theme-4), var(--colour-theme-3));
--theme-ring: var(--colour-theme-2);
--theme-on-surface: var(--colour-theme-0);
--theme-text: light-dark(var(--colour-theme-9), var(--colour-theme-2));
--theme-surface: light-dark(var(--colour-theme-7), var(--colour-theme-9));
--theme-surface-inverted: light-dark(var(--colour-theme-9), var(--colour-theme-2));
--theme-surface-hover: light-dark(var(--colour-theme-9), var(--colour-theme-7));
--theme-accent: light-dark(var(--colour-theme-5), var(--colour-theme-4));
--theme-surface-subtle: light-dark(var(--colour-theme-1), var(--colour-theme-9));
--theme-border: light-dark(var(--colour-theme-6), var(--colour-theme-5));
--theme-border-focus: light-dark(var(--colour-theme-4), var(--colour-theme-3));
--theme-ring: light-dark(var(--colour-theme-1), var(--colour-theme-9));
--theme-on-surface: var(--colour-theme-0);
--theme-text: light-dark(var(--colour-theme-9), var(--colour-theme-2));
--theme-text-inverted: light-dark(var(--colour-theme-9), var(--colour-theme-7));
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
--button-padding-block: 1rem;
--button-padding-inline: 1.8rem;

--button-border-radius: 0.8rem;
--button-border-radius: 0.2rem;
--button-border-radius-icon-only: 100vw;

--button-border-width: 0.1rem;
Expand Down
159 changes: 28 additions & 131 deletions app/components/01.atoms/prompt/DisplayPrompt.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,61 +7,55 @@
tabindex="0"
>
<div class="display-prompt-wrapper" :data-theme="theme" :class="[elementClasses]" data-test-id="display-prompt">
<div class="display-prompt-inner">
<div class="display-prompt-icon" data-test-id="prompt-icon" aria-hidden="true">
<slot name="customDecoratorIcon">
<Icon :name="themeIcons[theme]" class="icon" />
</slot>
</div>
<div class="display-prompt-content" :aria-live="useAutoFocus ? 'polite' : undefined">
<p class="title" data-test-id="display-prompt-title">
<slot name="title"></slot>
</p>
<p v-if="slots.content" class="text" data-test-id="display-prompt-content">
<slot name="content"></slot>
</p>
</div>
<button
v-if="dismissible"
data-test-id="display-prompt-action"
class="display-prompt-action"
@click.prevent="updateComponentState()"
>
<slot name="customCloseIcon">
<Icon name="bitcoin-icons:cross-filled" class="icon" />
</slot>
<span class="sr-only">
<slot name="customTitle">Close this prompt</slot>
</span>
</button>
</div>
<component
:is="contentComponent"
:theme="theme"
:dismissible="dismissible"
:aria-live="useAutoFocus ? 'polite' : undefined"
@dismiss="updateComponentState()"
>
<template v-if="slots.customDecoratorIcon" #icon>
<slot name="customDecoratorIcon"></slot>
</template>
<template v-if="slots.title" #title>
<slot name="title"></slot>
</template>
<template v-if="slots.content" #content>
<slot name="content"></slot>
</template>
<template v-if="slots.customCloseIcon" #dismissIcon>
<slot name="customCloseIcon"></slot>
</template>
<template #dismissLabel>
<slot name="customTitle">Close this prompt</slot>
</template>
</component>
</div>
</div>
</template>

<script setup lang="ts">
import AlertContent from "~/components/02.molecules/alert-content/AlertContent.vue";
import AlertMaskedContent from "~/components/02.molecules/alert-masked-content/AlertMaskedContent.vue";
import type { DisplayPromptTheme } from "~/types/components";

interface Props {
theme?: DisplayPromptTheme;
dismissible?: boolean;
useAutoFocus?: boolean;
masked?: boolean;
styleClassPassthrough?: string | string[];
}

const props = withDefaults(defineProps<Props>(), {
theme: "info",
dismissible: false,
useAutoFocus: false,
masked: false,
styleClassPassthrough: () => [],
});

const themeIcons: Record<DisplayPromptTheme, string> = {
info: "akar-icons:info",
success: "akar-icons:check",
warning: "akar-icons:circle-alert",
error: "akar-icons:circle-alert",
};
const contentComponent = computed(() => (props.masked ? AlertMaskedContent : AlertContent));

const slots = useSlots();
const promptElementRef = useTemplateRef<HTMLElement>("promptElementRef");
Expand Down Expand Up @@ -100,104 +94,7 @@ onMounted(async () => {
}

.display-prompt-wrapper {
background-color: var(--theme-surface);
border: 0px solid transparent;
border-radius: 4px;
border-start-start-radius: 8px;
border-end-start-radius: 8px;
padding-inline-start: 8px;

overflow: hidden;

&.outlined {
border: 1px solid var(--theme-border);
}

.display-prompt-inner {
align-items: center;
display: flex;
gap: 12px;
justify-content: space-between;
padding-block: 1rem;
padding-inline: 1.5rem;

border-start-start-radius: 8px;
border-end-start-radius: 8px;
background-color: light-dark(var(--colour-theme-0), var(--colour-theme-10));

.display-prompt-icon {
display: inline-flex;
.icon {
color: var(--theme-surface);
display: inline-block;
font-size: 3rem;
font-style: normal;
font-weight: normal;
overflow: hidden;
}
}

.display-prompt-content {
display: block flex;
flex-direction: column;
flex-grow: 1;
gap: 1rem;
margin: 0;
padding: 0.2rem;

.title {
font-size: var(--step-5);
font-weight: bold;
line-height: 1.3;
color: var(--theme-text);
margin: 0;
padding: 0;
}

.text {
font-size: var(--step-4);
font-weight: normal;
line-height: 1.3;
color: var(--theme-text);
margin: 0;
padding: 0;
}
}
.display-prompt-action {
background-color: transparent;
display: block flex;
align-items: center;
justify-content: center;
margin: 1rem;
padding: 0.5rem;
border: 0.1rem solid var(--theme-border);
border-radius: 50%;
outline: 1px solid var(--theme-ring);

transition:
border 200ms ease-in-out,
outline 200ms ease-in-out;

&:hover {
cursor: pointer;
border: 0.1rem solid var(--theme-surface);
outline: 2px solid var(--theme-border-focus);
}

&:focus-visible {
box-shadow: var(--focus-box-shadow-colour-on);
border: 0.1rem solid var(--theme-surface);
outline: 2px solid var(--theme-border-focus);
}

.icon {
color: var(--theme-text);
display: block;
font-size: var(--step-5);
padding: 1rem;
}
}
}
}
}
}
Expand Down
Loading
Loading