From 54b8b838e7a778747a89f4b6bf44891035196f98 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 24 Jul 2026 01:26:47 +0200 Subject: [PATCH] =?UTF-8?q?feat(blocks):=20F2.6=20`faq`=20=E2=80=94=20prox?= =?UTF-8?q?y=20fino=20del=20Accordion=20del=20canon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La sección de preguntas: una cabecera sobre un acordeón de P/R en columna estrecha. Compound (`.Item` repite) y —la regla al envolver un componente interactivo— un PROXY FINO del `Accordion` del canon: el block lee su API y la pasa tal cual, no reinventa el disclosure. `` + `.Header` + `.List` + `.Item`. - `.List` **ES** el `Accordion`: toda su API pasa sin gate — `type` (single/multiple), `bind:value`, `collapsible`, `variant`, `size`. Defaults de FAQ: single, collapsible (el abierto se puede cerrar), outline. - `.Item` proxya `Accordion.Item > Header > Trigger`(snippet `question`) / `Content`(children) y autogenera el `value` (clave de estado) con `$props.id()` si no se pasa — la única conveniencia sobre el andamiaje. - El teclado (flechas, Home/End, Enter/Espacio), `aria-expanded` y `aria-controls` salen del `Accordion`; el block no toca la a11y del disclosure. `Container` estrecho (`md`) para una columna legible. Demo (`web/routes/blocks/faq/`): full-bleed + ruta `preview`, cinco preguntas en una columna, con la cola «¿aún tienes dudas?» que la app pone tras `.List`. Huecos a decisión del usuario (Gaps): la **lista estática 2/3 columnas** (6 de 7 en TW NO son acordeón, sino P/R siempre abiertas) — otra disposición, prop `layout` o hermano. Verificado en navegador: el acordeón abre/cierra, claro/oscuro × LTR/RTL. `blocks:check` verde (7 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 --- docs/process/CONTINUE-blocks.md | 19 ++- docs/process/PLAN-blocks.md | 14 +++ src/uix/blocks/faq/README.md | 52 ++++++++ src/uix/blocks/faq/faq-header.svelte | 15 +++ src/uix/blocks/faq/faq-item.svelte | 21 ++++ src/uix/blocks/faq/faq-list.svelte | 23 ++++ src/uix/blocks/faq/faq.svelte | 28 +++++ src/uix/blocks/faq/index.ts | 45 +++++++ src/uix/blocks/faq/types.ts | 33 ++++++ web/routes/blocks/_lib/catalog.ts | 2 +- web/routes/blocks/faq/+page.svelte | 111 ++++++++++++++++++ web/routes/blocks/faq/FaqSite.svelte | 66 +++++++++++ web/routes/blocks/faq/preview/+layout@.svelte | 30 +++++ web/routes/blocks/faq/preview/+page.svelte | 9 ++ 14 files changed, 461 insertions(+), 7 deletions(-) create mode 100644 src/uix/blocks/faq/README.md create mode 100644 src/uix/blocks/faq/faq-header.svelte create mode 100644 src/uix/blocks/faq/faq-item.svelte create mode 100644 src/uix/blocks/faq/faq-list.svelte create mode 100644 src/uix/blocks/faq/faq.svelte create mode 100644 src/uix/blocks/faq/index.ts create mode 100644 src/uix/blocks/faq/types.ts create mode 100644 web/routes/blocks/faq/+page.svelte create mode 100644 web/routes/blocks/faq/FaqSite.svelte create mode 100644 web/routes/blocks/faq/preview/+layout@.svelte create mode 100644 web/routes/blocks/faq/preview/+page.svelte diff --git a/docs/process/CONTINUE-blocks.md b/docs/process/CONTINUE-blocks.md index 45c208d70..3fc4b0ef6 100644 --- a/docs/process/CONTINUE-blocks.md +++ b/docs/process/CONTINUE-blocks.md @@ -8,12 +8,19 @@ commit `ae7b4f8c3`. ## Lo siguiente -**F2.6 `faq`** (ficha en `PLAN-blocks.md` §F2.6). Compone `Section` + `Container` -(medida estrecha) + `Heading` + `Accordion`. `.Item` = proxy fino de -`Accordion.Item` (LEER el README del Accordion antes — respetar su API real). -v1 = una columna; la brecha del dossier es la lista estática 2/3 columnas + la -cola «Still have questions?» (scope-approval). Después: stats-band → cta → -newsletter → site-footer → banner → team → contact → content-section. +**F2.7 `stats-band`** (ficha en `PLAN-blocks.md` §F2.7). Compone `Section` + +`Group`/`AutoGrid` + `Metrics` + `CountUp` + `Text`. `.Stat` = valor + etiqueta; +`CountUp` opt-in por prop. v1 = banda 2–4 stats; los formatos numéricos llegan ya +formateados o vía `FormatNumber` del app (como pricing, el block no formatea). El +dossier dice: ninguna ref puede shippear count-up (markup estático) → es nuestra +superación literal. LEER `Metrics`/`CountUp` antes. Después: cta → newsletter → +site-footer → banner → team → contact → content-section. + +**F2.4 pricing · F2.5 testimonials · F2.6 faq HECHOS.** faq = **proxy fino** de un +componente del canon (`Accordion`): cuando un block envuelve un componente +interactivo, LEE su API y la pasa tal cual (`.List` ES el `Accordion`), no +reinventa el comportamiento — solo ahorra andamiaje con `.Item`. Mismo criterio +para stats-band con `Metrics`/`CountUp`. **F2.4 `pricing` + F2.5 `testimonials` HECHOS.** pricing = primer compound CON CONTEXTO (el `Switch` escribe el periodo, los `PlanPrice` lo leen; contexto diff --git a/docs/process/PLAN-blocks.md b/docs/process/PLAN-blocks.md index 861864297..69e2d2290 100644 --- a/docs/process/PLAN-blocks.md +++ b/docs/process/PLAN-blocks.md @@ -1194,3 +1194,17 @@ Blocks diferidos: `scheduler` (bloqueado por `chronos`) · `logo-cloud` disposición → candidato a hermano `testimonial-spotlight`. Verificado en claro/oscuro/RTL, caras alineadas, 5 colores distintos. Gates: `blocks:check` verde (6 blocks) · `svelte-check` sin errores propios. **Siguiente**: F2.6 `faq`. + +- 2026-07-24 — **F2.6 `faq` HECHO**. Compound (`.Item` repite) y **proxy fino del + `Accordion` del canon** (regla del handoff: leer su API, no reinventar el + disclosure): `` + `.Header` + `.List` + `.Item`. `.List` **ES** el + `Accordion` — toda su API pasa tal cual (`type`, `bind:value`, `collapsible`, + `variant`, `size`; defaults FAQ: single, collapsible, outline). `.Item` proxya + `Accordion.Item > Header > Trigger`(snippet `question`) / `Content`(children), y + autogenera el `value` con `$props.id()` si no se pasa. `Container` estrecho + (`md`). El teclado/ARIA del disclosure salen del `Accordion`, el block no los + toca. **Hueco a decisión del usuario**: la lista estática 2/3 columnas (6 de 7 + en TW NO son acordeón) — otra disposición, prop `layout` o hermano. La cola + «¿aún tienes dudas?» es app-land (la demo la pone tras `.List`). Verificado: el + acordeón abre/cierra, claro/oscuro/RTL. Gates: `blocks:check` verde (7 blocks) · + `svelte-check` sin errores propios. **Siguiente**: F2.7 `stats-band`. diff --git a/src/uix/blocks/faq/README.md b/src/uix/blocks/faq/README.md new file mode 100644 index 000000000..8045d3909 --- /dev/null +++ b/src/uix/blocks/faq/README.md @@ -0,0 +1,52 @@ +# Faq + +## Function + +The questions section: a header over an accordion of question/answer pairs, in a +narrow readable column. Usually near the end of a page, often after pricing. + +## Composition map + +| Part | Composes | Notes | +| --- | --- | --- | +| root | bare `
` + `Section` + `Container` (narrow) + `Stack` | places the header and the list | +| `.Header` | `Box` + `Stack` | the app's section `Heading` + `Text` | +| `.List` | `Accordion` | **is** the accordion — its whole API passes through | +| `.Item` | `Accordion.Item` + `.Header` + `.Trigger` + `.Content` | thin proxy: `question` snippet → trigger, children → content | + +## Why compound (a thin proxy) + +Its `Item` **repeats**, so it earns a compound API like `feature-grid`. But the +behaviour is the canon `Accordion`'s, not the block's — the block **reads that +API and passes it straight through** (`type`, `bind:value`, `collapsible`, +`variant`, `size` on `.List`) rather than reinventing disclosure. `.Item` is the +one convenience: it spares the app the `Accordion.Item > Header > Trigger` / +`Content` scaffolding and auto-generates the item `value` when omitted. + +## Decisions + +**2026-07-24 — reference floor** (dossier §P1: Tailwind Plus «FAQs 7» · +Untitled UI «FAQ 16» · Flowbite): + +- **Adopted**: the accordion column (single, collapsible) — the interactive + format, with the canon `Accordion`'s keyboard + ARIA for free. +- **`.List` exposes the accordion tal cual**: `type='multiple'`, `bind:value`, + `variant`… are the app's to set; the block does not gate them. +- **Discarded**: reinventing the disclosure. `.Item` is a proxy, not a new + behaviour. + +## Demo + +`web/routes/blocks/faq/` — the block full-bleed, a single-column accordion of +questions. Mini-page in `FaqSite.svelte`, shared by both surfaces. + +## Gaps + +| Gap | Disposition | +| --- | --- | +| **Static 2/3-column list** (questions + answers laid out, no disclosure) | **scope-approval pending** — the dossier's FAQ gap: 6 of 7 in TW are NOT accordions. It is a different arrangement (a grid of Q&A, always open), so likely a `layout` prop or a sibling — a user decision, not a silent add | +| **"Still have questions?" tail** (a CTA to support below the list) | **app-land** — the app puts a `Text` + `Link`/`Button` after `.List`; a dedicated slot is only worth it if a demo asks | + +## Found while composing + +- _(none yet — recorded here as they surface.)_ diff --git a/src/uix/blocks/faq/faq-header.svelte b/src/uix/blocks/faq/faq-header.svelte new file mode 100644 index 000000000..b705481be --- /dev/null +++ b/src/uix/blocks/faq/faq-header.svelte @@ -0,0 +1,15 @@ + + + + + {@render children?.()} + + diff --git a/src/uix/blocks/faq/faq-item.svelte b/src/uix/blocks/faq/faq-item.svelte new file mode 100644 index 000000000..77f61792f --- /dev/null +++ b/src/uix/blocks/faq/faq-item.svelte @@ -0,0 +1,21 @@ + + + + + {@render question?.()} + + {@render children?.()} + diff --git a/src/uix/blocks/faq/faq-list.svelte b/src/uix/blocks/faq/faq-list.svelte new file mode 100644 index 000000000..c9eaef7e5 --- /dev/null +++ b/src/uix/blocks/faq/faq-list.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/src/uix/blocks/faq/faq.svelte b/src/uix/blocks/faq/faq.svelte new file mode 100644 index 000000000..d50835384 --- /dev/null +++ b/src/uix/blocks/faq/faq.svelte @@ -0,0 +1,28 @@ + + +
+
+ + + {@render children?.()} + + +
+
diff --git a/src/uix/blocks/faq/index.ts b/src/uix/blocks/faq/index.ts new file mode 100644 index 000000000..ba93d1e3e --- /dev/null +++ b/src/uix/blocks/faq/index.ts @@ -0,0 +1,45 @@ +// Faq — a header over an accordion of questions. Compound (its `Item` repeats), +// a thin proxy over the canon `Accordion`. +// +// import { Faq } from '$blocks/faq'; +// +// +// +// Preguntas frecuentes +// +// +// +// {#snippet question()}¿Cómo empiezo?{/snippet} +// Conecta el SDK y ya estás midiendo. +// +// +// +// +// `Faq.List` IS the `Accordion` — its whole API (`type`, `bind:value`, +// `collapsible`, `variant`, `size`) passes through. Every visible string is the +// app's (B-7). +import FaqComponent from './faq.svelte'; +import Header from './faq-header.svelte'; +import List from './faq-list.svelte'; +import Item from './faq-item.svelte'; + +type FaqNamespace = typeof FaqComponent & { + Header: typeof Header; + List: typeof List; + Item: typeof Item; +}; + +const Faq = FaqComponent as FaqNamespace; +Faq.Header = Header; +Faq.List = List; +Faq.Item = Item; + +export { Faq }; +export default Faq; + +export type { + FaqProps, + FaqHeaderProps as HeaderProps, + FaqListProps as ListProps, + FaqItemProps as ItemProps +} from './types'; diff --git a/src/uix/blocks/faq/types.ts b/src/uix/blocks/faq/types.ts new file mode 100644 index 000000000..5df1f958b --- /dev/null +++ b/src/uix/blocks/faq/types.ts @@ -0,0 +1,33 @@ +import type { Snippet } from 'svelte'; +import type { HTMLAttributes } from 'svelte/elements'; +import type { ContainerSize } from '$uix/eidos/components/container'; +import type { SectionSize } from '$uix/eidos/components/section'; +import type { BoxProps } from '$uix/eidos/components/box'; +import type { AccordionProps } from '$uix/eidos/components/accordion'; + +export type FaqProps = Omit, 'children'> & { + /** Content measure — narrow by default for a single readable column. @default 'md' */ + container?: ContainerSize; + /** Block-axis padding. Passed straight to `Section`. @default 'lg' */ + size?: SectionSize; + /** `Header` and `List` in composition order. */ + children?: Snippet; +}; + +/** Wraps a `Box` (measure-cap, centered). `maxWidth` defaults to `48rem`. */ +export type FaqHeaderProps = BoxProps; + +/** Wraps the canon `Accordion` — the whole accordion API (`type`, `value` + * bindable, `collapsible`, `variant`, `size`) passes straight through. */ +export type FaqListProps = AccordionProps; + +export type FaqItemProps = { + /** Stable value for the accordion's open state. Auto-generated when omitted. */ + value?: string; + /** Disable this question. @default false */ + disabled?: boolean; + /** The question — the accordion trigger's label. */ + question?: Snippet; + /** The answer — the accordion content. */ + children?: Snippet; +}; diff --git a/web/routes/blocks/_lib/catalog.ts b/web/routes/blocks/_lib/catalog.ts index 39372f21a..882e6cc6b 100644 --- a/web/routes/blocks/_lib/catalog.ts +++ b/web/routes/blocks/_lib/catalog.ts @@ -25,7 +25,7 @@ export const BLOCK_CATALOG: BlockGroup[] = [ { slug: 'feature-split', label: 'Feature split', shipped: true }, { slug: 'pricing', label: 'Pricing', shipped: true }, { slug: 'testimonials', label: 'Testimonials', shipped: true }, - { slug: 'faq', label: 'FAQ', shipped: false }, + { slug: 'faq', label: 'FAQ', shipped: true }, { slug: 'stats-band', label: 'Stats band', shipped: false }, { slug: 'cta', label: 'CTA', shipped: false }, { slug: 'newsletter', label: 'Newsletter', shipped: false }, diff --git a/web/routes/blocks/faq/+page.svelte b/web/routes/blocks/faq/+page.svelte new file mode 100644 index 000000000..c58c043dd --- /dev/null +++ b/web/routes/blocks/faq/+page.svelte @@ -0,0 +1,111 @@ + + + + {#snippet preview()} + + {/snippet} + + {#snippet lede()} + La sección de preguntas: una cabecera sobre un acordeón de preguntas y respuestas, en columna + estrecha. Es un proxy fino del Accordion del canon — el block lee + su API y la pasa tal cual, no reinventa el disclosure. + {/snippet} + + {#snippet composition()} + + + El comportamiento es del Accordion; el block solo coloca y ahorra andamiaje. + + + + Es el Accordion: toda su API pasa tal cual — + type (single/multiple), bind:value, collapsible, + variant, size. Por defecto: single, collapsible, outline. + + + Proxy de Accordion.Item: el snippet question es el trigger, los + children son el contenido. El value (clave de estado) se autogenera si no lo + pasas. + + + + {/snippet} + + {#snippet api()} + + + container (def. md, estrecho) · size (def. + lg). + + + Todo el API del Accordion: type, bind:value, + collapsible, variant, size. + + + value (autogenerado) · disabled · question (snippet) + + children (la respuesta). + + + {/snippet} + + {#snippet a11y()} + + + El teclado (flechas, Home/End, Enter/Espacio), aria-expanded y + aria-controls salen del Accordion del canon — el block no toca la + a11y del disclosure. + + + La sección un h2; cada pregunta un h3 (nivel del + Accordion.Item). + + + {/snippet} + + {#snippet gaps()} + + + Scope-approval pendiente — la brecha del dossier: 6 de 7 en TW NO son + acordeón, sino una rejilla de P/R siempre abiertas. Otra disposición → prop + layout o hermano. + + + App-land — la app pone un Text + Link/Button + tras .List (como en esta demo); un slot propio solo si una demo lo pide. + + + {/snippet} + + {#snippet notes()} + + + El disclosure es del Accordion. El block no reimplementa apertura ni teclado — + pasa la API tal cual. La única conveniencia es .Item, que ahorra el andamiaje + Item > Header > Trigger / Content. + + + {/snippet} + diff --git a/web/routes/blocks/faq/FaqSite.svelte b/web/routes/blocks/faq/FaqSite.svelte new file mode 100644 index 000000000..44caff82d --- /dev/null +++ b/web/routes/blocks/faq/FaqSite.svelte @@ -0,0 +1,66 @@ + + + + + Preguntas frecuentes + + Lo que casi todo el mundo pregunta antes de conectar la primera fuente de eventos. + + + + + {#each faqs as item (item.q)} + + {#snippet question()}{item.q}{/snippet} + {item.a} + + {/each} + + + + +
+ + + ¿Te queda alguna duda? + + Escríbenos y te respondemos el mismo día. Hablar con soporte. + + + +
diff --git a/web/routes/blocks/faq/preview/+layout@.svelte b/web/routes/blocks/faq/preview/+layout@.svelte new file mode 100644 index 000000000..8da588099 --- /dev/null +++ b/web/routes/blocks/faq/preview/+layout@.svelte @@ -0,0 +1,30 @@ + + + + + + + +
+ {@render children?.()} +
+
diff --git a/web/routes/blocks/faq/preview/+page.svelte b/web/routes/blocks/faq/preview/+page.svelte new file mode 100644 index 000000000..abf0726a9 --- /dev/null +++ b/web/routes/blocks/faq/preview/+page.svelte @@ -0,0 +1,9 @@ + + +