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: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changelog

## Unreleased
## 0.2.3 - 2026-10-05

### Security

Expand All @@ -11,6 +11,14 @@

- Clarify that `e2e/` is a published showcase, not a starter. Copy examples from component docs and `mix corex.gen.*`.

### Dependencies

- Phoenix **1.8.15**, LiveView **1.2.12**, Zag.js **1.44.0**.
- Dev tooling: `ex_slop` **0.4.5**, Tidewave **0.9.1**, Makeup **1.2.3**, Prettier **3.9.9**, Vitest **5.0.3**, Sobelow **0.16.0**.
- `Corex.Code` renders highlighted HTML from a `{:safe, _}` assign so Sobelow 0.16 does not flag `Phoenix.HTML.raw/1`. The `code` attr stays trusted input.
- The integration test harness has no `lib/` sources, so its lint alias no longer runs Sobelow. Sobelow 0.16 aborts when the scan root is empty.
- Showcase and integration apps: LiveDashboard **0.9.1** (`~> 0.9.1`), Live Capture **0.3.0**, Swoosh **1.28.1**, Req **0.7.4**, Telemetry Metrics **1.2.0**, Motion **13.5.1**.

## 0.2.2 - 2026-08-29

### Upgrade notes
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ that still contain NUL, CR, or LF after that strip are rejected.
These APIs inject HTML. Pass **developer-authored or already-sanitized** markup only — never untrusted user input.

- **Toast action labels.** `labelHtml` is set only for `%Phoenix.LiveView.Rendered{}` or `{:safe, _}`. Plain string labels use `textContent`. Client `create` ignores `:action`.
- **`Corex.Code`.** Makeup output is injected with `Phoenix.HTML.raw/1`. An HTML-capable lexer can emit markup.
- **`Corex.Code`.** Makeup output is assigned as `{:safe, html}` and rendered unescaped. An HTML-capable lexer can emit markup.

## MCP

Expand Down
6 changes: 1 addition & 5 deletions assets/lib/form-array-submit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ import { notifyPhoenixFormChange, reapplyLiveViewValueInputUsage } from "./live-
import { associateInputWithFormIfOutside, getBoolean, getString } from "./util";

export type ArraySubmitScope =
| "tags-input"
| "combobox"
| "date-picker"
| "signature-pad"
| "pin-input";
"tags-input" | "combobox" | "date-picker" | "signature-pad" | "pin-input";

export type SyncArrayHiddenInputsOptions = {
onTouched?: () => void;
Expand Down
3 changes: 1 addition & 2 deletions assets/lib/read-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,7 @@ function numberListOrDefault(values: number[]): number[] {
}

export type NumberListZag =
| { value: number[]; defaultValue?: never }
| { value?: never; defaultValue: number[] };
{ value: number[]; defaultValue?: never } | { value?: never; defaultValue: number[] };

export function mountNumberListBinding(el: HTMLElement): NumberListZag {
if (getBoolean(el, "controlled")) {
Expand Down
3 changes: 1 addition & 2 deletions assets/test/component/date-picker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ describe("buildZagDatePickerTranslations", () => {
placeholderYear: "YYYY",
});
const placeholder = t.placeholder as
| (() => { day: string; month: string; year: string })
| undefined;
(() => { day: string; month: string; year: string }) | undefined;
expect(placeholder?.()).toEqual({ day: "DD", month: "MM", year: "YYYY" });
});
});
Expand Down
88 changes: 57 additions & 31 deletions assets/test/helpers/mock-hook.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,57 @@
import { expect, vi } from "vitest";
import { expect, vi, type Mock } from "vitest";
import type { CallbackRef } from "phoenix_live_view/assets/js/types/view_hook";
import { mockLiveSocket } from "./mock-live-socket";

