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
10 changes: 10 additions & 0 deletions .changeset/reativa-unicode-literals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@prescriptive/reativa": patch
---

Fix special characters rendering as mojibake in the reativa components. Text
like em dashes, ellipses, checkmarks and the collapsible/select carets was
written as UTF-8 byte escapes in plain OCaml string literals (`"\xe2\x80\x94"`),
which Melange emits one byte per JS code unit — so the browser showed `â€"`
instead of `—`. Every affected literal now uses Melange's js-quoted string
(`{js|—|js}`), which compiles to a real JavaScript string.
22 changes: 22 additions & 0 deletions packages/reativa/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@ one. It exports the JS surface the website consumes:
`mount_example(specId, containerId)`, `example_ids`,
`mount_playground(specId, containerId, props)`, `playground_ids`, and `built`.

## Non-ASCII text

An OCaml `string` is a byte sequence, and Melange emits it one byte per JS code
unit — so a plain literal holding a non-ASCII character (whether typed directly
or escaped as `"\xe2\x80\x94"`) reaches the browser as mojibake (`â€"` instead
of `—`). Write any user-visible text carrying an em dash, ellipsis, checkmark,
caret, bullet, or similar with Melange's js-quoted string, which compiles to a
real JavaScript string:

```ocaml
txt {js|Saving…|js}
```

The rewrite applies to *expression* position only. A js-quoted literal used as a
**pattern** is still the raw UTF-8 bytes and will never match, so compare
against a binding instead of matching on it:

```ocaml
let yes = {js|✓|js} in
if v = yes then (* … *)
```

## Building

reativa's core library has **no `public_name`**, so it is a *private* dune
Expand Down
2 changes: 1 addition & 1 deletion packages/reativa/src/Accordion.mlx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ let make ?(type_ : Contracts.Accordion.type_ = `single) ?(collapsible = true) ~v
in
let render (id, header, panel) =
let is_open () = List.mem id (Signal.get value) in
let mark () = if List.mem id (Signal.get value) then "\xe2\x88\x92" else "+" in
let mark () = if List.mem id (Signal.get value) then {js|−|js} else "+" in
<div>
<button
className="flex w-full items-center justify-between px-4 py-3 text-left text-sm font-medium text-neutral-800 hover:bg-neutral-50"
Expand Down
2 changes: 1 addition & 1 deletion packages/reativa/src/Collapsible.mlx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ open Reativa
open Reativa.View.Mlx

let make ~open_ ~label ~children () =
let caret () = if Signal.get open_ then "\xe2\x8c\x83" else "\xe2\x8c\x84" in
let caret () = if Signal.get open_ then {js|⌃|js} else {js|⌄|js} in
<div className="w-80 space-y-2">
(Button.make ~variant:`secondary ~extra_class:"w-full justify-between"
~on_click:(fun _ -> Signal.update open_ (fun v -> not v))
Expand Down
2 changes: 1 addition & 1 deletion packages/reativa/src/ContactSection.mlx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ let make ?(description = "") ?(details = []) ?on_submit ~heading () =
View.show
(fun () -> Signal.get sent)
(<p className="rounded-md bg-action-subtle px-3 py-2 text-sm text-ink">
("Thanks \xe2\x80\x94 your message is on its way. We reply within one business day.")
({js|Thanks your message is on its way. We reply within one business day.|js})
</p>);
]);
]
Expand Down
2 changes: 1 addition & 1 deletion packages/reativa/src/Newsletter.mlx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ let make ?(description = "") ?(consent = "") ?on_subscribe ~heading () =
(fun () -> Signal.get done_)
(<p className="mx-auto mt-5 flex max-w-md items-center justify-center gap-2 text-sm text-ink">
<span className="text-status-success">(Icon.make ~name:"check-circle" ())</span>
("Almost there \xe2\x80\x94 check your inbox to confirm.")
({js|Almost there check your inbox to confirm.|js})
</p>);
]);
(if consent = "" then View.empty
Expand Down
Loading
Loading