@twentyfirstmedia/ui-core 0.2.1 → 0.3.1
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/components/admin.d.ts.map +1 -1
- package/dist/components/admin.js +95 -71
- package/dist/components/admin.js.map +1 -1
- package/dist/components/agents.d.ts.map +1 -1
- package/dist/components/agents.js +49 -35
- package/dist/components/agents.js.map +1 -1
- package/dist/components/app-chrome.d.ts.map +1 -1
- package/dist/components/app-chrome.js +6 -2
- package/dist/components/app-chrome.js.map +1 -1
- package/dist/components/approvals.d.ts.map +1 -1
- package/dist/components/approvals.js +22 -18
- package/dist/components/approvals.js.map +1 -1
- package/dist/components/building-blocks.d.ts.map +1 -1
- package/dist/components/building-blocks.js +6 -2
- package/dist/components/building-blocks.js.map +1 -1
- package/dist/components/charts.d.ts.map +1 -1
- package/dist/components/charts.js +20 -10
- package/dist/components/charts.js.map +1 -1
- package/dist/components/chat.d.ts.map +1 -1
- package/dist/components/chat.js +31 -19
- package/dist/components/chat.js.map +1 -1
- package/dist/components/commerce.d.ts +2 -2
- package/dist/components/commerce.d.ts.map +1 -1
- package/dist/components/commerce.js +45 -31
- package/dist/components/commerce.js.map +1 -1
- package/dist/components/data-display.d.ts +5 -9
- package/dist/components/data-display.d.ts.map +1 -1
- package/dist/components/data-display.js +34 -35
- package/dist/components/data-display.js.map +1 -1
- package/dist/components/data-table.d.ts.map +1 -1
- package/dist/components/data-table.js +15 -10
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/developer.d.ts.map +1 -1
- package/dist/components/developer.js +20 -11
- package/dist/components/developer.js.map +1 -1
- package/dist/components/feedback.d.ts.map +1 -1
- package/dist/components/feedback.js +12 -6
- package/dist/components/feedback.js.map +1 -1
- package/dist/components/form.d.ts +2 -1
- package/dist/components/form.d.ts.map +1 -1
- package/dist/components/form.js +12 -9
- package/dist/components/form.js.map +1 -1
- package/dist/components/forms.d.ts +2 -1
- package/dist/components/forms.d.ts.map +1 -1
- package/dist/components/forms.js +92 -42
- package/dist/components/forms.js.map +1 -1
- package/dist/components/foundation.d.ts +5 -2
- package/dist/components/foundation.d.ts.map +1 -1
- package/dist/components/foundation.js +15 -12
- package/dist/components/foundation.js.map +1 -1
- package/dist/components/inputs.d.ts.map +1 -1
- package/dist/components/inputs.js +3 -1
- package/dist/components/inputs.js.map +1 -1
- package/dist/components/mail.d.ts.map +1 -1
- package/dist/components/mail.js +15 -10
- package/dist/components/mail.js.map +1 -1
- package/dist/components/media.d.ts.map +1 -1
- package/dist/components/media.js +39 -18
- package/dist/components/media.js.map +1 -1
- package/dist/components/navigation.d.ts.map +1 -1
- package/dist/components/navigation.js +21 -9
- package/dist/components/navigation.js.map +1 -1
- package/dist/components/operations.d.ts.map +1 -1
- package/dist/components/operations.js +37 -27
- package/dist/components/operations.js.map +1 -1
- package/dist/components/previews.d.ts.map +1 -1
- package/dist/components/previews.js +14 -10
- package/dist/components/previews.js.map +1 -1
- package/dist/components/primitives.d.ts.map +1 -1
- package/dist/components/primitives.js +4 -1
- package/dist/components/primitives.js.map +1 -1
- package/dist/components/real-estate.d.ts.map +1 -1
- package/dist/components/real-estate.js +43 -28
- package/dist/components/real-estate.js.map +1 -1
- package/dist/components/rich-text-editor.d.ts.map +1 -1
- package/dist/components/rich-text-editor.js +18 -15
- package/dist/components/rich-text-editor.js.map +1 -1
- package/dist/components/select-menu.d.ts.map +1 -1
- package/dist/components/select-menu.js +4 -1
- package/dist/components/select-menu.js.map +1 -1
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +8 -3
- package/dist/components/shell.js.map +1 -1
- package/dist/components/site.d.ts.map +1 -1
- package/dist/components/site.js +17 -13
- package/dist/components/site.js.map +1 -1
- package/dist/components/types.d.ts +102 -3
- package/dist/components/types.d.ts.map +1 -1
- package/dist/components/workspace.d.ts.map +1 -1
- package/dist/components/workspace.js +50 -34
- package/dist/components/workspace.js.map +1 -1
- package/dist/context.d.ts +39 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +76 -0
- package/dist/context.js.map +1 -0
- package/dist/format.d.ts +33 -6
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +75 -10
- package/dist/format.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +18 -11
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +11 -18
- package/dist/provider.js.map +1 -1
- package/dist/strings.d.ts +503 -0
- package/dist/strings.d.ts.map +1 -0
- package/dist/strings.js +537 -0
- package/dist/strings.js.map +1 -0
- package/dist/themes.css +4 -0
- package/package.json +1 -1
- package/src/components/admin.tsx +117 -93
- package/src/components/agents.tsx +55 -41
- package/src/components/app-chrome.tsx +6 -2
- package/src/components/approvals.tsx +26 -22
- package/src/components/building-blocks.tsx +6 -2
- package/src/components/charts.tsx +21 -11
- package/src/components/chat.tsx +45 -35
- package/src/components/commerce.tsx +61 -53
- package/src/components/data-display.tsx +38 -45
- package/src/components/data-table.tsx +20 -15
- package/src/components/developer.tsx +26 -15
- package/src/components/feedback.tsx +19 -7
- package/src/components/form.tsx +17 -9
- package/src/components/forms.tsx +131 -53
- package/src/components/foundation.tsx +23 -36
- package/src/components/inputs.tsx +4 -2
- package/src/components/mail.tsx +16 -11
- package/src/components/media.tsx +64 -21
- package/src/components/navigation.tsx +25 -13
- package/src/components/operations.tsx +40 -32
- package/src/components/previews.tsx +17 -15
- package/src/components/primitives.tsx +8 -5
- package/src/components/real-estate.tsx +55 -35
- package/src/components/rich-text-editor.tsx +20 -17
- package/src/components/select-menu.tsx +5 -2
- package/src/components/shell.tsx +8 -3
- package/src/components/site.tsx +18 -14
- package/src/components/types.ts +111 -3
- package/src/components/workspace.tsx +79 -60
- package/src/context.ts +112 -0
- package/src/format.ts +108 -15
- package/src/index.ts +43 -0
- package/src/provider.tsx +36 -40
- package/src/strings.ts +1500 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { useStrings } from "../context.js";
|
|
4
|
+
import { PREVIEWS_STRINGS } from "../strings.js";
|
|
3
5
|
import { useControlled } from "../hooks.js";
|
|
4
6
|
import { cx } from "../cx.js";
|
|
5
7
|
import { Avatar } from "./foundation.js";
|
|
@@ -10,6 +12,7 @@ import type { EmailPreviewProps, SearchAdPreviewProps } from "./types.js";
|
|
|
10
12
|
|
|
11
13
|
/* ── Email and ad previews ── */
|
|
12
14
|
export function EmailPreview(p: EmailPreviewProps) {
|
|
15
|
+
const t = useStrings(PREVIEWS_STRINGS);
|
|
13
16
|
const dev = useControlled<string>(p.device, "desktop", p.onDevice);
|
|
14
17
|
return (
|
|
15
18
|
<div className="tf-mailprev">
|
|
@@ -19,7 +22,7 @@ export function EmailPreview(p: EmailPreviewProps) {
|
|
|
19
22
|
<div className="tf-option-text">
|
|
20
23
|
<b>{p.fromName || p.from}</b>
|
|
21
24
|
<span className="tf-mailprev-subj">
|
|
22
|
-
<b>{p.subject ||
|
|
25
|
+
<b>{p.subject || t.noSubject}</b>
|
|
23
26
|
{p.preheader && <span>{" — " + p.preheader}</span>}
|
|
24
27
|
</span>
|
|
25
28
|
</div>
|
|
@@ -28,15 +31,15 @@ export function EmailPreview(p: EmailPreviewProps) {
|
|
|
28
31
|
size="sm"
|
|
29
32
|
value={dev[0]}
|
|
30
33
|
onChange={dev[1]}
|
|
31
|
-
label=
|
|
34
|
+
label={t.device}
|
|
32
35
|
options={[
|
|
33
36
|
{
|
|
34
37
|
value: "desktop",
|
|
35
|
-
label:
|
|
38
|
+
label: t.desktop,
|
|
36
39
|
},
|
|
37
40
|
{
|
|
38
41
|
value: "mobile",
|
|
39
|
-
label:
|
|
42
|
+
label: t.mobile,
|
|
40
43
|
},
|
|
41
44
|
]}
|
|
42
45
|
/>
|
|
@@ -44,7 +47,7 @@ export function EmailPreview(p: EmailPreviewProps) {
|
|
|
44
47
|
<div className={cx("tf-mailprev-stage", dev[0])}>
|
|
45
48
|
<div className="tf-mailprev-frame">
|
|
46
49
|
{p.html != null ? (
|
|
47
|
-
<iframe title=
|
|
50
|
+
<iframe title={t.emailPreview} sandbox="" srcDoc={p.html} />
|
|
48
51
|
) : (
|
|
49
52
|
<div className="tf-mailprev-body">{p.children}</div>
|
|
50
53
|
)}
|
|
@@ -62,6 +65,7 @@ export function EmailPreview(p: EmailPreviewProps) {
|
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
export function SearchAdPreview(p: SearchAdPreviewProps) {
|
|
68
|
+
const t = useStrings(PREVIEWS_STRINGS);
|
|
65
69
|
const dev = useControlled<string>(p.device, "mobile", p.onDevice),
|
|
66
70
|
hl = (p.headlines || []).filter(Boolean).slice(0, 3),
|
|
67
71
|
ds = (p.descriptions || []).filter(Boolean).slice(0, 2);
|
|
@@ -75,9 +79,7 @@ export function SearchAdPreview(p: SearchAdPreviewProps) {
|
|
|
75
79
|
(p[x[0]] || []).forEach((s, i) => {
|
|
76
80
|
if (s && s.length > x[1])
|
|
77
81
|
warn.push(
|
|
78
|
-
|
|
79
|
-
x[0] === "headlines" ? "Headline" : x[0] === "descriptions" ? "Description" : "Path"
|
|
80
|
-
} ${i + 1} is ${s.length} characters, the limit is ${x[1]}`,
|
|
82
|
+
t.adTooLong(x[0] as "headlines" | "descriptions" | "paths", i + 1, s.length, x[1]),
|
|
81
83
|
);
|
|
82
84
|
});
|
|
83
85
|
});
|
|
@@ -101,21 +103,21 @@ export function SearchAdPreview(p: SearchAdPreviewProps) {
|
|
|
101
103
|
padding: 0,
|
|
102
104
|
}}
|
|
103
105
|
>
|
|
104
|
-
{
|
|
106
|
+
{t.preview}
|
|
105
107
|
</span>
|
|
106
108
|
<Segmented
|
|
107
109
|
size="sm"
|
|
108
110
|
value={dev[0]}
|
|
109
111
|
onChange={dev[1]}
|
|
110
|
-
label=
|
|
112
|
+
label={t.device}
|
|
111
113
|
options={[
|
|
112
114
|
{
|
|
113
115
|
value: "mobile",
|
|
114
|
-
label:
|
|
116
|
+
label: t.mobile,
|
|
115
117
|
},
|
|
116
118
|
{
|
|
117
119
|
value: "desktop",
|
|
118
|
-
label:
|
|
120
|
+
label: t.desktop,
|
|
119
121
|
},
|
|
120
122
|
]}
|
|
121
123
|
/>
|
|
@@ -129,7 +131,7 @@ export function SearchAdPreview(p: SearchAdPreviewProps) {
|
|
|
129
131
|
<b>{p.name || p.domain}</b>
|
|
130
132
|
<small>{url}</small>
|
|
131
133
|
</div>
|
|
132
|
-
<span className="tf-adprev-tag">{p.sponsoredLabel ||
|
|
134
|
+
<span className="tf-adprev-tag">{p.sponsoredLabel || t.sponsored}</span>
|
|
133
135
|
</div>
|
|
134
136
|
<a
|
|
135
137
|
className="tf-adprev-hl"
|
|
@@ -138,9 +140,9 @@ export function SearchAdPreview(p: SearchAdPreviewProps) {
|
|
|
138
140
|
e.preventDefault();
|
|
139
141
|
}}
|
|
140
142
|
>
|
|
141
|
-
{hl.join(" | ") ||
|
|
143
|
+
{hl.join(" | ") || t.yourHeadline}
|
|
142
144
|
</a>
|
|
143
|
-
<p className="tf-adprev-desc">{ds.join(" ") ||
|
|
145
|
+
<p className="tf-adprev-desc">{ds.join(" ") || t.yourDescription}</p>
|
|
144
146
|
{p.sitelinks && p.sitelinks.length > 0 && (
|
|
145
147
|
<div className="tf-adprev-links">
|
|
146
148
|
{p.sitelinks.map((s, i) => (
|
|
@@ -8,6 +8,8 @@ import * as React from "react";
|
|
|
8
8
|
import { leaving, usePresence } from "../presence.js";
|
|
9
9
|
import { Icon } from "../Icon.js";
|
|
10
10
|
import { cx } from "../cx.js";
|
|
11
|
+
import { useStrings } from "../context.js";
|
|
12
|
+
import { PRIMITIVES_STRINGS } from "../strings.js";
|
|
11
13
|
import { moveFocus, useControlled, useId, useKeepInView } from "../hooks.js";
|
|
12
14
|
import type {
|
|
13
15
|
AccordionProps,
|
|
@@ -273,6 +275,7 @@ export function HoverCard(p: HoverCardProps) {
|
|
|
273
275
|
|
|
274
276
|
/* ── Carousel ── */
|
|
275
277
|
export function Carousel(p: CarouselProps) {
|
|
278
|
+
const t = useStrings(PRIMITIVES_STRINGS);
|
|
276
279
|
const slides = p.slides || [];
|
|
277
280
|
const n = slides.length;
|
|
278
281
|
const idx = useControlled(p.index, p.defaultIndex || 0, p.onChange);
|
|
@@ -332,8 +335,8 @@ export function Carousel(p: CarouselProps) {
|
|
|
332
335
|
key={i}
|
|
333
336
|
className="tf-carousel-slide"
|
|
334
337
|
role="group"
|
|
335
|
-
aria-roledescription=
|
|
336
|
-
aria-label={
|
|
338
|
+
aria-roledescription={t.slide}
|
|
339
|
+
aria-label={t.slideOf(i + 1, n)}
|
|
337
340
|
>
|
|
338
341
|
{s}
|
|
339
342
|
</div>
|
|
@@ -343,7 +346,7 @@ export function Carousel(p: CarouselProps) {
|
|
|
343
346
|
<button
|
|
344
347
|
type="button"
|
|
345
348
|
className="tf-iconbtn tf-iconbtn-soft tf-iconbtn-sm"
|
|
346
|
-
aria-label=
|
|
349
|
+
aria-label={t.previousSlide}
|
|
347
350
|
disabled={!p.loop && idx[0] <= 0}
|
|
348
351
|
onClick={() => go(idx[0] - 1)}
|
|
349
352
|
>
|
|
@@ -356,7 +359,7 @@ export function Carousel(p: CarouselProps) {
|
|
|
356
359
|
key={i}
|
|
357
360
|
type="button"
|
|
358
361
|
className="tf-carousel-dot"
|
|
359
|
-
aria-label={
|
|
362
|
+
aria-label={t.goToSlide(i + 1)}
|
|
360
363
|
aria-current={i === idx[0] ? "true" : undefined}
|
|
361
364
|
onClick={() => go(i)}
|
|
362
365
|
/>
|
|
@@ -366,7 +369,7 @@ export function Carousel(p: CarouselProps) {
|
|
|
366
369
|
<button
|
|
367
370
|
type="button"
|
|
368
371
|
className="tf-iconbtn tf-iconbtn-soft tf-iconbtn-sm"
|
|
369
|
-
aria-label=
|
|
372
|
+
aria-label={t.nextSlide}
|
|
370
373
|
disabled={!p.loop && idx[0] >= last}
|
|
371
374
|
onClick={() => go(idx[0] + 1)}
|
|
372
375
|
>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { Icon } from "../Icon.js";
|
|
4
|
-
import {
|
|
4
|
+
import { useFormat, useStrings, type Formatters } from "../context.js";
|
|
5
|
+
import { REAL_ESTATE_STRINGS, type UIStrings } from "../strings.js";
|
|
5
6
|
import { cx } from "../cx.js";
|
|
6
7
|
import { Switch, cbox } from "./inputs.js";
|
|
7
8
|
import { Badge, Button, IconButton, TintIcon } from "./foundation.js";
|
|
@@ -20,27 +21,35 @@ import type {
|
|
|
20
21
|
} from "./types.js";
|
|
21
22
|
|
|
22
23
|
/* ── Real estate ── */
|
|
23
|
-
|
|
24
|
+
type ListingStrings = Pick<UIStrings, "rooms" | "priceOnRequest" | "perMonth">;
|
|
25
|
+
|
|
26
|
+
function listingFacts(x: Listing, t: ListingStrings, { formatNumber }: Formatters) {
|
|
24
27
|
return [
|
|
25
28
|
x.area != null && [<Icon key="i" name="ruler" size={14} />, `${formatNumber(x.area)} m²`],
|
|
26
|
-
x.rooms != null && [
|
|
29
|
+
x.rooms != null && [
|
|
30
|
+
<Icon key="i" name="bed" size={14} />,
|
|
31
|
+
t.rooms(formatNumber(x.rooms), x.rooms),
|
|
32
|
+
],
|
|
27
33
|
x.place && [<Icon key="i" name="pin" size={14} />, x.place],
|
|
28
34
|
]
|
|
29
35
|
.filter(Boolean)
|
|
30
36
|
.map((f, i) => <span key={i}>{f}</span>);
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
function listingPrice(x: Listing) {
|
|
34
|
-
if (x.price == null) return <b className="tf-listing-price">{
|
|
39
|
+
function listingPrice(x: Listing, t: ListingStrings, { formatMoney }: Formatters) {
|
|
40
|
+
if (x.price == null) return <b className="tf-listing-price">{t.priceOnRequest}</b>;
|
|
35
41
|
return (
|
|
36
42
|
<b className="tf-listing-price">
|
|
37
43
|
{formatMoney(x.price, x.currency)}
|
|
38
|
-
{x.rent && <small> {
|
|
44
|
+
{x.rent && <small> {t.perMonth}</small>}
|
|
39
45
|
</b>
|
|
40
46
|
);
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
export function ListingCard(p: ListingCardProps) {
|
|
50
|
+
const t = useStrings(REAL_ESTATE_STRINGS);
|
|
51
|
+
const fm = useFormat();
|
|
52
|
+
const { formatMoney, formatNumber } = fm;
|
|
44
53
|
const x = p.listing || p;
|
|
45
54
|
return (
|
|
46
55
|
<article
|
|
@@ -65,7 +74,7 @@ export function ListingCard(p: ListingCardProps) {
|
|
|
65
74
|
<Icon name="image" size={24} />
|
|
66
75
|
</div>
|
|
67
76
|
)}
|
|
68
|
-
{x.isNew && <span className="tf-listing-new">{
|
|
77
|
+
{x.isNew && <span className="tf-listing-new">{t.isNew}</span>}
|
|
69
78
|
{(x.photos ?? 0) > 1 && (
|
|
70
79
|
<span className="tf-listing-photos">
|
|
71
80
|
<Icon name="image" size={12} />
|
|
@@ -84,14 +93,14 @@ export function ListingCard(p: ListingCardProps) {
|
|
|
84
93
|
onChange: () => {
|
|
85
94
|
p.onSelect?.(x);
|
|
86
95
|
},
|
|
87
|
-
label:
|
|
96
|
+
label: t.select,
|
|
88
97
|
})}
|
|
89
98
|
</label>
|
|
90
99
|
)}
|
|
91
100
|
</div>
|
|
92
101
|
<div className="tf-listing-body">
|
|
93
102
|
<div className="tf-listing-top">
|
|
94
|
-
{listingPrice(x)}
|
|
103
|
+
{listingPrice(x, t, fm)}
|
|
95
104
|
{x.pricePerSqm != null && (
|
|
96
105
|
<span className="tf-listing-ppsqm">
|
|
97
106
|
{formatMoney(x.pricePerSqm, x.currency) + " / m²"}
|
|
@@ -101,7 +110,7 @@ export function ListingCard(p: ListingCardProps) {
|
|
|
101
110
|
<IconButton
|
|
102
111
|
icon="heart"
|
|
103
112
|
size="sm"
|
|
104
|
-
label={x.wished ?
|
|
113
|
+
label={x.wished ? t.removeFromSaved : t.save}
|
|
105
114
|
className={cx("tf-listing-wish", x.wished && "on")}
|
|
106
115
|
onClick={(e) => {
|
|
107
116
|
e.stopPropagation();
|
|
@@ -111,16 +120,16 @@ export function ListingCard(p: ListingCardProps) {
|
|
|
111
120
|
)}
|
|
112
121
|
</div>
|
|
113
122
|
<b className="tf-listing-title">{x.title}</b>
|
|
114
|
-
<div className="tf-listing-facts">{listingFacts(x)}</div>
|
|
123
|
+
<div className="tf-listing-facts">{listingFacts(x, t, fm)}</div>
|
|
115
124
|
<div className="tf-listing-foot">
|
|
116
125
|
{x.source && <span className="tf-listing-src">{x.source}</span>}
|
|
117
126
|
{x.seller && (
|
|
118
127
|
<Badge tone={x.seller === "private" ? "success" : "neutral"} dot={false}>
|
|
119
|
-
{x.sellerLabel || (x.seller === "private" ?
|
|
128
|
+
{x.sellerLabel || (x.seller === "private" ? t.privateSeller : t.agent)}
|
|
120
129
|
</Badge>
|
|
121
130
|
)}
|
|
122
131
|
{x.phone && (
|
|
123
|
-
<span className="tf-listing-flag" title=
|
|
132
|
+
<span className="tf-listing-flag" title={t.phoneAvailable}>
|
|
124
133
|
<Icon name="phone" size={13} />
|
|
125
134
|
</span>
|
|
126
135
|
)}
|
|
@@ -143,7 +152,7 @@ export function ListingCard(p: ListingCardProps) {
|
|
|
143
152
|
onChange={(s) => {
|
|
144
153
|
p.onStatus && p.onStatus(x, s);
|
|
145
154
|
}}
|
|
146
|
-
label=
|
|
155
|
+
label={t.status}
|
|
147
156
|
/>
|
|
148
157
|
</span>
|
|
149
158
|
)}
|
|
@@ -154,6 +163,8 @@ export function ListingCard(p: ListingCardProps) {
|
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
export function ResultsToolbar(p: ResultsToolbarProps) {
|
|
166
|
+
const t = useStrings(REAL_ESTATE_STRINGS);
|
|
167
|
+
const { formatNumber } = useFormat();
|
|
157
168
|
const all = p.total > 0 && p.selected === p.total,
|
|
158
169
|
some = (p.selected ?? 0) > 0 && !all;
|
|
159
170
|
return (
|
|
@@ -166,7 +177,7 @@ export function ResultsToolbar(p: ResultsToolbarProps) {
|
|
|
166
177
|
onChange: () => {
|
|
167
178
|
p.onSelectAll?.(!all);
|
|
168
179
|
},
|
|
169
|
-
label: all ?
|
|
180
|
+
label: all ? t.deselectAll : t.selectAll,
|
|
170
181
|
})}
|
|
171
182
|
</label>
|
|
172
183
|
)}
|
|
@@ -178,10 +189,12 @@ export function ResultsToolbar(p: ResultsToolbarProps) {
|
|
|
178
189
|
>
|
|
179
190
|
<b>
|
|
180
191
|
{(p.selected ?? 0) > 0
|
|
181
|
-
?
|
|
182
|
-
: p.title ||
|
|
192
|
+
? t.selectedCount(p.selected ?? 0, formatNumber(p.selected ?? 0))
|
|
193
|
+
: p.title || t.results}
|
|
183
194
|
</b>
|
|
184
|
-
<small>
|
|
195
|
+
<small>
|
|
196
|
+
{t.foundCount(p.total || 0, formatNumber(p.total || 0)) + (p.meta ? " · " + p.meta : "")}
|
|
197
|
+
</small>
|
|
185
198
|
</div>
|
|
186
199
|
{(p.selected ?? 0) > 0 &&
|
|
187
200
|
(p.bulkActions || []).map((a, i) => (
|
|
@@ -193,12 +206,12 @@ export function ResultsToolbar(p: ResultsToolbarProps) {
|
|
|
193
206
|
disabled={a.busy}
|
|
194
207
|
onClick={a.onClick}
|
|
195
208
|
>
|
|
196
|
-
{a.busy ? a.busyLabel ||
|
|
209
|
+
{a.busy ? a.busyLabel || t.workingEllipsis : a.label}
|
|
197
210
|
</Button>
|
|
198
211
|
))}
|
|
199
212
|
{p.sortOptions && (
|
|
200
213
|
<label className="tf-results-sort">
|
|
201
|
-
<span>{
|
|
214
|
+
<span>{t.sort}</span>
|
|
202
215
|
<select
|
|
203
216
|
className="tf-input"
|
|
204
217
|
value={p.sort}
|
|
@@ -219,11 +232,11 @@ export function ResultsToolbar(p: ResultsToolbarProps) {
|
|
|
219
232
|
size="sm"
|
|
220
233
|
value={p.view}
|
|
221
234
|
onChange={p.onView}
|
|
222
|
-
label=
|
|
235
|
+
label={t.view}
|
|
223
236
|
options={p.views.map((v) => ({
|
|
224
237
|
value: v,
|
|
225
238
|
icon: v === "grid" ? "grid" : v === "map" ? "map" : "list",
|
|
226
|
-
label: v === "grid" ?
|
|
239
|
+
label: v === "grid" ? t.viewGrid : v === "map" ? t.viewMap : t.viewList,
|
|
227
240
|
}))}
|
|
228
241
|
/>
|
|
229
242
|
)}
|
|
@@ -232,6 +245,8 @@ export function ResultsToolbar(p: ResultsToolbarProps) {
|
|
|
232
245
|
}
|
|
233
246
|
|
|
234
247
|
export function SavedSearch(p: SavedSearchProps) {
|
|
248
|
+
const t = useStrings(REAL_ESTATE_STRINGS);
|
|
249
|
+
const { formatNumber } = useFormat();
|
|
235
250
|
return (
|
|
236
251
|
<div className={cx("tf-saved", p.enabled === false && "off")}>
|
|
237
252
|
<div className="tf-saved-head">
|
|
@@ -245,14 +260,14 @@ export function SavedSearch(p: SavedSearchProps) {
|
|
|
245
260
|
>
|
|
246
261
|
<b>{p.name}</b>
|
|
247
262
|
<small>
|
|
248
|
-
{[p.lastRun &&
|
|
263
|
+
{[p.lastRun && t.checkedAt(p.lastRun), p.frequency].filter(Boolean).join(" · ")}
|
|
249
264
|
</small>
|
|
250
265
|
</div>
|
|
251
266
|
{(p.newCount ?? 0) > 0 && (
|
|
252
267
|
<span className="tf-nav-count">{formatNumber(p.newCount ?? 0)}</span>
|
|
253
268
|
)}
|
|
254
269
|
{p.onToggle && (
|
|
255
|
-
<Switch checked={p.enabled !== false} onChange={p.onToggle} label=
|
|
270
|
+
<Switch checked={p.enabled !== false} onChange={p.onToggle} label={t.enabled} />
|
|
256
271
|
)}
|
|
257
272
|
</div>
|
|
258
273
|
<div className="tf-saved-crit">
|
|
@@ -271,12 +286,12 @@ export function SavedSearch(p: SavedSearchProps) {
|
|
|
271
286
|
>
|
|
272
287
|
{p.onEdit && (
|
|
273
288
|
<Button size="sm" variant="ghost" icon="edit" onClick={p.onEdit}>
|
|
274
|
-
{
|
|
289
|
+
{t.edit}
|
|
275
290
|
</Button>
|
|
276
291
|
)}
|
|
277
292
|
{p.onOpen && (
|
|
278
293
|
<Button size="sm" variant="soft" onClick={p.onOpen}>
|
|
279
|
-
{p.openLabel ||
|
|
294
|
+
{p.openLabel || t.showResults}
|
|
280
295
|
</Button>
|
|
281
296
|
)}
|
|
282
297
|
</div>
|
|
@@ -286,6 +301,8 @@ export function SavedSearch(p: SavedSearchProps) {
|
|
|
286
301
|
}
|
|
287
302
|
|
|
288
303
|
export function ValuationCard(p: ValuationCardProps) {
|
|
304
|
+
const t = useStrings(REAL_ESTATE_STRINGS);
|
|
305
|
+
const { formatMoney, formatNumber } = useFormat();
|
|
289
306
|
const lo = p.low,
|
|
290
307
|
hi = p.high,
|
|
291
308
|
v = p.value,
|
|
@@ -296,10 +313,10 @@ export function ValuationCard(p: ValuationCardProps) {
|
|
|
296
313
|
conf == null
|
|
297
314
|
? null
|
|
298
315
|
: conf >= 0.75
|
|
299
|
-
? ["ok",
|
|
316
|
+
? ["ok", t.highConfidence]
|
|
300
317
|
: conf >= 0.5
|
|
301
|
-
? ["warn",
|
|
302
|
-
: ["danger",
|
|
318
|
+
? ["warn", t.mediumConfidence]
|
|
319
|
+
: ["danger", t.lowConfidence];
|
|
303
320
|
return (
|
|
304
321
|
<div className="tf-valuation">
|
|
305
322
|
<div className="tf-valuation-head">
|
|
@@ -309,7 +326,7 @@ export function ValuationCard(p: ValuationCardProps) {
|
|
|
309
326
|
padding: 0,
|
|
310
327
|
}}
|
|
311
328
|
>
|
|
312
|
-
{p.title ||
|
|
329
|
+
{p.title || t.estimatedValue}
|
|
313
330
|
</span>
|
|
314
331
|
{cl && <StatusIndicator status={cl[0]} label={p.confidenceLabel || cl[1]} size="sm" />}
|
|
315
332
|
</div>
|
|
@@ -322,7 +339,7 @@ export function ValuationCard(p: ValuationCardProps) {
|
|
|
322
339
|
<div
|
|
323
340
|
className="tf-valuation-range"
|
|
324
341
|
role="img"
|
|
325
|
-
aria-label={
|
|
342
|
+
aria-label={t.range(formatMoney(lo, p.currency), formatMoney(hi, p.currency))}
|
|
326
343
|
>
|
|
327
344
|
<div className="tf-valuation-bar">
|
|
328
345
|
{p.asking != null && (
|
|
@@ -331,7 +348,7 @@ export function ValuationCard(p: ValuationCardProps) {
|
|
|
331
348
|
style={{
|
|
332
349
|
left: Math.max(0, Math.min(100, ((p.asking - lo) / span) * 100)) + "%",
|
|
333
350
|
}}
|
|
334
|
-
title=
|
|
351
|
+
title={t.askingPrice}
|
|
335
352
|
/>
|
|
336
353
|
)}
|
|
337
354
|
<span
|
|
@@ -350,7 +367,9 @@ export function ValuationCard(p: ValuationCardProps) {
|
|
|
350
367
|
<span
|
|
351
368
|
className={cx("tf-valuation-diff", p.asking > hi ? "bad" : p.asking < lo ? "good" : "")}
|
|
352
369
|
>
|
|
353
|
-
{
|
|
370
|
+
{t.askingPrice +
|
|
371
|
+
" " +
|
|
372
|
+
formatMoney(p.asking, p.currency) +
|
|
354
373
|
" · " +
|
|
355
374
|
(p.asking > v ? "+" : "") +
|
|
356
375
|
formatNumber((p.asking - v) / v, {
|
|
@@ -378,6 +397,8 @@ export function ValuationCard(p: ValuationCardProps) {
|
|
|
378
397
|
const ENERGY = ["A+", "A", "B", "C", "D", "E", "F", "G", "H"];
|
|
379
398
|
|
|
380
399
|
export function EnergyClass(p: EnergyClassProps) {
|
|
400
|
+
const t = useStrings(REAL_ESTATE_STRINGS);
|
|
401
|
+
const { formatNumber } = useFormat();
|
|
381
402
|
const scale = p.scale || ENERGY,
|
|
382
403
|
i = scale.indexOf(p.value);
|
|
383
404
|
return (
|
|
@@ -385,8 +406,7 @@ export function EnergyClass(p: EnergyClassProps) {
|
|
|
385
406
|
className="tf-energy"
|
|
386
407
|
role="img"
|
|
387
408
|
aria-label={
|
|
388
|
-
|
|
389
|
-
(p.kwh != null ? ", " + formatNumber(p.kwh) + " kWh/(m²·a)" : "")
|
|
409
|
+
t.energyClass(p.value) + (p.kwh != null ? ", " + formatNumber(p.kwh) + " kWh/(m²·a)" : "")
|
|
390
410
|
}
|
|
391
411
|
>
|
|
392
412
|
<div
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { formatNumber } from "../format.js";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
|
+
import { useStrings } from "../context.js";
|
|
6
|
+
import { RICH_TEXT_EDITOR_STRINGS } from "../strings.js";
|
|
5
7
|
import { Icon } from "../Icon.js";
|
|
6
8
|
import { FormField } from "./forms.js";
|
|
7
9
|
import { Popover } from "./overlays.js";
|
|
@@ -72,6 +74,7 @@ function rteClean(html: string | undefined): string {
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
export function RichTextEditor(p: RichTextEditorProps) {
|
|
77
|
+
const strings = useStrings(RICH_TEXT_EDITOR_STRINGS);
|
|
75
78
|
const ref = React.useRef<HTMLDivElement>(null),
|
|
76
79
|
count = React.useState(0),
|
|
77
80
|
active = React.useState<Record<string, boolean>>({}),
|
|
@@ -411,7 +414,7 @@ export function RichTextEditor(p: RichTextEditorProps) {
|
|
|
411
414
|
() => {
|
|
412
415
|
mark("B", "STRONG");
|
|
413
416
|
},
|
|
414
|
-
|
|
417
|
+
strings.bold,
|
|
415
418
|
"B",
|
|
416
419
|
],
|
|
417
420
|
[
|
|
@@ -419,7 +422,7 @@ export function RichTextEditor(p: RichTextEditorProps) {
|
|
|
419
422
|
() => {
|
|
420
423
|
mark("I", "EM");
|
|
421
424
|
},
|
|
422
|
-
|
|
425
|
+
strings.italic,
|
|
423
426
|
"I",
|
|
424
427
|
],
|
|
425
428
|
[
|
|
@@ -427,7 +430,7 @@ export function RichTextEditor(p: RichTextEditorProps) {
|
|
|
427
430
|
() => {
|
|
428
431
|
mark("U");
|
|
429
432
|
},
|
|
430
|
-
|
|
433
|
+
strings.underline,
|
|
431
434
|
"U",
|
|
432
435
|
],
|
|
433
436
|
"-",
|
|
@@ -436,38 +439,38 @@ export function RichTextEditor(p: RichTextEditorProps) {
|
|
|
436
439
|
() => {
|
|
437
440
|
setBlock("H3");
|
|
438
441
|
},
|
|
439
|
-
|
|
442
|
+
strings.heading,
|
|
440
443
|
],
|
|
441
444
|
[
|
|
442
445
|
"quote",
|
|
443
446
|
() => {
|
|
444
447
|
setBlock("BLOCKQUOTE");
|
|
445
448
|
},
|
|
446
|
-
|
|
449
|
+
strings.quote,
|
|
447
450
|
],
|
|
448
451
|
[
|
|
449
452
|
"list",
|
|
450
453
|
() => {
|
|
451
454
|
setList("UL");
|
|
452
455
|
},
|
|
453
|
-
|
|
456
|
+
strings.bulletedList,
|
|
454
457
|
],
|
|
455
458
|
[
|
|
456
459
|
"olist",
|
|
457
460
|
() => {
|
|
458
461
|
setList("OL");
|
|
459
462
|
},
|
|
460
|
-
|
|
463
|
+
strings.numberedList,
|
|
461
464
|
],
|
|
462
465
|
"-",
|
|
463
|
-
["link", openLink,
|
|
466
|
+
["link", openLink, strings.link, "K"],
|
|
464
467
|
"-",
|
|
465
468
|
[
|
|
466
469
|
"undo",
|
|
467
470
|
() => {
|
|
468
471
|
restore(-1);
|
|
469
472
|
},
|
|
470
|
-
|
|
473
|
+
strings.undo,
|
|
471
474
|
"Z",
|
|
472
475
|
],
|
|
473
476
|
[
|
|
@@ -475,7 +478,7 @@ export function RichTextEditor(p: RichTextEditorProps) {
|
|
|
475
478
|
() => {
|
|
476
479
|
restore(1);
|
|
477
480
|
},
|
|
478
|
-
|
|
481
|
+
strings.redo,
|
|
479
482
|
],
|
|
480
483
|
];
|
|
481
484
|
const over = p.maxLength && count[0] > p.maxLength,
|
|
@@ -486,7 +489,7 @@ export function RichTextEditor(p: RichTextEditorProps) {
|
|
|
486
489
|
return (
|
|
487
490
|
<FormField label={p.label} hint={p.hint} error={p.error}>
|
|
488
491
|
<div className={cx("tf-rte", over && "invalid")}>
|
|
489
|
-
<div className="tf-rte-bar" role="toolbar" aria-label=
|
|
492
|
+
<div className="tf-rte-bar" role="toolbar" aria-label={strings.formatting}>
|
|
490
493
|
{tools.map((t, i) => {
|
|
491
494
|
if (t === "-") return <span key={i} className="tf-rte-sep" aria-hidden />;
|
|
492
495
|
const btn = (
|
|
@@ -531,8 +534,8 @@ export function RichTextEditor(p: RichTextEditorProps) {
|
|
|
531
534
|
onChange={(e) => {
|
|
532
535
|
linkUrl[1](e.target.value);
|
|
533
536
|
}}
|
|
534
|
-
placeholder=
|
|
535
|
-
aria-label=
|
|
537
|
+
placeholder={strings.linkAddress}
|
|
538
|
+
aria-label={strings.linkAddress}
|
|
536
539
|
inputMode="url"
|
|
537
540
|
/>
|
|
538
541
|
<div
|
|
@@ -550,11 +553,11 @@ export function RichTextEditor(p: RichTextEditorProps) {
|
|
|
550
553
|
applyLink(true);
|
|
551
554
|
}}
|
|
552
555
|
>
|
|
553
|
-
{
|
|
556
|
+
{strings.removeLink}
|
|
554
557
|
</Button>
|
|
555
558
|
)}
|
|
556
559
|
<Button size="sm" type="submit">
|
|
557
|
-
{
|
|
560
|
+
{strings.apply}
|
|
558
561
|
</Button>
|
|
559
562
|
</div>
|
|
560
563
|
</form>
|
|
@@ -577,7 +580,7 @@ export function RichTextEditor(p: RichTextEditorProps) {
|
|
|
577
580
|
aria-expanded={s.open}
|
|
578
581
|
>
|
|
579
582
|
{"{ }"}
|
|
580
|
-
<span>{
|
|
583
|
+
<span>{strings.insertField}</span>
|
|
581
584
|
</button>
|
|
582
585
|
)}
|
|
583
586
|
>
|
|
@@ -617,7 +620,7 @@ export function RichTextEditor(p: RichTextEditorProps) {
|
|
|
617
620
|
contentEditable={!p.disabled}
|
|
618
621
|
role="textbox"
|
|
619
622
|
aria-multiline="true"
|
|
620
|
-
aria-label={typeof p.label === "string" ? p.label :
|
|
623
|
+
aria-label={typeof p.label === "string" ? p.label : strings.text}
|
|
621
624
|
data-placeholder={p.placeholder || ""}
|
|
622
625
|
suppressContentEditableWarning
|
|
623
626
|
onInput={input}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* picks, Escape closes. Inside a Form it submits under `name` and shows its message.
|
|
6
6
|
*/
|
|
7
7
|
import * as React from "react";
|
|
8
|
+
import { useStrings } from "../context.js";
|
|
9
|
+
import { SELECT_MENU_STRINGS } from "../strings.js";
|
|
8
10
|
import { cx } from "../cx.js";
|
|
9
11
|
import { Icon } from "../Icon.js";
|
|
10
12
|
import { useControlled, useId } from "../hooks.js";
|
|
@@ -16,6 +18,7 @@ const norm = (o: string | SelectMenuOption): SelectMenuOption =>
|
|
|
16
18
|
typeof o === "string" ? { value: o, label: o } : o;
|
|
17
19
|
|
|
18
20
|
export function SelectMenu(p: SelectMenuProps) {
|
|
21
|
+
const t = useStrings(SELECT_MENU_STRINGS);
|
|
19
22
|
const uid = useId("sel");
|
|
20
23
|
const id = p.id || uid;
|
|
21
24
|
const fromForm = useFieldError(p.name);
|
|
@@ -121,7 +124,7 @@ export function SelectMenu(p: SelectMenuProps) {
|
|
|
121
124
|
>
|
|
122
125
|
{cur?.icon && <Icon name={cur.icon} size={15} />}
|
|
123
126
|
<span className="tf-selectmenu-value">
|
|
124
|
-
{cur ? cur.label : p.placeholder ||
|
|
127
|
+
{cur ? cur.label : p.placeholder || t.selectPlaceholder}
|
|
125
128
|
</span>
|
|
126
129
|
<Icon name="chevronDown" size={16} className="tf-selectmenu-chev" />
|
|
127
130
|
</button>
|
|
@@ -134,7 +137,7 @@ export function SelectMenu(p: SelectMenuProps) {
|
|
|
134
137
|
role="listbox"
|
|
135
138
|
tabIndex={-1}
|
|
136
139
|
aria-labelledby={p.label ? `${id}-label` : undefined}
|
|
137
|
-
aria-label={p.label ? undefined : p.placeholder ||
|
|
140
|
+
aria-label={p.label ? undefined : p.placeholder || t.options}
|
|
138
141
|
aria-activedescendant={active >= 0 ? `${uid}-o${active}` : undefined}
|
|
139
142
|
onKeyDown={(e) => {
|
|
140
143
|
const keys: Record<string, () => void> = {
|