🔥 initial commit 🔥

This commit is contained in:
2026-03-30 01:02:24 -04:00
commit cb179333f0
77 changed files with 6974 additions and 0 deletions

106
assets/css/app.css Normal file
View File

@@ -0,0 +1,106 @@
/* See the Tailwind configuration guide for advanced usage
https://tailwindcss.com/docs/configuration */
@import "tailwindcss" source(none);
@source "../../deps/ash_authentication_phoenix";
@source "../css";
@source "../js";
@source "../../lib/mixer_web";
/* A Tailwind plugin that makes "hero-#{ICON}" classes available.
The heroicons installation itself is managed by your mix.exs */
@plugin "../vendor/heroicons";
/* daisyUI Tailwind Plugin. You can update this file by fetching the latest version with:
curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui.js
Make sure to look at the daisyUI changelog: https://daisyui.com/docs/changelog/ */
@plugin "../vendor/daisyui" {
themes: false;
}
/* daisyUI theme plugin. You can update this file by fetching the latest version with:
curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui-theme.js
We ship with two themes, a light one inspired on Phoenix colors and a dark one inspired
on Elixir colors. Build your own at: https://daisyui.com/theme-generator/ */
@plugin "../vendor/daisyui-theme" {
name: "dark";
default: false;
prefersdark: true;
color-scheme: "dark";
--color-base-100: oklch(30.33% 0.016 252.42);
--color-base-200: oklch(25.26% 0.014 253.1);
--color-base-300: oklch(20.15% 0.012 254.09);
--color-base-content: oklch(97.807% 0.029 256.847);
--color-primary: oklch(58% 0.233 277.117);
--color-primary-content: oklch(96% 0.018 272.314);
--color-secondary: oklch(58% 0.233 277.117);
--color-secondary-content: oklch(96% 0.018 272.314);
--color-accent: oklch(60% 0.25 292.717);
--color-accent-content: oklch(96% 0.016 293.756);
--color-neutral: oklch(37% 0.044 257.287);
--color-neutral-content: oklch(98% 0.003 247.858);
--color-info: oklch(58% 0.158 241.966);
--color-info-content: oklch(97% 0.013 236.62);
--color-success: oklch(60% 0.118 184.704);
--color-success-content: oklch(98% 0.014 180.72);
--color-warning: oklch(66% 0.179 58.318);
--color-warning-content: oklch(98% 0.022 95.277);
--color-error: oklch(58% 0.253 17.585);
--color-error-content: oklch(96% 0.015 12.422);
--radius-selector: 0.25rem;
--radius-field: 0.25rem;
--radius-box: 0.5rem;
--size-selector: 0.21875rem;
--size-field: 0.21875rem;
--border: 1.5px;
--depth: 1;
--noise: 0;
}
@plugin "../vendor/daisyui-theme" {
name: "light";
default: true;
prefersdark: false;
color-scheme: "light";
--color-base-100: oklch(98% 0 0);
--color-base-200: oklch(96% 0.001 286.375);
--color-base-300: oklch(92% 0.004 286.32);
--color-base-content: oklch(21% 0.006 285.885);
--color-primary: oklch(70% 0.213 47.604);
--color-primary-content: oklch(98% 0.016 73.684);
--color-secondary: oklch(55% 0.027 264.364);
--color-secondary-content: oklch(98% 0.002 247.839);
--color-accent: oklch(0% 0 0);
--color-accent-content: oklch(100% 0 0);
--color-neutral: oklch(44% 0.017 285.786);
--color-neutral-content: oklch(98% 0 0);
--color-info: oklch(62% 0.214 259.815);
--color-info-content: oklch(97% 0.014 254.604);
--color-success: oklch(70% 0.14 182.503);
--color-success-content: oklch(98% 0.014 180.72);
--color-warning: oklch(66% 0.179 58.318);
--color-warning-content: oklch(98% 0.022 95.277);
--color-error: oklch(58% 0.253 17.585);
--color-error-content: oklch(96% 0.015 12.422);
--radius-selector: 0.25rem;
--radius-field: 0.25rem;
--radius-box: 0.5rem;
--size-selector: 0.21875rem;
--size-field: 0.21875rem;
--border: 1.5px;
--depth: 1;
--noise: 0;
}
/* Add variants based on LiveView classes */
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
/* Use the data attribute for dark mode */
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
/* Make LiveView wrapper divs transparent for layout */
[data-phx-session], [data-phx-teleported-src] { display: contents }
/* This file is for your main application CSS */

395
assets/js/animation.ts Normal file
View File

