@a4ui/core 0.22.0 → 0.24.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/README.md +17 -9
- package/dist/NumberInput-enx_0iVh.js +488 -0
- package/dist/commerce/ConditionScale.d.ts +45 -0
- package/dist/commerce/FacetSidebar.d.ts +55 -0
- package/dist/commerce/PriceBlock.d.ts +50 -0
- package/dist/commerce/index.d.ts +3 -0
- package/dist/commerce.js +465 -150
- package/dist/elements.css +132 -0
- package/dist/elements.iife.js +2 -2
- package/dist/elements.js +2047 -1842
- package/dist/full.css +132 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3026 -2827
- package/dist/ui/AnnouncementBar.d.ts +36 -0
- package/dist/ui/LogoWall.d.ts +31 -0
- package/dist/ui/RatingsSummary.d.ts +44 -0
- package/dist/ui/SpecSheet.d.ts +47 -0
- package/package.json +1 -1
- package/src/commerce/ConditionScale.tsx +114 -0
- package/src/commerce/FacetSidebar.tsx +222 -0
- package/src/commerce/PriceBlock.tsx +133 -0
- package/src/commerce/index.ts +9 -0
- package/src/index.ts +5 -1
- package/src/ui/AnnouncementBar.tsx +124 -0
- package/src/ui/LogoWall.tsx +94 -0
- package/src/ui/RatingsSummary.tsx +128 -0
- package/src/ui/SpecSheet.tsx +89 -0
- package/dist/Checkbox-DIzLQltI.js +0 -431
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Full-bleed promo bar. Optionally dismissible (local signal, closes for the
|
|
2
|
+
// session only — no persistence) and optionally carries a copyable coupon
|
|
3
|
+
// code pill. Kept dependency-free: clipboard access is feature-detected, not
|
|
4
|
+
// polyfilled, so it degrades to "show the code, copy button is a no-op" on
|
|
5
|
+
// browsers/contexts without `navigator.clipboard`.
|
|
6
|
+
import type { JSX } from 'solid-js'
|
|
7
|
+
import { createSignal, Show } from 'solid-js'
|
|
8
|
+
import { Check, Copy, X } from 'lucide-solid'
|
|
9
|
+
|
|
10
|
+
import { cn } from '../lib/cn'
|
|
11
|
+
|
|
12
|
+
/** Background tone of an {@link AnnouncementBar}. */
|
|
13
|
+
export type AnnouncementTone = 'primary' | 'accent' | 'neutral'
|
|
14
|
+
|
|
15
|
+
const TONE: Record<AnnouncementTone, string> = {
|
|
16
|
+
primary: 'bg-primary text-primary-foreground',
|
|
17
|
+
accent: 'bg-accent text-accent-foreground',
|
|
18
|
+
neutral: 'bg-muted text-foreground',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AnnouncementBarProps {
|
|
22
|
+
/** Message content. */
|
|
23
|
+
children: JSX.Element
|
|
24
|
+
/** Background tone. Defaults to `'primary'`. */
|
|
25
|
+
tone?: AnnouncementTone
|
|
26
|
+
/** Show a dismiss (X) button. Defaults to `false`. */
|
|
27
|
+
dismissible?: boolean
|
|
28
|
+
/** Optional coupon code rendered as a copyable pill. */
|
|
29
|
+
couponCode?: string
|
|
30
|
+
/** Make the whole message a link. */
|
|
31
|
+
href?: string
|
|
32
|
+
/** Called after the bar is dismissed. */
|
|
33
|
+
onDismiss?: () => void
|
|
34
|
+
class?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A full-width promo/notice bar, meant to sit above or below the page header.
|
|
39
|
+
* Supports an optional dismiss button, an optional link wrapping the message,
|
|
40
|
+
* and an optional copyable coupon-code pill.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* <AnnouncementBar
|
|
45
|
+
* tone="accent"
|
|
46
|
+
* dismissible
|
|
47
|
+
* couponCode="SAVE20"
|
|
48
|
+
* href="/sale"
|
|
49
|
+
* >
|
|
50
|
+
* Summer sale is live — 20% off everything
|
|
51
|
+
* </AnnouncementBar>
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function AnnouncementBar(props: AnnouncementBarProps): JSX.Element {
|
|
55
|
+
const [dismissed, setDismissed] = createSignal(false)
|
|
56
|
+
const [copied, setCopied] = createSignal(false)
|
|
57
|
+
|
|
58
|
+
const copyCode = async () => {
|
|
59
|
+
const code = props.couponCode
|
|
60
|
+
if (!code || !navigator.clipboard) return
|
|
61
|
+
await navigator.clipboard.writeText(code)
|
|
62
|
+
setCopied(true)
|
|
63
|
+
setTimeout(() => setCopied(false), 1500)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const dismiss = () => {
|
|
67
|
+
setDismissed(true)
|
|
68
|
+
props.onDismiss?.()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Show when={!dismissed()}>
|
|
73
|
+
<div
|
|
74
|
+
role="region"
|
|
75
|
+
aria-label="Announcement"
|
|
76
|
+
class={cn(
|
|
77
|
+
'relative w-full rounded-none px-4 py-2 text-center text-sm',
|
|
78
|
+
TONE[props.tone ?? 'primary'],
|
|
79
|
+
props.dismissible && 'pr-10',
|
|
80
|
+
props.class,
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
<Show when={props.href} fallback={<span>{props.children}</span>}>
|
|
84
|
+
<a
|
|
85
|
+
href={props.href}
|
|
86
|
+
class="underline-offset-2 transition-colors hover:underline motion-reduce:transition-none"
|
|
87
|
+
>
|
|
88
|
+
{props.children}
|
|
89
|
+
</a>
|
|
90
|
+
</Show>
|
|
91
|
+
|
|
92
|
+
<Show when={props.couponCode}>
|
|
93
|
+
{(code) => (
|
|
94
|
+
<span class="ml-2 inline-flex items-center gap-1 rounded-md bg-black/10 px-2 py-0.5 font-mono">
|
|
95
|
+
{code()}
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
aria-label={copied() ? 'Coupon code copied' : 'Copy coupon code'}
|
|
99
|
+
onClick={copyCode}
|
|
100
|
+
class="inline-flex items-center gap-1 transition-opacity hover:opacity-80 motion-reduce:transition-none"
|
|
101
|
+
>
|
|
102
|
+
<Show when={copied()} fallback={<Copy class="h-3 w-3" />}>
|
|
103
|
+
<Check class="h-3 w-3" />
|
|
104
|
+
Copied
|
|
105
|
+
</Show>
|
|
106
|
+
</button>
|
|
107
|
+
</span>
|
|
108
|
+
)}
|
|
109
|
+
</Show>
|
|
110
|
+
|
|
111
|
+
<Show when={props.dismissible}>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
aria-label="Dismiss announcement"
|
|
115
|
+
onClick={dismiss}
|
|
116
|
+
class="absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-1 transition-opacity hover:opacity-80 motion-reduce:transition-none"
|
|
117
|
+
>
|
|
118
|
+
<X class="h-4 w-4" />
|
|
119
|
+
</button>
|
|
120
|
+
</Show>
|
|
121
|
+
</div>
|
|
122
|
+
</Show>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js'
|
|
2
|
+
import { For, Show } from 'solid-js'
|
|
3
|
+
|
|
4
|
+
import { cn } from '../lib/cn'
|
|
5
|
+
|
|
6
|
+
export interface LogoItem {
|
|
7
|
+
src: string
|
|
8
|
+
alt: string
|
|
9
|
+
href?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface LogoWallProps {
|
|
13
|
+
logos: LogoItem[]
|
|
14
|
+
/** Grayscale until hover. Defaults to true. */
|
|
15
|
+
grayscale?: boolean
|
|
16
|
+
/** Columns on desktop. Defaults to a responsive auto-fit. */
|
|
17
|
+
columns?: number
|
|
18
|
+
class?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Static Tailwind class lookups — dynamic `grid-cols-${n}` strings aren't
|
|
22
|
+
// picked up by the JIT scanner, so the desktop/mobile column counts must be
|
|
23
|
+
// literal class names known at build time.
|
|
24
|
+
const DESKTOP_COLS: Record<number, string> = {
|
|
25
|
+
2: 'md:grid-cols-2',
|
|
26
|
+
3: 'md:grid-cols-3',
|
|
27
|
+
4: 'md:grid-cols-4',
|
|
28
|
+
5: 'md:grid-cols-5',
|
|
29
|
+
6: 'md:grid-cols-6',
|
|
30
|
+
7: 'md:grid-cols-7',
|
|
31
|
+
8: 'md:grid-cols-8',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MOBILE_COLS: Record<number, string> = {
|
|
35
|
+
2: 'grid-cols-2',
|
|
36
|
+
3: 'grid-cols-3',
|
|
37
|
+
4: 'grid-cols-3',
|
|
38
|
+
5: 'grid-cols-3',
|
|
39
|
+
6: 'grid-cols-3',
|
|
40
|
+
7: 'grid-cols-3',
|
|
41
|
+
8: 'grid-cols-3',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* "As seen in" grid of press/partner logos, grayscale by default with a
|
|
46
|
+
* color-on-hover reveal. Each logo can optionally link out; sits neutral
|
|
47
|
+
* on any background.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* <LogoWall
|
|
52
|
+
* logos={[
|
|
53
|
+
* { src: 'https://picsum.photos/seed/techdaily/160/48', alt: 'TechDaily' },
|
|
54
|
+
* { src: 'https://picsum.photos/seed/stylemag/160/48', alt: 'StyleMag', href: 'https://stylemag.example' },
|
|
55
|
+
* { src: 'https://picsum.photos/seed/marketwire/160/48', alt: 'MarketWire' },
|
|
56
|
+
* ]}
|
|
57
|
+
* />
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function LogoWall(props: LogoWallProps): JSX.Element {
|
|
61
|
+
const grayscale = () => props.grayscale !== false
|
|
62
|
+
const gridCols = () =>
|
|
63
|
+
props.columns !== undefined
|
|
64
|
+
? cn(MOBILE_COLS[props.columns] ?? 'grid-cols-2', DESKTOP_COLS[props.columns] ?? 'md:grid-cols-6')
|
|
65
|
+
: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6'
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div class={cn('grid items-center gap-6 md:gap-8', gridCols(), props.class)}>
|
|
69
|
+
<For each={props.logos}>
|
|
70
|
+
{(logo) => {
|
|
71
|
+
const img = (
|
|
72
|
+
<img
|
|
73
|
+
src={logo.src}
|
|
74
|
+
alt={logo.alt}
|
|
75
|
+
loading="lazy"
|
|
76
|
+
class={cn(
|
|
77
|
+
'mx-auto max-h-10 w-auto object-contain opacity-70 transition duration-300',
|
|
78
|
+
grayscale() && 'grayscale hover:grayscale-0 hover:opacity-100',
|
|
79
|
+
!grayscale() && 'hover:opacity-100',
|
|
80
|
+
)}
|
|
81
|
+
/>
|
|
82
|
+
)
|
|
83
|
+
return (
|
|
84
|
+
<Show when={logo.href} fallback={img}>
|
|
85
|
+
<a href={logo.href} target="_blank" rel="noreferrer" aria-label={logo.alt}>
|
|
86
|
+
{img}
|
|
87
|
+
</a>
|
|
88
|
+
</Show>
|
|
89
|
+
)
|
|
90
|
+
}}
|
|
91
|
+
</For>
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// RatingsSummary — a compact trust-band grid of aggregate ratings pulled
|
|
2
|
+
// from multiple review sources, each rendered as a small glass-neutral tile.
|
|
3
|
+
import { Star } from 'lucide-solid'
|
|
4
|
+
import type { JSX } from 'solid-js'
|
|
5
|
+
import { For, Show } from 'solid-js'
|
|
6
|
+
|
|
7
|
+
import { cn } from '../lib/cn'
|
|
8
|
+
|
|
9
|
+
/** One aggregate rating rendered as a tile by {@link RatingsSummary}. */
|
|
10
|
+
export interface RatingSource {
|
|
11
|
+
/** Platform / source name, e.g. `"Google"`. */
|
|
12
|
+
name: string
|
|
13
|
+
/** Headline score, e.g. `4.9` or `"A+"`. */
|
|
14
|
+
score: string | number
|
|
15
|
+
/** Denominator, e.g. `5`. Shown as `score/outOf` when provided and `score` is numeric. */
|
|
16
|
+
outOf?: string | number
|
|
17
|
+
/** Number of reviews/feedback backing the score. */
|
|
18
|
+
count?: number
|
|
19
|
+
/** Optional logo/icon node; falls back to `name` when omitted. */
|
|
20
|
+
logo?: JSX.Element
|
|
21
|
+
/** When set, the tile renders as a link to the source. */
|
|
22
|
+
href?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RatingsSummaryProps {
|
|
26
|
+
/** Rating sources to render, in order, as a responsive grid. */
|
|
27
|
+
sources: RatingSource[]
|
|
28
|
+
/** Columns on desktop. Defaults to a responsive 4-column grid. */
|
|
29
|
+
columns?: number
|
|
30
|
+
class?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DESKTOP_COLS: Record<number, string> = {
|
|
34
|
+
2: 'md:grid-cols-2',
|
|
35
|
+
3: 'md:grid-cols-3',
|
|
36
|
+
4: 'md:grid-cols-4',
|
|
37
|
+
5: 'md:grid-cols-5',
|
|
38
|
+
6: 'md:grid-cols-6',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compact social-proof grid of aggregate ratings across multiple sources —
|
|
43
|
+
* a trust band for landing pages and checkout flows. Each source renders as
|
|
44
|
+
* a small glass-neutral tile with its logo/name, headline score (optionally
|
|
45
|
+
* `score/outOf`), a review count, and — when the score is numeric out of 5 —
|
|
46
|
+
* a row of filled stars. Tiles with an `href` render as links that open in
|
|
47
|
+
* a new tab.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* <RatingsSummary
|
|
52
|
+
* sources={[
|
|
53
|
+
* { name: 'Google', score: 4.8, outOf: 5, count: 2140, href: 'https://example.com/reviews' },
|
|
54
|
+
* { name: 'Trustpilot', score: 4.6, outOf: 5, count: 980 },
|
|
55
|
+
* { name: 'Verified Buyers', score: 4.9, outOf: 5, count: 512 },
|
|
56
|
+
* { name: 'App Store', score: 'A+', count: 310 },
|
|
57
|
+
* ]}
|
|
58
|
+
* />
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function RatingsSummary(props: RatingsSummaryProps): JSX.Element {
|
|
62
|
+
const desktopCols = () => (props.columns !== undefined ? DESKTOP_COLS[props.columns] : undefined)
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div class={cn('grid grid-cols-2 gap-3', desktopCols() ?? 'md:grid-cols-4', props.class)}>
|
|
66
|
+
<For each={props.sources}>
|
|
67
|
+
{(source) => {
|
|
68
|
+
const numericScore = () => (typeof source.score === 'number' ? source.score : undefined)
|
|
69
|
+
const tile = (
|
|
70
|
+
<>
|
|
71
|
+
<Show
|
|
72
|
+
when={source.logo}
|
|
73
|
+
fallback={<p class="text-sm font-medium text-muted-foreground">{source.name}</p>}
|
|
74
|
+
>
|
|
75
|
+
{(logo) => <div class="flex items-center justify-center text-muted-foreground">{logo()}</div>}
|
|
76
|
+
</Show>
|
|
77
|
+
<p class="mt-1 text-2xl font-bold text-foreground">
|
|
78
|
+
<Show
|
|
79
|
+
when={numericScore() !== undefined && source.outOf !== undefined}
|
|
80
|
+
fallback={source.score}
|
|
81
|
+
>
|
|
82
|
+
{source.score}/{source.outOf}
|
|
83
|
+
</Show>
|
|
84
|
+
</p>
|
|
85
|
+
<Show when={numericScore() !== undefined && (numericScore() as number) <= 5}>
|
|
86
|
+
<div class="mt-1 flex items-center justify-center gap-0.5" aria-hidden="true">
|
|
87
|
+
<For each={[0, 1, 2, 3, 4]}>
|
|
88
|
+
{(i) => (
|
|
89
|
+
<Star
|
|
90
|
+
class={cn(
|
|
91
|
+
'h-3.5 w-3.5',
|
|
92
|
+
i < Math.round(numericScore() as number)
|
|
93
|
+
? 'fill-primary text-primary'
|
|
94
|
+
: 'fill-muted text-muted',
|
|
95
|
+
)}
|
|
96
|
+
/>
|
|
97
|
+
)}
|
|
98
|
+
</For>
|
|
99
|
+
</div>
|
|
100
|
+
</Show>
|
|
101
|
+
<Show when={source.count !== undefined}>
|
|
102
|
+
<p class="mt-1 text-xs text-muted-foreground">{source.count?.toLocaleString()} reviews</p>
|
|
103
|
+
</Show>
|
|
104
|
+
</>
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Show
|
|
109
|
+
when={source.href}
|
|
110
|
+
fallback={<div class="rounded-xl border border-border bg-card/60 p-4 text-center">{tile}</div>}
|
|
111
|
+
>
|
|
112
|
+
{(href) => (
|
|
113
|
+
<a
|
|
114
|
+
href={href()}
|
|
115
|
+
target="_blank"
|
|
116
|
+
rel="noreferrer"
|
|
117
|
+
class="rounded-xl border border-border bg-card/60 p-4 text-center transition hover:border-primary/50"
|
|
118
|
+
>
|
|
119
|
+
{tile}
|
|
120
|
+
</a>
|
|
121
|
+
)}
|
|
122
|
+
</Show>
|
|
123
|
+
)
|
|
124
|
+
}}
|
|
125
|
+
</For>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js'
|
|
2
|
+
import { For, Show } from 'solid-js'
|
|
3
|
+
import { cn } from '../lib/cn'
|
|
4
|
+
|
|
5
|
+
export interface SpecRow {
|
|
6
|
+
label: string
|
|
7
|
+
value: JSX.Element | string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SpecGroup {
|
|
11
|
+
title?: string
|
|
12
|
+
rows: SpecRow[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SpecSheetProps {
|
|
16
|
+
groups: SpecGroup[]
|
|
17
|
+
/** 1 or 2 columns of rows per group on wider screens. Defaults to 1. */
|
|
18
|
+
columns?: 1 | 2
|
|
19
|
+
class?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A grouped key/value specification table, for product specs, tech sheets,
|
|
24
|
+
* or any other structured attribute list. Rows are label/value pairs with a
|
|
25
|
+
* subtle divider; groups can carry an optional uppercase title and rows can
|
|
26
|
+
* flow in one or two columns on wider screens. Designed to sit inside a
|
|
27
|
+
* `Card` with a clean, glassy-neutral look.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <SpecSheet
|
|
32
|
+
* columns={2}
|
|
33
|
+
* groups={[
|
|
34
|
+
* {
|
|
35
|
+
* title: 'Display',
|
|
36
|
+
* rows: [
|
|
37
|
+
* { label: 'Panel', value: 'AMOLED, 6.4"' },
|
|
38
|
+
* { label: 'Resolution', value: '2340 x 1080' },
|
|
39
|
+
* { label: 'Refresh rate', value: '120 Hz' },
|
|
40
|
+
* ],
|
|
41
|
+
* },
|
|
42
|
+
* {
|
|
43
|
+
* title: 'Battery',
|
|
44
|
+
* rows: [
|
|
45
|
+
* { label: 'Capacity', value: '4500 mAh' },
|
|
46
|
+
* { label: 'Charging', value: '65 W wired' },
|
|
47
|
+
* ],
|
|
48
|
+
* },
|
|
49
|
+
* ]}
|
|
50
|
+
* />
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function SpecSheet(props: SpecSheetProps): JSX.Element {
|
|
54
|
+
return (
|
|
55
|
+
<div class={cn('flex flex-col gap-6', props.class)}>
|
|
56
|
+
<For each={props.groups}>
|
|
57
|
+
{(group) => (
|
|
58
|
+
<div class="flex flex-col gap-2">
|
|
59
|
+
<Show when={group.title}>
|
|
60
|
+
<div class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
61
|
+
{group.title}
|
|
62
|
+
</div>
|
|
63
|
+
</Show>
|
|
64
|
+
<dl
|
|
65
|
+
class={cn(
|
|
66
|
+
'divide-y divide-border/60',
|
|
67
|
+
props.columns === 2 && 'grid grid-cols-1 gap-x-8 sm:grid-cols-2 sm:divide-y-0',
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
<For each={group.rows}>
|
|
71
|
+
{(row) => (
|
|
72
|
+
<div
|
|
73
|
+
class={cn(
|
|
74
|
+
'flex items-baseline justify-between gap-4 py-2 text-sm',
|
|
75
|
+
props.columns === 2 && 'border-b border-border/60 sm:border-b',
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
<dt class="text-muted-foreground">{row.label}</dt>
|
|
79
|
+
<dd class="text-right font-medium text-foreground">{row.value}</dd>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</For>
|
|
83
|
+
</dl>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</For>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|