@celestia-island/hikari 0.50.0 → 0.50.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.50.0",
3
+ "version": "0.50.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari Vue 3 component library — production-grade UI components based on shittim-chest design system",
@@ -9,6 +9,7 @@ import {
9
9
  } from "vue";
10
10
 
11
11
  import { useI18n } from "../i18n/context";
12
+ import { credentialAutocomplete } from "../runtime/credentialAutofill";
12
13
 
13
14
  import { onFrame, onceFrame, type AnimationHandle } from "../runtime/animationBus";
14
15
  import { scheduleCronAfter, type CronHandle } from "../runtime/cronBus";
@@ -763,7 +764,7 @@ export default defineComponent({
763
764
  type="password"
764
765
  value={props.modelValue}
765
766
  name={props.name}
766
- autocomplete={props.autocomplete ?? "off"}
767
+ autocomplete={props.autocomplete ?? credentialAutocomplete("password", "off")}
767
768
  data-1p-ignore
768
769
  data-lpignore="true"
769
770
  disabled={props.disabled}
@@ -1,3 +1,4 @@
1
+ import { credentialAutocomplete } from "../runtime/credentialAutofill";
1
2
  import { defineComponent, ref } from "vue";
2
3
  import { useI18n } from "../i18n/context";
3
4
 
@@ -56,9 +57,11 @@ export const HkSignInCard = defineComponent({
56
57
  loading: { type: Boolean, default: false },
57
58
  /** Extra guard on top of the built-in empty-field check. */
58
59
  disabled: { type: Boolean, default: false },
59
- /** autocomplete hints, override only when the flow demands it. */
60
- usernameAutocomplete: { type: String, default: "username" },
61
- passwordAutocomplete: { type: String, default: "current-password" },
60
+ /** Autocomplete hints. Undefined (the default) resolves through
61
+ * the runtime credential policy: browser = standard tokens, Tauri
62
+ * webview = suppression (no native credential chrome). */
63
+ usernameAutocomplete: { type: String, default: undefined },
64
+ passwordAutocomplete: { type: String, default: undefined },
62
65
  /** Submit label; defaults to the hikari::signIn.submit locale. */
63
66
  submitLabel: { type: String, default: undefined },
64
67
  /** Username-field type; switch to "email" for identifier logins. */
@@ -100,7 +103,7 @@ export const HkSignInCard = defineComponent({
100
103
  onUpdate:modelValue={(v: string) => (username.value = v)}
101
104
  type={props.usernameType}
102
105
  name="signin-username"
103
- autocomplete={props.usernameAutocomplete}
106
+ autocomplete={props.usernameAutocomplete ?? credentialAutocomplete("username", "username")}
104
107
  placeholder={
105
108
  props.usernamePlaceholder ??
106
109
  t("hikari::signIn.usernamePlaceholder", "Username")
@@ -136,7 +139,7 @@ export const HkSignInCard = defineComponent({
136
139
  modelValue={password.value}
137
140
  onUpdate:modelValue={(v: string) => (password.value = v)}
138
141
  name="signin-password"
139
- autocomplete={props.passwordAutocomplete}
142
+ autocomplete={props.passwordAutocomplete ?? credentialAutocomplete("password", "current-password")}
140
143
  placeholder={t("hikari::signIn.passwordPlaceholder", "Password")}
141
144
  disabled={props.loading || props.disabled}
142
145
  submitOnEnter={attemptSubmit}
@@ -135,7 +135,6 @@ export default defineComponent({
135
135
  <Transition
136
136
  name={dir === "back" ? "hk-stepflow-back" : "hk-stepflow-fwd"}
137
137
  mode="out-in"
138
- appear
139
138
  >
140
139
  <div key={props.modelValue} class="hk-stepflow-body">
141
140
  {slotFn?.({ key: props.modelValue, index, direction: dir })}
@@ -0,0 +1,90 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { createApp, h } from "vue";
3
+
4
+ import { credentialAutocomplete } from "./credentialAutofill";
5
+ import HkPasswordInput from "../components/HkPasswordInput";
6
+ import { HkSignInCard } from "../components/HkSignInCard";
7
+
8
+ const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
9
+
10
+ function mount(node: ReturnType<typeof h>) {
11
+ const container = document.createElement("div");
12
+ document.body.appendChild(container);
13
+ const app = createApp({ render: () => node });
14
+ app.mount(container);
15
+ mounts.push({ app, container });
16
+ return container;
17
+ }
18
+
19
+ afterEach(() => {
20
+ for (const { app, container } of mounts.splice(0)) {
21
+ app.unmount();
22
+ container.remove();
23
+ }
24
+ delete (window as unknown as Record<string, unknown>).__TAURI_INTERNALS__;
25
+ });
26
+
27
+ function setTauri(on: boolean) {
28
+ const w = window as unknown as Record<string, unknown>;
29
+ if (on) w.__TAURI_INTERNALS__ = { invoke: () => {} };
30
+ else delete w.__TAURI_INTERNALS__;
31
+ }
32
+
33
+ describe("credentialAutocomplete policy", () => {
34
+ it("keeps the browser defaults outside a Tauri webview", () => {
35
+ setTauri(false);
36
+ expect(credentialAutocomplete("username", "username")).toBe("username");
37
+ expect(credentialAutocomplete("password", "current-password")).toBe("current-password");
38
+ expect(credentialAutocomplete("password", "off")).toBe("off");
39
+ });
40
+
41
+ it("degrades to suppression tokens inside a Tauri webview", () => {
42
+ setTauri(true);
43
+ expect(credentialAutocomplete("username", "username")).toBe("off");
44
+ // new-password is the one token the Chromium family respects for
45
+ // suppressing saved-credential fill; plain "off" is ignored on login forms.
46
+ expect(credentialAutocomplete("password", "current-password")).toBe("new-password");
47
+ expect(credentialAutocomplete("password", "off")).toBe("new-password");
48
+ });
49
+ });
50
+
51
+ describe("HkSignInCard credential fields", () => {
52
+ it("renders standard tokens in the browser", () => {
53
+ setTauri(false);
54
+ const c = mount(h(HkSignInCard, { title: "t", onSubmit: () => {} }));
55
+ const inputs = Array.from(c.querySelectorAll("input"));
56
+ const username = inputs[0] as HTMLInputElement;
57
+ const password = inputs.find((i) => i.type === "password") as HTMLInputElement;
58
+ expect(username.autocomplete).toBe("username");
59
+ expect(password.autocomplete).toBe("current-password");
60
+ });
61
+
62
+ it("renders suppression tokens inside a Tauri webview", () => {
63
+ setTauri(true);
64
+ const c = mount(h(HkSignInCard, { title: "t", onSubmit: () => {} }));
65
+ const inputs = Array.from(c.querySelectorAll("input"));
66
+ const username = inputs[0] as HTMLInputElement;
67
+ const password = inputs.find((i) => i.type === "password") as HTMLInputElement;
68
+ expect(username.autocomplete).toBe("off");
69
+ expect(password.autocomplete).toBe("new-password");
70
+ });
71
+
72
+ it("lets an explicit prop override the policy in Tauri too", () => {
73
+ setTauri(true);
74
+ const c = mount(h(HkSignInCard, { title: "t", onSubmit: () => {}, usernameAutocomplete: "email" }));
75
+ const username = c.querySelector("input") as HTMLInputElement;
76
+ expect(username.autocomplete).toBe("email");
77
+ });
78
+ });
79
+
80
+ describe("HkPasswordInput credential field", () => {
81
+ it("keeps the off default in the browser and degrades in Tauri", () => {
82
+ setTauri(false);
83
+ const browser = mount(h(HkPasswordInput, { modelValue: "" }));
84
+ expect((browser.querySelector(".hk-pwd-input") as HTMLInputElement).autocomplete).toBe("off");
85
+
86
+ setTauri(true);
87
+ const tauri = mount(h(HkPasswordInput, { modelValue: "" }));
88
+ expect((tauri.querySelector(".hk-pwd-input") as HTMLInputElement).autocomplete).toBe("new-password");
89
+ });
90
+ });
@@ -0,0 +1,38 @@
1
+ import { isTauri } from "./env";
2
+
3
+ /**
4
+ * Credential-field autocomplete policy (user direction 2026-09-14): no
5
+ * Tauri app in the group may surface the webview's native credential
6
+ * machinery — the stored-password dropdown or the save-password bubble.
7
+ *
8
+ * The policy is *runtime Tauri-aware*, not absolute: the same hikari app
9
+ * runs in plain browsers too, where password managers are welcome and the
10
+ * standard tokens (`username` / `current-password`) are the correct UX.
11
+ *
12
+ * Inside a Tauri webview we degrade to the strongest suppression tokens:
13
+ *
14
+ * - password: `new-password` — the one autocomplete token Chromium-family
15
+ * engines (WebView2 included) respect for suppressing saved-credential
16
+ * fill; plain `off` is explicitly ignored by them for login forms.
17
+ * - username: `off` — WebView2's fill store is additionally emptied by the
18
+ * native hardening layer each app ships (`webview_hardening.rs` flips
19
+ * `ICoreWebView2Settings4.IsPasswordAutosaveEnabled` and
20
+ * `IsGeneralAutofillEnabled` off), so there is nothing to offer; the
21
+ * token here keeps the DOM contract aligned on engines without that
22
+ * native layer.
23
+ *
24
+ * Extensions such as 1Password/LastPass are signaled separately by the
25
+ * components (`data-1p-ignore` / `data-lpignore`).
26
+ *
27
+ * @param kind which credential field the token is for.
28
+ * @param browserDefault the token this component uses in a plain browser.
29
+ */
30
+ export function credentialAutocomplete(
31
+ kind: "username" | "password",
32
+ browserDefault: string,
33
+ ): string {
34
+ if (isTauri()) {
35
+ return kind === "password" ? "new-password" : "off";
36
+ }
37
+ return browserDefault;
38
+ }
@@ -1,3 +1,4 @@
1
+ export { credentialAutocomplete } from "./credentialAutofill";
1
2
  export * from "./animationBus";
2
3
  export * from "./cronBus";
3
4
  export * from "./intervalBus";