@@ -0,0 +1,395 @@
// AshTypescript landing page animation
// Typewriter effect with syntax highlighting, inspired by ash-hq.org
const HEXDOCS = "https://hexdocs.pm/ash_typescript";
interface Stage {
name: string;
description: string;
docsPath: string;
elixir: string;
typescript: string;
}
const stages: Stage[] = [
{
name: "Type-Safe RPC",
description: "Auto-generated TypeScript functions for every Ash action with full type inference.",
docsPath: "/first-rpc-action.html",
elixir: `use Ash.Domain,
extensions: [AshTypescript.Rpc]
typescript_rpc do
resource MyApp.Todo do
rpc_action :list_todos, :read
rpc_action :create_todo, :create
rpc_action :update_todo, :update
end
end`,
typescript: `import { listTodos, createTodo } from "./ash_rpc";
const result = await listTodos({
fields: ["id", "title", { user: ["name"] }],
filter: { completed: { eq: false } },
sort: [{ field: "insertedAt", order: "desc" }],
});
if (result.success) {
// result.data is fully typed!
result.data.forEach(todo => {
console.log(todo.title, todo.user.name);
});
}`,
},
{
name: "Typed Controllers",
description: "Typed route helpers and fetch functions for Phoenix controllers.",
docsPath: "/typed-controllers.html",
elixir: `use AshTypescript.TypedController
typed_controller do
module_name MyAppWeb.SessionController
get :current_user do
run fn conn, _params ->
json(conn, current_user(conn))
end
end
post :login do
argument :email, :string, allow_nil?: false
argument :password, :string, allow_nil?: false
run fn conn, params ->
# authenticate and respond
end
end
end`,
typescript: `import {
currentUserPath,
login,
} from "./routes";
// Typed path helpers
currentUserPath(); // => "/session/current_user"
// Typed fetch functions for mutations
const result = await login({
email: "user@example.com",
password: "secret",
headers: buildCSRFHeaders(),
});`,
},
{
name: "Typed Channels",
description: "Type-safe Phoenix channel event subscriptions with Ash PubSub.",
docsPath: "/typed-channels.html",
elixir: `# Resource with PubSub
pub_sub do
prefix "posts"
publish :create, [:id],
event: "post_created",
transform: :post_summary
end
# Typed channel definition
typed_channel do
topic "org:*"
resource MyApp.Post do
publish :post_created
publish :post_updated
end
end`,
typescript: `import {
createOrgChannel,
onOrgChannelMessages,
} from "./ash_typed_channels";
const channel = createOrgChannel(socket, orgId);
const refs = onOrgChannelMessages(channel, {
post_created: (payload) => {
// payload type inferred from calculation!
addPost(payload.id, payload.title);
},
post_updated: (payload) => {
updatePost(payload.id, payload.title);
},
});`,
},
{
name: "Field Selection",
description: "Request exactly the fields you need with full type narrowing.",
docsPath: "/field-selection.html",
elixir: `# Define your resource with relationships
attributes do
uuid_primary_key :id
attribute :title, :string, public?: true
attribute :body, :string, public?: true
attribute :view_count, :integer, public?: true
end
relationships do
belongs_to :author, MyApp.User, public?: true
end
calculations do
calculate :reading_time, :integer,
expr(string_length(body) / 200)
end`,
typescript: `// Only fetch what you need - response is narrowed
const posts = await listPosts({
fields: [
"id",
"title",
"readingTime",
{ author: ["name", "avatarUrl"] },
],
});
// TypeScript knows the exact shape:
posts.data[0].title; // string ✓
posts.data[0].readingTime; // number ✓
posts.data[0].author.name; // string ✓
posts.data[0].body; // Error! Not selected`,
},
];
// Elixir syntax highlighting
function highlightElixir(code: string): string {
const tokens: { start: number; end: number; cls: string }[] = [];
const patterns: [RegExp, string][] = [
[/#[^\n]*/g, "text-gray-500"],
[/"[^"]*"/g, "text-yellow-400"],
[/\b(defmodule|def|defp|do|end|use|fn|if|else|case|cond|with|for|unless|import|alias|require)\b/g, "text-pink-400"],
[/\b(true|false|nil)\b/g, "text-purple-400"],
[/(:[a-zA-Z_][a-zA-Z0-9_?!]*)/g, "text-cyan-400"],
[/\b([A-Z][a-zA-Z0-9]*(\.[A-Z][a-zA-Z0-9]*)*)\b/g, "text-blue-400"],
[/(\|>|->|<-|=>)/g, "text-pink-400"],
];
for (const [re, cls] of patterns) {
let m: RegExpExecArray | null;
while ((m = re.exec(code)) !== null) {
const overlaps = tokens.some(t => m!.index < t.end && m!.index + m![0].length > t.start);
if (!overlaps) tokens.push({ start: m.index, end: m.index + m[0].length, cls });
}
}
tokens.sort((a, b) => a.start - b.start);
let result = "";
let pos = 0;
for (const t of tokens) {
if (t.start > pos) result += esc(code.slice(pos, t.start));
result += `<span class="${t.cls}">${esc(code.slice(t.start, t.end))}</span>`;
pos = t.end;
}
if (pos < code.length) result += esc(code.slice(pos));
return result;
}
// TypeScript syntax highlighting
function highlightTS(code: string): string {
const tokens: { start: number; end: number; cls: string }[] = [];
const patterns: [RegExp, string][] = [
[/\/\/[^\n]*/g, "text-gray-500"],
[/"[^"]*"/g, "text-yellow-400"],
[/`[^`]*`/g, "text-yellow-400"],
[/\b(import|from|export|const|let|var|function|return|if|else|async|await|new|typeof|interface|type)\b/g, "text-pink-400"],
[/\b(true|false|null|undefined)\b/g, "text-purple-400"],
[/(=>)/g, "text-pink-400"],
];
for (const [re, cls] of patterns) {
let m: RegExpExecArray | null;
while ((m = re.exec(code)) !== null) {
const overlaps = tokens.some(t => m!.index < t.end && m!.index + m![0].length > t.start);
if (!overlaps) tokens.push({ start: m.index, end: m.index + m[0].length, cls });
}
}
tokens.sort((a, b) => a.start - b.start);
let result = "";
let pos = 0;
for (const t of tokens) {
if (t.start > pos) result += esc(code.slice(pos, t.start));
result += `<span class="${t.cls}">${esc(code.slice(t.start, t.end))}</span>`;
pos = t.end;
}
if (pos < code.length) result += esc(code.slice(pos));
return result;
}
function esc(s: string): string {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// Typewriter engine
function typewrite(
el: HTMLElement,
highlighted: string,
onComplete: () => void,
signal: { cancelled: boolean },
): void {
// Build a flat list of characters with their HTML wrapping
const chars: string[] = [];
let inTag = false;
let tagBuffer = "";
let openTags: string[] = [];
for (let i = 0; i < highlighted.length; i++) {
const ch = highlighted[i];
if (ch === "<") {
inTag = true;
tagBuffer = "<";
continue;
}
if (inTag) {
tagBuffer += ch;
if (ch === ">") {
inTag = false;
if (tagBuffer.startsWith("</")) {
openTags.pop();
} else {
openTags.push(tagBuffer);
}
}
continue;
}
// Handle HTML entities as single visible characters
let visibleChar = ch;
if (ch === "&") {
const semiIdx = highlighted.indexOf(";", i);
if (semiIdx !== -1 && semiIdx - i < 8) {
visibleChar = highlighted.slice(i, semiIdx + 1);
i = semiIdx;
}
}
let wrapped = visibleChar;
for (const tag of openTags) {
const cls = tag.match(/class="([^"]*)"/)?.[1] || "";
wrapped = `<span class="${cls}">${wrapped}</span>`;
}
chars.push(wrapped);
}
let idx = 0;
el.innerHTML = '<span class="inline-block w-[2px] h-[1.1em] bg-primary align-text-bottom animate-pulse"></span>';
function step() {
if (signal.cancelled) return;
if (idx >= chars.length) {
el.innerHTML = highlighted;
onComplete();
return;
}
// Insert character before cursor
const cursor = el.querySelector("span:last-child")!;
cursor.insertAdjacentHTML("beforebegin", chars[idx]);
idx++;
const c = chars[idx - 1];
const isNewline = c.includes("\n") || c === "\n";
const isSpace = c === " " || c.endsWith("> </span>");
const delay = isNewline ? 80 : isSpace ? 15 : 25 + Math.random() * 15;
setTimeout(step, delay);
}
step();
}
// Main initialization
export function initLandingPage(container: HTMLElement): () => void {
let currentStage = 0;
let signal = { cancelled: false };
let autoTimer: ReturnType<typeof setTimeout> | null = null;
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
container.innerHTML = buildHTML();
const elixirEl = container.querySelector<HTMLElement>("#elixir-code")!;
const tsEl = container.querySelector<HTMLElement>("#ts-code")!;
const descEl = container.querySelector<HTMLElement>("#stage-description")!;
const docsLink = container.querySelector<HTMLAnchorElement>("#stage-docs-link")!;
const dots = container.querySelectorAll<HTMLElement>(".stage-dot");
const tsPanel = container.querySelector<HTMLElement>("#ts-panel")!;
function showStage(index: number) {
signal.cancelled = true;
signal = { cancelled: false };
if (autoTimer) clearTimeout(autoTimer);
currentStage = index;
const stage = stages[index];
// Update dots
dots.forEach((dot, i) => {
dot.classList.toggle("bg-primary", i === index);
dot.classList.toggle("opacity-100", i === index);
dot.classList.toggle("bg-base-content", i !== index);
dot.classList.toggle("opacity-30", i !== index);
dot.classList.toggle("scale-125", i === index);
});
// Update description
descEl.textContent = stage.description;
docsLink.href = HEXDOCS + stage.docsPath;
// Reset panels — hide TS panel entirely (no layout space)
tsPanel.hidden = true;
tsPanel.style.opacity = "0";
const elixirHL = highlightElixir(stage.elixir);
const tsHL = highlightTS(stage.typescript);
if (reducedMotion) {
elixirEl.innerHTML = elixirHL;
tsEl.innerHTML = tsHL;
tsPanel.hidden = false;
tsPanel.style.opacity = "1";
autoTimer = setTimeout(() => showStage((currentStage + 1) % stages.length), 6000);
} else {
typewrite(elixirEl, elixirHL, () => {
if (signal.cancelled) return;
tsPanel.hidden = false;
requestAnimationFrame(() => { tsPanel.style.opacity = "1"; });
typewrite(tsEl, tsHL, () => {
if (signal.cancelled) return;
autoTimer = setTimeout(() => showStage((currentStage + 1) % stages.length), 4000);
}, signal);
}, signal);
}
}
// Dot click handlers
dots.forEach((dot, i) => {
dot.addEventListener("click", () => showStage(i));
});
// Start
showStage(0);
// Cleanup function
return () => {
signal.cancelled = true;
if (autoTimer) clearTimeout(autoTimer);
};
}
function buildHTML(): string {
const dotHTML = stages.map((s, i) =>
`<button class="stage-dot w-3 h-3 rounded-full transition-all duration-300 cursor-pointer ${i === 0 ? "bg-primary opacity-100 scale-125" : "bg-base-content opacity-30"}" title="${s.name}"></button>`
).join("");
return `
<div class="mb-12">
<div class="flex items-center justify-between mb-4">
<div class="flex gap-2 items-center">${dotHTML}</div>
<a id="stage-docs-link" href="${HEXDOCS}" target="_blank" rel="noopener noreferrer" class="text-sm text-primary hover:underline">View docs &rarr;</a>
</div>
<p id="stage-description" class="text-sm opacity-70 mb-4">${stages[0].description}</p>
<div class="space-y-3">
<div class="relative">
<div class="absolute top-2 right-3 text-xs opacity-40 font-mono select-none">Elixir</div>
<pre class="bg-base-300 rounded-lg p-4 pt-8 text-sm font-mono leading-relaxed overflow-x-auto"><code id="elixir-code" class="text-gray-300"></code></pre>
</div>
<div id="ts-panel" class="relative transition-opacity duration-500" style="opacity:0" hidden>
<div class="absolute top-2 right-3 text-xs opacity-40 font-mono select-none">TypeScript</div>
<pre class="bg-base-300 rounded-lg p-4 pt-8 text-sm font-mono leading-relaxed overflow-x-auto"><code id="ts-code" class="text-gray-300"></code></pre>
</div>
</div>
</div>`;
}

82
assets/js/app.js Normal file
View File

@@ -0,0 +1,82 @@
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"
// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
// import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
// import "some-package"
//
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
// To load it, simply add a second `<link>` to your `root.html.heex` file.
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import {hooks as colocatedHooks} from "phoenix-colocated/mixer"
import topbar from "topbar"
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken},
hooks: {...colocatedHooks},
})
// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
// connect if there are any LiveViews on the page
liveSocket.connect()
// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket
// The lines below enable quality of life phoenix_live_reload
// development features:
//
// 1. stream server logs to the browser console
// 2. click on elements to jump to their definitions in your code editor
//
if (process.env.NODE_ENV === "development") {
window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
// Enable server log streaming to client.
// Disable with reloader.disableServerLogs()
reloader.enableServerLogs()
// Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
//
// * click with "c" key pressed to open at caller location
// * click with "d" key pressed to open at function component definition location
let keyDown
window.addEventListener("keydown", e => keyDown = e.key)
window.addEventListener("keyup", _e => keyDown = null)
window.addEventListener("click", e => {
if(keyDown === "c"){
e.preventDefault()
e.stopImmediatePropagation()
reloader.openEditorAtCaller(e.target)
} else if(keyDown === "d"){
e.preventDefault()
e.stopImmediatePropagation()
reloader.openEditorAtDef(e.target)
}
}, true)
window.liveReloader = reloader
})
}

201
assets/js/ash_rpc.ts Normal file
View File

@@ -0,0 +1,201 @@
// Generated by AshTypescript - RPC Actions
// Do not edit this file manually
export type * from "./ash_types";
// Helper Functions
/**
* Configuration options for action RPC requests
*/
export interface ActionConfig {
// Request data
input?: Record<string, any>;
identity?: any;
fields?: Array<string | Record<string, any>>; // Field selection
filter?: Record<string, any>; // Filter options (for reads)
sort?: string | string[]; // Sort options
page?:
| {
// Offset-based pagination
limit?: number;
offset?: number;
count?: boolean;
}
| {
// Keyset pagination
limit?: number;
after?: string;
before?: string;
};
// Metadata
metadataFields?: ReadonlyArray<string>;
// HTTP customization
headers?: Record<string, string>; // Custom headers
fetchOptions?: RequestInit; // Fetch options (signal, cache, etc.)
customFetch?: (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
// Multitenancy
tenant?: string; // Tenant parameter
// Hook context
hookCtx?: Record<string, any>;
}
/**
* Configuration options for validation RPC requests
*/
export interface ValidationConfig {
// Request data
input?: Record<string, any>;
// HTTP customization
headers?: Record<string, string>;
fetchOptions?: RequestInit;
customFetch?: (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
// Hook context
hookCtx?: Record<string, any>;
}
/**
* Gets the CSRF token from the page's meta tag
* Returns null if no CSRF token is found
*/
export function getPhoenixCSRFToken(): string | null {
return document
?.querySelector("meta[name='csrf-token']")
?.getAttribute("content") || null;
}
/**
* Builds headers object with CSRF token for Phoenix applications
* Returns headers object with X-CSRF-Token (if available)
*/
export function buildCSRFHeaders(headers: Record<string, string> = {}): Record<string, string> {
const csrfToken = getPhoenixCSRFToken();
if (csrfToken) {
headers["X-CSRF-Token"] = csrfToken;
}
return headers;
}
/**
* Internal helper function for making action RPC requests
* Handles hooks, request configuration, fetch execution, and error handling
* @param config Configuration matching ActionConfig
*/
export async function executeActionRpcRequest<T>(
payload: Record<string, any>,
config: ActionConfig
): Promise<T> {
const processedConfig = config;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...processedConfig.headers,
...config.headers,
};
const fetchFunction = config.customFetch || processedConfig.customFetch || fetch;
const fetchOptions: RequestInit = {
...processedConfig.fetchOptions,
...config.fetchOptions,
method: "POST",
headers,
body: JSON.stringify(payload),
};
const response = await fetchFunction("/rpc/run", fetchOptions);
const result = response.ok ? await response.json() : null;
if (!response.ok) {
return {
success: false,
errors: [
{
type: "network_error",
message: `Network request failed: ${response.statusText}`,
shortMessage: "Network error",
vars: { statusCode: response.status, statusText: response.statusText },
fields: [],
path: [],
details: { statusCode: response.status }
}
],
} as T;
}
return result as T;
}
/**
* Internal helper function for making validation RPC requests
* Handles hooks, request configuration, fetch execution, and error handling
* @param config Configuration matching ValidationConfig
*/
export async function executeValidationRpcRequest<T>(
payload: Record<string, any>,
config: ValidationConfig
): Promise<T> {
const processedConfig = config;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...processedConfig.headers,
...config.headers,
};
const fetchFunction = config.customFetch || processedConfig.customFetch || fetch;
const fetchOptions: RequestInit = {
...processedConfig.fetchOptions,
...config.fetchOptions,
method: "POST",
headers,
body: JSON.stringify(payload),
};
const response = await fetchFunction("/rpc/validate", fetchOptions);
const result = response.ok ? await response.json() : null;
if (!response.ok) {
return {
success: false,
errors: [
{
type: "network_error",
message: `Network request failed: ${response.statusText}`,
shortMessage: "Network error",
vars: { statusCode: response.status, statusText: response.statusText },
fields: [],
path: [],
details: { statusCode: response.status }
}
],
} as T;
}
return result as T;
}

440
assets/js/ash_types.ts Normal file
View File

@@ -0,0 +1,440 @@
// Generated by AshTypescript - Shared Types
// Do not edit this file manually
// Utility Types
// Sort string type — allows optional direction prefix on sort field names
// Prefixes per Ash.Query.sort/3: + (asc), - (desc), ++ (asc_nils_first), -- (desc_nils_last)
export type SortString<T extends string> = T | `+${T}` | `-${T}` | `++${T}` | `--${T}`;
// Resource schema constraint
export type TypedSchema = {
__type: "Resource" | "TypedMap" | "Union";
__primitiveFields: string;
};
// Utility type to convert union to intersection
export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I,
) => void
? I
: never;
// Helper type to infer union field values, avoiding duplication between array and non-array unions
export type InferUnionFieldValue<
UnionSchema extends { __type: "Union"; __primitiveFields: any },
FieldSelection extends any[],
> = UnionToIntersection<
{
[FieldIndex in keyof FieldSelection]: FieldSelection[FieldIndex] extends UnionSchema["__primitiveFields"]
? FieldSelection[FieldIndex] extends keyof UnionSchema
? { [P in FieldSelection[FieldIndex]]: UnionSchema[FieldSelection[FieldIndex]] }
: never
: FieldSelection[FieldIndex] extends Record<string, any>
? {
[UnionKey in keyof FieldSelection[FieldIndex]]: UnionKey extends keyof UnionSchema
? NonNullable<UnionSchema[UnionKey]> extends { __array: true; __type: "TypedMap"; __primitiveFields: infer TypedMapFields }
? FieldSelection[FieldIndex][UnionKey] extends any[]
? Array<
UnionToIntersection<
{
[FieldIdx in keyof FieldSelection[FieldIndex][UnionKey]]: FieldSelection[FieldIndex][UnionKey][FieldIdx] extends TypedMapFields
? FieldSelection[FieldIndex][UnionKey][FieldIdx] extends keyof NonNullable<UnionSchema[UnionKey]>
? { [P in FieldSelection[FieldIndex][UnionKey][FieldIdx]]: NonNullable<UnionSchema[UnionKey]>[P] }
: never
: never;
}[number]
>
> | null
: never
: NonNullable<UnionSchema[UnionKey]> extends { __type: "TypedMap"; __primitiveFields: infer TypedMapFields }
? FieldSelection[FieldIndex][UnionKey] extends any[]
? UnionToIntersection<
{
[FieldIdx in keyof FieldSelection[FieldIndex][UnionKey]]: FieldSelection[FieldIndex][UnionKey][FieldIdx] extends TypedMapFields
? FieldSelection[FieldIndex][UnionKey][FieldIdx] extends keyof NonNullable<UnionSchema[UnionKey]>
? { [P in FieldSelection[FieldIndex][UnionKey][FieldIdx]]: NonNullable<UnionSchema[UnionKey]>[P] }
: never
: never;
}[number]
> | null
: never
: NonNullable<UnionSchema[UnionKey]> extends TypedSchema
? InferResult<NonNullable<UnionSchema[UnionKey]>, FieldSelection[FieldIndex][UnionKey]>
: never
: never;
}
: never;
}[number]
>;
export type HasComplexFields<T extends TypedSchema> = keyof Omit<
T,
"__primitiveFields" | "__type" | T["__primitiveFields"]
> extends never
? false
: true;
export type ComplexFieldKeys<T extends TypedSchema> = keyof Omit<
T,
"__primitiveFields" | "__type" | T["__primitiveFields"]
>;
export type LeafFieldSelection<T extends TypedSchema> = T["__primitiveFields"];
export type ComplexFieldSelection<T extends TypedSchema> = {
[K in ComplexFieldKeys<T>]?: T[K] extends {
__type: "Relationship";
__resource: infer Resource;
}
? NonNullable<Resource> extends TypedSchema
? UnifiedFieldSelection<NonNullable<Resource>>[]
: never
: T[K] extends {
__type: "ComplexCalculation";
__returnType: infer ReturnType;
}
? T[K] extends { __args: infer Args }
? NonNullable<ReturnType> extends TypedSchema
? {
args: Args;
fields: UnifiedFieldSelection<NonNullable<ReturnType>>[];
}
: { args: Args }
: NonNullable<ReturnType> extends TypedSchema
? { fields: UnifiedFieldSelection<NonNullable<ReturnType>>[] }
: never
: T[K] extends { __type: "TypedMap" }
? NonNullable<T[K]> extends TypedSchema
? UnifiedFieldSelection<NonNullable<T[K]>>[]
: never
: T[K] extends { __type: "Union"; __primitiveFields: infer PrimitiveFields }
? T[K] extends { __array: true }
? (PrimitiveFields | {
[UnionKey in keyof Omit<T[K], "__type" | "__primitiveFields" | "__array">]?: NonNullable<T[K][UnionKey]> extends { __type: "TypedMap"; __primitiveFields: any }
? NonNullable<T[K][UnionKey]>["__primitiveFields"][]
: NonNullable<T[K][UnionKey]> extends TypedSchema
? UnifiedFieldSelection<NonNullable<T[K][UnionKey]>>[]
: never;
})[]
: (PrimitiveFields | {
[UnionKey in keyof Omit<T[K], "__type" | "__primitiveFields">]?: NonNullable<T[K][UnionKey]> extends { __type: "TypedMap"; __primitiveFields: any }
? NonNullable<T[K][UnionKey]>["__primitiveFields"][]
: NonNullable<T[K][UnionKey]> extends TypedSchema
? UnifiedFieldSelection<NonNullable<T[K][UnionKey]>>[]
: never;
})[]
: NonNullable<T[K]> extends TypedSchema
? UnifiedFieldSelection<NonNullable<T[K]>>[]
: never;
};
// Main type: Use explicit base case detection to prevent infinite recursion
export type UnifiedFieldSelection<T extends TypedSchema> =
HasComplexFields<T> extends false
? LeafFieldSelection<T> // Base case: only primitives, no recursion
: LeafFieldSelection<T> | ComplexFieldSelection<T>; // Recursive case
export type InferFieldValue<
T extends TypedSchema,
Field,
> = Field extends T["__primitiveFields"]
? Field extends keyof T
? { [K in Field]: T[Field] }
: never
: Field extends Record<string, any>
? {
[K in keyof Field]: K extends keyof T
? T[K] extends {
__type: "Relationship";
__resource: infer Resource;
}
? NonNullable<Resource> extends TypedSchema
? T[K] extends { __array: true }
? Array<InferResult<NonNullable<Resource>, Field[K]>>
: null extends Resource
? InferResult<NonNullable<Resource>, Field[K]> | null
: InferResult<NonNullable<Resource>, Field[K]>
: never
: T[K] extends {
__type: "ComplexCalculation";
__returnType: infer ReturnType;
}
? NonNullable<ReturnType> extends TypedSchema
? null extends ReturnType
? InferResult<NonNullable<ReturnType>, Field[K]["fields"]> | null
: InferResult<NonNullable<ReturnType>, Field[K]["fields"]>
: ReturnType
: NonNullable<T[K]> extends { __type: "TypedMap"; __primitiveFields: infer TypedMapFields }
? NonNullable<T[K]> extends { __array: true }
? Field[K] extends any[]
? null extends T[K]
? Array<
UnionToIntersection<
{
[FieldIndex in keyof Field[K]]: Field[K][FieldIndex] extends infer E
? E extends TypedMapFields
? E extends keyof NonNullable<T[K]>
? { [P in E]: NonNullable<T[K]>[P] }
: never
: E extends Record<string, any>
? {
[NestedKey in keyof E]: NestedKey extends keyof NonNullable<T[K]>
? NonNullable<NonNullable<T[K]>[NestedKey]> extends TypedSchema
? null extends NonNullable<T[K]>[NestedKey]
? InferResult<NonNullable<NonNullable<T[K]>[NestedKey]>, E[NestedKey]> | null
: InferResult<NonNullable<NonNullable<T[K]>[NestedKey]>, E[NestedKey]>
: never
: never;
}
: E extends keyof NonNullable<T[K]>
? { [P in E]: NonNullable<T[K]>[P] }
: never
: never;
}[number]
>
> | null
: Array<
UnionToIntersection<
{
[FieldIndex in keyof Field[K]]: Field[K][FieldIndex] extends infer E
? E extends TypedMapFields
? E extends keyof NonNullable<T[K]>
? { [P in E]: NonNullable<T[K]>[P] }
: never
: E extends Record<string, any>
? {
[NestedKey in keyof E]: NestedKey extends keyof NonNullable<T[K]>
? NonNullable<NonNullable<T[K]>[NestedKey]> extends TypedSchema
? null extends NonNullable<T[K]>[NestedKey]
? InferResult<NonNullable<NonNullable<T[K]>[NestedKey]>, E[NestedKey]> | null
: InferResult<NonNullable<NonNullable<T[K]>[NestedKey]>, E[NestedKey]>
: never
: never;
}
: E extends keyof NonNullable<T[K]>
? { [P in E]: NonNullable<T[K]>[P] }
: never
: never;
}[number]
>
>
: never
: Field[K] extends any[]
? null extends T[K]
? UnionToIntersection<
{
[FieldIndex in keyof Field[K]]: Field[K][FieldIndex] extends infer E
? E extends TypedMapFields
? E extends keyof NonNullable<T[K]>
? { [P in E]: NonNullable<T[K]>[P] }
: never
: E extends Record<string, any>
? {
[NestedKey in keyof E]: NestedKey extends keyof NonNullable<T[K]>
? NonNullable<NonNullable<T[K]>[NestedKey]> extends TypedSchema
? null extends NonNullable<T[K]>[NestedKey]
? InferResult<NonNullable<NonNullable<T[K]>[NestedKey]>, E[NestedKey]> | null
: InferResult<NonNullable<NonNullable<T[K]>[NestedKey]>, E[NestedKey]>
: never
: never;
}
: E extends keyof NonNullable<T[K]>
? { [P in E]: NonNullable<T[K]>[P] }
: never
: never;
}[number]
> | null
: UnionToIntersection<
{
[FieldIndex in keyof Field[K]]: Field[K][FieldIndex] extends infer E
? E extends TypedMapFields
? E extends keyof T[K]
? { [P in E]: T[K][P] }
: never
: E extends Record<string, any>
? {
[NestedKey in keyof E]: NestedKey extends keyof NonNullable<T[K]>
? NonNullable<NonNullable<T[K]>[NestedKey]> extends TypedSchema
? null extends NonNullable<T[K]>[NestedKey]
? InferResult<NonNullable<NonNullable<T[K]>[NestedKey]>, E[NestedKey]> | null
: InferResult<NonNullable<NonNullable<T[K]>[NestedKey]>, E[NestedKey]>
: never
: never;
}
: E extends keyof NonNullable<T[K]>
? { [P in E]: NonNullable<T[K]>[P] }
: never
: never;
}[number]
>
: never
: T[K] extends { __type: "Union"; __primitiveFields: any }
? T[K] extends { __array: true }
? Field[K] extends any[]
? null extends T[K]
? Array<InferUnionFieldValue<T[K], Field[K]>> | null
: Array<InferUnionFieldValue<T[K], Field[K]>>
: never
: Field[K] extends any[]
? null extends T[K]
? InferUnionFieldValue<T[K], Field[K]> | null
: InferUnionFieldValue<T[K], Field[K]>
: never
: NonNullable<T[K]> extends TypedSchema
? null extends T[K]
? InferResult<NonNullable<T[K]>, Field[K]> | null
: InferResult<NonNullable<T[K]>, Field[K]>
: never
: never;
}
: never;
export type InferResult<
T extends TypedSchema,
SelectedFields extends UnifiedFieldSelection<T>[] | undefined,
> = SelectedFields extends undefined
? {}
: SelectedFields extends []
? {}
: SelectedFields extends UnifiedFieldSelection<T>[]
? UnionToIntersection<
{
[K in keyof SelectedFields]: InferFieldValue<T, SelectedFields[K]>;
}[number]
>
: {};
// Pagination conditional types
// Checks if a page configuration object has any pagination parameters
export type HasPaginationParams<Page> =
Page extends { offset: any } ? true :
Page extends { after: any } ? true :
Page extends { before: any } ? true :
false;
// Infer which pagination type is being used from the page config
export type InferPaginationType<Page> =
Page extends { offset: any } ? "offset" :
Page extends { after: any } | { before: any } ? "keyset" :
never;
// Returns either non-paginated (array) or paginated result based on page params
// For single pagination type support (offset-only or keyset-only)
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type ConditionalPaginatedResult<
Page,
RecordType,
PaginatedType
> = Page extends undefined
? RecordType
: HasPaginationParams<Page> extends true
? PaginatedType
: RecordType;
// For actions supporting both offset and keyset pagination
// Infers the specific pagination type based on which params were passed
export type ConditionalPaginatedResultMixed<
Page,
RecordType,
OffsetType,
KeysetType
> = Page extends undefined
? RecordType
: HasPaginationParams<Page> extends true
? InferPaginationType<Page> extends "offset"
? OffsetType
: InferPaginationType<Page> extends "keyset"
? KeysetType
: OffsetType | KeysetType // Fallback to union if can't determine
: RecordType;
export type SuccessDataFunc<T extends (...args: any[]) => Promise<any>> = Extract<
Awaited<ReturnType<T>>,
{ success: true }
>["data"];
export type ErrorData<T extends (...args: any[]) => Promise<any>> = Extract<
Awaited<ReturnType<T>>,
{ success: false }
>["errors"];
/**
* Represents an error from an unsuccessful RPC call.
*
* This type matches the error structure defined in the AshTypescript.Rpc.Error protocol.
*
* @example
* const error: AshRpcError = {
* type: "invalid_changes",
* message: "Invalid value for field %{field}",
* shortMessage: "Invalid changes",
* vars: { field: "email" },
* fields: ["email"],
* path: ["user", "email"],
* details: { suggestion: "Provide a valid email address" }
* }
*/
export type AshRpcError = {
/** Machine-readable error type (e.g., "invalid_changes", "not_found") */
type: string;
/** Full error message (may contain template variables like %{key}) */
message: string;
/** Concise version of the message */
shortMessage: string;
/** Variables to interpolate into the message template */
vars: Record<string, any>;
/** List of affected field names (for field-level errors) */
fields: string[];
/** Path to the error location in the data structure */
path: string[];
/** Optional map with extra details (e.g., suggestions, hints) */
details?: Record<string, any>;
}
/**
* Represents the result of a validation RPC call.
*
* All validation actions return this same structure, indicating either
* successful validation or a list of validation errors.
*
* @example
* // Successful validation
* const result: ValidationResult = { success: true };
*
* // Failed validation
* const result: ValidationResult = {
* success: false,
* errors: [
* {
* type: "required",
* message: "is required",
* shortMessage: "Required field",
* vars: { field: "email" },
* fields: ["email"],
* path: []
* }
* ]
* };
*/
export type ValidationResult =
| { success: true }
| { success: false; errors: AshRpcError[]; };

86
assets/js/index.tsx Normal file
View File

@@ -0,0 +1,86 @@
import React, { useEffect } from "react";
import { createRoot } from "react-dom/client";
import { initLandingPage } from "./animation";
function App() {
useEffect(() => {
const el = document.getElementById("animation-container");
if (el) return initLandingPage(el);
}, []);
return (
<div className="min-h-screen bg-base-100 text-base-content">
<div className="max-w-5xl mx-auto px-6 py-12">
<div className="flex items-center gap-5 mb-8">
<img
src="https://raw.githubusercontent.com/ash-project/ash_typescript/main/logos/ash-typescript.png"
alt="AshTypescript"
className="w-16 h-16"
/>
<div>
<h1 className="text-4xl font-bold">AshTypescript</h1>
<p className="text-lg opacity-70">End-to-end type safety from Ash to TypeScript</p>
</div>
</div>
<section className="mb-10">
<div className="flex flex-wrap items-center gap-3 mb-5">
<h2 className="text-2xl font-bold">Main Features</h2>
<div className="flex-1"></div>
<a href="https://hexdocs.pm/ash_typescript" target="_blank" rel="noopener noreferrer" className="btn btn-primary btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
Docs
</a>
<a href="https://github.com/ash-project/ash_typescript" target="_blank" rel="noopener noreferrer" className="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
GitHub
</a>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
<a href="https://hexdocs.pm/ash_typescript/first-rpc-action.html" target="_blank" rel="noopener noreferrer" className="card bg-base-200 hover:bg-base-300 transition-colors cursor-pointer">
<div className="card-body">
<h3 className="card-title text-base">Type-Safe RPC</h3>
<p className="text-sm opacity-70">Auto-generated typed functions for every Ash action.</p>
<div className="text-sm text-primary mt-1">View docs &rarr;</div>
</div>
</a>
<a href="https://hexdocs.pm/ash_typescript/typed-controllers.html" target="_blank" rel="noopener noreferrer" className="card bg-base-200 hover:bg-base-300 transition-colors cursor-pointer">
<div className="card-body">
<h3 className="card-title text-base">Typed Controllers</h3>
<p className="text-sm opacity-70">Typed route helpers for Phoenix controllers.</p>
<div className="text-sm text-primary mt-1">View docs &rarr;</div>
</div>
</a>
<a href="https://hexdocs.pm/ash_typescript/typed-channels.html" target="_blank" rel="noopener noreferrer" className="card bg-base-200 hover:bg-base-300 transition-colors cursor-pointer">
<div className="card-body">
<h3 className="card-title text-base">Typed Channels</h3>
<p className="text-sm opacity-70">Typed event subscriptions for Phoenix channels.</p>
<div className="text-sm text-primary mt-1">View docs &rarr;</div>
</div>
</a>
<a href="https://hexdocs.pm/ash_typescript/form-validation.html" target="_blank" rel="noopener noreferrer" className="card bg-base-200 hover:bg-base-300 transition-colors cursor-pointer">
<div className="card-body">
<h3 className="card-title text-base">Zod Validation</h3>
<p className="text-sm opacity-70">Generated Zod schemas for form validation.</p>
<div className="text-sm text-primary mt-1">View docs &rarr;</div>
</div>
</a>
</div>
</section>
<div id="animation-container"></div>
</div>
</div>
);
}
createRoot(document.getElementById("app")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

145
assets/package-lock.json generated Normal file
View File

@@ -0,0 +1,145 @@
{
"name": "assets",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"topbar": "^3.0.0"
},
"devDependencies": {
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9"
}
},
"../deps/phoenix": {
"version": "1.8.5",
"license": "MIT",
"devDependencies": {
"@babel/cli": "7.28.6",
"@babel/core": "7.29.0",
"@babel/preset-env": "7.29.0",
"@eslint/js": "^10.0.1",
"@stylistic/eslint-plugin": "^5.0.0",
"documentation": "^14.0.3",
"eslint": "10.0.2",
"eslint-plugin-jest": "29.15.0",
"jest": "^30.0.0",
"jest-environment-jsdom": "^30.0.0",
"jsdom": "^28.1.0",
"mock-socket": "^9.3.1"
}
},
"../deps/phoenix_html": {
"version": "4.3.0"
},
"../deps/phoenix_live_view": {
"version": "1.1.28",
"license": "MIT",
"dependencies": {
"morphdom": "2.7.8"
},
"devDependencies": {
"@babel/cli": "7.27.2",
"@babel/core": "7.27.4",
"@babel/preset-env": "7.27.2",
"@babel/preset-typescript": "^7.27.1",
"@eslint/js": "^9.29.0",
"@playwright/test": "^1.56.1",
"@types/jest": "^30.0.0",
"@types/phoenix": "^1.6.6",
"css.escape": "^1.5.1",
"eslint": "9.29.0",
"eslint-plugin-jest": "28.14.0",
"eslint-plugin-playwright": "^2.2.0",
"globals": "^16.2.0",
"jest": "^30.0.0",
"jest-environment-jsdom": "^30.0.0",
"jest-monocart-coverage": "^1.1.1",
"monocart-reporter": "^2.9.21",
"phoenix": "1.7.21",
"prettier": "3.5.3",
"ts-jest": "^29.4.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.34.0"
}
},
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/phoenix": {
"resolved": "../deps/phoenix",
"link": true
},
"node_modules/phoenix_html": {
"resolved": "../deps/phoenix_html",
"link": true
},
"node_modules/phoenix_live_view": {
"resolved": "../deps/phoenix_live_view",
"link": true
},
"node_modules/react": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.4"
}
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/topbar": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/topbar/-/topbar-3.0.1.tgz",
"integrity": "sha512-3CFWdkUC4KLsyLpW8IcX5EXskFtJtNl9O2/qNO/4Ncd/lWgipY/pmyv0D8bw2bTepl/+MUGhNhu2n3IitUuQNA==",
"license": "MIT"
}
}
}

14
assets/package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"dependencies": {
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"topbar": "^3.0.0"
},
"devDependencies": {
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9"
}
}

34
assets/tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
// This file is needed on most editors to enable the intelligent autocompletion
// of LiveView's JavaScript API methods. You can safely delete it if you don't need it.
//
// Note: This file assumes a basic esbuild setup without node_modules.
// We include a generic paths alias to deps to mimic how esbuild resolves
// the Phoenix and LiveView JavaScript assets.
// If you have a package.json in your project, you should remove the
// paths configuration and instead add the phoenix dependencies to the
// dependencies section of your package.json:
//
// {
// ...
// "dependencies": {
// ...,
// "phoenix": "../deps/phoenix",
// "phoenix_html": "../deps/phoenix_html",
// "phoenix_live_view": "../deps/phoenix_live_view"
// }
// }
//
// Feel free to adjust this configuration however you need.
{
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"*": ["../deps/*"]
},
"allowJs": true,
"noEmit": true
},
"include": ["js/**/*"]
}

124
assets/vendor/daisyui-theme.js vendored Normal file

File diff suppressed because one or more lines are too long

1031
assets/vendor/daisyui.js vendored Normal file

File diff suppressed because one or more lines are too long

43
assets/vendor/heroicons.js vendored Normal file
View File

@@ -0,0 +1,43 @@
const plugin = require("tailwindcss/plugin")
const fs = require("fs")
const path = require("path")
module.exports = plugin(function({matchComponents, theme}) {
let iconsDir = path.join(__dirname, "../../deps/heroicons/optimized")
let values = {}
let icons = [
["", "/24/outline"],
["-solid", "/24/solid"],
["-mini", "/20/solid"],
["-micro", "/16/solid"]
]
icons.forEach(([suffix, dir]) => {
fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
let name = path.basename(file, ".svg") + suffix
values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
})
})
matchComponents({
"hero": ({name, fullPath}) => {
let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
content = encodeURIComponent(content)
let size = theme("spacing.6")
if (name.endsWith("-mini")) {
size = theme("spacing.5")
} else if (name.endsWith("-micro")) {
size = theme("spacing.4")
}
return {
[`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
"-webkit-mask": `var(--hero-${name})`,
"mask": `var(--hero-${name})`,
"mask-repeat": "no-repeat",
"background-color": "currentColor",
"vertical-align": "middle",
"display": "inline-block",
"width": size,
"height": size
}
}
}, {values})
})