export function mockHookJs() {
type AnyMock = Mock<(...args: unknown[]) => unknown>;
type HandleEventMock = Mock<(event: string, callback: (payload: unknown) => void) => CallbackRef>;

const anyMock = (): AnyMock => vi.fn() as AnyMock;

export function mockHookJs(): {
exec: AnyMock;
show: AnyMock;
hide: AnyMock;
toggle: AnyMock;
addClass: AnyMock;
removeClass: AnyMock;
toggleClass: AnyMock;
transition: AnyMock;
setAttribute: AnyMock;
removeAttribute: AnyMock;
toggleAttribute: AnyMock;
push: AnyMock;
navigate: AnyMock;
patch: AnyMock;
ignoreAttributes: AnyMock;
} {
return {
exec: vi.fn(),
show: vi.fn(),
hide: vi.fn(),
toggle: vi.fn(),
addClass: vi.fn(),
removeClass: vi.fn(),
toggleClass: vi.fn(),
transition: vi.fn(),
setAttribute: vi.fn(),
removeAttribute: vi.fn(),
toggleAttribute: vi.fn(),
push: vi.fn(),
navigate: vi.fn(),
patch: vi.fn(),
ignoreAttributes: vi.fn(),
exec: anyMock(),
show: anyMock(),
hide: anyMock(),
toggle: anyMock(),
addClass: anyMock(),
removeClass: anyMock(),
toggleClass: anyMock(),
transition: anyMock(),
setAttribute: anyMock(),
removeAttribute: anyMock(),
toggleAttribute: anyMock(),
push: anyMock(),
navigate: anyMock(),
patch: anyMock(),
ignoreAttributes: anyMock(),
};
}

export type MockHookJs = ReturnType<typeof mockHookJs>;

type BaseHookContext<E extends HTMLElement> = {
el: E;
pushEvent: ReturnType<typeof vi.fn>;
pushEvent: AnyMock;
js: () => MockHookJs;
liveSocket: ReturnType<typeof mockLiveSocket>["ctx"]["liveSocket"];
handleEvent: ReturnType<
typeof vi.fn<(event: string, callback: (payload: unknown) => void) => CallbackRef>
>;
removeHandleEvent: ReturnType<typeof vi.fn>;
handleEvent: HandleEventMock;
removeHandleEvent: AnyMock;
};

type MockHookContextOptions<Extra extends Record<string, unknown>> = {
Expand All @@ -43,7 +62,16 @@ type MockHookContextOptions<Extra extends Record<string, unknown>> = {
export function mockHookContext<
E extends HTMLElement,
Extra extends Record<string, unknown> = Record<string, never>,
>(el: E, opts: MockHookContextOptions<Extra> = {}) {
>(
el: E,
opts: MockHookContextOptions<Extra> = {}
): {
hook: BaseHookContext<E> & Extra;
patch: AnyMock;
navigate: AnyMock;
jsCommands: MockHookJs;
liveSocket: ReturnType<typeof mockLiveSocket>["ctx"]["liveSocket"];
} {
const connected = opts.connected ?? false;
const { ctx, patch, navigate } = mockLiveSocket(connected);
const jsCommands = mockHookJs();
Expand All @@ -52,16 +80,14 @@ export function mockHookContext<

const base: BaseHookContext<E> = {
el,
pushEvent: vi.fn(),
pushEvent: anyMock(),
js: () => jsCommands,
liveSocket: ctx.liveSocket,
handleEvent: vi.fn(
(event: string, callback: (payload: unknown) => void): CallbackRef => ({
event,
callback,
})
),
removeHandleEvent: vi.fn(),
handleEvent: vi.fn((event: string, callback: (payload: unknown) => void): CallbackRef => ({
event,
callback,
})) as HandleEventMock,
removeHandleEvent: anyMock(),
};

const hook = { ...base, ...opts.overrides } as BaseHookContext<E> & Extra;
Expand Down
16 changes: 11 additions & 5 deletions assets/test/helpers/mock-live-socket.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { vi } from "vitest";
import { vi, type Mock } from "vitest";
import type { RedirectContext } from "../../lib/redirect";

export function mockLiveSocket(connected = true) {
const patch = vi.fn();
const navigate = vi.fn();
type AnyMock = Mock<(...args: unknown[]) => unknown>;

export function mockLiveSocket(connected = true): {
patch: AnyMock;
navigate: AnyMock;
ctx: RedirectContext;
} {
const patch = vi.fn() as AnyMock;
const navigate = vi.fn() as AnyMock;
return {
patch,
navigate,
Expand All @@ -12,6 +18,6 @@ export function mockLiveSocket(connected = true) {
getSocket: () => ({ isConnected: () => connected }),
js: () => ({ patch, navigate }),
},
} satisfies RedirectContext,
},
};
}
3 changes: 1 addition & 2 deletions assets/test/hooks/checkbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ describe("checkbox_set_checked_many", () => {
| ReturnType<typeof import("../../lib/hook-handlers").createHookHandleEventRegistry>
| undefined,
domRegistry: undefined as
| ReturnType<typeof import("../../lib/dom-events").createDomEventRegistry>
| undefined,
ReturnType<typeof import("../../lib/dom-events").createDomEventRegistry> | undefined,
},
});

Expand Down
37 changes: 11 additions & 26 deletions assets/test/hooks/lazy-hook.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { describe, expect, it, vi } from "vitest";
import type { Hook } from "phoenix_live_view";
import type { HookInterface } from "phoenix_live_view/assets/js/types/view_hook";
import { createLazyHook } from "../../hooks/lazy-hook";
import { createLazyHook, type HookModule } from "../../hooks/lazy-hook";

type HookFn<K extends "updated" | "beforeUpdate"> = NonNullable<Hook<object, HTMLElement>[K]>;

describe("createLazyHook", () => {
it("delegates mounted to the loaded hook", async () => {
Expand Down Expand Up @@ -48,13 +51,9 @@ describe("createLazyHook", () => {
});

it("replays updated after mount completes", async () => {
let resolveImport!: (value: {
TestHook: { mounted: () => void; updated: ReturnType<typeof vi.fn>; destroyed: () => void };
}) => void;
const updated = vi.fn();
const importPromise = new Promise<{
TestHook: { mounted: () => void; updated: ReturnType<typeof vi.fn>; destroyed: () => void };
}>((resolve) => {
let resolveImport!: (value: HookModule) => void;
const updated = vi.fn() as HookFn<"updated">;
const importPromise = new Promise<HookModule>((resolve) => {
resolveImport = resolve;
});

Expand All @@ -75,29 +74,15 @@ describe("createLazyHook", () => {
});

it("replays beforeUpdate before updated after mount completes", async () => {
let resolveImport!: (value: {
TestHook: {
mounted: () => void;
beforeUpdate: ReturnType<typeof vi.fn>;
updated: ReturnType<typeof vi.fn>;
destroyed: () => void;
};
}) => void;
let resolveImport!: (value: HookModule) => void;
const callOrder: string[] = [];
const beforeUpdate = vi.fn(() => {
callOrder.push("beforeUpdate");
});
}) as HookFn<"beforeUpdate">;
const updated = vi.fn(() => {
callOrder.push("updated");
});
const importPromise = new Promise<{
TestHook: {
mounted: () => void;
beforeUpdate: ReturnType<typeof vi.fn>;
updated: ReturnType<typeof vi.fn>;
destroyed: () => void;
};
}>((resolve) => {
}) as HookFn<"updated">;
const importPromise = new Promise<HookModule>((resolve) => {
resolveImport = resolve;
});

Expand Down
7 changes: 7 additions & 0 deletions design/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 0.2.3 - 2026-10-05

Patch release aligned with Corex 0.2.3. No Design API changes.

See the monorepo
[CHANGELOG](https://github.com/corex-ui/corex/blob/main/CHANGELOG.md).

## 0.2.2 - 2026-08-29

### Upgrade notes
Expand Down
2 changes: 1 addition & 1 deletion design/mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule CorexDesign.MixProject do
use Mix.Project

@version "0.2.2"
@version "0.2.3"
@scm_url "https://github.com/corex-ui/corex"

def project do
Expand Down
6 changes: 3 additions & 3 deletions design/mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
"earmark_parser": {:hex, :earmark_parser, "1.4.46", "67607a0532e810c6f630a515c548d0b24949643f168cc556303bee4cf96105c7", [:mix], [], "hexpm", "9c44636e8a1c68c62f526b2dcd85d941dbbcee7ab82cf64ba06ce28bef8e89f5"},
"erlex": {:hex, :erlex, "0.2.9", "7debbbaa9f4f368b8cd648983e0f1d7963028508e9c59e9d4ed504e94ef52a55", [:mix], [], "hexpm", "8cfffc0ec7159e6d73de2ab28a588064de80f88b2798d5cbe4482cbbc200178b"},
"ex_doc": {:hex, :ex_doc, "0.40.4", "66f2e42bf588594d5a8aab31cad87f2ddad09d0da1b1a2f379340ec2c2e497cb", [:mix], [{:earmark_parser, "~> 1.4.46", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "6222b9e423d76584ee34df2c82a5ed72c2d53dc153f7f483ad28b378694186cc"},
"ex_slop": {:hex, :ex_slop, "0.4.4", "8ca277b902e93193eb3545e414c309026a51204e5eb7d6ada5ff96555de6abcb", [:mix], [{:credo, "~> 1.7", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "03ef1b2553ebad45c8ceb3e482eed5f1ccd08fc992215a9da0ef6196935107e2"},
"ex_slop": {:hex, :ex_slop, "0.4.5", "4e3ea38397d08d422f8e92e4cef16877e4a31a93c6abd07b494f4458e37c3ce2", [:mix], [{:credo, "~> 1.7", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "174c1ce4da612f7d3e93734d08d81d93774bacb6a8e86123e7d6876369a2df81"},
"excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"},
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
"makeup": {:hex, :makeup, "1.2.2", "882d46dc0905e9ff7abf2aab61a7e6b3dcc555533977d8a23b06019e6c89ac94", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9a1a24e5b343b8ae16abea0822c10a6f75da27af7fa802ada5251f7579bfccfa"},
"makeup": {:hex, :makeup, "1.2.3", "c3ef0ff83acf305384acba0f70bff87d8732502baa1eb3eb081d8c154fbe18f5", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "15970751b7021c2bcd7ed56a54b3def23affd3969fb7c214dffdf28e97ce6f76"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.1.0", "835f7e60792e08824cda445639555d7bf1bbbddb1b60b306e33cb6f6db24dc74", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "1cd6780fb1dd1a03979abaed0fe82712b0625118fd5257d3ebbf73f960c73c3c"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"oeditus_credo": {:hex, :oeditus_credo, "0.11.2", "825f9184262bcdf202ee193568d28a08f42f6dab2813575e6c4721240a921bc9", [:mix], [{:credo, "~> 1.7", [hex: :credo, repo: "hexpm", optional: false]}, {:typle, "~> 0.3", [hex: :typle, repo: "hexpm", optional: true]}], "hexpm", "0c433edfbb96377e60b53f62e9a7b64c327f40ed7078dc00b107f99f9a526399"},
"sobelow": {:hex, :sobelow, "0.15.0", "b067d7f8522a9d758fa89cb2bfcbab7ad72c45a0993cb958c989c6fd956fdd56", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "24a800e2d7fa8c3bd21561b6ad8ad4745ed726a09fd606598981d9048708da98"},
"sobelow": {:hex, :sobelow, "0.16.0", "13e3f761bc7fcc88e81ae23ddef9140617a12a3efb26601e5516453290539398", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "00a0260edf9aeac35d9116366f55dc9451d11e0e696e28ec5570c6a56bd41f1e"},
}
2 changes: 1 addition & 1 deletion e2e/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ defmodule E2e.MixProject do
{:postgrex, ">= 0.22.4"},
{:phoenix_html, "~> 4.1"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:phoenix_live_dashboard, "~> 0.8.3"},
{:phoenix_live_dashboard, "~> 0.9.1"},
{:phoenix_live_view, "~> 1.1"},
{:live_capture, "~> 0.2"},
{:lazy_html, ">= 0.1.0", only: :test},
Expand Down
Loading
Loading