From b1064d442a3762a91afe58f9231bf236b32af0e6 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Thu, 27 Aug 2026 22:53:14 +0530 Subject: [PATCH] fix(ui): avoid icon spacing for plain inputs --- bun.lock | 2 +- packages/ui/components/input.wrn | 40 ++++++++++++++++++++++++++++---- packages/ui/package.json | 2 +- packages/ui/test/ui.test.ts | 37 +++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/bun.lock b/bun.lock index 2960d3f5..029d0baf 100644 --- a/bun.lock +++ b/bun.lock @@ -688,7 +688,7 @@ }, "packages/ui": { "name": "@wrnexus/ui", - "version": "0.8.66", + "version": "0.8.70", "dependencies": { "@wrnexus/core": "workspace:*", }, diff --git a/packages/ui/components/input.wrn b/packages/ui/components/input.wrn index b02f2803..5d95a609 100644 --- a/packages/ui/components/input.wrn +++ b/packages/ui/components/input.wrn @@ -78,6 +78,7 @@ component Input { data-inline="{inline}" data-floating="{variant === 'floating'}" data-invalid="{error || validationError ? 'true' : 'false'}" + data-input-type="{type}" >
- {#if icon} + {#if icon || type === "date" || type === "datetime-local" || type === "time" || type === "month" || type === "week"} @@ -154,6 +156,7 @@ component Input { id="{(id || name) + '-error'}" class="wrn-next__field-error" data-error="{name}" + data-empty="{error || validationError ? 'false' : 'true'}" aria-live="polite" > {error || validationError} @@ -308,6 +311,12 @@ component Input { font-size: 0.8125rem; font-weight: 500; } + /* Input is a block-level control. Shared field sizing must not cap it when + it participates in a grid or flex layout owned by the consumer. */ + .wrn-next--field.wrn-next--input { + width: 100%; + max-width: none; + } .wrn-next--field.wrn-next--size-sm, .wrn-next--choice-field.wrn-next--size-sm { font-size: 0.8125rem; @@ -346,10 +355,29 @@ component Input { background: var(--wrn-color-surface); font-size: 0.8125rem; } - .wrn-next__field-control[data-icon-position="start"] > :is(input, textarea, select) { + .wrn-next__field-control[data-has-icon="true"][data-icon-position="start"] > :is(input, textarea, select) { padding-inline-start: 2.5rem; } - .wrn-next__field-control[data-icon-position="end"] > :is(input, textarea, select) { + .wrn-next--input[data-input-type="date"] .wrn-next__field-control, + .wrn-next--input[data-input-type="datetime-local"] .wrn-next__field-control, + .wrn-next--input[data-input-type="time"] .wrn-next__field-control, + .wrn-next--input[data-input-type="month"] .wrn-next__field-control, + .wrn-next--input[data-input-type="week"] .wrn-next__field-control { + color-scheme: light dark; + } + .wrn-next--input :is(input[type="date"], input[type="datetime-local"], input[type="time"], input[type="month"], input[type="week"]) { + min-width: 0; + font-variant-numeric: tabular-nums; + } + .wrn-next--input :is(input[type="date"], input[type="datetime-local"], input[type="time"], input[type="month"], input[type="week"])::-webkit-calendar-picker-indicator { + width: 1.1rem; + height: 1.1rem; + margin: 0; + padding: 0; + cursor: pointer; + opacity: 0.72; + } + .wrn-next__field-control[data-has-icon="true"][data-icon-position="end"] > :is(input, textarea, select) { padding-inline-end: 2.5rem; } .wrn-next__field-icon { @@ -433,6 +461,10 @@ component Input { .wrn-next__field-error:empty { display: none; } + .wrn-next__field-error[data-empty="true"] { + display: none; + min-height: 0; + } .wrn-next--field[data-invalid="true"] :is(input, textarea, select), .wrn-next--choice-field[data-invalid="true"] input { border-color: var(--wrn-color-danger); diff --git a/packages/ui/package.json b/packages/ui/package.json index 7ae7b0ee..b7cdb56c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.8.66", + "version": "0.8.70", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/test/ui.test.ts b/packages/ui/test/ui.test.ts index f94caeaf..890b0925 100644 --- a/packages/ui/test/ui.test.ts +++ b/packages/ui/test/ui.test.ts @@ -1294,6 +1294,10 @@ test("basic form fields expose shared labels, variants, states, hints, values, a expect(validation.error).toBe("Enter a valid email address."); const source = readFileSync(uiComponentPath("Input"), "utf8"); + const sharedFieldRule = source.lastIndexOf(".wrn-next--field,"); + const inputWidthRule = source.lastIndexOf(".wrn-next--field.wrn-next--input"); + expect(inputWidthRule).toBeGreaterThan(sharedFieldRule); + expect(source.slice(inputWidthRule, inputWidthRule + 140)).toContain("max-width: none"); const dom = mountHtml( await renderComponent(source, { id: "account-email", @@ -1317,6 +1321,7 @@ test("basic form fields expose shared labels, variants, states, hints, values, a expect(root.dataset.variant).toBe("floating"); expect(root.dataset.inline).toBe("true"); expect(root.dataset.invalid).toBe("true"); + expect(dom.querySelector(".wrn-next__field-error")?.getAttribute("data-empty")).toBe("false"); expect(input.id).toBe("account-email"); expect(input.name).toBe("email"); expect(input.value).toBe("not-an-email"); @@ -1344,6 +1349,38 @@ test("basic form fields expose shared labels, variants, states, hints, values, a expect(emitted).toEqual(["input", "change", "focus", "blur", "keydown", "keyup"]); }); +test("Input does not reserve validation space while its error is empty", async () => { + const source = readFileSync(uiComponentPath("Input"), "utf8"); + const dom = mountHtml(await renderComponent(source, { name: "search", hiddenLabel: true })); + expect(dom.querySelector(".wrn-next__field-error")?.getAttribute("data-empty")).toBe("true"); + expect(source).toContain('.wrn-next__field-error[data-empty="true"]'); +}); + +test("Input only reserves icon space when it renders an icon", async () => { + const source = readFileSync(uiComponentPath("Input"), "utf8"); + const plain = mountHtml( + await renderComponent(source, { name: "limit", label: "Limit", placeholder: "Enter limit" }), + ); + const decorated = mountHtml( + await renderComponent(source, { + name: "email", + label: "Email", + icon: "icon-[lucide--mail]", + }), + ); + + expect(plain.querySelector(".wrn-next__field-control")?.getAttribute("data-has-icon")).toBe( + "false", + ); + expect(plain.querySelector(".wrn-next__field-icon")).toBeNull(); + expect(decorated.querySelector(".wrn-next__field-control")?.getAttribute("data-has-icon")).toBe( + "true", + ); + expect(decorated.querySelector(".wrn-next__field-icon")).not.toBeNull(); + expect(source).toContain('[data-has-icon="true"][data-icon-position="start"]'); + expect(source).toContain('[data-has-icon="true"][data-icon-position="end"]'); +}); + test("basic fields suppress native popups and render package constraint messages", async () => { const inputSource = readFileSync(uiComponentPath("Input"), "utf8"); const dom = mountHtml(