release: WRNexusJS 0.3.5
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ai",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/authz",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -95,6 +95,8 @@ Thumbs.db
|
||||
"devDependencies": {
|
||||
"@wrnexus/cli": "${frameworkVersion}",
|
||||
"@eslint/js": "^9.0.0",
|
||||
"@iconify-json/lucide": "^1.2.118",
|
||||
"@iconify/tailwind4": "^1.2.3",
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"@types/bun": "latest",
|
||||
"eslint": "^9.0.0",
|
||||
@@ -292,6 +294,7 @@ Allow: /
|
||||
* @source tells Tailwind which files to scan for class names.
|
||||
*/
|
||||
@import "tailwindcss";
|
||||
@plugin "@iconify/tailwind4";
|
||||
@source "../**/*.wrn";
|
||||
@source "../**/*.tsx";
|
||||
|
||||
@@ -309,6 +312,29 @@ body {
|
||||
Roboto,
|
||||
sans-serif;
|
||||
}
|
||||
`,
|
||||
"app/layouts/document.wrn": `// Global document layout. The framework renders this once around the selected
|
||||
// page layout and merges SEO metadata, styles, and scripts into <head>/<body>.
|
||||
// Request cookies, resolved theme, language, URL, and pathname are available
|
||||
// as SSR props, so document attributes do not need a client-side correction.
|
||||
layout Document {
|
||||
props {
|
||||
cookies = {}
|
||||
theme = "light"
|
||||
language = "en"
|
||||
url = ""
|
||||
pathname = "/"
|
||||
}
|
||||
|
||||
view {
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<div id="app"><slot /></div>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
`,
|
||||
"app/pages/index.wrn": `// Home page (route: /). SSR-first: the view is server-rendered, then components
|
||||
// (.wrn files under app/components) hydrate in the browser. Styled with Tailwind.
|
||||
|
||||
@@ -985,6 +985,14 @@ const MIGRATIONS: Migration[] = [
|
||||
// Compiler-only fix. Existing WRN source files require no migration.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.3.5",
|
||||
id: "new-component-added",
|
||||
description: "UI component Added.",
|
||||
apply() {
|
||||
// Compiler-only fix. Existing WRN source files require no migration.
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** Release tooling uses this to require an explicit migration entry per version. */
|
||||
@@ -1023,21 +1031,34 @@ function appVersion(appRoot: string, pkg: Record<string, unknown>): string {
|
||||
}
|
||||
|
||||
/** Refresh pure framework-owned reference files. Never touches user-edited CLAUDE.md. */
|
||||
function refreshFrameworkFiles(appRoot: string, dryRun: boolean, log: (m: string) => void): void {
|
||||
// Public llms.txt is generated and web-visible, so it is safe to refresh.
|
||||
const llms = join(appRoot, "public", "llms.txt");
|
||||
if (!existsSync(llms) || readFileSync(llms, "utf8") !== AI_GUIDE) {
|
||||
log("~ public/llms.txt refreshed");
|
||||
function refreshFrameworkFiles(
|
||||
appRoot: string,
|
||||
dryRun: boolean,
|
||||
log: (message: string) => void,
|
||||
): void {
|
||||
const publicDir = join(appRoot, "public");
|
||||
const llms = join(publicDir, "llms.txt");
|
||||
|
||||
if (!existsSync(llms)) {
|
||||
log("+ public/llms.txt created");
|
||||
|
||||
if (!dryRun) {
|
||||
mkdirSync(join(appRoot, "public"), { recursive: true });
|
||||
mkdirSync(publicDir, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
writeFileSync(llms, AI_GUIDE, "utf8");
|
||||
}
|
||||
}
|
||||
// CLAUDE.md is often user-edited — only create it when absent.
|
||||
|
||||
const claude = join(appRoot, "CLAUDE.md");
|
||||
|
||||
if (!existsSync(claude)) {
|
||||
log("+ CLAUDE.md created");
|
||||
if (!dryRun) writeFileSync(claude, CLAUDE_MD, "utf8");
|
||||
|
||||
if (!dryRun) {
|
||||
writeFileSync(claude, CLAUDE_MD, "utf8");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,11 @@ test("scaffoldApp pins the current framework and exposes llms.txt publicly", ()
|
||||
expect(dependency).toBe(version);
|
||||
}
|
||||
expect(pkg.devDependencies["@wrnexus/cli"]).toBe(version);
|
||||
expect(pkg.devDependencies["@iconify/tailwind4"]).toBe("^1.2.3");
|
||||
expect(pkg.devDependencies["@iconify-json/lucide"]).toBe("^1.2.118");
|
||||
expect(readFileSync(join(root, "app", "styles", "global.css"), "utf8")).toContain(
|
||||
'@plugin "@iconify/tailwind4";',
|
||||
);
|
||||
expect(existsSync(join(root, "public", "llms.txt"))).toBe(true);
|
||||
expect(existsSync(join(root, "llms.txt"))).toBe(false);
|
||||
expect(readFileSync(join(root, ".prettierignore"), "utf8")).toContain("CLAUDE.md");
|
||||
|
||||
@@ -163,6 +163,7 @@ A file opens with `page <Name>` or `component <Name>` followed by a `{ ... }` bo
|
||||
- `layout = "<name>"` — selects `app/layouts/<name>.wrn` (pages only).
|
||||
- `types { <TypeScript declarations> }` — reusable interfaces and aliases for the current file.
|
||||
- `props { name: Type = <default> ... }` — typed component props. Omit `= <default>` to make a prop required. Legacy inferred props remain supported.
|
||||
- `@event name = function` inside `props` — declares a public component event. Emit it from component behavior with `name(detail)` or `$emit("name", detail)`, and consume it with `<Component @name="handler(event)" />`.
|
||||
- `state <ident>: Type = <expr>` — typed reactive state seeded from a raw JS expression. The annotation is optional for backward compatibility.
|
||||
- `view { <html> }` — plain HTML with `{expr}` interpolation in text and attributes, hyphenated attributes, boolean attributes, `@event="..."` client bindings, and `<!-- comments -->`. Attribute expressions that reference `state` keep an SSR value and update reactively in the browser.
|
||||
- `seo { key = "value" ... }` — metadata merged into the generated `meta`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -43,6 +43,38 @@ function isComponentTag(tag: string): boolean {
|
||||
return /^[A-Z][A-Za-z0-9_$]*$/.test(tag);
|
||||
}
|
||||
|
||||
const HTML_BOOLEAN_ATTRIBUTES = new Set([
|
||||
"allowfullscreen",
|
||||
"async",
|
||||
"autofocus",
|
||||
"autoplay",
|
||||
"checked",
|
||||
"controls",
|
||||
"default",
|
||||
"defer",
|
||||
"disabled",
|
||||
"formnovalidate",
|
||||
"hidden",
|
||||
"inert",
|
||||
"ismap",
|
||||
"itemscope",
|
||||
"loop",
|
||||
"multiple",
|
||||
"muted",
|
||||
"nomodule",
|
||||
"novalidate",
|
||||
"open",
|
||||
"playsinline",
|
||||
"readonly",
|
||||
"required",
|
||||
"reversed",
|
||||
"selected",
|
||||
]);
|
||||
|
||||
function isHtmlBooleanAttribute(name: string): boolean {
|
||||
return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
|
||||
}
|
||||
|
||||
/** Escape a value placed inside a double-quoted HTML attribute. */
|
||||
function attrEscape(value: string): string {
|
||||
return value
|
||||
@@ -116,13 +148,29 @@ function reactiveAttrValue(raw: string, reactive: PageReactive): string | null {
|
||||
return found ? value : null;
|
||||
}
|
||||
|
||||
function renderAttrs(attrs: Attr[], csrId?: string, reactive: PageReactive | null = null): string {
|
||||
function renderAttrs(
|
||||
attrs: Attr[],
|
||||
csrId?: string,
|
||||
reactive: PageReactive | null = null,
|
||||
dynamicExpressions?: string[],
|
||||
): string {
|
||||
let bindIndex = 0;
|
||||
const rendered = attrs
|
||||
.map((attr) => {
|
||||
const base = renderAttr(attr);
|
||||
if (!reactive || attr.event || attr.boolean || !base || !attr.value.includes("{"))
|
||||
return base;
|
||||
const expression = wholeAttributeExpression(attr.value);
|
||||
if (
|
||||
expression &&
|
||||
exprRefsState(expression, reactive.runtimeStateNames) &&
|
||||
dynamicExpressions
|
||||
) {
|
||||
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
|
||||
const sentinel = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
|
||||
const marker = JSON.stringify([attr.name, attr.value]);
|
||||
return ` ${attr.name}="${sentinel}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`;
|
||||
}
|
||||
const initial = reactiveAttrValue(attr.value, reactive);
|
||||
if (initial === null) return base;
|
||||
const marker = JSON.stringify([attr.name, attr.value]);
|
||||
@@ -151,6 +199,7 @@ function htmlTextEscape(value: string): string {
|
||||
/** Reactive page context: state names + their initial (SSR) values. */
|
||||
interface PageReactive {
|
||||
stateNames: Set<string>;
|
||||
runtimeStateNames: Set<string>;
|
||||
scope: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -401,7 +450,7 @@ function renderNode(
|
||||
|
||||
// Void elements (<br>, <img>, …) have no closing tag and no children.
|
||||
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
||||
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>`;
|
||||
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive, loops)}>`;
|
||||
}
|
||||
|
||||
const inner =
|
||||
@@ -420,7 +469,7 @@ function renderNode(
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>${inner}</${node.tag}>`;
|
||||
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive, loops)}>${inner}</${node.tag}>`;
|
||||
}
|
||||
|
||||
function renderPageComponentInvocation(
|
||||
@@ -452,6 +501,11 @@ function renderNestedComponentInvocation(
|
||||
const attrs = node.attrs
|
||||
.filter((attr) => attr.name !== "data-component")
|
||||
.map((attr) => {
|
||||
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
|
||||
if (spread) {
|
||||
return `\${__wireSpreadAttrs(${ctx.resolveExpr(spread[1]!)})}`;
|
||||
}
|
||||
|
||||
if (attr.event) {
|
||||
return (
|
||||
escLit(` ${eventAttribute(attr.name)}="`) + escLit(attrEscape(attr.value)) + escLit(`"`)
|
||||
@@ -490,13 +544,18 @@ function renderNestedComponentInvocation(
|
||||
loops.length > 0
|
||||
? {
|
||||
...ctx,
|
||||
forwardRestAttrs: false,
|
||||
loopVars: new Set([...(ctx.loopVars ?? []), ...loops]),
|
||||
}
|
||||
: ctx;
|
||||
: { ...ctx, forwardRestAttrs: false };
|
||||
|
||||
const inner = node.children.map((child) => renderComponentNode(child, childCtx)).join("");
|
||||
|
||||
return `<div data-component="${attrEscape(node.tag)}"${attrs}>${inner}</div>`;
|
||||
return (
|
||||
`<div data-component="${attrEscape(node.tag)}"` +
|
||||
`${ctx.forwardRestAttrs ? "${__wireSpreadAttrs(__attrs)}" : ""}` +
|
||||
`${attrs}>${inner}</div>`
|
||||
);
|
||||
}
|
||||
|
||||
function ssrMarker(bindings: SsrBinding[], binding: RenderBinding): string {
|
||||
@@ -668,6 +727,7 @@ function hydrationId(ast: PageAst): string {
|
||||
kind: ast.kind,
|
||||
name: ast.name,
|
||||
props: ast.props.map((entry) => entry.name),
|
||||
events: ast.events.map((entry) => entry.name),
|
||||
states: ast.states.map((entry) => entry.name),
|
||||
computed: ast.computed.map((entry) => entry.name),
|
||||
view: ast.view,
|
||||
@@ -744,8 +804,13 @@ export function generate(ast: PageAst): string {
|
||||
...ast.states.map((entry) => entry.name),
|
||||
...ast.computed.map((entry) => entry.name),
|
||||
];
|
||||
const runtimeStateNames = new Set(
|
||||
ast.states.filter((entry) => /\bctx\b/.test(entry.expr)).map((entry) => entry.name),
|
||||
);
|
||||
const reactive: PageReactive | null =
|
||||
reactiveNames.length > 0 ? { stateNames: new Set(reactiveNames), scope: seedScope } : null;
|
||||
reactiveNames.length > 0
|
||||
? { stateNames: new Set(reactiveNames), runtimeStateNames, scope: seedScope }
|
||||
: null;
|
||||
const loops: string[] = [];
|
||||
let html = ast.view
|
||||
.map((node) => renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive))
|
||||
@@ -955,6 +1020,10 @@ interface CompCtx {
|
||||
loopVars?: Set<string>;
|
||||
/** Local identifiers introduced by server-rendered `{#each}` blocks. */
|
||||
serverLocals?: Set<string>;
|
||||
/** Forward undeclared component attributes to this element only. */
|
||||
forwardRestAttrs?: boolean;
|
||||
/** Public component events exposed from the component root. */
|
||||
eventNames?: string[];
|
||||
}
|
||||
|
||||
interface ComponentBehavior {
|
||||
@@ -1138,6 +1207,22 @@ function viewHasServerEach(nodes: ViewNode[]): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
function viewHasRestAttributeSpread(nodes: ViewNode[]): boolean {
|
||||
return nodes.some((node) => {
|
||||
if (node.type === "text") return false;
|
||||
if (node.type === "each") {
|
||||
return viewHasRestAttributeSpread(node.body) || viewHasRestAttributeSpread(node.empty);
|
||||
}
|
||||
if (node.type === "if") {
|
||||
return node.branches.some((branch) => viewHasRestAttributeSpread(branch.body));
|
||||
}
|
||||
return (
|
||||
node.attrs.some((attr) => /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.test(attr.name)) ||
|
||||
viewHasRestAttributeSpread(node.children)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a text node. Interpolations that reference state stay as client
|
||||
* mustaches (`{expr}`, hydrated by the reactive runtime); interpolations of
|
||||
@@ -1318,13 +1403,13 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
|
||||
const loopVariables = loopVarsOf(node);
|
||||
|
||||
const elementContext =
|
||||
loopVariables.length > 0
|
||||
? {
|
||||
...ctx,
|
||||
loopVars: new Set([...(ctx.loopVars ?? []), ...loopVariables]),
|
||||
}
|
||||
: ctx;
|
||||
const elementContext = {
|
||||
...ctx,
|
||||
forwardRestAttrs: false,
|
||||
...(loopVariables.length > 0
|
||||
? { loopVars: new Set([...(ctx.loopVars ?? []), ...loopVariables]) }
|
||||
: {}),
|
||||
};
|
||||
|
||||
let bindIndex = 0;
|
||||
const staticClasses: string[] = [];
|
||||
@@ -1349,6 +1434,11 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
const attrs = node.attrs
|
||||
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
|
||||
.map((a) => {
|
||||
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
|
||||
if (spread) {
|
||||
return `\${__wireSpreadAttrs(${elementContext.resolveExpr(spread[1]!)})}`;
|
||||
}
|
||||
|
||||
if (a.event) {
|
||||
return ` ${eventAttribute(a.name)}="${escLit(attrEscape(a.value))}"`;
|
||||
}
|
||||
@@ -1357,6 +1447,29 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
return ` ${a.name}`;
|
||||
}
|
||||
|
||||
if (isHtmlBooleanAttribute(a.name)) {
|
||||
const expression = wholeAttributeExpression(a.value);
|
||||
if (expression) {
|
||||
const referencesState = exprRefsState(a.value, ctx.stateNames);
|
||||
const referencesLoopVariable = elementContext.loopVars
|
||||
? exprRefsState(a.value, elementContext.loopVars)
|
||||
: false;
|
||||
const referencesServerLocal = ctx.serverLocals
|
||||
? exprRefsState(a.value, ctx.serverLocals)
|
||||
: false;
|
||||
const marker =
|
||||
referencesState || referencesLoopVariable || referencesServerLocal
|
||||
? ` data-wrn-bind-${bindIndex++}="${escLit(
|
||||
attrEscape(JSON.stringify([a.name, a.value])),
|
||||
)}"`
|
||||
: "";
|
||||
return `\${__wireBooleanAttr(${JSON.stringify(a.name)}, ${elementContext.resolveExpr(expression)})}${marker}`;
|
||||
}
|
||||
|
||||
if (a.value === "false") return "";
|
||||
if (a.value === "true" || a.value === "") return ` ${a.name}`;
|
||||
}
|
||||
|
||||
const rendered = ` ${a.name}="${compileAttrValue(a.value, elementContext)}"`;
|
||||
|
||||
const referencesState = exprRefsState(a.value, ctx.stateNames);
|
||||
@@ -1433,6 +1546,10 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
|
||||
const allAttrs =
|
||||
`${loopLocalsAttribute}` +
|
||||
`${ctx.forwardRestAttrs ? "${__wireSpreadAttrs(__attrs)}" : ""}` +
|
||||
`${
|
||||
ctx.eventNames?.length ? ` data-wrn-events="${attrEscape(ctx.eventNames.join(","))}"` : ""
|
||||
}` +
|
||||
`${classAttribute}` +
|
||||
`${classReactiveBinding}` +
|
||||
`${classBindings}` +
|
||||
@@ -1477,6 +1594,9 @@ function generateComponent(ast: PageAst): string {
|
||||
for (const p of effectiveProps) {
|
||||
nameRefs.set(p.name, safeRef(p.name));
|
||||
}
|
||||
if (!nameRefs.has("attrs")) {
|
||||
nameRefs.set("attrs", "__attrs");
|
||||
}
|
||||
for (const s of ast.states) nameRefs.set(s.name, safeRef(s.name));
|
||||
for (const entry of ast.computed) nameRefs.set(entry.name, safeRef(entry.name));
|
||||
const resolveExpr = (expr: string): string => {
|
||||
@@ -1486,14 +1606,33 @@ function generateComponent(ast: PageAst): string {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const ctx: CompCtx = { stateNames, resolveExpr };
|
||||
const ctx: CompCtx = {
|
||||
stateNames,
|
||||
resolveExpr,
|
||||
eventNames: ast.events.map((event) => event.name),
|
||||
};
|
||||
|
||||
const serverFunctions = ast.functions
|
||||
.map((body) => body.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
|
||||
const viewCode = ast.view.map((node) => renderComponentNode(node, ctx)).join("");
|
||||
const hasExplicitRestSpread = viewHasRestAttributeSpread(ast.view);
|
||||
const rootElementIndex = ast.view.findIndex((node) => node.type === "element");
|
||||
const automaticallyForwardRootAttrs =
|
||||
!hasExplicitRestSpread &&
|
||||
!effectiveProps.some((prop) => prop.name === "attrs") &&
|
||||
rootElementIndex >= 0;
|
||||
const viewCode = ast.view
|
||||
.map((node, index) =>
|
||||
renderComponentNode(
|
||||
node,
|
||||
automaticallyForwardRootAttrs && index === rootElementIndex
|
||||
? { ...ctx, forwardRestAttrs: true }
|
||||
: ctx,
|
||||
),
|
||||
)
|
||||
.join("");
|
||||
const styles = ast.styles.map((body) => body.trim()).filter(Boolean);
|
||||
const styleTag =
|
||||
styles.length > 0
|
||||
@@ -1534,6 +1673,11 @@ function generateComponent(ast: PageAst): string {
|
||||
` const ${nameRefs.get(prop.name)}: ${prop.valueType ?? "any"} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}), ${JSON.stringify(runtimeTypeOf(prop.valueType))});`,
|
||||
);
|
||||
}
|
||||
if (!effectiveProps.some((prop) => prop.name === "attrs")) {
|
||||
decls.push(
|
||||
` const __attrs = __restProps(__p, new Set(${JSON.stringify(effectiveProps.map((prop) => prop.name))}));`,
|
||||
);
|
||||
}
|
||||
for (const state of ast.states) {
|
||||
decls.push(
|
||||
` let ${nameRefs.get(state.name)}${state.valueType ? `: ${state.valueType}` : ""} = (${resolveExpr(state.expr)});`,
|
||||
@@ -1584,7 +1728,7 @@ function generateComponent(ast: PageAst): string {
|
||||
|
||||
if (effectiveProps.length > 0) {
|
||||
out.push(
|
||||
`export interface ${ast.name}Props {\n${effectiveProps
|
||||
`export interface ${ast.name}Props {\n [attribute: string]: unknown;\n${effectiveProps
|
||||
.map(
|
||||
(prop) =>
|
||||
` ${JSON.stringify(prop.name)}${prop.required ? "" : "?"}: ${prop.valueType ?? "unknown"};`,
|
||||
@@ -1664,6 +1808,15 @@ function generateComponent(ast: PageAst): string {
|
||||
return declared === "unknown" && def === undefined ? v : String(v);
|
||||
}
|
||||
|
||||
function __restProps(
|
||||
props: Record<string, any>,
|
||||
declared: Set<string>,
|
||||
): Record<string, any> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(props).filter(([name]) => !declared.has(name)),
|
||||
);
|
||||
}
|
||||
|
||||
function __wireHtml(v: any): string {
|
||||
return String(v == null ? "" : v).replace(
|
||||
/[&<>]/g,
|
||||
@@ -1690,6 +1843,48 @@ function __wireAttr(v: any): string {
|
||||
);
|
||||
}
|
||||
|
||||
function __wireBooleanAttr(name: string, value: any): string {
|
||||
return value === true ||
|
||||
value === "true" ||
|
||||
value === "" ||
|
||||
value === 1 ||
|
||||
value === "1" ||
|
||||
value === name
|
||||
? " " + name
|
||||
: "";
|
||||
}
|
||||
|
||||
function __wireSpreadAttrs(value: any): string {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
||||
|
||||
const booleanAttributes = new Set(${JSON.stringify([...HTML_BOOLEAN_ATTRIBUTES])});
|
||||
const attributes: string[] = [];
|
||||
|
||||
for (const [name, raw] of Object.entries(value)) {
|
||||
const lowerName = name.toLowerCase();
|
||||
if (
|
||||
!/^[A-Za-z_:][A-Za-z0-9_.:-]*$/.test(name) ||
|
||||
lowerName.startsWith("on") ||
|
||||
lowerName === "style" ||
|
||||
lowerName === "slot" ||
|
||||
lowerName === "data-component" ||
|
||||
lowerName.startsWith("data-wrn")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (booleanAttributes.has(lowerName)) {
|
||||
attributes.push(__wireBooleanAttr(name, raw));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (raw === false || raw === null || raw === undefined) continue;
|
||||
attributes.push(" " + name + '="' + __wireAttr(raw) + '"');
|
||||
}
|
||||
|
||||
return attributes.join("");
|
||||
}
|
||||
|
||||
function __wireProp(v: any): string {
|
||||
const value =
|
||||
v !== null && typeof v === "object"
|
||||
|
||||
@@ -38,6 +38,7 @@ export type {
|
||||
DataApiBlock,
|
||||
DataMode,
|
||||
EffectBlock,
|
||||
EventDecl,
|
||||
LoadBlock,
|
||||
ModeFunctionsBlock,
|
||||
PageAst,
|
||||
|
||||
@@ -80,6 +80,24 @@ test("parses a component with props and state", () => {
|
||||
expect(ast.states.map((s) => s.name)).toEqual(["count"]);
|
||||
});
|
||||
|
||||
test("component event declarations compile to public root metadata", async () => {
|
||||
const source = `component EventButton {
|
||||
props {
|
||||
label = "Save"
|
||||
@event complete = function
|
||||
}
|
||||
view { <button>{label}</button> }
|
||||
}`;
|
||||
const ast = parse(source);
|
||||
const output = generate(ast);
|
||||
const mod = await compileAndImport(source);
|
||||
const render = mod.render as (props: Record<string, unknown>) => string;
|
||||
|
||||
expect(ast.events).toEqual([{ name: "complete" }]);
|
||||
expect(output).toContain('data-wrn-events="complete"');
|
||||
expect(render({})).toContain('data-wrn-events="complete"');
|
||||
});
|
||||
|
||||
test("typed props, required props, state, custom types, and function parameters compile", () => {
|
||||
const source = `component TypedPicker {
|
||||
types {
|
||||
@@ -169,6 +187,89 @@ test("HTML view: void elements, boolean attrs, comments, lone <", () => {
|
||||
void ast;
|
||||
});
|
||||
|
||||
test("dynamic HTML boolean attributes are omitted when false", async () => {
|
||||
const mod = await compileAndImport(`component BooleanAttributes {
|
||||
props {
|
||||
disabled = false
|
||||
checked = false
|
||||
required = false
|
||||
loading = false
|
||||
}
|
||||
view {
|
||||
<button disabled="{disabled || loading}">Save</button>
|
||||
<input checked="{checked}" required="{required}" />
|
||||
}
|
||||
}`);
|
||||
const render = mod.render as (props?: Record<string, unknown>) => string;
|
||||
|
||||
const enabled = render();
|
||||
expect(enabled).not.toContain(" disabled");
|
||||
expect(enabled).not.toContain(" checked");
|
||||
expect(enabled).not.toContain(" required");
|
||||
|
||||
const disabled = render({ disabled: "true", checked: "true", required: "true" });
|
||||
expect(disabled).toContain("<button disabled>");
|
||||
expect(disabled).toContain("<input checked required>");
|
||||
});
|
||||
|
||||
test("components safely forward undeclared HTML attributes to their root", async () => {
|
||||
const mod = await compileAndImport(`component ForwardingButton {
|
||||
props {
|
||||
label = "Button"
|
||||
variant = "default"
|
||||
disabled = false
|
||||
}
|
||||
view {
|
||||
<button disabled="{disabled}">{label}</button>
|
||||
}
|
||||
}`);
|
||||
const render = mod.render as (props?: Record<string, unknown>) => string;
|
||||
const html = render({
|
||||
label: "Save",
|
||||
variant: "primary",
|
||||
formaction: "/submit-form",
|
||||
formenctype: "application/x-www-form-urlencoded",
|
||||
formmethod: "post",
|
||||
popovertarget: "myPopover",
|
||||
"aria-describedby": "save-help",
|
||||
"data-testid": "save",
|
||||
onclick: "alert(1)",
|
||||
style: "display:none",
|
||||
"data-wrn-bind-0": "unsafe",
|
||||
});
|
||||
|
||||
expect(html).toContain('formaction="/submit-form"');
|
||||
expect(html).toContain('formenctype="application/x-www-form-urlencoded"');
|
||||
expect(html).toContain('formmethod="post"');
|
||||
expect(html).toContain('popovertarget="myPopover"');
|
||||
expect(html).toContain('aria-describedby="save-help"');
|
||||
expect(html).toContain('data-testid="save"');
|
||||
expect(html).not.toContain("variant=");
|
||||
expect(html).not.toContain("onclick=");
|
||||
expect(html).not.toContain("style=");
|
||||
expect(html).not.toContain("data-wrn-bind");
|
||||
});
|
||||
|
||||
test("an explicit attrs spread overrides automatic root forwarding", async () => {
|
||||
const mod = await compileAndImport(`component WrappedControl {
|
||||
props {
|
||||
label = "Control"
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<span class="{class}">
|
||||
<button {...attrs}>{label}</button>
|
||||
</span>
|
||||
}
|
||||
}`);
|
||||
const render = mod.render as (props?: Record<string, unknown>) => string;
|
||||
const html = render({ label: "Save", class: "wrapper", formaction: "/save" });
|
||||
|
||||
expect(html).toContain('<span class="wrapper">');
|
||||
expect(html).not.toContain('<span formaction="/save"');
|
||||
expect(html).toContain('<button formaction="/save">Save</button>');
|
||||
});
|
||||
|
||||
test("stateless component bakes props into server HTML (zero JS)", async () => {
|
||||
const mod = await compileAndImport(
|
||||
`component Button {\n props {\n label = "Button"\n variant = "default"\n class = ""\n }\n view { <button class="wire-btn wire-btn--{variant} {class}">{label}</button> }\n}`,
|
||||
@@ -260,6 +361,24 @@ test("page state attributes bake their initial value and retain a reactive bindi
|
||||
expect(page).toContain("show ? 'text' : 'password'");
|
||||
});
|
||||
|
||||
test("request-dependent page state attributes resolve during server rendering", async () => {
|
||||
const mod = await compileAndImport(`page Playground {
|
||||
state label = ctx.url.searchParams.get("label") ?? "Default"
|
||||
state loading = ctx.url.searchParams.get("loading") ?? "false"
|
||||
view {
|
||||
<div data-component="Button" label="{label}" loading="{loading}"></div>
|
||||
}
|
||||
}`);
|
||||
const render = mod.default as (ctx: { url: URL }) => Promise<string>;
|
||||
const html = await render({
|
||||
url: new URL("https://example.test/playground?label=Visible&loading=false"),
|
||||
});
|
||||
|
||||
expect(html).toContain('label="Visible"');
|
||||
expect(html).toContain('loading="false"');
|
||||
expect(html).not.toContain('label=""');
|
||||
});
|
||||
|
||||
test("data-for: loop-variable mustaches stay literal (not baked server-side)", () => {
|
||||
const comp = compileWireFile(
|
||||
`component TodoList {\n state todos = []\n view { <ul><li data-for="t in todos">{t.text}</li></ul> }\n}`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/core",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -618,15 +618,34 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
var behaviorFunctions = {};
|
||||
var componentEventTarget = el.querySelector("[data-wrn-events]") || el;
|
||||
var declaredEvents = new Set(
|
||||
String(componentEventTarget.getAttribute("data-wrn-events") || "")
|
||||
.split(",")
|
||||
.map(function (name) { return name.trim(); })
|
||||
.filter(Boolean),
|
||||
);
|
||||
var stateWatchers = {};
|
||||
var anyStateListeners = new Set();
|
||||
var cleanupCallbacks = [];
|
||||
var disposed = false;
|
||||
|
||||
function readGlobal(name) {
|
||||
if (declaredEvents.has(name)) {
|
||||
return function (detail) {
|
||||
return dispatchComponentEvent(componentEventTarget, name, detail);
|
||||
};
|
||||
}
|
||||
if (name === "$emit") {
|
||||
return function (eventName, detail) {
|
||||
return dispatchComponentEvent(componentEventTarget, eventName, detail);
|
||||
};
|
||||
}
|
||||
if (name === "window") return window;
|
||||
if (name === "document") return document;
|
||||
if (name === "console") return console;
|
||||
if (name === "Array") return Array;
|
||||
if (name === "Number") return Number;
|
||||
if (name === "Math") return Math;
|
||||
if (name === "JSON") return JSON;
|
||||
if (name === "Date") return Date;
|
||||
@@ -1489,6 +1508,10 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
// Keep visible when evaluation is unavailable.
|
||||
}
|
||||
|
||||
node.setAttribute(
|
||||
"data-show",
|
||||
visible ? "true" : "false",
|
||||
);
|
||||
node.style.display = visible ? "" : "none";
|
||||
});
|
||||
});
|
||||
@@ -1718,6 +1741,19 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
if (behavior) {
|
||||
installBehaviorFunctions(behavior.functions);
|
||||
var publicScopeApi = {
|
||||
get: peekScope,
|
||||
set: writeScope,
|
||||
call: function (name) {
|
||||
var fn = behaviorFunctions[name];
|
||||
if (typeof fn !== "function") return undefined;
|
||||
return fn.apply(null, Array.prototype.slice.call(arguments, 1));
|
||||
},
|
||||
};
|
||||
el.__wrnexusScopeApi = publicScopeApi;
|
||||
el.querySelectorAll("[data-wrn-select]").forEach(function (select) {
|
||||
select.__wrnexusScopeApi = publicScopeApi;
|
||||
});
|
||||
|
||||
(behavior.effects || []).forEach(function (source) {
|
||||
if (typeof source !== "string" || !source.trim()) return;
|
||||
@@ -1774,6 +1810,10 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
mountedBehaviorRoots.delete(el);
|
||||
behaviorInstances.delete(el);
|
||||
delete el.__wrnexusScopeApi;
|
||||
el.querySelectorAll("[data-wrn-select]").forEach(function (select) {
|
||||
delete select.__wrnexusScopeApi;
|
||||
});
|
||||
el.__wrnexusScope = false;
|
||||
},
|
||||
};
|
||||
@@ -1909,6 +1949,602 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
ensureBehaviorObserver();
|
||||
hydrateSelectControllers(host);
|
||||
hydratePinInputControllers(host);
|
||||
}
|
||||
|
||||
var selectOutsideClickBound = false;
|
||||
|
||||
function selectScopeApi(root) {
|
||||
var scope = root;
|
||||
while (scope) {
|
||||
if (scope.__wrnexusScopeApi) return scope.__wrnexusScopeApi;
|
||||
scope = scope.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function selectPayloadOptions(payload) {
|
||||
if (Array.isArray(payload)) return payload;
|
||||
if (!payload || typeof payload !== "object") return [];
|
||||
if (Array.isArray(payload.options)) return payload.options;
|
||||
if (Array.isArray(payload.items)) return payload.items;
|
||||
if (Array.isArray(payload.results)) return payload.results;
|
||||
if (Array.isArray(payload.data)) return payload.data;
|
||||
return [];
|
||||
}
|
||||
|
||||
function syncComboboxInput(root) {
|
||||
if (!root || !root.hasAttribute("data-wrn-combobox")) return;
|
||||
var input = root.querySelector(".wire-next__combobox-input");
|
||||
var api = selectScopeApi(root);
|
||||
if (!input || !api) return;
|
||||
var value = api.call("inputText") || "";
|
||||
input.value = String(value);
|
||||
input.setAttribute("value", String(value));
|
||||
}
|
||||
|
||||
function selectEventDetail(root, extra) {
|
||||
var api = selectScopeApi(root);
|
||||
var multiple = !!(api && api.get("multiple"));
|
||||
var detail = {
|
||||
component: root.hasAttribute("data-wrn-combobox") ? "ComboBox" : "AdvancedSelect",
|
||||
query: api ? (api.get("query") || "") : "",
|
||||
value: api ? (api.get("selectedValue") || "") : "",
|
||||
values: api && Array.isArray(api.get("selectedValues")) ? api.get("selectedValues") : [],
|
||||
multiple: multiple,
|
||||
selectedOptions: api ? (api.call("selectedOptions") || []) : [],
|
||||
};
|
||||
if (extra && typeof extra === "object") {
|
||||
Object.keys(extra).forEach(function (key) { detail[key] = extra[key]; });
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
function emitSelectEvent(root, name, extra) {
|
||||
try {
|
||||
dispatchComponentEvent(root, name, selectEventDetail(root, extra));
|
||||
} catch (_) {
|
||||
// CustomEvent can be unavailable in minimal DOM environments.
|
||||
}
|
||||
}
|
||||
|
||||
function renderRemoteSelectOptions(root, options) {
|
||||
var list = root.querySelector(".wire-next__select-list");
|
||||
if (!list) return;
|
||||
|
||||
list.querySelectorAll(".wire-next__option-group, .wire-next__select-option, .wire-next__select-message")
|
||||
.forEach(function (node) { node.remove(); });
|
||||
|
||||
options.forEach(function (option) {
|
||||
if (!option || option.value == null) return;
|
||||
var button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "wire-next__select-option";
|
||||
button.setAttribute("role", "option");
|
||||
button.setAttribute("data-option-value", String(option.value));
|
||||
button.disabled = !!option.disabled;
|
||||
|
||||
if (option.icon) {
|
||||
var icon = document.createElement("i");
|
||||
icon.className = String(option.icon);
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
button.appendChild(icon);
|
||||
} else if (option.avatar) {
|
||||
var avatar = document.createElement("img");
|
||||
avatar.src = String(option.avatar);
|
||||
avatar.alt = "";
|
||||
button.appendChild(avatar);
|
||||
} else if (option.color) {
|
||||
var dot = document.createElement("i");
|
||||
dot.className = "wire-next__color-dot";
|
||||
dot.style.setProperty("--option-color", String(option.color));
|
||||
button.appendChild(dot);
|
||||
}
|
||||
|
||||
var copy = document.createElement("span");
|
||||
var label = document.createElement("strong");
|
||||
label.textContent = String(option.label == null ? option.value : option.label);
|
||||
copy.appendChild(label);
|
||||
if (option.description) {
|
||||
var description = document.createElement("small");
|
||||
description.textContent = String(option.description);
|
||||
copy.appendChild(description);
|
||||
}
|
||||
button.appendChild(copy);
|
||||
|
||||
var check = document.createElement("i");
|
||||
check.className = "wire-next__check icon-[lucide--check]";
|
||||
check.setAttribute("aria-hidden", "true");
|
||||
check.style.display = "none";
|
||||
button.appendChild(check);
|
||||
|
||||
button.addEventListener("click", function () {
|
||||
var api = selectScopeApi(root);
|
||||
if (!api || button.disabled) return;
|
||||
api.call("chooseOption", option);
|
||||
window.setTimeout(function () {
|
||||
var selected = api.call("isSelected", option);
|
||||
button.classList.toggle("wire-next__select-option--selected", !!selected);
|
||||
button.setAttribute("aria-selected", selected ? "true" : "false");
|
||||
check.style.display = selected ? "" : "none";
|
||||
syncComboboxInput(root);
|
||||
}, 0);
|
||||
});
|
||||
list.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
function setupSelectController(root) {
|
||||
if (!root || root.__wrnexusSelectController) return;
|
||||
root.__wrnexusSelectController = { attempts: 0 };
|
||||
|
||||
var remote = root.getAttribute("data-remote") === "true";
|
||||
var remoteUrl = root.getAttribute("data-remote-url") || "";
|
||||
var input = root.querySelector(".wire-next__select-search input, .wire-next__combobox-input");
|
||||
var list = root.querySelector(".wire-next__select-list");
|
||||
var loadMore = root.querySelector("[data-wrn-select-load-more]");
|
||||
var timer = 0;
|
||||
var requestId = 0;
|
||||
var loadedOptions = [];
|
||||
var retainedSelections = [];
|
||||
var currentPage = Number(root.getAttribute("data-page") || 1);
|
||||
|
||||
function setLoading(loading) {
|
||||
root.setAttribute("data-loading", loading ? "true" : "false");
|
||||
if (list) list.setAttribute("aria-busy", loading ? "true" : "false");
|
||||
var api = selectScopeApi(root);
|
||||
if (api) api.set("loading", loading);
|
||||
}
|
||||
|
||||
function load(append, allowEmptyQuery) {
|
||||
root.__wrnexusSelectController.attempts++;
|
||||
if (!remote || !remoteUrl) return;
|
||||
var api = selectScopeApi(root);
|
||||
if (!api) return;
|
||||
var query = input ? input.value : "";
|
||||
var minimum = Number(api.get("minSearchLength") || 0);
|
||||
if (!allowEmptyQuery && query.length < minimum) return;
|
||||
|
||||
var url;
|
||||
try {
|
||||
url = new URL(remoteUrl, window.location.href);
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
var parameter = root.getAttribute("data-remote-query-param") || "q";
|
||||
if (query) url.searchParams.set(parameter, query);
|
||||
var nextPage = append ? currentPage + 1 : 1;
|
||||
url.searchParams.set("page", String(nextPage));
|
||||
|
||||
var thisRequest = ++requestId;
|
||||
setLoading(true);
|
||||
fetch(url.toString(), { headers: { accept: "application/json" } })
|
||||
.then(function (response) {
|
||||
if (!response.ok) throw new Error("HTTP " + response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(function (payload) {
|
||||
if (thisRequest !== requestId) return;
|
||||
var nextOptions = selectPayloadOptions(payload);
|
||||
loadedOptions = append ? loadedOptions.concat(nextOptions) : nextOptions;
|
||||
currentPage = nextPage;
|
||||
var selectedValues = api.get("selectedValues");
|
||||
var selectedValue = api.get("selectedValue");
|
||||
var selectedKeys = api.get("multiple")
|
||||
? (Array.isArray(selectedValues) ? selectedValues : [])
|
||||
: (selectedValue ? [selectedValue] : []);
|
||||
var previousOptions = api.call("allOptions") || [];
|
||||
retainedSelections = retainedSelections
|
||||
.concat(previousOptions)
|
||||
.concat(loadedOptions)
|
||||
.filter(function (option, index, list) {
|
||||
return option && selectedKeys.includes(option.value) &&
|
||||
list.findIndex(function (item) { return item && item.value === option.value; }) === index;
|
||||
});
|
||||
var stateOptions = retainedSelections.concat(loadedOptions).filter(function (option, index, list) {
|
||||
return option && list.findIndex(function (item) { return item && item.value === option.value; }) === index;
|
||||
});
|
||||
api.set("options", stateOptions);
|
||||
api.set("groups", []);
|
||||
api.set("page", currentPage);
|
||||
api.set("hasMore", !!(payload && (payload.hasMore || payload.nextPage || payload.next)));
|
||||
renderRemoteSelectOptions(root, loadedOptions);
|
||||
emitSelectEvent(root, "load", {
|
||||
options: nextOptions,
|
||||
page: currentPage,
|
||||
hasMore: !!(payload && (payload.hasMore || payload.nextPage || payload.next)),
|
||||
append: !!append,
|
||||
url: url.toString(),
|
||||
payload: payload,
|
||||
});
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error("[wrnexus] advanced select remote request failed", error);
|
||||
emitSelectEvent(root, "error", {
|
||||
error: error,
|
||||
message: error && error.message ? error.message : String(error),
|
||||
url: url.toString(),
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
if (thisRequest === requestId) setLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (input) {
|
||||
input.addEventListener("input", function () {
|
||||
emitSelectEvent(root, "search", { query: input.value });
|
||||
if (!remote) return;
|
||||
window.clearTimeout(timer);
|
||||
var debounce = Number(root.getAttribute("data-remote-debounce") || 250);
|
||||
timer = window.setTimeout(function () { load(false); }, debounce);
|
||||
});
|
||||
}
|
||||
|
||||
if (loadMore) {
|
||||
loadMore.addEventListener("click", function () { load(true); });
|
||||
}
|
||||
|
||||
if (list && root.getAttribute("data-infinite") === "true") {
|
||||
list.addEventListener("scroll", function () {
|
||||
if (list.scrollTop + list.clientHeight < list.scrollHeight - 24) return;
|
||||
var api = selectScopeApi(root);
|
||||
if (api && api.get("hasMore") && root.getAttribute("data-loading") !== "true") load(true);
|
||||
});
|
||||
}
|
||||
|
||||
root.addEventListener("click", function (event) {
|
||||
if (!remote || !remoteUrl) return;
|
||||
if (!event.target.closest(".wire-next__select-trigger")) return;
|
||||
if (loadedOptions.length === 0) load(false);
|
||||
});
|
||||
root.__wrnexusSelectController.load = load;
|
||||
|
||||
var dropdown = root.querySelector(".wire-next__select-dropdown");
|
||||
var lastOpen = !!(selectScopeApi(root) && selectScopeApi(root).get("open"));
|
||||
function emitOpenStateIfChanged() {
|
||||
var api = selectScopeApi(root);
|
||||
var nextOpen = !!(api && api.get("open"));
|
||||
if (nextOpen === lastOpen) return;
|
||||
lastOpen = nextOpen;
|
||||
emitSelectEvent(root, nextOpen ? "open" : "close");
|
||||
}
|
||||
var ObserverConstructor =
|
||||
root.ownerDocument &&
|
||||
root.ownerDocument.defaultView &&
|
||||
root.ownerDocument.defaultView.MutationObserver
|
||||
? root.ownerDocument.defaultView.MutationObserver
|
||||
: (typeof MutationObserver !== "undefined" ? MutationObserver : null);
|
||||
if (ObserverConstructor) {
|
||||
new ObserverConstructor(function () {
|
||||
emitOpenStateIfChanged();
|
||||
}).observe(dropdown || root, {
|
||||
attributes: true,
|
||||
attributeFilter: dropdown ? ["data-show", "style"] : ["class"],
|
||||
});
|
||||
}
|
||||
|
||||
root.addEventListener("click", function (event) {
|
||||
var optionButton = event.target.closest(".wire-next__select-option");
|
||||
if (optionButton) {
|
||||
window.setTimeout(function () {
|
||||
var api = selectScopeApi(root);
|
||||
var optionValue = optionButton.getAttribute("data-option-value");
|
||||
var options = api ? (api.call("allOptions") || []) : [];
|
||||
var option = options.find(function (item) {
|
||||
return item && String(item.value) === String(optionValue);
|
||||
});
|
||||
emitSelectEvent(root, "select", { option: option || null });
|
||||
emitSelectEvent(root, "change", { option: option || null, reason: "select" });
|
||||
emitOpenStateIfChanged();
|
||||
}, 0);
|
||||
}
|
||||
if (event.target.closest(".wire-next__clear-select")) {
|
||||
window.setTimeout(function () {
|
||||
emitSelectEvent(root, "clear");
|
||||
emitSelectEvent(root, "change", { reason: "clear" });
|
||||
}, 0);
|
||||
}
|
||||
}, true);
|
||||
root.addEventListener("keydown", function (event) {
|
||||
if (event.key !== "Enter") return;
|
||||
var before = selectEventDetail(root);
|
||||
window.setTimeout(function () {
|
||||
var after = selectEventDetail(root);
|
||||
var beforeSelection = before.multiple ? JSON.stringify(before.values) : before.value;
|
||||
var afterSelection = after.multiple ? JSON.stringify(after.values) : after.value;
|
||||
if (afterSelection === beforeSelection) return;
|
||||
syncComboboxInput(root);
|
||||
emitSelectEvent(root, "select", { source: "keyboard" });
|
||||
emitSelectEvent(root, "change", { reason: "select", source: "keyboard" });
|
||||
emitOpenStateIfChanged();
|
||||
}, 0);
|
||||
}, true);
|
||||
|
||||
if (root.hasAttribute("data-wrn-combobox")) {
|
||||
root.wrnexusCombobox = {
|
||||
open: function () {
|
||||
var api = selectScopeApi(root);
|
||||
if (api) api.call("openDropdown");
|
||||
emitOpenStateIfChanged();
|
||||
},
|
||||
close: function () {
|
||||
var api = selectScopeApi(root);
|
||||
if (api) api.call("closeDropdown");
|
||||
emitOpenStateIfChanged();
|
||||
},
|
||||
clear: function () {
|
||||
var api = selectScopeApi(root);
|
||||
if (api) api.call("clearValue");
|
||||
syncComboboxInput(root);
|
||||
emitSelectEvent(root, "clear", { source: "method" });
|
||||
emitSelectEvent(root, "change", { reason: "clear", source: "method" });
|
||||
},
|
||||
setValue: function (value) {
|
||||
var api = selectScopeApi(root);
|
||||
if (api) api.call("setValue", value);
|
||||
syncComboboxInput(root);
|
||||
emitSelectEvent(root, "change", { reason: "setValue", source: "method" });
|
||||
},
|
||||
reload: function () {
|
||||
load(false, true);
|
||||
},
|
||||
};
|
||||
root.addEventListener("click", function (event) {
|
||||
if (!event.target.closest(".wire-next__select-option, .wire-next__clear-select")) return;
|
||||
window.setTimeout(function () { syncComboboxInput(root); }, 0);
|
||||
});
|
||||
if (input) {
|
||||
input.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Enter") {
|
||||
window.setTimeout(function () { syncComboboxInput(root); }, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (remote && remoteUrl && root.getAttribute("data-remote-auto-load") !== "false") {
|
||||
load(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
function hydrateSelectControllers(root) {
|
||||
var host = root || document;
|
||||
var selects = [];
|
||||
if (host.nodeType === 1 && host.matches && host.matches("[data-wrn-select]")) selects.push(host);
|
||||
if (host.querySelectorAll) {
|
||||
Array.prototype.push.apply(selects, host.querySelectorAll("[data-wrn-select]"));
|
||||
}
|
||||
selects.forEach(setupSelectController);
|
||||
|
||||
if (!selectOutsideClickBound) {
|
||||
selectOutsideClickBound = true;
|
||||
document.addEventListener("pointerdown", function (event) {
|
||||
document.querySelectorAll("[data-wrn-select].wire-next--open").forEach(function (select) {
|
||||
if (select.contains(event.target)) return;
|
||||
var api = selectScopeApi(select);
|
||||
if (api) api.set("open", false);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchComponentEvent(root, name, detail) {
|
||||
if (!root || !name) return null;
|
||||
var EventConstructor =
|
||||
root.ownerDocument &&
|
||||
root.ownerDocument.defaultView &&
|
||||
root.ownerDocument.defaultView.CustomEvent
|
||||
? root.ownerDocument.defaultView.CustomEvent
|
||||
: CustomEvent;
|
||||
var event = new EventConstructor(String(name), {
|
||||
bubbles: true,
|
||||
detail: detail || {},
|
||||
});
|
||||
root.dispatchEvent(event);
|
||||
// Compatibility for applications using the former prefixed contract.
|
||||
root.dispatchEvent(new EventConstructor("wrnexus:" + String(name), {
|
||||
bubbles: true,
|
||||
detail: detail || {},
|
||||
}));
|
||||
return event;
|
||||
}
|
||||
|
||||
function emitPinInputEvent(root, name, extra) {
|
||||
var hidden = root.querySelector("[data-pin-value]");
|
||||
var cells = Array.prototype.slice.call(root.querySelectorAll("[data-pin-cell]"));
|
||||
var value = hidden ? hidden.value : "";
|
||||
var detail = {
|
||||
component: "PinInput",
|
||||
value: value,
|
||||
digits: cells.map(function (cell) { return cell.value; }),
|
||||
complete: value.length === cells.length,
|
||||
length: cells.length,
|
||||
};
|
||||
if (extra && typeof extra === "object") {
|
||||
Object.keys(extra).forEach(function (key) { detail[key] = extra[key]; });
|
||||
}
|
||||
try {
|
||||
dispatchComponentEvent(root, name, detail);
|
||||
} catch (_) {
|
||||
// CustomEvent can be unavailable in minimal DOM environments.
|
||||
}
|
||||
}
|
||||
|
||||
function setupPinInputController(root) {
|
||||
if (!root || root.__wrnexusPinInputController) return;
|
||||
var cells = Array.prototype.slice.call(root.querySelectorAll("[data-pin-cell]"));
|
||||
var hidden = root.querySelector("[data-pin-value]");
|
||||
var clearButton = root.querySelector("[data-pin-clear]");
|
||||
var patternSource = root.getAttribute("data-pattern") || "[0-9]";
|
||||
var allowPaste = root.getAttribute("data-allow-paste") !== "false";
|
||||
var autoSubmit = root.getAttribute("data-auto-submit") === "true";
|
||||
var matcher;
|
||||
var exactMatcher;
|
||||
var NativeEventConstructor =
|
||||
root.ownerDocument &&
|
||||
root.ownerDocument.defaultView &&
|
||||
root.ownerDocument.defaultView.Event
|
||||
? root.ownerDocument.defaultView.Event
|
||||
: Event;
|
||||
|
||||
try {
|
||||
exactMatcher = new RegExp("^(?:" + patternSource + ")$");
|
||||
matcher = new RegExp("^(?:" + patternSource + ")$", "i");
|
||||
} catch (error) {
|
||||
matcher = /$a/;
|
||||
emitPinInputEvent(root, "error", {
|
||||
error: error,
|
||||
message: "Invalid PIN pattern: " + patternSource,
|
||||
});
|
||||
}
|
||||
|
||||
function acceptedCharacters(value) {
|
||||
return Array.from(String(value || "")).map(function (character) {
|
||||
matcher.lastIndex = 0;
|
||||
if (!matcher.test(character)) return "";
|
||||
exactMatcher.lastIndex = 0;
|
||||
if (exactMatcher.test(character)) return character;
|
||||
var upper = character.toUpperCase();
|
||||
exactMatcher.lastIndex = 0;
|
||||
if (exactMatcher.test(upper)) return upper;
|
||||
var lower = character.toLowerCase();
|
||||
exactMatcher.lastIndex = 0;
|
||||
return exactMatcher.test(lower) ? lower : character;
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function currentValue() {
|
||||
return cells.map(function (cell) { return cell.value; }).join("");
|
||||
}
|
||||
|
||||
function updateClearButton(value) {
|
||||
if (!clearButton) return;
|
||||
clearButton.style.display = value ? "" : "none";
|
||||
}
|
||||
|
||||
function syncValue(reason, index) {
|
||||
var previous = hidden ? hidden.value : "";
|
||||
var value = currentValue();
|
||||
if (hidden) {
|
||||
hidden.value = value;
|
||||
hidden.setAttribute("value", value);
|
||||
hidden.dispatchEvent(new NativeEventConstructor("input", { bubbles: true }));
|
||||
hidden.dispatchEvent(new NativeEventConstructor("change", { bubbles: true }));
|
||||
}
|
||||
root.setAttribute("data-value", value);
|
||||
root.setAttribute("data-complete", value.length === cells.length ? "true" : "false");
|
||||
updateClearButton(value);
|
||||
emitPinInputEvent(root, "input", { reason: reason, index: index });
|
||||
if (value !== previous) {
|
||||
emitPinInputEvent(root, "change", { reason: reason, index: index });
|
||||
}
|
||||
if (value.length === cells.length && previous.length !== cells.length) {
|
||||
emitPinInputEvent(root, "complete", { reason: reason, index: index });
|
||||
if (autoSubmit) {
|
||||
var form = root.closest("form");
|
||||
if (form && typeof form.requestSubmit === "function") form.requestSubmit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setValue(value, reason) {
|
||||
var characters = acceptedCharacters(value).slice(0, cells.length);
|
||||
cells.forEach(function (cell, index) {
|
||||
cell.value = characters[index] || "";
|
||||
cell.setAttribute("value", cell.value);
|
||||
});
|
||||
syncValue(reason || "setValue", characters.length ? characters.length - 1 : -1);
|
||||
}
|
||||
|
||||
function focusCell(index) {
|
||||
var cell = cells[Math.max(0, Math.min(cells.length - 1, index))];
|
||||
if (cell && !cell.disabled) {
|
||||
cell.focus();
|
||||
if (typeof cell.select === "function") cell.select();
|
||||
}
|
||||
}
|
||||
|
||||
cells.forEach(function (cell, index) {
|
||||
cell.addEventListener("focus", function () {
|
||||
if (typeof cell.select === "function") cell.select();
|
||||
});
|
||||
cell.addEventListener("input", function () {
|
||||
var characters = acceptedCharacters(cell.value);
|
||||
cell.value = characters.length ? characters[characters.length - 1] : "";
|
||||
cell.setAttribute("value", cell.value);
|
||||
syncValue("input", index);
|
||||
if (cell.value && index < cells.length - 1) focusCell(index + 1);
|
||||
});
|
||||
cell.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Backspace" && !cell.value && index > 0) {
|
||||
event.preventDefault();
|
||||
cells[index - 1].value = "";
|
||||
cells[index - 1].setAttribute("value", "");
|
||||
syncValue("backspace", index - 1);
|
||||
focusCell(index - 1);
|
||||
} else if (event.key === "ArrowLeft" && index > 0) {
|
||||
event.preventDefault();
|
||||
focusCell(index - 1);
|
||||
} else if (event.key === "ArrowRight" && index < cells.length - 1) {
|
||||
event.preventDefault();
|
||||
focusCell(index + 1);
|
||||
} else if (event.key === "Home") {
|
||||
event.preventDefault();
|
||||
focusCell(0);
|
||||
} else if (event.key === "End") {
|
||||
event.preventDefault();
|
||||
focusCell(cells.length - 1);
|
||||
}
|
||||
});
|
||||
cell.addEventListener("paste", function (event) {
|
||||
if (!allowPaste) return;
|
||||
var clipboard = event.clipboardData;
|
||||
if (!clipboard) return;
|
||||
event.preventDefault();
|
||||
var characters = acceptedCharacters(clipboard.getData("text")).slice(0, cells.length - index);
|
||||
characters.forEach(function (character, offset) {
|
||||
cells[index + offset].value = character;
|
||||
cells[index + offset].setAttribute("value", character);
|
||||
});
|
||||
syncValue("paste", index);
|
||||
emitPinInputEvent(root, "paste", { index: index, pasted: characters.join("") });
|
||||
focusCell(Math.min(index + characters.length, cells.length - 1));
|
||||
});
|
||||
});
|
||||
|
||||
if (clearButton) {
|
||||
clearButton.addEventListener("click", function () {
|
||||
setValue("", "clear");
|
||||
emitPinInputEvent(root, "clear");
|
||||
focusCell(0);
|
||||
});
|
||||
}
|
||||
|
||||
root.wrnexusPinInput = {
|
||||
focus: function (index) { focusCell(Number(index || 0)); },
|
||||
clear: function () {
|
||||
setValue("", "clear");
|
||||
emitPinInputEvent(root, "clear", { source: "method" });
|
||||
},
|
||||
setValue: function (value) { setValue(value, "setValue"); },
|
||||
getValue: function () { return currentValue(); },
|
||||
};
|
||||
root.__wrnexusPinInputController = root.wrnexusPinInput;
|
||||
setValue(hidden ? hidden.value : root.getAttribute("data-value"), "initial");
|
||||
}
|
||||
|
||||
function hydratePinInputControllers(root) {
|
||||
var host = root || document;
|
||||
var inputs = [];
|
||||
if (host.nodeType === 1 && host.matches && host.matches("[data-wrn-pin-input]")) {
|
||||
inputs.push(host);
|
||||
}
|
||||
if (host.querySelectorAll) {
|
||||
Array.prototype.push.apply(inputs, host.querySelectorAll("[data-wrn-pin-input]"));
|
||||
}
|
||||
inputs.forEach(setupPinInputController);
|
||||
}
|
||||
|
||||
function parseScopeDecl(decl) {
|
||||
@@ -2392,7 +3028,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
* trying to parse the callback in the same regex.
|
||||
*/
|
||||
var methodMatch =
|
||||
/^([\s\S]+)\.(filter|map|some|every|find|findIndex)\(\s*([\s\S]*)\s*\)$/.exec(
|
||||
/^([\s\S]+)\.(filter|map|flatMap|some|every|find|findIndex)\(\s*([\s\S]*)\s*\)$/.exec(
|
||||
source,
|
||||
);
|
||||
|
||||
@@ -2503,6 +3139,11 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
) {
|
||||
result =
|
||||
collection.map(callback);
|
||||
} else if (
|
||||
method === "flatMap"
|
||||
) {
|
||||
result =
|
||||
collection.flatMap(callback);
|
||||
} else if (
|
||||
method === "some"
|
||||
) {
|
||||
@@ -2561,11 +3202,13 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
function next() { return tokens[index++]; }
|
||||
function is(v) { return peek() && peek().value === v; }
|
||||
function match(v) { if (is(v)) { index++; return true; } return false; }
|
||||
function expect(v) { if (!match(v)) throw new Error("Expected '" + v + "'"); }
|
||||
function expect(v) {
|
||||
if (!match(v)) throw new Error("Expected '" + v + "' in '" + expr + "'");
|
||||
}
|
||||
|
||||
function parsePrimary() {
|
||||
var t = next();
|
||||
if (!t) throw new Error("Unexpected end of expression");
|
||||
if (!t) throw new Error("Unexpected end of expression in '" + expr + "'");
|
||||
if (t.type === "number" || t.type === "string") return { value: t.value };
|
||||
if (t.type === "ident") {
|
||||
if (t.value === "true") return { value: true };
|
||||
@@ -2659,12 +3302,18 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
function parseAnd() {
|
||||
var l = parseEq();
|
||||
while (match("&&")) l = l && parseEq();
|
||||
while (match("&&")) {
|
||||
var r = parseEq();
|
||||
l = l && r;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
function parseOr() {
|
||||
var l = parseAnd();
|
||||
while (match("||")) l = l || parseAnd();
|
||||
while (match("||")) {
|
||||
var r = parseAnd();
|
||||
l = l || r;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
function parseTernary() {
|
||||
|
||||
@@ -41,6 +41,23 @@ test("@event (data-on-click) mutates a signal and re-renders", () => {
|
||||
expect(btn.textContent).toBe("2");
|
||||
});
|
||||
|
||||
test("declared component events emit through the generic $emit function", () => {
|
||||
const win = mount(
|
||||
`<div data-scope="" data-wrn-events="complete">
|
||||
<button data-on-click="$emit('complete', { value: '4829' })">Complete</button>
|
||||
</div>`,
|
||||
);
|
||||
const root = win.document.querySelector("[data-wrn-events]")!;
|
||||
let detail: Record<string, unknown> | undefined;
|
||||
root.addEventListener("complete", (event) => {
|
||||
detail = (event as unknown as CustomEvent).detail;
|
||||
});
|
||||
|
||||
win.document.querySelector("button")!.click();
|
||||
|
||||
expect(detail).toEqual({ value: "4829" });
|
||||
});
|
||||
|
||||
test("nested scopes don't clobber each other (regression)", () => {
|
||||
// An empty outer scope must not touch inner scopes' values.
|
||||
const win = mount(
|
||||
@@ -131,6 +148,26 @@ test("expression evaluator: member access, ternary, comparison, calls", () => {
|
||||
expect(win.document.getElementById("c")!.textContent).toBe("3");
|
||||
});
|
||||
|
||||
test("logical expressions consume their right-hand side when the result short-circuits", () => {
|
||||
const win = mount(
|
||||
`<div data-scope="leftFalse: false, leftTrue: true">
|
||||
<span id="and" data-text="leftFalse && 123"></span>
|
||||
<span id="or" data-text="leftTrue || 456"></span>
|
||||
</div>`,
|
||||
);
|
||||
expect(win.document.getElementById("and")!.textContent).toBe("false");
|
||||
expect(win.document.getElementById("or")!.textContent).toBe("true");
|
||||
});
|
||||
|
||||
test("expression evaluator supports flatMap callbacks", () => {
|
||||
const win = mount(
|
||||
`<div data-scope="groups: [{items: [1, 2]}, {items: [3]}]">
|
||||
<span data-text="groups.flatMap((group) => group.items)"></span>
|
||||
</div>`,
|
||||
);
|
||||
expect(win.document.querySelector("span")!.textContent).toBe("1,2,3");
|
||||
});
|
||||
|
||||
test("data-show toggles visibility on a reactive expression (tabs pattern)", () => {
|
||||
const win = mount(
|
||||
`<div data-scope="tab: 0">
|
||||
@@ -143,9 +180,13 @@ test("data-show toggles visibility on a reactive expression (tabs pattern)", ()
|
||||
(win.document.getElementById(id) as unknown as HTMLElement).style.display;
|
||||
expect(disp("a")).toBe("");
|
||||
expect(disp("b")).toBe("none");
|
||||
expect(win.document.getElementById("a")!.getAttribute("data-show")).toBe("true");
|
||||
expect(win.document.getElementById("b")!.getAttribute("data-show")).toBe("false");
|
||||
(win.document.querySelector("button") as unknown as HTMLElement).click();
|
||||
expect(disp("a")).toBe("none");
|
||||
expect(disp("b")).toBe("");
|
||||
expect(win.document.getElementById("a")!.getAttribute("data-show")).toBe("false");
|
||||
expect(win.document.getElementById("b")!.getAttribute("data-show")).toBe("true");
|
||||
});
|
||||
|
||||
test("reactive attribute bindings update input and accessibility attributes", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/db",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1195,6 +1195,11 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
};
|
||||
pageCtx.__wrnexusCallApi = (path, method = "GET") => callApiFromContext(ctx, path, method);
|
||||
let body = await renderComponents(String(await component(pageCtx)), ctx.t);
|
||||
const resolvedTheme = deps.theme
|
||||
? resolveThemeName(ctx.cookies.get(THEME_COOKIE), deps.theme)
|
||||
: "";
|
||||
const language = deps.i18n && ctx.lang ? ctx.lang : (meta.lang ?? deps.seo?.lang ?? "en");
|
||||
let documentTemplate: string | undefined;
|
||||
|
||||
// Page layout: a page selects one by exporting `layout = "<name>"`
|
||||
// (app/layouts/<name>.wrn), else falls back to a `default` layout if one
|
||||
@@ -1229,6 +1234,46 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
}
|
||||
}
|
||||
|
||||
// A conventional app/layouts/document.wrn is a request-aware, top-level
|
||||
// document shell. It wraps the selected page layout and may own html, head,
|
||||
// body, and #app. The SSR renderer still merges framework metadata/assets.
|
||||
const documentLayout =
|
||||
layoutName === "document"
|
||||
? undefined
|
||||
: router.layouts.find((item) => item.name === "document");
|
||||
if (documentLayout) {
|
||||
try {
|
||||
const documentMod = await loadModule(documentLayout.file);
|
||||
const documentRender = (
|
||||
documentMod as { render?: (props: Record<string, unknown>) => string }
|
||||
).render;
|
||||
if (typeof documentRender === "function") {
|
||||
const rendered = String(
|
||||
documentRender({
|
||||
cookies: ctx.cookies.getAll(),
|
||||
theme: resolvedTheme,
|
||||
language,
|
||||
url: ctx.url.toString(),
|
||||
pathname: ctx.url.pathname,
|
||||
}),
|
||||
);
|
||||
documentTemplate = await renderComponents(fillSlots(rendered, body), ctx.t);
|
||||
body = documentTemplate;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[wrnexus] document layout failed to render", err);
|
||||
deps.devToolbar?.collector.add(
|
||||
issueFromError(err, {
|
||||
ruleId: "server/document-layout-render-error",
|
||||
category: "server",
|
||||
title: "Document layout failed to render",
|
||||
pathname: ctx.url.pathname,
|
||||
source: { file: documentLayout.file },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isMobileRequest) body = revealMobileOnlyHtml(body);
|
||||
|
||||
// i18n: resolve `{t:key}` / `t:attr` markers against the request language.
|
||||
@@ -1243,9 +1288,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
|
||||
// <html> attributes: no-flash theme (from cookie, validated) + active lang.
|
||||
const attrs: string[] = [];
|
||||
if (deps.theme)
|
||||
attrs.push(`data-theme="${resolveThemeName(ctx.cookies.get(THEME_COOKIE), deps.theme)}"`);
|
||||
const language = deps.i18n && ctx.lang ? ctx.lang : (meta.lang ?? deps.seo?.lang ?? "en");
|
||||
if (deps.theme) attrs.push(`data-theme="${resolvedTheme}"`);
|
||||
attrs.push(`lang="${safeLanguageTag(language)}"`);
|
||||
const htmlAttrs = attrs.length ? ` ${attrs.join(" ")}` : undefined;
|
||||
|
||||
@@ -1271,6 +1314,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
.filter(Boolean)
|
||||
.join("\n") || undefined,
|
||||
htmlAttrs,
|
||||
documentTemplate,
|
||||
});
|
||||
// Conditional GET: hash the page CONTENT (`body`), not the assembled shell —
|
||||
// the shell carries a per-request CSP nonce in dev, which would otherwise make
|
||||
@@ -1392,12 +1436,21 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
* attribute values, so component props arrive as the author wrote them.
|
||||
*/
|
||||
function decodeHtmlEntities(value: string): string {
|
||||
return value
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/&/g, "&");
|
||||
let decoded = value;
|
||||
// Static page attributes can be escaped by their author/generator and then
|
||||
// escaped again by the page compiler. Decode both layers so structured
|
||||
// component props arrive as valid JSON instead of `"` text.
|
||||
for (let pass = 0; pass < 2; pass += 1) {
|
||||
const next = decoded
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/&/g, "&");
|
||||
if (next === decoded) break;
|
||||
decoded = next;
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
/** Content types worth gzipping (text + text-like application types). */
|
||||
@@ -1554,10 +1607,10 @@ export function fillSlots(html: string, inner: string): string {
|
||||
*/
|
||||
export function parseComponentProps(attrStr: string): Record<string, string> {
|
||||
const props: Record<string, string> = {};
|
||||
for (const m of attrStr.matchAll(/([A-Za-z_][\w-]*)="([^"]*)"/g)) {
|
||||
for (const m of attrStr.matchAll(/([A-Za-z_][\w-]*)(?:="([^"]*)")?/g)) {
|
||||
const key = m[1]!;
|
||||
if (key === "data-component") continue;
|
||||
props[key] = decodeHtmlEntities(m[2]!);
|
||||
props[key] = decodeHtmlEntities(m[2] ?? "");
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,18 @@ import { test, expect } from "bun:test";
|
||||
import { parseComponentProps, resolveTProps } from "../src/runtime.ts";
|
||||
|
||||
test("parseComponentProps extracts quoted attrs, skips data-component", () => {
|
||||
const props = parseComponentProps('data-component="badge" label="New" count="3"');
|
||||
expect(props).toEqual({ label: "New", count: "3" });
|
||||
const props = parseComponentProps(
|
||||
'data-component="badge" label="New" count="3" disabled autofocus',
|
||||
);
|
||||
expect(props).toEqual({ label: "New", count: "3", disabled: "", autofocus: "" });
|
||||
});
|
||||
|
||||
test("parseComponentProps decodes compiler-escaped structured props", () => {
|
||||
const props = parseComponentProps(
|
||||
'data-component="list" items="[{&quot;label&quot;:&quot;First&quot;}]"',
|
||||
);
|
||||
expect(props.items).toBe('[{"label":"First"}]');
|
||||
expect(JSON.parse(props.items!)).toEqual([{ label: "First" }]);
|
||||
});
|
||||
|
||||
test("resolveTProps translates {t:key} markers via the active language", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-toolbar",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/encryption",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/helpers",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/i18n",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/jwt",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/mobile",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/native",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/oauth",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/plugin",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/pubsub",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/queue",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/reactive",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/router",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ssr",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -31,6 +31,12 @@ export interface RenderOptions {
|
||||
extraBody?: string;
|
||||
/** Attributes for the `<html>` element, e.g. ` data-theme="dark"` (trusted). */
|
||||
htmlAttrs?: string;
|
||||
/**
|
||||
* Optional application-authored full document shell. It must contain
|
||||
* `<html>`, `<head>`, and `<body>`. Framework metadata, assets, and scripts
|
||||
* are merged into it instead of wrapping the rendered body again.
|
||||
*/
|
||||
documentTemplate?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,6 +65,25 @@ export function renderDocument(opts: RenderOptions): string {
|
||||
? (opts.htmlAttrs ?? "")
|
||||
: `${opts.htmlAttrs ?? ""} lang="en"`;
|
||||
|
||||
if (opts.documentTemplate) {
|
||||
let document = opts.documentTemplate.trim();
|
||||
if (!/^<!doctype\s+html>/i.test(document)) document = `<!doctype html>\n${document}`;
|
||||
document = document.replace(/<html([^>]*)>/i, (_match, existing: string) => {
|
||||
const language = /(?:^|\s)lang\s*=/.test(`${existing}${opts.htmlAttrs ?? ""}`)
|
||||
? ""
|
||||
: ' lang="en"';
|
||||
return `<html${existing}${opts.htmlAttrs ?? ""}${language}>`;
|
||||
});
|
||||
const headContent = `
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<title>${title}</title>${seoTags}${preloadTags}${extraHead}`;
|
||||
document = document.replace(/<head([^>]*)>/i, `<head$1>${headContent}`);
|
||||
document = document.replace(/<\/body>/i, `${scriptTags}${extraBody}\n </body>`);
|
||||
return `${document}\n`;
|
||||
}
|
||||
|
||||
return `<!doctype html>
|
||||
<html${htmlAttrs}>
|
||||
<head>
|
||||
|
||||
@@ -36,6 +36,24 @@ test("always emits a document language and preserves an explicit language", () =
|
||||
expect(explicit).not.toContain('lang="en"');
|
||||
});
|
||||
|
||||
test("merges framework head and scripts into an application document template", () => {
|
||||
const html = renderDocument({
|
||||
meta: { title: "Document layout" },
|
||||
body: "",
|
||||
htmlAttrs: ' data-theme="dark"',
|
||||
scripts: ["/page.js"],
|
||||
documentTemplate:
|
||||
'<html data-ui-font="system"><head><meta name="custom" content="yes" /></head><body><div id="app">Page</div></body></html>',
|
||||
});
|
||||
expect(html).toStartWith("<!doctype html>");
|
||||
expect(html).toContain('<html data-ui-font="system" data-theme="dark" lang="en">');
|
||||
expect(html).toContain("<title>Document layout</title>");
|
||||
expect(html).toContain('<meta name="custom" content="yes" />');
|
||||
expect(html).toContain('<script type="module" src="/page.js"></script>');
|
||||
expect(html.match(/<html/g)).toHaveLength(1);
|
||||
expect(html.match(/<body/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("streams async body chunks inside the document shell", async () => {
|
||||
async function* body() {
|
||||
yield "<h1>Shell</h1>";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/styles",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/syntax",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -8,6 +8,7 @@ export type {
|
||||
DataApiBlock,
|
||||
DataMode,
|
||||
EffectBlock,
|
||||
EventDecl,
|
||||
LifecycleBlock,
|
||||
LoadBlock,
|
||||
ModeFunctionsBlock,
|
||||
|
||||
@@ -165,6 +165,11 @@ export interface PropDecl {
|
||||
default: string;
|
||||
}
|
||||
|
||||
export interface EventDecl {
|
||||
/** Public event name used by consumers as `@name="handler(event)"`. */
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface PageAst {
|
||||
type: "page";
|
||||
/** Static ES module imports declared before the WRN root declaration. */
|
||||
@@ -183,6 +188,8 @@ export interface PageAst {
|
||||
hydrate?: string;
|
||||
/** Declared component props (empty for pages). */
|
||||
props: PropDecl[];
|
||||
/** Public events exposed by a reusable component. */
|
||||
events: EventDecl[];
|
||||
/** Raw declarations from `types { ... }`, emitted as TypeScript. */
|
||||
types: string[];
|
||||
states: StateDecl[];
|
||||
@@ -289,6 +296,7 @@ export function parse(source: string): PageAst {
|
||||
let runtime: PageAst["runtime"];
|
||||
let hydrate: string | undefined;
|
||||
const props: PropDecl[] = [];
|
||||
const events: EventDecl[] = [];
|
||||
const types: string[] = [];
|
||||
const states: StateDecl[] = [];
|
||||
const computed: ComputedDecl[] = [];
|
||||
@@ -341,12 +349,31 @@ export function parse(source: string): PageAst {
|
||||
break;
|
||||
}
|
||||
case "props": {
|
||||
// props { name: Type = <default> } — omit the default for required props.
|
||||
// props { name: Type = <default>; @event name = function }
|
||||
lx.next();
|
||||
expect("lbrace");
|
||||
while (lx.peek().type !== "rbrace") {
|
||||
const t = lx.peek();
|
||||
if (t.type === "eof") throw new ParseError("Unexpected end of input inside props");
|
||||
if (t.type === "at") {
|
||||
lx.next();
|
||||
const declarationKind = expect("ident");
|
||||
if (declarationKind.value !== "event") {
|
||||
throw new ParseError(
|
||||
`Expected '@event' but got '@${declarationKind.value}' at offset ${declarationKind.pos}`,
|
||||
);
|
||||
}
|
||||
const eventName = expect("ident").value;
|
||||
expect("eq");
|
||||
const marker = lx.readPropInitializer();
|
||||
if (marker !== "function") {
|
||||
throw new ParseError(
|
||||
`Event '${eventName}' must be declared as '@event ${eventName} = function'`,
|
||||
);
|
||||
}
|
||||
events.push({ name: eventName });
|
||||
continue;
|
||||
}
|
||||
if (t.type !== "ident") {
|
||||
throw new ParseError(`Expected a prop name at offset ${t.pos}`);
|
||||
}
|
||||
@@ -627,6 +654,7 @@ export function parse(source: string): PageAst {
|
||||
runtime,
|
||||
hydrate,
|
||||
props,
|
||||
events,
|
||||
types,
|
||||
states,
|
||||
computed,
|
||||
|
||||
@@ -92,3 +92,17 @@ test("parses keyed each blocks without changing legacy loop syntax", () => {
|
||||
expect.objectContaining({ type: "each", key: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
test("parses public component event declarations inside props", () => {
|
||||
const ast = parse(`component SearchBox {
|
||||
props {
|
||||
value = ""
|
||||
@event search = function
|
||||
@event clear = function
|
||||
}
|
||||
view { <input value="{value}" /> }
|
||||
}`);
|
||||
|
||||
expect(ast.props.map((prop) => prop.name)).toEqual(["value"]);
|
||||
expect(ast.events).toEqual([{ name: "search" }, { name: "clear" }]);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/test",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -95,6 +95,10 @@ export function mountHtml(html: string): {
|
||||
g.window = win;
|
||||
g.document = win.document;
|
||||
g.NodeFilter = win.NodeFilter;
|
||||
g.HTMLInputElement = win.HTMLInputElement;
|
||||
g.HTMLOptionElement = win.HTMLOptionElement;
|
||||
g.HTMLSelectElement = win.HTMLSelectElement;
|
||||
g.HTMLTextAreaElement = win.HTMLTextAreaElement;
|
||||
(0, eval)(getReactiveRuntime());
|
||||
(win as { __wrnexusHydrateScopes?: (root: unknown) => void }).__wrnexusHydrateScopes?.(
|
||||
win.document,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/tracking",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
+583
-7938
File diff suppressed because it is too large
Load Diff
+312
-4056
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"generatedFrom": "full component catalog reset",
|
||||
"removedCount": 0,
|
||||
"replacements": {}
|
||||
}
|
||||
+3109
-45876
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
||||
component AcceptAllCookiesButton {
|
||||
props {
|
||||
label = "Action"
|
||||
href = ""
|
||||
type = "button"
|
||||
variant = "primary"
|
||||
disabled = false
|
||||
class = ""
|
||||
}
|
||||
view { <span class="wire-catalog-action {class}">{#if href}<a href="{href}" class="wire-btn wire-btn--{variant}">{label}<slot /></a>{:else}<button type="{type}" disabled="{disabled}" class="wire-btn wire-btn--{variant}">{label}<slot /></button>{/if}</span> }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
component AccessibleAccordion {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view { <article class="wire-card wire-catalog-card {class}">{#if image}<img src="{image}" alt="{alt}" class="wire-catalog-card__image" />{/if}{#if eyebrow}<span class="wire-eyebrow">{eyebrow}</span>{/if}{#if title}<h3>{title}</h3>{/if}{#if description}<p>{description}</p>{/if}<slot />{#if href}<a href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a>{/if}</article> }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
component AccessibleCarousel {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view { <article class="wire-card wire-catalog-card {class}">{#if image}<img src="{image}" alt="{alt}" class="wire-catalog-card__image" />{/if}{#if eyebrow}<span class="wire-eyebrow">{eyebrow}</span>{/if}{#if title}<h3>{title}</h3>{/if}{#if description}<p>{description}</p>{/if}<slot />{#if href}<a href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a>{/if}</article> }
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
component AccessibleChartSummary {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<article class="wire-card wire-catalog-card {class}"><div class="grid gap-3 rounded-xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-950/60"><div class="flex items-center gap-2 text-sm font-bold"><span class="icon-[lucide--chart-no-axes-combined] size-5 text-violet-500" aria-hidden="true"></span>Chart summary</div><dl class="grid gap-3"><div class="flex items-center justify-between gap-4"><dt class="text-sm text-slate-500">Delivered</dt><dd class="font-bold">96.8%</dd></div><div class="flex items-center justify-between gap-4"><dt class="text-sm text-slate-500">Engaged</dt><dd class="font-bold">42.1%</dd></div><div class="flex items-center justify-between gap-4"><dt class="text-sm text-slate-500">Converted</dt><dd class="font-bold">8.7%</dd></div></dl></div>{#if eyebrow}<span class="wire-eyebrow">{eyebrow}</span>{/if}{#if title}<h3>{title}</h3>{/if}{#if description}<p>{description}</p>{/if}<slot />{#if href}<a href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a>{/if}</article>
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component AccessibleDialog {
|
||||
props {
|
||||
title = ""
|
||||
description = ""
|
||||
open = false
|
||||
closeLabel = "Close"
|
||||
class = ""
|
||||
}
|
||||
view { <div data-show="open" class="wire-overlay wire-catalog-overlay {class}"><section role="dialog" aria-modal="true" aria-label="{title}" class="wire-dialog"><header><h2 data-show="title" class="wire-dialog__title">{title}</h2><p data-show="description" class="wire-dialog__description">{description}</p></header><slot /><button type="button" class="wire-btn wire-btn--ghost">{closeLabel}</button></section></div> }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
component AccessibleErrorSummary {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view { <article class="wire-card wire-catalog-card {class}">{#if image}<img src="{image}" alt="{alt}" class="wire-catalog-card__image" />{/if}{#if eyebrow}<span class="wire-eyebrow">{eyebrow}</span>{/if}{#if title}<h3>{title}</h3>{/if}{#if description}<p>{description}</p>{/if}<slot />{#if href}<a href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a>{/if}</article> }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
component AccessibleIcon {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view { <article class="wire-card wire-catalog-card {class}">{#if image}<img src="{image}" alt="{alt}" class="wire-catalog-card__image" />{/if}{#if eyebrow}<span class="wire-eyebrow">{eyebrow}</span>{/if}{#if title}<h3>{title}</h3>{/if}{#if description}<p>{description}</p>{/if}<slot />{#if href}<a href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a>{/if}</article> }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component AccessibleMenu {
|
||||
props {
|
||||
title = ""
|
||||
description = ""
|
||||
open = false
|
||||
closeLabel = "Close"
|
||||
class = ""
|
||||
}
|
||||
view { <div data-show="open" class="wire-overlay wire-catalog-overlay {class}"><section role="dialog" aria-modal="true" aria-label="{title}" class="wire-dialog"><header><h2 data-show="title" class="wire-dialog__title">{title}</h2><p data-show="description" class="wire-dialog__description">{description}</p></header><slot /><button type="button" class="wire-btn wire-btn--ghost">{closeLabel}</button></section></div> }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
component AccessibleTabs {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view { <article class="wire-card wire-catalog-card {class}">{#if image}<img src="{image}" alt="{alt}" class="wire-catalog-card__image" />{/if}{#if eyebrow}<span class="wire-eyebrow">{eyebrow}</span>{/if}{#if title}<h3>{title}</h3>{/if}{#if description}<p>{description}</p>{/if}<slot />{#if href}<a href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a>{/if}</article> }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
component AccessibleTooltip {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view { <article class="wire-card wire-catalog-card {class}">{#if image}<img src="{image}" alt="{alt}" class="wire-catalog-card__image" />{/if}{#if eyebrow}<span class="wire-eyebrow">{eyebrow}</span>{/if}{#if title}<h3>{title}</h3>{/if}{#if description}<p>{description}</p>{/if}<slot />{#if href}<a href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a>{/if}</article> }
|
||||
}
|
||||
@@ -1,31 +1,15 @@
|
||||
component Accordion {
|
||||
props {
|
||||
class = ""
|
||||
title = "Question"
|
||||
open = false
|
||||
}
|
||||
|
||||
state expanded = open
|
||||
|
||||
view {
|
||||
<div class="border-b border-slate-200 dark:border-slate-800 {class}" >
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between gap-4 py-5 text-left font-semibold"
|
||||
aria-expanded="{expanded}"
|
||||
@click="expanded = !expanded"
|
||||
>
|
||||
<span >
|
||||
{title}
|
||||
</span>
|
||||
<span class="text-xl transition-transform {expanded ? 'rotate-45' : ''}">+</span>
|
||||
</button>
|
||||
<div
|
||||
data-show="expanded"
|
||||
class="pb-5 text-slate-600 dark:text-slate-300"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
items = []
|
||||
multiple = false
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--accordion {class}">
|
||||
{#each items as item}<details open="{item.open}"><summary>{item.label}</summary><div>{item.content}</div></details>{/each}
|
||||
<slot />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
component AccountMenu {
|
||||
props {
|
||||
title = ""
|
||||
description = ""
|
||||
open = false
|
||||
closeLabel = "Close"
|
||||
class = ""
|
||||
}
|
||||
view { <div data-show="open" class="wire-overlay wire-catalog-overlay {class}"><section role="dialog" aria-modal="true" aria-label="{title}" class="wire-dialog"><header><h2 data-show="title" class="wire-dialog__title">{title}</h2><p data-show="description" class="wire-dialog__description">{description}</p></header><slot /><button type="button" class="wire-btn wire-btn--ghost">{closeLabel}</button></section></div> }
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
component AccountStatusBanner {
|
||||
props {
|
||||
label = "AccountStatus"
|
||||
title = ""
|
||||
description = ""
|
||||
value = ""
|
||||
variant = "default"
|
||||
class = ""
|
||||
}
|
||||
view { <aside role="status" class="wire-alert wire-alert--{variant} wire-catalog-feedback {class}"><strong data-show="title || label">{title || label}</strong><span data-show="value" class="wire-badge wire-badge--{variant}">{value}</span><p data-show="description">{description}</p><slot /></aside> }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component ActionMenu {
|
||||
props {
|
||||
title = ""
|
||||
description = ""
|
||||
open = false
|
||||
closeLabel = "Close"
|
||||
class = ""
|
||||
}
|
||||
view { <div data-show="open" class="wire-overlay wire-catalog-overlay {class}"><section role="dialog" aria-modal="true" aria-label="{title}" class="wire-dialog"><header><h2 data-show="title" class="wire-dialog__title">{title}</h2><p data-show="description" class="wire-dialog__description">{description}</p></header><slot /><button type="button" class="wire-btn wire-btn--ghost">{closeLabel}</button></section></div> }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component ActiveFilterList {
|
||||
props {
|
||||
title = ""
|
||||
description = ""
|
||||
empty = false
|
||||
emptyText = "No items available."
|
||||
class = ""
|
||||
}
|
||||
view { <section class="wire-catalog-collection {class}"><header data-show="title || description"><h2 data-show="title">{title}</h2><p data-show="description">{description}</p></header><div data-show="!empty" role="list" class="wire-catalog-collection__items"><slot /></div><p data-show="empty" role="status" class="wire-muted">{emptyText}</p></section> }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component ActivityList {
|
||||
props {
|
||||
title = ""
|
||||
description = ""
|
||||
empty = false
|
||||
emptyText = "No items available."
|
||||
class = ""
|
||||
}
|
||||
view { <section class="wire-catalog-collection {class}"><header data-show="title || description"><h2 data-show="title">{title}</h2><p data-show="description">{description}</p></header><div data-show="!empty" role="list" class="wire-catalog-collection__items"><slot /></div><p data-show="empty" role="status" class="wire-muted">{emptyText}</p></section> }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
component AddOnCard {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view { <article class="wire-card wire-catalog-card {class}">{#if image}<img src="{image}" alt="{alt}" class="wire-catalog-card__image" />{/if}{#if eyebrow}<span class="wire-eyebrow">{eyebrow}</span>{/if}{#if title}<h3>{title}</h3>{/if}{#if description}<p>{description}</p>{/if}<slot />{#if href}<a href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a>{/if}</article> }
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
component AddressInput {
|
||||
props {
|
||||
label = "Address"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
type = "text"
|
||||
required = false
|
||||
disabled = false
|
||||
readonly = false
|
||||
help = ""
|
||||
error = ""
|
||||
class = ""
|
||||
}
|
||||
view { <label class="wire-field wire-catalog-field {class}"><span class="wire-label">{label}<span data-show="required" class="wire-required"> *</span></span><input name="{name}" value="{value}" placeholder="{placeholder}" type="{type}" required="{required}" disabled="{disabled}" readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" class="wire-input" /><span data-show="help && !error" class="wire-field-message wire-field-message--help">{help}</span><span data-show="error" role="alert" class="wire-field-message wire-field-message--error">{error}</span><slot /></label> }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
component AddressPreview {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view { <article class="wire-card wire-catalog-card {class}">{#if image}<img src="{image}" alt="{alt}" class="wire-catalog-card__image" />{/if}{#if eyebrow}<span class="wire-eyebrow">{eyebrow}</span>{/if}{#if title}<h3>{title}</h3>{/if}{#if description}<p>{description}</p>{/if}<slot />{#if href}<a href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a>{/if}</article> }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
component AdvancedDatePicker {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Advanced Date Picker"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-date-picker wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<slot />
|
||||
</section>
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
component AdvancedFilterBuilder {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view { <article class="wire-card wire-catalog-card {class}">{#if image}<img src="{image}" alt="{alt}" class="wire-catalog-card__image" />{/if}{#if eyebrow}<span class="wire-eyebrow">{eyebrow}</span>{/if}{#if title}<h3>{title}</h3>{/if}{#if description}<p>{description}</p>{/if}<slot />{#if href}<a href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a>{/if}</article> }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
component AdvancedRangeSlider {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
title = "Advanced Range Slider"
|
||||
description = ""
|
||||
items = []
|
||||
variant = "default"
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<section class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-range-slider wire-next--variant-{variant} {class}">
|
||||
{#if title}<strong>{title}</strong>{/if}
|
||||
{#if description}<p>{description}</p>{/if}
|
||||
{#if items}<div class="wire-next__items">{#each items as item}<span>{item.label}</span>{/each}</div>{/if}
|
||||
<slot />
|
||||
</section>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
component AdvancedSelect {
|
||||
props {
|
||||
size = "default"
|
||||
color = "primary"
|
||||
label = "Advanced Select"
|
||||
name = ""
|
||||
value = ""
|
||||
values = []
|
||||
options = []
|
||||
groups = []
|
||||
placeholder = "Select an option"
|
||||
placeholderIcon = ""
|
||||
searchPlaceholder = "Search options…"
|
||||
multiple = false
|
||||
searchable = true
|
||||
defaultOpen = false
|
||||
clearable = true
|
||||
allowEmpty = true
|
||||
tags = false
|
||||
disabled = false
|
||||
required = false
|
||||
invalid = false
|
||||
validationMessage = ""
|
||||
helpText = ""
|
||||
loading = false
|
||||
loadingLabel = "Loading options…"
|
||||
emptyLabel = "No options found"
|
||||
selectedOptionsLabel = "Selected options"
|
||||
clearLabel = "Clear selection"
|
||||
createLabel = "Create"
|
||||
loadMoreLabel = "Load more"
|
||||
searchMode = "contains"
|
||||
searchFields = "label,description"
|
||||
minSearchLength = 0
|
||||
searchResultLimit = 0
|
||||
maxSelections = 0
|
||||
showCounter = false
|
||||
counterTemplate = "{selected} selected"
|
||||
optionTemplate = "default"
|
||||
selectedTemplate = "default"
|
||||
closeOnSelect = true
|
||||
scrollToSelected = true
|
||||
fixed = false
|
||||
placement = "bottom"
|
||||
remote = false
|
||||
remoteUrl = ""
|
||||
remoteQueryParam = "q"
|
||||
remoteDebounce = 250
|
||||
remoteAutoLoad = true
|
||||
infinite = false
|
||||
hasMore = false
|
||||
page = 1
|
||||
class = ""
|
||||
@event search = function
|
||||
@event select = function
|
||||
@event change = function
|
||||
@event clear = function
|
||||
@event open = function
|
||||
@event close = function
|
||||
@event load = function
|
||||
@event error = function
|
||||
}
|
||||
|
||||
state open = defaultOpen
|
||||
state query = ""
|
||||
state activeIndex = -1
|
||||
state selectedValue = value
|
||||
state selectedValues = values
|
||||
|
||||
functions {
|
||||
function allOptions() {
|
||||
return [...groups.flatMap((group) => group.options || []), ...options]
|
||||
}
|
||||
|
||||
function searchableText(option) {
|
||||
return ((option.label || "") + " " + (option.description || "")).toLowerCase()
|
||||
}
|
||||
|
||||
function matches(option) {
|
||||
if (!query || query.length < Number(minSearchLength || 0)) {
|
||||
return true
|
||||
}
|
||||
if (searchMode === "startsWith") {
|
||||
return searchableText(option).startsWith(query.toLowerCase())
|
||||
}
|
||||
if (searchMode === "exact") {
|
||||
return searchableText(option) === query.toLowerCase()
|
||||
}
|
||||
return searchableText(option).includes(query.toLowerCase())
|
||||
}
|
||||
|
||||
function matchingOptions(list) {
|
||||
return list.filter((option) => matches(option))
|
||||
}
|
||||
|
||||
function visibleOptions(list) {
|
||||
if (searchResultLimit > 0) {
|
||||
return matchingOptions(list).slice(0, Number(searchResultLimit))
|
||||
}
|
||||
return matchingOptions(list)
|
||||
}
|
||||
|
||||
function flatVisibleOptions() {
|
||||
return visibleOptions(allOptions())
|
||||
}
|
||||
|
||||
function isSelected(option) {
|
||||
return (
|
||||
multiple
|
||||
? selectedValues.includes(option.value)
|
||||
: selectedValue === option.value
|
||||
)
|
||||
}
|
||||
|
||||
function selectedOptions() {
|
||||
return allOptions().filter((option) => isSelected(option))
|
||||
}
|
||||
|
||||
function selectedCount() {
|
||||
return multiple ? selectedValues.length : selectedValue ? 1 : 0
|
||||
}
|
||||
|
||||
function counterText() {
|
||||
return (
|
||||
maxSelections
|
||||
? selectedCount() + " / " + maxSelections + " selected"
|
||||
: selectedCount() + " selected"
|
||||
)
|
||||
}
|
||||
|
||||
function selectedText() {
|
||||
return (
|
||||
multiple
|
||||
? selectedValues.join(", ")
|
||||
: selectedOptions().length
|
||||
? selectedOptions()[0].label
|
||||
: ""
|
||||
)
|
||||
}
|
||||
|
||||
function triggerText() {
|
||||
return selectedCount() ? selectedText() : placeholder
|
||||
}
|
||||
|
||||
function canSelect(option) {
|
||||
if (option.disabled) {
|
||||
return false
|
||||
}
|
||||
if (!multiple || isSelected(option)) {
|
||||
return true
|
||||
}
|
||||
if (maxSelections <= 0) {
|
||||
return true
|
||||
}
|
||||
return selectedCount() < maxSelections
|
||||
}
|
||||
|
||||
function chooseSingle(option) {
|
||||
if (!canSelect(option)) {
|
||||
return
|
||||
}
|
||||
selectedValue = option.value
|
||||
query = ""
|
||||
if (closeOnSelect) {
|
||||
open = false
|
||||
}
|
||||
}
|
||||
|
||||
function chooseMultiple(option) {
|
||||
if (!canSelect(option)) {
|
||||
return
|
||||
}
|
||||
if (isSelected(option)) {
|
||||
selectedValues = selectedValues.filter((item) => item !== option.value)
|
||||
} else {
|
||||
selectedValues = selectedValues.concat([option.value])
|
||||
}
|
||||
query = ""
|
||||
}
|
||||
|
||||
function chooseOption(option) {
|
||||
if (multiple) {
|
||||
chooseMultiple(option)
|
||||
return
|
||||
}
|
||||
chooseSingle(option)
|
||||
}
|
||||
|
||||
function clearSelection(event) {
|
||||
event.stopPropagation()
|
||||
selectedValue = ""
|
||||
selectedValues = []
|
||||
query = ""
|
||||
open = false
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (disabled) {
|
||||
return;
|
||||
};
|
||||
open = !open;
|
||||
activeIndex = open && flatVisibleOptions().length ? 0 : -1;
|
||||
}
|
||||
|
||||
function moveActive(direction) {
|
||||
if (!flatVisibleOptions().length) {
|
||||
return
|
||||
}
|
||||
activeIndex = (activeIndex + direction + flatVisibleOptions().length) % flatVisibleOptions().length
|
||||
}
|
||||
|
||||
function handleKeydown(event) {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault()
|
||||
if (!open) {
|
||||
open = true
|
||||
}
|
||||
moveActive(1)
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault()
|
||||
if (!open) {
|
||||
open = true
|
||||
}
|
||||
moveActive(-1)
|
||||
} else if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault()
|
||||
if (!open) {
|
||||
open = true
|
||||
} else if (activeIndex >= 0) {
|
||||
chooseOption(flatVisibleOptions()[activeIndex])
|
||||
}
|
||||
} else if (event.key === "Escape") {
|
||||
open = false
|
||||
} else if (event.key === "Home" && open) {
|
||||
event.preventDefault()
|
||||
activeIndex = 0
|
||||
} else if (event.key === "End" && open) {
|
||||
event.preventDefault()
|
||||
activeIndex = flatVisibleOptions().length - 1
|
||||
}
|
||||
}
|
||||
|
||||
function optionIndex(option) {
|
||||
return flatVisibleOptions().findIndex((item) => item.value === option.value)
|
||||
}
|
||||
}
|
||||
|
||||
view {
|
||||
<div
|
||||
class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--advanced-select {open ? 'wire-next--open' : ''} {fixed ? 'wire-next--advanced-select-fixed' : ''} {invalid ? 'wire-next--invalid' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
|
||||
data-placement="{placement}"
|
||||
data-search-mode="{searchMode}"
|
||||
data-remote="{remote ? 'true' : 'false'}"
|
||||
data-remote-url="{remoteUrl}"
|
||||
data-remote-query-param="{remoteQueryParam}"
|
||||
data-remote-debounce="{remoteDebounce}"
|
||||
data-remote-auto-load="{remoteAutoLoad ? 'true' : 'false'}"
|
||||
data-infinite="{infinite ? 'true' : 'false'}"
|
||||
data-page="{page}"
|
||||
data-wrn-select
|
||||
@focusout="if (!event.currentTarget.contains(event.relatedTarget)) { open = false }"
|
||||
>
|
||||
<div class="wire-next__row">
|
||||
<label id="{name}-label" for="{name}-trigger">{label}</label>
|
||||
<small data-show="showCounter" data-text="counterText()">{counterText()}</small>
|
||||
</div>
|
||||
|
||||
<div class="wire-next__select-control">
|
||||
<button
|
||||
{...attrs}
|
||||
id="{name}-trigger"
|
||||
type="button"
|
||||
class="wire-next__select-trigger"
|
||||
role="combobox"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded="{open}"
|
||||
aria-controls="{name}-listbox"
|
||||
aria-labelledby="{name}-label"
|
||||
aria-invalid="{invalid}"
|
||||
disabled="{disabled}"
|
||||
@click="toggle()"
|
||||
@keydown="handleKeydown(event)"
|
||||
>
|
||||
<span class="wire-next__select-value">
|
||||
<span data-show="!multiple" data-text="triggerText()">{triggerText()}</span>
|
||||
<span
|
||||
data-show="multiple && selectedTemplate === 'count' && selectedCount() > 0"
|
||||
data-text="selectedCount() + ' selected'"
|
||||
>{selectedCount()} selected</span>
|
||||
<span
|
||||
data-show="multiple && selectedTemplate === 'text' && selectedCount() > 0"
|
||||
data-text="selectedText()"
|
||||
>{selectedText()}</span>
|
||||
<span
|
||||
class="wire-next__select-tags"
|
||||
aria-label="{selectedOptionsLabel}"
|
||||
data-show="multiple && selectedTemplate !== 'count' && selectedTemplate !== 'text' && selectedCount() > 0"
|
||||
>
|
||||
{#each allOptions() as option}
|
||||
<span data-show="isSelected(option)">
|
||||
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
|
||||
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
|
||||
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
|
||||
{option.label}
|
||||
</span>
|
||||
{/each}
|
||||
</span>
|
||||
<span data-show="multiple && selectedCount() === 0">
|
||||
{#if placeholderIcon}<i class="{placeholderIcon}" aria-hidden="true"></i>{/if}
|
||||
<span class="wire-next__placeholder">{placeholder}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="wire-next__select-chevron icon-[lucide--chevrons-up-down]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="wire-next__clear-select"
|
||||
data-show="clearable && allowEmpty && selectedCount() > 0 && !disabled"
|
||||
aria-label="{clearLabel}"
|
||||
title="{clearLabel}"
|
||||
@click="clearSelection(event)"
|
||||
>
|
||||
<span class="icon-[lucide--x]" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="wire-next__select-dropdown {fixed ? 'wire-next__select-dropdown--fixed' : ''}"
|
||||
data-show="open"
|
||||
style="{open ? '' : 'display: none'}"
|
||||
>
|
||||
{#if searchable}
|
||||
<label class="wire-next__select-search">
|
||||
<span class="wire-visually-hidden">{searchPlaceholder}</span>
|
||||
<span class="wire-next__search-icon" aria-hidden="true">⌕</span>
|
||||
<input
|
||||
type="search"
|
||||
value="{query}"
|
||||
placeholder="{searchPlaceholder}"
|
||||
autocomplete="off"
|
||||
@input="query = event.target.value; activeIndex = flatVisibleOptions().length ? 0 : -1"
|
||||
@keydown="handleKeydown(event)"
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="wire-next__select-message"
|
||||
data-show="remote && query.length !== 0 && query.length < minSearchLength"
|
||||
>Enter at least {minSearchLength} characters.</div>
|
||||
<div class="wire-next__select-message" data-show="loading" role="status">
|
||||
<i class="wire-spinner wire-spinner--inline" aria-hidden="true"></i>
|
||||
{loadingLabel}
|
||||
</div>
|
||||
<div
|
||||
id="{name}-listbox"
|
||||
class="wire-next__select-list"
|
||||
data-show="(!remote || (query.length === 0 && remoteAutoLoad) || query.length >= minSearchLength) && !loading"
|
||||
role="listbox"
|
||||
aria-multiselectable="{multiple}"
|
||||
>
|
||||
{#each groups as group}
|
||||
{#if visibleOptions(group.options || []).length}
|
||||
<div class="wire-next__option-group" role="group" aria-label="{group.label}">
|
||||
<div class="wire-next__group-label">{group.label}</div>
|
||||
{#each group.options || [] as option}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
|
||||
data-option-value="{option.value}"
|
||||
data-show="matches(option)"
|
||||
role="option"
|
||||
aria-selected="{isSelected(option)}"
|
||||
disabled="{!canSelect(option)}"
|
||||
@mouseenter="activeIndex = optionIndex(option)"
|
||||
@click="chooseOption(option)"
|
||||
>
|
||||
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
|
||||
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
|
||||
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
|
||||
<span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
|
||||
<i
|
||||
class="wire-next__check icon-[lucide--check]"
|
||||
data-show="isSelected(option)"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#each options as option}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-next__select-option {isSelected(option) ? 'wire-next__select-option--selected' : ''} {optionIndex(option) === activeIndex ? 'wire-next__select-option--active' : ''}"
|
||||
data-option-value="{option.value}"
|
||||
data-show="matches(option)"
|
||||
role="option"
|
||||
aria-selected="{isSelected(option)}"
|
||||
disabled="{!canSelect(option)}"
|
||||
@mouseenter="activeIndex = optionIndex(option)"
|
||||
@click="chooseOption(option)"
|
||||
>
|
||||
{#if optionTemplate === "icon" && option.icon}<i class="{option.icon}" aria-hidden="true"></i>{/if}
|
||||
{#if optionTemplate === "avatar" && option.avatar}<img src="{option.avatar}" alt="" />{/if}
|
||||
{#if optionTemplate === "color" && option.color}<i class="wire-next__color-dot" style="--option-color: {option.color}"></i>{/if}
|
||||
<span><strong>{option.label}</strong>{#if option.description}<small>{option.description}</small>{/if}</span>
|
||||
<i
|
||||
class="wire-next__check icon-[lucide--check]"
|
||||
data-show="isSelected(option)"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
{#if !loading && !flatVisibleOptions().length}
|
||||
<div class="wire-next__select-message">{emptyLabel}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if tags && query && !allOptions().some((option) => option.label.toLowerCase() === query.toLowerCase())}
|
||||
<button type="button" class="wire-next__create-option">{createLabel} “{query}”</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="wire-next__load-more"
|
||||
data-wrn-select-load-more
|
||||
data-show="infinite && hasMore"
|
||||
>{loadMoreLabel}</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
name="{name}"
|
||||
value="{multiple ? selectedValues.join(',') : selectedValue}"
|
||||
required="{required}"
|
||||
aria-describedby="{validationMessage ? `${name}-validation` : helpText ? `${name}-help` : ''}"
|
||||
/>
|
||||
|
||||
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
|
||||
<small
|
||||
id="{name}-validation"
|
||||
class="wire-next__validation"
|
||||
data-error="{name}"
|
||||
>{validationMessage}</small>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component AlertDialog {
|
||||
props {
|
||||
title = ""
|
||||
description = ""
|
||||
open = false
|
||||
closeLabel = "Close"
|
||||
class = ""
|
||||
}
|
||||
view { <div data-show="open" class="wire-overlay wire-catalog-overlay {class}"><section role="dialog" aria-modal="true" aria-label="{title}" class="wire-dialog"><header><h2 data-show="title" class="wire-dialog__title">{title}</h2><p data-show="description" class="wire-dialog__description">{description}</p></header><slot /><button type="button" class="wire-btn wire-btn--ghost">{closeLabel}</button></section></div> }
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
component AmountInput {
|
||||
props {
|
||||
label = "Amount"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
type = "text"
|
||||
required = false
|
||||
disabled = false
|
||||
readonly = false
|
||||
help = ""
|
||||
error = ""
|
||||
class = ""
|
||||
}
|
||||
view { <label class="wire-field wire-catalog-field {class}"><span class="wire-label">{label}<span data-show="required" class="wire-required"> *</span></span><input name="{name}" value="{value}" placeholder="{placeholder}" type="{type}" required="{required}" disabled="{disabled}" readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" class="wire-input" /><span data-show="help && !error" class="wire-field-message wire-field-message--help">{help}</span><span data-show="error" role="alert" class="wire-field-message wire-field-message--error">{error}</span><slot /></label> }
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
component AnalyticsDashboardPreview {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
|
||||
view {
|
||||
<section class="wire-analytics-preview {class}">
|
||||
<div class="wire-analytics-preview__surface">
|
||||
<div class="wire-analytics-preview__visual">
|
||||
{#if image}
|
||||
<img
|
||||
src="{image}"
|
||||
alt="{alt}"
|
||||
class="wire-analytics-preview__image"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="wire-analytics-preview__chart"
|
||||
role="img"
|
||||
aria-label="Delivery trend rising over seven days"
|
||||
>
|
||||
<div class="wire-analytics-preview__chart-header">
|
||||
<strong>Delivery trend</strong>
|
||||
<span>↗ 18.4%</span>
|
||||
</div>
|
||||
|
||||
<svg
|
||||
class="wire-analytics-preview__chart-svg"
|
||||
viewBox="0 0 700 250"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="wire-analytics-area" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="currentColor" stop-opacity=".3"></stop>
|
||||
<stop offset="100%" stop-color="currentColor" stop-opacity="0"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g fill="none" stroke="var(--wire-color-border,#e2e8f0)">
|
||||
<path d="M20 50H680M20 115H680M20 180H680M20 230H680"></path>
|
||||
</g>
|
||||
<path fill="url(#wire-analytics-area)" d="M20 210 C100 195 120 150 190 165 S310 195 370 120 S475 130 525 100 S610 90 680 32 L680 230 L20 230Z"></path>
|
||||
<path fill="none" stroke="currentColor" stroke-width="6" stroke-linecap="round" d="M20 210 C100 195 120 150 190 165 S310 195 370 120 S475 130 525 100 S610 90 680 32"></path>
|
||||
</svg>
|
||||
|
||||
<div class="wire-analytics-preview__days">
|
||||
<span>Mon</span><span>Tue</span><span>Wed</span><span>Thu</span><span>Fri</span><span>Sat</span><span>Sun</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="wire-analytics-preview__content">
|
||||
{#if eyebrow}
|
||||
<p class="wire-analytics-preview__eyebrow">{eyebrow}</p>
|
||||
{/if}
|
||||
|
||||
{#if title}
|
||||
<h2 class="wire-analytics-preview__title">{title}</h2>
|
||||
{/if}
|
||||
|
||||
{#if description}
|
||||
<p class="wire-analytics-preview__description">{description}</p>
|
||||
{/if}
|
||||
|
||||
<div class="wire-analytics-preview__metrics"><slot /></div>
|
||||
|
||||
{#if href}
|
||||
<a href="{href}" class="wire-analytics-preview__action">
|
||||
{actionLabel}<span aria-hidden="true">→</span>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component AnchorNavigation {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
align = "start"
|
||||
class = ""
|
||||
}
|
||||
view { <section class="wire-catalog-section wire-text--{align} {class}"><header data-show="eyebrow || title || description"><span data-show="eyebrow" class="wire-eyebrow">{eyebrow}</span><h2 data-show="title">{title}</h2><p data-show="description" class="wire-muted">{description}</p></header><slot /></section> }
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
component AnnouncementBar {
|
||||
props {
|
||||
class: string = ""
|
||||
badge: string = "New"
|
||||
message: string = "WrNexus Organizations is now available."
|
||||
description: string = "Build secure multi-tenant applications with teams, roles, domains, and enterprise SSO."
|
||||
href: string = "/organizations"
|
||||
actionLabel: string = "Explore organizations"
|
||||
ariaLabel: string = "Announcement"
|
||||
badgeIcon: string = "icon-[lucide--sparkles]"
|
||||
actionIcon: string = "icon-[lucide--arrow-right]"
|
||||
dismissLabel: string = "Dismiss announcement"
|
||||
showBadge: boolean = true
|
||||
showDescription: boolean = true
|
||||
showAction: boolean = true
|
||||
dismissible: boolean = true
|
||||
}
|
||||
|
||||
state visible = true
|
||||
|
||||
view {
|
||||
<aside
|
||||
role="region"
|
||||
aria-label="{ariaLabel}"
|
||||
class="relative z-[70] overflow-hidden border-b border-indigo-200 bg-indigo-50 text-slate-900 dark:border-indigo-500/20 dark:bg-indigo-500/10 dark:text-white {class}"
|
||||
class:hidden="!visible"
|
||||
>
|
||||
<!-- Decorative background -->
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none absolute inset-0 overflow-hidden"
|
||||
>
|
||||
<div class="absolute -left-20 -top-24 h-40 w-40 rounded-full bg-indigo-300/30 blur-3xl dark:bg-indigo-500/10"></div>
|
||||
|
||||
<div class="absolute -right-20 -bottom-24 h-40 w-40 rounded-full bg-violet-300/30 blur-3xl dark:bg-violet-500/10"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="relative flex min-h-11 w-full items-center justify-between gap-4 px-4 py-2.5 sm:px-6 lg:px-10 xl:px-12"
|
||||
>
|
||||
<div class="flex min-w-0 flex-1 items-center justify-center gap-3">
|
||||
<!-- Badge -->
|
||||
<span data-show="showBadge"
|
||||
class="hidden shrink-0 items-center gap-1.5 rounded-full bg-indigo-600 px-2.5 py-1 text-[10px] font-bold uppercase tracking-[0.12em] text-white shadow-sm sm:inline-flex"
|
||||
>
|
||||
<span class="{badgeIcon} h-3 w-3" aria-hidden="true" ></span>
|
||||
|
||||
{badge}
|
||||
</span>
|
||||
|
||||
<!-- Message -->
|
||||
<p
|
||||
class="min-w-0 text-center text-xs font-medium leading-5 text-slate-700 dark:text-slate-300 sm:text-sm"
|
||||
>
|
||||
<span class="font-semibold text-slate-950 dark:text-white">
|
||||
{message}
|
||||
</span>
|
||||
|
||||
<span data-show="showDescription" class="hidden md:inline">
|
||||
{description}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<!-- CTA -->
|
||||
<a data-show="showAction"
|
||||
href="{href}"
|
||||
class="group hidden shrink-0 items-center gap-1.5 text-xs font-bold text-indigo-700 transition hover:text-indigo-900 dark:text-indigo-300 dark:hover:text-indigo-200 sm:inline-flex"
|
||||
>
|
||||
{actionLabel}
|
||||
|
||||
<span class="{actionIcon} h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" aria-hidden="true" ></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile CTA -->
|
||||
<a data-show="showAction"
|
||||
href="{href}"
|
||||
aria-label="{actionLabel}"
|
||||
class="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-indigo-100 text-indigo-700 transition hover:bg-indigo-200 dark:bg-indigo-500/15 dark:text-indigo-300 dark:hover:bg-indigo-500/25 sm:hidden"
|
||||
>
|
||||
<span class="icon-[lucide--arrow-up-right] h-4 w-4" aria-hidden="true" ></span>
|
||||
</a>
|
||||
|
||||
<!-- Close -->
|
||||
<button data-show="dismissible"
|
||||
type="button"
|
||||
@click="visible = false"
|
||||
aria-label="{dismissLabel}"
|
||||
class="grid h-8 w-8 shrink-0 place-items-center rounded-lg text-slate-500 transition hover:bg-indigo-100 hover:text-slate-950 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-400 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
>
|
||||
<span class="icon-[lucide--x] h-4 w-4" aria-hidden="true" ></span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
component ApartmentInput {
|
||||
props {
|
||||
label = "Apartment"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
type = "text"
|
||||
required = false
|
||||
disabled = false
|
||||
readonly = false
|
||||
help = ""
|
||||
error = ""
|
||||
class = ""
|
||||
}
|
||||
view { <label class="wire-field wire-catalog-field {class}"><span class="wire-label">{label}<span data-show="required" class="wire-required"> *</span></span><input name="{name}" value="{value}" placeholder="{placeholder}" type="{type}" required="{required}" disabled="{disabled}" readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" class="wire-input" /><span data-show="help && !error" class="wire-field-message wire-field-message--help">{help}</span><span data-show="error" role="alert" class="wire-field-message wire-field-message--error">{error}</span><slot /></label> }
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
component ApiAuthenticationNotice {
|
||||
props {
|
||||
label = "ApiAuthentication"
|
||||
title = ""
|
||||
description = ""
|
||||
value = ""
|
||||
variant = "default"
|
||||
class = ""
|
||||
}
|
||||
view { <aside role="status" class="wire-alert wire-alert--{variant} wire-catalog-feedback {class}"><strong data-show="title || label">{title || label}</strong><span data-show="value" class="wire-badge wire-badge--{variant}">{value}</span><p data-show="description">{description}</p><slot /></aside> }
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
component ApiEndpointCard {
|
||||
props {
|
||||
class = ""
|
||||
method = "GET"
|
||||
path = "/api/example"
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "View endpoint"
|
||||
}
|
||||
|
||||
view {
|
||||
<article
|
||||
class="rounded-2xl border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900 {class}"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-3" >
|
||||
<span
|
||||
class="rounded-lg px-2.5 py-1 font-mono text-xs font-bold {method === 'GET' ? 'bg-emerald-100 text-emerald-700' : method === 'POST' ? 'bg-blue-100 text-blue-700' : method === 'DELETE' ? 'bg-red-100 text-red-700' : 'bg-amber-100 text-amber-700'}"
|
||||
>
|
||||
{method}
|
||||
</span>
|
||||
<code class="text-sm font-semibold">
|
||||
{title || path}
|
||||
</code>
|
||||
</div>
|
||||
<p
|
||||
data-show="description"
|
||||
class="mt-3 text-sm text-slate-500"
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<slot />
|
||||
</div>
|
||||
{#if href}<a href="{href}" class="mt-5 inline-flex items-center gap-2 text-sm font-bold text-violet-600 dark:text-violet-300">{actionLabel}<span class="icon-[lucide--arrow-right] size-4" aria-hidden="true"></span></a>{/if}
|
||||
</article>
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
component ApiErrorExample {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view { <article class="wire-card wire-catalog-card {class}">{#if image}<img src="{image}" alt="{alt}" class="wire-catalog-card__image" />{/if}{#if eyebrow}<span class="wire-eyebrow">{eyebrow}</span>{/if}{#if title}<h3>{title}</h3>{/if}{#if description}<p>{description}</p>{/if}<slot />{#if href}<a href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a>{/if}</article> }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component ApiHeaderTable {
|
||||
props {
|
||||
title = ""
|
||||
description = ""
|
||||
empty = false
|
||||
emptyText = "No items available."
|
||||
class = ""
|
||||
}
|
||||
view { <section class="wire-catalog-collection {class}"><header data-show="title || description"><h2 data-show="title">{title}</h2><p data-show="description">{description}</p></header><div data-show="!empty" role="list" class="wire-catalog-collection__items"><slot /></div><p data-show="empty" role="status" class="wire-muted">{emptyText}</p></section> }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component ApiKeyCreateDialog {
|
||||
props {
|
||||
title = ""
|
||||
description = ""
|
||||
open = false
|
||||
closeLabel = "Close"
|
||||
class = ""
|
||||
}
|
||||
view { <div data-show="open" class="wire-overlay wire-catalog-overlay {class}"><section role="dialog" aria-modal="true" aria-label="{title}" class="wire-dialog"><header><h2 data-show="title" class="wire-dialog__title">{title}</h2><p data-show="description" class="wire-dialog__description">{description}</p></header><slot /><button type="button" class="wire-btn wire-btn--ghost">{closeLabel}</button></section></div> }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
component ApiKeyDisplay {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view { <article class="wire-card wire-catalog-card {class}">{#if image}<img src="{image}" alt="{alt}" class="wire-catalog-card__image" />{/if}{#if eyebrow}<span class="wire-eyebrow">{eyebrow}</span>{/if}{#if title}<h3>{title}</h3>{/if}{#if description}<p>{description}</p>{/if}<slot />{#if href}<a href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a>{/if}</article> }
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
component ApiKeyInput {
|
||||
props {
|
||||
label = "ApiKey"
|
||||
name = ""
|
||||
value = ""
|
||||
placeholder = ""
|
||||
type = "text"
|
||||
required = false
|
||||
disabled = false
|
||||
readonly = false
|
||||
help = ""
|
||||
error = ""
|
||||
class = ""
|
||||
}
|
||||
view { <label class="wire-field wire-catalog-field {class}"><span class="wire-label">{label}<span data-show="required" class="wire-required"> *</span></span><input name="{name}" value="{value}" placeholder="{placeholder}" type="{type}" required="{required}" disabled="{disabled}" readonly="{readonly}" aria-invalid="{error ? 'true' : 'false'}" class="wire-input" /><span data-show="help && !error" class="wire-field-message wire-field-message--help">{help}</span><span data-show="error" role="alert" class="wire-field-message wire-field-message--error">{error}</span><slot /></label> }
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
component ApiMethodBadge {
|
||||
props {
|
||||
label = "ApiMethod"
|
||||
title = ""
|
||||
description = ""
|
||||
value = ""
|
||||
variant = "default"
|
||||
class = ""
|
||||
}
|
||||
view { <aside role="status" class="wire-alert wire-alert--{variant} wire-catalog-feedback {class}"><strong data-show="title || label">{title || label}</strong><span data-show="value" class="wire-badge wire-badge--{variant}">{value}</span><p data-show="description">{description}</p><slot /></aside> }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component ApiParameterTable {
|
||||
props {
|
||||
title = ""
|
||||
description = ""
|
||||
empty = false
|
||||
emptyText = "No items available."
|
||||
class = ""
|
||||
}
|
||||
view { <section class="wire-catalog-collection {class}"><header data-show="title || description"><h2 data-show="title">{title}</h2><p data-show="description">{description}</p></header><div data-show="!empty" role="list" class="wire-catalog-collection__items"><slot /></div><p data-show="empty" role="status" class="wire-muted">{emptyText}</p></section> }
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
component ApiRateLimitNotice {
|
||||
props {
|
||||
label = "ApiRateLimit"
|
||||
title = ""
|
||||
description = ""
|
||||
value = ""
|
||||
variant = "default"
|
||||
class = ""
|
||||
}
|
||||
view { <aside role="status" class="wire-alert wire-alert--{variant} wire-catalog-feedback {class}"><strong data-show="title || label">{title || label}</strong><span data-show="value" class="wire-badge wire-badge--{variant}">{value}</span><p data-show="description">{description}</p><slot /></aside> }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
component ApiRequestExample {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view { <article class="wire-card wire-catalog-card {class}">{#if image}<img src="{image}" alt="{alt}" class="wire-catalog-card__image" />{/if}{#if eyebrow}<span class="wire-eyebrow">{eyebrow}</span>{/if}{#if title}<h3>{title}</h3>{/if}{#if description}<p>{description}</p>{/if}<slot />{#if href}<a href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a>{/if}</article> }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
component ApiResponseExample {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view { <article class="wire-card wire-catalog-card {class}">{#if image}<img src="{image}" alt="{alt}" class="wire-catalog-card__image" />{/if}{#if eyebrow}<span class="wire-eyebrow">{eyebrow}</span>{/if}{#if title}<h3>{title}</h3>{/if}{#if description}<p>{description}</p>{/if}<slot />{#if href}<a href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a>{/if}</article> }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component ApiSchemaTable {
|
||||
props {
|
||||
title = ""
|
||||
description = ""
|
||||
empty = false
|
||||
emptyText = "No items available."
|
||||
class = ""
|
||||
}
|
||||
view { <section class="wire-catalog-collection {class}"><header data-show="title || description"><h2 data-show="title">{title}</h2><p data-show="description">{description}</p></header><div data-show="!empty" role="list" class="wire-catalog-collection__items"><slot /></div><p data-show="empty" role="status" class="wire-muted">{emptyText}</p></section> }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
component ApiSchemaViewer {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
href = ""
|
||||
actionLabel = "Learn more"
|
||||
image = ""
|
||||
alt = ""
|
||||
class = ""
|
||||
}
|
||||
view { <article class="wire-card wire-catalog-card {class}">{#if image}<img src="{image}" alt="{alt}" class="wire-catalog-card__image" />{/if}{#if eyebrow}<span class="wire-eyebrow">{eyebrow}</span>{/if}{#if title}<h3>{title}</h3>{/if}{#if description}<p>{description}</p>{/if}<slot />{#if href}<a href="{href}" class="wire-btn wire-btn--ghost">{actionLabel}</a>{/if}</article> }
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
component ApiVersionBadge {
|
||||
props {
|
||||
label = "ApiVersion"
|
||||
title = ""
|
||||
description = ""
|
||||
value = ""
|
||||
variant = "default"
|
||||
class = ""
|
||||
}
|
||||
view { <aside role="status" class="wire-alert wire-alert--{variant} wire-catalog-feedback {class}"><strong data-show="title || label">{title || label}</strong><span data-show="value" class="wire-badge wire-badge--{variant}">{value}</span><p data-show="description">{description}</p><slot /></aside> }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component AppHeader {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
align = "start"
|
||||
class = ""
|
||||
}
|
||||
view { <section class="wire-catalog-section wire-text--{align} {class}"><header data-show="eyebrow || title || description"><span data-show="eyebrow" class="wire-eyebrow">{eyebrow}</span><h2 data-show="title">{title}</h2><p data-show="description" class="wire-muted">{description}</p></header><slot /></section> }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component ArchitectureSection {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
align = "start"
|
||||
class = ""
|
||||
}
|
||||
view { <section class="wire-catalog-section wire-text--{align} {class}"><header data-show="eyebrow || title || description"><span data-show="eyebrow" class="wire-eyebrow">{eyebrow}</span><h2 data-show="title">{title}</h2><p data-show="description" class="wire-muted">{description}</p></header><slot /></section> }
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
component AreaChart {
|
||||
props {
|
||||
title = ""
|
||||
value = ""
|
||||
description = ""
|
||||
summary = ""
|
||||
loading = false
|
||||
class = ""
|
||||
}
|
||||
view { <figure aria-label="{summary || title}" class="wire-card wire-catalog-visualization {class}"><figcaption><strong data-show="title">{title}</strong><span data-show="value" class="wire-metric__value">{value}</span><span data-show="description" class="wire-muted">{description}</span></figcaption><div data-show="loading" class="wire-skeleton wire-catalog-visualization__skeleton"></div><div data-show="!loading" class="wire-catalog-visualization__content"><slot /></div><p data-show="summary" class="wire-visually-hidden">{summary}</p></figure> }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component ArticleHero {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
align = "start"
|
||||
class = ""
|
||||
}
|
||||
view { <section class="wire-catalog-section wire-text--{align} {class}"><header data-show="eyebrow || title || description"><span data-show="eyebrow" class="wire-eyebrow">{eyebrow}</span><h2 data-show="title">{title}</h2><p data-show="description" class="wire-muted">{description}</p></header><slot /></section> }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component ArticleLayout {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
align = "start"
|
||||
class = ""
|
||||
}
|
||||
view { <section class="wire-catalog-section wire-text--{align} {class}"><header data-show="eyebrow || title || description"><span data-show="eyebrow" class="wire-eyebrow">{eyebrow}</span><h2 data-show="title">{title}</h2><p data-show="description" class="wire-muted">{description}</p></header><slot /></section> }
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
component ArticleNewsletterCTA {
|
||||
props {
|
||||
label = "Action"
|
||||
href = ""
|
||||
type = "button"
|
||||
variant = "primary"
|
||||
disabled = false
|
||||
class = ""
|
||||
}
|
||||
view { <span class="wire-catalog-action {class}">{#if href}<a href="{href}" class="wire-btn wire-btn--{variant}">{label}<slot /></a>{:else}<button type="{type}" disabled="{disabled}" class="wire-btn wire-btn--{variant}">{label}<slot /></button>{/if}</span> }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
component ArticlePageShell {
|
||||
props {
|
||||
eyebrow = ""
|
||||
title = ""
|
||||
description = ""
|
||||
align = "start"
|
||||
class = ""
|
||||
}
|
||||
view { <section class="wire-catalog-section wire-text--{align} {class}"><header data-show="eyebrow || title || description"><span data-show="eyebrow" class="wire-eyebrow">{eyebrow}</span><h2 data-show="title">{title}</h2><p data-show="description" class="wire-muted">{description}</p></header><slot /></section> }
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
component ArticleShareActions {
|
||||
props {
|
||||
label = "Action"
|
||||
href = ""
|
||||
type = "button"
|
||||
variant = "primary"
|
||||
disabled = false
|
||||
class = ""
|
||||
}
|
||||
view { <span class="wire-catalog-action {class}">{#if href}<a href="{href}" class="wire-btn wire-btn--{variant}">{label}<slot /></a>{:else}<button type="{type}" disabled="{disabled}" class="wire-btn wire-btn--{variant}">{label}<slot /></button>{/if}</span> }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
component AspectRatio {
|
||||
props {
|
||||
class = ""
|
||||
}
|
||||
view { <div class="wire-aspect-ratio {class}"><slot /></div> }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user