@a4ui/core 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/elements.css +76 -1
- package/dist/full.css +76 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2379 -2177
- package/dist/layout/Aurora.d.ts +25 -0
- package/dist/styles.css +15 -0
- package/dist/ui/Configurator.d.ts +76 -0
- package/package.json +1 -1
- package/preset.js +3 -1
- package/src/index.ts +10 -1
- package/src/layout/Aurora.tsx +66 -0
- package/src/ui/Configurator.tsx +215 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface AuroraProps {
|
|
3
|
+
/** Blob opacity multiplier, 0..1 — higher = more visible color. @default 0.45 */
|
|
4
|
+
intensity?: number;
|
|
5
|
+
/** Slowly drift/scale the blobs. Reduced-motion aware (holds still). @default false */
|
|
6
|
+
animated?: boolean;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Ambient, theme-tinted blurred-blob backdrop that makes glass surfaces read as
|
|
11
|
+
* glass. Fixed behind the page (paints its own base background); keep the page
|
|
12
|
+
* root transparent. Reduced-motion aware when `animated`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* // at the top of your app/layout (root must NOT set its own bg):
|
|
17
|
+
* <div class="relative min-h-screen text-foreground">
|
|
18
|
+
* <Aurora />
|
|
19
|
+
* <YourPage />
|
|
20
|
+
* </div>
|
|
21
|
+
*
|
|
22
|
+
* <Aurora intensity={0.6} animated />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function Aurora(props: AuroraProps): JSX.Element;
|
package/dist/styles.css
CHANGED
|
@@ -228,6 +228,21 @@ body {
|
|
|
228
228
|
animation: list-row-in 0.35s both ease-out;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/* Aurora — slow ambient drift for the blurred backdrop blobs (opt-in via
|
|
232
|
+
<Aurora animated>). Held still under reduced motion by the global rule below. */
|
|
233
|
+
@keyframes aurora-drift {
|
|
234
|
+
0%,
|
|
235
|
+
100% {
|
|
236
|
+
transform: translate3d(0, 0, 0) scale(1);
|
|
237
|
+
}
|
|
238
|
+
50% {
|
|
239
|
+
transform: translate3d(3%, -3%, 0) scale(1.08);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
.aurora-drift {
|
|
243
|
+
animation: aurora-drift 18s ease-in-out infinite;
|
|
244
|
+
}
|
|
245
|
+
|
|
231
246
|
/* Table row enter/exit fade. */
|
|
232
247
|
.row-enter-active,
|
|
233
248
|
.row-exit-active {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
/** A selectable option in a `'select'` group. */
|
|
3
|
+
export interface ConfiguratorOption {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
/** Added to the running total when selected. */
|
|
7
|
+
price?: number;
|
|
8
|
+
}
|
|
9
|
+
/** A countable item in a `'counter'` group (each with a +/- stepper). */
|
|
10
|
+
export interface ConfiguratorItem {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
/** Added to the total as `price * qty`. */
|
|
14
|
+
price?: number;
|
|
15
|
+
}
|
|
16
|
+
/** One group of controls. `select` = single choice, `counter` = quantities, `text` = free text. */
|
|
17
|
+
export type ConfiguratorGroup = {
|
|
18
|
+
type: 'select';
|
|
19
|
+
name: string;
|
|
20
|
+
label: string;
|
|
21
|
+
options: ConfiguratorOption[];
|
|
22
|
+
} | {
|
|
23
|
+
type: 'counter';
|
|
24
|
+
name: string;
|
|
25
|
+
label: string;
|
|
26
|
+
items: ConfiguratorItem[];
|
|
27
|
+
} | {
|
|
28
|
+
type: 'text';
|
|
29
|
+
name: string;
|
|
30
|
+
label: string;
|
|
31
|
+
placeholder?: string;
|
|
32
|
+
maxLength?: number;
|
|
33
|
+
};
|
|
34
|
+
/** The configurator's state: keyed by group `name`. select/text -> string, counter -> `{ [id]: qty }`. */
|
|
35
|
+
export type ConfiguratorState = Record<string, string | Record<string, number>>;
|
|
36
|
+
export interface ConfiguratorProps {
|
|
37
|
+
groups: ConfiguratorGroup[];
|
|
38
|
+
value: ConfiguratorState;
|
|
39
|
+
onChange: (next: ConfiguratorState) => void;
|
|
40
|
+
/** Format the running total. @default `n => $${n}` */
|
|
41
|
+
formatTotal?: (total: number) => string;
|
|
42
|
+
/** Live preview, rendered beside the controls; receives the state and running total. */
|
|
43
|
+
preview?: (state: ConfiguratorState, total: number) => JSX.Element;
|
|
44
|
+
/** CTA under the summary (e.g. "add to cart" / "request quote"). */
|
|
45
|
+
action?: {
|
|
46
|
+
label: string;
|
|
47
|
+
onClick?: () => void;
|
|
48
|
+
href?: string;
|
|
49
|
+
};
|
|
50
|
+
class?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Data-driven product configurator: declare `groups` (select / counter / text),
|
|
54
|
+
* hold the `value` state in the parent, and get option pickers, quantity
|
|
55
|
+
* steppers, a live summary with a running total, and an optional `preview`.
|
|
56
|
+
* Engine-free, fully typed.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```tsx
|
|
60
|
+
* const [cfg, setCfg] = createSignal<ConfiguratorState>({ size: 'm', extras: {} })
|
|
61
|
+
* <Configurator
|
|
62
|
+
* value={cfg()}
|
|
63
|
+
* onChange={setCfg}
|
|
64
|
+
* groups={[
|
|
65
|
+
* { type: 'select', name: 'size', label: 'Tamaño', options: [
|
|
66
|
+
* { value: 's', label: 'Chico', price: 320 }, { value: 'm', label: 'Mediano', price: 620 } ] },
|
|
67
|
+
* { type: 'counter', name: 'extras', label: 'Extras', items: [
|
|
68
|
+
* { id: 'jamon', label: 'Jamón serrano', price: 90 } ] },
|
|
69
|
+
* { type: 'text', name: 'nota', label: 'Nota', placeholder: 'Para grabar…' },
|
|
70
|
+
* ]}
|
|
71
|
+
* preview={(state, total) => <MyPreview state={state} total={total} />}
|
|
72
|
+
* action={{ label: 'Solicitar', onClick: submit }}
|
|
73
|
+
* />
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare function Configurator(props: ConfiguratorProps): JSX.Element;
|
package/package.json
CHANGED
package/preset.js
CHANGED
|
@@ -35,7 +35,9 @@ const glass = plugin(({ addComponents }) => {
|
|
|
35
35
|
transition: 'transform .25s cubic-bezier(.16,1,.3,1), box-shadow .25s ease, border-color .2s ease',
|
|
36
36
|
},
|
|
37
37
|
"[data-theme='light'] .card": {
|
|
38
|
-
|
|
38
|
+
// A touch more transparent than fully opaque so the frosted blur reads on
|
|
39
|
+
// light themes (over an Aurora/scenery backdrop) — text stays legible.
|
|
40
|
+
background: 'hsl(var(--card) / 0.6)',
|
|
39
41
|
border: '1px solid hsl(var(--border))',
|
|
40
42
|
},
|
|
41
43
|
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// import '@a4ui/core/styles.css'
|
|
9
9
|
// import { Button, Card, Modal } from '@a4ui/core'
|
|
10
10
|
|
|
11
|
-
export const A4UI_VERSION = '0.
|
|
11
|
+
export const A4UI_VERSION = '0.21.0'
|
|
12
12
|
|
|
13
13
|
// Helpers (src/lib) — generic, framework-level utilities.
|
|
14
14
|
export { cn } from './lib/cn'
|
|
@@ -88,6 +88,14 @@ export { Timeline, type TimelineItem, type TimelineTone, type TimelineProps } fr
|
|
|
88
88
|
export { Rating, type RatingProps } from './ui/Rating'
|
|
89
89
|
export { PricingTable, type PricingTier, type PricingPeriod } from './ui/PricingTable'
|
|
90
90
|
export { BeforeAfter, type BeforeAfterProps } from './ui/BeforeAfter'
|
|
91
|
+
export {
|
|
92
|
+
Configurator,
|
|
93
|
+
type ConfiguratorProps,
|
|
94
|
+
type ConfiguratorGroup,
|
|
95
|
+
type ConfiguratorState,
|
|
96
|
+
type ConfiguratorOption,
|
|
97
|
+
type ConfiguratorItem,
|
|
98
|
+
} from './ui/Configurator'
|
|
91
99
|
export { Empty, type EmptyProps } from './ui/Empty'
|
|
92
100
|
export { Calendar, type CalendarProps } from './ui/Calendar'
|
|
93
101
|
export { Tree, type TreeNode, type TreeProps } from './ui/Tree'
|
|
@@ -164,6 +172,7 @@ export { flyToCart, type FlyToCartOptions } from './lib/flyToCart'
|
|
|
164
172
|
// (Sidebar, Topbar, CompanySwitcher, DemoBanner, CommandPalette) stay in the app.
|
|
165
173
|
export { AppShell } from './layout/AppShell'
|
|
166
174
|
export { SpaceBackground } from './layout/SpaceBackground'
|
|
175
|
+
export { Aurora, type AuroraProps } from './layout/Aurora'
|
|
167
176
|
export { ThemedScenery, type ThemedSceneryProps } from './layout/ThemedScenery'
|
|
168
177
|
export { SnowScenery } from './layout/SnowScenery'
|
|
169
178
|
export { ChristmasBackground } from './layout/ChristmasBackground'
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Aurora — an ambient backdrop of soft, blurred color blobs tinted with the
|
|
2
|
+
// theme tokens (`--primary` / `--accent`). Fixed behind the page so glass
|
|
3
|
+
// surfaces read as glass: as glassy cards scroll over the fixed blobs, their
|
|
4
|
+
// backdrop-blur samples the color behind them. Every theme tints it differently
|
|
5
|
+
// for free. This is the lightweight, no-starfield alternative to
|
|
6
|
+
// `SpaceBackground` / `ThemedScenery`.
|
|
7
|
+
//
|
|
8
|
+
// Usage: render it once at the top of your layout and keep the page root's
|
|
9
|
+
// background transparent (don't put `bg-background` on the root) so the Aurora
|
|
10
|
+
// shows through — the component paints the base background itself.
|
|
11
|
+
import { For, type JSX } from 'solid-js'
|
|
12
|
+
|
|
13
|
+
import { cn } from '../lib/cn'
|
|
14
|
+
|
|
15
|
+
export interface AuroraProps {
|
|
16
|
+
/** Blob opacity multiplier, 0..1 — higher = more visible color. @default 0.45 */
|
|
17
|
+
intensity?: number
|
|
18
|
+
/** Slowly drift/scale the blobs. Reduced-motion aware (holds still). @default false */
|
|
19
|
+
animated?: boolean
|
|
20
|
+
class?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Static class strings (scanned by Tailwind). token = which theme color; a =
|
|
24
|
+
// per-blob opacity weight, multiplied by `intensity`.
|
|
25
|
+
const BLOBS = [
|
|
26
|
+
{ pos: '-left-40 -top-32', size: 'h-[40rem] w-[40rem]', token: '--primary', a: 1 },
|
|
27
|
+
{ pos: '-right-32 top-1/4', size: 'h-[38rem] w-[38rem]', token: '--accent', a: 0.9 },
|
|
28
|
+
{ pos: 'bottom-[-10%] left-1/3', size: 'h-[36rem] w-[36rem]', token: '--primary', a: 0.75 },
|
|
29
|
+
] as const
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Ambient, theme-tinted blurred-blob backdrop that makes glass surfaces read as
|
|
33
|
+
* glass. Fixed behind the page (paints its own base background); keep the page
|
|
34
|
+
* root transparent. Reduced-motion aware when `animated`.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* // at the top of your app/layout (root must NOT set its own bg):
|
|
39
|
+
* <div class="relative min-h-screen text-foreground">
|
|
40
|
+
* <Aurora />
|
|
41
|
+
* <YourPage />
|
|
42
|
+
* </div>
|
|
43
|
+
*
|
|
44
|
+
* <Aurora intensity={0.6} animated />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function Aurora(props: AuroraProps): JSX.Element {
|
|
48
|
+
const intensity = (): number => props.intensity ?? 0.45
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
aria-hidden="true"
|
|
52
|
+
class={cn('pointer-events-none fixed inset-0 -z-10 overflow-hidden bg-background', props.class)}
|
|
53
|
+
>
|
|
54
|
+
<For each={BLOBS}>
|
|
55
|
+
{(b) => (
|
|
56
|
+
<div
|
|
57
|
+
class={cn('absolute rounded-full blur-3xl', b.pos, b.size, props.animated && 'aurora-drift')}
|
|
58
|
+
style={{
|
|
59
|
+
background: `radial-gradient(circle, hsl(var(${b.token}) / ${(intensity() * b.a).toFixed(2)}), transparent 70%)`,
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
</For>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Configurator — a data-driven "pick options -> live preview + running total"
|
|
2
|
+
// builder. Distilled from two templates that hand-rolled the same shape: a
|
|
3
|
+
// laser-engraving customizer (text + product/style pickers) and a charcuterie
|
|
4
|
+
// board builder (size + per-item counters). Controlled: the parent owns the
|
|
5
|
+
// state object; the Configurator renders the group controls, a live summary
|
|
6
|
+
// with a running total, and an optional preview render-prop.
|
|
7
|
+
import type { JSX } from 'solid-js'
|
|
8
|
+
import { createMemo, For, Show } from 'solid-js'
|
|
9
|
+
|
|
10
|
+
import { cn } from '../lib/cn'
|
|
11
|
+
import { Button } from './Button'
|
|
12
|
+
|
|
13
|
+
/** A selectable option in a `'select'` group. */
|
|
14
|
+
export interface ConfiguratorOption {
|
|
15
|
+
value: string
|
|
16
|
+
label: string
|
|
17
|
+
/** Added to the running total when selected. */
|
|
18
|
+
price?: number
|
|
19
|
+
}
|
|
20
|
+
/** A countable item in a `'counter'` group (each with a +/- stepper). */
|
|
21
|
+
export interface ConfiguratorItem {
|
|
22
|
+
id: string
|
|
23
|
+
label: string
|
|
24
|
+
/** Added to the total as `price * qty`. */
|
|
25
|
+
price?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One group of controls. `select` = single choice, `counter` = quantities, `text` = free text. */
|
|
29
|
+
export type ConfiguratorGroup =
|
|
30
|
+
| { type: 'select'; name: string; label: string; options: ConfiguratorOption[] }
|
|
31
|
+
| { type: 'counter'; name: string; label: string; items: ConfiguratorItem[] }
|
|
32
|
+
| { type: 'text'; name: string; label: string; placeholder?: string; maxLength?: number }
|
|
33
|
+
|
|
34
|
+
/** The configurator's state: keyed by group `name`. select/text -> string, counter -> `{ [id]: qty }`. */
|
|
35
|
+
export type ConfiguratorState = Record<string, string | Record<string, number>>
|
|
36
|
+
|
|
37
|
+
export interface ConfiguratorProps {
|
|
38
|
+
groups: ConfiguratorGroup[]
|
|
39
|
+
value: ConfiguratorState
|
|
40
|
+
onChange: (next: ConfiguratorState) => void
|
|
41
|
+
/** Format the running total. @default `n => $${n}` */
|
|
42
|
+
formatTotal?: (total: number) => string
|
|
43
|
+
/** Live preview, rendered beside the controls; receives the state and running total. */
|
|
44
|
+
preview?: (state: ConfiguratorState, total: number) => JSX.Element
|
|
45
|
+
/** CTA under the summary (e.g. "add to cart" / "request quote"). */
|
|
46
|
+
action?: { label: string; onClick?: () => void; href?: string }
|
|
47
|
+
class?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const asString = (v: string | Record<string, number> | undefined): string => (typeof v === 'string' ? v : '')
|
|
51
|
+
const asCounts = (v: string | Record<string, number> | undefined): Record<string, number> =>
|
|
52
|
+
v && typeof v === 'object' ? v : {}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Data-driven product configurator: declare `groups` (select / counter / text),
|
|
56
|
+
* hold the `value` state in the parent, and get option pickers, quantity
|
|
57
|
+
* steppers, a live summary with a running total, and an optional `preview`.
|
|
58
|
+
* Engine-free, fully typed.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* const [cfg, setCfg] = createSignal<ConfiguratorState>({ size: 'm', extras: {} })
|
|
63
|
+
* <Configurator
|
|
64
|
+
* value={cfg()}
|
|
65
|
+
* onChange={setCfg}
|
|
66
|
+
* groups={[
|
|
67
|
+
* { type: 'select', name: 'size', label: 'Tamaño', options: [
|
|
68
|
+
* { value: 's', label: 'Chico', price: 320 }, { value: 'm', label: 'Mediano', price: 620 } ] },
|
|
69
|
+
* { type: 'counter', name: 'extras', label: 'Extras', items: [
|
|
70
|
+
* { id: 'jamon', label: 'Jamón serrano', price: 90 } ] },
|
|
71
|
+
* { type: 'text', name: 'nota', label: 'Nota', placeholder: 'Para grabar…' },
|
|
72
|
+
* ]}
|
|
73
|
+
* preview={(state, total) => <MyPreview state={state} total={total} />}
|
|
74
|
+
* action={{ label: 'Solicitar', onClick: submit }}
|
|
75
|
+
* />
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function Configurator(props: ConfiguratorProps): JSX.Element {
|
|
79
|
+
const fmt = (n: number): string => (props.formatTotal ?? ((x: number) => `$${x}`))(n)
|
|
80
|
+
|
|
81
|
+
const total = createMemo(() => {
|
|
82
|
+
let sum = 0
|
|
83
|
+
for (const g of props.groups) {
|
|
84
|
+
if (g.type === 'select') {
|
|
85
|
+
const chosen = asString(props.value[g.name])
|
|
86
|
+
sum += g.options.find((o) => o.value === chosen)?.price ?? 0
|
|
87
|
+
} else if (g.type === 'counter') {
|
|
88
|
+
const counts = asCounts(props.value[g.name])
|
|
89
|
+
for (const it of g.items) sum += (it.price ?? 0) * (counts[it.id] ?? 0)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return sum
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const setSelect = (name: string, value: string): void => props.onChange({ ...props.value, [name]: value })
|
|
96
|
+
const setCount = (name: string, id: string, qty: number): void => {
|
|
97
|
+
const counts = { ...asCounts(props.value[name]) }
|
|
98
|
+
if (qty <= 0) delete counts[id]
|
|
99
|
+
else counts[id] = qty
|
|
100
|
+
props.onChange({ ...props.value, [name]: counts })
|
|
101
|
+
}
|
|
102
|
+
const setText = (name: string, value: string): void => props.onChange({ ...props.value, [name]: value })
|
|
103
|
+
|
|
104
|
+
const controls = (
|
|
105
|
+
<div class="space-y-6">
|
|
106
|
+
<For each={props.groups}>
|
|
107
|
+
{(g) => (
|
|
108
|
+
<div>
|
|
109
|
+
<p class="mb-2 text-sm font-medium text-foreground">{g.label}</p>
|
|
110
|
+
<Show when={g.type === 'select' && g}>
|
|
111
|
+
{(sel) => (
|
|
112
|
+
<div class="flex flex-wrap gap-2">
|
|
113
|
+
<For each={sel().options}>
|
|
114
|
+
{(o) => (
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
aria-pressed={asString(props.value[sel().name]) === o.value}
|
|
118
|
+
onClick={() => setSelect(sel().name, o.value)}
|
|
119
|
+
class={cn(
|
|
120
|
+
'rounded-lg border px-3 py-1.5 text-sm transition-colors',
|
|
121
|
+
asString(props.value[sel().name]) === o.value
|
|
122
|
+
? 'border-primary bg-primary/10 text-primary'
|
|
123
|
+
: 'border-border text-muted-foreground hover:text-foreground',
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
{o.label}
|
|
127
|
+
<Show when={o.price}>
|
|
128
|
+
<span class="ml-1 opacity-70">· {fmt(o.price as number)}</span>
|
|
129
|
+
</Show>
|
|
130
|
+
</button>
|
|
131
|
+
)}
|
|
132
|
+
</For>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</Show>
|
|
136
|
+
<Show when={g.type === 'counter' && g}>
|
|
137
|
+
{(cnt) => (
|
|
138
|
+
<div class="space-y-2">
|
|
139
|
+
<For each={cnt().items}>
|
|
140
|
+
{(it) => {
|
|
141
|
+
const qty = (): number => asCounts(props.value[cnt().name])[it.id] ?? 0
|
|
142
|
+
return (
|
|
143
|
+
<div class="flex items-center justify-between gap-3">
|
|
144
|
+
<span class="text-sm text-foreground">
|
|
145
|
+
{it.label}
|
|
146
|
+
<Show when={it.price}>
|
|
147
|
+
<span class="ml-1 text-muted-foreground">· {fmt(it.price as number)}</span>
|
|
148
|
+
</Show>
|
|
149
|
+
</span>
|
|
150
|
+
<div class="flex items-center gap-2">
|
|
151
|
+
<Button
|
|
152
|
+
variant="outline"
|
|
153
|
+
aria-label={`Quitar ${it.label}`}
|
|
154
|
+
onClick={() => setCount(cnt().name, it.id, qty() - 1)}
|
|
155
|
+
class="h-8 w-8 p-0"
|
|
156
|
+
>
|
|
157
|
+
−
|
|
158
|
+
</Button>
|
|
159
|
+
<span class="w-6 text-center text-sm tabular-nums">{qty()}</span>
|
|
160
|
+
<Button
|
|
161
|
+
variant="outline"
|
|
162
|
+
aria-label={`Agregar ${it.label}`}
|
|
163
|
+
onClick={() => setCount(cnt().name, it.id, qty() + 1)}
|
|
164
|
+
class="h-8 w-8 p-0"
|
|
165
|
+
>
|
|
166
|
+
+
|
|
167
|
+
</Button>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
)
|
|
171
|
+
}}
|
|
172
|
+
</For>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
</Show>
|
|
176
|
+
<Show when={g.type === 'text' && g}>
|
|
177
|
+
{(txt) => (
|
|
178
|
+
<input
|
|
179
|
+
type="text"
|
|
180
|
+
value={asString(props.value[txt().name])}
|
|
181
|
+
maxLength={txt().maxLength}
|
|
182
|
+
placeholder={txt().placeholder}
|
|
183
|
+
aria-label={txt().label}
|
|
184
|
+
onInput={(e) => setText(txt().name, e.currentTarget.value)}
|
|
185
|
+
class="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
186
|
+
/>
|
|
187
|
+
)}
|
|
188
|
+
</Show>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
</For>
|
|
192
|
+
|
|
193
|
+
<div class="flex items-center justify-between border-t border-border pt-4">
|
|
194
|
+
<span class="text-sm text-muted-foreground">Total</span>
|
|
195
|
+
<span class="text-lg font-semibold text-foreground tabular-nums">{fmt(total())}</span>
|
|
196
|
+
</div>
|
|
197
|
+
<Show when={props.action}>
|
|
198
|
+
{(a) => (
|
|
199
|
+
<Button ripple href={a().href} onClick={() => a().onClick?.()} class="w-full justify-center">
|
|
200
|
+
{a().label}
|
|
201
|
+
</Button>
|
|
202
|
+
)}
|
|
203
|
+
</Show>
|
|
204
|
+
</div>
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div class={cn('grid gap-6', props.preview ? 'md:grid-cols-2' : '', props.class)}>
|
|
209
|
+
{controls}
|
|
210
|
+
<Show when={props.preview}>
|
|
211
|
+
<div class="min-w-0">{props.preview?.(props.value, total())}</div>
|
|
212
|
+
</Show>
|
|
213
|
+
</div>
|
|
214
|
+
)
|
|
215
|
+
}
|