fix(ui): readable date-time trigger, showcase code panel, release ui 0.8.88

- DatePicker trigger shows the value formatted in the component locale
  ("Oct 2, 2026 · 9:30 AM", "Jul 15, 2026"); the hidden input still submits
  the ISO value.
- Showcase playground code panel: the reactive runtime read "{...}" in the
  rewritten code as interpolations and blanked every object in array props.
  Braces are now written as their own spans, like the server-rendered code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClintchizandClaude Opus 5.5 committed 2026-09-30 16:23:13 +05:30
1 parent 1bed8e4b74
commit 9030e7d3a3
5 files changed
+42 -6

No files matched your search

+1 -1
View File
@@ -749,7 +749,7 @@
},
"packages/ui": {
"name": "@wrnexus/ui",
"version": "0.8.87",
"version": "0.8.88",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
@@ -169,11 +169,33 @@
const mount = publicTag ? [] : [`data-component="${sourceAttribute(component)}"`];
const lines = [...mount, ...attributes].map((attribute) => ` ${attribute}`).join("\n");
const opening = lines ? `<${tag}\n${lines}` : `<${tag}`;
code.textContent = hasSlot
let source = hasSlot
? `${opening}>\n <!-- Add slot content here -->\n</${tag}>`
: `${opening}\n/>`;
if (declarations.length) code.textContent = `page ${component.replace(/[^A-Za-z0-9]/g, "")}Example {\n${declarations.join("\n")}\n view {\n${code.textContent}\n }\n}`;
if (declarations.length) source = `page ${component.replace(/[^A-Za-z0-9]/g, "")}Example {\n${declarations.join("\n")}\n view {\n${source}\n }\n}`;
writeCode(code, source);
}
/*
* The reactive runtime treats "{expr}" inside a text node as an
* interpolation, so a sample like { "label": "Mon" } written with
* textContent was evaluated and rendered as an empty string. Putting each
* brace in its own <span> means no single text node ever holds a {...}
* pair -- the same markup the generator uses for the server-rendered code.
*/
function writeCode(code, source) {
const fragment = document.createDocumentFragment();
for (const part of source.split(/([{}])/)) {
if (!part) continue;
if (part === "{" || part === "}") {
const brace = document.createElement("span");
brace.textContent = part;
fragment.append(brace);
} else {
fragment.append(document.createTextNode(part));
}
}
code.replaceChildren(fragment);
}
function valuesFrom(form) {
+15 -1
View File
@@ -52,6 +52,20 @@ size: string = "default"
output.change({ value: currentValue, name: name, sourceEvent: sourceEvent })
}
// Human-readable trigger text in the component locale. The hidden input
// keeps the ISO value, so forms still submit 2026-10-02 / 2026-10-02T09:30.
shared function displayValue(parts, label, time, clock) {
if (!currentValue) { return placeholder || "Select date" }
parts = currentValue.slice(0, 10).split("-")
if (parts.length !== 3 || !Number(parts[0]) || !Number(parts[1]) || !Number(parts[2])) { return currentValue }
label = Intl.DateTimeFormat(locale, { dateStyle: "medium", timeZone: "UTC" }).format(Date.UTC(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2])))
if (currentValue.charAt(10) === "T") {
time = currentValue.slice(11, 16).split(":")
clock = Intl.DateTimeFormat(locale, { timeStyle: "short", timeZone: "UTC" }).format(Date.UTC(1970, 0, 1, Number(time[0]) || 0, Number(time[1]) || 0))
return label + " · " + clock
}
return label
}
shared function calendarYears() {
if (years.includes(Number(selectedYear))) { return years }
return years.concat(Number(selectedYear))
@@ -142,7 +156,7 @@ size: string = "default"
view {
<div {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--field wrn-next--date-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}" data-expanded="{expanded}" @keydown="calendarKeydown(event)">
<div class="wrn-next__field-heading"><label for="{id || name}">{label}</label>{#if cornerHint}<span class="wrn-next__field-hint">{cornerHint}</span>{/if}</div>
<div class="wrn-next__picker-control"><input id="{id || name}" class="wrn-next__sr-only wrn-next__picker-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" /><button type="button" class="wrn-next__date-trigger" aria-label="{label}" disabled="{disabled || readonly}" aria-haspopup="dialog" aria-expanded="{expanded}" @click="openCalendar(event)" @focus="handleFocus(event)" @blur="handleBlur(event)"><span>{currentValue || placeholder || "Select date"}</span><i class="icon-[lucide--calendar-days]" aria-hidden="true"></i></button>{#if currentValue && !readonly && !disabled}<button type="button" class="wrn-next__picker-clear" aria-label="Clear date" @click="clearDate(event)">×</button>{/if}</div>
<div class="wrn-next__picker-control"><input id="{id || name}" class="wrn-next__sr-only wrn-next__picker-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" /><button type="button" class="wrn-next__date-trigger" aria-label="{label}" disabled="{disabled || readonly}" aria-haspopup="dialog" aria-expanded="{expanded}" @click="openCalendar(event)" @focus="handleFocus(event)" @blur="handleBlur(event)"><span>{displayValue()}</span><i class="icon-[lucide--calendar-days]" aria-hidden="true"></i></button>{#if currentValue && !readonly && !disabled}<button type="button" class="wrn-next__picker-clear" aria-label="Clear date" @click="clearDate(event)">×</button>{/if}</div>
<button type="button" class="wrn-picker-dismiss" aria-label="Close calendar" data-show="expanded" @click="finishDate(event)"></button><div data-show="expanded" class="wrn-next__calendar" role="dialog" aria-label="{label}"><div class="wrn-next__date-selectors"><label>Month<select value="{selectedMonth}" @change="updateDate('month', event.currentTarget.value, event)">{#each months as month}<option value="{month.value}" selected="{month.value === selectedMonth}">{month.label}</option>{/each}</select></label><label>Year<select value="{selectedYear}" @change="updateDate('year', event.currentTarget.value, event)">{#each calendarYears() as calendarYear}<option value="{calendarYear}" selected="{String(calendarYear) === selectedYear}" data-text="calendarYear"></option>{/each}</select></label></div><div class="wrn-next__calendar-weekdays" aria-hidden="true">{#each weekdays() as weekday}<span>{weekday}</span>{/each}</div><div class="wrn-next__calendar-grid">{#each calendarDays() as day}<button type="button" data-value="{day.value}" data-today="{day.today}" style="grid-column: {day.column}" disabled="{day.disabled}" aria-label="{day.label}" aria-pressed="{day.value === currentValue.slice(0, 10)}" @click="selectDay(event)">{day.day}</button>{/each}</div>{#if type === "datetime-local"}<TimePicker label="Time" value="{selectedTime}" @change="selectedTime = payload.value; currentValue = (currentValue ? currentValue.slice(0, 10) : selectedYear + '-' + selectedMonth + '-01') + 'T' + selectedTime; output.change({ value: currentValue, name: name })" />{/if}<button type="button" class="wrn-next__date-done" disabled="{!currentValue}" @click="finishDate(event)">Done</button></div>
{#if helperText}<small class="wrn-next__field-help">{helperText}</small>{/if}<small class="wrn-next__field-error" data-error="{name}">{error}</small>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ui",
"version": "0.8.87",
"version": "0.8.88",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1687,7 +1687,7 @@ test("date picker renders a custom calendar trigger with accessible field state
)[14] as HTMLButtonElement;
fifteenth.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true }));
expect(input.value).toBe("2026-07-15");
expect(dom.querySelector(".wrn-next__date-trigger span")?.textContent).toBe("2026-07-15");
expect(dom.querySelector(".wrn-next__date-trigger span")?.textContent).toBe("Jul 15, 2026");
});
test("time picker uses a custom reactive panel instead of the browser time control", async () => {